@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,13 +1,12 @@
|
|
|
1
1
|
import { createMeshGeometry } from './meshGeometry';
|
|
2
|
-
import { refreshMeshGeometryBounds } from './meshGeometryCompute';
|
|
2
|
+
import { computeMeshGeometryTangents, refreshMeshGeometryBounds } from './meshGeometryCompute';
|
|
3
3
|
// Primitive builders for the canonical interleaved PBR vertex record:
|
|
4
4
|
// position(3) + normal(3) + tangent(4, w = handedness) + uv0(2) = 12 f32 / 48 bytes.
|
|
5
5
|
// Coordinate and winding convention is pinned across the 3D suite: RIGHT-HANDED coordinates,
|
|
6
6
|
// COUNTER-CLOCKWISE (CCW) front faces, and the tangent `w` component is the bitangent sign per
|
|
7
7
|
// glTF — bitangent = cross(normal, tangent.xyz) * tangent.w. Every builder writes outward-facing
|
|
8
|
-
// normals, generates UVs, computes per-vertex tangents
|
|
9
|
-
//
|
|
10
|
-
// not need the general authoring API's topology reconciliation.
|
|
8
|
+
// normals, generates UVs, computes per-vertex tangents (via computeMeshGeometryTangents, which
|
|
9
|
+
// sets the correct w-sign from the UV gradient), and fills the cached local-space bounds.
|
|
11
10
|
// Builds an axis-aligned box of the given dimensions centered at the origin, one quad per face
|
|
12
11
|
// with per-face outward normals and per-face 0..1 UVs (so each face textures independently).
|
|
13
12
|
export function createBoxMeshGeometry(width = 1, height = 1, depth = 1) {
|
|
@@ -41,9 +40,10 @@ export function createBoxMeshGeometry(width = 1, height = 1, depth = 1) {
|
|
|
41
40
|
// (excluding the caps) centered at the origin, with `radialSegments` around the axis and
|
|
42
41
|
// `capSegments` latitudinal divisions per hemisphere. Normals are smooth across the seam between
|
|
43
42
|
// the caps and the side wall. UVs run v from 0 (top) to 1 (bottom) along the full length.
|
|
43
|
+
// Segment counts must be finite; fractional values are floored after clamping to their minima.
|
|
44
44
|
export function createCapsuleMeshGeometry(radius = 0.5, height = 1, radialSegments = 16, capSegments = 8) {
|
|
45
|
-
const rSeg =
|
|
46
|
-
const cSeg =
|
|
45
|
+
const rSeg = normalizeMeshCount(radialSegments, 3);
|
|
46
|
+
const cSeg = normalizeMeshCount(capSegments, 1);
|
|
47
47
|
const halfH = height * 0.5;
|
|
48
48
|
const positions = [];
|
|
49
49
|
const normals = [];
|
|
@@ -63,17 +63,20 @@ export function createCapsuleMeshGeometry(radius = 0.5, height = 1, radialSegmen
|
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
const ringVertexCount = rSeg + 1;
|
|
66
|
-
const
|
|
66
|
+
const ringCount = 2 * cSeg + 2;
|
|
67
|
+
const vDivisor = ringCount - 1;
|
|
67
68
|
// Top hemisphere (phi from 0 to PI/2).
|
|
68
69
|
for (let j = 0; j <= cSeg; j++) {
|
|
69
70
|
addRing((j / cSeg) * (Math.PI * 0.5), halfH);
|
|
70
71
|
}
|
|
72
|
+
// Duplicate the equator at the bottom of the cylindrical span. The two equators share radial
|
|
73
|
+
// normals but need distinct positions (and UV rows) to form the side wall between the caps.
|
|
74
|
+
addRing(Math.PI * 0.5, -halfH);
|
|
71
75
|
// Bottom hemisphere (phi from PI/2 to PI).
|
|
72
76
|
for (let j = 1; j <= cSeg; j++) {
|
|
73
77
|
addRing(Math.PI * 0.5 + (j / cSeg) * (Math.PI * 0.5), -halfH);
|
|
74
78
|
}
|
|
75
79
|
// Fix up the v UV coordinate.
|
|
76
|
-
const ringCount = 2 * cSeg + 1;
|
|
77
80
|
for (let j = 0; j < ringCount; j++) {
|
|
78
81
|
const v = j / vDivisor;
|
|
79
82
|
for (let i = 0; i <= rSeg; i++) {
|
|
@@ -81,14 +84,15 @@ export function createCapsuleMeshGeometry(radius = 0.5, height = 1, radialSegmen
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
// Connect rings into quads.
|
|
84
|
-
|
|
85
|
-
for (let j = 0; j < totalRings - 1; j++) {
|
|
87
|
+
for (let j = 0; j < ringCount - 1; j++) {
|
|
86
88
|
for (let i = 0; i < rSeg; i++) {
|
|
87
89
|
const a = j * ringVertexCount + i;
|
|
88
90
|
const b = a + 1;
|
|
89
91
|
const c = a + ringVertexCount;
|
|
90
92
|
const d = c + 1;
|
|
91
|
-
|
|
93
|
+
// Reversed against the sphere's identical-looking line on purpose: this builder walks its
|
|
94
|
+
// rings the opposite way round, so the same index order faces the other way.
|
|
95
|
+
indices.push(a, b, c, b, d, c);
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
|
|
@@ -97,8 +101,9 @@ export function createCapsuleMeshGeometry(radius = 0.5, height = 1, radialSegmen
|
|
|
97
101
|
// `radius` is the outer radius; `segments` is the number of rim vertices. A center vertex fans
|
|
98
102
|
// out to `segments` rim vertices via a triangle fan, so the result is `segments` triangles.
|
|
99
103
|
// UVs map from the disc plane onto 0..1 with (0.5, 0.5) at the center.
|
|
104
|
+
// `segments` must be finite; a fractional value is floored after clamping to the minimum of 3.
|
|
100
105
|
export function createCircleMeshGeometry(radius = 0.5, segments = 32) {
|
|
101
|
-
const segs =
|
|
106
|
+
const segs = normalizeMeshCount(segments, 3);
|
|
102
107
|
const positions = [];
|
|
103
108
|
const normals = [];
|
|
104
109
|
const uvs = [];
|
|
@@ -109,6 +114,7 @@ export function createCircleMeshGeometry(radius = 0.5, segments = 32) {
|
|
|
109
114
|
// Builds a right circular cone of `radius` and `height` centered at the origin, apex at +Y, base
|
|
110
115
|
// at -Y, with `radialSegments` around the axis. A capped base disc is included when `capped` is
|
|
111
116
|
// true (default). Side normals are smooth around the ring and slanted by the cone half-angle.
|
|
117
|
+
// `radialSegments` must be finite; a fractional value is floored after clamping to 3.
|
|
112
118
|
export function createConeMeshGeometry(radius = 0.5, height = 1, radialSegments = 32, capped = true) {
|
|
113
119
|
return createCylinderMeshGeometry(0, radius, height, radialSegments, capped);
|
|
114
120
|
}
|
|
@@ -116,15 +122,17 @@ export function createConeMeshGeometry(radius = 0.5, height = 1, radialSegments
|
|
|
116
122
|
// top (`topRadius`) and bottom (`bottomRadius`) radii so it also serves as a truncated cone (a
|
|
117
123
|
// zero top radius yields a cone). `radialSegments` rings around the axis; `capped` adds top and
|
|
118
124
|
// bottom discs (skipping a zero-radius cap).
|
|
125
|
+
// `radialSegments` must be finite; a fractional value is floored after clamping to 3. A zero
|
|
126
|
+
// height is a valid collapsed cylinder and uses stable radial side normals.
|
|
119
127
|
export function createCylinderMeshGeometry(topRadius = 0.5, bottomRadius = 0.5, height = 1, radialSegments = 32, capped = true) {
|
|
120
|
-
const segments =
|
|
128
|
+
const segments = normalizeMeshCount(radialSegments, 3);
|
|
121
129
|
const halfHeight = height * 0.5;
|
|
122
130
|
const positions = [];
|
|
123
131
|
const normals = [];
|
|
124
132
|
const uvs = [];
|
|
125
133
|
const indices = [];
|
|
126
134
|
// Side wall: slope of the silhouette feeds the radial normal tilt.
|
|
127
|
-
const slope = (bottomRadius - topRadius) / height;
|
|
135
|
+
const slope = height !== 0 ? (bottomRadius - topRadius) / height : 0;
|
|
128
136
|
const sideStart = positions.length / 3;
|
|
129
137
|
for (let y = 0; y <= 1; y++) {
|
|
130
138
|
const radius = y === 0 ? bottomRadius : topRadius;
|
|
@@ -143,12 +151,24 @@ export function createCylinderMeshGeometry(topRadius = 0.5, bottomRadius = 0.5,
|
|
|
143
151
|
uvs.push(s / segments, y);
|
|
144
152
|
}
|
|
145
153
|
}
|
|
154
|
+
// Each segment is a quad split into (a, c, b) and (b, c, d). When one radius is zero that ring
|
|
155
|
+
// collapses to a single point, so one of the two triangles has two coincident corners and encloses no
|
|
156
|
+
// area: it consumes index bandwidth and a vertex-shader invocation to rasterise nothing. A cone is
|
|
157
|
+
// exactly that case — it is this cylinder with a zero top radius — so a third of its triangles were
|
|
158
|
+
// dead. Dropping the collapsed triangle leaves the surviving fan untouched, vertices included: the
|
|
159
|
+
// apex ring keeps one vertex per segment because each carries its own u, which is what lets the
|
|
160
|
+
// texture vary around the cone.
|
|
161
|
+
const topCollapsed = topRadius === 0;
|
|
162
|
+
const bottomCollapsed = bottomRadius === 0;
|
|
146
163
|
for (let s = 0; s < segments; s++) {
|
|
147
164
|
const a = sideStart + s;
|
|
148
165
|
const b = sideStart + s + 1;
|
|
149
166
|
const c = sideStart + (segments + 1) + s;
|
|
150
167
|
const d = sideStart + (segments + 1) + s + 1;
|
|
151
|
-
|
|
168
|
+
if (!bottomCollapsed)
|
|
169
|
+
indices.push(a, c, b);
|
|
170
|
+
if (!topCollapsed)
|
|
171
|
+
indices.push(b, c, d);
|
|
152
172
|
}
|
|
153
173
|
if (capped) {
|
|
154
174
|
if (bottomRadius > 0) {
|
|
@@ -161,11 +181,13 @@ export function createCylinderMeshGeometry(topRadius = 0.5, bottomRadius = 0.5,
|
|
|
161
181
|
return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
|
|
162
182
|
}
|
|
163
183
|
// Builds a regular dodecahedron (12 pentagonal faces, triangulated) inscribed in a sphere of `radius`.
|
|
184
|
+
// `detail` must be finite; a fractional value is floored after clamping to 0..5.
|
|
164
185
|
export function createDodecahedronMeshGeometry(radius = 0.5, detail = 0) {
|
|
165
186
|
return createPolyhedronMeshGeometry(DODECAHEDRON_VERTS, DODECAHEDRON_FACES, radius, detail);
|
|
166
187
|
}
|
|
167
188
|
// Builds a regular icosahedron (20 triangular faces) inscribed in a sphere of `radius`.
|
|
168
189
|
// This is the base shape for createIcosphereMeshGeometry at detail=0.
|
|
190
|
+
// `detail` must be finite; a fractional value is floored after clamping to 0..5.
|
|
169
191
|
export function createIcosahedronMeshGeometry(radius = 0.5, detail = 0) {
|
|
170
192
|
return createPolyhedronMeshGeometry(ICOSAHEDRON_VERTS, ICOSAHEDRON_FACES, radius, detail);
|
|
171
193
|
}
|
|
@@ -174,8 +196,9 @@ export function createIcosahedronMeshGeometry(radius = 0.5, detail = 0) {
|
|
|
174
196
|
// from a regular icosahedron, each face is subdivided `subdivisions` times (0 = raw icosahedron,
|
|
175
197
|
// 1 = 80 triangles, 2 = 320, etc.). Normals are the unit-sphere position direction; UVs are
|
|
176
198
|
// spherical (longitude/latitude).
|
|
199
|
+
// `subdivisions` must be finite; a fractional value is floored after clamping to 0..6.
|
|
177
200
|
export function createIcosphereMeshGeometry(radius = 0.5, subdivisions = 2) {
|
|
178
|
-
const subs =
|
|
201
|
+
const subs = normalizeMeshCount(subdivisions, 0, 6); // cap at 6 to prevent > 262k triangles
|
|
179
202
|
// Icosahedron base vertices (unit sphere).
|
|
180
203
|
const phi = (1 + Math.sqrt(5)) * 0.5;
|
|
181
204
|
const scale = 1 / Math.sqrt(1 + phi * phi);
|
|
@@ -250,18 +273,18 @@ export function createIcosphereMeshGeometry(radius = 0.5, subdivisions = 2) {
|
|
|
250
273
|
const normals = [];
|
|
251
274
|
const uvs = [];
|
|
252
275
|
const faceIndices = [];
|
|
253
|
-
// Emit unique vertices with spherical UVs.
|
|
254
|
-
//
|
|
276
|
+
// Emit unique vertices with spherical UVs. Independent vertices per face (non-indexed, then
|
|
277
|
+
// buildCanonicalMeshGeometry) are what let a seam vertex carry a different u on each face it touches;
|
|
278
|
+
// correctSeamU then chooses those values, which is the half that makes the seam actually work.
|
|
255
279
|
for (const [a, b, c] of faces) {
|
|
256
|
-
|
|
257
|
-
|
|
280
|
+
const corner = [a, b, c];
|
|
281
|
+
const seamU = faceSphericalU(verts[a], verts[b], verts[c]);
|
|
282
|
+
for (let k = 0; k < 3; k++) {
|
|
283
|
+
const v = verts[corner[k]];
|
|
258
284
|
const nx = v[0], ny = v[1], nz = v[2];
|
|
259
285
|
positions.push(radius * nx, radius * ny, radius * nz);
|
|
260
286
|
normals.push(nx, ny, nz);
|
|
261
|
-
|
|
262
|
-
const u = 0.5 + Math.atan2(nz, nx) / (Math.PI * 2);
|
|
263
|
-
const sv = 0.5 - Math.asin(Math.max(-1, Math.min(1, ny))) / Math.PI;
|
|
264
|
-
uvs.push(u, sv);
|
|
287
|
+
uvs.push(seamU[k], sphericalV(ny));
|
|
265
288
|
}
|
|
266
289
|
}
|
|
267
290
|
for (let i = 0; i < positions.length / 3; i++) {
|
|
@@ -270,15 +293,17 @@ export function createIcosphereMeshGeometry(radius = 0.5, subdivisions = 2) {
|
|
|
270
293
|
return buildCanonicalMeshGeometry(positions, normals, uvs, faceIndices);
|
|
271
294
|
}
|
|
272
295
|
// Builds a regular octahedron (8 triangular faces) inscribed in a sphere of `radius`.
|
|
296
|
+
// `detail` must be finite; a fractional value is floored after clamping to 0..5.
|
|
273
297
|
export function createOctahedronMeshGeometry(radius = 0.5, detail = 0) {
|
|
274
298
|
return createPolyhedronMeshGeometry(OCTAHEDRON_VERTS, OCTAHEDRON_FACES, radius, detail);
|
|
275
299
|
}
|
|
276
300
|
// Builds a flat plane in the XZ plane (Y up, normal +Y) centered at the origin, subdivided into
|
|
277
301
|
// `widthSegments` x `depthSegments` quads with a 0..1 UV grid. Suitable as a ground plane or a
|
|
278
302
|
// textured "panel" mesh.
|
|
303
|
+
// Segment counts must be finite; fractional values are floored after clamping to 1.
|
|
279
304
|
export function createPlaneMeshGeometry(width = 1, depth = 1, widthSegments = 1, depthSegments = 1) {
|
|
280
|
-
const wSeg =
|
|
281
|
-
const dSeg =
|
|
305
|
+
const wSeg = normalizeMeshCount(widthSegments, 1);
|
|
306
|
+
const dSeg = normalizeMeshCount(depthSegments, 1);
|
|
282
307
|
const hw = width * 0.5, hd = depth * 0.5;
|
|
283
308
|
const positions = [];
|
|
284
309
|
const normals = [];
|
|
@@ -313,8 +338,9 @@ export function createPlaneMeshGeometry(width = 1, depth = 1, widthSegments = 1,
|
|
|
313
338
|
// This is the shared backend for the Platonic solid builders and is also exported for custom
|
|
314
339
|
// polyhedra. Vertices are projected onto the unit sphere before scaling so the output is a
|
|
315
340
|
// geodesic sphere approximation.
|
|
341
|
+
// `detail` must be finite; a fractional value is floored after clamping to 0..5.
|
|
316
342
|
export function createPolyhedronMeshGeometry(vertexPositions, faceIndices, radius = 0.5, detail = 0) {
|
|
317
|
-
const subs =
|
|
343
|
+
const subs = normalizeMeshCount(detail, 0, 5);
|
|
318
344
|
const verts = vertexPositions.map(([x, y, z]) => {
|
|
319
345
|
const len = Math.sqrt(x * x + y * y + z * z);
|
|
320
346
|
return [x / len, y / len, z / len];
|
|
@@ -353,14 +379,14 @@ export function createPolyhedronMeshGeometry(vertexPositions, faceIndices, radiu
|
|
|
353
379
|
const uvs = [];
|
|
354
380
|
const flatIndices = [];
|
|
355
381
|
for (const [a, b, c] of faces) {
|
|
356
|
-
|
|
357
|
-
|
|
382
|
+
const corner = [a, b, c];
|
|
383
|
+
const seamU = faceSphericalU(verts[a], verts[b], verts[c]);
|
|
384
|
+
for (let k = 0; k < 3; k++) {
|
|
385
|
+
const v = verts[corner[k]];
|
|
358
386
|
const nx = v[0], ny = v[1], nz = v[2];
|
|
359
387
|
positions.push(radius * nx, radius * ny, radius * nz);
|
|
360
388
|
normals.push(nx, ny, nz);
|
|
361
|
-
|
|
362
|
-
const sv = 0.5 - Math.asin(Math.max(-1, Math.min(1, ny))) / Math.PI;
|
|
363
|
-
uvs.push(u, sv);
|
|
389
|
+
uvs.push(seamU[k], sphericalV(ny));
|
|
364
390
|
}
|
|
365
391
|
}
|
|
366
392
|
for (let i = 0; i < positions.length / 3; i++)
|
|
@@ -381,8 +407,9 @@ export function createQuadMeshGeometry(width = 1, height = 1) {
|
|
|
381
407
|
// Builds a flat ring (annulus) in the XZ plane centered at the origin. `innerRadius` is the
|
|
382
408
|
// hole radius and `outerRadius` is the ring's outer edge. Normal is +Y. `segments` is the
|
|
383
409
|
// number of vertices around the ring. UVs map inner radius to u=0 and outer to u=1.
|
|
410
|
+
// `segments` must be finite; a fractional value is floored after clamping to 3.
|
|
384
411
|
export function createRingMeshGeometry(innerRadius = 0.25, outerRadius = 0.5, segments = 32) {
|
|
385
|
-
const segs =
|
|
412
|
+
const segs = normalizeMeshCount(segments, 3);
|
|
386
413
|
const positions = [];
|
|
387
414
|
const normals = [];
|
|
388
415
|
const uvs = [];
|
|
@@ -412,9 +439,10 @@ export function createRingMeshGeometry(innerRadius = 0.25, outerRadius = 0.5, se
|
|
|
412
439
|
// Builds a UV sphere of `radius` centered at the origin, with `widthSegments` longitudinal and
|
|
413
440
|
// `heightSegments` latitudinal divisions. Normals are the unit position direction; UVs map
|
|
414
441
|
// longitude to u and latitude to v.
|
|
442
|
+
// Segment counts must be finite; fractional values are floored after clamping to 3x2.
|
|
415
443
|
export function createSphereMeshGeometry(radius = 0.5, widthSegments = 32, heightSegments = 16) {
|
|
416
|
-
const wSeg =
|
|
417
|
-
const hSeg =
|
|
444
|
+
const wSeg = normalizeMeshCount(widthSegments, 3);
|
|
445
|
+
const hSeg = normalizeMeshCount(heightSegments, 2);
|
|
418
446
|
const positions = [];
|
|
419
447
|
const normals = [];
|
|
420
448
|
const uvs = [];
|
|
@@ -448,15 +476,17 @@ export function createSphereMeshGeometry(radius = 0.5, widthSegments = 32, heigh
|
|
|
448
476
|
return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
|
|
449
477
|
}
|
|
450
478
|
// Builds a regular tetrahedron (4 triangular faces) inscribed in a sphere of `radius`.
|
|
479
|
+
// `detail` must be finite; a fractional value is floored after clamping to 0..5.
|
|
451
480
|
export function createTetrahedronMeshGeometry(radius = 0.5, detail = 0) {
|
|
452
481
|
return createPolyhedronMeshGeometry(TETRAHEDRON_VERTS, TETRAHEDRON_FACES, radius, detail);
|
|
453
482
|
}
|
|
454
483
|
// Builds a torus knot: a torus-shaped curve wound `p` times around the Z axis and `q` times
|
|
455
484
|
// around the tube axis. `radius` is the distance from the center to the tube center line;
|
|
456
485
|
// `tube` is the tube radius. Normals and UVs are generated per the standard torus knot geometry.
|
|
486
|
+
// Segment counts must be finite; fractional values are floored after clamping to 3.
|
|
457
487
|
export function createTorusKnotMeshGeometry(radius = 0.5, tube = 0.15, tubularSegments = 64, radialSegments = 8, p = 2, q = 3) {
|
|
458
|
-
const tSeg =
|
|
459
|
-
const rSeg =
|
|
488
|
+
const tSeg = normalizeMeshCount(tubularSegments, 3);
|
|
489
|
+
const rSeg = normalizeMeshCount(radialSegments, 3);
|
|
460
490
|
const positions = [];
|
|
461
491
|
const normals = [];
|
|
462
492
|
const uvs = [];
|
|
@@ -517,7 +547,9 @@ export function createTorusKnotMeshGeometry(radius = 0.5, tube = 0.15, tubularSe
|
|
|
517
547
|
const b = a + 1;
|
|
518
548
|
const c = a + rowStride;
|
|
519
549
|
const d = c + 1;
|
|
520
|
-
|
|
550
|
+
// Reversed for the same reason as the capsule: this builder sweeps its tube rings the
|
|
551
|
+
// opposite way round from the sphere, so the same index order faced inward.
|
|
552
|
+
indices.push(a, b, c, b, d, c);
|
|
521
553
|
}
|
|
522
554
|
}
|
|
523
555
|
return buildCanonicalMeshGeometry(positions, normals, uvs, indices);
|
|
@@ -525,9 +557,10 @@ export function createTorusKnotMeshGeometry(radius = 0.5, tube = 0.15, tubularSe
|
|
|
525
557
|
// Builds a torus in the XY plane: a ring of `radius` (center to tube center) with a tube of
|
|
526
558
|
// `tube` radius, divided into `radialSegments` around the ring and `tubularSegments` around the
|
|
527
559
|
// tube. Normals point radially outward from the tube center.
|
|
560
|
+
// Segment counts must be finite; fractional values are floored after clamping to 3.
|
|
528
561
|
export function createTorusMeshGeometry(radius = 0.5, tube = 0.2, radialSegments = 24, tubularSegments = 48) {
|
|
529
|
-
const rSeg =
|
|
530
|
-
const tSeg =
|
|
562
|
+
const rSeg = normalizeMeshCount(radialSegments, 3);
|
|
563
|
+
const tSeg = normalizeMeshCount(tubularSegments, 3);
|
|
531
564
|
const positions = [];
|
|
532
565
|
const normals = [];
|
|
533
566
|
const uvs = [];
|
|
@@ -584,17 +617,81 @@ function addDisc(positions, normals, uvs, indices, segments, radius, y, directio
|
|
|
584
617
|
for (let s = 0; s < segments; s++) {
|
|
585
618
|
const a = ringStart + s;
|
|
586
619
|
const b = ringStart + s + 1;
|
|
620
|
+
// Right-hand rule over the winding: for an up-facing disc the ring must be walked b-then-a to
|
|
621
|
+
// put (p1 - p0) x (p2 - p0) along +Y, which is the direction the authored normals already point.
|
|
587
622
|
if (direction > 0) {
|
|
588
|
-
indices.push(center,
|
|
623
|
+
indices.push(center, b, a);
|
|
589
624
|
}
|
|
590
625
|
else {
|
|
591
|
-
indices.push(center,
|
|
626
|
+
indices.push(center, a, b);
|
|
592
627
|
}
|
|
593
628
|
}
|
|
594
629
|
}
|
|
630
|
+
function normalizeMeshCount(value, min, max = Number.POSITIVE_INFINITY) {
|
|
631
|
+
if (!Number.isFinite(value))
|
|
632
|
+
throw new RangeError('mesh segment and subdivision counts must be finite');
|
|
633
|
+
return Math.floor(Math.max(min, Math.min(value, max)));
|
|
634
|
+
}
|
|
595
635
|
// Interleaves separate position/normal/uv0 arrays into the canonical 12-float record, allocates
|
|
596
636
|
// the MeshGeometry (indices auto-promote past 65k), then fills tangents from the UV gradient and
|
|
597
637
|
// the cached local bounds. The single finalize path every builder funnels through.
|
|
638
|
+
// Spherical UV for one direction on the unit sphere: u is longitude in 0..1, v is latitude with 0 at
|
|
639
|
+
// the top. Shared by every builder that projects onto a sphere so the two call sites cannot drift.
|
|
640
|
+
function sphericalU(nx, nz) {
|
|
641
|
+
return 0.5 + Math.atan2(nz, nx) / (Math.PI * 2);
|
|
642
|
+
}
|
|
643
|
+
function sphericalV(ny) {
|
|
644
|
+
return 0.5 - Math.asin(Math.max(-1, Math.min(1, ny))) / Math.PI;
|
|
645
|
+
}
|
|
646
|
+
// ★ THE SPHERICAL-UV BUILDERS EXPECT A REPEATING ADDRESSING MODE. Every builder that routes through
|
|
647
|
+
// faceSphericalU — icosphere, and the polyhedra (dodecahedron, icosahedron, octahedron, tetrahedron)
|
|
648
|
+
// via createPolyhedronMeshGeometry — emits u > 1 on the faces that cross the longitude seam. That is a
|
|
649
|
+
// property of a wrapped parameterisation, not an accident: no assignment confined to 0..1 can describe
|
|
650
|
+
// a face crossing the seam continuously. Sampled with a REPEAT wrap those values land where they should;
|
|
651
|
+
// sampled with clamp-to-edge, the part past 1 collapses onto the edge texel. Flight's default sampler is
|
|
652
|
+
// clamp-to-edge (texture/src/sampler.ts) and createTilingSampler is the repeating one, so a texture
|
|
653
|
+
// applied to these solids wants the latter.
|
|
654
|
+
//
|
|
655
|
+
// The three corner longitudes of one face, with both spherical-mapping degeneracies resolved.
|
|
656
|
+
//
|
|
657
|
+
// SEAM: longitude wraps at u = 0/1, so a face with corners either side gets values like 0.99 and 0.01
|
|
658
|
+
// and interpolates BACKWARDS across the whole texture — the entire map crushed into one triangle.
|
|
659
|
+
// Lifting the low corners past 1 makes the face interpolate forward through the seam instead. A seam
|
|
660
|
+
// face therefore carries u > 1 BY DESIGN: no parameterisation confined to 0..1 can express a face
|
|
661
|
+
// crossing the seam continuously, and sampling wraps it.
|
|
662
|
+
//
|
|
663
|
+
// POLE: a vertex on the Y axis has zero horizontal radius, so atan2 gives it an arbitrary longitude
|
|
664
|
+
// (0, hence u = 0.5) unrelated to the face it belongs to. It has no longitude of its own to preserve —
|
|
665
|
+
// every meridian meets there — so it takes the average of its face-mates and the face stays narrow.
|
|
666
|
+
//
|
|
667
|
+
// Order matters: the face-mates are seam-corrected FIRST, then the pole averages the corrected values.
|
|
668
|
+
// Averaging raw 0.99 and 0.01 would put the pole at 0.5, on the far side of the sphere from both.
|
|
669
|
+
//
|
|
670
|
+
// Emitting per-face vertices is what makes any of this possible — a shared vertex could only carry one
|
|
671
|
+
// u — but it is not sufficient on its own: the values still have to be chosen, which is the half that
|
|
672
|
+
// was missing.
|
|
673
|
+
function faceSphericalU(a, b, c) {
|
|
674
|
+
const directions = [a, b, c];
|
|
675
|
+
const isPole = directions.map((d) => Math.hypot(d[0], d[2]) <= POLE_EPSILON);
|
|
676
|
+
const u = [sphericalU(a[0], a[2]), sphericalU(b[0], b[2]), sphericalU(c[0], c[2])];
|
|
677
|
+
const defined = [0, 1, 2].filter((i) => !isPole[i]);
|
|
678
|
+
if (defined.length > 0) {
|
|
679
|
+
const min = Math.min(...defined.map((i) => u[i]));
|
|
680
|
+
const max = Math.max(...defined.map((i) => u[i]));
|
|
681
|
+
if (max - min > 0.5) {
|
|
682
|
+
for (const i of defined)
|
|
683
|
+
if (u[i] < 0.5)
|
|
684
|
+
u[i] += 1;
|
|
685
|
+
}
|
|
686
|
+
const mean = defined.reduce((sum, i) => sum + u[i], 0) / defined.length;
|
|
687
|
+
for (let i = 0; i < 3; i++)
|
|
688
|
+
if (isPole[i])
|
|
689
|
+
u[i] = mean;
|
|
690
|
+
}
|
|
691
|
+
return u;
|
|
692
|
+
}
|
|
693
|
+
// A direction within this of the Y axis has no meaningful longitude: every meridian meets at a pole.
|
|
694
|
+
const POLE_EPSILON = 1e-6;
|
|
598
695
|
function buildCanonicalMeshGeometry(positions, normals, uvs, indices) {
|
|
599
696
|
const vertexCount = positions.length / 3;
|
|
600
697
|
const vertices = new Float32Array(vertexCount * CANONICAL_FLOATS_PER_VERTEX);
|
|
@@ -617,92 +714,10 @@ function buildCanonicalMeshGeometry(positions, normals, uvs, indices) {
|
|
|
617
714
|
layout: CANONICAL_VERTEX_LAYOUT,
|
|
618
715
|
vertices: vertices,
|
|
619
716
|
});
|
|
620
|
-
|
|
717
|
+
computeMeshGeometryTangents(geometry, geometry);
|
|
621
718
|
refreshMeshGeometryBounds(geometry);
|
|
622
719
|
return geometry;
|
|
623
720
|
}
|
|
624
|
-
// Primitive builders own their topology and already emit separate records at normal and UV seams.
|
|
625
|
-
// Keeping this trusted-topology path local avoids pulling the general, topology-repairing tangent
|
|
626
|
-
// authoring routine into applications that only create Flight primitives.
|
|
627
|
-
function computeCanonicalMeshTangents(vertices, indices) {
|
|
628
|
-
const vertexCount = vertices.length / CANONICAL_FLOATS_PER_VERTEX;
|
|
629
|
-
const tangents = new Float32Array(vertexCount * 3);
|
|
630
|
-
const bitangents = new Float32Array(vertexCount * 3);
|
|
631
|
-
for (let element = 0; element + 2 < indices.length; element += 3) {
|
|
632
|
-
const i0 = indices[element];
|
|
633
|
-
const i1 = indices[element + 1];
|
|
634
|
-
const i2 = indices[element + 2];
|
|
635
|
-
const p0 = i0 * CANONICAL_FLOATS_PER_VERTEX;
|
|
636
|
-
const p1 = i1 * CANONICAL_FLOATS_PER_VERTEX;
|
|
637
|
-
const p2 = i2 * CANONICAL_FLOATS_PER_VERTEX;
|
|
638
|
-
const x1 = vertices[p1] - vertices[p0];
|
|
639
|
-
const y1 = vertices[p1 + 1] - vertices[p0 + 1];
|
|
640
|
-
const z1 = vertices[p1 + 2] - vertices[p0 + 2];
|
|
641
|
-
const x2 = vertices[p2] - vertices[p0];
|
|
642
|
-
const y2 = vertices[p2 + 1] - vertices[p0 + 1];
|
|
643
|
-
const z2 = vertices[p2 + 2] - vertices[p0 + 2];
|
|
644
|
-
const du1 = vertices[p1 + 10] - vertices[p0 + 10];
|
|
645
|
-
const dv1 = vertices[p1 + 11] - vertices[p0 + 11];
|
|
646
|
-
const du2 = vertices[p2 + 10] - vertices[p0 + 10];
|
|
647
|
-
const dv2 = vertices[p2 + 11] - vertices[p0 + 11];
|
|
648
|
-
const determinant = du1 * dv2 - du2 * dv1;
|
|
649
|
-
const reciprocal = determinant === 0 ? 0 : 1 / determinant;
|
|
650
|
-
const tx = (dv2 * x1 - dv1 * x2) * reciprocal;
|
|
651
|
-
const ty = (dv2 * y1 - dv1 * y2) * reciprocal;
|
|
652
|
-
const tz = (dv2 * z1 - dv1 * z2) * reciprocal;
|
|
653
|
-
const bx = (du1 * x2 - du2 * x1) * reciprocal;
|
|
654
|
-
const by = (du1 * y2 - du2 * y1) * reciprocal;
|
|
655
|
-
const bz = (du1 * z2 - du2 * z1) * reciprocal;
|
|
656
|
-
for (let corner = 0; corner < 3; corner++) {
|
|
657
|
-
const vertex = corner === 0 ? i0 : corner === 1 ? i1 : i2;
|
|
658
|
-
const tangent = vertex * 3;
|
|
659
|
-
tangents[tangent] += tx;
|
|
660
|
-
tangents[tangent + 1] += ty;
|
|
661
|
-
tangents[tangent + 2] += tz;
|
|
662
|
-
bitangents[tangent] += bx;
|
|
663
|
-
bitangents[tangent + 1] += by;
|
|
664
|
-
bitangents[tangent + 2] += bz;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
for (let vertex = 0; vertex < vertexCount; vertex++) {
|
|
668
|
-
const base = vertex * CANONICAL_FLOATS_PER_VERTEX;
|
|
669
|
-
const tangent = vertex * 3;
|
|
670
|
-
const nx = vertices[base + 3];
|
|
671
|
-
const ny = vertices[base + 4];
|
|
672
|
-
const nz = vertices[base + 5];
|
|
673
|
-
const normalDot = nx * tangents[tangent] + ny * tangents[tangent + 1] + nz * tangents[tangent + 2];
|
|
674
|
-
let tx = tangents[tangent] - nx * normalDot;
|
|
675
|
-
let ty = tangents[tangent + 1] - ny * normalDot;
|
|
676
|
-
let tz = tangents[tangent + 2] - nz * normalDot;
|
|
677
|
-
let length = Math.hypot(tx, ty, tz);
|
|
678
|
-
if (length === 0) {
|
|
679
|
-
if (Math.abs(nx) < Math.abs(nz)) {
|
|
680
|
-
tx = 0;
|
|
681
|
-
ty = nz;
|
|
682
|
-
tz = -ny;
|
|
683
|
-
}
|
|
684
|
-
else {
|
|
685
|
-
tx = ny;
|
|
686
|
-
ty = -nx;
|
|
687
|
-
tz = 0;
|
|
688
|
-
}
|
|
689
|
-
length = Math.hypot(tx, ty, tz) || 1;
|
|
690
|
-
}
|
|
691
|
-
tx /= length;
|
|
692
|
-
ty /= length;
|
|
693
|
-
tz /= length;
|
|
694
|
-
vertices[base + 6] = tx;
|
|
695
|
-
vertices[base + 7] = ty;
|
|
696
|
-
vertices[base + 8] = tz;
|
|
697
|
-
vertices[base + 9] =
|
|
698
|
-
(ny * tz - nz * ty) * bitangents[tangent] +
|
|
699
|
-
(nz * tx - nx * tz) * bitangents[tangent + 1] +
|
|
700
|
-
(nx * ty - ny * tx) * bitangents[tangent + 2] <
|
|
701
|
-
0
|
|
702
|
-
? -1
|
|
703
|
-
: 1;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
721
|
const CANONICAL_FLOATS_PER_VERTEX = 12;
|
|
707
722
|
// The canonical interleaved PBR vertex layout shared by every builder: position + normal +
|
|
708
723
|
// tangent(w = handedness) + uv0, stride 48 bytes.
|
|
@@ -813,11 +828,14 @@ const DODECAHEDRON_VERTS = [
|
|
|
813
828
|
[_phi, 0, -_d],
|
|
814
829
|
[_phi, 0, _d],
|
|
815
830
|
];
|
|
831
|
+
// Nine of these faces were wound the opposite way from the other twenty-seven, which put their
|
|
832
|
+
// geometric face normals against the outward vertex normals the builder authors. Only those nine
|
|
833
|
+
// are reversed; the winding here is per-face data, not a convention the whole table shares.
|
|
816
834
|
const DODECAHEDRON_FACES = [
|
|
817
835
|
[3, 11, 7],
|
|
818
836
|
[3, 7, 15],
|
|
819
837
|
[3, 15, 13],
|
|
820
|
-
[7,
|
|
838
|
+
[7, 11, 19],
|
|
821
839
|
[7, 11, 9],
|
|
822
840
|
[7, 9, 19],
|
|
823
841
|
[15, 7, 19],
|
|
@@ -832,21 +850,21 @@ const DODECAHEDRON_FACES = [
|
|
|
832
850
|
[11, 17, 1],
|
|
833
851
|
[11, 1, 9],
|
|
834
852
|
[9, 1, 5],
|
|
835
|
-
[5,
|
|
853
|
+
[5, 17, 1],
|
|
836
854
|
[5, 17, 4],
|
|
837
|
-
[5,
|
|
838
|
-
[9,
|
|
839
|
-
[9,
|
|
840
|
-
[9,
|
|
855
|
+
[5, 14, 4],
|
|
856
|
+
[9, 14, 5],
|
|
857
|
+
[9, 12, 14],
|
|
858
|
+
[9, 0, 12],
|
|
841
859
|
[1, 0, 12],
|
|
842
860
|
[1, 12, 4],
|
|
843
|
-
[1,
|
|
861
|
+
[1, 17, 4],
|
|
844
862
|
[6, 18, 8],
|
|
845
863
|
[6, 8, 10],
|
|
846
864
|
[6, 10, 2],
|
|
847
865
|
[18, 19, 8],
|
|
848
|
-
[19,
|
|
849
|
-
[7,
|
|
866
|
+
[19, 8, 7],
|
|
867
|
+
[7, 8, 15],
|
|
850
868
|
[2, 10, 16],
|
|
851
869
|
[10, 8, 0],
|
|
852
870
|
[0, 8, 12],
|