diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts
index e901eba76..d1a53b749 100755
--- a/packages/cli/src/cli.ts
+++ b/packages/cli/src/cli.ts
@@ -6,7 +6,7 @@ import { gzip } from 'node-gzip';
import { program } from '@caporal/core';
import { Logger, NodeIO, PropertyType, VertexLayout, vec2 } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
-import { CenterOptions, InstanceOptions, PartitionOptions, PruneOptions, QUANTIZE_DEFAULTS, ResampleOptions, SequenceOptions, TEXTURE_RESIZE_DEFAULTS, TextureResizeFilter, UnweldOptions, WeldOptions, center, dedup, instance, metalRough, partition, prune, quantize, resample, sequence, tangents, textureResize, unweld, weld, reorder, dequantize, oxipng, mozjpeg, webp, unlit, meshopt, DRACO_DEFAULTS, draco, DracoOptions, simplify, SimplifyOptions, SIMPLIFY_DEFAULTS } from '@gltf-transform/functions';
+import { CenterOptions, InstanceOptions, PartitionOptions, PruneOptions, QUANTIZE_DEFAULTS, ResampleOptions, SequenceOptions, TEXTURE_RESIZE_DEFAULTS, TextureResizeFilter, UnweldOptions, WeldOptions, center, dedup, instance, metalRough, partition, prune, quantize, resample, sequence, tangents, textureResize, unweld, weld, reorder, dequantize, oxipng, mozjpeg, webp, unlit, meshopt, DRACO_DEFAULTS, draco, DracoOptions, simplify, SimplifyOptions, SIMPLIFY_DEFAULTS, WELD_DEFAULTS } from '@gltf-transform/functions';
import { InspectFormat, inspect } from './inspect';
import { ETC1S_DEFAULTS, Filter, Mode, UASTC_DEFAULTS, ktxfix, merge, toktx, XMPOptions, xmp } from './transforms';
import { formatBytes, MICROMATCH_OPTIONS, underline } from './util';
@@ -543,14 +543,20 @@ program
.help(`
Index geometry and optionally merge similar vertices. When merged and indexed,
data is shared more efficiently between vertices. File size can be reduced, and
-the GPU can sometimes use the vertex cache more efficiently. With --tolerance=0,
-geometry is indexed in place, without merging.
+the GPU can sometimes use the vertex cache more efficiently.
+
+When welding, the --tolerance threshold determines which vertices qualify for
+welding based on distance between the vertices as a fraction of the primitive's
+bounding box (AABB). For example, --tolerance=0.01 welds vertices within +/-1%
+of the AABB's longest dimension. Other vertex attributes are also compared
+during welding, with attribute-specific thresholds. For --tolerance=0, geometry
+is indexed in place, without merging.
`.trim())
.argument(' ', INPUT_DESC)
.argument('', OUTPUT_DESC)
- .option('--tolerance', 'Per-attribute tolerance to merge similar vertices', {
+ .option('--tolerance', 'Tolerance for vertex welding', {
validator: program.NUMBER,
- default: 1e-4,
+ default: WELD_DEFAULTS.tolerance,
})
.action(({args, options, logger}) =>
Session.create(io, logger, args.input, args.output)
diff --git a/packages/functions/src/utils.ts b/packages/functions/src/utils.ts
index 8e9925ca3..9fceec6b5 100644
--- a/packages/functions/src/utils.ts
+++ b/packages/functions/src/utils.ts
@@ -157,3 +157,9 @@ export function remapAttribute(attribute: Accessor, remap: Uint32Array, dstCount
attribute.setArray(dstArray);
}
+
+export function createIndices(count: number, maxIndex = count): Uint16Array | Uint32Array {
+ const array = maxIndex <= 65534 ? new Uint16Array(count) : new Uint32Array(count);
+ for (let i = 0; i < array.length; i++) array[i] = i;
+ return array;
+}
diff --git a/packages/functions/src/weld.ts b/packages/functions/src/weld.ts
index ef9440dca..303c3c651 100644
--- a/packages/functions/src/weld.ts
+++ b/packages/functions/src/weld.ts
@@ -7,28 +7,64 @@ import {
Transform,
TransformContext,
TypedArray,
+ vec3,
} from '@gltf-transform/core';
import { dedup } from './dedup';
-import { createTransform, formatDeltaOp, isTransformPending } from './utils';
+import { createIndices, createTransform, formatDeltaOp, isTransformPending } from './utils';
const NAME = 'weld';
+const Tolerance = {
+ DEFAULT: 0.0001,
+ TEXCOORD: 0.0001, // [0, 1]
+ COLOR: 0.01, // [0, 1]
+ NORMAL: 0.01, // [-1, 1]
+ JOINTS: 0.0, // [0, ∞]
+ WEIGHTS: 0.01, // [0, ∞]
+};
+
/** Options for the {@link weld} function. */
export interface WeldOptions {
- /** Per-attribute tolerance used when merging similar vertices. */
+ /** Tolerance, as a fraction of primitive AABB, used when merging similar vertices. */
tolerance?: number;
/** Whether to overwrite existing indices. */
overwrite?: boolean;
}
-const WELD_DEFAULTS: Required = { tolerance: 1e-4, overwrite: true };
+export const WELD_DEFAULTS: Required = {
+ tolerance: Tolerance.DEFAULT,
+ overwrite: true,
+};
/**
- * Index {@link Primitive}s and (optionally) merge similar vertices.
+ * Index {@link Primitive}s and (optionally) merge similar vertices. When merged
+ * and indexed, data is shared more efficiently between vertices. File size can
+ * be reduced, and the GPU can sometimes use the vertex cache more efficiently.
+ *
+ * When welding, the 'tolerance' threshold determines which vertices qualify for
+ * welding based on distance between the vertices as a fraction of the primitive's
+ * bounding box (AABB). For example, tolerance=0.01 welds vertices within +/-1%
+ * of the AABB's longest dimension. Other vertex attributes are also compared
+ * during welding, with attribute-specific thresholds. For --tolerance=0, geometry
+ * is indexed in place, without merging.
+ *
+ * Example:
+ *
+ * ```js
+ * import { weld } from '@gltf-transform/functions';
+ *
+ * await document.transform(
+ * weld({ tolerance: 0.001 })
+ * );
+ * ```
*/
export function weld(_options: WeldOptions = WELD_DEFAULTS): Transform {
const options = { ...WELD_DEFAULTS, ..._options } as Required;
+ if (options.tolerance > 0.1 || options.tolerance < 0) {
+ throw new Error(`${NAME}: Requires 0 ≤ tolerance ≤ 0.1`);
+ }
+
return createTransform(NAME, async (doc: Document, context?: TransformContext): Promise => {
const logger = doc.getLogger();
@@ -68,93 +104,110 @@ function weldOnly(doc: Document, prim: Primitive): void {
prim.setIndices(indices);
}
-/**
- * Weld and merge, combining vertices that are similar on all vertex attributes. Morph target
- * attributes are not considered when scoring vertex similarity, but are retained when merging.
- */
+/** Weld and merge, combining vertices that are similar on all vertex attributes. */
function weldAndMerge(doc: Document, prim: Primitive, options: Required): void {
const logger = doc.getLogger();
+
+ const srcPosition = prim.getAttribute('POSITION')!;
+ const srcIndices = prim.getIndices() || doc.createAccessor().setArray(createIndices(srcPosition.getCount()));
+ const uniqueIndices = new Uint32Array(new Set(srcIndices.getArray()!));
+
+ // (1) Compute per-attribute tolerances, pre-sort vertices.
+
const tolerance = Math.max(options.tolerance, Number.EPSILON);
- const decimalShift = Math.log10(1 / tolerance);
- const shiftFactor = Math.pow(10, decimalShift);
-
- const hashToIndex: { [key: string]: number } = {};
- const srcIndices = prim.getIndices();
- const vertexCount = srcIndices ? srcIndices.getCount() : prim.listAttributes()[0].getCount();
-
- // Prepare storage for new elements of each attribute.
- const dstAttributes = new Map();
- prim.listAttributes().forEach((attr) => dstAttributes.set(attr, []));
- prim.listTargets().forEach((target) => {
- target.listAttributes().forEach((attr) => dstAttributes.set(attr, []));
+ const attributeTolerance: Record = {};
+ for (const semantic of prim.listSemantics()) {
+ const attribute = prim.getAttribute(semantic)!;
+ attributeTolerance[semantic] = getAttributeTolerance(semantic, attribute, tolerance);
+ }
+
+ logger.debug(`${NAME}: Tolerance thresholds: ${formatKV(attributeTolerance)}`);
+
+ const posA: vec3 = [0, 0, 0];
+ const posB: vec3 = [0, 0, 0];
+
+ uniqueIndices.sort((a, b) => {
+ srcPosition.getElement(a, posA);
+ srcPosition.getElement(b, posB);
+ return posA[0] > posB[0] ? 1 : -1;
});
- const dstIndicesArray = [];
- let nextIndex = 0;
+ // (2) Compare and identify vertices to weld. Use sort to keep iterations below O(n²),
- // For each vertex, compute a hash based on its tolerance and merge with any sufficiently
- // similar vertices.
- for (let i = 0; i < vertexCount; i++) {
- const index = srcIndices ? srcIndices.getScalar(i) : i;
+ const weldMap = createIndices(uniqueIndices.length); // oldIndex → oldCommonIndex
+ const writeMap = createIndices(uniqueIndices.length); // oldIndex → newIndex
- const hashElements: number[] = [];
- const el: number[] = [];
- for (const attribute of prim.listAttributes()) {
- for (let j = 0; j < attribute.getElementSize(); j++) {
- hashElements.push(~~(attribute.getElement(index, el)[j] * shiftFactor));
- }
- }
+ const srcVertexCount = srcPosition.getCount();
+ let dstVertexCount = 0;
+ let backIters = 0;
- const hash = hashElements.join('|');
- if (hash in hashToIndex) {
- dstIndicesArray.push(hashToIndex[hash]);
- } else {
- for (const attr of prim.listAttributes()) {
- dstAttributes.get(attr)!.push(attr.getElement(index, []));
+ for (let i = 0; i < uniqueIndices.length; i++) {
+ const a = uniqueIndices[i];
+
+ for (let j = i - 1; j >= 0; j--) {
+ const b = weldMap[uniqueIndices[j]];
+
+ srcPosition.getElement(a, posA);
+ srcPosition.getElement(b, posB);
+
+ // Sort order allows early exit on X-axis distance.
+ if (Math.abs(posA[0] - posB[0]) > attributeTolerance['POSITION']) {
+ break;
}
- for (const target of prim.listTargets()) {
- for (const attr of target.listAttributes()) {
- dstAttributes.get(attr)!.push(attr.getElement(index, []));
- }
+
+ backIters++;
+
+ // Weld if base attributes and morph target attributes match.
+ const isBaseMatch = prim.listSemantics().every((semantic) => {
+ const attribute = prim.getAttribute(semantic)!;
+ const tolerance = attributeTolerance[semantic];
+ return compareAttributes(attribute, a, b, tolerance, semantic);
+ });
+ const isTargetMatch = prim.listTargets().every((target) => {
+ return target.listSemantics().every((semantic) => {
+ const attribute = prim.getAttribute(semantic)!;
+ const tolerance = attributeTolerance[semantic];
+ return compareAttributes(attribute, a, b, tolerance, semantic);
+ });
+ });
+
+ if (isBaseMatch && isTargetMatch) {
+ weldMap[a] = b;
+ break;
}
+ }
- hashToIndex[hash] = nextIndex;
- dstIndicesArray.push(nextIndex);
- nextIndex++;
+ // Output the vertex if we didn't find a match, else record the index of the match.
+ if (weldMap[a] === a) {
+ writeMap[a] = dstVertexCount++; // note: reorders the primitive on x-axis sort.
+ } else {
+ writeMap[a] = writeMap[weldMap[a]];
}
}
- const srcVertexCount = prim.listAttributes()[0].getCount();
- const dstVertexCount = dstAttributes.get(prim.getAttribute('POSITION')!)!.length;
+ logger.debug(`${NAME}: Iterations per vertex: ${Math.round(backIters / uniqueIndices.length)} (avg)`);
logger.debug(`${NAME}: ${formatDeltaOp(srcVertexCount, dstVertexCount)} vertices.`);
- // Update the primitive.
- for (const srcAttr of prim.listAttributes()) {
- swapAttributes(prim, srcAttr, dstAttributes.get(srcAttr)!);
+ // (3) Update indices.
- // Clean up.
- if (srcAttr.listParents().length === 1) srcAttr.dispose();
+ const dstIndicesCount = srcIndices.getCount(); // # primitives does not change.
+ const dstIndicesArray = createIndices(dstIndicesCount, uniqueIndices.length);
+ for (let i = 0; i < dstIndicesCount; i++) {
+ dstIndicesArray[i] = writeMap[srcIndices.getScalar(i)];
+ }
+ prim.setIndices(srcIndices.clone().setArray(dstIndicesArray));
+ if (srcIndices.listParents().length === 1) srcIndices.dispose();
+
+ // (4) Update vertex attributes.
+
+ for (const srcAttr of prim.listAttributes()) {
+ swapAttributes(prim, srcAttr, writeMap, dstVertexCount);
}
for (const target of prim.listTargets()) {
for (const srcAttr of target.listAttributes()) {
- swapAttributes(target, srcAttr, dstAttributes.get(srcAttr)!);
-
- // Clean up.
- if (srcAttr.listParents().length === 1) srcAttr.dispose();
+ swapAttributes(target, srcAttr, writeMap, dstVertexCount);
}
}
- if (srcIndices) {
- const dstIndicesTypedArray = createArrayOfType(srcIndices.getArray()!, dstIndicesArray.length);
- dstIndicesTypedArray.set(dstIndicesArray);
- prim.setIndices(srcIndices.clone().setArray(dstIndicesTypedArray));
-
- // Clean up.
- if (srcIndices.listParents().length === 1) srcIndices.dispose();
- } else {
- const indicesArray =
- srcVertexCount <= 65534 ? new Uint16Array(dstIndicesArray) : new Uint32Array(dstIndicesArray);
- prim.setIndices(doc.createAccessor().setArray(indicesArray));
- }
}
/** Creates a new TypedArray of the same type as an original, with a new length. */
@@ -164,14 +217,63 @@ function createArrayOfType(array: T, length: number): T {
}
/** Replaces an {@link Attribute}, creating a new one with the given elements. */
-function swapAttributes(parent: Primitive | PrimitiveTarget, srcAttr: Accessor, dstAttrElements: number[][]): void {
- const dstAttrArrayLength = dstAttrElements.length * srcAttr.getElementSize();
- const dstAttrArray = createArrayOfType(srcAttr.getArray()!, dstAttrArrayLength);
+function swapAttributes(
+ parent: Primitive | PrimitiveTarget,
+ srcAttr: Accessor,
+ reorder: Uint32Array | Uint16Array,
+ dstCount: number
+): void {
+ const dstAttrArray = createArrayOfType(srcAttr.getArray()!, dstCount * srcAttr.getElementSize());
const dstAttr = srcAttr.clone().setArray(dstAttrArray);
+ const done = new Uint8Array(dstCount);
- for (let i = 0; i < dstAttrElements.length; i++) {
- dstAttr.setElement(i, dstAttrElements[i]);
+ for (let i = 0, el = [] as number[]; i < reorder.length; i++) {
+ if (!done[reorder[i]]) {
+ dstAttr.setElement(reorder[i], srcAttr.getElement(i, el));
+ done[reorder[i]] = 1;
+ }
}
parent.swap(srcAttr, dstAttr);
+
+ // Clean up.
+ if (srcAttr.listParents().length === 1) srcAttr.dispose();
+}
+
+const _a = [] as number[];
+const _b = [] as number[];
+
+/** Computes a per-attribute tolerance, based on domain and usage of the attribute. */
+function getAttributeTolerance(semantic: string, attribute: Accessor, tolerance: number): number {
+ // Attributes like NORMAL and COLOR_# do not vary in range like POSITION,
+ // so do not apply the given tolerance factor to these attributes.
+ if (semantic === 'NORMAL' || semantic === 'TANGENT') return Tolerance.NORMAL;
+ if (semantic.startsWith('COLOR_')) return Tolerance.COLOR;
+ if (semantic.startsWith('TEXCOORD_')) return Tolerance.TEXCOORD;
+ if (semantic.startsWith('JOINTS_')) return Tolerance.JOINTS;
+ if (semantic.startsWith('WEIGHTS_')) return Tolerance.WEIGHTS;
+
+ _a.length = _b.length = 0;
+ attribute.getMinNormalized(_a);
+ attribute.getMaxNormalized(_b);
+ const range = Math.max(..._b) - Math.min(..._a) || 1;
+ return tolerance * range;
+}
+
+/** Compares two vertex attributes against a tolerance threshold. */
+function compareAttributes(attribute: Accessor, a: number, b: number, tolerance: number, _semantic: string): boolean {
+ attribute.getElement(a, _a);
+ attribute.getElement(b, _b);
+ for (let i = 0, il = attribute.getElementSize(); i < il; i++) {
+ if (Math.abs(_a[i] - _b[i]) > tolerance) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function formatKV(kv: Record): string {
+ return Object.entries(kv)
+ .map(([k, v]) => `${k}=${v}`)
+ .join(', ');
}
diff --git a/packages/functions/test/weld.test.ts b/packages/functions/test/weld.test.ts
index 5a156045e..0ad851278 100644
--- a/packages/functions/test/weld.test.ts
+++ b/packages/functions/test/weld.test.ts
@@ -3,13 +3,15 @@ require('source-map-support').install();
import test from 'tape';
import fs from 'fs/promises';
import path from 'path';
-import { Accessor, Document, Primitive } from '@gltf-transform/core';
+import { Accessor, Document, Logger, Primitive } from '@gltf-transform/core';
import { weld } from '../';
+const LOGGER = new Logger(Logger.Verbosity.SILENT);
+
test('@gltf-transform/functions::weld | tolerance=0', async (t) => {
- const doc = new Document();
+ const doc = new Document().setLogger(LOGGER);
const positionArray = new Float32Array([0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, -1]);
- const position = doc.createAccessor().setType(Accessor.Type.VEC3).setArray(positionArray);
+ const position = doc.createAccessor().setType('VEC3').setArray(positionArray);
const indices = doc.createAccessor().setArray(new Uint32Array([3, 4, 5, 0, 1, 2]));
const prim1 = doc.createPrimitive().setAttribute('POSITION', position).setMode(Primitive.Mode.TRIANGLES);
const prim2 = doc
@@ -29,45 +31,176 @@ test('@gltf-transform/functions::weld | tolerance=0', async (t) => {
});
test('@gltf-transform/functions::weld | tolerance>0', async (t) => {
- const doc = new Document();
- const positionArray = new Float32Array([0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, -1]);
- const positionTargetArray = new Float32Array([0, 10, 0, 0, 10, 1, 0, 10, -1, 0, 15, 0, 0, 15, 1, 0, 15, -1]);
- const position = doc.createAccessor().setType(Accessor.Type.VEC3).setArray(positionArray);
- const positionTarget = doc.createAccessor().setType(Accessor.Type.VEC3).setArray(positionTargetArray);
-
- const prim1 = doc.createPrimitive().setAttribute('POSITION', position).setMode(Primitive.Mode.TRIANGLES);
+ const doc = new Document().setLogger(LOGGER);
+ // prettier-ignore
+ const positionArray = new Float32Array([
+ 0, 0, 0,
+ 0, 0, 1,
+ 0, 0, -1,
+ 0, 0, 0,
+ 0, 0, 1,
+ 0, 0, -1
+ ]);
+ // prettier-ignore
+ const normalArray = new Float32Array([
+ 0, 0, 1,
+ 0, 0.5, 0.5,
+ 0.5, 0.5, 0,
+ 0, 0, 1,
+ 0, 0.5, 0.50001, // should still be welded
+ 0.5, 0.5, 0
+ ]);
+ // prettier-ignore
+ const positionTargetArray = new Float32Array([
+ 0, 10, 0,
+ 0, 10, 1,
+ 0, 10, -1,
+ 0, 15, 0,
+ 0, 15, 1,
+ 0, 15, -1
+ ]);
+ const position = doc.createAccessor().setType('VEC3').setArray(positionArray);
+ const normal = doc.createAccessor().setType('VEC3').setArray(normalArray);
+ const positionTarget = doc.createAccessor().setType('VEC3').setArray(positionTargetArray);
+
+ const prim1 = doc
+ .createPrimitive()
+ .setMode(Primitive.Mode.TRIANGLES)
+ .setAttribute('POSITION', position)
+ .setAttribute('NORMAL', normal);
const prim2Indices = doc.createAccessor().setArray(new Uint32Array([3, 4, 5, 0, 1, 2]));
const prim2Target = doc.createPrimitiveTarget().setAttribute('POSITION', positionTarget);
const prim2 = doc
.createPrimitive()
+ .setMode(Primitive.Mode.TRIANGLES)
.setIndices(prim2Indices)
.setAttribute('POSITION', position)
- .setMode(Primitive.Mode.TRIANGLES)
+ .setAttribute('NORMAL', normal)
.addTarget(prim2Target);
doc.createMesh().addPrimitive(prim1).addPrimitive(prim2);
- await doc.transform(weld({ tolerance: 1e-8 }));
+ await doc.transform(weld());
t.deepEquals(prim1.getIndices().getArray(), new Uint16Array([0, 1, 2, 0, 1, 2]), 'indices on prim1');
- t.deepEquals(prim2.getIndices().getArray(), new Uint32Array([0, 1, 2, 0, 1, 2]), 'indices on prim2');
+ t.deepEquals(prim2.getIndices().getArray(), new Uint16Array([0, 1, 2, 0, 1, 2]), 'indices on prim2');
t.deepEquals(prim1.getAttribute('POSITION').getArray(), positionArray.slice(0, 9), 'vertices on prim1');
t.deepEquals(prim2.getAttribute('POSITION').getArray(), positionArray.slice(0, 9), 'vertices on prim2');
t.deepEquals(
prim2.listTargets()[0].getAttribute('POSITION').getArray(),
- positionTargetArray.slice(9, 18), // Uses later targets, because of index order.
+ positionTargetArray.slice(0, 9), // Uses later targets, because of index order.
'morph targets on prim2'
);
t.equals(doc.getRoot().listAccessors().length, 4, 'keeps only needed accessors');
t.end();
});
+test('@gltf-transform/functions::weld | attributes', async (t) => {
+ const doc = new Document().setLogger(LOGGER);
+ // prettier-ignore
+ const positionArray = new Uint8Array([
+ 0, 0, 0, // ⎡
+ 0, 0, 0, // ⎢ all match, weld 3
+ 0, 0, 0, // ⎣
+ 1, 0, 0, // ⎡
+ 1, 0, 0, // ⎢ normals differ, weld 2
+ 1, 0, 0, // ⎣ ❌
+ 0, 1, 1, // ⎡ ❌
+ 0, 1, 1, // ⎢ colors differ, weld 2
+ 0, 1, 1, // ⎣
+ ]);
+ // prettier-ignore
+ const normalArray = new Uint8Array([
+ 127, 127, 0,
+ 127, 127, 0,
+ 127, 127, 0,
+ 0, 127, 126,
+ 0, 128, 127,
+ 0, 150, 127, // ❌
+ 127, 0, 127,
+ 127, 0, 127,
+ 127, 0, 127,
+ ]);
+ // prettier-ignore
+ const colorArray = new Uint8Array([
+ 255, 0, 0, 1,
+ 255, 0, 0, 1,
+ 255, 0, 0, 1,
+ 0, 255, 0, 1,
+ 0, 255, 0, 1,
+ 0, 255, 0, 1,
+ 0, 0, 200, 1, // ❌
+ 0, 0, 255, 1,
+ 0, 0, 255, 1,
+ ]);
+ const position = doc.createAccessor().setType('VEC3').setArray(positionArray);
+ const normal = doc.createAccessor().setType('VEC3').setArray(normalArray).setNormalized(true);
+ const color = doc.createAccessor().setType('VEC4').setArray(colorArray).setNormalized(true);
+ const prim = doc
+ .createPrimitive()
+ .setMode(Primitive.Mode.TRIANGLES)
+ .setAttribute('POSITION', position)
+ .setAttribute('NORMAL', normal)
+ .setAttribute('COLOR_0', color);
+ doc.createMesh().addPrimitive(prim);
+
+ await doc.transform(weld({ tolerance: 0.0001 }));
+
+ // prettier-ignore
+ t.deepEquals(
+ Array.from(prim.getIndices()!.getArray()!),
+ [
+ 0, 0, 0,
+ 3, 3, 4,
+ 1, 2, 2,
+ ],
+ 'indices'
+ );
+ // prettier-ignore
+ t.deepEquals(
+ Array.from(prim.getAttribute('POSITION')!.getArray()!),
+ [
+ 0, 0, 0,
+ 0, 1, 1,
+ 0, 1, 1,
+ 1, 0, 0,
+ 1, 0, 0,
+ ],
+ 'position'
+ );
+ // prettier-ignore
+ t.deepEquals(
+ Array.from(prim.getAttribute('NORMAL')!.getArray()!),
+ [
+ 127, 127, 0,
+ 127, 0, 127,
+ 127, 0, 127,
+ 0, 127, 126,
+ 0, 150, 127,
+ ],
+ 'normal'
+ );
+ // prettier-ignore
+ t.deepEquals(
+ Array.from(prim.getAttribute('COLOR_0')!.getArray()!),
+ [
+ 255, 0, 0, 1,
+ 0, 0, 200, 1,
+ 0, 0, 255, 1,
+ 0, 255, 0, 1,
+ 0, 255, 0, 1,
+ ],
+ 'color'
+ );
+ t.end();
+});
+
test('@gltf-transform/functions::weld | u16 vs u32', async (t) => {
- const doc = new Document();
+ const doc = new Document().setLogger(LOGGER);
const smArray = new Float32Array(65534 * 3);
const lgArray = new Float32Array(65535 * 3);
- const smPosition = doc.createAccessor().setType(Accessor.Type.VEC3).setArray(smArray);
- const lgPosition = doc.createAccessor().setType(Accessor.Type.VEC3).setArray(lgArray);
+ const smPosition = doc.createAccessor().setType('VEC3').setArray(smArray);
+ const lgPosition = doc.createAccessor().setType('VEC3').setArray(lgArray);
const smPrim = doc.createPrimitive().setAttribute('POSITION', smPosition).setMode(Primitive.Mode.TRIANGLES);
const lgPrim = doc.createPrimitive().setAttribute('POSITION', lgPosition).setMode(Primitive.Mode.TRIANGLES);
doc.createMesh().addPrimitive(smPrim).addPrimitive(lgPrim);
@@ -88,7 +221,7 @@ test('@gltf-transform/functions::weld | modes', async (t) => {
for (let i = 0; i < dataset.length; i++) {
const primDef = dataset[i];
- const document = new Document();
+ const document = new Document().setLogger(LOGGER);
const position = document
.createAccessor()
.setArray(new Float32Array(primDef.attributes.POSITION))