From f7f92280f7a2d7f9541990c9902841d6d516c2f7 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Sat, 22 Jun 2024 14:31:26 -0400 Subject: [PATCH] fix(functions): Remove side effects of no-op meshopt() and quantize() calls (#1438) --- packages/functions/src/meshopt.ts | 4 ++ packages/functions/src/quantize.ts | 57 +++++++++++++++++++----- packages/functions/test/meshopt.test.ts | 27 +++++++++++ packages/functions/test/quantize.test.ts | 13 ++++++ 4 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 packages/functions/test/meshopt.test.ts diff --git a/packages/functions/src/meshopt.ts b/packages/functions/src/meshopt.ts index 231dfb049..9077603e8 100644 --- a/packages/functions/src/meshopt.ts +++ b/packages/functions/src/meshopt.ts @@ -55,6 +55,10 @@ export function meshopt(_options: MeshoptOptions): Transform { let patternTargets: RegExp; let quantizeNormal = options.quantizeNormal; + if (document.getRoot().listAccessors().length === 0) { + return; + } + // IMPORTANT: Vertex attributes should be quantized in 'high' mode IFF they are // _not_ filtered in 'packages/extensions/src/ext-meshopt-compression/encoder.ts'. // Note that normals and tangents use octahedral filters, but _morph_ normals diff --git a/packages/functions/src/quantize.ts b/packages/functions/src/quantize.ts index 2f1bba2e4..e1dd2e30e 100644 --- a/packages/functions/src/quantize.ts +++ b/packages/functions/src/quantize.ts @@ -106,11 +106,9 @@ export function quantize(_options: QuantizeOptions = QUANTIZE_DEFAULTS): Transfo ..._options, }); - return createTransform(NAME, async (doc: Document): Promise => { - const logger = doc.getLogger(); - const root = doc.getRoot(); - - doc.createExtension(KHRMeshQuantization).setRequired(true); + return createTransform(NAME, async (document: Document): Promise => { + const logger = document.getLogger(); + const root = document.getRoot(); // Compute vertex position quantization volume. let nodeTransform: VectorTransform | undefined = undefined; @@ -119,13 +117,13 @@ export function quantize(_options: QuantizeOptions = QUANTIZE_DEFAULTS): Transfo } // Quantize mesh primitives. - for (const mesh of doc.getRoot().listMeshes()) { + for (const mesh of document.getRoot().listMeshes()) { if (options.quantizationVolume === 'mesh') { nodeTransform = getNodeTransform(getPositionQuantizationVolume(mesh)); } if (nodeTransform && options.pattern.test('POSITION')) { - transformMeshParents(doc, mesh, nodeTransform); + transformMeshParents(document, mesh, nodeTransform); transformMeshMaterials(mesh, 1 / nodeTransform.scale); } @@ -135,15 +133,23 @@ export function quantize(_options: QuantizeOptions = QUANTIZE_DEFAULTS): Transfo if (renderCount < uploadCount / 2) { compactPrimitive(prim); } - quantizePrimitive(doc, prim, nodeTransform!, options); + quantizePrimitive(document, prim, nodeTransform!, options); for (const target of prim.listTargets()) { - quantizePrimitive(doc, target, nodeTransform!, options); + quantizePrimitive(document, target, nodeTransform!, options); } } } + const needsExtension = root + .listMeshes() + .flatMap((mesh) => mesh.listPrimitives()) + .some(isQuantizedPrimitive); + if (needsExtension) { + document.createExtension(KHRMeshQuantization).setRequired(true); + } + if (options.cleanup) { - await doc.transform( + await document.transform( prune({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.SKIN, PropertyType.MATERIAL], keepAttributes: true, @@ -495,6 +501,37 @@ function getPositionQuantizationVolume(mesh: Mesh): bbox { return bbox; } +function isQuantizedAttribute(semantic: string, attribute: Accessor): boolean { + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview + const componentSize = attribute.getComponentSize(); + if (semantic === 'POSITION') return componentSize < 4; + if (semantic === 'NORMAL') return componentSize < 4; + if (semantic === 'TANGENT') return componentSize < 4; + if (semantic.startsWith('TEXCOORD_')) { + const componentType = attribute.getComponentType(); + const normalized = attribute.getNormalized(); + return ( + componentSize < 4 && + !(normalized && componentType === Accessor.ComponentType.UNSIGNED_BYTE) && + !(normalized && componentType === Accessor.ComponentType.UNSIGNED_SHORT) + ); + } + return false; +} + +function isQuantizedPrimitive(prim: Primitive | PrimitiveTarget): boolean { + for (const semantic of prim.listSemantics()) { + const attribute = prim.getAttribute('POSITION')!; + if (isQuantizedAttribute(semantic, attribute)) { + return true; + } + } + if (prim.propertyType === PropertyType.PRIMITIVE) { + return prim.listTargets().some(isQuantizedPrimitive); + } + return false; +} + /** Computes total min and max of all Accessors in a list. */ function flatBounds(accessors: Accessor[], elementSize: number): { min: T; max: T } { const min: number[] = new Array(elementSize).fill(Infinity); diff --git a/packages/functions/test/meshopt.test.ts b/packages/functions/test/meshopt.test.ts new file mode 100644 index 000000000..512cbdcc4 --- /dev/null +++ b/packages/functions/test/meshopt.test.ts @@ -0,0 +1,27 @@ +import test from 'ava'; +import { Document } from '@gltf-transform/core'; +import { meshopt } from '@gltf-transform/functions'; +import { createTorusKnotPrimitive, logger } from '@gltf-transform/test-utils'; +import { MeshoptEncoder } from 'meshoptimizer'; + +test('basic', async (t) => { + const document = new Document().setLogger(logger); + document.createMesh().addPrimitive(createTorusKnotPrimitive(document, { tubularSegments: 6 })); + + await document.transform(meshopt({ encoder: MeshoptEncoder })); + + t.true(hasMeshopt(document), 'adds extension'); +}); + +test('noop', async (t) => { + const document = new Document().setLogger(logger); + await document.transform(meshopt({ encoder: MeshoptEncoder })); + + t.false(hasMeshopt(document), 'skips extension if no accessors found'); +}); + +const hasMeshopt = (document: Document): boolean => + document + .getRoot() + .listExtensionsUsed() + .some((ext) => ext.extensionName === 'EXT_meshopt_compression'); diff --git a/packages/functions/test/quantize.test.ts b/packages/functions/test/quantize.test.ts index 45c46c2b5..57bcd1c6c 100644 --- a/packages/functions/test/quantize.test.ts +++ b/packages/functions/test/quantize.test.ts @@ -16,6 +16,19 @@ import { EXTMeshGPUInstancing, KHRMaterialsVolume, Volume } from '@gltf-transfor import { quantize } from '@gltf-transform/functions'; import { logger, round, roundBbox } from '@gltf-transform/test-utils'; +test('noop', async (t) => { + const document = new Document().setLogger(logger); + await document.transform(quantize()); + + t.false( + document + .getRoot() + .listExtensionsUsed() + .some((ext) => ext.extensionName === 'KHR_mesh_quantization'), + 'skips extension', + ); +}); + test('exclusions', async (t) => { const doc = new Document().setLogger(logger); const prim = createPrimitive(doc);