@forgeax/engine-shader 0.1.20 → 0.1.23

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 (74) hide show
  1. package/README.md +10 -0
  2. package/dist/ShaderRegistry.d.ts.map +1 -1
  3. package/dist/index.d.ts +6 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.mjs +33070 -8
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/ltc/provenance.d.ts +17 -0
  8. package/dist/ltc/provenance.d.ts.map +1 -0
  9. package/dist/ltc/tables.d.ts +22 -0
  10. package/dist/ltc/tables.d.ts.map +1 -0
  11. package/dist/material/artifact-types.d.ts +52 -0
  12. package/dist/material/artifact-types.d.ts.map +1 -1
  13. package/dist/material-schemas.d.ts +5 -0
  14. package/dist/material-schemas.d.ts.map +1 -1
  15. package/dist/register-default-sprite-lit.d.ts +3 -6
  16. package/dist/register-default-sprite-lit.d.ts.map +1 -1
  17. package/dist/types.d.ts +2 -1
  18. package/dist/types.d.ts.map +1 -1
  19. package/package.json +4 -4
  20. package/src/ShaderRegistry.ts +65 -0
  21. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -4
  22. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +7 -0
  23. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +138 -30
  24. package/src/__tests__/direct-light-layout.unit.test.ts +40 -0
  25. package/src/__tests__/ibl-irradiance.unit.test.ts +7 -4
  26. package/src/__tests__/ibl-sampling.unit.test.ts +3 -4
  27. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +16 -2
  28. package/src/__tests__/lighting-punctual.unit.test.ts +61 -8
  29. package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +1 -1
  30. package/src/__tests__/ltc-provenance.unit.test.ts +33 -0
  31. package/src/__tests__/manifest.fixture.json +33 -0
  32. package/src/__tests__/material-contract.unit.test.ts +61 -1
  33. package/src/__tests__/material-derived-builtins.integration.test.ts +27 -0
  34. package/src/__tests__/probe-lighting-composition.unit.test.ts +38 -0
  35. package/src/__tests__/rect-area-brdf.unit.test.ts +36 -0
  36. package/src/__tests__/reflection-probe-sampling.unit.test.ts +45 -0
  37. package/src/__tests__/scene-temporal.unit.test.ts +20 -0
  38. package/src/__tests__/shader.unit.test.ts +40 -15
  39. package/src/__tests__/shadow-pcss.unit.test.ts +131 -0
  40. package/src/__tests__/spot-cookie-composition.unit.test.ts +57 -0
  41. package/src/__tests__/spot-modifier-composition.unit.test.ts +19 -0
  42. package/src/__tests__/sprite-lit-shader.test.ts +24 -11
  43. package/src/__tests__/sprite-variants.unit.test.ts +5 -1
  44. package/src/__tests__/transmission-thickness-scale.unit.test.ts +7 -1
  45. package/src/__tests__/transparent-pbr.unit.test.ts +7 -0
  46. package/src/brdf.wgsl +115 -1
  47. package/src/common.wgsl +95 -91
  48. package/src/default-standard-pbr-skin.wgsl +108 -92
  49. package/src/default-standard-pbr.wgsl +189 -191
  50. package/src/fxaa.wgsl +27 -6
  51. package/src/ibl-irradiance.wgsl +4 -1
  52. package/src/ibl-sampling.wgsl +56 -2
  53. package/src/index.ts +23 -0
  54. package/src/lighting-directional.wgsl +161 -132
  55. package/src/lighting-probe.wgsl +51 -0
  56. package/src/lighting-punctual.wgsl +69 -29
  57. package/src/lighting-rect-area.wgsl +162 -0
  58. package/src/lighting-spot-modifiers.wgsl +93 -0
  59. package/src/ltc/provenance.ts +19 -0
  60. package/src/ltc/tables.ts +2831 -0
  61. package/src/material/artifact-types.ts +137 -0
  62. package/src/material-schemas.ts +12 -0
  63. package/src/register-default-sprite-lit.ts +3 -6
  64. package/src/scene-temporal.wgsl +14 -8
  65. package/src/shadow-pcf.wgsl +44 -1
  66. package/src/sprite-lit.wgsl +37 -53
  67. package/src/sprite.wgsl +1 -1
  68. package/src/standard-cluster.wgsl +274 -0
  69. package/src/tonemap.wgsl +8 -3
  70. package/src/types.ts +2 -1
  71. package/src/volume/volume-inject.wgsl +48 -38
  72. package/src/volume/volume-integrate.wgsl +47 -42
  73. package/dist/.tsbuildinfo +0 -1
  74. package/src/hdrp-cluster-forward.wgsl +0 -224
