@forgeax/engine-shader 0.1.21 → 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 (59) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +4 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.mjs +33019 -8
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/ltc/provenance.d.ts +17 -0
  7. package/dist/ltc/provenance.d.ts.map +1 -0
  8. package/dist/ltc/tables.d.ts +22 -0
  9. package/dist/ltc/tables.d.ts.map +1 -0
  10. package/dist/material/artifact-types.d.ts +52 -0
  11. package/dist/material/artifact-types.d.ts.map +1 -1
  12. package/dist/material-schemas.d.ts +5 -0
  13. package/dist/material-schemas.d.ts.map +1 -1
  14. package/dist/register-default-sprite-lit.d.ts +3 -6
  15. package/dist/register-default-sprite-lit.d.ts.map +1 -1
  16. package/dist/types.d.ts +2 -1
  17. package/dist/types.d.ts.map +1 -1
  18. package/package.json +4 -4
  19. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +121 -58
  20. package/src/__tests__/direct-light-layout.unit.test.ts +40 -0
  21. package/src/__tests__/ibl-irradiance.unit.test.ts +7 -4
  22. package/src/__tests__/ibl-sampling.unit.test.ts +3 -4
  23. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +8 -2
  24. package/src/__tests__/lighting-punctual.unit.test.ts +29 -2
  25. package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +1 -1
  26. package/src/__tests__/ltc-provenance.unit.test.ts +33 -0
  27. package/src/__tests__/manifest.fixture.json +33 -0
  28. package/src/__tests__/material-contract.unit.test.ts +61 -1
  29. package/src/__tests__/probe-lighting-composition.unit.test.ts +38 -0
  30. package/src/__tests__/rect-area-brdf.unit.test.ts +36 -0
  31. package/src/__tests__/reflection-probe-sampling.unit.test.ts +24 -2
  32. package/src/__tests__/shader.unit.test.ts +6 -2
  33. package/src/__tests__/spot-cookie-composition.unit.test.ts +57 -0
  34. package/src/__tests__/spot-modifier-composition.unit.test.ts +19 -0
  35. package/src/__tests__/sprite-variants.unit.test.ts +4 -1
  36. package/src/brdf.wgsl +115 -1
  37. package/src/common.wgsl +53 -79
  38. package/src/default-standard-pbr-skin.wgsl +80 -23
  39. package/src/default-standard-pbr.wgsl +119 -50
  40. package/src/ibl-irradiance.wgsl +4 -1
  41. package/src/ibl-sampling.wgsl +2 -2
  42. package/src/index.ts +17 -0
  43. package/src/lighting-directional.wgsl +6 -116
  44. package/src/lighting-probe.wgsl +51 -0
  45. package/src/lighting-punctual.wgsl +21 -11
  46. package/src/lighting-rect-area.wgsl +162 -0
  47. package/src/lighting-spot-modifiers.wgsl +93 -0
  48. package/src/ltc/provenance.ts +19 -0
  49. package/src/ltc/tables.ts +2831 -0
  50. package/src/material/artifact-types.ts +137 -0
  51. package/src/material-schemas.ts +12 -0
  52. package/src/register-default-sprite-lit.ts +3 -6
  53. package/src/sprite-lit.wgsl +3 -3
  54. package/src/sprite.wgsl +1 -1
  55. package/src/standard-cluster.wgsl +94 -57
  56. package/src/types.ts +2 -1
  57. package/src/volume/volume-inject.wgsl +34 -36
  58. package/src/volume/volume-integrate.wgsl +48 -38
  59. package/dist/.tsbuildinfo +0 -1
@@ -1,9 +1,10 @@
1
1
  #define_import_path forgeax_material::standard
2
- #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture}
2
+ #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, shadowMap, shadowSampler, sampleMaterialTexture}
3
3
  #import forgeax_scene_temporal::{sceneViewZ}
4
4
  #import forgeax_pbr::temporal::{projectPbrSceneTemporal}
5
5
  #import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
6
- #import forgeax_pbr::ibl_sampling::{box_project, sampleIblDiffuse, sampleIblSpecular, sampleReflectionProbeSpecular}
6
+ #import forgeax_pbr::ibl_sampling::{sampleIblDiffuse, sampleIblSpecular, sampleReflectionProbeSpecular}
7
+ #import forgeax_pbr::lighting_probe::{evaluateProbeDiffuse}
7
8
  #import forgeax_pbr::tbn::{decodeTangentSpaceNormalRg, scaleTangentSpaceNormal, applyTBN}
8
9
  #import forgeax_pbr::lighting_directional::{evalDirectionalNoShadow, evalDirectionalShadowFactor}
9
10
  #ifdef CLUSTER_FORWARD_AVAILABLE
@@ -13,21 +14,12 @@
13
14
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
14
15
  #pragma variant_axis CLUSTER_FORWARD_AVAILABLE
15
16
  #pragma variant_axis VERTEX_COLOR_AVAILABLE
17
+ #pragma variant_axis PROBE_BLEND_AVAILABLE
18
+ #pragma variant_axis EXTENDED_LIGHTING_AVAILABLE
16
19
  #pragma variant_axis TRANSMISSION_AVAILABLE
17
20
  #pragma variant_axis DIRECTIONAL_PCSS_AVAILABLE
18
21
  #pragma variant_axis PROJECTOR_AVAILABLE
19
-
20
- // Reflection probes remain a bounded scene-projected sampling lane. Their
21
- // optional texture resources are the same Skylight inputs; when no probe is
22
- // admitted, the explicit Skylight path below remains authoritative.
23
- fn reflection_probe_sampling_contract(
24
- worldPosition: vec3<f32>,
25
- direction: vec3<f32>,
26
- boxCenter: vec3<f32>,
27
- boxExtents: vec3<f32>,
28
- ) -> vec3<f32> {
29
- return box_project(worldPosition, direction, boxCenter, boxExtents);
30
- }
22
+ #pragma variant_axis REFLECTION_FALLBACK_AVAILABLE
31
23
 
32
24
  // @forgeax/engine-shader - default-standard-pbr.wgsl
33
25
  // (feat-20260523-shader-template-instance-split M5 / T04).
