@flighthq/mesh 0.3.0 → 0.3.1-next.1041.35b950c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/meshGeometry.d.ts +1 -0
  6. package/dist/meshGeometry.d.ts.map +1 -1
  7. package/dist/meshGeometry.js +17 -8
  8. package/dist/meshGeometry.js.map +1 -1
  9. package/dist/meshGeometryAttributes.d.ts.map +1 -1
  10. package/dist/meshGeometryAttributes.js +47 -87
  11. package/dist/meshGeometryAttributes.js.map +1 -1
  12. package/dist/meshGeometryBuilders.d.ts.map +1 -1
  13. package/dist/meshGeometryBuilders.js +152 -134
  14. package/dist/meshGeometryBuilders.js.map +1 -1
  15. package/dist/meshGeometryCompute.d.ts +3 -2
  16. package/dist/meshGeometryCompute.d.ts.map +1 -1
  17. package/dist/meshGeometryCompute.js +283 -294
  18. package/dist/meshGeometryCompute.js.map +1 -1
  19. package/dist/meshGeometryIndex.js +4 -4
  20. package/dist/meshGeometryIndex.js.map +1 -1
  21. package/dist/meshGeometryLayout.d.ts.map +1 -1
  22. package/dist/meshGeometryLayout.js +2 -67
  23. package/dist/meshGeometryLayout.js.map +1 -1
  24. package/dist/meshGeometryOperations.d.ts.map +1 -1
  25. package/dist/meshGeometryOperations.js +93 -7
  26. package/dist/meshGeometryOperations.js.map +1 -1
  27. package/dist/meshGeometryTransforms.d.ts.map +1 -1
  28. package/dist/meshGeometryTransforms.js +90 -19
  29. package/dist/meshGeometryTransforms.js.map +1 -1
  30. package/dist/meshGeometryUvs.d.ts +0 -1
  31. package/dist/meshGeometryUvs.d.ts.map +1 -1
  32. package/dist/meshGeometryUvs.js +10 -18
  33. package/dist/meshGeometryUvs.js.map +1 -1
  34. package/dist/vertexFormat.d.ts +6 -0
  35. package/dist/vertexFormat.d.ts.map +1 -0
  36. package/dist/vertexFormat.js +61 -0
  37. package/dist/vertexFormat.js.map +1 -0
  38. package/package.json +4 -4
  39. package/src/meshGeometry.test.ts +23 -22
  40. package/src/meshGeometryAttributes.test.ts +37 -0
  41. package/src/meshGeometryBuilders.test.ts +333 -10
  42. package/src/meshGeometryCompute.test.ts +246 -95
  43. package/src/meshGeometryDeformationClone.test.ts +2 -0
  44. package/src/meshGeometryLayout.test.ts +1 -12
  45. package/src/meshGeometryOperations.test.ts +98 -0
  46. package/src/meshGeometryTransforms.test.ts +103 -1
  47. package/src/meshGeometryUvs.test.ts +1 -40
  48. package/src/vertexFormat.test.ts +38 -0
@@ -1,11 +1,12 @@
1
1
  import { createAabb } from '@flighthq/geometry/contract';
2
2
  import { EntityRuntimeKey } from '@flighthq/types/contract';
3
+ import { getMeshGeometryTriangleCount, getMeshGeometryTriangleVertexIndices } from './meshGeometryOperations';
3
4
  // Per-vertex compute over the canonical interleaved PBR record: position(3) + normal(3) +
4
5
  // tangent(4) + uv0(2) = 12 floats / 48 bytes, stride read from geometry.layout. These functions
5
6
  // derive normals, tangents, and the local-space AABB from `geometry.vertices` (+ `geometry.indices`)
6
7
  // and write into `out`. They read every input field they need into locals before writing, so each
7
8
  // is safe when `out` aliases `geometry`. Normals write back in place; tangents do too unless an
8
- // indexed UV-frame seam requires complete interleaved vertex records to be duplicated first.
9
+ // indexed mirrored-UV seam requires complete interleaved vertex records to be duplicated first.
9
10
  // Writes the bounding sphere of all vertex positions into `out`. Uses the AABB midpoint as the
10
11
  // center (fast, not minimal) and max-distance from that center as the radius. An empty vertex
11
12
  // stream yields center = (0,0,0) and a negative radius (empty convention). The radius is always