@@ -31,8 +31,12 @@
31
31
  // - evalSpot(lightPos, lightDir, colorTimesIntensity, cosInner, cosOuter,
32
32
  // invRangeSquared, ...) -> vec3<f32>
33
33
 
34
- #import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
35
- #import forgeax_pbr::lighting_attenuation::{evalDistanceAttenuation, evalSpotAttenuation}
34
+ #import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx, threeR184DirectMultiScatter}
35
+ #import forgeax_pbr::lighting_spot_modifiers::{spotModifierProduct}
36
+
37
+ // extendedLighting is one closed resource topology for IES, Cookie, and
38
+ // probe factors. The ordinary path supplies identity factors.
39
+ #import forgeax_pbr::lighting_attenuation::{evalDistanceAttenuation, evalSpotAttenuation, projectSpotUv}
36
40
  // feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-5):
37
41
  // spot shadow sampling reuses the shared 2D 9-tap PCF core (sample_shadow_2d)
38
42
  // and the always-on `spotShadowMap` (binding 8) + `shadowSampler` (binding 4).
@@ -78,16 +82,6 @@ fn evalVolumeSpot(
78
82
  );
79
83
  }
80
84
 
81
- // One projector UV helper is shared by surface and volume. The accepted
82
- // projectorRevision travels in the host tuple; it is deliberately not a
83
- // second shader registry or a device-side identity.
84
- fn projectSpotUv(lightViewProj : mat4x4<f32>, worldPos : vec3<f32>) -> vec2<f32> {
85
- let clip = lightViewProj * vec4<f32>(worldPos, 1.0);
86
- let invW = select(1.0 / clip.w, 0.0, abs(clip.w) < 1e-6);
87
- let projectorUv = vec2<f32>(clip.x * invW * 0.5 + 0.5, clip.y * invW * -0.5 + 0.5);
88
- return projectorUv;
89
- }
90
-
91
85
  // Shared punctual BRDF body returning (diffuse + specular) *
92
86
  // colorTimesIntensity * nDotL * attenuation. Cone factor is applied by the
93
87
  // caller (evalSpot only).
@@ -112,9 +106,13 @@ fn evalPunctualBody(
112
106
  let nDotH = max(dot(normal, h), 0.0);
113
107
  let vDotH = max(dot(viewDir, h), 0.0);
114
108
  let f = f_schlick(vDotH, F0);
115
- let specular = d_ggx(nDotH, alphaSq) * v_smith(nDotV, nDotL, alphaSq) * f;
116
- let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);
117
- let diffuse = kd * baseColor / 3.14159265;
109
+ let roughness = sqrt(max(alphaSq, 0.0));
110
+ let multiScatter = threeR184DirectMultiScatter(roughness, nDotV, nDotL, F0);
111
+ let specular = d_ggx(nDotH, alphaSq) * v_smith(nDotV, nDotL, alphaSq) * f + multiScatter;
112
+ // Three r184 direct-light parity keeps diffuse independent of the
113
+ // view-dependent Schlick term; the metallic factor is the only diffuse
114
+ // energy gate in this punctual path.
115
+ let diffuse = (1.0 - metallic) * baseColor / 3.14159265;
118
116
  let attenuation = evalDistanceAttenuation(dSquared, invRangeSquared);
119
117
  return (diffuse + specular) * colorTimesIntensity * nDotL * attenuation;
120
118
  }
@@ -138,6 +136,50 @@ fn evalPoint(
138
136
  );
139
137
  }
140
138
 