@@ -223,6 +215,12 @@ struct SkylightUniforms {
223
215
  @group(1) @binding(22) var brdfLutSampler : sampler;
224
216
  @group(1) @binding(23) var<uniform> skylight : SkylightUniforms;
225
217
 
218
+ #ifdef PROBE_BLEND_AVAILABLE
219
+ // Optional consumer lane. The host must provide the retained 160B record
220
+ // buffer and matching BGL before enabling this capability.
221
+ @group(3) @binding(1) var<storage, read> probeBlendRecords : array<vec4<f32>>;
222
+ #endif
223
+
226
224
  // feat-20260612-hdrp-ssao M7 (round 2) D-B + D-C, scope-amend-webgl2-ubo:
227
225
  // SSAO sampling lives on the HDRP unified BGL @group(2) alongside the
228
226
  // cluster bindings (binding 7 = ssao texture, binding 8 = sampler). The
@@ -305,18 +303,12 @@ fn pick_channel(rgba : vec4<f32>, channelIndex : u32) -> f32 {
305
303
  }
306
304
  }
307
305
 
308
- // Light evaluators live in forgeax_pbr::lighting_directional +
309
- // forgeax_pbr::lighting_punctual (M5 / T02). evalDirectional consumes the
310
- // host's view UBO + shadowMap (LO 3.1.3 slope-scaled-bias 3x3 PCF;
311
- // feat-20260520-directional-light-shadow-mapping byte-equivalent). evalPoint
312
- // / evalSpot share evalPunctualBody (GGX specular + Lambertian diffuse +
313
- // KHR_lights_punctual quartic range attenuation;
314
- // feat-20260519-light-casters-point-spot-pbr M4 / w22 byte-equivalent).
315
- // Charter P4: spot light is a thin cone-multiplier on top of the punctual
316
- // body, point light is the body unchanged -- no magic-value collapse.
306
+ // Directional lighting lives in forgeax_pbr::lighting_directional. Point,
307
+ // Spot, and Rect local lights are evaluated by the Standard Cluster module
308
+ // below from one indexed light_data corpus; no per-kind fixed-slot loop is
309
+ // present in this material shader.
317
310
 
318
- @vertex
319
- fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
311
+ fn vs_main_impl(in : VsIn, meshIndex : u32, instanceIndex : u32) -> VsOut {
320
312
  // feat-20260604-instances-per-instance-transform-shader-group3-bin M1 / w4:
321
313
  // Entity world from meshes[0] — the @group(2) dynamic-offset window has
322
314
  // already been aimed at this entity's slot (render-system-record.ts:2996
@@ -324,8 +316,8 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
324
316
  // local from instances[idx] — @group(3) is a flat per-instance buffer indexed
325
317
  // directly by instance_index (firstInstance=0, render-system-record.ts:2898).
326
318
  // Combine: entity_world * per_instance_local.
327
- let instanceLocal = instances[idx].localFromInstance;
328
- let entityWorld = meshes[0].worldFromLocal;
319
+ let instanceLocal = instances[instanceIndex].localFromInstance;
320
+ let entityWorld = meshes[meshIndex].worldFromLocal;
329
321
  let localToWorld = entityWorld * instanceLocal;
330
322
  let world = localToWorld * vec4<f32>(in.pos, 1.0);
331
323
  var out : VsOut;
@@ -396,6 +388,19 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
396
388
  return out;
397
389
  }
398
390
 
391
+ @vertex
392
+ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
393
+ return vs_main_impl(in, 0u, idx);
394
+ }
395
+
396
+ @vertex
397
+ fn vs_scene_index(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
398
+ // GPU-driven scene projection packs one Mesh row per visible candidate and
399
+ // uses the identity instance row. The scene index therefore selects the
400
+ // projected Mesh directly while retaining the Standard PBR varyings.
401
+ return vs_main_impl(in, idx, 0u);
402
+ }
403
+
399
404
  fn transformedMaterialUv(transform : vec4<f32>, metadata : vec4<f32>, in : VsOut) -> vec2<f32> {
400
405
  var source = in.uv;
401
406
  if (metadata.x >= 1.0) { source = in.uv1; }
@@ -457,8 +462,17 @@ fn alphaTest(alpha : f32) {
457
462
  }
458
463
  }
459
464
 
465
+ struct StandardPbrOutput {
466
+ @location(0) color : vec4<f32>,
467
+ #ifdef REFLECTION_FALLBACK_AVAILABLE
468
+ // Linear HDR environment contribution from this same Standard BRDF callsite.
469
+ // The detached producer names this second target S_fallback.
470
+ @location(1) reflectionFallback : vec4<f32>,
471
+ #endif
472
+ };
473
+
460
474
  @fragment
