@forgeax/engine-shader 0.1.7 → 0.1.20

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 (117) hide show
  1. package/README.md +34 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/ShaderRegistry.d.ts.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.mjs +53 -2
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/material-schemas.d.ts.map +1 -1
  9. package/package.json +6 -3
  10. package/src/ShaderRegistry.ts +50 -0
  11. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +86 -12
  12. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +185 -0
  13. package/src/__tests__/fog-oracle.unit.test.ts +110 -114
  14. package/src/__tests__/fog-orthographic-ray.unit.test.ts +12 -37
  15. package/src/__tests__/fog-producer-matrix.integration.test.ts +16 -51
  16. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +48 -0
  17. package/src/__tests__/lighting-punctual.unit.test.ts +1 -1
  18. package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +26 -0
  19. package/src/__tests__/lighting-spot-volume.unit.test.ts +25 -0
  20. package/src/__tests__/material-builtins.unit.test.ts +38 -1
  21. package/src/__tests__/material-contract.unit.test.ts +123 -0
  22. package/src/__tests__/motion-blur.unit.test.ts +44 -0
  23. package/src/__tests__/scene-temporal.unit.test.ts +23 -5
  24. package/src/__tests__/shader-manifest-transmission.unit.test.ts +75 -0
  25. package/src/__tests__/shader.unit.test.ts +44 -3
  26. package/src/__tests__/sprite-variants.unit.test.ts +20 -0
  27. package/src/__tests__/taa-resolve.unit.test.ts +75 -3
  28. package/src/__tests__/transmission-thickness-scale.unit.test.ts +19 -0
  29. package/src/common.wgsl +34 -65
  30. package/src/default-standard-pbr-skin.wgsl +29 -28
  31. package/src/default-standard-pbr.wgsl +257 -57
  32. package/src/fxaa.wgsl +5 -6
  33. package/src/hdrp-cluster-forward.wgsl +41 -3
  34. package/src/hdrp-deferred-lighting.wgsl +3 -27
  35. package/src/index.ts +3 -0
  36. package/src/lighting-attenuation.wgsl +58 -0
  37. package/src/lighting-directional.wgsl +31 -3
  38. package/src/lighting-punctual.wgsl +57 -12
  39. package/src/material-schemas.ts +7 -0
  40. package/src/motion-blur.wgsl +77 -0
  41. package/src/msdf-text.wgsl +4 -23
  42. package/src/pbr-temporal.wgsl +15 -2
  43. package/src/scene-temporal.wgsl +54 -0
  44. package/src/shadow-pcf.wgsl +31 -10
  45. package/src/shadow_caster.wgsl +27 -2
  46. package/src/skybox.wgsl +2 -7
  47. package/src/sprite-lit.wgsl +7 -25
  48. package/src/sprite.wgsl +7 -25
  49. package/src/taa-resolve.wgsl +76 -81
  50. package/src/tonemap.wgsl +7 -9
  51. package/src/unlit.wgsl +7 -28
  52. package/src/volume/volume-composite.wgsl +113 -0
  53. package/src/volume/volume-inject.wgsl +249 -0
  54. package/src/volume/volume-integrate.wgsl +342 -0
  55. package/src/volume/volume-temporal.wgsl +70 -0
  56. package/dist/__tests__/atmosphere-background.test.d.ts +0 -2
  57. package/dist/__tests__/atmosphere-background.test.d.ts.map +0 -1
  58. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts +0 -2
  59. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts.map +0 -1
  60. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts +0 -2
  61. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts.map +0 -1
  62. package/dist/__tests__/atmosphere-preetham-red.test.d.ts +0 -2
  63. package/dist/__tests__/atmosphere-preetham-red.test.d.ts.map +0 -1
  64. package/dist/__tests__/atmosphere-preetham.test.d.ts +0 -2
  65. package/dist/__tests__/atmosphere-preetham.test.d.ts.map +0 -1
  66. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts +0 -2
  67. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts.map +0 -1
  68. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts +0 -2
  69. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts.map +0 -1
  70. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts +0 -2
  71. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts.map +0 -1
  72. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts +0 -2
  73. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts.map +0 -1
  74. package/dist/__tests__/deferred-lighting-ssao.test.d.ts +0 -2
  75. package/dist/__tests__/deferred-lighting-ssao.test.d.ts.map +0 -1
  76. package/dist/__tests__/fog-oracle.unit.test.d.ts +0 -2
  77. package/dist/__tests__/fog-oracle.unit.test.d.ts.map +0 -1
  78. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts +0 -2
  79. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts.map +0 -1
  80. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts +0 -2
  81. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts.map +0 -1
  82. package/dist/__tests__/hdrp-ssao-compile.test.d.ts +0 -2
  83. package/dist/__tests__/hdrp-ssao-compile.test.d.ts.map +0 -1
  84. package/dist/__tests__/ibl-irradiance.unit.test.d.ts +0 -2
  85. package/dist/__tests__/ibl-irradiance.unit.test.d.ts.map +0 -1
  86. package/dist/__tests__/ibl-sampling.unit.test.d.ts +0 -2
  87. package/dist/__tests__/ibl-sampling.unit.test.d.ts.map +0 -1
  88. package/dist/__tests__/lighting-punctual.unit.test.d.ts +0 -2
  89. package/dist/__tests__/lighting-punctual.unit.test.d.ts.map +0 -1
  90. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts +0 -2
  91. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts.map +0 -1
  92. package/dist/__tests__/material-builtins.unit.test.d.ts +0 -2
  93. package/dist/__tests__/material-builtins.unit.test.d.ts.map +0 -1
  94. package/dist/__tests__/material-contract.unit.test.d.ts +0 -2
  95. package/dist/__tests__/material-contract.unit.test.d.ts.map +0 -1
  96. package/dist/__tests__/material-derived-builtins.integration.test.d.ts +0 -2
  97. package/dist/__tests__/material-derived-builtins.integration.test.d.ts.map +0 -1
  98. package/dist/__tests__/points-lines-shader.unit.test.d.ts +0 -2
  99. package/dist/__tests__/points-lines-shader.unit.test.d.ts.map +0 -1
  100. package/dist/__tests__/public-surface.unit.test.d.ts +0 -2
  101. package/dist/__tests__/public-surface.unit.test.d.ts.map +0 -1
  102. package/dist/__tests__/scene-temporal.unit.test.d.ts +0 -2
  103. package/dist/__tests__/scene-temporal.unit.test.d.ts.map +0 -1
  104. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts +0 -2
  105. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts.map +0 -1
  106. package/dist/__tests__/shader.unit.test.d.ts +0 -2
  107. package/dist/__tests__/shader.unit.test.d.ts.map +0 -1
  108. package/dist/__tests__/sprite-lit-shader.test.d.ts +0 -2
  109. package/dist/__tests__/sprite-lit-shader.test.d.ts.map +0 -1
  110. package/dist/__tests__/sprite-variants.unit.test.d.ts +0 -2
  111. package/dist/__tests__/sprite-variants.unit.test.d.ts.map +0 -1
  112. package/dist/__tests__/taa-resolve.unit.test.d.ts +0 -2
  113. package/dist/__tests__/taa-resolve.unit.test.d.ts.map +0 -1
  114. package/dist/__tests__/transparent-pbr.unit.test.d.ts +0 -2
  115. package/dist/__tests__/transparent-pbr.unit.test.d.ts.map +0 -1
  116. package/dist/__tests__/vertex-color-variant.unit.test.d.ts +0 -2
  117. package/dist/__tests__/vertex-color-variant.unit.test.d.ts.map +0 -1
