diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6bdbd141b..3ac8d9ad0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -89,6 +89,8 @@ jobs: run: node js/meshopt_simplifier.test.js - name: test clusterizer run: node js/meshopt_clusterizer.test.js + - name: check typescript declarations + run: npx --yes -p typescript@6 tsc --noEmit --moduleResolution node16 --module node16 --strict js/index.d.ts - name: check es5 run: | npm install -g es-check@7.2.1 diff --git a/Makefile b/Makefile index 9a70e43db..813a16a13 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,9 @@ WASM_SIMPLIFIER_EXPORTS=meshopt_simplify meshopt_simplifyWithAttributes meshopt_ WASM_CLUSTERIZER_SOURCES=src/clusterizer.cpp src/meshletutils.cpp tools/wasmstubs.cpp WASM_CLUSTERIZER_EXPORTS=meshopt_buildMeshletsBound meshopt_buildMeshletsFlex meshopt_buildMeshletsSpatial meshopt_computeClusterBounds meshopt_computeMeshletBounds meshopt_computeSphereBounds meshopt_optimizeMeshlet sbrk __wasm_call_ctors +WASM_TANGENTS_SOURCES=src/tangentspace.cpp tools/wasmstubs.cpp +WASM_TANGENTS_EXPORTS=meshopt_generateTangents sbrk __wasm_call_ctors + ifneq ($(werror),) CFLAGS+=-Werror CXXFLAGS+=-Werror @@ -141,7 +144,7 @@ format: formatjs: prettier -w js/*.js gltf/*.js demo/*.html js/*.ts -js: js/meshopt_decoder.cjs js/meshopt_decoder.mjs js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js +js: js/meshopt_decoder.cjs js/meshopt_decoder.mjs js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js js/meshopt_tangents.js symbols: $(BUILD)/amalgamated.so nm $< -U -g @@ -185,6 +188,10 @@ build/clusterizer.wasm: $(WASM_CLUSTERIZER_SOURCES) @mkdir -p build $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_CLUSTERIZER_EXPORTS)) -lc -o $@ +build/tangents.wasm: $(WASM_TANGENTS_SOURCES) + @mkdir -p build + $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_TANGENTS_EXPORTS)) -lc -o $@ + js/meshopt_decoder.mjs: 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.]+')#" $@ @@ -194,8 +201,9 @@ js/meshopt_decoder.mjs: build/decoder_base.wasm build/decoder_simd.wasm tools/wa js/meshopt_encoder.js: build/encoder.wasm tools/wasmpack.py js/meshopt_simplifier.js: build/simplifier.wasm tools/wasmpack.py js/meshopt_clusterizer.js: build/clusterizer.wasm tools/wasmpack.py +js/meshopt_tangents.js: build/tangents.wasm tools/wasmpack.py -js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js: +js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js js/meshopt_tangents.js: 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.]+')#" $@ python3 tools/wasmpack.py patch $@ wasm <$< diff --git a/js/README.md b/js/README.md index 631f09c5a..9ac0cb364 100644 --- a/js/README.md +++ b/js/README.md @@ -135,7 +135,7 @@ By default, `encodeVertexBuffer` uses v1 version of the encoding; this encoding To simplify the mesh, the following function needs to be called first: ```ts -simplify(indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, target_index_count: number, target_error: number, flags?: Flags[]) => [Uint32Array, number]; +simplify(indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, target_index_count: number, target_error: number, flags?: SimplifierFlags[]) => [Uint32Array, number]; ``` Given an input triangle mesh represented by an index buffer and a position buffer, the algorithm tries to simplify the mesh down to the target index count while maintaining the appearance. For meshes with inconsistent topology or many seams, such as faceted meshes, it can result in simplifier getting "stuck" and not being able to simplify the mesh fully. Therefore it's critical that identical vertices are "welded" together, that is, the input vertex buffer does not contain duplicates. Additionally, it may be possible to preprocess the index buffer to discard any vertex attributes that aren't critical and can be rebuilt later. @@ -165,7 +165,7 @@ This can be done before regular simplification or as the only step, which is use While `simplify` is aware of attribute discontinuities by default (and infers them through the supplied index buffer) and tries to preserve them, it can be useful to provide information about attribute values. This allows the simplifier to take attribute error into account which can improve shading (by using vertex normals), texture deformation (by using texture coordinates), and may be necessary to preserve vertex colors when textures are not used in the first place. This can be done by using a variant of the simplification function that takes attribute values and weight factors, `simplifyWithAttributes`: ```ts -simplifyWithAttributes: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, vertex_attributes: Float32Array, vertex_attributes_stride: number, attribute_weights: number[], vertex_lock: Uint8Array | null, target_index_count: number, target_error: number, flags?: Flags[]) => [Uint32Array, number]; +simplifyWithAttributes: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, vertex_attributes: Float32Array, vertex_attributes_stride: number, attribute_weights: number[], vertex_lock: Uint8Array | null, target_index_count: number, target_error: number, flags?: SimplifierFlags[]) => [Uint32Array, number]; ``` This function takes an additional `vertex_attributes` buffer that contains all the attributes to be used. The `attribute_weights` array contains a weight for each attribute, which is used to balance the importance of each attribute during simplification. For normalized attributes like normals and vertex colors, a weight around 1.0 is usually appropriate; internally, a change of `1/weight` in attribute value over a distance `d` is approximately equivalent to a change of `d` in position. Using higher weights may be appropriate to preserve attribute quality at the cost of position quality. If the attribute has a different scale (e.g. unnormalized vertex colors in [0..255] range), the weight should be divided by the scaling factor (1/255 in this example). @@ -268,6 +268,20 @@ Finally, it is possible to compute spherical bounds of an arbitrary set of point computeSphereBounds: (positions: Float32Array, positions_stride: number, radii?: Float32Array, radii_stride?: number) => Bounds; ``` +## Tangents + +`MeshoptTangents` (`meshopt_tangents.js`) implements tangent space generation for meshes that use tangent space normal maps. These meshes often require per-vertex tangent vectors in addition to normals; these could be exported alongside mesh data, but this module can generate them from positions, normals and texture coordinates: + +```ts +generateTangents: (indices: Uint32Array | null, vertex_positions: Float32Array, vertex_positions_stride: number, vertex_normals: Float32Array, vertex_normals_stride: number, vertex_uvs: Float32Array, vertex_uvs_stride: number, flags?: TangentsFlags[]) => Float32Array; +``` + +For each triangle *corner* this writes a normalized tangent vector (xyz) and an orientation sign (+-1); the bitangent can be reconstructed in the shader as `cross(normal, tangent.xyz) * tangent.w`. Note that some coordinate space conventions that flip V direction in the texture space require negating orientation sign. The input can be indexed, or not (`indices=null`); this does not affect the output tangents. + +Because tangents are computed per corner, applying them to mesh vertices requires de-indexing the mesh and generating a new index/vertex buffer afterwards, or using indexed data and copying tangents to existing vertex data while duplicating vertices with different tangents. With the indexed input, if it contains UV mirroring, vertices along the mirror edge may have different tangent spaces on different sides of the edge and need to be split - copying tangents to existing vertex data without splitting will not produce correct results. + +The algorithm uses a MikkTSpace-like construction but by default, uses a modified weighting scheme that significantly improves tangent quality around beveled regions in the mesh. If the normal maps are baked from higher resolution geometry using MikkTSpace weighting, it's possible to produce MikkTSpace-compatible tangents by passing `'Compatible'` option in `flags` + ## License This library is available to anybody free of charge, under the terms of MIT License (see LICENSE.md). diff --git a/js/index.d.ts b/js/index.d.ts index 71332ffff..71b08d788 100644 --- a/js/index.d.ts +++ b/js/index.d.ts @@ -2,3 +2,4 @@ export * from './meshopt_encoder.js'; export * from './meshopt_decoder.js'; export * from './meshopt_simplifier.js'; export * from './meshopt_clusterizer.js'; +export * from './meshopt_tangents.js'; diff --git a/js/index.js b/js/index.js index 334ee8e43..1ebea45d8 100644 --- a/js/index.js +++ b/js/index.js @@ -2,3 +2,4 @@ export * from './meshopt_encoder.js'; export * from './meshopt_decoder.mjs'; export * from './meshopt_simplifier.js'; export * from './meshopt_clusterizer.js'; +export * from './meshopt_tangents.js'; diff --git a/js/meshopt_simplifier.d.ts b/js/meshopt_simplifier.d.ts index 0de2d46aa..c9f4541d7 100644 --- a/js/meshopt_simplifier.d.ts +++ b/js/meshopt_simplifier.d.ts @@ -1,6 +1,9 @@ // This file is part of meshoptimizer library and is distributed under the terms of MIT License. // Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) -export type Flags = 'LockBorder' | 'Sparse' | 'ErrorAbsolute' | 'Prune' | 'Regularize' | 'Permissive'; +export type SimplifierFlags = 'LockBorder' | 'Sparse' | 'ErrorAbsolute' | 'Prune' | 'Regularize' | 'Permissive'; + +/** @deprecated Use SimplifierFlags instead. */ +export type Flags = SimplifierFlags; export const MeshoptSimplifier: { supported: boolean; @@ -15,7 +18,7 @@ export const MeshoptSimplifier: { vertex_positions_stride: number, target_index_count: number, target_error: number, - flags?: Flags[] + flags?: SimplifierFlags[] ) => [Uint32Array, number]; simplifyWithAttributes: ( @@ -28,7 +31,7 @@ export const MeshoptSimplifier: { vertex_lock: Uint8Array | null, target_index_count: number, target_error: number, - flags?: Flags[] + flags?: SimplifierFlags[] ) => [Uint32Array, number]; simplifyWithUpdate: ( @@ -41,7 +44,7 @@ export const MeshoptSimplifier: { vertex_lock: Uint8Array | null, target_index_count: number, target_error: number, - flags?: Flags[] + flags?: SimplifierFlags[] ) => [number, number]; simplifySloppy: ( diff --git a/js/meshopt_tangents.d.ts b/js/meshopt_tangents.d.ts new file mode 100644 index 000000000..5f8fb1eb1 --- /dev/null +++ b/js/meshopt_tangents.d.ts @@ -0,0 +1,20 @@ +// This file is part of meshoptimizer library and is distributed under the terms of MIT License. +// Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) + +export type TangentsFlags = 'Compatible' | 'ZeroFallback'; + +export const MeshoptTangents: { + supported: boolean; + ready: Promise; + + generateTangents: ( + indices: Uint32Array | Int32Array | Uint16Array | Int16Array | null, + vertex_positions: Float32Array, + vertex_positions_stride: number, + vertex_normals: Float32Array, + vertex_normals_stride: number, + vertex_uvs: Float32Array, + vertex_uvs_stride: number, + flags?: TangentsFlags[] + ) => Float32Array; +}; diff --git a/js/meshopt_tangents.js b/js/meshopt_tangents.js new file mode 100644 index 000000000..1ae56e0a1 --- /dev/null +++ b/js/meshopt_tangents.js @@ -0,0 +1,164 @@ +// This file is part of meshoptimizer library and is distributed under the terms of MIT License. +// Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) +var MeshoptTangents = (function () { + // Built with clang version 22.1.0-wasi-sdk + // Built from meshoptimizer 1.1 + var wasm = + 'b9H79Tebbbegv9Geueu9Geub9Gbb9Gkuuuuuuuuuuub9Giuuueuirodiblbelve9Weiiviebeoweuecj:Gdkrnlo9TW9T9VV95dbH9F9F939H79T9F9J9H229F9Jt9VV7bbK9TW79O9V9Wt9F9NW9UWV9HtW9u9H9U9NW9Ut7bel79IV9RbdDwebcekdlvq;m8Lodbk:wgvOuw99iuv99vu8Jjjjjbc;Wb9Rgk8Kjjjjbakcxfcbc;Kbz:djjjb8AakcualcdtgxalcFFFFi0Ecbyd:q:G:cjbHjjjjbbgmBdxakceBd2adci9UhPalcd4alfhscehzinazgHcethzaHas6mbkakcuaHcdtgzaHcFFFFi0Ecbyd:q:G:cjbHjjjjbbgsBdzakcdBd2ascFeazz:djjjbhOaDcd4hAarcd4hCavcd4hXdnalTmbaHcufhrcbhvindndnaOcbaiavaX2cdtfgHydlgDaDcjjjj94SEgscH4cbaoavaC2cdtfgzydlgQcs4aQcjjjj94SE7as7c:F:b:DD2cbaHydbgLaLcjjjj94SEgscH4cbazydbgKcs4aKcjjjj94SE7as7c;D;O:B8J27cbaHydwgYaYcjjjj94SEgHcH4cbazydwg8Acs4a8Acjjjj94SE7aH7c:3F;N8N27awavaA2cdtfgHydlgEaHydbg37cFFFFrGgHcm4aH7c:fjjK27arGgscdtfgHydbgzcuSmbaE::h5a3::h8Ea8A::h8FaQ::haaK::hhaY::hgaD::h8JaL::h8KcehDinaDhHdnaiazaX2cdtfgDIdba8K9CmbaDIdla8J9CmbaDIdwag9CmbaoazaC2cdtfgDIdbah9CmbaDIdlaa9CmbaDIdwa8F9CmbawazaA2cdtfgDIdba8E9CmbaDIdla59BmikaHcefhDaOasaHfarGgscdtfgHydbgzcu9hmbkkaHavBdbavhzkamavcdtfazBdbavcefgval9hmbkkaOcbydN:G:cjbH:bjjjbbakceBd2akcualcefgHcdtaHcFFFFi0Ecbyd:q:G:cjbHjjjjbbg8LBdzakcdBd2akcuadcdtadcFFFFi0EgYcbyd:q:G:cjbHjjjjbbg8MBdCakciBd2a8Lclfcbaxz:djjjbhzdnadTmbdnaeTmbaehHadhsinazamaHydbcdtfydbcdtfgDaDydbcefBdbaHclfhHascufgsmbxdkkamhHadhsinazaHydbcdtfgDaDydbcefBdbaHclfhHascufgsmbkkdnalTmbcbhsazhHalhDinaHydbhOaHasBdbaHclfhHaOasfhsaDcufgDmbkkdnadci6gQmbcbhscdhHaPhOindndnaeTmbamaeasfgvydbcdtfhDamavcwfydbcdtfhramavclfydbcdtfhvxekamasfgDcwfhraDclfhvkarydbhravydbhvazaDydbcdtfgDaDydbgDcefBdba8MaDcdtfaHc9:fBdbazavcdtfgDaDydbgDcefBdba8MaDcdtfaHcufBdbazarcdtfgDaDydbgDcefBdba8MaDcdtfaHBdbascxfhsaHclfhHaOcufgOmbkkcbhsa8LcbBdbakcuaPcltadcFFFFd0Ecbyd:q:G:cjbHjjjjbbg8NBdKakclBd2dnaQmbaehza8NhHaPhvindndnaeTmbazcwfydbhDazclfydbhOazydbhrxekascdfhDascefhOashrkJbbbbh8KJbbbbJbbbbJbbbbJbbbbJbbjZJbbj:;awaOaA2cdtfgQIdbawaraA2cdtfgLIdbgh:tawaDaA2cdtfgKIdlaLIdlgg:tg8JNaQIdlag:tggaKIdbah:tN:tghJbbbb9EEahJbbbb9BEg8EaiaraX2cdtfgrIdwghaiaOaX2cdtfgOIdwg59BEa8EarIdlgaaOIdlgy9BEa8EarIdbg8FaOIdbg8P9BEg8EahaiaDaX2cdtfgDIdwgI9BEa8EaaaDIdlg8R9BEa8Ea8FaDIdbg8S9BEg8Ea5aI9BEa8Eaya8R9BEa8Ea8Pa8S9BEh8Edna8Ja5ah:tNagaIah:tN:tghahNa8Ja8Pa8F:tNaga8Sa8F:tN:tg8Fa8FNa8Jayaa:tNaga8Raa:tN:tg8Ja8JNMMggJbbbb9Bmba8Eag:r:vh8KkaHcxfa8EUdbaHcwfaha8KNUdbaHclfa8Ja8KNUdbaHa8Fa8KNUdbazcxfhzaHczfhHascifhsavcufgvmbkkcbhHakaYcbyd:q:G:cjbHjjjjbbgDBd3akcvBd2dnadTmbaDhzinazaHBdbazclfhzadaHcefgH9hmbkkakcuaPcdtadcFFFF970Ecbyd:q:G:cjbHjjjjbbgvBdaakcoBd2akaPcbyd:q:G:cjbHjjjjbbg8ABd8KakcrBd2dnadci6mba8NcxfhzavhscbhHinasaHBdba8AaHfcdcbazIdbg8KJbbbb9DEa8KJbbbb9EV86bbasclfhsazczfhzaPaHcefgH9hmbkkdnalTmbcbhRinaRcdthHdna8LaRcefgRcdtfydbgza8LaHfydbgHSmbazaH9RhLa8MaHcdtfhKcbh8UinaKa8Ucdtfg8VydbgHcd4g8Wci2gxaHciGcdtgzyd:e:G:cjbfhHaxazydj:G:cjbfhzdnaeTmbaeaHcdtfydbhHaeazcdtfydbhzkdna8Ucefg8UaL9pmbamaHcdtfydbhYamazcdtfydbh3a8Aa8WfhEava8Wcdtfh8Xa8UhwinaKawcdtfgQydbgHcd4gzci2gAaHciGcdtgHyd:e:G:cjbfhsaAaHydj:G:cjbfhHdnaeTmbaeascdtfydbhsaeaHcdtfydbhHkdndnamaHcdtfydbaYSmbamascdtfydba39hmeka8AazfRbbgHaERbbgsVciSmbdnaHasGmba8Whsdna8Wa8XydbgHSmba8XhOinaOavaHgscdtfgrydbgHBdbarhOasaH9hmbkkdnazavazcdtfgOydbgHSmbinaOavaHgzcdtfgrydbgHBdbarhOazaH9hmbkkasazSmba8AazfgORbba8AasfgHRbbVciSmeavazcdtfasBdbaHaHRbbaORbbV86bbkdna8VydbciGaxfgzaDazcdtfgsydbgHSmbinasaDaHgzcdtfgOydbgHBdbaOhsazaH9hmbkkdnaQydbciGaAfgsaDascdtfgOydbgHSmbinaOaDaHgscdtfgrydbgHBdbarhOasaH9hmbkkazasSmbaDascdtfazBdbkawcefgwaL9hmbkka8UaL9hmbkkaRal9hmbkkdnadTmbcbhrinarhzdnaraDarcdtfgwydbgHSmbawhsinasaDaHgzcdtfgOydbgHBdbaOhsazaH9hmbkkawazBdbarcefgrad9hmbkcbh8Vabcbadcltz:djjjbhKdnadci6mbaqceGhEaDhYaeh3cbh8Windna8Na8WcltfgsIdxJbbbb9Bmbama8Wcx2gHfhxaeaHfhLcbhza8VhwinaKaYazfydbcltfhHdndnaeTmbamaLazc:e:G:cjbfydbcdtfydbcdtfhAamaLazcj:G:cjbfydbcdtfydbcdtfhra3azfydbhOxekaxazc:e:G:cjbfydbcdtfhAaxazcj:G:cjbfydbcdtfhrawhOkaHasIdbggaoamaOcdtfydbgQaC2cdtfgOIdbg8KasIdwg8FaOIdwg8JNaga8KNasIdlg8EaOIdlggNMMghN:tgaJbbbbJbbjZa8Fa8JahN:tg8Fa8FNaaaaNa8EagahN:tghahNMMga:r:vaaJbbbb9BEJ;As6nJbbjZaiarydbaX2cdtfgOIdwaiaQaX2cdtfgrIdwg5:tgaa8Jaaa8JNaOIdbarIdbgy:tg8Pa8KNagaOIdlarIdlgI:tg8RNMMgaN:tg8EaiaAydbaX2cdtfgOIdwa5:tg5a8Ja5a8JNaOIdbay:tg8Sa8KNagaOIdlaI:tgINMMg5N:tg8JNa8Pa8KaaN:tgya8Sa8Ka5N:tg8KNa8RagaaN:tgaaIaga5N:tggNMMJbbbbJbbjZa8Ea8ENayayNaaaaNMMa8Ja8JNa8Ka8KNagagNMMNg8K:rg8J:va8KJbbbb9BENgg:lg8Ka8KJbbjZ9EEg8Ka8KJ7;A9s89NJ:L9t9s::MNJ;ob;jZMJbbjZa8K:t:rNg8K:ta8KagJbbbb9DENg8Ka8Ja8KNaEEg8KNaHIdbMUdbaHaha8KNaHIdlMUdlaHa8Fa8KNaHIdwMUdwawcefhwazclfgzcx9hmbkkaYcxfhYa3cxfh3a8Vcifh8Va8Wcefg8WaP9hmbkcbhrinarhzdnaravarcdtfgsydbgHSmbinasavaHgzcdtfgOydbgHBdbaOhsazaH9hmbkkaKarc8W2fgHc3fJbbjZJbbj:;a8AazfRbbceGEg8KUdbaHc8Sfa8KUdbaHa8KUdxarcefgraP9hmbkkaqcdGhvcbhzaDhsaKhHindnazasydb9hmbJbbbbh8KdnaHcwfgOIdbg8Ja8JNaHIdbggagNaHclfgrIdbghahNMMgaJbbbb9BmbJbbjZaa:r:vh8KkaOa8Ja8KNUdbaraha8KNUdbaHaga8KNg8JUdbavmbaHJbbjZa8Ja8KJbbbb9BEUdbkasclfhsaHczfhHadazcefgz9hmbkcbhHaKhzindnaHaDydbgsSmbazaKascltfgsydwBdwazas8Pdb83dbkaDclfhDazczfhzadaHcefgH9hmbkkdnakyd2gzTmbazcdtakcxffc98fhHinaHydbcbydN:G:cjbH:bjjjbbaHc98fhHazcufgzmbkkakc;Wbf8Kjjjjbk9teiucbcbyd:y:G:cjbgeabcifc98GfgbBd:y:G:cjbdndnabZbcztgd9nmbcuhiabad9RcFFifcz4nbcuSmekaehikaik;aeedudndnabciGTmbabhixekaecFeGc:b:c:ew2hldndnadcz9pmbabhixekabhiinaialBdbaicxfalBdbaicwfalBdbaiclfalBdbaiczfhiadc9Wfgdcs0mbkkadcl6mbinaialBdbaiclfhiadc98fgdci0mbkkdnadTmbinaiae86bbaicefhiadcufgdmbkkabk9teiucbcbyd:y:G:cjbgeabcrfc94GfgbBd:y:G:cjbdndnabZbcztgd9nmbcuhiabad9RcFFifcz4nbcuSmekaehikaikTeeucbabcbyd:y:G:cjbge9Rcifc98GaefgbBd:y:G:cjbdnabZbcztge9nmbabae9RcFFifcz4nb8Akkk8Rdbcj:Gdkzebbbdbbbbbbbebbbbc:q:Gdkxebbbdbbba:qbb'; // embed! wasm + + var wasmpack = new Uint8Array([ + 32, 0, 65, 2, 1, 106, 34, 33, 3, 128, 11, 4, 13, 64, 6, 253, 10, 7, 15, 116, 127, 5, 8, 12, 40, 16, 19, 54, 20, 9, 27, 255, 113, 17, 42, 67, + 24, 23, 146, 148, 18, 14, 22, 45, 70, 69, 56, 114, 101, 21, 25, 63, 75, 136, 108, 28, 118, 29, 73, 115, + ]); + + if (typeof WebAssembly !== 'object') { + return { + supported: false, + }; + } + + var instance; + + var ready = WebAssembly.instantiate(unpack(wasm), {}).then(function (result) { + instance = result.instance; + instance.exports.__wasm_call_ctors(); + }); + + function unpack(data) { + var result = new Uint8Array(data.length); + for (var i = 0; i < data.length; ++i) { + var ch = data.charCodeAt(i); + result[i] = ch > 96 ? ch - 97 : ch > 64 ? ch - 39 : ch + 4; + } + var write = 0; + for (var i = 0; i < data.length; ++i) { + result[write++] = result[i] < 60 ? wasmpack[result[i]] : (result[i] - 60) * 64 + result[++i]; + } + return result.buffer.slice(0, write); + } + + function assert(cond) { + if (!cond) { + throw new Error('Assertion failed'); + } + } + + function bytes(view) { + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); + } + + function generate( + indices, + index_count, + vertex_positions, + vertex_count, + vertex_positions_stride, + vertex_normals, + vertex_normals_stride, + vertex_uvs, + vertex_uvs_stride, + options + ) { + var sbrk = instance.exports.sbrk; + + var resultp = sbrk(index_count * 16); + var indicesp = indices ? sbrk(indices.byteLength) : 0; + var positionsp = sbrk(vertex_positions.byteLength); + var normalsp = sbrk(vertex_normals.byteLength); + var uvsp = sbrk(vertex_uvs.byteLength); + + var heap = new Uint8Array(instance.exports.memory.buffer); + if (indices) heap.set(bytes(indices), indicesp); + heap.set(bytes(vertex_positions), positionsp); + heap.set(bytes(vertex_normals), normalsp); + heap.set(bytes(vertex_uvs), uvsp); + + instance.exports.meshopt_generateTangents( + resultp, + indicesp, + index_count, + positionsp, + vertex_count, + vertex_positions_stride * 4, + normalsp, + vertex_normals_stride * 4, + uvsp, + vertex_uvs_stride * 4, + options + ); + + // heap may have grown + heap = new Uint8Array(instance.exports.memory.buffer); + + var result = new Float32Array(heap.buffer, resultp, index_count * 4).slice(); + sbrk(resultp - sbrk(0)); + + return result; + } + + var tangentOptions = { + Compatible: 1, + ZeroFallback: 2, + }; + + return { + ready: ready, + supported: true, + generateTangents: function ( + indices, + vertex_positions, + vertex_positions_stride, + vertex_normals, + vertex_normals_stride, + vertex_uvs, + vertex_uvs_stride, + flags + ) { + assert( + indices === null || + indices instanceof Uint32Array || + indices instanceof Int32Array || + indices instanceof Uint16Array || + indices instanceof Int16Array + ); + assert(indices === null || indices.length % 3 == 0); + assert(vertex_positions instanceof Float32Array); + assert(vertex_positions.length % vertex_positions_stride == 0); + assert(vertex_positions_stride >= 3); + assert(vertex_normals instanceof Float32Array); + assert(vertex_normals.length % vertex_normals_stride == 0); + assert(vertex_normals_stride >= 3); + assert(vertex_uvs instanceof Float32Array); + assert(vertex_uvs.length % vertex_uvs_stride == 0); + assert(vertex_uvs_stride >= 2); + assert(vertex_positions.length / vertex_positions_stride == vertex_normals.length / vertex_normals_stride); + assert(vertex_positions.length / vertex_positions_stride == vertex_uvs.length / vertex_uvs_stride); + assert(indices !== null || (vertex_positions.length / vertex_positions_stride) % 3 == 0); + + var options = 0; + for (var i = 0; i < (flags ? flags.length : 0); ++i) { + assert(flags[i] in tangentOptions); + options |= tangentOptions[flags[i]]; + } + + var vertex_count = vertex_positions.length / vertex_positions_stride; + var index_count = indices ? indices.length : vertex_count; + + var indices32 = indices === null || indices.BYTES_PER_ELEMENT == 4 ? indices : new Uint32Array(indices); + return generate( + indices32, + index_count, + vertex_positions, + vertex_count, + vertex_positions_stride, + vertex_normals, + vertex_normals_stride, + vertex_uvs, + vertex_uvs_stride, + options + ); + }, + }; +})(); + +export { MeshoptTangents }; diff --git a/js/meshopt_tangents.test.js b/js/meshopt_tangents.test.js new file mode 100644 index 000000000..549502b6f --- /dev/null +++ b/js/meshopt_tangents.test.js @@ -0,0 +1,66 @@ +import assert from 'assert/strict'; +import { MeshoptTangents as tangents } from './meshopt_tangents.js'; + +process.on('unhandledRejection', (error) => { + console.log('unhandledRejection', error); + process.exit(1); +}); + +var tests = { + tangentDegenerate: function () { + var positions = new Float32Array([0, 0, 0, 1, 1, 0, 2, 2, 0, -1, -2, 1, 0, 1, 1, -1, 0, 0]); + var normals = new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]); + var uvs = new Float32Array([0, 0, 1, 1, 2, 2, 0, -2, 1, 0, 1, 0]); + + var indices = new Uint32Array([ + // outer, positive + 0, 3, 1, + // inner, degenerate UVs + 0, 1, 2, + // outer, positive + 1, 4, 2, + // outer, negative + 2, 5, 0, + ]); + + var vx = 0.0836; + var vy = 0.9965; + var expected = [ + // outer, positive + [1, 0, 0, 1], + [1, 0, 0, 1], + [vx, vy, 0, 1], + // inner, degenerate UVs + [1, 0, 0, 1], + [vx, vy, 0, 1], + [0, 1, 0, 1], + // outer, positive + [vx, vy, 0, 1], + [0, 1, 0, 1], + [0, 1, 0, 1], + // outer, negative + [-1, 0, 0, -1], + [-1, 0, 0, -1], + [-1, 0, 0, -1], + ].flat(); + + var result = tangents.generateTangents(indices, positions, 3, normals, 3, uvs, 2, ['Compatible']); + + assert.equal(result.length, expected.length); + + for (var i = 0; i < result.length; ++i) { + assert(Math.abs(result[i] - expected[i]) < 1e-3); + } + }, +}; + +tangents.ready.then(() => { + var count = 0; + + for (var key in tests) { + tests[key](); + count++; + } + + console.log(count, 'tests passed'); +}); diff --git a/js/package.json b/js/package.json index 78296a435..39e409134 100644 --- a/js/package.json +++ b/js/package.json @@ -44,12 +44,16 @@ "types": "./meshopt_clusterizer.d.ts", "default": "./meshopt_clusterizer.js" }, + "./tangents": { + "types": "./meshopt_tangents.d.ts", + "default": "./meshopt_tangents.js" + }, "./decoder.cjs": { "require": "./meshopt_decoder.cjs" } }, "scripts": { - "test": "node meshopt_encoder.test.js && node meshopt_decoder.test.js && node meshopt_simplifier.test.js && node meshopt_clusterizer.test.js", + "test": "node meshopt_encoder.test.js && node meshopt_decoder.test.js && node meshopt_simplifier.test.js && node meshopt_clusterizer.test.js && node meshopt_tangents.test.js", "prepublishOnly": "npm test" } }