rayzee 7.4.1 → 7.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.4.1",
3
+ "version": "7.5.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -1446,12 +1446,18 @@ export class AssetLoader extends EventDispatcher {
1446
1446
 
1447
1447
  if ( userData.type === 'RectAreaLight' ) {
1448
1448
 
1449
+ // Intensity is authored in Watts (Blender-style); emitted radiance
1450
+ // is computed at render time as power/(π·area). Blender emission
1451
+ // defaults: power-normalized, full Lambertian spread (π), rectangle.
1449
1452
  const light = new RectAreaLight(
1450
1453
  new Color( ...userData.color ),
1451
- userData.intensity * 0.1 / Math.PI, // Adjust intensity for better visual results
1454
+ userData.intensity * 0.1,
1452
1455
  userData.width,
1453
1456
  userData.height
1454
1457
  );
1458
+ light.userData.normalize = userData.normalize ?? true;
1459
+ light.userData.spread = Number.isFinite( userData.spread ) ? userData.spread : Math.PI;
1460
+ light.userData.shape = userData.shape ?? 'rectangle';
1455
1461
  light.position.z = - 2;
1456
1462
  light.name = userData.name;
1457
1463
  object.add( light );
@@ -1,4 +1,31 @@
1
1
  import { Vector3, Quaternion } from 'three';
2
+ import { blackbodyToLinearRGB } from './blackbody.js';
3
+
4
+ // Point & spot lights specify Power in Watts; the shader uses radiant intensity
5
+ // (W/sr) as `intensity / dist²`, so convert with I = P / 4π (isotropic emitter),
6
+ // matching Blender. Area lights are already radiant power; the Sun is irradiance.
7
+ const INV_4PI = 1 / ( 4 * Math.PI );
8
+
9
+ // Effective emission colour + intensity after per-light Exposure (EV stops) and
10
+ // optional blackbody Temperature tint (Blender-style). Both fold into the values
11
+ // already sent to the GPU — no shader or struct change needed.
12
+ function effectiveEmission( light ) {
13
+
14
+ const ud = light.userData || {};
15
+ const exposure = Number.isFinite( ud.exposure ) ? ud.exposure : 0;
16
+ const intensity = light.intensity * Math.pow( 2, exposure );
17
+
18
+ let r = light.color.r, g = light.color.g, b = light.color.b;
19
+ if ( ud.useTemperature ) {
20
+
21
+ const [ tr, tg, tb ] = blackbodyToLinearRGB( ud.temperature ?? 6500 );
22
+ r *= tr; g *= tg; b *= tb;
23
+
24
+ }
25
+
26
+ return { r, g, b, intensity };
27
+
28
+ }
2
29
 
3
30
  export class LightSerializer {
4
31
 
@@ -90,6 +117,8 @@ export class LightSerializer {
90
117
  // Get angle parameter from light (default to 0 for sharp shadows)
91
118
  const angle = light.userData.angle || light.angle || 0.0; // In radians
92
119
 
120
+ const em = effectiveEmission( light );
121
+
93
122
  // Optional projection mask. Sign of intensity carries the inverted flag.
94
123
  const gobo = light.userData?.gobo;
95
124
  const goboIndex = ( gobo && Number.isInteger( gobo.index ) ) ? gobo.index : - 1;
@@ -101,8 +130,8 @@ export class LightSerializer {
101
130
  this.directionalLightCache.push( {
102
131
  data: [
103
132
  direction.x, direction.y, direction.z, // direction toward light (3)
104
- light.color.r, light.color.g, light.color.b, // color (3)
105
- light.intensity, // intensity (1)
133
+ em.r, em.g, em.b, // color (3) — incl. temperature tint
134
+ em.intensity, // intensity (1) — incl. exposure
106
135
  angle, // angular diameter in radians (1)
107
136
  goboIndex, // gobo layer index, -1 = none (1)
108
137
  goboIntensity, // signed gobo strength (1)
@@ -133,14 +162,25 @@ export class LightSerializer {
133
162
  // Calculate importance for sorting
134
163
  const importance = this.calculateLightImportance( light, 'area' );
135
164
 
136
- // Store in cache with importance
165
+ // Blender-style emission controls (stored on userData; sensible defaults).
166
+ // normalize ON → radiance ∝ 1/area (resizing keeps total power constant).
167
+ const normalize = ( light.userData?.normalize ?? true ) ? 1.0 : 0.0;
168
+ const spread = Number.isFinite( light.userData?.spread ) ? light.userData.spread : Math.PI;
169
+ const shape = ( light.userData?.shape === 'ellipse' || light.userData?.shape === 'disk' || light.userData?.shape === 1 ) ? 1.0 : 0.0;
170
+
171
+ const em = effectiveEmission( light );
172
+
173
+ // Store in cache with importance (16 floats, vec4-aligned)
137
174
  this.areaLightCache.push( {
138
175
  data: [
139
176
  position.x, position.y, position.z, // position (3)
140
- u.x, u.y, u.z, // u vector (3)
141
- v.x, v.y, v.z, // v vector (3)
142
- light.color.r, light.color.g, light.color.b, // color (3)
143
- light.intensity // intensity (1)
177
+ u.x, u.y, u.z, // u half-vector (3)
178
+ v.x, v.y, v.z, // v half-vector (3)
179
+ em.r, em.g, em.b, // color (3) — incl. temperature tint
180
+ em.intensity, // radiant power in Watts (1) — incl. exposure
181
+ normalize, // power-normalize flag (1)
182
+ spread, // emission spread in radians (1)
183
+ shape, // 0 = rect, 1 = disk/ellipse (1)
144
184
  ],
145
185
  importance: importance,
146
186
  light: light
@@ -159,12 +199,14 @@ export class LightSerializer {
159
199
  // Calculate importance for sorting
160
200
  const importance = this.calculateLightImportance( light, 'point' );
161
201
 
202
+ const em = effectiveEmission( light );
203
+
162
204
  // Store in cache with importance
163
205
  this.pointLightCache.push( {
164
206
  data: [
165
207
  position.x, position.y, position.z, // position (3)
166
- light.color.r, light.color.g, light.color.b, // color (3)
167
- light.intensity, // intensity (1)
208
+ em.r, em.g, em.b, // color (3) — incl. temperature tint
209
+ em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
168
210
  light.distance || 0.0, // cutoff distance (0 = infinite) (1)
169
211
  light.decay !== undefined ? light.decay : 2.0 // decay exponent (1)
170
212
  ],
@@ -198,13 +240,15 @@ export class LightSerializer {
198
240
  const iesIndex = ( ies && Number.isInteger( ies.index ) ) ? ies.index : - 1;
199
241
  const iesIntensity = ( ies && typeof ies.intensity === 'number' ) ? ies.intensity : 1.0;
200
242
 
243
+ const em = effectiveEmission( light );
244
+
201
245
  // Store in cache with importance
202
246
  this.spotLightCache.push( {
203
247
  data: [
204
248
  position.x, position.y, position.z, // position (3)
205
249
  direction.x, direction.y, direction.z, // direction (3)
206
- light.color.r, light.color.g, light.color.b, // color (3)
207
- light.intensity, // intensity (1)
250
+ em.r, em.g, em.b, // color (3) — incl. temperature tint
251
+ em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
208
252
  light.angle || Math.PI / 4, // cone half-angle in radians (1)
209
253
  light.penumbra || 0.0, // penumbra [0,1] (1)
210
254
  light.distance || 0.0, // cutoff distance (0 = infinite) (1)
@@ -290,7 +334,7 @@ export class LightSerializer {
290
334
 
291
335
  // Divide flat array lengths by per-light stride to get actual light counts
292
336
  const directionalCount = Math.floor( this.lightData.directional.length / 12 );
293
- const areaCount = Math.floor( this.lightData.rectArea.length / 13 );
337
+ const areaCount = Math.floor( this.lightData.rectArea.length / 16 );
294
338
  const pointCount = Math.floor( this.lightData.point.length / 9 );
295
339
  const spotCount = Math.floor( this.lightData.spot.length / 20 );
296
340
 
@@ -0,0 +1,74 @@
1
+ // Blackbody temperature → linear Rec.709 (= linear sRGB) RGB.
2
+ //
3
+ // Ported verbatim from Blender/Cycles `svm_math_blackbody_color_rec709`
4
+ // (Lukas Stockner's 7-segment rational/cubic fit; coefficients from
5
+ // intern/cycles/kernel/tables.h). The fit is luminance-balanced — the Rec.709
6
+ // luminance of the returned colour is ≈1.0 across the whole range — so it can be
7
+ // multiplied straight into a light's colour without altering its power: the
8
+ // temperature shifts hue only, brightness stays governed by intensity. This is
9
+ // exactly how Cycles applies it (raw multiply into the light colour, no
10
+ // renormalisation).
11
+
12
+ const BB_R = [
13
+ [ 1.61919106e3, - 2.05010916e-3, 5.02995757e0 ],
14
+ [ 2.48845471e3, - 1.11330907e-3, 3.22621544e0 ],
15
+ [ 3.34143193e3, - 4.86551192e-4, 1.76486769e0 ],
16
+ [ 4.09461742e3, - 1.27446582e-4, 7.25731635e-1 ],
17
+ [ 4.67028036e3, 2.91258199e-5, 1.26703442e-1 ],
18
+ [ 4.59509185e3, 2.87495649e-5, 1.50345020e-1 ],
19
+ [ 3.78717450e3, 9.35907826e-6, 3.99075871e-1 ],
20
+ ];
21
+
22
+ const BB_G = [
23
+ [ - 4.88999748e2, 6.04330754e-4, - 7.55807526e-2 ],
24
+ [ - 7.55994277e2, 3.16730098e-4, 4.78306139e-1 ],
25
+ [ - 1.02363977e3, 1.20223470e-4, 9.36662319e-1 ],
26
+ [ - 1.26571316e3, 4.87340896e-6, 1.27054498e0 ],
27
+ [ - 1.42529332e3, - 4.01150431e-5, 1.43972784e0 ],
28
+ [ - 1.17554822e3, - 2.16378048e-5, 1.30408023e0 ],
29
+ [ - 5.00799571e2, - 4.59832026e-6, 1.09098763e0 ],
30
+ ];
31
+
32
+ const BB_B = [
33
+ [ 5.96945309e-11, - 4.85742887e-8, - 9.70622247e-5, - 4.07936148e-3 ],
34
+ [ 2.40430366e-11, 5.55021075e-8, - 1.98503712e-4, 2.89312858e-2 ],
35
+ [ - 1.40949732e-11, 1.89878968e-7, - 3.56632824e-4, 9.10767778e-2 ],
36
+ [ - 3.61460868e-11, 2.84822009e-7, - 4.93211319e-4, 1.56723440e-1 ],
37
+ [ - 1.97075738e-11, 1.75359352e-7, - 2.50542825e-4, - 2.22783266e-2 ],
38
+ [ - 1.61997957e-13, - 1.64216008e-8, 3.86216271e-4, - 7.38077418e-1 ],
39
+ [ 6.72650283e-13, - 2.73078809e-8, 4.24098264e-4, - 7.52335691e-1 ],
40
+ ];
41
+
42
+ /**
43
+ * Convert a colour temperature in Kelvin to linear Rec.709 / linear-sRGB RGB.
44
+ * Negative out-of-gamut channels (very low temperatures) are clamped to 0.
45
+ * @param {number} kelvin
46
+ * @returns {[number, number, number]}
47
+ */
48
+ export function blackbodyToLinearRGB( kelvin ) {
49
+
50
+ let r, g, b;
51
+
52
+ if ( kelvin >= 12000 ) {
53
+
54
+ r = 0.8262954810464208; g = 0.9945080501520986; b = 1.566307710274283;
55
+
56
+ } else if ( kelvin < 800 ) {
57
+
58
+ r = 5.413294490189271; g = - 0.20319390035873933; b = - 0.0822535242887164;
59
+
60
+ } else {
61
+
62
+ const i = kelvin >= 6365 ? 6 : kelvin >= 3315 ? 5 : kelvin >= 1902 ? 4
63
+ : kelvin >= 1449 ? 3 : kelvin >= 1167 ? 2 : kelvin >= 965 ? 1 : 0;
64
+ const cr = BB_R[ i ], cg = BB_G[ i ], cb = BB_B[ i ];
65
+ const tInv = 1 / kelvin;
66
+ r = cr[ 0 ] * tInv + cr[ 1 ] * kelvin + cr[ 2 ];
67
+ g = cg[ 0 ] * tInv + cg[ 1 ] * kelvin + cg[ 2 ];
68
+ b = ( ( cb[ 0 ] * kelvin + cb[ 1 ] ) * kelvin + cb[ 2 ] ) * kelvin + cb[ 3 ];
69
+
70
+ }
71
+
72
+ return [ Math.max( 0, r ), Math.max( 0, g ), Math.max( 0, b ) ];
73
+
74
+ }
@@ -571,11 +571,11 @@ export class PathTracerStage extends RenderStage {
571
571
 
572
572
  }
573
573
 
574
- // Area lights (13 floats per light)
574
+ // Area lights (16 floats per light — 13 base + normalize/spread/shape)
575
575
  if ( this.areaLightsData && this.areaLightsData.length > 0 ) {
576
576
 
577
577
  this.areaLightsBufferNode.array = Array.from( this.areaLightsData );
578
- this.numAreaLights.value = Math.floor( this.areaLightsData.length / 13 );
578
+ this.numAreaLights.value = Math.floor( this.areaLightsData.length / 16 );
579
579
 
580
580
  } else {
581
581
 
@@ -42,12 +42,15 @@ export const DirectionalLight = struct( {
42
42
 
43
43
  export const AreaLight = struct( {
44
44
  position: 'vec3',
45
- u: 'vec3', // First axis of the rectangular light
46
- v: 'vec3', // Second axis of the rectangular light
45
+ u: 'vec3', // First axis half-vector of the rectangular light
46
+ v: 'vec3', // Second axis half-vector of the rectangular light
47
47
  color: 'vec3',
48
- intensity: 'float',
49
- normal: 'vec3',
50
- area: 'float',
48
+ intensity: 'float', // radiant power (Watts), Blender-style
49
+ normalize: 'float', // 1 = power normalized over area (radiance ∝ 1/area), 0 = constant radiance
50
+ spread: 'float', // emission cone half-fan in radians, π = full Lambertian hemisphere
51
+ shape: 'float', // 0 = rectangle, 1 = disk/ellipse
52
+ normal: 'vec3', // derived
53
+ area: 'float', // derived (true world area: rect = w·h, ellipse = π/4·w·h)
51
54
  } );
52
55
 
53
56
  export const PointLight = struct( {
@@ -125,7 +128,7 @@ export const getDirectionalLight = Fn( ( [ directionalLightsBuffer, index ] ) =>
125
128
 
126
129
  export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
127
130
 
128
- const baseIndex = index.mul( 13 );
131
+ const baseIndex = index.mul( 16 );
129
132
  const u = vec3(
130
133
  areaLightsBuffer.element( baseIndex.add( 3 ) ),
131
134
  areaLightsBuffer.element( baseIndex.add( 4 ) ),
@@ -138,6 +141,10 @@ export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
138
141
  ).toVar();
139
142
 
140
143
  const crossUV = cross( u, v );
144
+ const shape = areaLightsBuffer.element( baseIndex.add( 15 ) );
145
+
146
+ // u,v are half-vectors → full rectangle area = 4·|u×v|; disk/ellipse = π/4 of that.
147
+ const rectArea = length( crossUV ).mul( 4.0 );
141
148
 
142
149
  return AreaLight( {
143
150
  position: vec3(
@@ -153,8 +160,11 @@ export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
153
160
  areaLightsBuffer.element( baseIndex.add( 11 ) ),
154
161
  ),
155
162
  intensity: areaLightsBuffer.element( baseIndex.add( 12 ) ),
163
+ normalize: areaLightsBuffer.element( baseIndex.add( 13 ) ),
164
+ spread: areaLightsBuffer.element( baseIndex.add( 14 ) ),
165
+ shape: shape,
156
166
  normal: normalize( crossUV ),
157
- area: length( crossUV ).mul( 4.0 ),
167
+ area: select( shape.greaterThan( 0.5 ), rectArea.mul( 0.7853981633974483 ), rectArea ),
158
168
  } );
159
169
 
160
170
  } );
@@ -501,12 +511,18 @@ export const intersectAreaLight = Fn( ( [ light, rayOrigin, rayDirection ] ) =>
501
511
  const u_dir = light.u.div( uLen );
502
512
  const v_dir = light.v.div( vLen );
503
513
 
504
- // Project onto axes
505
- const u_proj = dot( localPoint, u_dir );
506
- const v_proj = dot( localPoint, v_dir );
514
+ // Project onto axes, normalized to [-1,1] across the half-extents
515
+ const u_proj = dot( localPoint, u_dir ).div( uLen );
516
+ const v_proj = dot( localPoint, v_dir ).div( vLen );
507
517
 
508
- // Check within rectangle bounds (half-lengths)
509
- If( abs( u_proj ).lessThanEqual( uLen ).and( abs( v_proj ).lessThanEqual( vLen ) ), () => {
518
+ // Rectangle: |u|≤1 |v|≤1. Disk/ellipse: u²+v²≤1.
519
+ const inside = select(
520
+ light.shape.greaterThan( 0.5 ),
521
+ u_proj.mul( u_proj ).add( v_proj.mul( v_proj ) ).lessThanEqual( 1.0 ),
522
+ abs( u_proj ).lessThanEqual( 1.0 ).and( abs( v_proj ).lessThanEqual( 1.0 ) ),
523
+ );
524
+
525
+ If( inside, () => {
510
526
 
511
527
  result.assign( t );
512
528
 
@@ -520,3 +536,114 @@ export const intersectAreaLight = Fn( ( [ light, rayOrigin, rayDirection ] ) =>
520
536
 
521
537
  } );
522
538
 
539
+ // ================================================================================
540
+ // SPHERICAL RECTANGLE SAMPLING (Ureña et al. 2013, "An Area-Preserving
541
+ // Parametrization for Spherical Rectangles"). Same scheme Cycles uses for rect
542
+ // area lights — samples the light's solid angle directly, giving pdf = 1/S in
543
+ // solid-angle measure (much lower variance than uniform-area sampling for
544
+ // large/near lights). Inputs:
545
+ // o = shading point (ray origin)
546
+ // s = rectangle corner = center - u - v
547
+ // ex = full edge vector along u (= 2·u)
548
+ // ey = full edge vector along v (= 2·v)
549
+ // ================================================================================
550
+
551
+ // Solid angle S subtended by the rectangle from o (used for the BSDF-hit MIS pdf).
552
+ export const sphQuadSolidAngle = /*@__PURE__*/ wgslFn( `
553
+ fn sphQuadSolidAngle( o: vec3f, s: vec3f, ex: vec3f, ey: vec3f ) -> f32 {
554
+ let PI = 3.141592653589793f;
555
+ let exl = length( ex );
556
+ let eyl = length( ey );
557
+ let x = ex / max( exl, 1e-12f );
558
+ let y = ey / max( eyl, 1e-12f );
559
+ var z = cross( x, y );
560
+ let d = s - o;
561
+ var z0 = dot( d, z );
562
+ if ( z0 > 0.0f ) { z = -z; z0 = -z0; }
563
+ let x0 = dot( d, x );
564
+ let y0 = dot( d, y );
565
+ let x1 = x0 + exl;
566
+ let y1 = y0 + eyl;
567
+ let v00 = vec3f( x0, y0, z0 );
568
+ let v01 = vec3f( x0, y1, z0 );
569
+ let v10 = vec3f( x1, y0, z0 );
570
+ let v11 = vec3f( x1, y1, z0 );
571
+ let n0 = normalize( cross( v00, v10 ) );
572
+ let n1 = normalize( cross( v10, v11 ) );
573
+ let n2 = normalize( cross( v11, v01 ) );
574
+ let n3 = normalize( cross( v01, v00 ) );
575
+ let g0 = acos( clamp( -dot( n0, n1 ), -1.0f, 1.0f ) );
576
+ let g1 = acos( clamp( -dot( n1, n2 ), -1.0f, 1.0f ) );
577
+ let g2 = acos( clamp( -dot( n2, n3 ), -1.0f, 1.0f ) );
578
+ let g3 = acos( clamp( -dot( n3, n0 ), -1.0f, 1.0f ) );
579
+ return g0 + g1 + g2 + g3 - 2.0f * PI;
580
+ }
581
+ ` );
582
+
583
+ // Sample a direction toward the rectangle uniformly in solid angle.
584
+ // Returns vec4( pointOnRect.xyz, pdf ); pdf = 1/S. pdf <= 0 signals the caller
585
+ // to fall back to uniform-area sampling (degenerate / tiny solid angle).
586
+ export const sampleSphQuad = /*@__PURE__*/ wgslFn( `
587
+ fn sampleSphQuad( o: vec3f, s: vec3f, ex: vec3f, ey: vec3f, uv: vec2f ) -> vec4f {
588
+ let PI = 3.141592653589793f;
589
+ let exl = length( ex );
590
+ let eyl = length( ey );
591
+ let x = ex / max( exl, 1e-12f );
592
+ let y = ey / max( eyl, 1e-12f );
593
+ var z = cross( x, y );
594
+ let d = s - o;
595
+ var z0 = dot( d, z );
596
+ if ( z0 > 0.0f ) { z = -z; z0 = -z0; }
597
+ let z0sq = z0 * z0;
598
+ let x0 = dot( d, x );
599
+ let y0 = dot( d, y );
600
+ let x1 = x0 + exl;
601
+ let y1 = y0 + eyl;
602
+ let y0sq = y0 * y0;
603
+ let y1sq = y1 * y1;
604
+ let v00 = vec3f( x0, y0, z0 );
605
+ let v01 = vec3f( x0, y1, z0 );
606
+ let v10 = vec3f( x1, y0, z0 );
607
+ let v11 = vec3f( x1, y1, z0 );
608
+ let n0 = normalize( cross( v00, v10 ) );
609
+ let n1 = normalize( cross( v10, v11 ) );
610
+ let n2 = normalize( cross( v11, v01 ) );
611
+ let n3 = normalize( cross( v01, v00 ) );
612
+ let g0 = acos( clamp( -dot( n0, n1 ), -1.0f, 1.0f ) );
613
+ let g1 = acos( clamp( -dot( n1, n2 ), -1.0f, 1.0f ) );
614
+ let g2 = acos( clamp( -dot( n2, n3 ), -1.0f, 1.0f ) );
615
+ let g3 = acos( clamp( -dot( n3, n0 ), -1.0f, 1.0f ) );
616
+ let b0 = n0.z;
617
+ let b1 = n2.z;
618
+ let b0sq = b0 * b0;
619
+ let k = 2.0f * PI - g2 - g3;
620
+ let S = g0 + g1 + g2 + g3 - 2.0f * PI;
621
+
622
+ if ( S <= 1e-5f ) {
623
+ return vec4f( 0.0f, 0.0f, 0.0f, -1.0f );
624
+ }
625
+
626
+ let au = uv.x * S + k;
627
+ let sinAu = sin( au );
628
+ let fu = ( cos( au ) * b0 - b1 ) / max( abs( sinAu ), 1e-7f ) * sign( sinAu );
629
+ var cu = select( -1.0f, 1.0f, fu >= 0.0f ) / sqrt( fu * fu + b0sq );
630
+ cu = clamp( cu, -1.0f, 1.0f );
631
+
632
+ var xu = -( cu * z0 ) / max( sqrt( 1.0f - cu * cu ), 1e-7f );
633
+ xu = clamp( xu, x0, x1 );
634
+
635
+ let dd = sqrt( xu * xu + z0sq );
636
+ let h0 = y0 / sqrt( dd * dd + y0sq );
637
+ let h1 = y1 / sqrt( dd * dd + y1sq );
638
+ let hv = h0 + uv.y * ( h1 - h0 );
639
+ let hv2 = hv * hv;
640
+ var yv = y1;
641
+ if ( hv2 < 1.0f - 1e-6f ) {
642
+ yv = ( hv * dd ) / sqrt( 1.0f - hv2 );
643
+ }
644
+
645
+ let p = o + xu * x + yv * y + z0 * z;
646
+ return vec4f( p, 1.0f / S );
647
+ }
648
+ ` );
649
+