Skip to content

Commit

Permalink
fix(functions): Remove side effects of no-op meshopt() and quantize()…
Browse files Browse the repository at this point in the history
… calls (#1438)
  • Loading branch information
donmccurdy authored Jun 22, 2024
1 parent 8a67c1d commit f7f9228
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 10 deletions.
4 changes: 4 additions & 0 deletions packages/functions/src/meshopt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 47 additions & 10 deletions packages/functions/src/quantize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,9 @@ export function quantize(_options: QuantizeOptions = QUANTIZE_DEFAULTS): Transfo
..._options,
});

return createTransform(NAME, async (doc: Document): Promise<void> => {
const logger = doc.getLogger();
const root = doc.getRoot();

doc.createExtension(KHRMeshQuantization).setRequired(true);
return createTransform(NAME, async (document: Document): Promise<void> => {
const logger = document.getLogger();
const root = document.getRoot();

// Compute vertex position quantization volume.
let nodeTransform: VectorTransform<vec3> | undefined = undefined;
Expand All @@ -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);
}

Expand All @@ -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,
Expand Down Expand Up @@ -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<T = vec2 | vec3>(accessors: Accessor[], elementSize: number): { min: T; max: T } {
const min: number[] = new Array(elementSize).fill(Infinity);
Expand Down
27 changes: 27 additions & 0 deletions packages/functions/test/meshopt.test.ts
Original file line number Diff line number Diff line change
@@ -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');
13 changes: 13 additions & 0 deletions packages/functions/test/quantize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit f7f9228

Please sign in to comment.