@@ -1,13 +1,12 @@
1
1
  #define_import_path forgeax_view::hdrp_deferred_lighting
2
2
 
3
3
  #import forgeax_view::common::FullscreenOutput
4
- #import forgeax_view::common::{View, FogViewParams, FogRay}
4
+ #import forgeax_view::common::View
5
5
  #import forgeax_view::common::fullscreen_triangle
6
- #import forgeax_view::fog::{apply_fog}
7
6
 
8
7
  // HDRP deferred lighting producer. The lighting pass consumes the material
9
8
  // G-buffer and current-frame depth, reconstructs the fragment world position,
10
- // then applies the shared analytic Fog exactly once to linear radiance.
9
+ // then writes linear radiance for the shared volume composite pass.
11
10
 
12
11
  @group(0) @binding(0) var<uniform> view : View;
13
12
 
@@ -24,28 +23,6 @@ fn vs_main(@builtin(vertex_index) vertex_index : u32) -> FullscreenOutput {
24
23
  return fullscreen_triangle(vertex_index);
25
24
  }
26
25
 
27
- fn reconstructWorldPosition(uv : vec2<f32>, depth : f32) -> vec3<f32> {
28
- let clip = vec4<f32>(uv * 2.0 - vec2<f32>(1.0), depth, 1.0);
29
- let worldH = view.inverseViewProj * clip;
30
- return worldH.xyz / worldH.w;
31
- }
32
-
33
- fn fogDeferredRadiance(color : vec4<f32>, worldPos : vec3<f32>) -> vec4<f32> {
34
- var origin = view.cameraPos;
35
- var direction = normalize(worldPos - origin);
36
- var rayDistance = length(worldPos - origin);
37
- if (view.temporalProjection.z >= 0.5) {
38
- let nearH = view.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
39
- let farH = view.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
40
- let nearPoint = nearH.xyz / nearH.w;
41
- let farPoint = farH.xyz / farH.w;
42
- direction = normalize(farPoint - nearPoint);
43
- origin = worldPos - direction * dot(worldPos - view.cameraPos, direction);
44
- rayDistance = max(dot(worldPos - origin, direction), 0.0);
45
- }
46
- return apply_fog(view.fog, FogRay(origin, direction, rayDistance), color);
47
- }
48
-
49
26
  @fragment
