@autoremesher/wasm
v0.2.0
Published
Automatic quad remeshing (AutoRemesher) compiled to WebAssembly, with a TypeScript-first API for Three.js, glTF, OBJ, and raw buffers.
Maintainers
Readme
@autoremesher/wasm
Automatic quad remeshing in the browser and Node.js — powered by WebAssembly.
This project ports AutoRemesher (Jeremy Hu) to WebAssembly and wraps it in a small TypeScript-first API. Give it a triangle mesh; get back a quad-dominant mesh suitable for subdivision, look-dev, and further modeling.
npm install @autoremesher/wasmimport { remesh } from "@autoremesher/wasm";
const result = await remesh(
{ vertices, indices }, // Float32Array xyz + Uint32Array triangles
{ targetQuads: 2000 }
);
// result.quads — 4 indices per face (true quads + rare tris)
// result.indices — triangulated for GPU / Three.js
// result.vertices — remeshed positions
// result.quality — "remeshed" | "near-sealed"
// result.watertightStatus:
0.2.x— open-source beta. Works well on clean, closed organic meshes. Hard-surface, scans, and multi-part exports are improving; expect retries or errors on pathological topology. Use a Web Worker in the browser so the UI stays responsive.
Features
- Quad-dominant remesh — native AutoRemesher / MIQ pipeline in WASM
- TypeScript-first — full
.d.tstypes, ESM + CommonJS - Flexible inputs — raw buffers, OBJ text, GLB / glTF, or a Three.js
BufferGeometry-like object - Self-contained — ~1.1 MB single-thread WASM (+ optional pthreads build), no native install
- Browser + Node — same API; Node example included
- UV re-projection — closest-point transfer when the input has UVs
- Quality gates — topology checks, density scoring, safe hole fill, explicit
quality/watertighton the result - Optional threads — pthreads binary for multi-island speedups (needs COOP/COEP in browsers)
Repository layout
This repository is the full open-source package:
.
├── src/ TypeScript API (source of truth)
├── lib/ Built ESM + CJS + types (published)
├── wasm/ Prebuilt .mjs + .wasm (st + mt)
├── emscripten/ Bindings, shims, build scripts
├── cpp/ Vendored AutoRemesher + third-party C++
├── test/ Node test suite
├── examples/
│ ├── node-remesh.mjs CLI sample
│ └── web/ Browser playground (Vite + React + Three)
├── package.json @autoremesher/wasm
└── README.mdPrebuilt lib/ and wasm/ are committed so consumers and CI can install without the Emscripten SDK. Rebuild with npm run build when you change C++ or TypeScript.
Installation
npm install @autoremesher/wasm| Package | Role |
| --- | --- |
| @autoremesher/wasm | Core remesher (this package) |
| meshoptimizer | Runtime dependency (input decimation helpers) |
| three | Optional peer — only if you use @autoremesher/wasm/three |
Quick start
Raw buffers
import { remesh } from "@autoremesher/wasm";
const result = await remesh(
{
vertices: /* Float32Array length % 3 === 0 */,
indices: /* Uint32Array length % 3 === 0 */,
},
{
targetQuads: 2000,
adaptivity: 0.5,
onProgress: (p, status) => console.log(Math.round(p * 100), status),
}
);
console.log(result.quadCount, result.quality, result.watertight);Three.js
import { remesh } from "@autoremesher/wasm";
import { fromBufferGeometry, toBufferGeometry } from "@autoremesher/wasm/three";
const result = await remesh(fromBufferGeometry(sourceGeometry), {
targetQuads: 4000,
adaptivity: 0.8,
});
mesh.geometry = toBufferGeometry(result);You can also pass a BufferGeometry (or any object with the same shape) straight to remesh(). The core package never imports three.
OBJ
import { remesh, resultToObj } from "@autoremesher/wasm";
const objText = await fetch("model.obj").then((r) => r.text());
const result = await remesh(objText, { targetQuads: 3000 });
const out = resultToObj(result); // native quad faces in OBJGLB / glTF
import { remesh } from "@autoremesher/wasm";
const glb = await fetch("model.glb").then((r) => r.arrayBuffer());
const result = await remesh(glb, { targetQuads: 5000 });Triangle primitives are merged before remeshing. External .bin URIs are not fetched — use GLB or embedded / data-URI buffers.
Node.js
import { remesh, resultToObj } from "@autoremesher/wasm";
import { readFile, writeFile } from "node:fs/promises";
const result = await remesh(await readFile("input.obj", "utf8"), {
targetQuads: 2000,
onProgress: (p, status) => console.log(`${(p * 100) | 0}% ${status}`),
});
await writeFile("output.obj", resultToObj(result));Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| targetQuads | number | ~1000 if unset | Approximate output face count. |
| targetTriangleCount | number | — | Raw engine density target; overrides targetQuads. |
| edgeScaling | number | 1.0 | Edge scale (CLI-style). Larger → bigger / fewer quads. Useful range ~1.0–2.0. |
| sharpEdgeThreshold | number | 90 | Dihedral angle (°) treated as a sharp feature. |
| adaptivity | number | 0.5–1.0 in practice | Curvature adaptivity in [0, 1]. |
| smoothNormals | boolean | true | Fill result.normals. |
| smoothNormalDegrees | number | 0 | Resampling normal smooth; 0 disables. |
| modelType | "organic" \| "hardSurface" | "organic" | Matches the desktop app switch. |
| preserveSharpFeatures | boolean | false | Shortcut for hardSurface. |
| preserveUVs | boolean | true if input has UVs | Closest-point UV transfer → result.uvs. |
| allowHoles | boolean | false | Allow open shells. When false, the pipeline tries to seal; large residual holes throw. |
| maxParts | number | 1 | How many connected shells to keep (largest first). |
| minPartTriangles | number | 32 | Drop smaller islands before remesh. |
| maxInputTriangles | number | auto | Soft cap; dense meshes are decimated with meshoptimizer before the native solve. |
| onProgress | (progress, status) => void | — | progress in [0, 1]. |
| moduleOptions | ModuleLoadOptions | — | WASM URL / binary / threads (first load per variant). |
Result shape
| Field | Description |
| --- | --- |
| vertices | Float32Array positions (xyz) |
| quads | Uint32Array, 4 indices per face. A triangle is encoded as i2 === i3. |
| indices | Triangulated index buffer for rendering |
| normals | Optional smooth vertex normals |
| uvs | Optional re-projected UVs |
| quadCount | quads.length / 4 |
| watertight | true if no boundary edges remain |
| quality | "remeshed" (closed / ok) or "near-sealed" (native remesh with a small residual boundary) |
| processingTimeMs | Wall time for the remesh |
Prefer checking quality / watertight before exporting production assets.
Web Worker (recommended in browsers)
remesh() runs heavy WASM on the calling thread. Offload it:
// remesh.worker.ts
import { remesh } from "@autoremesher/wasm";
self.onmessage = async (event) => {
const { id, vertices, indices, options } = event.data;
try {
const result = await remesh(
{ vertices, indices },
{
...options,
moduleOptions: { threads: false }, // deterministic single-thread path
onProgress: (progress, status) =>
self.postMessage({ id, type: "progress", progress, status }),
}
);
const transfer = [
result.vertices.buffer,
result.indices.buffer,
result.quads.buffer,
];
if (result.normals) transfer.push(result.normals.buffer);
self.postMessage({ id, type: "done", result }, { transfer });
} catch (error) {
self.postMessage({
id,
type: "error",
message: error instanceof Error ? error.message : String(error),
});
}
};Bundlers and WASM loading
The binary ships next to its glue:
wasm/autoremesher.mjs+wasm/autoremesher.wasm(single-thread)wasm/autoremesher-mt.mjs+wasm/autoremesher-mt.wasm(pthreads)
Vite / webpack 5 usually resolve new URL(..., import.meta.url) inside the package. If assets are relocated:
import wasmUrl from "@autoremesher/wasm/wasm/autoremesher.wasm?url"; // Vite
await remesh(input, {
targetQuads: 2000,
moduleOptions: { wasmUrl },
});moduleOptions also accepts wasmBinary, Emscripten locateFile, and print / printErr.
Warm the module early:
import { loadAutoRemesherModule } from "@autoremesher/wasm";
await loadAutoRemesherModule();Multithreading
await remesh(input, {
moduleOptions: { threads: "auto" }, // or true / false
});"auto"— pthreads whenSharedArrayBufferis available (browsers need COOP/COEP).- Use
threadsSupported()to query the environment. - Gains are mainly on multi-island meshes; single solids see less benefit.
- For a simple product path, prefer
threads: false.
UV / attribute transfer
When the source has UVs, remesh() fills result.uvs by default. For arbitrary attributes:
import { transferAttribute } from "@autoremesher/wasm";
const colors = transferAttribute(
{ vertices: srcPositions, indices: srcTriangles },
srcColors,
3,
result.vertices
);Closest-point sampling can smear UV islands at seams.
Error handling
Failures reject with AutoRemesherError and a numeric code:
import { remesh, AutoRemesherError } from "@autoremesher/wasm";
try {
await remesh(input, { targetQuads: 2000 });
} catch (error) {
if (error instanceof AutoRemesherError) {
console.error(error.code, error.message);
}
}Common cases: empty / invalid topology (-101, -2), collapsed or unusable solve (-6), residual holes when allowHoles is false (-7).
Tips
- Prefer closed manifold meshes; weld UV seams before remesh when possible.
- Start around
targetQuads500–3000 for previews; raise carefully for density. - Very coarse targets on thin shapes can collapse — raise density or adjust
edgeScaling. - Peak memory tracks the intermediate resample; terminate workers between large jobs.
- Multi-object exports: keep
maxParts: 1(default) unless you need several shells.
Development
Prerequisites
- Node.js ≥ 18
- For WASM rebuilds: CMake ≥ 3.16 and the Emscripten SDK
- TypeScript-only changes need only Node
Scripts
npm install
npm run build:ts # tsc → lib/ (ESM + CJS + types)
npm test # node --test test/*.test.mjs
npm run build # full: Emscripten WASM + TypeScriptFull native rebuild:
# Windows: use a bash-capable shell (Git Bash, WSL, etc.)
source /path/to/emsdk/emsdk_env.sh
npm run build:wasm
npm run build:ts
npm testThe Emscripten tree compiles AutoRemesher + a geogram 1.8.3 subset with Qt/TBB shims under emscripten/.
Tests
npm testCoverage includes remesh on closed primitives, UV transfer, format parsers, quality / hole policy, and CJS load.
Publishing to npm
npm login
npm whoami # must be a member of the @autoremesher org
npm pack --dry-run # inspect tarball: lib/, wasm/, README, LICENSE
npm publish # publishConfig.access = publicOnly paths listed in package.json → "files" are published (lib, wasm/*, README, LICENSE).
Contributing
Contributions are welcome.
- Fork and clone this repository.
npm install&&npm test.- Prefer small, focused PRs (API, quality gates, docs, or WASM build).
- Keep
npm testgreen; add a test when you fix a failure mode. - Do not commit local assets (
.stldumps,test-out-*, debug logs).
Please file issues with a short mesh description (closed/open, tri count, options) and, when possible, a minimal OBJ/GLB.
Browser demo (included)
Full-viewport playground under examples/web: drag-and-drop mesh, pre-decimate large models, remesh in a Web Worker, inspect original vs quads, download OBJ.
# from the package root (lib/ is committed; rebuild with npm run build:ts if you change src/)
npm run demo:install
npm run demo
# → http://localhost:5173Or:
cd examples/web
npm install
npm run devThe demo depends on this package via "@autoremesher/wasm": "file:../..". It is not part of the npm tarball (files only ships lib, wasm, README, LICENSE) — clone the repo to run it.
Roadmap
- [x] Single-thread WASM remesh + TypeScript API
- [x] pthreads build behind COOP/COEP
- [x] UV re-projection
- [x] Topology / density quality gates +
quality/watertight - [x] meshoptimizer pre-decimation for large inputs
- [x] Browser demo playground (
examples/web) - [ ] Better hard-surface / crease preservation
- [ ] Feature curves / guide constraints (needs upstream engine support)
- [ ] Smaller binary / streaming progress from native
License
MIT © Sarah Yung — see LICENSE.
This package redistributes WebAssembly builds of third-party software. See LICENSE for full notices. Upstream projects include:
| Project | License | Link | | --- | --- | --- | | AutoRemesher | MIT | huxingyi/autoremesher | | geogram | BSD-3-Clause | BrunoLevy/geogram | | Eigen | MPL-2.0 | eigen.tuxfamily.org | | isotropicremesher | MIT | (vendored with AutoRemesher) | | meshoptimizer | MIT | zeux/meshoptimizer |
Acknowledgments
- Jeremy Hu — AutoRemesher
- Geogram / OpenNL authors — parameterization and geometry kernels
- Everyone who files issues and shares hard meshes
npm: @autoremesher/wasm
Issues: use this GitHub repository’s issue tracker after the project is published under the @autoremesher organization.
