rayzee 7.31.0 → 8.0.0

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 (53) hide show
  1. package/README.md +164 -0
  2. package/dist/rayzee.es.js +6547 -4334
  3. package/dist/rayzee.umd.js +122 -73
  4. package/package.json +1 -1
  5. package/src/EngineDefaults.js +168 -13
  6. package/src/EngineIssues.js +6 -0
  7. package/src/PathTracerApp.js +128 -20
  8. package/src/Processor/ArchiveReader.js +538 -0
  9. package/src/Processor/AssetLoader.js +357 -22
  10. package/src/Processor/BVHBuilder.js +48 -32
  11. package/src/Processor/BVHRefitter.js +244 -99
  12. package/src/Processor/ChunkedRecords.js +302 -0
  13. package/src/Processor/EmissiveTriangleBuilder.js +48 -21
  14. package/src/Processor/GeometryExtractor.js +577 -92
  15. package/src/Processor/HostMemory.js +222 -0
  16. package/src/Processor/InstanceTable.js +548 -97
  17. package/src/Processor/PBRT/EqualAreaOctahedral.js +173 -0
  18. package/src/Processor/PBRT/LoopSubdivision.js +240 -0
  19. package/src/Processor/PBRT/PBRTCurves.js +249 -0
  20. package/src/Processor/PBRT/PBRTMath.js +28 -0
  21. package/src/Processor/PBRT/PBRTParser.js +187 -41
  22. package/src/Processor/PBRT/PBRTSceneBuilder.js +659 -34
  23. package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
  24. package/src/Processor/PBRT/index.js +306 -45
  25. package/src/Processor/PackedRayBuffer.js +29 -18
  26. package/src/Processor/ParallelBVHBuilder.js +9 -8
  27. package/src/Processor/QueueManager.js +5 -3
  28. package/src/Processor/SceneProcessor.js +1063 -366
  29. package/src/Processor/TLASBuilder.js +546 -201
  30. package/src/Processor/TextureCreator.js +11 -9
  31. package/src/Processor/VRAMTracker.js +12 -3
  32. package/src/Processor/Workers/BVHRefitWorker.js +16 -11
  33. package/src/Processor/Workers/BVHWorker.js +12 -12
  34. package/src/Processor/Workers/TLASWorker.js +37 -0
  35. package/src/Stages/NormalDepth.js +6 -2
  36. package/src/Stages/PathTracer.js +5 -5
  37. package/src/Stages/PathTracerStage.js +163 -41
  38. package/src/TSL/BVHTraversal.js +208 -90
  39. package/src/TSL/Common.js +135 -2
  40. package/src/TSL/Displacement.js +21 -11
  41. package/src/TSL/EmissiveSampling.js +49 -25
  42. package/src/TSL/ExtendKernel.js +3 -2
  43. package/src/TSL/LightBVHSampling.js +7 -4
  44. package/src/TSL/LightsDirect.js +30 -29
  45. package/src/TSL/ShadeKernel.js +8 -6
  46. package/src/TSL/Struct.js +3 -0
  47. package/src/TSL/TextureSampling.js +24 -9
  48. package/src/TSL/patches.js +128 -3
  49. package/src/index.js +3 -0
  50. package/src/managers/AnimationManager.js +86 -76
  51. package/src/managers/CameraManager.js +11 -0
  52. package/src/managers/MaterialDataManager.js +29 -23
  53. package/src/managers/TransformManager.js +9 -192
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Equal-area octahedral environment maps, as pbrt-v4 stores them.
3
+ *
4
+ * `LightSource "infinite" "string filename"` does NOT take a lat-long panorama.
5
+ * pbrt-v4's ImageInfiniteLight reads a square image through EqualAreaSphereToSquare:
6
+ * the upper hemisphere fills a centre diamond and the lower hemisphere the four
7
+ * corner triangles (`imgtool makeequiarea` / `makesky` write this). Sampling one as
8
+ * equirectangular smears the black corners over the whole sphere and puts the sun
9
+ * in the wrong place — the scene goes dark.
10
+ *
11
+ * The engine's environment path (CDF build, importance sampling, background) is
12
+ * equirectangular throughout, so convert once at load rather than teach every
13
+ * consumer a second projection. The light's own transform folds into the same
14
+ * resample, which is also how the engine gets a rotation it cannot otherwise
15
+ * express (its environmentRotation is a single Y angle; pbrt lights carry a full
16
+ * matrix).
17
+ */
18
+
19
+ import * as M from './PBRTMath.js';
20
+
21
+ /**
22
+ * pbrt-v4 `EqualAreaSphereToSquare`: unit direction → [0,1]² in the octahedral map.
23
+ * Uses Math.atan where pbrt uses a polynomial approximation — this runs once on the
24
+ * CPU, so take the exact value.
25
+ */
26
+ export function equalAreaSphereToSquare( dx, dy, dz ) {
27
+
28
+ const x = Math.abs( dx ), y = Math.abs( dy ), z = Math.abs( dz );
29
+ const r = Math.sqrt( Math.max( 0, 1 - z ) );
30
+
31
+ const a = Math.max( x, y );
32
+ const b = a === 0 ? 0 : Math.min( x, y ) / a;
33
+
34
+ let phi = Math.atan( b ) * ( 2 / Math.PI );
35
+ if ( x < y ) phi = 1 - phi;
36
+
37
+ let v = phi * r;
38
+ let u = r - v;
39
+
40
+ if ( dz < 0 ) {
41
+
42
+ const prevU = u;
43
+ u = 1 - v;
44
+ v = 1 - prevU;
45
+
46
+ }
47
+
48
+ if ( dx < 0 ) u = - u;
49
+ if ( dy < 0 ) v = - v;
50
+
51
+ return [ 0.5 * ( u + 1 ), 0.5 * ( v + 1 ) ];
52
+
53
+ }
54
+
55
+ /** 8-bit sRGB → linear, tabulated so a per-tap decode costs a lookup rather than a pow(). */
56
+ const SRGB_TO_LINEAR = ( () => {
57
+
58
+ const t = new Float32Array( 256 );
59
+ for ( let i = 0; i < 256; i ++ ) {
60
+
61
+ const c = i / 255;
62
+ t[ i ] = c <= 0.04045 ? c / 12.92 : Math.pow( ( c + 0.055 ) / 1.055, 2.4 );
63
+
64
+ }
65
+
66
+ return t;
67
+
68
+ } )();
69
+
70
+ /**
71
+ * Bilinear fetch from a tightly packed image, clamped at the edges.
72
+ * `table` maps stored values to linear before filtering; null reads them as-is.
73
+ */
74
+ function sampleBilinear( data, width, height, channels, u, v, out, table ) {
75
+
76
+ const fx = Math.min( Math.max( u * width - 0.5, 0 ), width - 1 );
77
+ const fy = Math.min( Math.max( v * height - 0.5, 0 ), height - 1 );
78
+ const x0 = Math.floor( fx ), y0 = Math.floor( fy );
79
+ const x1 = Math.min( x0 + 1, width - 1 ), y1 = Math.min( y0 + 1, height - 1 );
80
+ const tx = fx - x0, ty = fy - y0;
81
+
82
+ const i00 = ( y0 * width + x0 ) * channels;
83
+ const i10 = ( y0 * width + x1 ) * channels;
84
+ const i01 = ( y1 * width + x0 ) * channels;
85
+ const i11 = ( y1 * width + x1 ) * channels;
86
+
87
+ for ( let c = 0; c < 3; c ++ ) {
88
+
89
+ const a = table ? table[ data[ i00 + c ] ] : data[ i00 + c ];
90
+ const b = table ? table[ data[ i10 + c ] ] : data[ i10 + c ];
91
+ const d = table ? table[ data[ i01 + c ] ] : data[ i01 + c ];
92
+ const e = table ? table[ data[ i11 + c ] ] : data[ i11 + c ];
93
+ const top = a * ( 1 - tx ) + b * tx;
94
+ const bottom = d * ( 1 - tx ) + e * tx;
95
+ out[ c ] = top * ( 1 - ty ) + bottom * ty;
96
+
97
+ }
98
+
99
+ }
100
+
101
+ /**
102
+ * Resample a square equal-area octahedral map into an equirectangular RGBA float image.
103
+ *
104
+ * The output uses the engine's own mapping (Environment.js): u = atan2(z,x)/2pi + 0.5,
105
+ * v = 1 - acos(y)/pi, with row 0 at v = 1 so "up" lands at the top like any other HDRI.
106
+ *
107
+ * @param {{data: ArrayLike<number>, width: number, height: number, channels: number,
108
+ * bottomUp?: boolean, srgb?: boolean}} src - `bottomUp` when row 0 holds the BOTTOM of the
109
+ * image, which is what three's EXR/HDR loaders produce; pbrt indexes the square top-down, and
110
+ * reading it the wrong way round turns out to rotate the sky 180° about the light's z axis.
111
+ * `srgb` when the samples are 8-bit sRGB, as a PNG read back through a canvas is.
112
+ * @param {number[]} lightToWorld - the light's pbrt CTM (column-major 16), or null
113
+ * @param {number} scale - the light's `scale` parameter, baked into the output
114
+ * @param {number} [outWidth] - defaults to the source width, capped at 4096
115
+ * @returns {{data: Float32Array, width: number, height: number}}
116
+ */
117
+ export function octahedralToEquirect( src, lightToWorld, scale = 1, outWidth = 0 ) {
118
+
119
+ const width = outWidth || Math.min( src.width, 4096 );
120
+ const height = Math.max( 1, width >> 1 );
121
+ const out = new Float32Array( width * height * 4 );
122
+
123
+ // Directions go world → light, the opposite of the CTM, matching pbrt's
124
+ // renderFromLight.ApplyInverse(ray.d) before the square lookup.
125
+ const worldToLight = lightToWorld ? M.invert( lightToWorld ) : null;
126
+ const table = src.srgb ? SRGB_TO_LINEAR : null;
127
+ const rgb = [ 0, 0, 0 ];
128
+
129
+ for ( let j = 0; j < height; j ++ ) {
130
+
131
+ // Row index IS the v axis the engine samples on, so row 0 is v = 0 (straight down)
132
+ // and the last row is v = 1 (up). Verified against a rendered frame — inverting
133
+ // this turns the sky upside down with no other visible symptom.
134
+ const v = ( j + 0.5 ) / height;
135
+ const polar = ( 1 - v ) * Math.PI;
136
+ const sinPolar = Math.sin( polar ), cosPolar = Math.cos( polar );
137
+
138
+ for ( let i = 0; i < width; i ++ ) {
139
+
140
+ const u = ( i + 0.5 ) / width;
141
+ const azimuth = ( u - 0.5 ) * 2 * Math.PI;
142
+
143
+ let dx = sinPolar * Math.cos( azimuth );
144
+ let dy = cosPolar;
145
+ let dz = sinPolar * Math.sin( azimuth );
146
+
147
+ if ( worldToLight ) {
148
+
149
+ const m = worldToLight;
150
+ const tx = m[ 0 ] * dx + m[ 4 ] * dy + m[ 8 ] * dz;
151
+ const ty = m[ 1 ] * dx + m[ 5 ] * dy + m[ 9 ] * dz;
152
+ const tz = m[ 2 ] * dx + m[ 6 ] * dy + m[ 10 ] * dz;
153
+ const len = Math.hypot( tx, ty, tz ) || 1;
154
+ dx = tx / len; dy = ty / len; dz = tz / len;
155
+
156
+ }
157
+
158
+ const [ su, sv ] = equalAreaSphereToSquare( dx, dy, dz );
159
+ sampleBilinear( src.data, src.width, src.height, src.channels, su, src.bottomUp ? 1 - sv : sv, rgb, table );
160
+
161
+ const o = ( j * width + i ) * 4;
162
+ out[ o ] = rgb[ 0 ] * scale;
163
+ out[ o + 1 ] = rgb[ 1 ] * scale;
164
+ out[ o + 2 ] = rgb[ 2 ] * scale;
165
+ out[ o + 3 ] = 1;
166
+
167
+ }
168
+
169
+ }
170
+
171
+ return { data: out, width, height };
172
+
173
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Loop subdivision for pbrt's `loopsubdiv` shape.
3
+ *
4
+ * Mirrors pbrt-v4's loopsubdiv.cpp: Warren's simplified beta for the even-vertex
5
+ * one-ring rule, 1/8 for the boundary rule, 3/8–1/8 for interior edge points, and a
6
+ * final push onto the limit surface. Scenes use this shape for exactly the objects
7
+ * that read wrong as a coarse cage — a tub, a cushion, a curtain — so approximating
8
+ * it by its control mesh is visibly not the same model.
9
+ *
10
+ * Topology is rebuilt per level from the index buffer alone; no half-edge structure,
11
+ * since the meshes involved are control cages (thousands of faces, not millions).
12
+ */
13
+
14
+ /** pbrt: Beta(valence) — the one-ring weight for an interior even vertex. */
15
+ function beta( valence ) {
16
+
17
+ return valence === 3 ? 3 / 16 : 3 / ( 8 * valence );
18
+
19
+ }
20
+
21
+ /** pbrt: LoopGamma(valence) — the one-ring weight for the limit-surface push. */
22
+ function loopGamma( valence ) {
23
+
24
+ return 1 / ( valence + 3 / ( 8 * beta( valence ) ) );
25
+
26
+ }
27
+
28
+ /**
29
+ * Edge records plus per-vertex neighbour rings.
30
+ *
31
+ * A vertex is on the boundary when it touches an edge with a single adjacent face;
32
+ * `boundaryRing` keeps only those neighbours, because the boundary rule weights the
33
+ * two vertices along the boundary curve, not the whole ring.
34
+ */
35
+ function buildTopology( nv, indices ) {
36
+
37
+ const edgeIndex = new Map();
38
+ const edgeV0 = [], edgeV1 = [], edgeOpp0 = [], edgeOpp1 = [], edgeFaces = [];
39
+ const faceCount = indices.length / 3;
40
+
41
+ for ( let f = 0; f < faceCount; f ++ ) {
42
+
43
+ for ( let e = 0; e < 3; e ++ ) {
44
+
45
+ const a = indices[ f * 3 + e ];
46
+ const b = indices[ f * 3 + ( e + 1 ) % 3 ];
47
+ const opposite = indices[ f * 3 + ( e + 2 ) % 3 ];
48
+ const lo = a < b ? a : b;
49
+ const hi = a < b ? b : a;
50
+ const key = lo * nv + hi;
51
+
52
+ const existing = edgeIndex.get( key );
53
+ if ( existing === undefined ) {
54
+
55
+ edgeIndex.set( key, edgeV0.length );
56
+ edgeV0.push( lo );
57
+ edgeV1.push( hi );
58
+ edgeOpp0.push( opposite );
59
+ edgeOpp1.push( - 1 );
60
+ edgeFaces.push( 1 );
61
+
62
+ } else {
63
+
64
+ if ( edgeOpp1[ existing ] < 0 ) edgeOpp1[ existing ] = opposite;
65
+ edgeFaces[ existing ] ++;
66
+
67
+ }
68
+
69
+ }
70
+
71
+ }
72
+
73
+ const ring = Array.from( { length: nv }, () => [] );
74
+ const boundaryRing = Array.from( { length: nv }, () => [] );
75
+
76
+ for ( let i = 0; i < edgeV0.length; i ++ ) {
77
+
78
+ const v0 = edgeV0[ i ], v1 = edgeV1[ i ];
79
+ ring[ v0 ].push( v1 );
80
+ ring[ v1 ].push( v0 );
81
+
82
+ if ( edgeFaces[ i ] === 1 ) {
83
+
84
+ boundaryRing[ v0 ].push( v1 );
85
+ boundaryRing[ v1 ].push( v0 );
86
+
87
+ }
88
+
89
+ }
90
+
91
+ return { edgeIndex, edgeV0, edgeV1, edgeOpp0, edgeOpp1, edgeFaces, ring, boundaryRing };
92
+
93
+ }
94
+
95
+ /**
96
+ * Reposition every vertex by a one-ring weighting: (1 - n·w)·v + w·Σring for an
97
+ * interior vertex, (1 - 2w)·v + w·(both boundary neighbours) on a boundary. Used for
98
+ * both the even-vertex step and the limit push, which differ only in the weights.
99
+ */
100
+ function weightVertices( P, topology, nv, interiorWeight, boundaryWeight ) {
101
+
102
+ const out = new Float64Array( P.length );
103
+
104
+ for ( let v = 0; v < nv; v ++ ) {
105
+
106
+ const base = v * 3;
107
+ const boundary = topology.boundaryRing[ v ];
108
+
109
+ // Anything other than a clean two-neighbour boundary (a wire edge, a
110
+ // non-manifold fan) has no defined rule — leave the vertex where it is.
111
+ if ( boundary.length === 2 ) {
112
+
113
+ const a = boundary[ 0 ] * 3, b = boundary[ 1 ] * 3;
114
+ const centre = 1 - 2 * boundaryWeight;
115
+ for ( let k = 0; k < 3; k ++ ) {
116
+
117
+ out[ base + k ] = centre * P[ base + k ] + boundaryWeight * ( P[ a + k ] + P[ b + k ] );
118
+
119
+ }
120
+
121
+ continue;
122
+
123
+ }
124
+
125
+ const ring = topology.ring[ v ];
126
+ const n = ring.length;
127
+ if ( n === 0 || boundary.length > 0 ) {
128
+
129
+ for ( let k = 0; k < 3; k ++ ) out[ base + k ] = P[ base + k ];
130
+ continue;
131
+
132
+ }
133
+
134
+ const w = interiorWeight( n );
135
+ let sx = 0, sy = 0, sz = 0;
136
+ for ( let i = 0; i < n; i ++ ) {
137
+
138
+ const r = ring[ i ] * 3;
139
+ sx += P[ r ];
140
+ sy += P[ r + 1 ];
141
+ sz += P[ r + 2 ];
142
+
143
+ }
144
+
145
+ const centre = 1 - n * w;
146
+ out[ base ] = centre * P[ base ] + w * sx;
147
+ out[ base + 1 ] = centre * P[ base + 1 ] + w * sy;
148
+ out[ base + 2 ] = centre * P[ base + 2 ] + w * sz;
149
+
150
+ }
151
+
152
+ return out;
153
+
154
+ }
155
+
156
+ /** One subdivision step: 4 faces per face, one new vertex per edge. */
157
+ function subdivideOnce( P, indices ) {
158
+
159
+ const nv = P.length / 3;
160
+ const topology = buildTopology( nv, indices );
161
+ const edgeCount = topology.edgeV0.length;
162
+
163
+ const even = weightVertices( P, topology, nv, beta, 1 / 8 );
164
+ const out = new Float64Array( ( nv + edgeCount ) * 3 );
165
+ out.set( even );
166
+
167
+ for ( let i = 0; i < edgeCount; i ++ ) {
168
+
169
+ const v0 = topology.edgeV0[ i ] * 3;
170
+ const v1 = topology.edgeV1[ i ] * 3;
171
+ const dst = ( nv + i ) * 3;
172
+ const opp1 = topology.edgeOpp1[ i ];
173
+
174
+ if ( opp1 < 0 ) {
175
+
176
+ for ( let k = 0; k < 3; k ++ ) out[ dst + k ] = 0.5 * ( P[ v0 + k ] + P[ v1 + k ] );
177
+
178
+ } else {
179
+
180
+ const o0 = topology.edgeOpp0[ i ] * 3, o1 = opp1 * 3;
181
+ for ( let k = 0; k < 3; k ++ ) {
182
+
183
+ out[ dst + k ] = 0.375 * ( P[ v0 + k ] + P[ v1 + k ] ) + 0.125 * ( P[ o0 + k ] + P[ o1 + k ] );
184
+
185
+ }
186
+
187
+ }
188
+
189
+ }
190
+
191
+ const faceCount = indices.length / 3;
192
+ const outIndices = new Int32Array( faceCount * 12 );
193
+ const midpoint = ( a, b ) => nv + topology.edgeIndex.get( ( a < b ? a : b ) * nv + ( a < b ? b : a ) );
194
+
195
+ for ( let f = 0; f < faceCount; f ++ ) {
196
+
197
+ const a = indices[ f * 3 ], b = indices[ f * 3 + 1 ], c = indices[ f * 3 + 2 ];
198
+ const ab = midpoint( a, b ), bc = midpoint( b, c ), ca = midpoint( c, a );
199
+ const o = f * 12;
200
+
201
+ outIndices[ o ] = a; outIndices[ o + 1 ] = ab; outIndices[ o + 2 ] = ca;
202
+ outIndices[ o + 3 ] = ab; outIndices[ o + 4 ] = b; outIndices[ o + 5 ] = bc;
203
+ outIndices[ o + 6 ] = ca; outIndices[ o + 7 ] = bc; outIndices[ o + 8 ] = c;
204
+ outIndices[ o + 9 ] = ab; outIndices[ o + 10 ] = bc; outIndices[ o + 11 ] = ca;
205
+
206
+ }
207
+
208
+ return { positions: out, indices: outIndices };
209
+
210
+ }
211
+
212
+ /**
213
+ * @param {ArrayLike<number>} P - control-cage positions, 3 per vertex
214
+ * @param {ArrayLike<number>} indices - control-cage triangles, 3 per face
215
+ * @param {number} levels - refinement steps; each multiplies face count by 4
216
+ * @param {number} [maxTriangles=2000000] - stop refining early rather than exhaust memory
217
+ * @returns {{positions: Float32Array, indices: Uint32Array, levels: number}} levels actually applied
218
+ */
219
+ export function loopSubdivide( P, indices, levels, maxTriangles = 2000000 ) {
220
+
221
+ let positions = Float64Array.from( P );
222
+ let faces = Int32Array.from( indices );
223
+ let applied = 0;
224
+
225
+ for ( let i = 0; i < levels; i ++ ) {
226
+
227
+ if ( faces.length / 3 * 4 > maxTriangles ) break;
228
+ const step = subdivideOnce( positions, faces );
229
+ positions = step.positions;
230
+ faces = step.indices;
231
+ applied ++;
232
+
233
+ }
234
+
235
+ const nv = positions.length / 3;
236
+ const limit = weightVertices( positions, buildTopology( nv, faces ), nv, loopGamma, 1 / 5 );
237
+
238
+ return { positions: Float32Array.from( limit ), indices: Uint32Array.from( faces ), levels: applied };
239
+
240
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Tessellation for pbrt-v4's `Shape "curve"`.
3
+ *
4
+ * pbrt intersects curves analytically as swept cubic splines. The engine only traces
5
+ * triangles, so each curve becomes a strip: one ribbon for `flat`/`ribbon`, a crossed
6
+ * pair for `cylinder` (view-independent and a quarter the cost of a real tube), or a
7
+ * closed tube when asked for one.
8
+ *
9
+ * Cost is the whole story here — Moana's isMountainB ground cover alone declares 5.18
10
+ * million curve shapes, so `steps` and `sides` are the knobs that decide whether a
11
+ * scene is 20 million triangles or 200 million.
12
+ */
13
+
14
+ const DEFAULT_STEPS = 2;
15
+
16
+ /** Uniform cubic B-spline basis at t for the four control points of one span. */
17
+ function bsplineWeights( t, out ) {
18
+
19
+ const t2 = t * t;
20
+ const t3 = t2 * t;
21
+ out[ 0 ] = ( 1 - 3 * t + 3 * t2 - t3 ) / 6;
22
+ out[ 1 ] = ( 4 - 6 * t2 + 3 * t3 ) / 6;
23
+ out[ 2 ] = ( 1 + 3 * t + 3 * t2 - 3 * t3 ) / 6;
24
+ out[ 3 ] = t3 / 6;
25
+
26
+ }
27
+
28
+ /** Cubic Bezier basis at t. */
29
+ function bezierWeights( t, out ) {
30
+
31
+ const u = 1 - t;
32
+ out[ 0 ] = u * u * u;
33
+ out[ 1 ] = 3 * u * u * t;
34
+ out[ 2 ] = 3 * u * t * t;
35
+ out[ 3 ] = t * t * t;
36
+
37
+ }
38
+
39
+ function normalize( v ) {
40
+
41
+ const l = Math.hypot( v[ 0 ], v[ 1 ], v[ 2 ] );
42
+ if ( l > 0 ) {
43
+
44
+ v[ 0 ] /= l; v[ 1 ] /= l; v[ 2 ] /= l;
45
+
46
+ }
47
+
48
+ return v;
49
+
50
+ }
51
+
52
+ function cross( a, b, out ) {
53
+
54
+ out[ 0 ] = a[ 1 ] * b[ 2 ] - a[ 2 ] * b[ 1 ];
55
+ out[ 1 ] = a[ 2 ] * b[ 0 ] - a[ 0 ] * b[ 2 ];
56
+ out[ 2 ] = a[ 0 ] * b[ 1 ] - a[ 1 ] * b[ 0 ];
57
+ return out;
58
+
59
+ }
60
+
61
+ /**
62
+ * Sample a curve's centreline.
63
+ * @returns {{points: Float64Array, count: number}} count points, 3 floats each
64
+ */
65
+ function sampleCenterline( P, basis, steps ) {
66
+
67
+ const cps = P.length / 3;
68
+ // bspline: every window of 4 control points is one span. bezier: 4 points per span,
69
+ // sharing an endpoint between consecutive spans.
70
+ const spans = basis === 'bspline' ? cps - 3 : Math.max( 1, ( cps - 1 ) / 3 );
71
+ if ( spans < 1 ) return null;
72
+
73
+ const total = Math.max( 1, Math.round( steps * spans ) );
74
+ const points = new Float64Array( ( total + 1 ) * 3 );
75
+ const w = [ 0, 0, 0, 0 ];
76
+
77
+ for ( let i = 0; i <= total; i ++ ) {
78
+
79
+ const u = ( i / total ) * spans;
80
+ let span = Math.floor( u );
81
+ if ( span >= spans ) span = spans - 1;
82
+ const t = u - span;
83
+
84
+ if ( basis === 'bspline' ) bsplineWeights( t, w );
85
+ else bezierWeights( t, w );
86
+
87
+ const base = basis === 'bspline' ? span : span * 3;
88
+ let x = 0, y = 0, z = 0;
89
+ for ( let k = 0; k < 4; k ++ ) {
90
+
91
+ const o = ( base + k ) * 3;
92
+ x += w[ k ] * P[ o ];
93
+ y += w[ k ] * P[ o + 1 ];
94
+ z += w[ k ] * P[ o + 2 ];
95
+
96
+ }
97
+
98
+ points[ i * 3 ] = x; points[ i * 3 + 1 ] = y; points[ i * 3 + 2 ] = z;
99
+
100
+ }
101
+
102
+ return { points, count: total + 1 };
103
+
104
+ }
105
+
106
+ /**
107
+ * @param {object} spec
108
+ * @param {ArrayLike<number>} spec.P - control points, 3 floats each
109
+ * @param {string} [spec.basis='bezier'] - 'bezier' | 'bspline'
110
+ * @param {number} [spec.width0=1]
111
+ * @param {number} [spec.width1=1]
112
+ * @param {ArrayLike<number>} [spec.N] - ribbon normals (start, end), 3 floats each
113
+ * @param {number} [spec.steps] - samples per spline span
114
+ * @param {number} [spec.sides=1] - 1 ribbon, 2 crossed ribbons, >=3 closed tube
115
+ * @returns {{positions: Float32Array, indices: Uint32Array}|null}
116
+ */
117
+ export function tessellateCurve( spec ) {
118
+
119
+ const { P, basis = 'bezier', width0 = 1, width1 = 1, N = null } = spec;
120
+ const steps = Math.max( 1, Math.round( spec.steps ?? DEFAULT_STEPS ) );
121
+ const sides = Math.max( 1, Math.round( spec.sides ?? 1 ) );
122
+
123
+ if ( ! P || P.length < 12 ) return null;
124
+
125
+ const line = sampleCenterline( P, basis, steps );
126
+ if ( ! line ) return null;
127
+
128
+ const { points, count } = line;
129
+ const rings = sides >= 3 ? sides : sides * 2;
130
+ const positions = new Float32Array( count * rings * 3 );
131
+
132
+ const tangent = [ 0, 0, 0 ];
133
+ const side = [ 0, 0, 0 ];
134
+ const up = [ 0, 0, 0 ];
135
+ const tmp = [ 0, 0, 0 ];
136
+ let haveSide = false;
137
+
138
+ for ( let i = 0; i < count; i ++ ) {
139
+
140
+ const a = Math.max( 0, i - 1 ), b = Math.min( count - 1, i + 1 );
141
+ tangent[ 0 ] = points[ b * 3 ] - points[ a * 3 ];
142
+ tangent[ 1 ] = points[ b * 3 + 1 ] - points[ a * 3 + 1 ];
143
+ tangent[ 2 ] = points[ b * 3 + 2 ] - points[ a * 3 + 2 ];
144
+ normalize( tangent );
145
+
146
+ if ( ! haveSide ) {
147
+
148
+ // Seed from the ribbon normal when the scene gives one, else from whichever
149
+ // axis the tangent leans on least.
150
+ if ( N && N.length >= 3 ) {
151
+
152
+ side[ 0 ] = N[ 0 ]; side[ 1 ] = N[ 1 ]; side[ 2 ] = N[ 2 ];
153
+ cross( tangent, side, tmp );
154
+ side[ 0 ] = tmp[ 0 ]; side[ 1 ] = tmp[ 1 ]; side[ 2 ] = tmp[ 2 ];
155
+
156
+ } else {
157
+
158
+ const ax = Math.abs( tangent[ 0 ] ), ay = Math.abs( tangent[ 1 ] ), az = Math.abs( tangent[ 2 ] );
159
+ const ref = ax < ay && ax < az ? [ 1, 0, 0 ] : ay < az ? [ 0, 1, 0 ] : [ 0, 0, 1 ];
160
+ cross( tangent, ref, side );
161
+
162
+ }
163
+
164
+ if ( Math.hypot( side[ 0 ], side[ 1 ], side[ 2 ] ) < 1e-9 ) cross( tangent, [ 0, 1, 0 ], side );
165
+ normalize( side );
166
+ haveSide = true;
167
+
168
+ } else {
169
+
170
+ // Rotation-minimising: drop the previous side back onto the new normal plane,
171
+ // so a coiled curve does not twist its ribbon.
172
+ const d = side[ 0 ] * tangent[ 0 ] + side[ 1 ] * tangent[ 1 ] + side[ 2 ] * tangent[ 2 ];
173
+ side[ 0 ] -= d * tangent[ 0 ];
174
+ side[ 1 ] -= d * tangent[ 1 ];
175
+ side[ 2 ] -= d * tangent[ 2 ];
176
+ if ( Math.hypot( side[ 0 ], side[ 1 ], side[ 2 ] ) < 1e-9 ) cross( tangent, [ 0, 1, 0 ], side );
177
+ normalize( side );
178
+
179
+ }
180
+
181
+ cross( tangent, side, up );
182
+ normalize( up );
183
+
184
+ const u = i / ( count - 1 || 1 );
185
+ const radius = ( width0 + ( width1 - width0 ) * u ) * 0.5;
186
+ const cx = points[ i * 3 ], cy = points[ i * 3 + 1 ], cz = points[ i * 3 + 2 ];
187
+
188
+ for ( let r = 0; r < rings; r ++ ) {
189
+
190
+ let dx, dy, dz;
191
+ if ( sides >= 3 ) {
192
+
193
+ const a2 = ( r / sides ) * Math.PI * 2;
194
+ const c = Math.cos( a2 ), s = Math.sin( a2 );
195
+ dx = side[ 0 ] * c + up[ 0 ] * s;
196
+ dy = side[ 1 ] * c + up[ 1 ] * s;
197
+ dz = side[ 2 ] * c + up[ 2 ] * s;
198
+
199
+ } else {
200
+
201
+ // Ribbon r spans -1..+1 along its own axis: `side` first, `up` for the cross.
202
+ const axis = r < 2 ? side : up;
203
+ const sign = r % 2 === 0 ? - 1 : 1;
204
+ dx = axis[ 0 ] * sign; dy = axis[ 1 ] * sign; dz = axis[ 2 ] * sign;
205
+
206
+ }
207
+
208
+ const o = ( i * rings + r ) * 3;
209
+ positions[ o ] = cx + dx * radius;
210
+ positions[ o + 1 ] = cy + dy * radius;
211
+ positions[ o + 2 ] = cz + dz * radius;
212
+
213
+ }
214
+
215
+ }
216
+
217
+ const quads = sides >= 3 ? sides : sides;
218
+ const indices = new Uint32Array( ( count - 1 ) * quads * 6 );
219
+ let w = 0;
220
+
221
+ for ( let i = 0; i < count - 1; i ++ ) {
222
+
223
+ for ( let q = 0; q < quads; q ++ ) {
224
+
225
+ const r0 = sides >= 3 ? q : q * 2;
226
+ const r1 = sides >= 3 ? ( q + 1 ) % sides : q * 2 + 1;
227
+ const a = i * rings + r0, b = i * rings + r1;
228
+ const c = ( i + 1 ) * rings + r1, d = ( i + 1 ) * rings + r0;
229
+ indices[ w ++ ] = a; indices[ w ++ ] = b; indices[ w ++ ] = c;
230
+ indices[ w ++ ] = a; indices[ w ++ ] = c; indices[ w ++ ] = d;
231
+
232
+ }
233
+
234
+ }
235
+
236
+ return { positions, indices: w === indices.length ? indices : indices.slice( 0, w ) };
237
+
238
+ }
239
+
240
+ /** Triangles `tessellateCurve` would produce, without building anything. */
241
+ export function curveTriangleCount( controlPoints, basis, steps, sides ) {
242
+
243
+ const cps = controlPoints / 3;
244
+ const spans = basis === 'bspline' ? cps - 3 : Math.max( 1, ( cps - 1 ) / 3 );
245
+ if ( spans < 1 ) return 0;
246
+ const segments = Math.max( 1, Math.round( Math.max( 1, steps ) * spans ) );
247
+ return segments * Math.max( 1, Math.round( sides ) ) * 2;
248
+
249
+ }
@@ -17,7 +17,35 @@ export function identity() {
17
17
 
18
18
  }
19
19
 
20
+ /** Determinant of the upper-left 3x3 basis. Negative means the transform mirrors. */
21
+ export function determinant3( m ) {
22
+
23
+ return m[ 0 ] * ( m[ 5 ] * m[ 10 ] - m[ 6 ] * m[ 9 ] )
24
+ - m[ 4 ] * ( m[ 1 ] * m[ 10 ] - m[ 2 ] * m[ 9 ] )
25
+ + m[ 8 ] * ( m[ 1 ] * m[ 6 ] - m[ 2 ] * m[ 5 ] );
26
+
27
+ }
28
+
20
29
  /** Matrix product a*b (applies b first, then a, to a column vector). */
30
+ /** a*b written into `out`, so a hot loop over millions of placements allocates nothing. */
31
+ export function multiplyInto( out, a, aOff, b ) {
32
+
33
+ for ( let c = 0; c < 4; c ++ ) {
34
+
35
+ const b0 = b[ c * 4 ], b1 = b[ c * 4 + 1 ], b2 = b[ c * 4 + 2 ], b3 = b[ c * 4 + 3 ];
36
+ for ( let r = 0; r < 4; r ++ ) {
37
+
38
+ out[ c * 4 + r ] = a[ aOff + r ] * b0 + a[ aOff + 4 + r ] * b1
39
+ + a[ aOff + 8 + r ] * b2 + a[ aOff + 12 + r ] * b3;
40
+
41
+ }
42
+
43
+ }
44
+
45
+ return out;
46
+
47
+ }
48
+
21
49
  export function multiply( a, b ) {
22
50
 
23
51
  const a11 = a[ 0 ], a21 = a[ 1 ], a31 = a[ 2 ], a41 = a[ 3 ];