mirror of
https://github.com/openharmony/third_party_meshoptimizer.git
synced 2026-07-01 09:25:03 -04:00
Merge pull request #738 from JolifantoBambla/clusterizer-wasm
Add JS bindings for clusterizer API
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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" $@
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
Vendored
+1
@@ -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,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
Vendored
+38
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user