Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export { Graph, GraphEdge, type Ref, RefList, RefMap, RefSet } from 'property-graph';
export {
Graph,
GraphEdge,
type Ref,
RefList,
RefMap,
RefSet,
} from 'property-graph';
export {
type bbox,
ComponentTypeToTypedArray,
Expand All @@ -19,7 +26,14 @@ export {
} from './constants.js';
export { Document, type Transform, type TransformContext } from './document.js';
export { Extension } from './extension.js';
export { DenoIO, NodeIO, PlatformIO, ReaderContext, WebIO, WriterContext } from './io/index.js';
export {
DenoIO,
NodeIO,
PlatformIO,
ReaderContext,
WebIO,
WriterContext,
} from './io/index.js';
export type { JSONDocument } from './json-document.js';
export {
Accessor,
Expand Down Expand Up @@ -57,6 +71,7 @@ export {
type ImageUtilsFormat,
Logger,
MathUtils,
Triangle,
uuid,
Verbosity,
} from './utils/index.js';
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export * from './is-plain-object.js';
export * from './logger.js';
export * from './math-utils.js';
export * from './property-utils.js';
export * from './triangle.js';
export * from './uuid.js';
64 changes: 64 additions & 0 deletions packages/core/src/utils/triangle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { vec3 } from 'gl-matrix';

/**
* A geometric triangle as defined by three vectors representing its three corners.
*/
class Triangle {
a: vec3;
b: vec3;
c: vec3;
/**
* Constructs a new triangle.
*
* @param {vec3} [a=(0,0,0)] - The first corner of the triangle.
* @param {vec3} [b=(0,0,0)] - The second corner of the triangle.
* @param {vec3} [c=(0,0,0)] - The third corner of the triangle.
*/
constructor(a: vec3 = [0, 0, 0], b: vec3 = [0, 0, 0], c: vec3 = [0, 0, 0]) {
/**
* The first corner of the triangle.
*
* @type {vec3}
*/
this.a = a;

/**
* The second corner of the triangle.
*
* @type {vec3}
*/
this.b = b;

/**
* The third corner of the triangle.
*
* @type {vec3}
*/
this.c = c;
}

/**
* Computes the normal vector of a triangle.
*
* @param {vec3} a - The first corner of the triangle.
* @param {vec3} b - The second corner of the triangle.
* @param {vec3} c - The third corner of the triangle.
* @param {vec3} target - The target vector that is used to store the method's result.
* @return {vec3} The triangle's normal.
*/
static getNormal(a: vec3, b: vec3, c: vec3, target: vec3) {
vec3.subtract(target, c, b);
const _v0 = vec3.create();
vec3.subtract(_v0, a, b);
vec3.cross(target, target, _v0);

const targetLengthSq = vec3.squaredLength(target);
if (targetLengthSq > 0) {
return vec3.scale(target, target, 1 / Math.sqrt(targetLengthSq));
}

return (target = [0, 0, 0]);
}
}

export { Triangle };
129 changes: 129 additions & 0 deletions packages/functions/src/create-edge-primitive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Document, Primitive, Triangle, type vec3 } from '@gltf-transform/core';
import { vec3 as glvec3 } from 'gl-matrix';
import { dequantizeAttribute } from './dequantize.js';
import { VertexStream } from './hash-table.js';

const NAME = 'primitive-outline';

export interface PrimitiveOutlineOptions {
thresholdAngel: number;
}

export const PRIMITIVE_OUTLINE_DEFAULTS: Required<PrimitiveOutlineOptions> = {
thresholdAngel: 0.05,
};

export function createEdgePrimitive(prim: Primitive, thresholdRadians: number): Primitive {
const graph = prim.getGraph();
const document = Document.fromGraph(graph)!;
const positionAccessor = prim.getAttribute('POSITION');

if (
document
.getRoot()
.listExtensionsUsed()
.some((ext) => ext.extensionName === 'KHR_mesh_quantization')
) {
for (const semantic of ['POSITION', 'NORMAL', 'TANGENT']) {
const attribute = prim.getAttribute(semantic);
if (attribute) dequantizeAttribute(attribute);
}
}
const indices = prim.getIndices()?.getArray();
const indexCount = indices ? indices.length : positionAccessor?.getCount();
const precisionCount = 4;
const precision = Math.pow(10, precisionCount);

const indexArr = [0, 0, 0];
const vertKeys = ['a', 'b', 'c'];
const triangle = new Triangle();
const hashes = new Array(3);
const edgeData = {};
const vertices = [];
const vertexStream = new VertexStream(prim);

for (let i = 0; i < indexCount; i += 3) {
if (indices) {
indexArr[0] = indices[i * 3 + 0];
indexArr[1] = indices[i * 3 + 1];
indexArr[2] = indices[i * 3 + 2];
} else {
indexArr[0] = i * 3 + 0;
indexArr[1] = i * 3 + 1;
indexArr[2] = i * 3 + 2;
}
const a = [] as vec3;
positionAccessor.getElement(indexArr[0], a);
const b = [] as vec3;
positionAccessor.getElement(indexArr[1], b);
const c = [] as vec3;
positionAccessor.getElement(indexArr[2], c);
const _triangle = new Triangle(a, b, c);
const normal = [0, 0, 0] as vec3;
Triangle.getNormal(a, b, c, normal);

hashes[0] = vertexStream.hash(indexArr[0]);
hashes[1] = vertexStream.hash(indexArr[1]);
hashes[2] = vertexStream.hash(indexArr[2]);

// skip degenerate triangles
if (hashes[0] === hashes[1] || hashes[1] === hashes[2] || hashes[2] === hashes[0]) {
continue;
}

for (let j = 0; j < 3; j++) {
const jNext = (j + 1) % 3;
const vecHash0 = hashes[j];
const vecHash1 = hashes[jNext];
const v0 = _triangle[vertKeys[j]];
const v1 = _triangle[vertKeys[jNext]];
const hash = `${vecHash0}_${vecHash1}`;
const reverseHash = `${vecHash1}_${vecHash0}`;
if (reverseHash in edgeData && edgeData[reverseHash]) {
// if we found a sibling edge add it into the vertex array if
// it meets the angle threshold and delete the edge from the map.
if (glvec3.dot(normal, edgeData[reverseHash].normal) <= thresholdRadians) {
vertices.push(v0[0], v0[1], v0[2]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are converting vertices to Float32Array later, make a block-based allocator with typed array could make gc happy.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is OK as-is for now — it's a new feature and can be optimized later.

vertices.push(v1[0], v1[1], v1[2]);
}

edgeData[reverseHash] = null;
} else if (!(hash in edgeData)) {
// if we've already got an edge here then skip adding a new one
edgeData[hash] = {
index0: indexArr[j],
index1: indexArr[jNext],
normal: glvec3.clone(normal),
};
}
}
}

// iterate over all remaining, unmatched edges and add them to the vertex array
for (const key in edgeData) {
if (edgeData[key]) {
const { index0, index1 } = edgeData[key];
const v0 = [] as vec3;
positionAccessor.getElement(index0, v0);
const v1 = [] as vec3;
positionAccessor.getElement(index1, v1);

vertices.push(v0[0], v0[1], v0[2]);
vertices.push(v1[0], v1[1], v1[2]);
}
}

const accesor = document
.createAccessor()
.setArray(new Float32Array(vertices))
.setType('VEC3')
.setBuffer(positionAccessor.getBuffer());

const edgePrim = document
.createPrimitive()
.setName(prim.getName() + '_edges')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surprisingly, glTF primitives do not have names, so this line has no effect on export and can be removed. The .setName() method in glTF Transform exists only as inherited from the Property class.

.setMode(Primitive.Mode.LINES)
.setAttribute('POSITION', accesor);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps ideally, all vertex attributes from the source primitive would be preserved in the output, but I recognize that's a fairly tricky change. For example, so that edges of a skinned mesh can be animated along with the rest of the mesh. Feel free to skip it and we can include it as a later improvement!


return edgePrim;
}
6 changes: 5 additions & 1 deletion packages/functions/src/hash-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { deepListAttributes } from './utils.js';
export const EMPTY_U32: number = 2 ** 32 - 1;

export class VertexStream {
private attributes: { u8: Uint8Array; byteStride: number; paddedByteStride: number }[] = [];
private attributes: {
u8: Uint8Array;
byteStride: number;
paddedByteStride: number;
}[] = [];

/** Temporary vertex views in 4-byte-aligned memory. */
private u8: Uint8Array;
Expand Down
5 changes: 1 addition & 4 deletions packages/functions/src/quantize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,7 @@ function transformBatch(document: Document, batch: InstancedMesh, nodeTransform:

// biome-ignore format: Readability.
const instanceMatrix = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
] as mat4;

const transformMatrix = fromTransform(nodeTransform);
Expand Down
79 changes: 79 additions & 0 deletions packages/functions/test/create-edge-primitive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Document, type vec3 } from '@gltf-transform/core';
import { quantize } from '@gltf-transform/functions';
import { logger } from '@gltf-transform/test-utils';
import test from 'ava';
import { createEdgePrimitive } from '../src/create-edge-primitive';

async function createGeometries(doc: Document, vertices: vec3[], indices: number[], quantized: boolean = false) {
const node = doc.createNode('test-primitive-node');
const mesh = doc.createMesh('test-primitive-mesh');
const primitive = doc
.createPrimitive()
.setName('test-primitive')
.setAttribute(
'POSITION',
doc.createAccessor('test-primitive-positions').setType('VEC3').setArray(new Float32Array(vertices.flat())),
)
.setIndices(doc.createAccessor('test-primitive-indices').setType('SCALAR').setArray(new Uint32Array(indices)));
mesh.addPrimitive(primitive);
node.setMesh(mesh);
doc.createScene().addChild(node);
if (quantized) {
await doc.transform(
quantize({
quantizePosition: 8,
}),
);
}
}

const vertices: vec3[] = [
[0, 0, 0] as vec3,
[1, 0, 0] as vec3,
[1, 1, 0] as vec3,
[0, 1, 0] as vec3,
[1, 1, 1] as vec3,
];

const quantizeVertices: vec3[] = [
[-0.19211, -0.93457, 0.29946],
[-0.08526, -0.99393, 0.06957],
[0.82905, -0.39715, 0],
];

function testEdge(doc: Document, vertices: vec3[], indices: number[], expectedLineCount: number, t) {}

test('primitiveOutline_zero0', async (t) => {
const doc = new Document().setLogger(logger);
await testEdges(doc, vertices, [1, 1, 1], 0, false, t);
});

test('primitiveOutline_zero1', async (t) => {
const doc = new Document().setLogger(logger);
await testEdges(doc, vertices, [0, 0, 1], 0, false, t);
});

test('primitiveOutline_2', async (t) => {
const doc = new Document().setLogger(logger);
await testEdges(doc, vertices, [0, 1, 2], 3, false, t);
});

test('quanztized_primitive_edge', async (t) => {
const doc = new Document().setLogger(logger);
await testEdges(doc, quantizeVertices, [0, 1, 2], 3, true, t);
});

async function testEdges(doc: Document, vertices: vec3[], indices: number[], edgesCount: number, quantize: boolean, t) {
createGeometries(doc, vertices, indices, quantize);
const root = doc.getRoot();
if (quantize) {
t.deepEqual(
root.listExtensionsUsed().some((ext) => ext.extensionName === 'KHR_mesh_quantization'),
true,
'has quantization extension',
);
}
const edgePrimitive = createEdgePrimitive(root.listMeshes()[0].listPrimitives()[0], 0.05);
const lineCount = edgePrimitive.getAttribute('POSITION').getCount() / 2;
t.deepEqual(lineCount, edgesCount, 'count edge line');
}