@@ -102,13 +103,17 @@ export function computeMeshGeometryBounds(out, geometry) {
102
103
  export function computeMeshGeometryFlatNormals(out, geometry) {
103
104
  const srcVerts = geometry.vertices;
104
105
  const floatsPerVertex = geometry.layout.stride / 4;
105
- const indices = geometry.indices;
106
- const indexCount = indices ? indices.length : floatsPerVertex > 0 ? Math.floor(srcVerts.length / floatsPerVertex) : 0;
107
106
  const dstVerts = out.vertices;
108
- for (let t = 0; t + 2 < indexCount; t += 3) {
109
- const i0 = indices ? indices[t] : t;
110
- const i1 = indices ? indices[t + 1] : t + 1;
111
- const i2 = indices ? indices[t + 2] : t + 2;
107
+ // Decode through the shared triangle walker so triangle-strip topology is honoured; hand-decoding
108
+ // index triples silently drops every strip triangle after the first.
109
+ const triangleCount = getMeshGeometryTriangleCount(geometry);
110
+ const corner = { i0: 0, i1: 0, i2: 0 };
111
+ for (let triangle = 0; triangle < triangleCount; triangle++) {
112
+ if (!getMeshGeometryTriangleVertexIndices(corner, geometry, triangle))
113
+ continue;
114
+ const i0 = corner.i0;
115
+ const i1 = corner.i1;
116
+ const i2 = corner.i2;
112
117
  const p0 = i0 * floatsPerVertex + POSITION_OFFSET;
113
118
  const p1 = i1 * floatsPerVertex + POSITION_OFFSET;
114
119
  const p2 = i2 * floatsPerVertex + POSITION_OFFSET;
@@ -146,26 +151,25 @@ export function computeMeshGeometryFlatNormals(out, geometry) {
146
151
  // Recomputes per-vertex smooth normals by area-weighted accumulation of triangle face normals
147
152
  // (right-handed, CCW front-face), normalizes them, and writes them into the normal slot of
148
153
  // `out.vertices`. Operates on indexed triangle-list geometry; non-indexed streams are treated as
149
- // sequential triangles. `out` is normally `geometry` itself (in-place), which is safe: positions
150
- // are only read and normals are accumulated in a scratch buffer before any write-back.
151
- export function computeMeshGeometryNormals(out, geometry) {
154
+ // sequential triangles. When `positionGroups` is present, it must contain one canonical vertex
155
+ // index per vertex; face normals accumulate into those groups before normalization and every member
156
+ // reads back from its group. The opt-in map avoids smoothing unrelated surfaces that merely happen
157
+ // to meet at the same position. `out` is normally `geometry` itself (in-place), which is safe:
158
+ // positions are only read and normals are accumulated in a scratch buffer before any write-back.
159
+ export function computeMeshGeometryNormals(out, geometry, positionGroups = null) {
152
160
  const vertices = geometry.vertices;
153
161
  const floatsPerVertex = geometry.layout.stride / 4;
154
162
  const vertexCount = floatsPerVertex > 0 ? Math.floor(vertices.length / floatsPerVertex) : 0;
155
- const indices = geometry.indices;
156
- const indexCount = indices ? indices.length : vertexCount;
157
- const runtime = geometry[EntityRuntimeKey];
158
- const recordedSmoothingSources = runtime?.tangentSmoothingSources;
159
- const smoothingSources = recordedSmoothingSources !== null &&
160
- recordedSmoothingSources !== undefined &&
161
- recordedSmoothingSources.length === vertexCount
162
- ? recordedSmoothingSources
163
- : null;
164
163
  const accum = new Float64Array(vertexCount * 3);
165
- for (let t = 0; t + 2 < indexCount; t += 3) {
166
- const i0 = indices ? indices[t] : t;
167
- const i1 = indices ? indices[t + 1] : t + 1;
168
- const i2 = indices ? indices[t + 2] : t + 2;
164
+ // Same shared walker as the flat pass, for the same reason.
165
+ const triangleCount = getMeshGeometryTriangleCount(geometry);
166
+ const corner = { i0: 0, i1: 0, i2: 0 };
167
+ for (let triangle = 0; triangle < triangleCount; triangle++) {
168
+ if (!getMeshGeometryTriangleVertexIndices(corner, geometry, triangle))
169
+ continue;
170
+ const i0 = corner.i0;
171
+ const i1 = corner.i1;
172
+ const i2 = corner.i2;
169
173
  const b0 = i0 * floatsPerVertex + POSITION_OFFSET;
170
174
  const b1 = i1 * floatsPerVertex + POSITION_OFFSET;
171
175
  const b2 = i2 * floatsPerVertex + POSITION_OFFSET;
@@ -179,14 +183,14 @@ export function computeMeshGeometryNormals(out, geometry) {
179
183
  const nx = e1y * e2z - e1z * e2y;
180
184
  const ny = e1z * e2x - e1x * e2z;
181
185
  const nz = e1x * e2y - e1y * e2x;
182
- accumulateNormal(accum, smoothingSources ? smoothingSources[i0] : i0, nx, ny, nz);
183
- accumulateNormal(accum, smoothingSources ? smoothingSources[i1] : i1, nx, ny, nz);
184
- accumulateNormal(accum, smoothingSources ? smoothingSources[i2] : i2, nx, ny, nz);
186
+ accumulateNormal(accum, positionGroups === null ? i0 : positionGroups[i0], nx, ny, nz);
187
+ accumulateNormal(accum, positionGroups === null ? i1 : positionGroups[i1], nx, ny, nz);
188
+ accumulateNormal(accum, positionGroups === null ? i2 : positionGroups[i2], nx, ny, nz);
185
189
  }
186
190
  const target = out.vertices;
187
191
  for (let i = 0; i < vertexCount; i++) {
188
- const source = smoothingSources ? smoothingSources[i] : i;
189
- let nx = accum[source * 3], ny = accum[source * 3 + 1], nz = accum[source * 3 + 2];
192
+ const accumulated = (positionGroups === null ? i : positionGroups[i]) * 3;
193
+ let nx = accum[accumulated], ny = accum[accumulated + 1], nz = accum[accumulated + 2];
190
194
  const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
191
195
  if (len > 0) {
192
196
  nx /= len;
@@ -198,212 +202,196 @@ export function computeMeshGeometryNormals(out, geometry) {
198
202
  target[base + 1] = ny;
199
203
  target[base + 2] = nz;
200
204
  }
205
+ out.version++;
206
+ }
207
+ // Returns one canonical representative index per vertex, grouping only vertices whose three stored
208
+ // Float32 position components are bit-identical. Other attributes do not participate, and no
209
+ // tolerance is applied. The first vertex encountered at a position is its canonical representative.
210
+ export function computeMeshGeometryPositionGroups(geometry) {
211
+ const vertices = geometry.vertices;
212
+ const floatsPerVertex = geometry.layout.stride / 4;
213
+ const vertexCount = floatsPerVertex > 0 ? Math.floor(vertices.length / floatsPerVertex) : 0;
214
+ const groups = new Uint32Array(vertexCount);
215
+ const bits = new Uint32Array(vertices.buffer, vertices.byteOffset, vertices.length);
216
+ const buckets = new Map();
217
+ for (let vertex = 0; vertex < vertexCount; vertex++) {
218
+ const position = vertex * floatsPerVertex + POSITION_OFFSET;
219
+ const hash = hashMeshGeometryPosition(bits[position], bits[position + 1], bits[position + 2]);
220
+ let bucket = buckets.get(hash);
221
+ let representative = vertex;
222
+ if (bucket === undefined) {
223
+ bucket = [];
224
+ buckets.set(hash, bucket);
225
+ }
226
+ else {
227
+ for (const candidate of bucket) {
228
+ const candidatePosition = candidate * floatsPerVertex + POSITION_OFFSET;
229
+ if (bits[position] === bits[candidatePosition] &&
230
+ bits[position + 1] === bits[candidatePosition + 1] &&
231
+ bits[position + 2] === bits[candidatePosition + 2]) {
232
+ representative = candidate;
233
+ break;
234
+ }
235
+ }
236
+ }
237
+ groups[vertex] = representative;
238
+ if (representative === vertex)
239
+ bucket.push(vertex);
240
+ }
241
+ return groups;
201
242
  }
202
- function accumulateNormal(accumulator, vertex, nx, ny, nz) {
243
+ function accumulateNormal(accum, vertex, nx, ny, nz) {
203
244
  const base = vertex * 3;
204
- accumulator[base] += nx;
205
- accumulator[base + 1] += ny;
206
- accumulator[base + 2] += nz;
245
+ accum[base] += nx;
246
+ accum[base + 1] += ny;
247
+ accum[base + 2] += nz;
248
+ }
249
+ function hashMeshGeometryPosition(x, y, z) {
250
+ let hash = 0x811c9dc5;
251
+ hash = Math.imul(hash ^ x, 0x01000193);
252
+ hash = Math.imul(hash ^ y, 0x01000193);
253
+ hash = Math.imul(hash ^ z, 0x01000193);
254
+ return hash >>> 0;
207
255
  }
208
- // Recomputes a tangent basis from positions, normals, and uv0 with Flight-owned, dependency-free
209
- // vector math. Each corner contributes a normalized tangent weighted by its geometric corner angle,
210
- // preventing a tiny or UV-stretched triangle from dominating its neighbours. Indexed corners are
211
- // clustered by handedness and direction: a corner more than 60 degrees from a cluster's running frame
212
- // starts another record, so the complete interleaved vertex is duplicated and the corner is remapped. This preserves
213
- // joints0/weights0, colors, and secondary UVs while repairing both mirrored and folded UV seams.
256
+ // Recomputes tangents from positions, normals, and uv0 using the Lengyel method, then
257
+ // Gram-Schmidt-orthogonalizes each tangent against its normal and stores it with the glTF handedness
258
+ // sign (`bitangent = cross(normal, tangent.xyz) * tangent.w`). A single indexed vertex cannot carry
259
+ // both handednesses at a mirrored-UV boundary, so the indexed path duplicates the complete vertex
260
+ // record and remaps the opposite-handed triangle corners before accumulating. Copying the complete
261
+ // record is load-bearing for extended layouts: joints0/weights0, colors, and secondary UVs survive a
262
+ // split unchanged. Non-indexed streams already have one vertex per corner and need no topology edit.
263
+ // When `positionGroups` is present, mirrored contributions share a canonical frame at each exact
264
+ // position: negative-orientation tangents are negated while bitangents are accumulated unchanged,
265
+ // then each source/split output reads the frame with its own orientation. As with grouped normals,
266
+ // this is opt-in so unrelated surfaces that merely coincide are not smoothed together.
214
267
  //
215
- // The runtime records which generated vertices came from the same authored vertex. A later normal
216
- // recomputation therefore keeps those tangent-only seams geometrically smooth. Non-indexed streams
217
- // already have one vertex per corner and require no topology edit. This is still an authoring operation:
218
- // call it before morph targets or skin bind poses are captured.
219
- export function computeMeshGeometryTangents(out, geometry) {
268
+ // This is an authoring operation: call it before capturing morph/skin bind-pose runtime data. Safe
269
+ // in-place (out === geometry): all source attributes and corner contributions are read into scratch
270
+ // storage before tangent write-back or replacement of the output arrays.
271
+ export function computeMeshGeometryTangents(out, geometry, positionGroups = null) {
220
272
  const sourceVertices = geometry.vertices;
221
- const sourceStride = geometry.layout.stride / 4;
222
- const sourceVertexCount = sourceStride > 0 ? Math.floor(sourceVertices.length / sourceStride) : 0;
273
+ const sourceFloatsPerVertex = geometry.layout.stride / 4;
274
+ const sourceVertexCount = sourceFloatsPerVertex > 0 ? Math.floor(sourceVertices.length / sourceFloatsPerVertex) : 0;
223
275
  const sourceIndices = geometry.indices;
224
- const targetStride = out.layout.stride / 4;
225
- if (sourceIndices === null) {
226
- computeNonIndexedTangents(out.vertices, targetStride, sourceVertices, sourceStride, sourceVertexCount);
227
- copyTangentSmoothingSources(out, geometry, sourceVertexCount);
228
- out.version++;
229
- return;
276
+ const elementCount = sourceIndices ? sourceIndices.length : sourceVertexCount;
277
+ // Indexed geometry needs an orientation census before accumulation. Grouped non-indexed geometry
278
+ // also needs the signs so each already-distinct corner can read the canonical group frame in its
279
+ // own orientation. Store one byte per triangle and one per source vertex, not per corner. A state's
280
+ // sign records the first orientation and magnitude 2 records that both occurred.
281
+ // Walk logical triangles through the shared decoder so triangle-strip topology is honoured, and
282
+ // resolve every corner UP FRONT: the mirrored-UV split below replaces `out.indices`, and with
283
+ // `out === geometry` that would change what the decoder reads part way through the pass.
284
+ const triangleCount = getMeshGeometryTriangleCount(geometry);
285
+ const cornerIndices = new Uint32Array(triangleCount * 3);
286
+ {
287
+ const corner = { i0: 0, i1: 0, i2: 0 };
288
+ for (let triangle = 0; triangle < triangleCount; triangle++) {
289
+ if (!getMeshGeometryTriangleVertexIndices(corner, geometry, triangle))
290
+ continue;
291
+ cornerIndices[triangle * 3] = corner.i0;
292
+ cornerIndices[triangle * 3 + 1] = corner.i1;
293
+ cornerIndices[triangle * 3 + 2] = corner.i2;
294
+ }
230
295
  }
231
- const elementCount = sourceIndices.length;
232
- const triangleElementCount = elementCount - (elementCount % 3);
233
- const primaryClusters = new Int32Array(sourceVertexCount);
234
- primaryClusters.fill(-1);
235
- const clusterHeads = new Int32Array(sourceVertexCount);
236
- clusterHeads.fill(-1);
237
- const clusterNext = new Int32Array(triangleElementCount);
238
- clusterNext.fill(-1);
239
- const clusterSources = new Uint32Array(triangleElementCount);
240
- const clusterSigns = new Int8Array(triangleElementCount);
241
- const clusterTangents = new Float64Array(triangleElementCount * 3);
242
- const elementClusters = new Uint32Array(triangleElementCount);
243
- let clusterCount = 0;
296
+ let triangleSigns = null;
297
+ let orientationStates = null;
244
298
  let splitCount = 0;
245
- for (let triangle = 0; triangle < triangleElementCount; triangle += 3) {
246
- const i0 = sourceIndices[triangle];
247
- const i1 = sourceIndices[triangle + 1];
248
- const i2 = sourceIndices[triangle + 2];
249
- const p0 = i0 * sourceStride + POSITION_OFFSET;
250
- const p1 = i1 * sourceStride + POSITION_OFFSET;
251
- const p2 = i2 * sourceStride + POSITION_OFFSET;
252
- const e1x = sourceVertices[p1] - sourceVertices[p0];
253
- const e1y = sourceVertices[p1 + 1] - sourceVertices[p0 + 1];
254
- const e1z = sourceVertices[p1 + 2] - sourceVertices[p0 + 2];
255
- const e2x = sourceVertices[p2] - sourceVertices[p0];
256
- const e2y = sourceVertices[p2 + 1] - sourceVertices[p0 + 1];
257
- const e2z = sourceVertices[p2 + 2] - sourceVertices[p0 + 2];
258
- const u0 = i0 * sourceStride + UV0_OFFSET;
259
- const u1 = i1 * sourceStride + UV0_OFFSET;
260
- const u2 = i2 * sourceStride + UV0_OFFSET;
261
- const du1 = sourceVertices[u1] - sourceVertices[u0];
262
- const dv1 = sourceVertices[u1 + 1] - sourceVertices[u0 + 1];
263
- const du2 = sourceVertices[u2] - sourceVertices[u0];
264
- const dv2 = sourceVertices[u2 + 1] - sourceVertices[u0 + 1];
265
- const determinant = du1 * dv2 - du2 * dv1;
266
- const reciprocal = determinant !== 0 ? 1 / determinant : 0;
267
- const faceTx = (dv2 * e1x - dv1 * e2x) * reciprocal;
268
- const faceTy = (dv2 * e1y - dv1 * e2y) * reciprocal;
269
- const faceTz = (dv2 * e1z - dv1 * e2z) * reciprocal;
270
- const faceBx = (du1 * e2x - du2 * e1x) * reciprocal;
271
- const faceBy = (du1 * e2y - du2 * e1y) * reciprocal;
272
- const faceBz = (du1 * e2z - du2 * e1z) * reciprocal;
273
- for (let corner = 0; corner < 3; corner++) {
274
- const element = triangle + corner;
275
- const vertex = sourceIndices[element];
276
- const normal = vertex * sourceStride + NORMAL_OFFSET;
277
- const nx = sourceVertices[normal];
278
- const ny = sourceVertices[normal + 1];
279
- const nz = sourceVertices[normal + 2];
280
- const normalDot = nx * faceTx + ny * faceTy + nz * faceTz;
281
- let tx = faceTx - nx * normalDot;
282
- let ty = faceTy - ny * normalDot;
283
- let tz = faceTz - nz * normalDot;
284
- const tangentLength = Math.sqrt(tx * tx + ty * ty + tz * tz);
285
- let sign = 0;
286
- if (tangentLength > 0) {
287
- tx /= tangentLength;
288
- ty /= tangentLength;
289
- tz /= tangentLength;
290
- const cx = ny * tz - nz * ty;
291
- const cy = nz * tx - nx * tz;
292
- const cz = nx * ty - ny * tx;
293
- sign = cx * faceBx + cy * faceBy + cz * faceBz < 0 ? -1 : 1;
299
+ if (sourceIndices !== null || positionGroups !== null) {
300
+ triangleSigns = new Int8Array(triangleCount);
301
+ orientationStates = new Int8Array(sourceVertexCount);
302
+ for (let triangle = 0; triangle < triangleCount; triangle++) {
303
+ const i0 = cornerIndices[triangle * 3];
304
+ const i1 = cornerIndices[triangle * 3 + 1];
305
+ const i2 = cornerIndices[triangle * 3 + 2];
306
+ const u0 = i0 * sourceFloatsPerVertex + UV0_OFFSET;
307
+ const u1 = i1 * sourceFloatsPerVertex + UV0_OFFSET;
308
+ const u2 = i2 * sourceFloatsPerVertex + UV0_OFFSET;
309
+ const du1 = sourceVertices[u1] - sourceVertices[u0];
310
+ const dv1 = sourceVertices[u1 + 1] - sourceVertices[u0 + 1];
311
+ const du2 = sourceVertices[u2] - sourceVertices[u0];
312
+ const dv2 = sourceVertices[u2 + 1] - sourceVertices[u0 + 1];
313
+ const determinant = du1 * dv2 - du2 * dv1;
314
+ const sign = determinant < 0 ? -1 : determinant > 0 ? 1 : 0;
315
+ triangleSigns[triangle] = sign;
316
+ if (sign !== 0) {
317
+ markTangentOrientation(orientationStates, i0, sign);
318
+ markTangentOrientation(orientationStates, i1, sign);
319
+ markTangentOrientation(orientationStates, i2, sign);
294
320
  }
295
- let cluster = clusterHeads[vertex];
296
- let compatibleCluster = -1;
297
- while (cluster !== -1) {
298
- const clusterSign = clusterSigns[cluster];
299
- const tangent = cluster * 3;
300
- const clusterLength = Math.sqrt(clusterTangents[tangent] * clusterTangents[tangent] +
301
- clusterTangents[tangent + 1] * clusterTangents[tangent + 1] +
302
- clusterTangents[tangent + 2] * clusterTangents[tangent + 2]);
303
- if (tangentLength === 0 ||
304
- clusterLength === 0 ||
305
- (clusterSign === sign &&
306
- (tx * clusterTangents[tangent] + ty * clusterTangents[tangent + 1] + tz * clusterTangents[tangent + 2]) /
307
- clusterLength >=
308
- MIN_TANGENT_CLUSTER_DOT)) {
309
- compatibleCluster = cluster;
310
- break;
311
- }
312
- cluster = clusterNext[cluster];
313
- }
314
- if (compatibleCluster === -1) {
315
- compatibleCluster = clusterCount++;
316
- clusterSources[compatibleCluster] = vertex;
317
- clusterSigns[compatibleCluster] = sign;
318
- clusterNext[compatibleCluster] = clusterHeads[vertex];
319
- clusterHeads[vertex] = compatibleCluster;
320
- if (primaryClusters[vertex] === -1)
321
- primaryClusters[vertex] = compatibleCluster;
322
- else
321
+ }
322
+ if (sourceIndices !== null) {
323
+ for (let vertex = 0; vertex < sourceVertexCount; vertex++) {
324
+ if (Math.abs(orientationStates[vertex]) === BOTH_TANGENT_ORIENTATIONS)
323
325
  splitCount++;
324
326
  }
325
- else if (clusterSigns[compatibleCluster] === 0 && sign !== 0) {
326
- clusterSigns[compatibleCluster] = sign;
327
- }
328
- elementClusters[element] = compatibleCluster;
329
- if (tangentLength > 0) {
330
- const other1 = sourceIndices[triangle + ((corner + 1) % 3)];
331
- const other2 = sourceIndices[triangle + ((corner + 2) % 3)];
332
- const weight = getMeshCornerAngle(sourceVertices, sourceStride, vertex, other1, other2);
333
- const tangent = compatibleCluster * 3;
334
- clusterTangents[tangent] += tx * weight;
335
- clusterTangents[tangent + 1] += ty * weight;
336
- clusterTangents[tangent + 2] += tz * weight;
337
- }
338
327
  }
339
328
  }
340
- const outputVertexCount = sourceVertexCount + splitCount;
341
- const clusterOutputs = new Uint32Array(clusterCount);
329
+ const targetFloatsPerVertex = out.layout.stride / 4;
342
330
  let targetVertices = out.vertices;
343
- if (splitCount > 0) {
344
- const expanded = new Float32Array(outputVertexCount * targetStride);
345
- expanded.set(targetVertices.subarray(0, sourceVertexCount * targetStride));
331
+ let remappedIndices = null;
332
+ const outputVertexCount = sourceVertexCount + splitCount;
333
+ // The mirrored-UV split remaps INDEX ELEMENTS, which is only well defined for a triangle list,
334
+ // where each element belongs to exactly one triangle. A strip element is shared by up to three
335
+ // triangles that may not agree on orientation, so there is no single vertex to remap it to;
336
+ // splitting a strip would mean converting it to a list, which would change the output topology.
337
+ const canSplitMirroredUVs = geometry.topology === 'triangle-list';
338
+ if (canSplitMirroredUVs &&
339
+ splitCount > 0 &&
340
+ sourceIndices !== null &&
341
+ triangleSigns !== null &&
342
+ orientationStates !== null) {
343
+ const splitVertices = new Uint32Array(sourceVertexCount);
344
+ splitVertices.fill(UINT32_UNMAPPED);
345
+ const expanded = new Float32Array(outputVertexCount * targetFloatsPerVertex);
346
+ expanded.set(targetVertices.subarray(0, sourceVertexCount * targetFloatsPerVertex));
346
347
  let nextVertex = sourceVertexCount;
347
- for (let cluster = 0; cluster < clusterCount; cluster++) {
348
- const source = clusterSources[cluster];
349
- if (primaryClusters[source] === cluster) {
350
- clusterOutputs[cluster] = source;
351
- }
352
- else {
353
- const output = nextVertex++;
354
- clusterOutputs[cluster] = output;
355
- const sourceOffset = source * targetStride;
356
- expanded.set(targetVertices.subarray(sourceOffset, sourceOffset + targetStride), output * targetStride);
357
- }
348
+ for (let vertex = 0; vertex < sourceVertexCount; vertex++) {
349
+ if (Math.abs(orientationStates[vertex]) !== BOTH_TANGENT_ORIENTATIONS)
350
+ continue;
351
+ splitVertices[vertex] = nextVertex;
352
+ const source = vertex * targetFloatsPerVertex;
353
+ expanded.set(targetVertices.subarray(source, source + targetFloatsPerVertex), nextVertex * targetFloatsPerVertex);
354
+ nextVertex++;
358
355
  }
359
356
  targetVertices = expanded;
360
357
  out.vertices = expanded;
361
358
  const needsUint32 = sourceIndices instanceof Uint32Array || out.indices instanceof Uint32Array || outputVertexCount > UINT16_MAX;
362
- const remappedIndices = needsUint32
363
- ? new Uint32Array(elementCount)
364
- : new Uint16Array(elementCount);
365
- for (let element = 0; element < triangleElementCount; element++) {
366
- remappedIndices[element] = clusterOutputs[elementClusters[element]];
367
- }
368
- for (let element = triangleElementCount; element < elementCount; element++) {
369
- remappedIndices[element] = sourceIndices[element];
359
+ remappedIndices = needsUint32 ? new Uint32Array(elementCount) : new Uint16Array(elementCount);
360
+ for (let element = 0; element < elementCount; element++) {
361
+ const sourceVertex = sourceIndices[element];
362
+ const sign = triangleSigns[Math.floor(element / 3)];
363
+ const primarySign = orientationStates[sourceVertex] < 0 ? -1 : 1;
364
+ remappedIndices[element] = sign !== 0 && sign !== primarySign ? splitVertices[sourceVertex] : sourceVertex;
370
365
  }
371
366
  out.indices = remappedIndices;
372
- recordTangentSmoothingSources(out, geometry, clusterSources, clusterOutputs, clusterCount, sourceVertexCount, outputVertexCount);
373
- }
374
- else {
375
- for (let cluster = 0; cluster < clusterCount; cluster++)
376
- clusterOutputs[cluster] = clusterSources[cluster];
377
- copyTangentSmoothingSources(out, geometry, sourceVertexCount);
378
367
  }
379
- for (let cluster = 0; cluster < clusterCount; cluster++) {
380
- const tangent = cluster * 3;
381
- writeMeshTangent(targetVertices, targetStride, clusterOutputs[cluster], clusterTangents[tangent], clusterTangents[tangent + 1], clusterTangents[tangent + 2], clusterSigns[cluster] < 0 ? -1 : 1);
382
- }
383
- for (let vertex = 0; vertex < sourceVertexCount; vertex++) {
384
- if (primaryClusters[vertex] === -1)
385
- writeMeshTangent(targetVertices, targetStride, vertex, 0, 0, 0, 1);
386
- }
387
- out.version++;
388
- }
389
- function computeNonIndexedTangents(target, targetStride, source, sourceStride, vertexCount) {
390
- for (let triangle = 0; triangle + 2 < vertexCount; triangle += 3) {
391
- const p0 = triangle * sourceStride + POSITION_OFFSET;
392
- const p1 = (triangle + 1) * sourceStride + POSITION_OFFSET;
393
- const p2 = (triangle + 2) * sourceStride + POSITION_OFFSET;
394
- const e1x = source[p1] - source[p0];
395
- const e1y = source[p1 + 1] - source[p0 + 1];
396
- const e1z = source[p1 + 2] - source[p0 + 2];
397
- const e2x = source[p2] - source[p0];
398
- const e2y = source[p2 + 1] - source[p0 + 1];
399
- const e2z = source[p2 + 2] - source[p0 + 2];
400
- const u0 = triangle * sourceStride + UV0_OFFSET;
401
- const u1 = (triangle + 1) * sourceStride + UV0_OFFSET;
402
- const u2 = (triangle + 2) * sourceStride + UV0_OFFSET;
403
- const du1 = source[u1] - source[u0];
404
- const dv1 = source[u1 + 1] - source[u0 + 1];
405
- const du2 = source[u2] - source[u0];
406
- const dv2 = source[u2 + 1] - source[u0 + 1];
368
+ const tan = new Float64Array(outputVertexCount * 3);
369
+ const bitan = new Float64Array(outputVertexCount * 3);
370
+ for (let triangle = 0; triangle < triangleCount; triangle++) {
371
+ const i0 = cornerIndices[triangle * 3];
372
+ const i1 = cornerIndices[triangle * 3 + 1];
373
+ const i2 = cornerIndices[triangle * 3 + 2];
374
+ // `remappedIndices` is only built for a triangle list, where element == triangle * 3 + corner.
375
+ const element = triangle * 3;
376
+ const o0 = remappedIndices ? remappedIndices[element] : i0;
377
+ const o1 = remappedIndices ? remappedIndices[element + 1] : i1;
378
+ const o2 = remappedIndices ? remappedIndices[element + 2] : i2;
379
+ const p0 = i0 * sourceFloatsPerVertex + POSITION_OFFSET;
380
+ const p1 = i1 * sourceFloatsPerVertex + POSITION_OFFSET;
381
+ const p2 = i2 * sourceFloatsPerVertex + POSITION_OFFSET;
382
+ const e1x = sourceVertices[p1] - sourceVertices[p0];
383
+ const e1y = sourceVertices[p1 + 1] - sourceVertices[p0 + 1];
384
+ const e1z = sourceVertices[p1 + 2] - sourceVertices[p0 + 2];
385
+ const e2x = sourceVertices[p2] - sourceVertices[p0];
386
+ const e2y = sourceVertices[p2 + 1] - sourceVertices[p0 + 1];
387
+ const e2z = sourceVertices[p2 + 2] - sourceVertices[p0 + 2];
388
+ const u0 = i0 * sourceFloatsPerVertex + UV0_OFFSET;
389
+ const u1 = i1 * sourceFloatsPerVertex + UV0_OFFSET;
390
+ const u2 = i2 * sourceFloatsPerVertex + UV0_OFFSET;
391
+ const du1 = sourceVertices[u1] - sourceVertices[u0];
392
+ const dv1 = sourceVertices[u1 + 1] - sourceVertices[u0 + 1];
393
+ const du2 = sourceVertices[u2] - sourceVertices[u0];
394
+ const dv2 = sourceVertices[u2 + 1] - sourceVertices[u0 + 1];
407
395
  const determinant = du1 * dv2 - du2 * dv1;
408
396
  const reciprocal = determinant !== 0 ? 1 / determinant : 0;
409
397
  const tx = (dv2 * e1x - dv1 * e2x) * reciprocal;
@@ -412,54 +400,54 @@ function computeNonIndexedTangents(target, targetStride, source, sourceStride, v
412
400
  const bx = (du1 * e2x - du2 * e1x) * reciprocal;
413
401
  const by = (du1 * e2y - du2 * e1y) * reciprocal;
414
402
  const bz = (du1 * e2z - du2 * e1z) * reciprocal;
415
- for (let corner = 0; corner < 3; corner++) {
416
- const vertex = triangle + corner;
417
- const normal = vertex * sourceStride + NORMAL_OFFSET;
418
- const nx = source[normal];
419
- const ny = source[normal + 1];
420
- const nz = source[normal + 2];
421
- const normalDot = nx * tx + ny * ty + nz * tz;
422
- const tangentX = tx - nx * normalDot;
423
- const tangentY = ty - ny * normalDot;
424
- const tangentZ = tz - nz * normalDot;
425
- const cx = ny * tangentZ - nz * tangentY;
426
- const cy = nz * tangentX - nx * tangentZ;
427
- const cz = nx * tangentY - ny * tangentX;
428
- const sign = cx * bx + cy * by + cz * bz < 0 ? -1 : 1;
429
- writeMeshTangent(target, targetStride, vertex, tangentX, tangentY, tangentZ, sign);
403
+ const tangentSign = positionGroups !== null && determinant < 0 ? -1 : 1;
404
+ accumulateTangent(tan, bitan, positionGroups === null ? o0 : positionGroups[i0], tx * tangentSign, ty * tangentSign, tz * tangentSign, bx, by, bz);
405
+ accumulateTangent(tan, bitan, positionGroups === null ? o1 : positionGroups[i1], tx * tangentSign, ty * tangentSign, tz * tangentSign, bx, by, bz);
406
+ accumulateTangent(tan, bitan, positionGroups === null ? o2 : positionGroups[i2], tx * tangentSign, ty * tangentSign, tz * tangentSign, bx, by, bz);
407
+ }
408
+ if (positionGroups === null) {
409
+ for (let output = 0; output < outputVertexCount; output++) {
410
+ writeAccumulatedTangent(targetVertices, targetFloatsPerVertex, output, tan, bitan, output, 1);
430
411
  }
431
412
  }
432
- for (let vertex = vertexCount - (vertexCount % 3); vertex < vertexCount; vertex++) {
433
- writeMeshTangent(target, targetStride, vertex, 0, 0, 0, 1);
413
+ else {
414
+ // Source outputs can read their groups directly. A split output has no positionGroups entry;
415
+ // write it from the source corner that produced it, keeping the source/output index spaces apart.
416
+ for (let source = 0; source < sourceVertexCount; source++) {
417
+ const sign = orientationStates !== null && orientationStates[source] < 0 ? -1 : 1;
418
+ writeAccumulatedTangent(targetVertices, targetFloatsPerVertex, source, tan, bitan, positionGroups[source], sign);
419
+ }
420
+ if (remappedIndices !== null && sourceIndices !== null && triangleSigns !== null) {
421
+ for (let element = 0; element < elementCount; element++) {
422
+ const output = remappedIndices[element];
423
+ if (output < sourceVertexCount)
424
+ continue;
425
+ const source = sourceIndices[element];
426
+ const sign = triangleSigns[Math.floor(element / 3)] < 0 ? -1 : 1;
427
+ writeAccumulatedTangent(targetVertices, targetFloatsPerVertex, output, tan, bitan, positionGroups[source], sign);
428
+ }
429
+ }
434
430
  }
431
+ out.version++;
435
432
  }
436
- function getMeshCornerAngle(vertices, stride, center, other1, other2) {
437
- const centerOffset = center * stride + POSITION_OFFSET;
438
- const other1Offset = other1 * stride + POSITION_OFFSET;
439
- const other2Offset = other2 * stride + POSITION_OFFSET;
440
- const ax = vertices[other1Offset] - vertices[centerOffset];
441
- const ay = vertices[other1Offset + 1] - vertices[centerOffset + 1];
442
- const az = vertices[other1Offset + 2] - vertices[centerOffset + 2];
443
- const bx = vertices[other2Offset] - vertices[centerOffset];
444
- const by = vertices[other2Offset + 1] - vertices[centerOffset + 1];
445
- const bz = vertices[other2Offset + 2] - vertices[centerOffset + 2];
446
- const denominator = Math.sqrt((ax * ax + ay * ay + az * az) * (bx * bx + by * by + bz * bz));
447
- if (denominator === 0)
448
- return 1;
449
- const cosine = (ax * bx + ay * by + az * bz) / denominator;
450
- return Math.acos(Math.max(-1, Math.min(1, cosine)));
451
- }
452
- function writeMeshTangent(vertices, stride, vertex, sourceX, sourceY, sourceZ, sign) {
453
- const normal = vertex * stride + NORMAL_OFFSET;
454
- const nx = vertices[normal];
455
- const ny = vertices[normal + 1];
456
- const nz = vertices[normal + 2];
457
- const normalDot = nx * sourceX + ny * sourceY + nz * sourceZ;
458
- let tx = sourceX - nx * normalDot;
459
- let ty = sourceY - ny * normalDot;
460
- let tz = sourceZ - nz * normalDot;
461
- let length = Math.sqrt(tx * tx + ty * ty + tz * tz);
462
- if (length === 0) {
433
+ function writeAccumulatedTangent(targetVertices, targetFloatsPerVertex, outputVertex, tangents, bitangents, accumulatedVertex, tangentSign) {
434
+ const nBase = outputVertex * targetFloatsPerVertex + NORMAL_OFFSET;
435
+ const nx = targetVertices[nBase], ny = targetVertices[nBase + 1], nz = targetVertices[nBase + 2];
436
+ let tx = tangents[accumulatedVertex * 3] * tangentSign, ty = tangents[accumulatedVertex * 3 + 1] * tangentSign, tz = tangents[accumulatedVertex * 3 + 2] * tangentSign;
437
+ // Gram-Schmidt: t = normalize(t - n * dot(n, t)).
438
+ const ndt = nx * tx + ny * ty + nz * tz;
439
+ tx -= nx * ndt;
440
+ ty -= ny * ndt;
441
+ tz -= nz * ndt;
442
+ const len = Math.sqrt(tx * tx + ty * ty + tz * tz);
443
+ if (len > 0) {
444
+ tx /= len;
445
+ ty /= len;
446
+ tz /= len;
447
+ }
448
+ else {
449
+ // Degenerate UVs: choose the coordinate axis least parallel to the normal, then cross it with
450
+ // the normal. Unlike the old fixed +X fallback this stays perpendicular even for an X normal.
463
451
  if (Math.abs(nx) <= Math.abs(ny) && Math.abs(nx) <= Math.abs(nz)) {
464
452
  tx = 0;
465
453
  ty = nz;
@@ -475,51 +463,51 @@ function writeMeshTangent(vertices, stride, vertex, sourceX, sourceY, sourceZ, s
475
463
  ty = -nx;
476
464
  tz = 0;
477
465
  }
478
- length = Math.sqrt(tx * tx + ty * ty + tz * tz);
479
- if (length === 0) {
466
+ const fallbackLength = Math.sqrt(tx * tx + ty * ty + tz * tz);
467
+ if (fallbackLength > 0) {
468
+ tx /= fallbackLength;
469
+ ty /= fallbackLength;
470
+ tz /= fallbackLength;
471
+ }
472
+ else {
480
473
  tx = 1;
481
474
  ty = 0;
482
475
  tz = 0;
483
- length = 1;
484
476
  }
485
477
  }
486
- const tangent = vertex * stride + TANGENT_OFFSET;
487
- vertices[tangent] = tx / length;
488
- vertices[tangent + 1] = ty / length;
489
- vertices[tangent + 2] = tz / length;
490
- vertices[tangent + 3] = sign;
478
+ // Handedness: w = sign(dot(cross(n, t), accumulated bitangent)).
479
+ const cx = ny * tz - nz * ty;
480
+ const cy = nz * tx - nx * tz;
481
+ const cz = nx * ty - ny * tx;
482
+ const w = cx * bitangents[accumulatedVertex * 3] +
483
+ cy * bitangents[accumulatedVertex * 3 + 1] +
484
+ cz * bitangents[accumulatedVertex * 3 + 2] <
485
+ 0
486
+ ? -1
487
+ : 1;
488
+ const base = outputVertex * targetFloatsPerVertex + TANGENT_OFFSET;
489
+ targetVertices[base] = tx;
490
+ targetVertices[base + 1] = ty;
491
+ targetVertices[base + 2] = tz;
492
+ targetVertices[base + 3] = w;
491
493
  }
492
- function copyTangentSmoothingSources(out, geometry, vertexCount) {
493
- if (out === geometry)
494
- return;
495
- const sourceRuntime = geometry[EntityRuntimeKey];
496
- const targetRuntime = out[EntityRuntimeKey];
497
- const sources = sourceRuntime?.tangentSmoothingSources;
498
- if (targetRuntime !== undefined) {
499
- targetRuntime.tangentSmoothingSources =
500
- sources !== null && sources !== undefined && sources.length === vertexCount ? sources.slice() : null;
494
+ function markTangentOrientation(orientationStates, vertex, sign) {
495
+ const state = orientationStates[vertex];
496
+ if (state === 0) {
497
+ orientationStates[vertex] = sign;
501
498
  }
502
- }
503
- function recordTangentSmoothingSources(out, geometry, clusterSources, clusterOutputs, clusterCount, sourceVertexCount, outputVertexCount) {
504
- const sourceRuntime = geometry[EntityRuntimeKey];
505
- const targetRuntime = out[EntityRuntimeKey];
506
- if (targetRuntime === undefined)
507
- return;
508
- const previous = sourceRuntime?.tangentSmoothingSources;
509
- const sources = new Uint32Array(outputVertexCount);
510
- for (let vertex = 0; vertex < sourceVertexCount; vertex++) {
511
- sources[vertex] =
512
- previous !== null && previous !== undefined && previous.length === sourceVertexCount ? previous[vertex] : vertex;
513
- }
514
- for (let cluster = 0; cluster < clusterCount; cluster++) {
515
- const output = clusterOutputs[cluster];
516
- if (output >= sourceVertexCount)
517
- sources[output] = sources[clusterSources[cluster]];
499
+ else if ((state < 0 ? -1 : 1) !== sign) {
500
+ orientationStates[vertex] = state < 0 ? -BOTH_TANGENT_ORIENTATIONS : BOTH_TANGENT_ORIENTATIONS;
518
501
  }
519
- targetRuntime.tangentSmoothingSources = sources;
520
- targetRuntime.skinBindPose = null;
521
- targetRuntime.morphBindPose = null;
522
- targetRuntime.morphBlendedWeights = null;
502
+ }
503
+ function accumulateTangent(tangents, bitangents, vertex, tx, ty, tz, bx, by, bz) {
504
+ const base = vertex * 3;
505
+ tangents[base] += tx;
506
+ tangents[base + 1] += ty;
507
+ tangents[base + 2] += tz;
508
+ bitangents[base] += bx;
509
+ bitangents[base + 1] += by;
510
+ bitangents[base + 2] += bz;
523
511
  }
524
512
  // Returns the geometry's cached local bounds, recomputing them first if a vertex edit has invalidated
525
513
  // them. THIS IS THE ONLY CORRECT WAY TO READ BOUNDS: `geometry.bounds` is a dirty-gated cache, so a
@@ -564,6 +552,7 @@ const NORMAL_OFFSET = 3;
564
552
  const POSITION_OFFSET = 0;
565
553
  const TANGENT_OFFSET = 6;
566
554
  const UV0_OFFSET = 10;
567
- const MIN_TANGENT_CLUSTER_DOT = 0.5;
555
+ const BOTH_TANGENT_ORIENTATIONS = 2;
568
556
  const UINT16_MAX = 65_535;
557
+ const UINT32_UNMAPPED = 0xffffffff;
569
558
  //# sourceMappingURL=meshGeometryCompute.js.map