461
- fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
475
+ fn fs_main(in : VsOut) -> StandardPbrOutput {
462
476
  let baseUv = transformedMaterialUv(material.baseColorTextureCoordinatesTransform, material.baseColorTextureCoordinatesMetadata, in);
463
477
  let baseSample = sampleMaterialTexture(baseColorTexture, baseColorSampler, baseUv, material.baseColorTextureCoordinatesMetadata.zw);
464
478
  let vertexColor = materialVertexColor(in);
@@ -571,7 +585,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
571
585
  // feat-20260520-directional-light-shadow-mapping +
572
586
  // feat-20260518-pbr-direct-lighting-mvp Finding 4):
573
587
  //
574
- // color = ambient(IBL) + directional(shadowed) + sum(point) + sum(spot)
588
+ // color = ambient(IBL) + directional(shadowed) + clustered local lights
575
589
  //
576
590
  // When the host Skylight bind group provides default-zero resources the IBL
577
591
  // helpers sample to vec3(0) and ambient = 0, so the shader falls through to
@@ -587,22 +601,19 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
587
601
  // split-sum mip lookup, so we re-derive the post-shader-clamp roughness
588
602
  // here (matches sampleIblSpecular's `mip = roughness * 4.0` expectation).
589
603
  //
590
- // Direct lights (1 + N + N): one directional + N point + N spot, summed
591
- // sequentially. LIGHT_ARRAY_MAX_SLOTS = 4 host-side bounds the loop trip
592
- // counts. GGX BRDF helpers (d_ggx / v_smith / f_schlick from
593
- // forgeax_pbr::brdf) run inside each evalDirectional / evalPoint / evalSpot
594
- // so all three light types share the same microfacet specular + Lambertian
595
- // diffuse form. The directional path additionally projects worldPos
596
- // through the per-cascade light-space matrix in the fragment stage for
597
- // shadow-map PCF lookup (CSM, feat-20260613).
604
+ // Directional lighting is evaluated here. All local lights (Point, Spot,
605
+ // Rect) are evaluated by the Standard Cluster module below, which reads
606
+ // one indexed light_data corpus and never performs a fixed-slot loop.
598
607
  let kD = (vec3<f32>(1.0) - f_schlick(max(dot(n, v), 0.0), f0)) * (1.0 - metallic);
599
608
  let iblRoughness = clamp(max(finiteScalar(material.roughness, 0.5), 0.04) * roughnessTex, 0.04, 1.0);
600
609
  var irradiance = vec3<f32>(0.0);
601
610
  var specularIbl = vec3<f32>(0.0);
602
611
  if (skylight.intensity < 0.0) {
603
- // A negative intensity is the renderer-owned probe sentinel. The four
604
- // color/rotation lanes carry probe center and half extents in this mode;
605
- // Skylight resources remain unchanged and therefore preserve the ABI.
612
+ // A negative intensity is the renderer-owned probe sentinel. The producer
613
+ // stores -(probeIntensity + 1), so intensity=0 remains distinguishable
614
+ // from the ordinary Skylight zero payload. The four color/rotation lanes
615
+ // carry probe center and half extents in this mode; Skylight resources
616
+ // remain unchanged and therefore preserve the ABI.
606
617
  specularIbl = sampleReflectionProbeSpecular(
607
618
  n, v, iblRoughness, f0, in.worldPos,
608
619
  vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB),
@@ -624,20 +635,67 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
624
635
  // CLUSTER_FORWARD_AVAILABLE branch below can `ambient *=` the SSAO
625
636
  // factor. The non-HDRP path leaves ambient untouched.
626
637
  let skyColor = vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB);
638
+ let skyFactor = skyColor * skylight.intensity;
639
+ var reflectionFallback = specularIbl * (vec3<f32>(1.0) - coatF);
627
640
  var ambient = (kD * irradiance * diffuseAlbedo + specularIbl) * (vec3<f32>(1.0) - coatF);
641
+ #ifdef PROBE_BLEND_AVAILABLE
642
+ // group(3) is the per-object bind group; record zero is the object selected
643
+ // by this draw, independent of instance count.
644
+ let probeShPreblend = array<vec4<f32>, 9>(
645
+ probeBlendRecords[1], probeBlendRecords[2], probeBlendRecords[3],
646
+ probeBlendRecords[4], probeBlendRecords[5], probeBlendRecords[6],
647
+ probeBlendRecords[7], probeBlendRecords[8], probeBlendRecords[9],
648
+ );
649
+ let probeLocalBlendFraction = probeBlendRecords[0].z;
650
+ let probeDiffuseK = kD / max(vec3<f32>(1.0 - metallic), vec3<f32>(0.0001));
651
+ let probeDiffuse = evaluateProbeDiffuse(probeShPreblend, probeLocalBlendFraction, n, irradiance * skyFactor, probeDiffuseK, albedo, metallic);
652
+ ambient = (probeDiffuse + specularIbl * skyFactor) * (vec3<f32>(1.0) - coatF);
653
+ #endif
628
654
  if (material.clearcoat != 0.0) {
629
- let clearcoatIbl = sampleIblSpecular(
630
- n, v, coatRoughness, vec3<f32>(0.04),
631
- skylight.rotation,
632
- prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
633
- );
634
- ambient = ambient + clearcoatIbl * material.clearcoat;
655
+ // Keep the clearcoat lobe on the same selected environment as the base
656
+ // specular lobe. Probe mode reuses the packed center/extents lanes in the
657
+ // per-probe uniform; routing it through global Skylight IBL would make the
658
+ // fallback a mixed-source projection and break c=1 source equivalence.
659
+ var clearcoatIbl = vec3<f32>(0.0);
660
+ if (skylight.intensity < 0.0) {
661
+ clearcoatIbl = sampleReflectionProbeSpecular(
662
+ n, v, coatRoughness, vec3<f32>(0.04), in.worldPos,
663
+ vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB),
664
+ skylight.rotation.xyz, vec4<f32>(0.0, 0.0, 0.0, 1.0),
665
+ prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
666
+ );
667
+ } else {
668
+ clearcoatIbl = sampleIblSpecular(
669
+ n, v, coatRoughness, vec3<f32>(0.04),
670
+ skylight.rotation,
671
+ prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
672
+ );
673
+ }
674
+ let clearcoatContribution = clearcoatIbl * material.clearcoat;
675
+ #ifdef PROBE_BLEND_AVAILABLE
676
+ if (skylight.intensity < 0.0) {
677
+ ambient = ambient + clearcoatContribution;
678
+ } else {
679
+ ambient = ambient + clearcoatIbl * material.clearcoat * skyFactor;
680
+ }
681
+ #else
682
+ ambient = ambient + clearcoatContribution;
683
+ #endif
684
+ reflectionFallback = reflectionFallback + clearcoatContribution;
635
685
  }
686
+ #ifdef PROBE_BLEND_AVAILABLE
687
+ ambient = ambient * ao;
688
+ #else
636
689
  if (skylight.intensity < 0.0) {
637
- ambient = ambient * (-skylight.intensity) * ao;
690
+ let environmentScale = max(-skylight.intensity - 1.0, 0.0) * ao;
691
+ ambient = ambient * environmentScale;
692
+ reflectionFallback = reflectionFallback * environmentScale;
638
693
  } else {
639
- ambient = ambient * skyColor * skylight.intensity * ao;
694
+ let environmentScale = skyColor * skylight.intensity * ao;
695
+ ambient = ambient * environmentScale;
696
+ reflectionFallback = reflectionFallback * environmentScale;
640
697
  }
698
+ #endif
641
699
  #ifdef CLUSTER_FORWARD_AVAILABLE
642
700
  // feat-20260612-hdrp-ssao M2 round-1 + M7 round-2 (plan-strategy D-7 + D-B + D-C):
643
701
  // SSAO ambient synthesis. Reads the half-res R8 `ssaoBlurredTexture` from
@@ -653,7 +711,9 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
653
711
  let ssaoFactor = textureSample(ssaoBlurredTexture, ssaoBlurredSampler, ssaoUv).r;
654
712
  // scope-amend-webgl2-ubo: intensity packed into cluster_uniform.near_far_log.w.
655
713
  let ssaoIntensity = get_ssao_intensity();
714
+ let ssaoScale = mix(1.0, ssaoFactor * ao, ssaoIntensity);
656
715
  ambient *= mix(1.0, ssaoFactor * ao, ssaoIntensity);
716
+ reflectionFallback *= ssaoScale;
657
717
  #endif
658
718
  var color = ambient;
659
719
  #ifdef TRANSMISSION_AVAILABLE
@@ -770,7 +830,12 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
770
830
  let emissiveUv = transformedMaterialUv(material.emissiveTextureCoordinatesTransform, material.emissiveTextureCoordinatesMetadata, in);
771
831
  let emissiveSample = sampleMaterialTexture(emissiveTexture, emissiveSampler, emissiveUv, material.emissiveTextureCoordinatesMetadata.zw).rgb;
