rayzee 6.0.1 → 6.1.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 (43) hide show
  1. package/README.md +5 -0
  2. package/dist/assets/TexturesWorker-DBqGmVdR.js.map +1 -1
  3. package/dist/rayzee.es.js +2419 -2078
  4. package/dist/rayzee.es.js.map +1 -1
  5. package/dist/rayzee.umd.js +48 -52
  6. package/dist/rayzee.umd.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/EngineDefaults.js +3 -0
  9. package/src/PathTracerApp.js +18 -8
  10. package/src/Pipeline/RenderStage.js +3 -0
  11. package/src/Processor/IESParser.js +340 -0
  12. package/src/Processor/LightSerializer.js +32 -4
  13. package/src/Processor/SceneProcessor.js +0 -1
  14. package/src/Processor/ShaderBuilder.js +40 -1
  15. package/src/Processor/Workers/TexturesWorker.js +1 -1
  16. package/src/RenderSettings.js +3 -0
  17. package/src/Stages/NormalDepth.js +3 -19
  18. package/src/Stages/PathTracer.js +15 -9
  19. package/src/TSL/BVHTraversal.js +4 -6
  20. package/src/TSL/Common.js +1 -1
  21. package/src/TSL/Debugger.js +0 -2
  22. package/src/TSL/EmissiveSampling.js +20 -22
  23. package/src/TSL/Environment.js +60 -14
  24. package/src/TSL/Fresnel.js +13 -4
  25. package/src/TSL/LightsCore.js +238 -5
  26. package/src/TSL/LightsDirect.js +16 -5
  27. package/src/TSL/LightsIndirect.js +4 -37
  28. package/src/TSL/LightsSampling.js +119 -185
  29. package/src/TSL/MaterialEvaluation.js +25 -14
  30. package/src/TSL/MaterialProperties.js +14 -34
  31. package/src/TSL/MaterialTransmission.js +18 -37
  32. package/src/TSL/PathTracer.js +5 -4
  33. package/src/TSL/PathTracerCore.js +144 -139
  34. package/src/TSL/Struct.js +7 -1
  35. package/src/TSL/TextureSampling.js +2 -2
  36. package/src/index.js +2 -0
  37. package/src/managers/AnimationManager.js +3 -6
  38. package/src/managers/DenoisingManager.js +1 -1
  39. package/src/managers/GoboManager.js +277 -0
  40. package/src/managers/IESManager.js +268 -0
  41. package/src/managers/LightManager.js +33 -1
  42. package/src/managers/TransformManager.js +3 -3
  43. package/src/managers/UniformManager.js +5 -5
