Skip to content

Commit

Permalink
Add listTextureInfoByMaterial(), prevent prune from leaving gaps in…
Browse files Browse the repository at this point in the history
… TEXCOORD_n indices (#947)

* Prevent `prune` from leaving gaps in TEXCOORD_n indices, fixes #821
* Add listTextureInfoByMaterial()
  • Loading branch information
donmccurdy authored May 12, 2023
1 parent 7f23b4f commit c95b94b
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 12 deletions.
56 changes: 49 additions & 7 deletions packages/functions/src/list-texture-info.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { Texture, TextureInfo } from '@gltf-transform/core';
import { ExtensionProperty, Material, Property, Texture, TextureInfo } from '@gltf-transform/core';

/**
* Lists all {@link TextureInfo} definitions associated with a given {@link Texture}.
* Lists all {@link TextureInfo} definitions associated with a given
* {@link Texture}. May be used to determine which UV transforms
* and texCoord indices are applied to the material, without explicitly
* checking the material properties and extensions.
*
* Example:
*
* ```js
* ```typescript
* // Find TextureInfo instances associated with the texture.
* const results = listTextureInfo(texture);
*
* // Find which UV sets (TEXCOORD_0, TEXCOORD_1, ...) are required.
* const texCoords = results.map((info) => info.getTexCoord());
* // → [0, 0, 1]
* // → [0, 1]
* ```
*/
export function listTextureInfo(texture: Texture): TextureInfo[] {
const graph = texture.getGraph();
const results: TextureInfo[] = [];
const results = new Set<TextureInfo>();

for (const textureEdge of graph.listParentEdges(texture)) {
const parent = textureEdge.getParent();
Expand All @@ -25,10 +28,49 @@ export function listTextureInfo(texture: Texture): TextureInfo[] {
for (const edge of graph.listChildEdges(parent)) {
const child = edge.getChild();
if (child instanceof TextureInfo && edge.getName() === name) {
results.push(child);
results.add(child);
}
}
}

return results;
return Array.from(results);
}

/**
* Lists all {@link TextureInfo} definitions associated with any {@link Texture}
* on the given {@link Material}. May be used to determine which UV transforms
* and texCoord indices are applied to the material, without explicitly
* checking the material properties and extensions.
*
* Example:
*
* ```typescript
* const results = listTextureInfoByMaterial(material);
*
* const texCoords = results.map((info) => info.getTexCoord());
* // → [0, 1]
* ```
*/
export function listTextureInfoByMaterial(material: Material): TextureInfo[] {
const graph = material.getGraph();
const visited = new Set<Property>();
const results = new Set<TextureInfo>();

function traverse(prop: Material | ExtensionProperty) {
for (const child of graph.listChildren(prop)) {
if (visited.has(child)) continue;
visited.add(child);

if (child instanceof Texture) {
for (const textureInfo of listTextureInfo(child)) {
results.add(textureInfo);
}
} else if (child instanceof ExtensionProperty) {
traverse(child);
}
}
}

traverse(material);
return Array.from(results);
}
61 changes: 60 additions & 1 deletion packages/functions/src/prune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
TextureInfo,
} from '@gltf-transform/core';
import { createTransform } from './utils.js';
import { listTextureInfoByMaterial } from './list-texture-info.js';

const NAME = 'prune';

Expand Down Expand Up @@ -101,14 +102,24 @@ export function prune(_options: PruneOptions = PRUNE_DEFAULTS): Transform {

// Prune unused vertex attributes.
if (!options.keepAttributes && propertyTypes.has(PropertyType.ACCESSOR)) {
const materialPrims = new Map<Material, Set<Primitive>>();
for (const mesh of root.listMeshes()) {
for (const prim of mesh.listPrimitives()) {
const required = listRequiredSemantics(doc, prim.getMaterial());
const material = prim.getMaterial();
const required = listRequiredSemantics(doc, material);
const unused = listUnusedSemantics(prim, required);
pruneAttributes(prim, unused);
prim.listTargets().forEach((target) => pruneAttributes(target, unused));
if (material) {
materialPrims.has(material)
? materialPrims.get(material)!.add(prim)
: materialPrims.set(material, new Set([prim]));
}
}
}
for (const [material, prims] of materialPrims) {
shiftTexCoords(material, Array.from(prims));
}
}

// Pruning animations is a bit more complicated:
Expand Down Expand Up @@ -273,3 +284,51 @@ function listRequiredSemantics(

return semantics;
}

/**
* Shifts texCoord indices on the given material and primitives assigned to
* that material, such that indices start at zero and ascend without gaps.
* Prior to calling this function, the implementation must ensure that:
* - All TEXCOORD_n attributes on these prims are used by the material.
* - Material does not require any unavailable TEXCOORD_n attributes.
*
* TEXCOORD_n attributes on morph targets are shifted alongside the parent
* prim, but gaps may remain in their semantic lists.
*/
function shiftTexCoords(material: Material, prims: Primitive[]) {
// Create map from srcTexCoord → dstTexCoord.
const textureInfoList = listTextureInfoByMaterial(material);
const texCoordSet = new Set(textureInfoList.map((info: TextureInfo) => info.getTexCoord()));
const texCoordList = Array.from(texCoordSet).sort();
const texCoordMap = new Map(texCoordList.map((texCoord, index) => [texCoord, index]));
const semanticMap = new Map(texCoordList.map((texCoord, index) => [`TEXCOORD_${texCoord}`, `TEXCOORD_${index}`]));

// Update material.
for (const textureInfo of textureInfoList) {
const texCoord = textureInfo.getTexCoord();
textureInfo.setTexCoord(texCoordMap.get(texCoord)!);
}

// Update prims.
for (const prim of prims) {
const semantics = prim
.listSemantics()
.filter((semantic) => semantic.startsWith('TEXCOORD_'))
.sort();
updatePrim(prim, semantics);
prim.listTargets().forEach((target) => updatePrim(target, semantics));
}

function updatePrim(prim: Primitive | PrimitiveTarget, srcSemantics: string[]) {
for (const srcSemantic of srcSemantics) {
const uv = prim.getAttribute(srcSemantic);
if (!uv) continue;

const dstSemantic = semanticMap.get(srcSemantic)!;
if (dstSemantic === srcSemantic) continue;

prim.setAttribute(dstSemantic, uv);
prim.setAttribute(srcSemantic, null);
}
}
}
27 changes: 24 additions & 3 deletions packages/functions/test/list-texture-info.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import test from 'ava';
import { Document } from '@gltf-transform/core';
import { listTextureInfo } from '@gltf-transform/functions';
import { KHRMaterialsSheen } from '@gltf-transform/extensions';
import { listTextureInfo, listTextureInfoByMaterial } from '@gltf-transform/functions';
import { KHRMaterialsSheen, KHRMaterialsVolume } from '@gltf-transform/extensions';

test('basic', (t) => {
test('listTextureInfo', (t) => {
const document = new Document();
const textureA = document.createTexture();
const textureB = document.createTexture();
Expand All @@ -29,3 +29,24 @@ test('basic', (t) => {
'texture B'
);
});

test('listTextureInfoByMaterial', (t) => {
const document = new Document();
const textureA = document.createTexture();
const textureB = document.createTexture();
const textureC = document.createTexture();
const volumeExtension = document.createExtension(KHRMaterialsVolume);
const volume = volumeExtension.createVolume().setThicknessTexture(textureC);
const material = document
.createMaterial()
.setBaseColorTexture(textureA)
.setNormalTexture(textureB)
.setExtension('KHR_materials_volume', volume);

const textureInfo = new Set(listTextureInfoByMaterial(material));

t.is(textureInfo.size, 3, 'finds TextureInfo x 3');
t.true(textureInfo.has(material.getBaseColorTextureInfo()), 'finds material.baseColorTextureInfo');
t.true(textureInfo.has(material.getNormalTextureInfo()), 'finds material.normalTextureInfo');
t.true(textureInfo.has(volume.getThicknessTextureInfo()), 'finds material.volume.thicknessTextureInfo');
});
60 changes: 59 additions & 1 deletion packages/functions/test/prune.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'ava';
import { Document, Logger, PropertyType } from '@gltf-transform/core';
import { Accessor, Document, Logger, PropertyType } from '@gltf-transform/core';
import { prune } from '@gltf-transform/functions';

const logger = new Logger(Logger.Verbosity.SILENT);
Expand Down Expand Up @@ -160,3 +160,61 @@ test('attributes', async (t) => {
'discards TANGENT, TEXCOORD_1'
);
});

test('attributes - texcoords', async (t) => {
const document = new Document().setLogger(logger);

// Material.
const texture1 = document.createTexture();
const texture3 = document.createTexture();
const material = document.createMaterial();
material.setBaseColorTexture(texture1).getBaseColorTextureInfo().setTexCoord(1);
material.setNormalTexture(texture3).getNormalTextureInfo().setTexCoord(3);

// Primitives.
const uvs: Accessor[] = [];
const primA = document
.createPrimitive()
.setMaterial(material)
.setAttribute('POSITION', document.createAccessor())
.setAttribute('TEXCOORD_0', (uvs[0] = document.createAccessor())) // unused
.setAttribute('TEXCOORD_1', (uvs[1] = document.createAccessor()))
.setAttribute('TEXCOORD_2', (uvs[2] = document.createAccessor())) // unused
.setAttribute('TEXCOORD_3', (uvs[3] = document.createAccessor()));
const primB = primA
.clone()
.setAttribute('TEXCOORD_4', (uvs[4] = document.createAccessor())) // unused
.setAttribute('TEXCOORD_5', (uvs[5] = document.createAccessor())); // unused
document.createMesh().addPrimitive(primA).addPrimitive(primB);

await document.transform(prune({ propertyTypes: [PropertyType.ACCESSOR] }));

t.deepEqual(
uvs.map((a) => a.isDisposed()),
[false, false, false, false, false, false],
'keeps all texcoords'
);

await document.transform(prune({ propertyTypes: [PropertyType.ACCESSOR], keepAttributes: false }));

t.deepEqual(
uvs.map((a) => a.isDisposed()),
[true, false, true, false, true, true],
'disposes TEXCOORD_0, TEXCOORD_2, TEXCOORD_4, and TEXCOORD_5'
);

t.true(primA.getAttribute('TEXCOORD_0') === uvs[1], 'primA.TEXCOORD_0');
t.true(primA.getAttribute('TEXCOORD_1') === uvs[3], 'primA.TEXCOORD_1');
t.true(primA.getAttribute('TEXCOORD_2') === null, 'primA.TEXCOORD_2 → null');
t.true(primA.getAttribute('TEXCOORD_3') === null, 'primA.TEXCOORD_3 → null');

t.true(primB.getAttribute('TEXCOORD_0') === uvs[1], 'primB.TEXCOORD_0');
t.true(primB.getAttribute('TEXCOORD_1') === uvs[3], 'primB.TEXCOORD_1');
t.true(primB.getAttribute('TEXCOORD_2') === null, 'primB.TEXCOORD_2 → null');
t.true(primB.getAttribute('TEXCOORD_3') === null, 'primB.TEXCOORD_3 → null');
t.true(primB.getAttribute('TEXCOORD_4') === null, 'primB.TEXCOORD_4 → null');
t.true(primB.getAttribute('TEXCOORD_5') === null, 'primB.TEXCOORD_5 → null');

t.is(material.getBaseColorTextureInfo().getTexCoord(), 0, 'material.baseColorTexture.texCoord = 0');
t.is(material.getNormalTextureInfo().getTexCoord(), 1, 'material.normalTexture.texCoord → 1');
});

0 comments on commit c95b94b

Please sign in to comment.