772
832
  color = color + material.emissive * material.emissiveIntensity * emissiveSample;
773
- return vec4<f32>(color, alpha);
833
+ var output : StandardPbrOutput;
834
+ output.color = vec4<f32>(color, alpha);
835
+ #ifdef REFLECTION_FALLBACK_AVAILABLE
836
+ output.reflectionFallback = vec4<f32>(reflectionFallback, 1.0);
837
+ #endif
838
+ return output;
774
839
  }
775
840
 
776
841
  // ── G-buffer output struct (feat-20260612-hdrp-deferred-shading M2 / w12) ──
@@ -869,7 +934,7 @@ fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32) -> TemporalVsOut {
869
934
  let currentWorld = meshes[0].worldFromLocal *
870
935
  instances[idx].localFromInstance * vec4<f32>(in.pos, 1.0);
871
936
  var previousWorld = currentWorld;
872
- #if STORAGE_BUFFER_AVAILABLE == true
937
+ #ifdef STORAGE_BUFFER_AVAILABLE
873
938
  previousWorld = meshes[0].previousWorldFromLocal *
874
939
  instances[idx].previousLocalFromInstance * vec4<f32>(in.pos, 1.0);
875
940
  #endif
@@ -906,6 +971,10 @@ fn temporalVertexAlpha(in : TemporalVsOut) -> f32 {
906
971
 
907
972
  @fragment
908
973
  fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
974
+ var reactive = 0.0;
975
+ #ifdef STORAGE_BUFFER_AVAILABLE
976
+ reactive = meshes[0].temporal.x;
977
+ #endif
909
978
  return projectPbrSceneTemporal(
910
979
  material.baseColor.a * temporalVertexAlpha(in),
911
980
  material.alphaCutoff,
@@ -83,7 +83,10 @@ fn irradianceConvolve_fs(in0: CubemapVsOut) -> @location(0) vec4<f32> {
83
83
  phi += IRRADIANCE_SAMPLE_DELTA;
84
84
  }
85
85
 
86
- // IRRADIANCE_PAYLOAD_E_TIMES_PI: the stored payload is E * pi.
86
+ // IRRADIANCE_PAYLOAD_E_OVER_PI: PI * the uniform theta/phi sample average
87
+ // produces the Lambert-normalized diffuse radiance E / PI. Runtime sampling
88
+ // consumes this payload directly; applying another Lambert divide would
89
+ // darken diffuse IBL by PI.
87
90
  irradiance = PI * irradiance / max(nrSamples, 1.0);
88
91
  return vec4<f32>(irradiance, 1.0);
89
92
  }
@@ -19,7 +19,7 @@
19
19
  // - box_project(worldPosition, direction, boxCenter, boxExtents)
20
20
  // - sampleReflectionProbeSpecular(..., probeMap, probeSampler, ...)
21
21
 
22
- #import forgeax_pbr::ibl_shared::{PI, fresnelSchlickRoughness, inverseRotateEnvironment}
22
+ #import forgeax_pbr::ibl_shared::{fresnelSchlickRoughness, inverseRotateEnvironment}
23
23
 
24
24
  // Sample pre-convolved irradiance from the irradiance cubemap.
25
25
  // Y is negated to compensate for WebGPU's top-left texture origin vs the
@@ -33,7 +33,7 @@ fn sampleIblDiffuse(
33
33
  let rotated = inverseRotateEnvironment(normal, rotation);
34
34
  let dir = vec3<f32>(rotated.x, -rotated.y, rotated.z);
35
35
  let irradianceEOverPi = textureSample(irradianceMap, irradianceSampler, dir).rgb;
36
- return irradianceEOverPi / PI;
36
+ return irradianceEOverPi;
37
37
  }
38
38
 
39
39
  // Split-sum specular IBL: prefiltered env * (F0 * scale + bias).
package/src/index.ts CHANGED
@@ -39,6 +39,16 @@ export {
39
39
  type ShaderErrorDetail,
40
40
  shaderNotFound,
41
41
  } from './errors.js';
42
+ export {
43
+ generateLtcTables,
44
+ hashLtcTable,
45
+ LTC_SOURCE_PROVENANCE,
46
+ LTC_TABLE_HASHES,
47
+ LTC_TABLE_HEIGHT,
48
+ LTC_TABLE_INPUT_HASHES,
49
+ LTC_TABLE_WIDTH,
50
+ LTC_TABLES,
51
+ } from './ltc/tables.js';
42
52
  export {
43
53
  type MaterialArtifactConflictError,
44
54
  type MaterialArtifactInspection,
@@ -46,8 +56,12 @@ export {
46
56
  type MaterialRuntimeArtifact,
47
57
  } from './material/artifact-registry.js';
48
58
  export {
59
+ createStandardPbrArtifactReceipt,
49
60
  isMaterialShaderArtifact,
50
61
  type MaterialShaderArtifact,
62
+ type MaterialShaderArtifactReceipt,
63
+ type MaterialShaderResourceSlot,
64
+ type MaterialShaderVertexInput,
51
65
  } from './material/artifact-types.js';
52
66
  export {
53
67
  DEFAULT_MSDF_TEXT_PARAM_SCHEMA,
@@ -55,12 +69,15 @@ export {
55
69
  DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
56
70
  DEFAULT_UNLIT_PARAM_SCHEMA,
57
71
  STANDARD_PBR_ALPHA_CUTOFF_DEFAULT,
72
+ STANDARD_PBR_ARTIFACT_RECEIPT,
73
+ STANDARD_PBR_SKIN_ARTIFACT_RECEIPT,
58
74
  } from './material-schemas.js';
59
75
  export {
60
76
  registerDefaultSpriteLit,
61
77
  type SpriteLitCaps,
62
78
  } from './register-default-sprite-lit.js';
63
79
  export { registerDefaultStandardPbrSkin } from './register-default-standard-pbr-skin.js';
80
+ export const RECT_AREA_LTC_SHADER_MODULE = 'forgeax::lighting-rect-area' as const;
64
81
  export {
65
82
  FORGEAX_RESERVED_PATH_PREFIX,
66
83
  type MaterialShaderEntry,
@@ -46,7 +46,7 @@
46
46
  // layered BRDFs such as base + clearcoat)
47
47
 
48
48
  #import forgeax_view::common::{view, shadowMap, shadowSampler}
49
- #import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
49
+ #import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx, threeR184DirectMultiScatter}
50
50
  #import forgeax_pbr::shadow_pcf::{sample_shadow_2d}
51
51
  #ifdef DIRECTIONAL_PCSS_AVAILABLE
52
52
  #import forgeax_pbr::shadow_pcf::{shadow_biased_receiver_depth, shadow_clamp_texel_to_tile, shadow_load_raw_depth, shadow_sample_compare}
@@ -80,118 +80,9 @@ const PCSS_DISK_OFFSETS : array<vec2<f32>, 32> = array<vec2<f32>, 32>(
80
80
  );
81
81
  #endif
82
82
 
83
- // Source: Three.js r184 src/nodes/functions/BSDF/DFGLUT.js. The full 16x16
84
- // RG16F table is decoded to exact f32 constants here; the lookup below mirrors
85
- // the source DataTexture's linear filtering and clamp-to-edge sampling without
86
- // adding a host binding or coupling direct light to the engine IBL LUT.
87
- const THREE_R184_DFG_LUT_SIZE : u32 = 16u;
88
- const THREE_R184_DFG_LUT : array<vec2<f32>, 256> = array<vec2<f32>, 256>(
89
- vec2<f32>(0.1470947265625, 0.85205078125), vec2<f32>(0.16552734375, 0.78759765625), vec2<f32>(0.244384765625, 0.638671875), vec2<f32>(0.370849609375, 0.51953125),
90
- vec2<f32>(0.496826171875, 0.41552734375), vec2<f32>(0.60205078125, 0.326416015625), vec2<f32>(0.68408203125, 0.25390625), vec2<f32>(0.74609375, 0.197509765625),
91
- vec2<f32>(0.79052734375, 0.154296875), vec2<f32>(0.822265625, 0.12164306640625), vec2<f32>(0.84326171875, 0.09698486328125), vec2<f32>(0.8564453125, 0.07843017578125),
92
- vec2<f32>(0.86328125, 0.06439208984375), vec2<f32>(0.86572265625, 0.0537109375), vec2<f32>(0.8642578125, 0.045440673828125), vec2<f32>(0.85986328125, 0.039031982421875),
93
- vec2<f32>(0.388671875, 0.611328125), vec2<f32>(0.39306640625, 0.6005859375), vec2<f32>(0.412353515625, 0.5458984375), vec2<f32>(0.45654296875, 0.4482421875),
94
- vec2<f32>(0.52783203125, 0.3525390625), vec2<f32>(0.607421875, 0.27392578125), vec2<f32>(0.6787109375, 0.21142578125), vec2<f32>(0.7333984375, 0.16259765625),
95
- vec2<f32>(0.77099609375, 0.1253662109375), vec2<f32>(0.79345703125, 0.0972900390625), vec2<f32>(0.80322265625, 0.07623291015625), vec2<f32>(0.8037109375, 0.06036376953125),
96
- vec2<f32>(0.79638671875, 0.048431396484375), vec2<f32>(0.78564453125, 0.03936767578125), vec2<f32>(0.77197265625, 0.03240966796875), vec2<f32>(0.7548828125, 0.0269775390625),
97
- vec2<f32>(0.572265625, 0.427490234375), vec2<f32>(0.57373046875, 0.424072265625), vec2<f32>(0.57958984375, 0.403564453125), vec2<f32>(0.591796875, 0.3544921875),
98
- vec2<f32>(0.6162109375, 0.2880859375), vec2<f32>(0.6552734375, 0.224853515625), vec2<f32>(0.69873046875, 0.172607421875), vec2<f32>(0.7353515625, 0.1318359375),
99
- vec2<f32>(0.75927734375, 0.10089111328125), vec2<f32>(0.77001953125, 0.07745361328125), vec2<f32>(0.77197265625, 0.0599365234375), vec2<f32>(0.76611328125, 0.046844482421875),
100
- vec2<f32>(0.751953125, 0.0369873046875), vec2<f32>(0.732421875, 0.029541015625), vec2<f32>(0.70947265625, 0.023834228515625), vec2<f32>(0.68359375, 0.019439697265625),
101
- vec2<f32>(0.708984375, 0.291015625), vec2<f32>(0.708984375, 0.289794921875), vec2<f32>(0.7099609375, 0.28125), vec2<f32>(0.7099609375, 0.258544921875),
102
- vec2<f32>(0.71142578125, 0.220458984375), vec2<f32>(0.7197265625, 0.1768798828125), vec2<f32>(0.734375, 0.1370849609375), vec2<f32>(0.748046875, 0.10479736328125),
103
- vec2<f32>(0.755859375, 0.07989501953125), vec2<f32>(0.759765625, 0.061004638671875), vec2<f32>(0.75341796875, 0.046844482421875), vec2<f32>(0.73876953125, 0.036224365234375),
104
- vec2<f32>(0.7177734375, 0.02825927734375), vec2<f32>(0.69189453125, 0.0222625732421875), vec2<f32>(0.6611328125, 0.0177001953125), vec2<f32>(0.62841796875, 0.01419830322265625),
105
- vec2<f32>(0.80810546875, 0.1917724609375), vec2<f32>(0.8076171875, 0.1912841796875), vec2<f32>(0.80615234375, 0.18798828125), vec2<f32>(0.8017578125, 0.1781005859375),
106
- vec2<f32>(0.79296875, 0.15869140625), vec2<f32>(0.78369140625, 0.1322021484375), vec2<f32>(0.775390625, 0.1048583984375), vec2<f32>(0.7685546875, 0.08111572265625),
107
- vec2<f32>(0.7646484375, 0.061981201171875), vec2<f32>(0.75537109375, 0.047271728515625), vec2<f32>(0.740234375, 0.036163330078125), vec2<f32>(0.71875, 0.0277557373046875),
108
- vec2<f32>(0.69091796875, 0.021484375), vec2<f32>(0.658203125, 0.0167388916015625), vec2<f32>(0.6220703125, 0.01316070556640625), vec2<f32>(0.583984375, 0.01041412353515625),
109
- vec2<f32>(0.87841796875, 0.1217041015625), vec2<f32>(0.8779296875, 0.1217041015625), vec2<f32>(0.875, 0.12060546875), vec2<f32>(0.869140625, 0.116943359375),
110
- vec2<f32>(0.85693359375, 0.1080322265625), vec2<f32>(0.837890625, 0.09375), vec2<f32>(0.81494140625, 0.076904296875), vec2<f32>(0.7958984375, 0.060699462890625),
111
- vec2<f32>(0.7763671875, 0.046875), vec2<f32>(0.75634765625, 0.035919189453125), vec2<f32>(0.732421875, 0.0274505615234375), vec2<f32>(0.703125, 0.021026611328125),
112
- vec2<f32>(0.6689453125, 0.01617431640625), vec2<f32>(0.63037109375, 0.01251220703125), vec2<f32>(0.58935546875, 0.00974273681640625), vec2<f32>(0.54638671875, 0.007633209228515625),
113
- vec2<f32>(0.92626953125, 0.07379150390625), vec2<f32>(0.92578125, 0.07391357421875), vec2<f32>(0.9228515625, 0.07391357421875), vec2<f32>(0.9169921875, 0.0731201171875),
114
- vec2<f32>(0.90380859375, 0.06982421875), vec2<f32>(0.88134765625, 0.0631103515625), vec2<f32>(0.85107421875, 0.053802490234375), vec2<f32>(0.8212890625, 0.043701171875),
115
- vec2<f32>(0.791015625, 0.034393310546875), vec2<f32>(0.76123046875, 0.026611328125), vec2<f32>(0.72802734375, 0.02044677734375), vec2<f32>(0.69189453125, 0.015655517578125),
116
- vec2<f32>(0.65087890625, 0.0120086669921875), vec2<f32>(0.607421875, 0.00925445556640625), vec2<f32>(0.56103515625, 0.0071563720703125), vec2<f32>(0.51416015625, 0.005565643310546875),
117
- vec2<f32>(0.95751953125, 0.042327880859375), vec2<f32>(0.95703125, 0.042449951171875), vec2<f32>(0.95458984375, 0.042816162109375), vec2<f32>(0.94921875, 0.043182373046875),
118
- vec2<f32>(0.93701171875, 0.04266357421875), vec2<f32>(0.9130859375, 0.040252685546875), vec2<f32>(0.8818359375, 0.035797119140625), vec2<f32>(0.8447265625, 0.0301361083984375),
119
- vec2<f32>(0.80615234375, 0.0243377685546875), vec2<f32>(0.76708984375, 0.0191497802734375), vec2<f32>(0.7265625, 0.01485443115234375), vec2<f32>(0.6826171875, 0.01142120361328125),
120
- vec2<f32>(0.63623046875, 0.0087738037109375), vec2<f32>(0.5869140625, 0.006740570068359375), vec2<f32>(0.53662109375, 0.005199432373046875), vec2<f32>(0.486083984375, 0.00402069091796875),
121
- vec2<f32>(0.9775390625, 0.0226287841796875), vec2<f32>(0.97705078125, 0.0227508544921875), vec2<f32>(0.97509765625, 0.023193359375), vec2<f32>(0.9697265625, 0.0239105224609375),
122
- vec2<f32>(0.95947265625, 0.0244903564453125), vec2<f32>(0.93603515625, 0.0241241455078125), vec2<f32>(0.9052734375, 0.02252197265625), vec2<f32>(0.865234375, 0.0197601318359375),
123
- vec2<f32>(0.82177734375, 0.016510009765625), vec2<f32>(0.7744140625, 0.0132904052734375), vec2<f32>(0.7265625, 0.01045989990234375), vec2<f32>(0.67626953125, 0.0081329345703125),
124
- vec2<f32>(0.6240234375, 0.00627899169921875), vec2<f32>(0.56982421875, 0.004825592041015625), vec2<f32>(0.51513671875, 0.0037136077880859375), vec2<f32>(0.46142578125, 0.0028629302978515625),
125
- vec2<f32>(0.98876953125, 0.01107025146484375), vec2<f32>(0.98876953125, 0.01116180419921875), vec2<f32>(0.98681640625, 0.01151275634765625), vec2<f32>(0.98291015625, 0.01221466064453125),
126
- vec2<f32>(0.97314453125, 0.01299285888671875), vec2<f32>(0.953125, 0.013519287109375), vec2<f32>(0.9228515625, 0.01328277587890625), vec2<f32>(0.88232421875, 0.01226043701171875),
127
- vec2<f32>(0.8349609375, 0.01065826416015625), vec2<f32>(0.783203125, 0.00885009765625), vec2<f32>(0.728515625, 0.007114410400390625), vec2<f32>(0.671875, 0.00560760498046875),
128
- vec2<f32>(0.61376953125, 0.004360198974609375), vec2<f32>(0.5546875, 0.0033721923828125), vec2<f32>(0.496337890625, 0.0025997161865234375), vec2<f32>(0.439453125, 0.0020046234130859375),
129
- vec2<f32>(0.9951171875, 0.00479888916015625), vec2<f32>(0.9951171875, 0.004863739013671875), vec2<f32>(0.99365234375, 0.005096435546875), vec2<f32>(0.990234375, 0.005603790283203125),
130
- vec2<f32>(0.9814453125, 0.0063323974609375), vec2<f32>(0.96435546875, 0.006977081298828125), vec2<f32>(0.93603515625, 0.007297515869140625), vec2<f32>(0.896484375, 0.0071258544921875),
131
- vec2<f32>(0.8466796875, 0.00649261474609375), vec2<f32>(0.79150390625, 0.005596160888671875), vec2<f32>(0.7314453125, 0.004627227783203125), vec2<f32>(0.6689453125, 0.0037174224853515625),
132
- vec2<f32>(0.60546875, 0.0029296875), vec2<f32>(0.54150390625, 0.00228118896484375), vec2<f32>(0.479248046875, 0.001766204833984375), vec2<f32>(0.419677734375, 0.00136566162109375),
133
- vec2<f32>(0.998046875, 0.0017604827880859375), vec2<f32>(0.998046875, 0.0017938613891601562), vec2<f32>(0.99658203125, 0.0019292831420898438), vec2<f32>(0.994140625, 0.002239227294921875),
134
- vec2<f32>(0.986328125, 0.0027294158935546875), vec2<f32>(0.9716796875, 0.0032501220703125), vec2<f32>(0.94580078125, 0.00366973876953125), vec2<f32>(0.90771484375, 0.003818511962890625),
135
- vec2<f32>(0.8583984375, 0.003681182861328125), vec2<f32>(0.79931640625, 0.0033206939697265625), vec2<f32>(0.7353515625, 0.0028438568115234375), vec2<f32>(0.66748046875, 0.0023441314697265625),
136
- vec2<f32>(0.5986328125, 0.0018796920776367188), vec2<f32>(0.5302734375, 0.0014829635620117188), vec2<f32>(0.464111328125, 0.0011587142944335938), vec2<f32>(0.402099609375, 0.0008993148803710938),
137
- vec2<f32>(0.99951171875, 0.0005011558532714844), vec2<f32>(0.99951171875, 0.0005154609680175781), vec2<f32>(0.998046875, 0.000583648681640625), vec2<f32>(0.99658203125, 0.0007505416870117188),
138
- vec2<f32>(0.9892578125, 0.00101470947265625), vec2<f32>(0.97607421875, 0.001346588134765625), vec2<f32>(0.95263671875, 0.0016527175903320312), vec2<f32>(0.916015625, 0.0018558502197265625),
139
- vec2<f32>(0.8671875, 0.0019054412841796875), vec2<f32>(0.8076171875, 0.0018167495727539062), vec2<f32>(0.7392578125, 0.001621246337890625), vec2<f32>(0.6669921875, 0.0013799667358398438),
140
- vec2<f32>(0.59326171875, 0.0011358261108398438), vec2<f32>(0.52001953125, 0.0009121894836425781), vec2<f32>(0.450439453125, 0.0007219314575195312), vec2<f32>(0.385986328125, 0.0005650520324707031),
141
- vec2<f32>(1.0, 0.00009316205978393555), vec2<f32>(1.0, 0.0000978708267211914), vec2<f32>(0.9990234375, 0.00012540817260742188), vec2<f32>(0.9970703125, 0.00019216537475585938),
142
- vec2<f32>(0.99072265625, 0.0003104209899902344), vec2<f32>(0.97900390625, 0.0004699230194091797), vec2<f32>(0.95751953125, 0.0006470680236816406), vec2<f32>(0.92333984375, 0.0007915496826171875),
143
- vec2<f32>(0.87548828125, 0.0008769035339355469), vec2<f32>(0.81494140625, 0.0008869171142578125), vec2<f32>(0.744140625, 0.0008325576782226562), vec2<f32>(0.66748046875, 0.0007386207580566406),
144
- vec2<f32>(0.58837890625, 0.0006265640258789062), vec2<f32>(0.51123046875, 0.0005168914794921875), vec2<f32>(0.438232421875, 0.0004165172576904297), vec2<f32>(0.37109375, 0.0003311634063720703),
145
- vec2<f32>(1.0, 0.000007271766662597656), vec2<f32>(1.0, 0.000008165836334228516), vec2<f32>(0.9990234375, 0.000016987323760986328), vec2<f32>(0.99755859375, 0.00003790855407714844),
146
- vec2<f32>(0.9921875, 0.00007593631744384766), vec2<f32>(0.9814453125, 0.00013744831085205078), vec2<f32>(0.96142578125, 0.00020754337310791016), vec2<f32>(0.92919921875, 0.00028014183044433594),
147
- vec2<f32>(0.8828125, 0.0003345012664794922), vec2<f32>(0.82177734375, 0.00036263465881347656), vec2<f32>(0.7490234375, 0.00036215782165527344), vec2<f32>(0.66845703125, 0.0003380775451660156),
148
- vec2<f32>(0.58544921875, 0.00029969215393066406), vec2<f32>(0.50390625, 0.00025582313537597656), vec2<f32>(0.427001953125, 0.0002117156982421875), vec2<f32>(0.357666015625, 0.00017201900482177734),
149
- vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 5.960464477539063e-8), vec2<f32>(0.99951171875, 0.0000012516975402832031), vec2<f32>(0.99755859375, 0.000005304813385009766),
150
- vec2<f32>(0.9931640625, 0.000015079975128173828), vec2<f32>(0.98291015625, 0.00002855062484741211), vec2<f32>(0.96435546875, 0.00004744529724121094), vec2<f32>(0.93408203125, 0.00006842613220214844),
151
- vec2<f32>(0.88916015625, 0.00008893013000488281), vec2<f32>(0.828125, 0.0001042485237121582), vec2<f32>(0.75390625, 0.00011217594146728516), vec2<f32>(0.67041015625, 0.00011241436004638672),
152
- vec2<f32>(0.5830078125, 0.00010627508163452148), vec2<f32>(0.4970703125, 0.00009584426879882812), vec2<f32>(0.4169921875, 0.0000833272933959961), vec2<f32>(0.34521484375, 0.00007051229476928711),
153
- );
154
-
155
- fn _sampleThreeR184DfgLut(roughness : f32, dotNV : f32) -> vec2<f32> {
156
- let uv = clamp(vec2<f32>(roughness, dotNV), vec2<f32>(0.0), vec2<f32>(1.0));
157
- let samplePosition = uv * f32(THREE_R184_DFG_LUT_SIZE) - vec2<f32>(0.5);
158
- let base = vec2<i32>(floor(samplePosition));
159
- let weight = fract(samplePosition);
160
- let last = i32(THREE_R184_DFG_LUT_SIZE - 1u);
161
- let lo = clamp(base, vec2<i32>(0), vec2<i32>(last));
162
- let hi = clamp(base + vec2<i32>(1), vec2<i32>(0), vec2<i32>(last));
163
- let rowLo = mix(
164
- THREE_R184_DFG_LUT[u32(lo.y) * THREE_R184_DFG_LUT_SIZE + u32(lo.x)],
165
- THREE_R184_DFG_LUT[u32(lo.y) * THREE_R184_DFG_LUT_SIZE + u32(hi.x)],
166
- weight.x,
167
- );
168
- let rowHi = mix(
169
- THREE_R184_DFG_LUT[u32(hi.y) * THREE_R184_DFG_LUT_SIZE + u32(lo.x)],
170
- THREE_R184_DFG_LUT[u32(hi.y) * THREE_R184_DFG_LUT_SIZE + u32(hi.x)],
171
- weight.x,
172
- );
173
- return mix(rowLo, rowHi, weight.y);
174
- }
175
-
176
- fn _threeR184DirectMultiScatter(
177
- roughness : f32,
178
- nDotV : f32,
179
- nDotL : f32,
180
- F0 : vec3<f32>,
181
- ) -> vec3<f32> {
182
- let dfgV = _sampleThreeR184DfgLut(roughness, nDotV);
183
- let dfgL = _sampleThreeR184DfgLut(roughness, nDotL);
184
- let fssEssV = F0 * dfgV.x + vec3<f32>(dfgV.y);
185
- let fssEssL = F0 * dfgL.x + vec3<f32>(dfgL.y);
186
- let emsV = 1.0 - dfgV.x - dfgV.y;
187
- let emsL = 1.0 - dfgL.x - dfgL.y;
188
- let favg = F0 + (vec3<f32>(1.0) - F0) * 0.047619;
189
- let energyLoss = emsV * emsL;
190
- let fms = fssEssV * fssEssL * favg
191
- / (vec3<f32>(1.0) - energyLoss * favg * favg + vec3<f32>(1e-6));
192
- return fms * energyLoss;
193
- }
194
-
83
+ // Direct multiscatter is owned by forgeax_pbr::brdf. Keep directional
84
+ // lighting as a consumer of that shared function so its LUT and energy
85
+ // compensation cannot diverge from punctual and cluster lighting.
195
86
  // Pick the cascade layer for a positive view-space depth -- walks
196
87
  // splitPlanes in order, returns the first split the depth falls below. Last
197
88
  // layer (count - 1) catches everything beyond splits[count-2]. cascadeCount=1
@@ -522,10 +413,9 @@ fn evalDirectionalNoShadow(
522
413
  let nDotV = max(dot(normal, viewDir), 1e-5);
523
414
  let nDotH = max(dot(normal, h), 0.0);
524
415
  let vDotH = max(dot(viewDir, h), 0.0);
525
- let fresnel = exp2((-5.55473 * vDotH - 6.98316) * vDotH);
526
- let f = F0 * (vec3<f32>(1.0) - fresnel) + vec3<f32>(fresnel);
416
+ let f = f_schlick(vDotH, F0);
527
417
  let roughness = sqrt(max(alphaSq, 0.0));
528
- let multiScatter = _threeR184DirectMultiScatter(roughness, nDotV, nDotL, F0);
418
+ let multiScatter = threeR184DirectMultiScatter(roughness, nDotV, nDotL, F0);
529
419
  let specular = d_ggx(nDotH, alphaSq) * v_smith(nDotV, nDotL, alphaSq) * f + multiScatter;
530
420
  let diffuse = (1.0 - metallic) * baseColor / 3.14159265;
531
421
  return (diffuse + specular) * view.lightColor * nDotL;
@@ -0,0 +1,51 @@
1
+ // @forgeax/engine-shader - lighting-probe.wgsl
2
+ // Local diffuse LightProbe composition. The record is supplied by the
3
+ // renderer's per-object retained projection; Skylight remains the sole Sky
4
+ // and specular owner.
5
+
6
+ #define_import_path forgeax_pbr::lighting_probe
7
+
8
+ const PROBE_INV_PI : f32 = 0.3183098861837907;
9
+
10
+ fn probe_sh9(shPreblend : array<vec4<f32>, 9>, normal : vec3<f32>) -> vec3<f32> {
11
+ let x = normal.x;
12
+ let y = normal.y;
13
+ let z = normal.z;
14
+ let basis = array<f32, 9>(
15
+ 0.2820947918,
16
+ 0.4886025119 * y,
17
+ 0.4886025119 * z,
18
+ 0.4886025119 * x,
19
+ 1.0925484306 * x * y,
20
+ 1.0925484306 * y * z,
21
+ 0.3153915653 * (3.0 * z * z - 1.0),
22
+ 1.0925484306 * x * z,
23
+ 0.5462742153 * (x * x - y * y),
24
+ );
25
+ var result = vec3<f32>(0.0);
26
+ for (var band = 0u; band < 9u; band = band + 1u) {
27
+ result += shPreblend[band].xyz * basis[band];
28
+ }
29
+ return result;
30
+ }
31
+
32
+ // One SH9 evaluation, then the real Skylight residual. No Sky q, probe loop,
33
+ // hidden gain, smoothstep, or specular probe path is present here.
34
+ fn evaluateProbeDiffuse(
35
+ shPreblend : array<vec4<f32>, 9>,
36
+ localBlendFraction : f32,
37
+ normal : vec3<f32>,
38
+ e_sky : vec3<f32>,
39
+ k_d : vec3<f32>,
40
+ albedo : vec3<f32>,
41
+ metallic : f32,
42
+ ) -> vec3<f32> {
43
+ // `shPreblend` stores cosine-convolved irradiance. Match the existing
44
+ // Standard-PBR Skylight path and Three's BRDF_Lambert: irradiance becomes
45
+ // reflected diffuse radiance only after the single 1/pi factor. The Sky
46
+ // argument is already the Skylight path's E/pi * color * intensity value,
47
+ // so it must not be divided a second time below.
48
+ let local = max(probe_sh9(shPreblend, normal), vec3<f32>(0.0)) * PROBE_INV_PI;
49
+ let skyResidualFraction = 1.0 - localBlendFraction;
50
+ return (local + skyResidualFraction * e_sky) * k_d * albedo * (1.0 - metallic);
51
+ }
@@ -31,7 +31,11 @@
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}
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.
35
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)
@@ -102,9 +106,13 @@ fn evalPunctualBody(
102
106
  let nDotH = max(dot(normal, h), 0.0);
103
107
  let vDotH = max(dot(viewDir, h), 0.0);
104
108
  let f = f_schlick(vDotH, F0);
105
- let specular = d_ggx(nDotH, alphaSq) * v_smith(nDotV, nDotL, alphaSq) * f;
106
- let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);
107
- 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;
108
116
  let attenuation = evalDistanceAttenuation(dSquared, invRangeSquared);
109
117
  return (diffuse + specular) * colorTimesIntensity * nDotL * attenuation;
110
118
  }
@@ -128,14 +136,14 @@ fn evalPoint(
128
136
  );
129
137
  }
130
138
 
131
- // Flat 2D punctual evaluators share the range/cone math with the compatibility
132
- // path and the clustered path, but intentionally skip the 3D BRDF normal term.
139
+ // Flat 2D punctual evaluators share the range/cone math with the clustered
140
+ // path, but intentionally skip the 3D BRDF normal term.
133
141
  // Sprite-lit treats every quad as an omnidirectional receiver; keeping this
134
142
  // owner here prevents URP and Cluster from drifting when the light lies in the
135
143
  // sprite plane (the common 2D flashlight setup).
136
144
  fn evalFlatRangeAttenuation(dSquared : f32, invRangeSquared : f32) -> f32 {
137
145
  let factor = max(min(1.0 - (dSquared * invRangeSquared) * (dSquared * invRangeSquared), 1.0), 0.0);
138
- return factor / dSquared;
146
+ return factor * factor / dSquared;
139
147
  }
140
148
 
141
149
  fn evalPointFlat(
@@ -182,7 +190,8 @@ fn evalSpotFlat(
182
190
  // projection (research L0.5 Bevy pattern); the caller passes `near` / `far`
183
191
  // directly so both pipelines route the same constants without owning the
184
192
  // upstream binding (URP reads them from `shadowParams[layer]` at @group(0)
185
- // 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
186
195
  // plan-strategy §D-8).
187
196
  //
188
197
  // Caller responsibility: gate this on `shadowAtlasLayer >= 0` so the
@@ -258,9 +267,10 @@ fn evalSpot(
258
267
  lightPos, colorTimesIntensity, invRangeSquared,
259
268
  worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
260
269
  );
261
- return body * evalSpotAttenuation(
262
- lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
263
- ) / 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);
264
274
  }
265
275
 
266
276
  // feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-4 +