139
+ // Flat 2D punctual evaluators share the range/cone math with the clustered
140
+ // path, but intentionally skip the 3D BRDF normal term.
141
+ // Sprite-lit treats every quad as an omnidirectional receiver; keeping this
142
+ // owner here prevents URP and Cluster from drifting when the light lies in the
143
+ // sprite plane (the common 2D flashlight setup).
144
+ fn evalFlatRangeAttenuation(dSquared : f32, invRangeSquared : f32) -> f32 {
145
+ let factor = max(min(1.0 - (dSquared * invRangeSquared) * (dSquared * invRangeSquared), 1.0), 0.0);
146
+ return factor * factor / dSquared;
147
+ }
148
+
149
+ fn evalPointFlat(
150
+ lightPos : vec3<f32>,
151
+ colorTimesIntensity : vec3<f32>,
152
+ invRangeSquared : f32,
153
+ worldPos : vec3<f32>,
154
+ baseColor : vec3<f32>,
155
+ ) -> vec3<f32> {
156
+ // The flat 2D path shares the same finite-range attenuation owner as the
157
+ // clustered and direct Standard paths; its only intentional difference is
158
+ // that a sprite is an omnidirectional receiver.
159
+ let toLight = lightPos - worldPos;
160
+ let dSquared = max(dot(toLight, toLight), 1e-4);
161
+ return baseColor * colorTimesIntensity * evalFlatRangeAttenuation(dSquared, invRangeSquared);
162
+ }
163
+
164
+ fn evalSpotFlat(
165
+ lightPos : vec3<f32>,
166
+ lightDir : vec3<f32>,
167
+ colorTimesIntensity : vec3<f32>,
168
+ cosInner : f32,
169
+ cosOuter : f32,
170
+ invRangeSquared : f32,
171
+ worldPos : vec3<f32>,
172
+ baseColor : vec3<f32>,
173
+ ) -> vec3<f32> {
174
+ let toLight = lightPos - worldPos;
175
+ let dSquared = max(dot(toLight, toLight), 1e-4);
176
+ let l = toLight / sqrt(dSquared);
177
+ let cone = smoothstep(cosOuter, cosInner, dot(l, -lightDir));
178
+ return evalPointFlat(
179
+ lightPos, colorTimesIntensity, invRangeSquared, worldPos, baseColor,
180
+ ) * cone;
181
+ }
182
+
141
183
  #ifdef POINT_SHADOW_AVAILABLE
142
184
  // Shadow-modulated omnidirectional point light: same BRDF body * shadow factor.
143
185
  //
@@ -148,7 +190,8 @@ fn evalPoint(
148
190
  // projection (research L0.5 Bevy pattern); the caller passes `near` / `far`
149
191
  // directly so both pipelines route the same constants without owning the
150
192
  // upstream binding (URP reads them from `shadowParams[layer]` at @group(0)
151
- // binding 6; HDRP unpacks them off `LightSlot.kind_and_pad.zw` per
193
+ // binding 6; the shared DirectLightSlot metadata remains the identity owner
194
+ // while this shadow-parameter buffer carries the reconstruction constants per
152
195
  // plan-strategy §D-8).
153
196
  //
154
197
  // Caller responsibility: gate this on `shadowAtlasLayer >= 0` so the
@@ -175,13 +218,13 @@ fn evalPointShadowed(
175
218
  lightPos, colorTimesIntensity, invRangeSquared,
176
219
  worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
177
220
  );
178
- // Fragment-to-light direction; cubemap sample uses the local-space
179
- // direction (research L0.5: Bevy convention). For a right-handed world,
180
- // the cubemap convention flips Z so the +Z face look direction matches.
221
+ // Keep the fragment-to-light vector for BRDF and nDotL. Shadow matrices are
222
+ // authored light-to-fragment (`lookAt(lightPos, fragment)`), so the cube
223
+ // lookup must use the opposite direction. For a right-handed world, the
224
+ // cubemap convention flips Z so the +Z face look direction matches.
181
225
  let toLight = lightPos - worldPos;
182
- // Cubemap sample direction is from-fragment-to-light (Bevy + LearnOpenGL),
183
- // negated to fragment-from-light when reconstructing the depth ref.
184
- let lightLocal = vec3<f32>(toLight.x, toLight.y, -toLight.z);
226
+ let fromLight = worldPos - lightPos;
227
+ let lightLocal = vec3<f32>(fromLight.x, fromLight.y, -fromLight.z);
185
228
  // Reconstruct [0,1] NDC depth from world-space distance: largest-axis
186
229
  // projection (research L0.5). Match the per-face perspective near / far
187
230
  // configured by buildPointShadowMatrices (PointLightShadow.nearPlane /
@@ -224,9 +267,10 @@ fn evalSpot(
224
267
  lightPos, colorTimesIntensity, invRangeSquared,
225
268
  worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
226
269
  );
227
- return body * evalSpotAttenuation(
228
- lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
229
- ) / max(evalDistanceAttenuation(dot(lightPos - worldPos, lightPos - worldPos), invRangeSquared), 1e-4);
270
+ let toLight = lightPos - worldPos;
271
+ let l = normalize(toLight);
272
+ let cone = smoothstep(cosOuter, cosInner, dot(l, -lightDir));
273
+ return body * spotModifierProduct(1.0, 1.0, cone, 1.0, 1.0, 1.0);
230
274
  }
