-
-
Notifications
You must be signed in to change notification settings - Fork 192
feat: primive-outline #1720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: primive-outline #1720
Changes from all commits
7ad3a7a
49b3524
3ba4f8c
e7ba7d5
50b2d65
294ef8e
6a65953
38540f4
79e280e
8953c1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 }; |
| 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(); | ||
WallanceLee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you are converting
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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') | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| .setMode(Primitive.Mode.LINES) | ||
| .setAttribute('POSITION', accesor); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| 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'); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.