@@ -93,7 +93,6 @@ export class NormalDepth extends RenderStage {
93
93
  // Own storage nodes — created lazily when data is available
94
94
  this._triStorageNode = null;
95
95
  this._bvhStorageNode = null;
96
- this._matStorageNode = null;
97
96
 
98
97
  // Last-seen attribute identities. PathTracer replaces these in-place
99
98
  // across model load / BVH rebuild; the compute's bind group is locked
@@ -101,7 +100,6 @@ export class NormalDepth extends RenderStage {
101
100
  // when any of them swaps to a new object.
102
101
  this._lastTriAttr = null;
103
102
  this._lastBvhAttr = null;
104
- this._lastMatAttr = null;
105
103
 
106
104
  // Compute node — built once when storage buffers are ready
107
105
  this._computeNode = null;
@@ -146,8 +144,6 @@ export class NormalDepth extends RenderStage {
146
144
  const pt = this.pathTracer;
147
145
  if ( ! pt ) return false;
148
146
 
149
- const matStorageAttr = pt.materialData.materialStorageAttr;
150
-
151
147
  // Detect attribute identity swap (PathTracer.setTriangleData /
152
148
  // setBVHData replace the attribute object on growth). The compute
153
149
  // node's bind group is locked to the buffer bound at compile time —
@@ -155,9 +151,8 @@ export class NormalDepth extends RenderStage {
155
151
  // pointing at the now-discarded buffer, so every traversal misses.
156
152
  const triSwapped = pt.triangleStorageAttr && pt.triangleStorageAttr !== this._lastTriAttr;
157
153
  const bvhSwapped = pt.bvhStorageAttr && pt.bvhStorageAttr !== this._lastBvhAttr;
158
- const matSwapped = matStorageAttr && matStorageAttr !== this._lastMatAttr;
159
154
 
160
- if ( triSwapped || bvhSwapped || matSwapped ) {
155
+ if ( triSwapped || bvhSwapped ) {
161
156
 
162
157
  // Drop compute + storage nodes so they get rebuilt against the
163
158
  // current buffers. Cheap: this only happens on model load.
@@ -166,7 +161,6 @@ export class NormalDepth extends RenderStage {
166
161
  this._computeBuilt = false;
167
162
  this._triStorageNode = null;
168
163
  this._bvhStorageNode = null;
169
- this._matStorageNode = null;
170
164
  this._dirty = true;
171
165
 
172
166
  }
@@ -187,19 +181,10 @@ export class NormalDepth extends RenderStage {
187
181
 
188
182
  }
189
183
 
190
- if ( matStorageAttr && ! this._matStorageNode ) {
191
-
192
- this._matStorageNode = storage(
193
- matStorageAttr, 'vec4', matStorageAttr.count
194
- ).toReadOnly();
195
-
196
- }
197
-
198
184
  this._lastTriAttr = pt.triangleStorageAttr || this._lastTriAttr;
199
185
  this._lastBvhAttr = pt.bvhStorageAttr || this._lastBvhAttr;
200
- this._lastMatAttr = matStorageAttr || this._lastMatAttr;
201
186
 
202
- return !! ( this._triStorageNode && this._bvhStorageNode && this._matStorageNode );
187
+ return !! ( this._triStorageNode && this._bvhStorageNode );
203
188
 
204
189
  }
205
190
 
@@ -211,7 +196,6 @@ export class NormalDepth extends RenderStage {
211
196
 
212
197
  const triStorage = this._triStorageNode;
213
198
  const bvhStorage = this._bvhStorageNode;
214
- const matStorage = this._matStorageNode;
215
199
  const camWorld = this.cameraWorldMatrix;
216
200
  const camProjInv = this.cameraProjectionMatrixInverse;
217
201
  const resW = this.resolutionWidth;
@@ -249,7 +233,7 @@ export class NormalDepth extends RenderStage {
249
233
  const ray = Ray( { origin: rayOrigin, direction: rayDirWorld } );
250
234
 
251
235
  // BVH traversal (primary ray only) — wrap result for struct field access
252
- const hit = HitInfo.wrap( traverseBVH( ray, bvhStorage, triStorage, matStorage ) );
236
+ const hit = HitInfo.wrap( traverseBVH( ray, bvhStorage, triStorage ) );
253
237
 
254
238
  // Encode: normal * 0.5 + 0.5 in RGB, linear depth in A
255
239
  const encodedNormal = hit.normal.mul( 0.5 ).add( 0.5 );
@@ -188,6 +188,16 @@ export class PathTracer extends RenderStage {
188
188
  this.spotLightsData = null;
189
189
  this.areaLightsData = null;
190
190
 
191
+ // Spot light gobo (projection mask) DataArrayTexture. Owned externally
192
+ // (GoboManager); ShaderBuilder reads via this property at graph build time
193
+ // and refreshes the bound TextureNode in-place when it changes.
194
+ this.goboMaps = null;
195
+
196
+ // Spot light IES photometric profiles DataArrayTexture. Owned externally
197
+ // (IESManager); ShaderBuilder reads via this property at graph build time
198
+ // and refreshes the bound TextureNode in-place when it changes.
199
+ this.iesProfiles = null;
200
+
191
201
  // STBN noise textures
192
202
  this.stbnScalarTexture = null;
193
203
  this.stbnVec2Texture = null;
@@ -487,9 +497,6 @@ export class PathTracer extends RenderStage {
487
497
  this.setInstanceTable( this.sdfs.instanceTable );
488
498
  this.materialData.setMaterialData( this.sdfs.materialData );
489
499
 
490
- // Update triangle count
491
- this.totalTriangleCount.value = this.sdfs.triangleCount || 0;
492
-
493
500
  // Material texture arrays
494
501
  this.materialData.loadTexturesFromSdfs();
495
502
 
@@ -588,11 +595,11 @@ export class PathTracer extends RenderStage {
588
595
  */
589
596
  _updateLightBufferNodes() {
590
597
 
591
- // Directional lights (8 floats per light)
598
+ // Directional lights (12 floats per light — 8 light fields + gobo {index, signed intensity, scale, pad})
592
599
  if ( this.directionalLightsData && this.directionalLightsData.length > 0 ) {
593
600
 
594
601
  this.directionalLightsBufferNode.array = Array.from( this.directionalLightsData );
595
- this.numDirectionalLights.value = Math.floor( this.directionalLightsData.length / 8 );
602
+ this.numDirectionalLights.value = Math.floor( this.directionalLightsData.length / 12 );
596
603
 
597
604
  } else {
598
605
 
@@ -624,11 +631,11 @@ export class PathTracer extends RenderStage {
624
631
 
625
632
  }
626
633
 
627
- // Spot lights (14 floats per light)
634
+ // Spot lights (20 floats per light — 14 light fields + gobo {idx, signed intensity} + IES {idx, intensity} + 2 reserved)
628
635
  if ( this.spotLightsData && this.spotLightsData.length > 0 ) {
629
636
 
630
637
  this.spotLightsBufferNode.array = Array.from( this.spotLightsData );
631
- this.numSpotLights.value = Math.floor( this.spotLightsData.length / 14 );
638
+ this.numSpotLights.value = Math.floor( this.spotLightsData.length / 20 );
632
639
 
633
640
  } else {
634
641
 
@@ -1087,9 +1094,8 @@ export class PathTracer extends RenderStage {
1087
1094
  /**
1088
1095
  * Renders the path tracing pass with accumulation.
1089
1096
  * @param {PipelineContext} context - Pipeline context
1090
- * @param {RenderTarget} writeBuffer - Output render target
1091
1097
  */
1092
- render( context, writeBuffer ) {
1098
+ render( context ) {
1093
1099
 
1094
1100
  if ( ! this.isReady ) return;
1095
1101
 
@@ -13,7 +13,6 @@ import {
13
13
  sign,
14
14
  min,
15
15
  normalize,
16
- cross,
17
16
  mix,
18
17
  vec4,
19
18
  notEqual,
@@ -116,7 +115,6 @@ export const traverseBVH = Fn( ( [
116
115
  ray,
117
116
  bvhBuffer,
118
117
  triangleBuffer,
119
- materialBuffer,
120
118
  ] ) => {
121
119
 
122
120
  const closestHit = HitInfo( {
@@ -332,7 +330,6 @@ export const traverseBVHShadow = Fn( ( [
332
330
  ray,
333
331
  bvhBuffer,
334
332
  triangleBuffer,
335
- _materialBuffer, // eslint-disable-line no-unused-vars -- kept for call-site compatibility
336
333
  maxShadowDist,
337
334
  ] ) => {
338
335
 
@@ -398,10 +395,11 @@ export const traverseBVHShadow = Fn( ( [
398
395
  closestHit.materialIndex.assign( int( uvData2.z ) );
399
396
  closestHit.meshIndex.assign( int( uvData2.w ) );
400
397
 
401
- // Compute hit point and geometric normal -- required for transmissive
402
- // Fresnel in traceShadowRay (cosThetaI needs a real normal, not vec3(0))
398
+ // Hit point is cheap (origin + dir*t). Geometric normal is deferred
399
+ // to traceShadowRay only the transmission branch needs it, so we
400
+ // skip the cross+normalize for the (much more common) opaque-blocker
401
+ // and alpha-cutout paths. Normal stays vec3(0) from struct init.
403
402
  closestHit.hitPoint.assign( ray.origin.add( ray.direction.mul( triResult.x ) ) );
404
- closestHit.normal.assign( normalize( cross( pB.sub( pA ), pC.sub( pA ) ) ) );
405
403
 
406
404
  // Store barycentrics + triangle index for deferred UV computation.
407
405
  // Actual UV interpolation happens in traceShadowRay only when
package/src/TSL/Common.js CHANGED
@@ -246,7 +246,7 @@ export const classifyMaterial = Fn( ( [ metalness, roughness, transmission, clea
246
246
  } );
247
247
 
248
248
  // Dynamic MIS strategy based on material properties
249
- export const selectOptimalMISStrategy = Fn( ( [ roughness, metalness, transmission, bounceIndex, throughput ] ) => {
249
+ export const selectOptimalMISStrategy = Fn( ( [ roughness, metalness, bounceIndex, throughput ] ) => {
250
250
 
251
251
  const throughputStrength = maxComponent( { v: throughput } ).toVar();
252
252
 
@@ -88,7 +88,6 @@ export const TraceDebugMode = Fn( ( [
88
88
  ray,
89
89
  bvhBuffer,
90
90
  triangleBuffer,
91
- materialBuffer,
92
91
  ).toVar() );
93
92
 
94
93
  // Case 7: Triangle Tests
@@ -351,7 +350,6 @@ export const TraceDebugMode = Fn( ( [
351
350
  bounceRay,
352
351
  bvhBuffer,
353
352
  triangleBuffer,
354
- materialBuffer,
355
353
  ).toVar() );
356
354
 
357
355
  const incoming = vec3( 0.0 ).toVar();
@@ -5,7 +5,6 @@ import {
5
5
  Fn,
6
6
  vec2,
7
7
  vec3,
8
- vec4,
9
8
  float,
10
9
  int,
11
10
  bool as tslBool,
@@ -16,7 +15,6 @@ import {
16
15
  cross,
17
16
  length,
18
17
  max,
19
- min,
20
18
  sqrt,
21
19
  abs,
22
20
  clamp,
@@ -28,9 +26,11 @@ import {
28
26
  } from 'three/tsl';
29
27
 
30
28
  import { struct } from './patches.js';
31
- import { MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT } from './Common.js';
29
+ import { MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT, computeDotProducts } from './Common.js';
32
30
  import { RandomValue } from './Random.js';
33
- import { calculateMaterialPDF } from './LightsSampling.js';
31
+ import { calculateMaterialPDFFromDots } from './LightsSampling.js';
32
+ import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
33
+ import { DotProducts } from './Struct.js';
34
34
 
35
35
  // ================================================================================
36
36
  // STRUCTS
@@ -378,7 +378,7 @@ const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveOffset, emissive
378
378
  // `emissiveTriangleBuffer` may be the shared packed light buffer; `emissiveVec4Offset`
379
379
  // gives the vec4 offset where emissive entries begin.
380
380
  export const sampleEmissiveTriangle = Fn( ( [
381
- hitPoint, surfaceNormal, totalTriangleCount,
381
+ hitPoint, surfaceNormal,
382
382
  rngState,
383
383
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
384
384
  triangleBuffer,
@@ -514,19 +514,19 @@ export const sampleEmissiveTriangle = Fn( ( [
514
514
  // EMISSIVE TRIANGLE DIRECT LIGHTING CONTRIBUTION
515
515
  // ================================================================================
516
516
 
517
- // Note: calculateEmissiveTriangleContributionDebug requires traceShadowRay and
518
- // evaluateMaterialResponse which creates circular dependencies.
519
- // These are passed as function parameters to avoid the cycle.
517
+ // Note: traceShadowRay and calculateRayOffset are passed as Fn parameters to
518
+ // avoid a circular module dependency. BRDF evaluation no longer goes through a
519
+ // callback we import the FromDots variant directly so we can share dot
520
+ // products with the PDF call below.
520
521
 
521
522
  export const calculateEmissiveTriangleContributionDebug = Fn( ( [
522
523
  hitPoint, normal, viewDir, material,
523
- totalTriangleCount, bounceIndex, rngState,
524
+ bounceIndex, rngState,
524
525
  emissiveBoost,
525
526
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
526
527
  triangleBuffer,
527
528
  // Callback functions to avoid circular deps
528
529
  traceShadowRayFn,
529
- evaluateMaterialResponseFn,
530
530
  calculateRayOffsetFn,
531
531
  ] ) => {
532
532
 
@@ -544,7 +544,7 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
544
544
 
545
545
  // Sample emissive triangle (CDF importance-weighted)
546
546
  const emissiveSample = EmissiveSample.wrap( sampleEmissiveTriangle(
547
- hitPoint, normal, totalTriangleCount, rngState,
547
+ hitPoint, normal, rngState,
548
548
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
549
549
  triangleBuffer,
550
550
  ) );
@@ -566,15 +566,15 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
566
566
 
567
567
  // Trace shadow ray
568
568
  const shadowDist = emissiveSample.distance.sub( 0.001 );
569
- const visibility = traceShadowRayFn( rayOrigin, emissiveSample.direction, shadowDist, rngState );
569
+ const visibility = traceShadowRayFn( rayOrigin, emissiveSample.direction, shadowDist );
570
570
 
571
571
  If( visibility.greaterThan( 0.0 ), () => {
572
572
 
573
- // Evaluate BRDF
574
- const brdfValue = evaluateMaterialResponseFn( viewDir, emissiveSample.direction, normal, material );
575
-
576
- // Calculate BRDF PDF for MIS
577
- const brdfPdf = calculateMaterialPDF( viewDir, emissiveSample.direction, normal, material );
573
+ // Share H + dot products between BRDF eval and PDF (computeDotProducts
574
+ // would otherwise run twice with identical inputs).
575
+ const dots = DotProducts.wrap( computeDotProducts( normal, viewDir, emissiveSample.direction ) );
576
+ const brdfValue = evaluateMaterialResponseFromDots( material, dots );
577
+ const brdfPdf = calculateMaterialPDFFromDots( material, dots );
578
578
 
579
579
  // MIS weight: balance light sampling vs BRDF sampling
580
580
  const misWeight = select(
@@ -602,26 +602,24 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
602
602
 
603
603
  } );
604
604
 
605
- // Wrapper function for backward compatibility
605
+ // Wrapper that returns just the contribution vec3
606
606
  export const calculateEmissiveTriangleContribution = Fn( ( [
607
607
  hitPoint, normal, viewDir, material,
608
- totalTriangleCount, bounceIndex, rngState,
608
+ bounceIndex, rngState,
609
609
  emissiveBoost,
610
610
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
611
611
  triangleBuffer,
612
612
  traceShadowRayFn,
613
- evaluateMaterialResponseFn,
614
613
  calculateRayOffsetFn,
615
614
  ] ) => {
616
615
 
617
616
  const result = EmissiveContributionResult.wrap( calculateEmissiveTriangleContributionDebug(
618
617
  hitPoint, normal, viewDir, material,
619
- totalTriangleCount, bounceIndex, rngState,
618
+ bounceIndex, rngState,
620
619
  emissiveBoost,
621
620
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
622
621
  triangleBuffer,
623
622
  traceShadowRayFn,
624
- evaluateMaterialResponseFn,
625
623
  calculateRayOffsetFn,
626
624
  ) );
627
625
  return result.contribution;
@@ -1,4 +1,4 @@
1
- import { Fn, wgslFn, vec2, vec4, float, int, If, texture, sampler, dot, sin, floor, fract, min, max, mix, clamp } from 'three/tsl';
1
+ import { Fn, wgslFn, vec2, vec4, float, int, If, texture, dot, sin, sqrt, floor, fract, min, max, mix, clamp } from 'three/tsl';
2
2
 
3
3
  import { REC709_LUMINANCE_COEFFICIENTS } from './Common.js';
4
4
 
@@ -28,17 +28,6 @@ export const equirectUvToDirection = /*@__PURE__*/ wgslFn( `
28
28
  }
29
29
  ` );
30
30
 
31
- // Calculate PDF for uniform sphere sampling with Jacobian
32
- export const equirectDirectionPdf = /*@__PURE__*/ wgslFn( `
33
- fn equirectDirectionPdf( direction: vec3f, environmentMatrix: mat4x4f ) -> f32 {
34
- let uv = equirectDirectionToUv( direction, environmentMatrix );
35
- let theta = uv.y * 3.14159265358979323846f;
36
- let sinTheta = sin( theta );
37
- if ( sinTheta == 0.0f ) { return 0.0f; }
38
- return 1.0f / ( 6.28318530717958647692f * 3.14159265358979323846f * sinTheta );
39
- }
40
- `, [ equirectDirectionToUv ] );
41
-
42
31
  // Evaluate PDF for a given direction (for MIS)
43
32
  // Returns vec4(color.rgb, pdf) since TSL cannot use inout params
44
33
  // Uses MIS-compensated PDF (Karlík et al. 2019): max(0, lum - delta) / compensatedTotalSum
@@ -64,7 +53,12 @@ export const sampleEquirect = Fn( ( [ environment, direction, environmentMatrix,
64
53
  const compensatedWeight = max( float( 0.0 ), weightedLum.sub( envCompensationDelta ) ).toVar();
65
54
  const pdf = compensatedWeight.div( envTotalSum ).toVar();
66
55
 
67
- const dirPdf = equirectDirectionPdf( { direction, environmentMatrix } ).toVar();
56
+ // Inline equirectDirectionPdf using the uv + sinTheta already in scope —
57
+ // the helper would otherwise re-derive uv via atan2+acos and recompute sin.
58
+ const dirPdf = sinTheta.greaterThan( 0.0 ).select(
59
+ float( 1.0 ).div( float( 2.0 * Math.PI * Math.PI ).mul( sinTheta ) ),
60
+ float( 0.0 )
61
+ ).toVar();
68
62
  const finalPdf = float( envResolution.x ).mul( float( envResolution.y ) ).mul( pdf ).mul( dirPdf ).toVar();
69
63
 
70
64
  result.assign( vec4( color, finalPdf ) );
@@ -138,7 +132,12 @@ export const sampleEquirectProbability = Fn( ( [
138
132
  const compensatedWeight = max( float( 0.0 ), weightedLum.sub( envCompensationDelta ) ).toVar();
139
133
  const pdf = compensatedWeight.div( envTotalSum ).toVar();
140
134
 
141
- const dirPdf = equirectDirectionPdf( { direction, environmentMatrix } ).toVar();
135
+ // Inline equirectDirectionPdf uv + sinTheta are already in scope, so we
136
+ // skip the helper's redundant uv-from-direction + sin recompute.
137
+ const dirPdf = sinTheta.greaterThan( 0.0 ).select(
138
+ float( 1.0 ).div( float( 2.0 * Math.PI * Math.PI ).mul( sinTheta ) ),
139
+ float( 0.0 )
140
+ ).toVar();
142
141
  const finalPdf = float( envResolution.x ).mul( float( envResolution.y ) ).mul( pdf ).mul( dirPdf ).toVar();
143
142
 
144
143
  return vec4( direction, finalPdf );
@@ -163,3 +162,50 @@ export const sampleEnvironment = /*@__PURE__*/ wgslFn( `
163
162
  return texSample * environmentIntensity;
164
163
  }
165
164
  `, [ equirectDirectionToUv ] );
165
+
166
+ // Port of three.js PR #33611 (getGroundProjectedNormal) adapted from rasterizer fragment math
167
+ // (cameraPosition + positionWorld) to path-tracer ray math (rayOrigin + rayDirection). When the
168
+ // ray misses the projection sphere it falls back to rayDirection so distant scenes degrade gracefully.
169
+ export const getGroundProjectedDirection = Fn( ( [ rayOrigin, rayDirection, radius, height ] ) => {
170
+
171
+ const p = rayDirection.toConst();
172
+ const camPos = rayOrigin.toVar();
173
+ camPos.y.subAssign( height );
174
+
175
+ const r2 = radius.mul( radius ).toConst();
176
+ const b = camPos.dot( p ).toConst();
177
+ const c = camPos.dot( camPos ).sub( r2 ).toConst();
178
+ const h = b.mul( b ).sub( c ).toConst();
179
+
180
+ const projected = rayDirection.toVar();
181
+
182
+ If( h.greaterThanEqual( 0.0 ), () => {
183
+
184
+ const tSphere = sqrt( h ).sub( b ).toVar();
185
+
186
+ // Disk sits at world y=0; the camPos shift only repositions the sphere.
187
+ const tDisk = float( 1e6 ).toVar();
188
+ const py = p.y.toConst();
189
+ If( py.lessThanEqual( 0.0 ), () => {
190
+
191
+ const t = rayOrigin.y.negate().div( py ).toConst();
192
+ const q = rayOrigin.add( p.mul( t ) ).toConst();
193
+ If( q.dot( q ).lessThan( r2 ), () => {
194
+
195
+ tDisk.assign( t );
196
+
197
+ } );
198
+
199
+ } );
200
+
201
+ If( tSphere.greaterThan( 0.0 ), () => {
202
+
203
+ projected.assign( camPos.add( p.mul( min( tSphere, tDisk ) ) ).div( radius ) );
204
+
205
+ } );
206
+
207
+ } );
208
+
209
+ return projected;
210
+
211
+ } );
@@ -1,25 +1,34 @@
1
- import { Fn, float, vec3, max, pow, clamp, sqrt } from 'three/tsl';
1
+ import { Fn, float, vec3, max, clamp, sqrt } from 'three/tsl';
2
2
 
3
3
  const EPSILON = 1e-6;
4
4
 
5
+ // Schlick exponent factored as 4 multiplies — pow(x, 5.0) compiles to
6
+ // exp2(5*log2(x)) on most backends, far slower than (x²)²·x.
7
+ const pow5 = ( c ) => {
8
+
9
+ const c2 = c.mul( c );
10
+ return c2.mul( c2 ).mul( c );
11
+
12
+ };
13
+
5
14
  export const fresnel = Fn( ( [ f0, NoV, roughness ] ) => {
6
15
 
7
16
  const maxR = max( vec3( float( 1.0 ).sub( roughness ) ), f0 );
8
- return f0.add( maxR.sub( f0 ).mul( pow( float( 1.0 ).sub( NoV ), 5.0 ) ) );
17
+ return f0.add( maxR.sub( f0 ).mul( pow5( float( 1.0 ).sub( NoV ) ) ) );
9
18
 
10
19
  } );
11
20
 
12
21
  export const fresnelSchlickFloat = Fn( ( [ cosTheta, F0 ] ) => {
13
22
 
14
23
  const clampedCos = clamp( cosTheta, 0.0, 1.0 );
15
- return F0.add( float( 1.0 ).sub( F0 ).mul( pow( float( 1.0 ).sub( clampedCos ), 5.0 ) ) );
24
+ return F0.add( float( 1.0 ).sub( F0 ).mul( pow5( float( 1.0 ).sub( clampedCos ) ) ) );
16
25
 
17
26
  } );
18
27
 
19
28
  export const fresnelSchlick = Fn( ( [ cosTheta, F0 ] ) => {
20
29
 
21
30
  const clampedCos = clamp( cosTheta, 0.0, 1.0 );
22
- return F0.add( vec3( 1.0 ).sub( F0 ).mul( pow( float( 1.0 ).sub( clampedCos ), 5.0 ) ) );
31
+ return F0.add( vec3( 1.0 ).sub( F0 ).mul( pow5( float( 1.0 ).sub( clampedCos ) ) ) );
23
32
 
24
33
  } );
25
34