231
275
 
232
276
  // feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-4 +
@@ -287,10 +331,6 @@ fn evalSpotShadowed(
287
331
  let invW = select(1.0 / splane.w, 0.0, abs(splane.w) < 1e-6);
288
332
  let clipUv = projectSpotUv(lightViewProj, worldPos);
289
333
  let depthRef = splane.z * invW;
290
- // The accepted projectorRevision selects the same texture tuple for
291
- // surface and volume; a bound projector uses textureSampleLevel here.
292
- let projectorRevision = 0.0;
293
-
294
334
  // OOB / NaN gate: outside the light frustum (or NaN from a degenerate matrix)
295
335
  // returns fully lit. Mirrors the directional `>= 0 && <= 1` NaN-safe form.
296
336
  if (!(clipUv.x >= 0.0 && clipUv.x <= 1.0 && clipUv.y >= 0.0 && clipUv.y <= 1.0 && depthRef <= 1.0)) {
@@ -0,0 +1,162 @@
1
+ #define_import_path forgeax_pbr::lighting_rect_area
2
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
3
+ #import forgeax_view::common::{ltcLambertTexture, ltcGgxTexture, spotModifierSampler}
4
+ #endif
5
+
6
+ // One-sided finite-area direct lighting using the same Heitz LTC formulation
7
+ // and tracked 64x64 tables as Three.js r184. The rectangle corners, rather
8
+ // than a punctual center direction, are transformed into the fitted cosine
9
+ // space before the spherical edge integral is evaluated.
10
+
11
+ fn ltcUv(n : vec3<f32>, v : vec3<f32>, roughness : f32) -> vec2<f32> {
12
+ let lutScale = 63.0 / 64.0;
13
+ let lutBias = 0.5 / 64.0;
14
+ let nDotV = clamp(dot(n, v), 0.0, 1.0);
15
+ return vec2<f32>(clamp(roughness, 0.0, 1.0), sqrt(1.0 - nDotV)) * lutScale + lutBias;
16
+ }
17
+
18
+ fn ltcClippedSphereFormFactor(f : vec3<f32>) -> f32 {
19
+ let magnitude = length(f);
20
+ return max((magnitude * magnitude + f.z) / (magnitude + 1.0), 0.0);
21
+ }
22
+
23
+ fn ltcEdgeVectorFormFactor(v1 : vec3<f32>, v2 : vec3<f32>) -> vec3<f32> {
24
+ let x = clamp(dot(v1, v2), -1.0, 1.0);
25
+ let y = abs(x);
26
+ let a = 0.8543985 + (0.4965155 + 0.0145206 * y) * y;
27
+ let b = 3.4175940 + (4.1616724 + y) * y;
28
+ let rational = a / b;
29
+ let thetaOverSinTheta = select(
30
+ 0.5 * inverseSqrt(max(1.0 - x * x, 1e-7)) - rational,
31
+ rational,
32
+ x > 0.0,
33
+ );
34
+ return cross(v1, v2) * thetaOverSinTheta;
35
+ }
36
+
37
+ fn ltcEvaluate(
38
+ n : vec3<f32>,
39
+ v : vec3<f32>,
40
+ worldPos : vec3<f32>,
41
+ mInv : mat3x3<f32>,
42
+ lightPos : vec3<f32>,
43
+ axisX : vec3<f32>,
44
+ axisY : vec3<f32>,
45
+ halfWidth : f32,
46
+ halfHeight : f32,
47
+ ) -> f32 {
48
+ let halfX = axisX * halfWidth;
49
+ let halfY = axisY * halfHeight;
50
+ // Counter-clockwise from the authored front (+cross(axisX, axisY)).
51
+ let rect0 = lightPos - halfX - halfY;
52
+ let rect1 = lightPos + halfX - halfY;
53
+ let rect2 = lightPos + halfX + halfY;
54
+ let rect3 = lightPos - halfX + halfY;
55
+ let lightNormal = cross(rect1 - rect0, rect3 - rect0);
56
+ if (dot(lightNormal, worldPos - rect0) <= 0.0) {
57
+ return 0.0;
58
+ }
59
+
60
+ let tangentCandidate = v - n * dot(v, n);
61
+ let tangentLengthSquared = dot(tangentCandidate, tangentCandidate);
62
+ var tangent = vec3<f32>(1.0, 0.0, 0.0);
63
+ if (tangentLengthSquared > 1e-7) {
64
+ tangent = tangentCandidate * inverseSqrt(tangentLengthSquared);
65
+ } else {
66
+ let fallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(n.x) < 0.9);
67
+ tangent = normalize(cross(fallback, n));
68
+ }
69
+ let bitangent = -cross(n, tangent);
70
+ let transform = mInv * transpose(mat3x3<f32>(tangent, bitangent, n));
71
+
72
+ let c0 = normalize(transform * (rect0 - worldPos));
73
+ let c1 = normalize(transform * (rect1 - worldPos));
74
+ let c2 = normalize(transform * (rect2 - worldPos));
75
+ let c3 = normalize(transform * (rect3 - worldPos));
76
+ let vectorFormFactor =
77
+ ltcEdgeVectorFormFactor(c0, c1) +
78
+ ltcEdgeVectorFormFactor(c1, c2) +
79
+ ltcEdgeVectorFormFactor(c2, c3) +
80
+ ltcEdgeVectorFormFactor(c3, c0);
81
+ return ltcClippedSphereFormFactor(vectorFormFactor);
82
+ }
83
+
84
+ fn rectAreaRangeFactor(lightPos : vec3<f32>, worldPos : vec3<f32>, invRangeSquared : f32) -> f32 {
85
+ let toLight = lightPos - worldPos;
86
+ let distanceSquared = dot(toLight, toLight);
87
+ let rangeTerm = distanceSquared * invRangeSquared;
88
+ let factor = clamp(1.0 - rangeTerm * rangeTerm, 0.0, 1.0);
89
+ return factor * factor;
90
+ }
91
+
92
+ fn evalRectAreaLtcDiffuse(
93
+ lightPos : vec3<f32>, lightColor : vec3<f32>, axisX : vec3<f32>, axisY : vec3<f32>,
94
+ halfWidth : f32, halfHeight : f32, invRangeSquared : f32, worldPos : vec3<f32>,
95
+ n : vec3<f32>, v : vec3<f32>, baseColor : vec3<f32>, metallic : f32, alphaSq : f32, F0 : vec3<f32>,
96
+ ) -> vec3<f32> {
97
+ let identity = mat3x3<f32>(
98
+ vec3<f32>(1.0, 0.0, 0.0),
99
+ vec3<f32>(0.0, 1.0, 0.0),
100
+ vec3<f32>(0.0, 0.0, 1.0),
101
+ );
102
+ let formFactor = ltcEvaluate(
103
+ n, v, worldPos, identity, lightPos, axisX, axisY, halfWidth, halfHeight,
104
+ );
105
+ // The LTC edge fit is already normalized by 1/(2*pi); unlike punctual
106
+ // irradiance this form factor must not receive another Lambert 1/pi.
107
+ let diffuse = baseColor * (1.0 - metallic);
108
+ return lightColor * diffuse * formFactor * rectAreaRangeFactor(lightPos, worldPos, invRangeSquared);
109
+ }
110
+
111
+ fn evalRectAreaLtcStandard(
112
+ lightPos : vec3<f32>, lightColor : vec3<f32>, axisX : vec3<f32>, axisY : vec3<f32>,
113
+ halfWidth : f32, halfHeight : f32, invRangeSquared : f32, worldPos : vec3<f32>,
114
+ n : vec3<f32>, v : vec3<f32>, baseColor : vec3<f32>, metallic : f32, alphaSq : f32, F0 : vec3<f32>,
115
+ ) -> vec3<f32> {
116
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
117
+ let uv = ltcUv(n, v, sqrt(max(alphaSq, 0.0)));
118
+ let matrixSample = textureSampleLevel(ltcLambertTexture, spotModifierSampler, uv, 0.0);
119
+ let fresnelSample = textureSampleLevel(ltcGgxTexture, spotModifierSampler, uv, 0.0);
120
+ let mInv = mat3x3<f32>(
121
+ vec3<f32>(matrixSample.x, 0.0, matrixSample.y),
122
+ vec3<f32>(0.0, 1.0, 0.0),
123
+ vec3<f32>(matrixSample.z, 0.0, matrixSample.w),
124
+ );
125
+ let fresnel = F0 * fresnelSample.x + (vec3<f32>(1.0) - F0) * fresnelSample.y;
126
+ let specularFormFactor = ltcEvaluate(
127
+ n, v, worldPos, mInv, lightPos, axisX, axisY, halfWidth, halfHeight,
128
+ );
129
+ let diffuse = evalRectAreaLtcDiffuse(
130
+ lightPos, lightColor, axisX, axisY, halfWidth, halfHeight, invRangeSquared,
131
+ worldPos, n, v, baseColor, metallic, alphaSq, F0,
132
+ );
133
+ return diffuse + lightColor * fresnel * specularFormFactor * rectAreaRangeFactor(lightPos, worldPos, invRangeSquared);
134
+ #else
135
+ return vec3<f32>(0.0);
136
+ #endif
137
+ }
138
+
139
+ // Both renderer routes consume one complete Standard-PBR Rect contribution.
140
+ // The public names retain the direct/cluster shader import contract while the
141
+ // Lambert and fitted-GGX terms stay inseparable in one owner implementation.
142
+ fn evalRectAreaLtcLambert(
143
+ lightPos : vec3<f32>, lightColor : vec3<f32>, axisX : vec3<f32>, axisY : vec3<f32>,
144
+ halfWidth : f32, halfHeight : f32, invRangeSquared : f32, worldPos : vec3<f32>,
145
+ n : vec3<f32>, v : vec3<f32>, baseColor : vec3<f32>, metallic : f32, alphaSq : f32, F0 : vec3<f32>,
146
+ ) -> vec3<f32> {
147
+ return evalRectAreaLtcStandard(
148
+ lightPos, lightColor, axisX, axisY, halfWidth, halfHeight, invRangeSquared,
149
+ worldPos, n, v, baseColor, metallic, alphaSq, F0,
150
+ );
151
+ }
152
+
153
+ fn evalRectAreaLtcGgx(
154
+ lightPos : vec3<f32>, lightColor : vec3<f32>, axisX : vec3<f32>, axisY : vec3<f32>,
155
+ halfWidth : f32, halfHeight : f32, invRangeSquared : f32, worldPos : vec3<f32>,
156
+ n : vec3<f32>, v : vec3<f32>, baseColor : vec3<f32>, metallic : f32, alphaSq : f32, F0 : vec3<f32>,
157
+ ) -> vec3<f32> {
158
+ return evalRectAreaLtcStandard(
159
+ lightPos, lightColor, axisX, axisY, halfWidth, halfHeight, invRangeSquared,
160
+ worldPos, n, v, baseColor, metallic, alphaSq, F0,
161
+ );
162
+ }
@@ -0,0 +1,93 @@
1
+ #define_import_path forgeax_pbr::lighting_spot_modifiers
2
+
3
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
4
+ #import forgeax_view::common::{spotModifierSampler, iesProfileTexture, cookieTexture, cookieMatrices}
5
+ #endif
6
+
7
+ // DirectLightSlot.metadata.y carries this marker for the SpotLight projector
8
+ // path. A marked spot samples the shared array with its lightViewProj in the
9
+ // punctual owner, so the Cookie projection
10
+ // below must not modulate it a second time.
11
+ const PROJECTOR_FLAG: u32 = 0x40000000u;
12
+
13
+ // Shared Spot modifier composition. Identity defaults preserve the ordinary
14
+ // punctual path when an extension resource is absent.
15
+ fn spotModifierProduct(
16
+ brdf : f32,
17
+ range : f32,
18
+ cone : f32,
19
+ ies : f32,
20
+ cookie : f32,
21
+ shadow : f32,
22
+ ) -> f32 {
23
+ return brdf * range * cone * ies * cookie * shadow;
24
+ }
25
+
26
+ // Shared IES/Cookie projection owner. The light direction is the authored
27
+ // outgoing axis (light -> scene), matching evalSpot's `dot(L, -lightDir)`.
28
+ // Cookie matrices carry only source-texture aspect correction; keeping the
29
+ // cone term here allows a Cookie slice to be shared by Spots with different
30
+ // outer cones.
31
+ fn spotModifierFactors(
32
+ lightPos : vec3<f32>,
33
+ lightDir : vec3<f32>,
34
+ cosOuter : f32,
35
+ worldPos : vec3<f32>,
36
+ rollDeg : f32,
37
+ metadata : vec4<u32>,
38
+ ) -> vec4<f32> {
39
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
40
+ let toSurface = worldPos - lightPos;
41
+ let distanceToSurface = length(toSurface);
42
+ var ies = 1.0;
43
+ var cookie = vec3<f32>(1.0);
44
+ if (distanceToSurface <= 0.000001) {
45
+ return vec4<f32>(ies, cookie);
46
+ }
47
+
48
+ let outgoing = toSurface / distanceToSurface;
49
+ let forward = normalize(lightDir);
50
+ var referenceUp = vec3<f32>(0.0, 1.0, 0.0);
51
+ if (abs(forward.y) > 0.999) {
52
+ referenceUp = vec3<f32>(0.0, 0.0, 1.0);
53
+ }
54
+ let right = normalize(cross(forward, referenceUp));
55
+ let up = normalize(cross(right, forward));
56
+ let localX = dot(outgoing, right);
57
+ let localY = dot(outgoing, up);
58
+ let depth = dot(outgoing, forward);
59
+ let roll = rollDeg * 0.01745329252;
60
+ let rolledX = localX * cos(roll) - localY * sin(roll);
61
+ let rolledY = localX * sin(roll) + localY * cos(roll);
62
+
63
+ if (metadata.z != 0xffffffffu) {
64
+ let azimuth = (atan2(rolledY, rolledX) + 6.283185307) % 6.283185307;
65
+ let elevation = acos(clamp(depth, -1.0, 1.0)) / 3.14159265;
66
+ let iesUv = vec2<f32>(azimuth / 6.283185307, elevation);
67
+ ies = max(textureSampleLevel(iesProfileTexture, spotModifierSampler, iesUv, metadata.z, 0.0).r, 0.0);
68
+ }
69
+
70
+ if (metadata.w != 0xffffffffu && (metadata.y & PROJECTOR_FLAG) == 0u) {
71
+ if (depth <= 0.0) {
72
+ cookie = vec3<f32>(0.0);
73
+ } else {
74
+ let sinOuter = sqrt(max(1.0 - cosOuter * cosOuter, 0.0));
75
+ let tanOuter = sinOuter / max(cosOuter, 0.0001);
76
+ let matrixPlane = cookieMatrices[metadata.w] * vec4<f32>(rolledX / depth, rolledY / depth, 0.0, 1.0);
77
+ let cookieUv = vec2<f32>(0.5) + matrixPlane.xy / tanOuter * 0.5;
78
+ if (
79
+ cookieUv.x < 0.0 || cookieUv.x > 1.0 ||
80
+ cookieUv.y < 0.0 || cookieUv.y > 1.0
81
+ ) {
82
+ cookie = vec3<f32>(0.0);
83
+ } else {
84
+ let cookieSample = textureSampleLevel(cookieTexture, spotModifierSampler, cookieUv, metadata.w, 0.0);
85
+ cookie = max(cookieSample.rgb * cookieSample.a, vec3<f32>(0.0));
86
+ }
87
+ }
88
+ }
89
+ return vec4<f32>(ies, cookie);
90
+ #else
91
+ return vec4<f32>(1.0);
92
+ #endif
93
+ }
@@ -0,0 +1,19 @@
1
+ // @forgeax/engine-shader - provenance for the tracked RectArea LTC tables.
2
+
3
+ /**
4
+ * The table source is the Three.js r184 RectAreaLight implementation. The
5
+ * source arrays are converted to IEEE-754 binary16 offline and checked into
6
+ * `tables.ts`; the runtime never fetches or parses a source file.
7
+ */
8
+ export const LTC_SOURCE_PROVENANCE = {
9
+ source: 'Three.js',
10
+ revision: 'r184',
11
+ sourceUrl:
12
+ 'https://github.com/mrdoob/three.js/blob/r184/examples/jsm/lights/RectAreaLightTexturesLib.js',
13
+ upstreamData: 'selfshadow/ltc_code fit/results/ltc.js',
14
+ generator: 'deterministic Three.js source-array to rgba16float conversion',
15
+ inputSha256: {
16
+ lambert: 'cf5cf21e5c112d2095c7e2418cb0a1ac54636e275d73e42f3453646c67f26814',
17
+ ggx: '3b1b09080b26104498db277c14fc1733786465c6958e7a8403d688b1e24c2ff5',
18
+ },
19
+ } as const;