Merge pull request #738 from JolifantoBambla/clusterizer-wasm

Add JS bindings for clusterizer API
This commit is contained in:
Arseny Kapoulkine
2024-08-21 15:17:21 -07:00
committed by GitHub
11 changed files with 941 additions and 6 deletions
+5 -2
View File
@@ -68,11 +68,13 @@ jobs:
run: node js/meshopt_encoder.test.js
- name: test simplifier
run: node js/meshopt_simplifier.test.js
- name: test clusterizer
run: node js/meshopt_clusterizer.test.js
- name: check es5
run: |
npm install -g es-check
npx es-check es5 js/meshopt_decoder.js js/meshopt_encoder.js js/meshopt_simplifier.js
npx es-check --module es5 js/meshopt_decoder.module.js js/meshopt_encoder.module.js js/meshopt_simplifier.module.js
npx es-check es5 js/meshopt_decoder.js js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js
npx es-check --module es5 js/meshopt_decoder.module.js js/meshopt_encoder.module.js js/meshopt_simplifier.module.js js/meshopt_clusterizer.module.js
npx es-check es5 gltf/library.js
gltfpack:
@@ -125,6 +127,7 @@ jobs:
node js/meshopt_decoder.test.js
node js/meshopt_encoder.test.js
node js/meshopt_simplifier.test.js
node js/meshopt_clusterizer.test.js
gltfpack-basis:
runs-on: ubuntu-latest
+14 -2
View File
@@ -42,7 +42,7 @@ WASM_FLAGS=--target=wasm32-wasi --sysroot=$(WASIROOT)
WASM_FLAGS+=-O3 -DNDEBUG -nostartfiles -nostdlib -Wl,--no-entry -Wl,-s
WASM_FLAGS+=-mcpu=mvp # make sure clang doesn't use post-MVP features like sign extension
WASM_FLAGS+=-fno-slp-vectorize -fno-vectorize -fno-unroll-loops
WASM_FLAGS+=-Wl,-z -Wl,stack-size=24576 -Wl,--initial-memory=65536
WASM_FLAGS+=-Wl,-z -Wl,stack-size=36864 -Wl,--initial-memory=65536
WASM_EXPORT_PREFIX=-Wl,--export
WASM_DECODER_SOURCES=src/vertexcodec.cpp src/indexcodec.cpp src/vertexfilter.cpp tools/wasmstubs.cpp
@@ -54,6 +54,9 @@ WASM_ENCODER_EXPORTS=meshopt_encodeVertexBuffer meshopt_encodeVertexBufferBound
WASM_SIMPLIFIER_SOURCES=src/simplifier.cpp src/vfetchoptimizer.cpp tools/wasmstubs.cpp
WASM_SIMPLIFIER_EXPORTS=meshopt_simplify meshopt_simplifyWithAttributes meshopt_simplifyScale meshopt_simplifyPoints meshopt_optimizeVertexFetchRemap sbrk __wasm_call_ctors
WASM_CLUSTERIZER_SOURCES=src/clusterizer.cpp tools/wasmstubs.cpp
WASM_CLUSTERIZER_EXPORTS=meshopt_buildMeshletsBound meshopt_buildMeshlets meshopt_computeClusterBounds meshopt_computeMeshletBounds meshopt_optimizeMeshlet sbrk __wasm_call_ctors
ifeq ($(config),iphone)
IPHONESDK=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk
CFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK)
@@ -111,7 +114,7 @@ format:
formatjs:
prettier -w js/*.js gltf/*.js demo/*.html js/*.ts
js: js/meshopt_decoder.js js/meshopt_decoder.module.js js/meshopt_encoder.js js/meshopt_encoder.module.js js/meshopt_simplifier.js js/meshopt_simplifier.module.js
js: js/meshopt_decoder.js js/meshopt_decoder.module.js js/meshopt_encoder.js js/meshopt_encoder.module.js js/meshopt_simplifier.js js/meshopt_simplifier.module.js js/meshopt_clusterizer.js js/meshopt_clusterizer.module.js
symbols: $(BUILD)/amalgamated.so
nm $< -U -g
@@ -151,6 +154,10 @@ build/simplifier.wasm: $(WASM_SIMPLIFIER_SOURCES)
@mkdir -p build
$(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_SIMPLIFIER_EXPORTS)) -lc -o $@
build/clusterizer.wasm: $(WASM_CLUSTERIZER_SOURCES)
@mkdir -p build
$(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_CLUSTERIZER_EXPORTS)) -lc -o $@
js/meshopt_decoder.js: build/decoder_base.wasm build/decoder_simd.wasm tools/wasmpack.py
sed -i "s#Built with clang.*#Built with $$($(WASMCC) --version | head -n 1 | sed 's/\s\+(.*//')#" $@
sed -i "s#Built from meshoptimizer .*#Built from meshoptimizer $$(cat src/meshoptimizer.h | grep -Po '(?<=version )[0-9.]+')#" $@
@@ -167,6 +174,11 @@ js/meshopt_simplifier.js: build/simplifier.wasm tools/wasmpack.py
sed -i "s#Built from meshoptimizer .*#Built from meshoptimizer $$(cat src/meshoptimizer.h | grep -Po '(?<=version )[0-9.]+')#" $@
sed -i "s#\([\"']\).*\(;\s*//\s*embed! wasm\)#\\1$$(cat build/simplifier.wasm | python3 tools/wasmpack.py)\\1\\2#" $@
js/meshopt_clusterizer.js: build/clusterizer.wasm tools/wasmpack.py
sed -i "s#Built with clang.*#Built with $$($(WASMCC) --version | head -n 1 | sed 's/\s\+(.*//')#" $@
sed -i "s#Built from meshoptimizer .*#Built from meshoptimizer $$(cat src/meshoptimizer.h | grep -Po '(?<=version )[0-9.]+')#" $@
sed -i "s#\([\"']\).*\(;\s*//\s*embed! wasm\)#\\1$$(cat build/clusterizer.wasm | python3 tools/wasmpack.py)\\1\\2#" $@
js/%.module.js: js/%.js
sed '\#// export!#q' <$< >$@
sed -i "/use strict.;/d" $@
+68
View File
@@ -155,6 +155,74 @@ The simplification algorithm uses relative errors for input and output; to conve
getScale: (vertex_positions: Float32Array, vertex_positions_stride: number) => number;
```
## Clusterizer
`MeshoptClusterizer` (`meshopt_clusterizer.js`) implements meshlet generation and optimization.
To split a triangle mesh into clusters, call `buildMeshlets`, which tries to balance topological efficiency (by maximizing vertex reuse inside meshlets) with culling efficiency.
```ts
buildMeshlets(indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, max_vertices: number, max_triangles: number, cone_weight?: number) => MeshletBuffers;
```
The algorithm uses position data stored in a strided array; `vertex_positions_stride` represents the distance between subsequent positions in `Float32` units.
The maximum number of triangles and number of vertices per meshlet can be controlled via `max_triangles` and `max_vertices` parameters. However, `max_vertices` must not be greater than 255 and `max_triangles` must not be greater than 512.
Additionally, if cluster cone culling is to be used, `buildMeshlets` allows specifying a `cone_weight` as a value between 0 and 1 to balance culling efficiency with other forms of culling. By default, `cone_weight` is set to 0.
All meshlets are implicitly optimized for better triangle and vertex locality by `buildMeshlets`.
The algorithm returns the meshlet data as packed buffers:
```ts
const buffers = MeshoptClusterizer.buildMeshlets(indices, positions, stride, /* args */);
console.log(buffers.meshlets); // prints the raw packed Uint32Array containing the meshlet data, i.e., the indices into the vertices and triangles array
console.log(buffers.vertices); // prints the raw packed Uint32Array containing the indices into the original meshes vertices
console.log(buffers.triangles); // prints the raw packed Uint8Array containing the indices into the verices array.
console.log(buffers.meshletCount); // prints the number of meshlets - this is not the same as buffers.meshlets.length because each meshlet consists of 4 unsigned 32-bit integers
```
Individual meshlets can be extracted from the packed buffers using `extractMeshlet`. The memory of the returned `Meshlet` object's `vertices` and `triangles` arrays is backed by the `MeshletBuffers` object.
```ts
const buffers = MeshoptClusterizer.buildMeshlets(indices, positions, stride, /* args */);
const meshlet = MeshoptClusterizer.extractMeshlet(buffers, 0);
console.log(meshlet.vertices); // prints the packed Uint32Array of the first meshlet's vertex indices, i.e., indices into the original meshes vertex buffer
console.log(meshlet.triangles); // prints the packed Uint8Array of the first meshlet's indices into its own vertices array
console.log(MeshoptClusterizer.extractMeshlet(buffers, 0).triangles[0] === meshlet.triangles[0]) // prints true
meshlet.triangles.set([123], 0);
console.log(MeshoptClusterizer.extractMeshlet(buffers, 0).triangles[0] === meshlet.triangles[0]) // still prints true
```
After generating the meshlet data, it's also possible to generate extra culling data for one or more meshlets:
```ts
computeMeshletBounds(buffers: MeshletBuffers, vertex_positions: Float32Array, vertex_positions_stride: number) => Bounds | Bounds[];
```
If `buffers` contains more than one meshlet, `computeMeshletBounds` returns an array of `Bounds`. Otherwise, a single `Bounds` object is returned.
```ts
const buffers = MeshoptClusterizer.buildMeshlets(indices, positions, stride, /* args */);
const bounds = MeshoptClusterizer.computeMeshletBounds(buffers, positions, stride);
console.log(bounds[0].centerX, bounds[0].centerY, bounds[0].centerZ); // prints the center of the first meshlet's bounding sphere
console.log(bounds[0].radius); // prints the radius of the first meshlet's bounding sphere
console.log(bounds[0].coneApexX, bounds[0].coneApexY, bounds[0].coneApexZ); // prints the apex of the first meshlet's normal cone
console.log(bounds[0].coneAxisX, bounds[0].coneAxisY, bounds[0].coneAxisZ); // prints the axis of the first meshlet's normal cone
console.log(bounds[0].coneCutoff); // prins the cutoff angle of the first meshlet's normal cone
```
It is also possible to compute bounds of a vertex cluster that is not generated by `MeshoptClusterizer` using `computeClusterBounds`. Like `buildMeshlets`, this algorithm takes vertex indices and a strided vertex positions array with a vertex stride in `Float32` units as input.
```ts
computeClusterBounds(indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number) => Bounds;
```
## License
This library is available to anybody free of charge, under the terms of MIT License (see LICENSE.md).
+2 -1
View File
@@ -1,5 +1,6 @@
const MeshoptEncoder = require('./meshopt_encoder.js');
const MeshoptDecoder = require('./meshopt_decoder.js');
const MeshoptSimplifier = require('./meshopt_simplifier.js');
const MeshoptClusterizer = require('./meshopt_clusterizer');
module.exports = { MeshoptEncoder, MeshoptDecoder, MeshoptSimplifier };
module.exports = { MeshoptEncoder, MeshoptDecoder, MeshoptSimplifier, MeshoptClusterizer };
+1
View File
@@ -1,3 +1,4 @@
export * from './meshopt_encoder.module';
export * from './meshopt_decoder.module';
export * from './meshopt_simplifier.module';
export * from './meshopt_clusterizer.module';
+1
View File
@@ -1,3 +1,4 @@
export * from './meshopt_encoder.module.js';
export * from './meshopt_decoder.module.js';
export * from './meshopt_simplifier.module.js';
export * from './meshopt_clusterizer.module.js';
File diff suppressed because one or more lines are too long
+38
View File
@@ -0,0 +1,38 @@
// This file is part of meshoptimizer library and is distributed under the terms of MIT License.
// Copyright (C) 2016-2024, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
export class Bounds {
centerX: number;
centerY: number;
centerZ: number;
radius: number;
coneApexX: number;
coneApexY: number;
coneApexZ: number;
coneAxisX: number;
coneAxisY: number;
coneAxisZ: number;
coneCutoff: number;
}
export class MeshletBuffers {
meshlets: Uint32Array;
vertices: Uint32Array;
triangles: Uint8Array;
meshletCount: number;
}
export class Meshlet {
vertices: Uint32Array;
triangles: Uint8Array;
}
export const MeshoptClusterizer: {
supported: boolean;
ready: Promise<void>;
buildMeshlets: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, max_vertices: number, max_triangles: number, cone_weight?: number) => MeshletBuffers;
computeClusterBounds: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number) => Bounds;
computeMeshletBounds: (buffers: MeshletBuffers, vertex_positions: Float32Array, vertex_positions_stride: number) => Bounds | Bounds[];
extractMeshlet: (buffers: MeshletBuffers, index: number) => Meshlet;
};
File diff suppressed because one or more lines are too long
+150
View File
@@ -0,0 +1,150 @@
const assert = require('assert').strict;
const clusterizer = require('./meshopt_clusterizer.js');
process.on('unhandledRejection', (error) => {
console.log('unhandledRejection', error);
process.exit(1);
});
const cubeWithNormals = {
vertices: new Float32Array([
// n = (0, 0, 1)
-1.0, -1.0, 1.0, 0.0, 0.0, 1.0,
1.0, -1.0, 1.0, 0.0, 0.0, 1.0,
1.0, 1.0, 1.0, 0.0, 0.0, 1.0,
-1.0, 1.0, 1.0, 0.0, 0.0, 1.0,
// n = (0, 0, -1)
-1.0, 1.0, -1.0, 0.0, 0.0, -1.0,
1.0, 1.0, -1.0, 0.0, 0.0, -1.0,
1.0, -1.0, -1.0, 0.0, 0.0, -1.0,
-1.0, -1.0, -1.0, 0.0, 0.0, -1.0,
// n = (1, 0, 0)
1.0, -1.0, -1.0, 1.0, 0.0, 0.0,
1.0, 1.0, -1.0, 1.0, 0.0, 0.0,
1.0, 1.0, 1.0, 1.0, 0.0, 0.0,
1.0, -1.0, 1.0, 1.0, 0.0, 0.0,
// n = (-1, 0, 0)
-1.0, -1.0, 1.0, -1.0, 0.0, 0.0,
-1.0, 1.0, 1.0, -1.0, 0.0, 0.0,
-1.0, 1.0, -1.0, -1.0, 0.0, 0.0,
-1.0, -1.0, -1.0, -1.0, 0.0, 0.0,
// n = (0, 1, 0)
1.0, 1.0, -1.0, 0.0, 1.0, 0.0,
-1.0, 1.0, -1.0, 0.0, 1.0, 0.0,
-1.0, 1.0, 1.0, 0.0, 1.0, 0.0,
1.0, 1.0, 1.0, 0.0, 1.0, 0.0,
// n = (0, -1, 0)
1.0, -1.0, 1.0, 0.0, -1.0, 0.0,
-1.0, -1.0, 1.0, 0.0, -1.0, 0.0,
-1.0, -1.0, -1.0, 0.0, -1.0, 0.0,
1.0, -1.0, -1.0, 0.0, -1.0, 0.0,
]),
indices: new Uint32Array([
// n = (0, 0, 1)
0, 1, 2,
2, 3, 0,
// n = (0, 0, -1)
4, 5, 6,
6, 7, 4,
// n = (1, 0, 0)
8, 9, 10,
10, 11, 8,
// n = (-1, 0, 0)
12, 13, 14,
14, 15, 12,
// n = (0, 1, 0)
16, 17, 18,
18, 19, 16,
// n = (0, -1, 0)
20, 21, 22,
22, 23, 20,
]),
vertexStride: 6, // in floats
};
const tests = {
buildMeshlets: function () {
const maxVertices = 4;
const buffers = clusterizer.buildMeshlets(cubeWithNormals.indices, cubeWithNormals.vertices, cubeWithNormals.vertexStride, maxVertices, 512);
const expectedVertices = [
new Uint32Array([2, 3, 0, 1]),
new Uint32Array([12, 13, 14, 15]),
new Uint32Array([6, 7, 4, 5]),
new Uint32Array([16, 17, 18, 19]),
new Uint32Array([8, 9, 10, 11]),
new Uint32Array([22, 23, 20, 21]),
];
const expectedTriangles = new Uint8Array([0, 1, 2, 2, 3, 0]);
assert.equal(buffers.meshletCount, 6);
for (let i = 0; i < buffers.meshletCount; ++i) {
const m = clusterizer.extractMeshlet(buffers, i);
assert.deepStrictEqual(m.vertices, expectedVertices[i]);
assert.deepStrictEqual(m.triangles, expectedTriangles);
}
},
computeClusterBounds: function () {
for (let i = 0; i < 6; ++i) {
const indexOffset = i * 6;
const normalOffset = i * 4 * cubeWithNormals.vertexStride;
const bounds = clusterizer.computeClusterBounds(
cubeWithNormals.indices.subarray(indexOffset, 6 + indexOffset),
cubeWithNormals.vertices,
cubeWithNormals.vertexStride,
);
assert.deepStrictEqual(
new Int32Array([bounds.coneAxisX, bounds.coneAxisY, bounds.coneAxisZ]),
new Int32Array(cubeWithNormals.vertices.subarray(3 + normalOffset, 6 + normalOffset))
);
}
},
computeMeshletBounds: function () {
const maxVertices = 4;
const buffers = clusterizer.buildMeshlets(cubeWithNormals.indices, cubeWithNormals.vertices, cubeWithNormals.vertexStride, maxVertices, 512);
const expectedNormals = [
new Int32Array([0, 0, 1]),
new Int32Array([-1, 0, 0]),
new Int32Array([0, 0, -1]),
new Int32Array([0, 1, 0]),
new Int32Array([1, 0, 0]),
new Int32Array([0, -1, 0]),
];
const bounds = clusterizer.computeMeshletBounds(buffers, cubeWithNormals.vertices, cubeWithNormals.vertexStride);
assert(bounds.length === 6);
assert(bounds.length === buffers.meshletCount);
bounds.forEach((b, i) => {
const normal = new Int32Array([b.coneAxisX, b.coneAxisY, b.coneAxisZ]);
assert.deepStrictEqual(normal, expectedNormals[i]);
});
},
};
clusterizer.ready.then(_ => {
let passed = 0;
let failed = 0;
for (const key in tests) {
try {
tests[key]();
++passed;
} catch (e) {
console.error(e);
++failed;
}
}
if (failed === 0) {
console.log(passed, 'tests passed');
} else {
console.log(passed, 'tests passed &', failed, 'tests failed');
}
});
+1 -1
View File
@@ -21,7 +21,7 @@
"module": "index.module.js",
"types": "index.module.d.ts",
"scripts": {
"test": "node meshopt_encoder.test.js && node meshopt_decoder.test.js && node meshopt_simplifier.test.js",
"test": "node meshopt_encoder.test.js && node meshopt_decoder.test.js && node meshopt_simplifier.test.js && node meshopt_clusterizer.test.js",
"prepublishOnly": "npm test"
}
}