@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/meshGeometry.d.ts +1 -0
- package/dist/meshGeometry.d.ts.map +1 -1
- package/dist/meshGeometry.js +17 -8
- package/dist/meshGeometry.js.map +1 -1
- package/dist/meshGeometryAttributes.d.ts.map +1 -1
- package/dist/meshGeometryAttributes.js +47 -87
- package/dist/meshGeometryAttributes.js.map +1 -1
- package/dist/meshGeometryBuilders.d.ts.map +1 -1
- package/dist/meshGeometryBuilders.js +152 -134
- package/dist/meshGeometryBuilders.js.map +1 -1
- package/dist/meshGeometryCompute.d.ts +3 -2
- package/dist/meshGeometryCompute.d.ts.map +1 -1
- package/dist/meshGeometryCompute.js +283 -294
- package/dist/meshGeometryCompute.js.map +1 -1
- package/dist/meshGeometryIndex.js +4 -4
- package/dist/meshGeometryIndex.js.map +1 -1
- package/dist/meshGeometryLayout.d.ts.map +1 -1
- package/dist/meshGeometryLayout.js +2 -67
- package/dist/meshGeometryLayout.js.map +1 -1
- package/dist/meshGeometryOperations.d.ts.map +1 -1
- package/dist/meshGeometryOperations.js +93 -7
- package/dist/meshGeometryOperations.js.map +1 -1
- package/dist/meshGeometryTransforms.d.ts.map +1 -1
- package/dist/meshGeometryTransforms.js +90 -19
- package/dist/meshGeometryTransforms.js.map +1 -1
- package/dist/meshGeometryUvs.d.ts +0 -1
- package/dist/meshGeometryUvs.d.ts.map +1 -1
- package/dist/meshGeometryUvs.js +10 -18
- package/dist/meshGeometryUvs.js.map +1 -1
- package/dist/vertexFormat.d.ts +6 -0
- package/dist/vertexFormat.d.ts.map +1 -0
- package/dist/vertexFormat.js +61 -0
- package/dist/vertexFormat.js.map +1 -0
- package/package.json +4 -4
- package/src/meshGeometry.test.ts +23 -22
- package/src/meshGeometryAttributes.test.ts +37 -0
- package/src/meshGeometryBuilders.test.ts +333 -10
- package/src/meshGeometryCompute.test.ts +246 -95
- package/src/meshGeometryDeformationClone.test.ts +2 -0
- package/src/meshGeometryLayout.test.ts +1 -12
- package/src/meshGeometryOperations.test.ts +98 -0
- package/src/meshGeometryTransforms.test.ts +103 -1
- package/src/meshGeometryUvs.test.ts +1 -40
- 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
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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. `
|
|
150
|
-
//
|
|
151
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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,
|
|
183
|
-
accumulateNormal(accum,
|
|
184
|
-
accumulateNormal(accum,
|
|
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
|
|
189
|
-
let nx = accum[
|
|
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(
|
|
243
|
+
function accumulateNormal(accum, vertex, nx, ny, nz) {
|
|
203
244
|
const base = vertex * 3;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
// joints0/weights0, colors, and secondary UVs
|
|
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
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
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
|
|
222
|
-
const sourceVertexCount =
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
341
|
-
const clusterOutputs = new Uint32Array(clusterCount);
|
|
329
|
+
const targetFloatsPerVertex = out.layout.stride / 4;
|
|
342
330
|
let targetVertices = out.vertices;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
479
|
-
if (
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
555
|
+
const BOTH_TANGENT_ORIENTATIONS = 2;
|
|
568
556
|
const UINT16_MAX = 65_535;
|
|
557
|
+
const UINT32_UNMAPPED = 0xffffffff;
|
|
569
558
|
//# sourceMappingURL=meshGeometryCompute.js.map
|