50
27
  fn fs_lighting(in : FullscreenOutput) -> @location(0) vec4<f32> {
51
28
  // The lighting pass runs before the forward lane and shares its cleared
@@ -64,6 +41,5 @@ fn fs_lighting(in : FullscreenOutput) -> @location(0) vec4<f32> {
64
41
  let light = max(dot(normal, normalize(-view.lightDir)), 0.0);
65
42
  let direct = material.rgb * view.lightColor * light * clamp(emissiveSample.a, 0.0, 1.0);
66
43
  let radiance = vec4<f32>(direct + emissiveSample.rgb, material.a);
67
- let worldPos = reconstructWorldPosition(in.uv, depth);
68
- return fogDeferredRadiance(radiance, worldPos);
44
+ return radiance;
69
45
  }
package/src/index.ts CHANGED
@@ -84,6 +84,9 @@ export const BUILTIN_MATERIAL_MODULES = {
84
84
  sprite: 'forgeax_material::sprite',
85
85
  } as const;
86
86
 
87
+ /** Stable build-time module id for the temporal-v1 accessor owner. */
88
+ export const SCENE_DATA_TEMPORAL_V1_SHADER_MODULE = 'forgeax_scene_temporal' as const;
89
+
87
90
  /**
88
91
  * Material modules whose shader source and parameter contract are owned by
89
92
  * the Engine. They live in the runtime ShaderRegistry, so a Pack containing
@@ -0,0 +1,58 @@
1
+ #define_import_path forgeax_pbr::lighting_attenuation
2
+
3
+ // Shared punctual light optics. Range is represented by the host-derived
4
+ // inverse squared range; the shader never reconstructs authoring units.
5
+ fn evalDistanceAttenuation(dSquared : f32, invRangeSquared : f32) -> f32 {
6
+ let safeDistance = max(dSquared, 1e-4);
7
+ let window = clamp(1.0 - (safeDistance * invRangeSquared) * (safeDistance * invRangeSquared), 0.0, 1.0);
8
+ return window * window / safeDistance;
9
+ }
10
+
11
+ fn evalSpotAttenuation(
12
+ lightPos : vec3<f32>,
13
+ lightDir : vec3<f32>,
14
+ worldPos : vec3<f32>,
15
+ cosInner : f32,
16
+ cosOuter : f32,
17
+ invRangeSquared : f32,
18
+ ) -> f32 {
19
+ let toLight = lightPos - worldPos;
20
+ let dSquared = dot(toLight, toLight);
21
+ let distance = evalDistanceAttenuation(dSquared, invRangeSquared);
22
+ let direction = normalize(select(vec3<f32>(0.0, 0.0, -1.0), toLight, dSquared > 1e-4));
23
+ let cone = smoothstep(cosOuter, cosInner, dot(direction, -normalize(lightDir)));
24
+ return distance * cone;
25
+ }
26
+
27
+ // Isolated volume radiance helpers: this module has no resource bindings, so
28
+ // compute stages can reuse the exact surface attenuation and cone facts.
29
+ fn evalVolumePointRadiance(
30
+ lightPos : vec3<f32>,
31
+ colorTimesIntensity : vec3<f32>,
32
+ invRangeSquared : f32,
33
+ worldPos : vec3<f32>,
34
+ ) -> vec3<f32> {
35
+ let toLight = lightPos - worldPos;
36
+ let dSquared = max(dot(toLight, toLight), 1e-4);
37
+ return colorTimesIntensity * evalDistanceAttenuation(dSquared, invRangeSquared);
38
+ }
39
+
40
+ fn evalVolumeSpotRadiance(
41
+ lightPos : vec3<f32>,
42
+ lightDir : vec3<f32>,
43
+ colorTimesIntensity : vec3<f32>,
44
+ cosInner : f32,
45
+ cosOuter : f32,
46
+ invRangeSquared : f32,
47
+ worldPos : vec3<f32>,
48
+ ) -> vec3<f32> {
49
+ return colorTimesIntensity * evalSpotAttenuation(
50
+ lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
51
+ );
52
+ }
53
+
54
+ fn projectSpotUv(lightViewProj : mat4x4<f32>, worldPos : vec3<f32>) -> vec2<f32> {
55
+ let clip = lightViewProj * vec4<f32>(worldPos, 1.0);
56
+ let invW = select(1.0 / clip.w, 0.0, abs(clip.w) < 1e-6);
57
+ return vec2<f32>(clip.x * invW * 0.5 + 0.5, clip.y * invW * -0.5 + 0.5);
58
+ }
@@ -238,6 +238,7 @@ fn _sampleShadowForCascade(
238
238
  count : u32,
239
239
  normal : vec3<f32>,
240
240
  l : vec3<f32>,
241
+ useNormalBias : bool,
241
242
  ) -> f32 {
242
243
  let lvp = _cascadeLightViewProj(layer);
243
244
  let lightClip = lvp * vec4<f32>(worldPos, 1.0);
@@ -253,7 +254,7 @@ fn _sampleShadowForCascade(
253
254
  // shadow fields carried in the View UBO -- normalBias scales the
254
255
  // (1 - N.L) slope term, depthBias is the constant floor. Replaces the prior
255
256
  // hardcoded max(0.05*(1-N.L), 0.005).
256
- let bias = max(view.normalBias * (1.0 - dot(normal, l)), view.depthBias);
257
+ let bias = select(view.depthBias, max(view.normalBias * (1.0 - dot(normal, l)), view.depthBias), useNormalBias);
257
258
  let adjustedDepth = currentDepth - bias;
258
259
  // NaN-safe bounds: relational < and > do not reject NaN (NaN < 0 is
259
260
  // false), so a zero / degenerate lightViewProj matrix that produces
@@ -414,7 +415,7 @@ fn evalDirectionalShadowFactor(
414
415
  return 1.0;
415
416
  }
416
417
  let layer = _pickCascadeLayer(viewDepth, count);
417
- let shadowCurr = _sampleShadowForCascade(worldPos, layer, count, normal, l);
418
+ let shadowCurr = _sampleShadowForCascade(worldPos, layer, count, normal, l, true);
418
419
 
419
420
  var shadow = shadowCurr;
420
421
  if (view.cascadeBlend > 0.0 && layer + 1u < count) {
@@ -424,7 +425,7 @@ fn evalDirectionalShadowFactor(
424
425
  let dist = spCurr - viewDepth;
425
426
  let t = clamp(1.0 - dist / blendWidth, 0.0, 1.0);
426
427
  if (t > 0.0) {
427
- let shadowNext = _sampleShadowForCascade(worldPos, layer + 1u, count, normal, l);
428
+ let shadowNext = _sampleShadowForCascade(worldPos, layer + 1u, count, normal, l, true);
428
429
  shadow = mix(shadowCurr, shadowNext, t);
429
430
  }
430
431
  }
@@ -432,6 +433,33 @@ fn evalDirectionalShadowFactor(
432
433
  return shadow;
433
434
  }
434
435
 
436
+ // Volume receivers are samples in a medium, not surfaces. They therefore use
437
+ // the same cascade selection, atlas placement, PCF, and shadow-distance SSOT
438
+ // but only the bounded constant receiver bias; a ray direction is never
439
+ // treated as a geometric normal for normalBias.
440
+ fn evalDirectionalVolumeShadowFactor(
441
+ worldPos : vec3<f32>,
442
+ viewDepth : f32,
443
+ ) -> f32 {
444
+ if (view.cascadeCount < 1.0 || viewDepth > view.splitPlanes[u32(max(view.cascadeCount, 1.0)) - 1u].x) {
445
+ return 1.0;
446
+ }
447
+ let count = u32(max(view.cascadeCount, 1.0));
448
+ let layer = _pickCascadeLayer(viewDepth, count);
449
+ let lightDirection = normalize(-view.lightDir);
450
+ let current = _sampleShadowForCascade(worldPos, layer, count, vec3<f32>(0.0), lightDirection, false);
451
+ if (view.cascadeBlend <= 0.0 || layer + 1u >= count) {
452
+ return current;
453
+ }
454
+ let blendWidth = view.splitPlanes[layer].x * view.cascadeBlend;
455
+ if (blendWidth <= 0.0) {
456
+ return current;
457
+ }
458
+ let blend = clamp(1.0 - (view.splitPlanes[layer].x - viewDepth) / blendWidth, 0.0, 1.0);
459
+ let next = _sampleShadowForCascade(worldPos, layer + 1u, count, vec3<f32>(0.0), lightDirection, false);
460
+ return mix(current, next, blend);
461
+ }
462
+
435
463
  // `evalDirectional` evaluates the GGX direct-lighting term for the single
436
464
  // directional light carried in `view.lightDir / view.lightColor`. CSM
437
465
  // pathway: pick cascade layer from viewZ + splitPlanes, sample the atlas
@@ -32,12 +32,13 @@
32
32
  // invRangeSquared, ...) -> vec3<f32>
33
33
 
34
34
  #import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
35
+ #import forgeax_pbr::lighting_attenuation::{evalDistanceAttenuation, evalSpotAttenuation}
35
36
  // feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-5):
36
37
  // spot shadow sampling reuses the shared 2D 9-tap PCF core (sample_shadow_2d)
37
38
  // and the always-on `spotShadowMap` (binding 8) + `shadowSampler` (binding 4).
38
39
  // `shadowSampler` is imported UNCONDITIONALLY here (spot is always-on, D-5):
39
40
  // the point-shadow #ifdef block below must NOT re-import it (double import).
40
- #import forgeax_pbr::shadow_pcf::{sample_shadow_2d}
41
+ #import forgeax_pbr::shadow_pcf::{sample_shadow_2d_kernel}
41
42
  #import forgeax_view::common::{spotShadowMap, shadowSampler}
42
43
  #ifdef POINT_SHADOW_AVAILABLE
43
44
  #import forgeax_pbr::shadow_pcf::{sample_shadow_cube_hw2x2}
@@ -48,6 +49,45 @@
48
49
  #import forgeax_view::common::{shadowAtlas}
49
50
  #endif
50
51
 
52
+ // Volume lighting uses the same host-derived range and cone facts as surface
53
+ // lighting. These wrappers intentionally return radiance factors only; the
54
+ // volume integrator owns density, phase, transmittance, and accumulation.
55
+ fn evalVolumePoint(
56
+ lightPos : vec3<f32>,
57
+ colorTimesIntensity : vec3<f32>,
58
+ invRangeSquared : f32,
59
+ worldPos : vec3<f32>,
60
+ ) -> vec3<f32> {
61
+ let toLight = lightPos - worldPos;
62
+ let dSquared = max(dot(toLight, toLight), 1e-4);
63
+ let safeDistance = max(dSquared, 1e-4);
64
+ return colorTimesIntensity * evalDistanceAttenuation(safeDistance, invRangeSquared);
65
+ }
66
+
67
+ fn evalVolumeSpot(
68
+ lightPos : vec3<f32>,
69
+ lightDir : vec3<f32>,
70
+ colorTimesIntensity : vec3<f32>,
71
+ cosInner : f32,
72
+ cosOuter : f32,
73
+ invRangeSquared : f32,
74
+ worldPos : vec3<f32>,
75
+ ) -> vec3<f32> {
76
+ return colorTimesIntensity * evalSpotAttenuation(
77
+ lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
78
+ );
79
+ }
80
+
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
+
51
91
  // Shared punctual BRDF body returning (diffuse + specular) *
52
92
  // colorTimesIntensity * nDotL * attenuation. Cone factor is applied by the
53
93
  // caller (evalSpot only).
@@ -75,8 +115,7 @@ fn evalPunctualBody(
75
115
  let specular = d_ggx(nDotH, alphaSq) * v_smith(nDotV, nDotL, alphaSq) * f;
76
116
  let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);
77
117
  let diffuse = kd * baseColor / 3.14159265;
78
- let factor = max(min(1.0 - (dSquared * invRangeSquared) * (dSquared * invRangeSquared), 1.0), 0.0);
79
- let attenuation = factor * factor / dSquared;
118
+ let attenuation = evalDistanceAttenuation(dSquared, invRangeSquared);
80
119
  return (diffuse + specular) * colorTimesIntensity * nDotL * attenuation;
81
120
  }
82
121
 
@@ -185,10 +224,9 @@ fn evalSpot(
185
224
  lightPos, colorTimesIntensity, invRangeSquared,
186
225
  worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
187
226
  );
188
- let toLight = lightPos - worldPos;
189
- let l = normalize(toLight);
190
- let cone = smoothstep(cosOuter, cosInner, dot(l, -lightDir));
191
- return body * cone;
227
+ return body * evalSpotAttenuation(
228
+ lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
229
+ ) / max(evalDistanceAttenuation(dot(lightPos - worldPos, lightPos - worldPos), invRangeSquared), 1e-4);
192
230
  }
193
231
 
194
232
  // feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-4 +
@@ -234,6 +272,8 @@ fn evalSpotShadowed(
234
272
  shadowAtlasTile : i32,
235
273
  depthBias : f32,
236
274
  normalBias : f32,
275
+ pcfKernelSize : f32,
276
+ shadowIntensity : f32,
237
277
  ) -> vec3<f32> {
238
278
  let body = evalSpot(
239
279
  lightPos, lightDir, colorTimesIntensity, cosInner, cosOuter, invRangeSquared,
@@ -245,10 +285,11 @@ fn evalSpotShadowed(
245
285
  // Perspective divide; guard a zero/near-zero w (fragment behind the light or
246
286
  // a degenerate matrix) so the OOB gate below catches it as fully lit.
247
287
  let invW = select(1.0 / splane.w, 0.0, abs(splane.w) < 1e-6);
248
- let ndcXY = splane.xy * invW;
288
+ let clipUv = projectSpotUv(lightViewProj, worldPos);
249
289
  let depthRef = splane.z * invW;
250
- // Clip-space [-1,1] -> texture UV [0,1] with the standard Y flip.
251
- let clipUv = vec2<f32>(ndcXY.x * 0.5 + 0.5, ndcXY.y * -0.5 + 0.5);
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;
252
293
 
253
294
  // OOB / NaN gate: outside the light frustum (or NaN from a degenerate matrix)
254
295
  // returns fully lit. Mirrors the directional `>= 0 && <= 1` NaN-safe form.
@@ -267,8 +308,12 @@ fn evalSpotShadowed(
267
308
  let texel = vec2<f32>(1.0, 1.0) / atlasDims;
268
309
 
269
310
  let nDotL = max(dot(normal, normalize(lightPos - worldPos)), 0.0);
270
- let shadowFactor = sample_shadow_2d(
311
+ let shadowFactor = sample_shadow_2d_kernel(
271
312
  spotShadowMap, shadowSampler, atlasUv, texel, depthRef, normalBias, depthBias, nDotL,
313
+ pcfKernelSize,
272
314
  );
273
- return body * shadowFactor;
315
+ // Three's SpotLight.shadow.intensity is the opacity of the shadow, not a
316
+ // multiplier on the light's candela radiance. A value of 1 keeps the
317
+ // sampled visibility fully opaque; 0 leaves the unshadowed body intact.
318
+ return body * mix(1.0, shadowFactor, clamp(shadowIntensity, 0.0, 1.0));
274
319
  }
@@ -19,12 +19,19 @@ export const DEFAULT_STANDARD_PBR_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
19
19
  { name: 'clearcoatRoughness', type: 'f32', default: 0.5 },
20
20
  { name: 'specularTint', type: 'vec3', colorSpace: 'srgb', default: [1, 1, 1] },
21
21
  { name: 'normalScale', type: 'f32', default: 1 },
22
+ { name: 'transmission', type: 'f32', default: 0 },
23
+ { name: 'ior', type: 'f32', default: 1.5 },
24
+ { name: 'thickness', type: 'f32', default: 0 },
25
+ { name: 'attenuationColor', type: 'vec3', colorSpace: 'linear', default: [1, 1, 1] },
26
+ { name: 'attenuationDistance', type: 'f32' },
22
27
  { name: 'baseColorTexture', type: 'texture2d' },
23
28
  { name: 'metallicRoughnessTexture', type: 'texture2d' },
24
29
  { name: 'normalTexture', type: 'texture2d' },
25
30
  { name: 'specularTintTexture', type: 'texture2d' },
26
31
  { name: 'emissiveTexture', type: 'texture2d' },
27
32
  { name: 'occlusionTexture', type: 'texture2d' },
33
+ { name: 'transmissionTexture', type: 'texture2d' },
34
+ { name: 'thicknessTexture', type: 'texture2d' },
28
35
  ];
29
36
 
30
37
  export const DEFAULT_UNLIT_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
@@ -0,0 +1,77 @@
1
+ #define_import_path forgeax_view::motion_blur
2
+
3
+ #import forgeax_view::common::FullscreenOutput
4
+ #import forgeax_view::common::fullscreen_triangle
5
+ #import forgeax_scene_temporal::{unpackSceneTemporalV1}
6
+
7
+ struct MotionBlurParams {
8
+ shutterAngle : f32,
9
+ maxRadiusPixels : f32,
10
+ sampleCount : u32,
11
+ reset : u32,
12
+ };
13
+
14
+ @group(1) @binding(0) var currentColor : texture_2d<f32>;
15
+ @group(1) @binding(1) var linearSampler : sampler;
16
+ @group(1) @binding(2) var<uniform> params : MotionBlurParams;
17
+ @group(1) @binding(3) var sceneTemporal : texture_2d<f32>;
18
+
19
+ fn depthReject(center : f32, sample : f32) -> bool {
20
+ return abs(center - sample) > max(0.01, center * 0.01);
21
+ }
22
+
23
+ fn symmetricOffset(index : u32, count : u32, motion : vec2<f32>) -> vec2<f32> {
24
+ let denominator = max(f32(count), 1.0);
25
+ let signedIndex = f32(index) - (denominator - 1.0) * 0.5;
26
+ return motion * (signedIndex / denominator);
27
+ }
28
+
29
+ @vertex
30
+ fn vs_main(@builtin(vertex_index) vertexIndex : u32) -> FullscreenOutput {
31
+ return fullscreen_triangle(vertexIndex);
32
+ }
33
+
34
+ @fragment
35
+ fn fs_main(in : FullscreenOutput) -> @location(0) vec4<f32> {
36
+ let current = textureSampleLevel(currentColor, linearSampler, in.uv, 0.0);
37
+ let packed = textureSampleLevel(sceneTemporal, linearSampler, in.uv, 0.0);
38
+ let temporal = unpackSceneTemporalV1(packed);
39
+ let dimensions = vec2<f32>(textureDimensions(currentColor));
40
+ let pixelMotion = temporal.motionUv * dimensions;
41
+ let shutter = clamp(params.shutterAngle / 360.0, 0.0, 1.0);
42
+ let radius = min(length(pixelMotion) * shutter, params.maxRadiusPixels);
43
+ let motion = normalize(select(vec2<f32>(0.0), pixelMotion, radius > 1e-5));
44
+ let invalidDepth = !temporal.validDepth;
45
+ let reactive = clamp(temporal.reactive, 0.0, 1.0);
46
+ let subpixel = radius <= 1e-5;
47
+ let reset = params.reset != 0u;
48
+ var output = current;
49
+ output.a = current.a;
50
+ if (invalidDepth || subpixel || reset || params.sampleCount < 4u) {
51
+ return output;
52
+ }
53
+
54
+ var accum = vec3<f32>(0.0);
55
+ var weight = 0.0;
56
+ let temporalDimensions = vec2<i32>(textureDimensions(sceneTemporal));
57
+ for (var index = 0u; index < 16u; index += 1u) {
58
+ if (index >= params.sampleCount) { break; }
59
+ let offset = symmetricOffset(index, params.sampleCount, motion * radius) / dimensions;
60
+ let sampleUv = clamp(in.uv + offset, vec2<f32>(0.0), vec2<f32>(1.0));
61
+ let samplePixel = clamp(
62
+ vec2<i32>(sampleUv * dimensions),
63
+ vec2<i32>(0),
64
+ temporalDimensions - vec2<i32>(1),
65
+ );
66
+ let sampleTemporal = unpackSceneTemporalV1(textureLoad(sceneTemporal, samplePixel, 0));
67
+ if (sampleTemporal.validDepth && !depthReject(temporal.viewDepth, sampleTemporal.viewDepth)) {
68
+ accum += textureSampleLevel(currentColor, linearSampler, sampleUv, 0.0).rgb;
69
+ weight += 1.0;
70
+ }
71
+ }
72
+ let gathered = accum / max(weight, 1.0);
73
+ let blurWeight = (1.0 - reactive) * select(0.0, 1.0, weight > 0.0);
74
+ output = vec4<f32>(mix(current.rgb, gathered, blurWeight), current.a);
75
+ output.a = current.a;
76
+ return output;
77
+ }
@@ -1,7 +1,6 @@
1
1
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
2
2
 
3
- #import forgeax_view::common::{View, FogViewParams, FogRay, Mesh, view, meshes, sampleMaterialTexture}
4
- #import forgeax_view::fog::{apply_fog}
3
+ #import forgeax_view::common::{View, Mesh, view, meshes, sampleMaterialTexture}
5
4
 
6
5
  // @forgeax/engine-shader - msdf-text.wgsl
7
6
  // (feat-20260531-world-space-msdf-text-rendering M5 / w20).
@@ -146,22 +145,6 @@ fn screen_px_range(uv : vec2<f32>) -> f32 {
146
145
  return max(0.5 * dot(unit_range, screen_tex_size), 1.0);
147
146
  }
148
147
 
149
- fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
150
- var origin = viewParams.cameraPos;
151
- var direction = normalize(worldPos - origin);
152
- var rayDistance = length(worldPos - origin);
153
- if (viewParams.temporalProjection.z >= 0.5) {
154
- let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
155
- let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
156
- let nearPoint = nearH.xyz / nearH.w;
157
- let farPoint = farH.xyz / farH.w;
158
- direction = normalize(farPoint - nearPoint);
159
- origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
160
- rayDistance = max(dot(worldPos - origin, direction), 0.0);
161
- }
162
- return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
163
- }
164
-
165
148
  // fs_main_hdr: outputs linear premultiplied alpha for the rgba16float
166
149
  // offscreen target (D-7 / R-7). The tonemap fullscreen pass handles sRGB
167
150
  // encoding; writing hdrColor lets the bloom bright-pass catch the text.
@@ -176,8 +159,7 @@ fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
176
159
  let alpha = clamp(dist + 0.5, 0.0, 1.0) * material.tintColor.a;
177
160
  // Premultiplied output: rgb already multiplied by alpha for srcFactor=ONE /
178
161
  // dstFactor=ONE_MINUS_SRC_ALPHA (wiki section 6 SSOT).
179
- let fogged = applySceneFog(view, material.tintColor.rgb, alpha, in.worldPos);
180
- return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
162
+ return vec4<f32>(material.tintColor.rgb * alpha, alpha);
181
163
  }
182
164
 
183
165
  // linear_to_srgb: per-channel IEC 61966-2-1 transfer function for the LDR
@@ -197,12 +179,11 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
197
179
  let sd = median(msd.r, msd.g, msd.b);
198
180
  let dist = (sd - 0.5) * screen_px_range(in.uv);
199
181
  let alpha = clamp(dist + 0.5, 0.0, 1.0) * material.tintColor.a;
200
- let fogged = applySceneFog(view, material.tintColor.rgb, alpha, in.worldPos);
201
- let premult = fogged.rgb * fogged.a;
182
+ let premult = material.tintColor.rgb * alpha;
202
183
  return vec4<f32>(
203
184
  linear_to_srgb(premult.r),
204
185
  linear_to_srgb(premult.g),
205
186
  linear_to_srgb(premult.b),
206
- fogged.a,
187
+ alpha,
207
188
  );
208
189
  }
@@ -1,6 +1,7 @@
1
1
  #define_import_path forgeax_pbr::temporal
2
2
 
3
- #import forgeax_view::common::{sampleMaterialTexture, packSceneTemporal}
3
+ #import forgeax_view::common::{sampleMaterialTexture}
4
+ #import forgeax_scene_temporal::{packSceneTemporalV1}
4
5
 
5
6
  fn transformedPbrTemporalUv(
6
7
  transform : vec4<f32>,
@@ -29,6 +30,16 @@ fn transformedPbrTemporalUv(
29
30
  return vec2<f32>(scaled.x * c - scaled.y * s, scaled.x * s + scaled.y * c) + transform.xy;
30
31
  }
31
32
 
33
+ fn resolvePbrTemporalReactive(
34
+ reactive : f32,
35
+ baseColorAlpha : f32,
36
+ sampledAlpha : f32,
37
+ ) -> f32 {
38
+ let coverage = clamp(baseColorAlpha * sampledAlpha, 0.0, 1.0);
39
+ let coverageReactive = 1.0 - coverage;
40
+ return max(clamp(reactive, 0.0, 1.0), coverageReactive);
41
+ }
42
+
32
43
  fn projectPbrSceneTemporal(
33
44
  baseColorAlpha : f32,
34
45
  alphaCutoff : f32,
@@ -38,6 +49,7 @@ fn projectPbrSceneTemporal(
38
49
  metadata : vec4<f32>,
39
50
  currentClip : vec4<f32>,
40
51
  previousClip : vec4<f32>,
52
+ temporalProjection : vec4<f32>,
41
53
  reactive : f32,
42
54
  uv0 : vec2<f32>,
43
55
  uv1 : vec2<f32>,
@@ -69,5 +81,6 @@ fn projectPbrSceneTemporal(
69
81
  if (alphaCutoff > 0.0 && baseColorAlpha * baseSample.a <= alphaCutoff) {
70
82
  discard;
71
83
  }
72
- return packSceneTemporal(currentClip, previousClip, reactive);
84
+ let reactiveCoverage = resolvePbrTemporalReactive(reactive, baseColorAlpha, baseSample.a);
85
+ return packSceneTemporalV1(currentClip, previousClip, temporalProjection, reactiveCoverage);
73
86
  }
@@ -0,0 +1,54 @@
1
+ #define_import_path forgeax_scene_temporal
2
+
3
+ // The sole shader interpretation of forgeax::scene-data::temporal-v1.
4
+ // The packed target is rgba16float at render resolution and is sampled only.
5
+
6
+ const SCENE_DATA_TEMPORAL_V1_CLEAR : vec4<f32> = vec4<f32>(0.0, 0.0, -1.0, 1.0);
7
+
8
+ struct SceneTemporalV1 {
9
+ motionUv : vec2<f32>,
10
+ viewDepth : f32,
11
+ reactive : f32,
12
+ validDepth : bool,
13
+ };
14
+
15
+ fn unpackSceneTemporalV1(packed : vec4<f32>) -> SceneTemporalV1 {
16
+ let invalidDepth = packed.z < 0.0;
17
+ let validDepth = !invalidDepth;
18
+ let viewDepth = select(0.0, exp2(packed.z) - 1.0, validDepth);
19
+ let motionUv = packed.xy;
20
+ let reactive = clamp(packed.w, 0.0, 1.0);
21
+ return SceneTemporalV1(motionUv, viewDepth, reactive, validDepth);
22
+ }
23
+
24
+ fn sceneTemporalUv(clip : vec4<f32>) -> vec2<f32> {
25
+ let safeW = select(1e-6, clip.w, abs(clip.w) >= 1e-6);
26
+ let ndc = clip.xy / safeW;
27
+ return vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
28
+ }
29
+
30
+ fn sceneTemporalViewDepth(clip : vec4<f32>, temporalProjection : vec4<f32>) -> f32 {
31
+ let perspectiveDepth = max(clip.w, 0.0);
32
+ let ndcDepth = clip.z / max(abs(clip.w), 1e-6);
33
+ let orthographicDepth = temporalProjection.x +
34
+ ndcDepth * (temporalProjection.y - temporalProjection.x);
35
+ let viewDepth = select(
36
+ perspectiveDepth,
37
+ max(orthographicDepth, 0.0),
38
+ temporalProjection.z >= 0.5,
39
+ );
40
+ return log2(1.0 + viewDepth);
41
+ }
42
+
43
+ fn packSceneTemporalV1(
44
+ currentClip : vec4<f32>,
45
+ previousClip : vec4<f32>,
46
+ temporalProjection : vec4<f32>,
47
+ reactive : f32,
48
+ ) -> vec4<f32> {
49
+ return vec4<f32>(
50
+ sceneTemporalUv(currentClip) - sceneTemporalUv(previousClip),
51
+ sceneTemporalViewDepth(currentClip, temporalProjection),
52
+ clamp(reactive, 0.0, 1.0),
53
+ );
54
+ }
@@ -49,6 +49,22 @@ fn sample_shadow_2d(
49
49
  normalBias : f32,
50
50
  depthBias : f32,
51
51
  nDotL : f32,
52
+ ) -> f32 {
53
+ return sample_shadow_2d_kernel(
54
+ shadowMap, shadowSampler, uv, texel, depthRef, normalBias, depthBias, nDotL, 3.0,
55
+ );
56
+ }
57
+
58
+ fn sample_shadow_2d_kernel(
59
+ shadowMap : texture_depth_2d,
60
+ shadowSampler: sampler_comparison,
61
+ uv : vec2<f32>,
62
+ texel : vec2<f32>,
63
+ depthRef : f32,
64
+ normalBias : f32,
65
+ depthBias : f32,
66
+ nDotL : f32,
67
+ kernelSize : f32,
52
68
  ) -> f32 {
53
69
  // feat-20260621-merge-directionallightshadow-into-directionallight M3 / m3-t5:
54
70
  // param names aligned to the D-1 directional convention -- the slope
@@ -60,17 +76,22 @@ fn sample_shadow_2d(
60
76
  let bias = max(normalBias * (1.0 - nDotL), depthBias / 1000.0);
61
77
  let adjustedDepth = depthRef - bias;
62
78
 
63
- // 9-tap 3x3 PCF kernel. textureSampleCompareLevel returns 1.0 when
64
- // adjustedDepth < sampledDepth (sampler compare op = 'less'), 0.0 otherwise.
65
- // Sampler uses clamp-to-edge wrap so OOB texels return the nearest border value.
79
+ let halfWidth = clamp((i32(round(kernelSize)) - 1) / 2, 0, 2);
80
+ // Variable odd-kernel PCF. textureSampleCompareLevel returns 1.0 when
81
+ // adjustedDepth < sampledDepth; sampler clamp-to-edge handles OOB texels.
66
82
  var blocked = 0.0;
67
- for (var i = 0u; i < 9u; i++) {
68
- let off = PCF_OFFSETS[i];
69
- let offsetUv = uv + vec2<f32>(f32(off.x), f32(off.y)) * texel;
70
- let lit = textureSampleCompareLevel(shadowMap, shadowSampler, offsetUv, adjustedDepth);
71
- blocked = blocked + (1.0 - lit);
83
+ var samples = 0.0;
84
+ for (var y = -2; y <= 2; y = y + 1) {
85
+ for (var x = -2; x <= 2; x = x + 1) {
86
+ if (abs(x) <= halfWidth && abs(y) <= halfWidth) {
87
+ let offsetUv = uv + vec2<f32>(f32(x), f32(y)) * texel;
88
+ let lit = textureSampleCompareLevel(shadowMap, shadowSampler, offsetUv, adjustedDepth);
89
+ blocked = blocked + (1.0 - lit);
90
+ samples = samples + 1.0;
91
+ }
92
+ }
72
93
  }
73
- return 1.0 - blocked / 9.0;
94
+ return 1.0 - blocked / max(samples, 1.0);
74
95
  }
75
96
 
76
97
  // === Cube point-light-shadow wrapper =========================================
@@ -117,4 +138,4 @@ fn sample_shadow_cube_hw2x2(
117
138
  // handles face-boundary texel fetches. Returns 1.0 = fully lit, 0.0 = fully
118
139
  // shadowed.
119
140
  return textureSampleCompareLevel(shadowAtlas, shadowSampler, lightLocal, layer, adjustedDepth);
120
- }
141
+ }