@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,4 +1,5 @@
1
1
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
2
+ #pragma variant_axis SKINNING_DISABLED
2
3
 
3
4
  // @forgeax/engine-shader shadow_caster.wgsl
4
5
  // feat-20260520-directional-light-shadow-mapping M1c / w9 (D-9 / AC-09):
@@ -20,15 +21,28 @@
20
21
  // @group(2) binding(0) meshes : array<Mesh> -- common.wgsl
21
22
  // @group(3) binding(0) instances : array<InstanceData> -- common.wgsl
22
23
  //
23
- // Vertex layout: matches the 12F procedural layout of every mesh
24
- // (position vec3, normal vec3, uv vec2, tangent vec4; 48 B stride).
24
+ // SKINNING_DISABLED=true consumes the ordinary 12F mesh layout; the explicit
25
+ // false variant consumes JOINTS_0 / WEIGHTS_0 from the same 18F layout as pbr-skin and
26
+ // projects the animated world-space palette result into the shadow view.
25
27
 
26
28
  #import forgeax_view::common::{View, Mesh, InstanceData, ShadowCasterCascade, view, shadowCasterCascade, meshes, instances}
27
29
 
28
30
  struct VsInput {
29
31
  @location(0) position : vec3<f32>,
32
+ #if SKINNING_DISABLED == false
33
+ @location(4) skinIndex : vec4<u32>,
34
+ @location(5) skinWeight : vec4<f32>,
35
+ #endif
30
36
  };
31
37
 
38
+ #if SKINNING_DISABLED == false
39
+ #if STORAGE_BUFFER_AVAILABLE == true
40
+ @group(2) @binding(1) var<storage, read> palette : array<mat4x4<f32>>;
41
+ #else
42
+ @group(2) @binding(1) var<uniform> palette : array<mat4x4<f32>, 255>;
43
+ #endif
44
+ #endif
45
+
32
46
  fn _cascadeLightViewProj(layer : u32) -> mat4x4<f32> {
33
47
  switch (layer) {
34
48
  case 0u: { return view.lightViewProj_A; }
@@ -40,7 +54,18 @@ fn _cascadeLightViewProj(layer : u32) -> mat4x4<f32> {
40
54
 
41
55
  @vertex
42
56
  fn vs_main(in : VsInput, @builtin(instance_index) idx : u32) -> @builtin(position) vec4<f32> {
57
+ #if SKINNING_DISABLED == false
58
+ let skinMatrix = palette[in.skinIndex.x] * in.skinWeight.x +
59
+ palette[in.skinIndex.y] * in.skinWeight.y +
60
+ palette[in.skinIndex.z] * in.skinWeight.z +
61
+ palette[in.skinIndex.w] * in.skinWeight.w;
62
+ // Palette entries are jointWorld * inverseBind and therefore already
63
+ // produce world-space positions. Applying meshes[0].worldFromLocal again
64
+ // would double-transform a parented skinned entity.
65
+ let worldPos = skinMatrix * vec4<f32>(in.position, 1.0);
66
+ #else
43
67
  let worldPos = meshes[0].worldFromLocal * instances[idx].localFromInstance * vec4<f32>(in.position, 1.0);
68
+ #endif
44
69
  // feat-20260625-spot-light-shadow-mapping M2 / w10 (D-1): spot shadow passes
45
70
  // set `isSpot = 1u` and write their perspective matrix into
46
71
  // `spotLightViewProj`; directional cascade passes keep `isSpot = 0u` and read
package/src/skybox.wgsl CHANGED
@@ -23,9 +23,8 @@
23
23
  // @binding(3) rotation : SkyboxRotation (UBO)
24
24
 
25
25
  #import forgeax_view::common::FullscreenOutput
26
- #import forgeax_view::common::{View, FogViewParams, FogRay}
26
+ #import forgeax_view::common::View
27
27
  #import forgeax_view::common::fullscreen_triangle
28
- #import forgeax_view::fog::{apply_fog}
29
28
  #import forgeax_pbr::ibl_shared::{inverseRotateEnvironment}
30
29
 
31
30
  @group(0) @binding(0) var cubemap : texture_cube<f32>;
@@ -67,9 +66,5 @@ fn skyboxDirection(uv : vec2<f32>) -> vec3<f32> {
67
66
  fn skybox_fs(in : FullscreenOutput) -> @location(0) vec4<f32> {
68
67
  let dir = skyboxDirection(in.uv);
69
68
  let color = textureSample(cubemap, cubemapSampler, dir).rgb;
70
- return apply_fog(
71
- view.fog,
72
- FogRay(view.cameraPos, dir, max(view.temporalProjection.y, 0.0)),
73
- vec4<f32>(color, 1.0),
74
- );
69
+ return vec4<f32>(color, 1.0);
75
70
  }
@@ -1,8 +1,8 @@
1
1
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
2
2
  #define_import_path forgeax_material::sprite-lit
3
3
 
4
- #import forgeax_view::common::{View, FogViewParams, FogRay, Mesh, InstanceData, PointLight, SpotLight, view, meshes, instances, pointLightsBuffer, spotLightsBuffer, sampleMaterialTexture, packSceneTemporal}
5
- #import forgeax_view::fog::{apply_fog}
4
+ #import forgeax_view::common::{View, Mesh, InstanceData, PointLight, SpotLight, view, meshes, instances, pointLightsBuffer, spotLightsBuffer, sampleMaterialTexture}
5
+ #import forgeax_scene_temporal::{packSceneTemporalV1}
6
6
 
7
7
  // @forgeax/engine-shader - sprite-lit.wgsl
8
8
  // (tweak-20260701-sprite-lit-flat-default-drop-ndotl-for-2d).
@@ -221,22 +221,6 @@ fn spriteLitShadeAccum(albedo : vec3<f32>, worldPos : vec3<f32>) -> vec3<f32> {
221
221
  return lit;
222
222
  }
223
223
 
224
- fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
225
- var origin = viewParams.cameraPos;
226
- var direction = normalize(worldPos - origin);
227
- var rayDistance = length(worldPos - origin);
228
- if (viewParams.temporalProjection.z >= 0.5) {
229
- let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
230
- let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
231
- let nearPoint = nearH.xyz / nearH.w;
232
- let farPoint = farH.xyz / farH.w;
233
- direction = normalize(farPoint - nearPoint);
234
- origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
235
- rayDistance = max(dot(worldPos - origin, direction), 0.0);
236
- }
237
- return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
238
- }
239
-
240
224
  @fragment
241
225
  fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
242
226
  let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
@@ -245,8 +229,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
245
229
  // Strict clamp 0..1 before premultiplied alpha multiply keeps the LDR
246
230
  // path bounded even when a scene stacks many lights.
247
231
  let lit_rgba = clamp(vec4<f32>(lit, albedo4.a), vec4<f32>(0.0), vec4<f32>(1.0));
248
- let fogged = applySceneFog(view, lit_rgba.rgb, lit_rgba.a, in.worldPos);
249
- let premult = vec4<f32>(fogged.rgb * fogged.a, fogged.a);
232
+ let premult = vec4<f32>(lit_rgba.rgb * lit_rgba.a, lit_rgba.a);
250
233
  // LDR target is bgra8unorm; encode rgb via the sRGB transfer in-shader.
251
234
  return vec4<f32>(
252
235
  linear_to_srgb(premult.r),
@@ -265,15 +248,14 @@ fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
265
248
  // pass absorb HDR values > 1. Alpha stays clamped because the premult
266
249
  // math requires alpha in [0, 1].
267
250
  let alpha = clamp(albedo4.a, 0.0, 1.0);
268
- let fogged = applySceneFog(view, lit, alpha, in.worldPos);
269
- return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
251
+ return vec4<f32>(lit * alpha, alpha);
270
252
  }
271
253
 
272
254
  struct TemporalVsOut {
273
255
  @builtin(position) clip : vec4<f32>,
274
256
  @location(0) uvAtlas : vec2<f32>,
275
- @location(1) @interpolate(linear) currentClip : vec4<f32>,
276
- @location(2) @interpolate(linear) previousClip : vec4<f32>,
257
+ @location(1) @interpolate(perspective) currentClip : vec4<f32>,
258
+ @location(2) @interpolate(perspective) previousClip : vec4<f32>,
277
259
  };
278
260
 
279
261
  @vertex
@@ -301,7 +283,7 @@ fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
301
283
  if (alpha <= 0.0) {
302
284
  discard;
303
285
  }
304
- return packSceneTemporal(in.currentClip, in.previousClip, 1.0);
286
+ return packSceneTemporalV1(in.currentClip, in.previousClip, view.temporalProjection, 1.0);
305
287
  }
306
288
  // sprite-lit needs at least one light in the scene to be visible; for unlit
307
289
  // sprites use forgeax::sprite (1 string change in MaterialAsset).
package/src/sprite.wgsl CHANGED
@@ -2,8 +2,8 @@
2
2
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
3
3
  #pragma variant_axis PER_INSTANCE_REGION
4
4
 
5
- #import forgeax_view::common::{View, FogViewParams, FogRay, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture, packSceneTemporal}
6
- #import forgeax_view::fog::{apply_fog}
5
+ #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture}
6
+ #import forgeax_scene_temporal::{packSceneTemporalV1}
7
7
 
8
8
  // @forgeax/engine-shader - sprite.wgsl (feat-20260520-2d-sprite-layer-mvp /
9
9
  // M-3 / w19). 2D sprite material — third variant of the MaterialAsset
@@ -270,22 +270,6 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index)
270
270
  return out;
271
271
  }
272
272
 
273
- fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
274
- var origin = viewParams.cameraPos;
275
- var direction = normalize(worldPos - origin);
276
- var rayDistance = length(worldPos - origin);
277
- if (viewParams.temporalProjection.z >= 0.5) {
278
- let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
279
- let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
280
- let nearPoint = nearH.xyz / nearH.w;
281
- let farPoint = farH.xyz / farH.w;
282
- direction = normalize(farPoint - nearPoint);
283
- origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
284
- rayDistance = max(dot(worldPos - origin, direction), 0.0);
285
- }
286
- return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
287
- }
288
-
289
273
  // linear_to_srgb: per-channel IEC 61966-2-1 transfer function used by the
290
274
  // LDR fragment entry to encode linear premultiplied RGB into the bgra8unorm
291
275
  // swap-chain storage. The bgra8unorm target is not hardware-sRGB-encoded
@@ -303,8 +287,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
303
287
  let rgba = clamp(texel * material.colorTint, vec4<f32>(0.0), vec4<f32>(1.0));
304
288
  // Premultiplied alpha: rgb pre-multiplied by alpha for srcFactor='one' /
305
289
  // dstFactor='one-minus-src-alpha' blend.
306
- let fogged = applySceneFog(view, rgba.rgb, rgba.a, in.worldPos);
307
- let premult = vec4<f32>(fogged.rgb * fogged.a, fogged.a);
290
+ let premult = vec4<f32>(rgba.rgb * rgba.a, rgba.a);
308
291
  // LDR target is bgra8unorm (blendable per WebGPU spec); hardware does not
309
292
  // sRGB-encode bgra8unorm, so apply the transfer function in the shader.
310
293
  // Only RGB channels are encoded; alpha is linear throughout the blend.
@@ -323,15 +306,14 @@ fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
323
306
  let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
324
307
  // R6 mitigation: strict clamp 0..1 before premultiplied alpha multiply.
325
308
  let rgba = clamp(texel * material.colorTint, vec4<f32>(0.0), vec4<f32>(1.0));
326
- let fogged = applySceneFog(view, rgba.rgb, rgba.a, in.worldPos);
327
- return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
309
+ return vec4<f32>(rgba.rgb * rgba.a, rgba.a);
328
310
  }
329
311
 
330
312
  struct TemporalVsOut {
331
313
  @builtin(position) clip : vec4<f32>,
332
314
  @location(0) uvAtlas : vec2<f32>,
333
- @location(1) @interpolate(linear) currentClip : vec4<f32>,
334
- @location(2) @interpolate(linear) previousClip : vec4<f32>,
315
+ @location(1) @interpolate(perspective) currentClip : vec4<f32>,
316
+ @location(2) @interpolate(perspective) previousClip : vec4<f32>,
335
317
  };
336
318
 
337
319
  @vertex
@@ -359,7 +341,7 @@ fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
359
341
  if (alpha <= 0.0) {
360
342
  discard;
361
343
  }
362
- return packSceneTemporal(in.currentClip, in.previousClip, 1.0);
344
+ return packSceneTemporalV1(in.currentClip, in.previousClip, view.temporalProjection, 1.0);
363
345
  }
364
346
  // sprite is unlit; do NOT read the directional-light fields of the View
365
347
  // UBO -- same SSOT hard rule as the header comment above; R5 mitigation.
@@ -1,7 +1,28 @@
1
1
  #define_import_path forgeax_view::taa_resolve
2
2
 
3
- #import forgeax_view::common::fullscreen_triangle
4
- #import forgeax_view::common::FullscreenOutput
3
+ // Keep the fragment input shape explicit for compilers that tree-shake imported
4
+ // type declarations before resolving the vertex entry point.
5
+ struct FullscreenOutput {
6
+ @builtin(position) position : vec4<f32>,
7
+ @location(0) uv : vec2<f32>,
8
+ };
9
+
10
+ fn fullscreen_triangle(vertex_index : u32) -> FullscreenOutput {
11
+ var x : f32 = -1.0;
12
+ var y : f32 = -1.0;
13
+ if (vertex_index == 1u) {
14
+ x = 3.0;
15
+ }
16
+ if (vertex_index == 2u) {
17
+ y = 3.0;
18
+ }
19
+ let u : f32 = (x + 1.0) * 0.5;
20
+ let v : f32 = 1.0 - (y + 1.0) * 0.5;
21
+ var out : FullscreenOutput;
22
+ out.position = vec4<f32>(x, y, 0.0, 1.0);
23
+ out.uv = vec2<f32>(u, v);
24
+ return out;
25
+ }
5
26
 
6
27
  struct TaaResolveParams {
7
28
  currentJitterUv : vec2<f32>,
@@ -14,12 +35,15 @@ struct TaaResolveOutput {
14
35
  @location(1) temporal : vec4<f32>,
15
36
  };
16
37
 
17
- @group(0) @binding(0) var currentColor : texture_2d<f32>;
18
- @group(0) @binding(1) var currentTemporal : texture_2d<f32>;
19
- @group(0) @binding(2) var historyColor : texture_2d<f32>;
20
- @group(0) @binding(3) var historyTemporal : texture_2d<f32>;
21
- @group(0) @binding(4) var historySampler : sampler;
22
- @group(0) @binding(5) var<uniform> params : TaaResolveParams;
38
+ @group(1) @binding(0) var currentColor : texture_2d<f32>;
39
+ @group(1) @binding(1) var currentSampler : sampler;
40
+ @group(1) @binding(2) var historyColor : texture_2d<f32>;
41
+ @group(1) @binding(3) var historySampler : sampler;
42
+ @group(1) @binding(4) var historyTemporal : texture_2d<f32>;
43
+ @group(1) @binding(5) var temporalSampler : sampler;
44
+ @group(1) @binding(6) var currentTemporal : texture_2d<f32>;
45
+ @group(1) @binding(7) var currentTemporalSampler : sampler;
46
+ @group(1) @binding(8) var<uniform> params : TaaResolveParams;
23
47
 
24
48
  fn rgbToYCoCg(rgb : vec3<f32>) -> vec3<f32> {
25
49
  return vec3<f32>(
@@ -37,93 +61,64 @@ fn luminance(rgb : vec3<f32>) -> f32 {
37
61
  return dot(rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
38
62
  }
39
63
 
40
- fn jitterCorrectedCurrent(uv : vec2<f32>) -> vec4<f32> {
41
- // The resolve and history live in the unjittered output domain. The main
42
- // scene raster is the only jittered consumer, so re-center its color here;
43
- // packed motion is already unjittered currentUv - previousUv.
44
- return textureSampleLevel(currentColor, historySampler, uv + params.currentJitterUv, 0.0);
64
+ fn closestCurrentTemporal(pixel : vec2<i32>, dimensions : vec2<i32>) -> vec4<f32> {
65
+ let clamped = clamp(pixel, vec2<i32>(0), dimensions - vec2<i32>(1));
66
+ return textureLoad(currentTemporal, clamped, 0);
45
67
  }
46
68
 
47
- fn closestCurrentTemporal(pixel : vec2<i32>, dimensions : vec2<i32>) -> vec4<f32> {
48
- var closest = vec4<f32>(0.0, 0.0, -1.0, 1.0);
49
- var closestDepth = 1e20;
50
- for (var y = -1; y <= 1; y += 1) {
51
- for (var x = -1; x <= 1; x += 1) {
52
- let samplePixel = clamp(pixel + vec2<i32>(x, y), vec2<i32>(0), dimensions - vec2<i32>(1));
53
- let candidate = textureLoad(currentTemporal, samplePixel, 0);
54
- if candidate.z >= 0.0 && candidate.z < closestDepth {
55
- closest = candidate;
56
- closestDepth = candidate.z;
57
- }
58
- }
59
- }
60
- return closest;
69
+ fn neighborhoodSquareMean(uv : vec2<f32>, texel : vec2<f32>) -> vec3<f32> {
70
+ let north = textureSampleLevel(currentColor, currentSampler, uv + vec2<f32>(0.0, texel.y), 0.0).rgb;
71
+ let south = textureSampleLevel(currentColor, currentSampler, uv - vec2<f32>(0.0, texel.y), 0.0).rgb;
72
+ let east = textureSampleLevel(currentColor, currentSampler, uv + vec2<f32>(texel.x, 0.0), 0.0).rgb;
73
+ let west = textureSampleLevel(currentColor, currentSampler, uv - vec2<f32>(texel.x, 0.0), 0.0).rgb;
74
+ return (north + south + east + west) * 0.25;
61
75
  }
62
76
 
63
77
  @vertex
64
- fn vs_taa_resolve(@builtin(vertex_index) vertexIndex : u32) -> FullscreenOutput {
78
+ fn vs_main(@builtin(vertex_index) vertexIndex : u32) -> FullscreenOutput {
65
79
  return fullscreen_triangle(vertexIndex);
66
80
  }
67
81
 
68
- @fragment
69
82
  fn fs_taa_resolve(in : FullscreenOutput) -> TaaResolveOutput {
70
- let dimensions = vec2<i32>(textureDimensions(currentColor));
71
- let pixel = clamp(vec2<i32>(in.position.xy), vec2<i32>(0), dimensions - vec2<i32>(1));
72
- let current = jitterCorrectedCurrent(in.uv);
83
+ let dimensions = vec2<i32>(textureDimensions(currentColor, 0));
84
+ let texel = 1.0 / vec2<f32>(dimensions);
85
+ let current = textureSampleLevel(currentColor, currentSampler, in.uv + params.currentJitterUv, 0.0);
86
+ let pixel = vec2<i32>(in.uv * vec2<f32>(dimensions));
73
87
  let temporal = closestCurrentTemporal(pixel, dimensions);
74
-
75
- var output : TaaResolveOutput;
76
- output.temporal = temporal;
77
- output.color = current;
78
-
79
88
  let historyUv = in.uv - temporal.xy;
80
- let historyInBounds = all(historyUv >= vec2<f32>(0.0)) && all(historyUv < vec2<f32>(1.0));
81
- if params.historyValid == 0u || !historyInBounds || temporal.z < 0.0 {
82
- return output;
83
- }
84
-
85
- let previousTemporal = textureSampleLevel(historyTemporal, historySampler, historyUv, 0.0);
89
+ let historyInBounds = all(historyUv >= vec2<f32>(0.0)) && all(historyUv <= vec2<f32>(1.0));
90
+ let history = textureSampleLevel(historyColor, historySampler, historyUv, 0.0);
91
+ let previousTemporal = textureSampleLevel(historyTemporal, temporalSampler, historyUv, 0.0);
86
92
  let depthDelta = abs(previousTemporal.z - temporal.z);
87
- let depthThreshold = max(0.01, temporal.z * 0.01);
88
- if previousTemporal.z < 0.0 || depthDelta > depthThreshold {
89
- return output;
90
- }
91
-
92
- var neighborhoodMin = vec3<f32>(1e20);
93
- var neighborhoodMax = vec3<f32>(-1e20);
94
- var neighborhoodMean = vec3<f32>(0.0);
95
- var neighborhoodSquareMean = vec3<f32>(0.0);
96
- for (var y = -1; y <= 1; y += 1) {
97
- for (var x = -1; x <= 1; x += 1) {
98
- let sampleUv = in.uv + vec2<f32>(f32(x), f32(y)) / vec2<f32>(dimensions);
99
- let sampleValue = rgbToYCoCg(jitterCorrectedCurrent(sampleUv).rgb);
100
- neighborhoodMin = min(neighborhoodMin, sampleValue);
101
- neighborhoodMax = max(neighborhoodMax, sampleValue);
102
- neighborhoodMean += sampleValue;
103
- neighborhoodSquareMean += sampleValue * sampleValue;
104
- }
105
- }
106
- neighborhoodMean /= 9.0;
107
- neighborhoodSquareMean /= 9.0;
108
- let sigma = sqrt(max(neighborhoodSquareMean - neighborhoodMean * neighborhoodMean, vec3<f32>(0.0)));
109
- let clipMin = max(neighborhoodMin, neighborhoodMean - sigma * 1.25);
110
- let clipMax = min(neighborhoodMax, neighborhoodMean + sigma * 1.25);
111
-
112
- let sampledHistory = textureSampleLevel(historyColor, historySampler, historyUv, 0.0);
113
- let clippedHistoryRgb = yCoCgToRgb(clamp(rgbToYCoCg(sampledHistory.rgb), clipMin, clipMax));
114
- let velocityFactor = 1.0 - clamp(length(temporal.xy) * 64.0, 0.0, 1.0);
93
+ let depthThreshold = 0.0025 + temporal.z * 0.01;
94
+ let rejected =
95
+ params.historyValid == 0u ||
96
+ !historyInBounds ||
97
+ temporal.z < 0.0 ||
98
+ previousTemporal.z < 0.0 ||
99
+ depthDelta > depthThreshold;
100
+ let neighborhood = neighborhoodSquareMean(in.uv, texel);
101
+ let neighborhoodDelta = abs(luminance(current.rgb) - luminance(neighborhood));
102
+ let clipPadding = vec3<f32>(0.5);
103
+ let clipMin = rgbToYCoCg(min(current.rgb, neighborhood - vec3<f32>(neighborhoodDelta) - clipPadding));
104
+ let clipMax = rgbToYCoCg(max(current.rgb, neighborhood + vec3<f32>(neighborhoodDelta) + clipPadding));
105
+ let clippedHistoryRgb = yCoCgToRgb(clamp(rgbToYCoCg(history.rgb), clipMin, clipMax));
115
106
  let reactiveFactor = 1.0 - clamp(temporal.w, 0.0, 1.0);
107
+ let velocityFactor = 1.0 - clamp(length(temporal.xy) * 64.0, 0.0, 1.0);
116
108
  let depthFactor = 1.0 - clamp(depthDelta / depthThreshold, 0.0, 1.0);
117
- let currentLuma = luminance(current.rgb);
118
- let historyLuma = luminance(clippedHistoryRgb);
119
- let lumaDelta = abs(historyLuma - currentLuma);
120
- let unbiasedLumaDelta = clamp(lumaDelta / max(max(currentLuma, historyLuma), 0.2), 0.0, 1.0);
109
+ let unbiasedLumaDelta = clamp(abs(luminance(current.rgb) - luminance(history.rgb)), 0.0, 1.0);
121
110
  let lumaFactor = (1.0 - unbiasedLumaDelta) * (1.0 - unbiasedLumaDelta);
122
111
  let fixedPresetWeight = mix(0.88, 0.97, lumaFactor);
123
- let startupWeight = f32(params.temporalFrameIndex) / f32(params.temporalFrameIndex + 1u);
124
- let progressiveWeight = select(fixedPresetWeight, min(startupWeight, fixedPresetWeight), params.temporalFrameIndex < 8u);
125
- let rejectionFactor = min(reactiveFactor, min(velocityFactor, depthFactor));
126
- let historyWeight = progressiveWeight * rejectionFactor;
127
- output.color = vec4<f32>(mix(current.rgb, clippedHistoryRgb, historyWeight), current.a);
128
- return output;
112
+ let progressiveWeight = select(fixedPresetWeight, 0.0, params.temporalFrameIndex < 8u);
113
+ let historyWeight = progressiveWeight * reactiveFactor * velocityFactor * depthFactor;
114
+ let resolved = select(mix(current.rgb, clippedHistoryRgb, historyWeight), current.rgb, rejected);
115
+ return TaaResolveOutput(
116
+ vec4<f32>(resolved, current.a),
117
+ temporal,
118
+ );
119
+ }
120
+
121
+ @fragment
122
+ fn fs_main(in : FullscreenOutput) -> TaaResolveOutput {
123
+ return fs_taa_resolve(in);
129
124
  }
package/src/tonemap.wgsl CHANGED
@@ -1,6 +1,6 @@
1
1
  #define_import_path forgeax_view::tonemap
2
2
 
3
- // @forgeax/engine-shader - Three r184 tone mapping output module.
3
+ // @forgeax/engine-shader - Three r184 Output Transform module.
4
4
  //
5
5
  // The public modes and formulas are the Three.js r184 oracle. The numeric
6
6
  // values are the binding contract mirrored by TONEMAP_SHADER_MODE in tonemap.ts.
@@ -153,8 +153,7 @@ fn tonemapNeutral(color : vec3<f32>) -> vec3<f32> {
153
153
 
154
154
  @fragment
155
155
  fn fs_main(in : FullscreenOutput) -> @location(0) vec4<f32> {
156
- // linearHdrColorDomain: hdr is sampled before the only HDR-to-LDR stage.
157
- // linearLdrColorDomain: the output pass emits the display-target domain.
156
+ // Output Transform consumes linear HDR and emits display-encoded color.
158
157
  let source = textureSample(hdr, samp, in.uv);
159
158
  let sample : vec3<f32> = source.rgb;
160
159
  let exposed : vec3<f32> = sample * params.exposure;
@@ -167,11 +166,10 @@ fn fs_main(in : FullscreenOutput) -> @location(0) vec4<f32> {
167
166
  case 5u: { mapped = tonemapAgx(exposed); }
168
167
  case 6u: { mapped = tonemapNeutral(exposed); }
169
168
  case 7u: { mapped = tonemapReinhard(exposed); }
170
- // Mode 0 is the linear-LDR output pass used when tone mapping is disabled.
171
- // It targets the raw canvas format and mirrors Three r184's output OETF.
172
- default: { mapped = linearToSrgbOetf(exposed); }
169
+ // Mode 0 skips the tone curve but still performs the output conversion.
170
+ default: { mapped = sample; }
173
171
  }
174
- // The mode-0 output pass is display-encoded; other modes retain their
175
- // existing tonemap output contract.
176
- return vec4<f32>(mapped, source.a);
172
+ // Every public mode reaches exactly one shared sRGB OETF.
173
+ let encoded = linearToSrgbOetf(mapped);
174
+ return vec4<f32>(encoded, source.a);
177
175
  }
package/src/unlit.wgsl CHANGED
@@ -1,6 +1,6 @@
1
1
  #define_import_path forgeax_material::unlit
2
- #import forgeax_view::common::{View, FogViewParams, FogRay, Mesh, InstanceData, view, meshes, instances, sampleMaterialTextureLinear, packSceneTemporal}
3
- #import forgeax_view::fog::{apply_fog}
2
+ #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTextureLinear}
3
+ #import forgeax_scene_temporal::{packSceneTemporalV1}
4
4
 
5
5
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
6
6
  #pragma variant_axis VERTEX_COLOR_AVAILABLE
@@ -100,22 +100,6 @@ fn materialVertexColor(in : VsOut) -> vec4<f32> {
100
100
  #endif
101
101
  }
102
102
 
103
- fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
104
- var origin = viewParams.cameraPos;
105
- var direction = normalize(worldPos - origin);
106
- var rayDistance = length(worldPos - origin);
107
- if (viewParams.temporalProjection.z >= 0.5) {
108
- let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
109
- let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
110
- let nearPoint = nearH.xyz / nearH.w;
111
- let farPoint = farH.xyz / farH.w;
112
- direction = normalize(farPoint - nearPoint);
113
- origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
114
- rayDistance = max(dot(worldPos - origin, direction), 0.0);
115
- }
116
- return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
117
- }
118
-
119
103
  @fragment
120
104
  fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
121
105
  let texSample = sampleMaterialTextureLinear(baseColorTexture, baseColorSampler, in.uv, material.baseColorTextureCoordinatesMetadata.zw);
@@ -124,19 +108,14 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
124
108
  if (material.alphaCutoff > 0.0 && alpha < material.alphaCutoff) {
125
109
  discard;
126
110
  }
127
- return applySceneFog(
128
- view,
129
- material.baseColor.rgb * texSample.rgb * vertexColor.rgb,
130
- alpha,
131
- in.worldPos,
132
- );
111
+ return vec4<f32>(material.baseColor.rgb * texSample.rgb * vertexColor.rgb, alpha);
133
112
  }
134
113
 
135
114
  struct TemporalVsOut {
136
115
  @builtin(position) clip : vec4<f32>,
137
116
  @location(0) uv : vec2<f32>,
138
- @location(1) @interpolate(linear) currentClip : vec4<f32>,
139
- @location(2) @interpolate(linear) previousClip : vec4<f32>,
117
+ @location(1) @interpolate(perspective) currentClip : vec4<f32>,
118
+ @location(2) @interpolate(perspective) previousClip : vec4<f32>,
140
119
  #ifdef VERTEX_COLOR_AVAILABLE
141
120
  @location(14) color : vec4<f32>,
142
121
  #endif
@@ -178,9 +157,9 @@ fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
178
157
  if (material.alphaCutoff > 0.0 && alpha < material.alphaCutoff) {
179
158
  discard;
180
159
  }
181
- var reactive = 1.0;
160
+ var reactive = 0.0;
182
161
  #if STORAGE_BUFFER_AVAILABLE == true
183
162
  reactive = meshes[0].temporal.x;
184
163
  #endif
185
- return packSceneTemporal(in.currentClip, in.previousClip, reactive);
164
+ return packSceneTemporalV1(in.currentClip, in.previousClip, view.temporalProjection, reactive);
186
165
  }
@@ -0,0 +1,113 @@
1
+ #define_import_path forgeax_view::volume_composite
2
+ // This is a view-UBO projection, not a second matrix source. The fields and
3
+ // padding preserve common::View camera/inverse-projection byte offsets.
4
+ struct VolumeDepthView {
5
+ _prefix : array<vec4<f32>, 6>,
6
+ cameraPos : vec4<f32>,
7
+ _lightViewProjA : mat4x4<f32>,
8
+ inverseViewProj : mat4x4<f32>,
9
+ };
10
+
11
+ struct VolumeOutput {
12
+ @builtin(position) position : vec4<f32>,
13
+ @location(0) uv : vec2<f32>,
14
+ };
15
+
16
+ @group(0) @binding(0) var resolved_volume : texture_2d<f32>;
17
+ @group(0) @binding(1) var volume_sampler : sampler;
18
+ @group(0) @binding(2) var scene_depth : texture_depth_2d;
19
+ @group(0) @binding(3) var<uniform> volume_view : VolumeDepthView;
20
+
21
+ @vertex
22
+ fn volume_vs(@builtin(vertex_index) index : u32) -> VolumeOutput {
23
+ var positions = array<vec2<f32>, 3>(
24
+ vec2<f32>(-1.0, -1.0),
25
+ vec2<f32>(3.0, -1.0),
26
+ vec2<f32>(-1.0, 3.0),
27
+ );
28
+ var output : VolumeOutput;
29
+ output.position = vec4<f32>(positions[index], 0.0, 1.0);
30
+ output.uv = vec2<f32>(
31
+ positions[index].x * 0.5 + 0.5,
32
+ 0.5 - positions[index].y * 0.5,
33
+ );
34
+ return output;
35
+ }
36
+
37
+ fn composite_resolved_volume(sample : vec4<f32>) -> vec4<f32> {
38
+ let transmittance = clamp(sample.a, 0.0, 1.0);
39
+ return vec4<f32>(sample.rgb, 1.0 - transmittance);
40
+ }
41
+
42
+ fn linear_scene_depth(uv : vec2<f32>) -> f32 {
43
+ let dimensions = max(vec2<f32>(textureDimensions(scene_depth)), vec2<f32>(1.0));
44
+ let pixel = vec2<i32>(clamp(uv * dimensions, vec2<f32>(0.0), dimensions - vec2<f32>(1.0)));
45
+ let device_depth = textureLoad(scene_depth, pixel, 0);
46
+ // WebGPU's framebuffer Y is opposite the view UBO's NDC Y.
47
+ let clip = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, device_depth, 1.0);
48
+ let world = volume_view.inverseViewProj * clip;
49
+ let world_position = world.xyz / max(abs(world.w), 1e-5);
50
+ return distance(world_position, volume_view.cameraPos.xyz);
51
+ }
52
+
53
+ fn radiance_luma(sample : vec4<f32>) -> f32 {
54
+ return max(dot(sample.rgb, vec3<f32>(0.2126, 0.7152, 0.0722)), 0.0);
55
+ }
56
+
57
+ fn radiance_luma_weight(center : vec4<f32>, neighbor : vec4<f32>) -> f32 {
58
+ let center_luma = radiance_luma(center);
59
+ let neighbor_luma = radiance_luma(neighbor);
60
+ let relative_luma = abs(neighbor_luma - center_luma) /
61
+ max(max(neighbor_luma, center_luma), 1e-4);
62
+ return exp(-relative_luma * 8.0);
63
+ }
64
+
65
+ fn edge_aware_resolved_volume(uv : vec2<f32>) -> vec4<f32> {
66
+ let dimensions = max(vec2<f32>(textureDimensions(resolved_volume)), vec2<f32>(1.0));
67
+ let texel = 1.0 / dimensions;
68
+ let center = textureSampleLevel(resolved_volume, volume_sampler, uv, 0.0);
69
+ let center_depth = linear_scene_depth(uv);
70
+ var accumulated = center * 4.0;
71
+ var total_weight = 4.0;
72
+ let offsets = array<vec2<f32>, 8>(
73
+ vec2<f32>(texel.x, 0.0),
74
+ vec2<f32>(-texel.x, 0.0),
75
+ vec2<f32>(0.0, texel.y),
76
+ vec2<f32>(0.0, -texel.y),
77
+ vec2<f32>(texel.x, texel.y),
78
+ vec2<f32>(-texel.x, texel.y),
79
+ vec2<f32>(texel.x, -texel.y),
80
+ vec2<f32>(-texel.x, -texel.y),
81
+ );
82
+ let spatial_weights = array<f32, 8>(2.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0);
83
+ for (var index = 0u; index < 8u; index += 1u) {
84
+ let neighbor_uv = clamp(uv + offsets[index], vec2<f32>(0.0), vec2<f32>(1.0));
85
+ let neighbor = textureSampleLevel(
86
+ resolved_volume,
87
+ volume_sampler,
88
+ neighbor_uv,
89
+ 0.0,
90
+ );
91
+ let neighbor_depth = linear_scene_depth(neighbor_uv);
92
+ // Relative view-depth rejection keeps the silhouette transition local;
93
+ // a fixed world-unit threshold creates halos at far occluders.
94
+ let depth_weight = select(0.0, exp(-abs(neighbor_depth - center_depth) * 0.08),
95
+ abs(neighbor_depth - center_depth) <= max(center_depth * 0.08, 0.25));
96
+ // Transmittance is the available edge signal: retain hard depth/occluder
97
+ // boundaries while reconstructing smooth low-frequency scattering.
98
+ let edge_weight = spatial_weights[index] * depth_weight * exp(-abs(neighbor.a - center.a) * 6.0) *
99
+ radiance_luma_weight(center, neighbor);
100
+ accumulated += neighbor * edge_weight;
101
+ total_weight += edge_weight;
102
+ }
103
+ return accumulated / max(total_weight, 1e-5);
104
+ }
105
+
106
+ @fragment
107
+ fn volume_fs(input : VolumeOutput) -> @location(0) vec4<f32> {
108
+ // Injection zeroes samples behind the observed scene depth. Sampling the
109
+ // cumulative far slice therefore applies the full ray integral while the
110
+ // depth test remains owned by the injection path.
111
+ let sample = edge_aware_resolved_volume(input.uv);
112
+ return composite_resolved_volume(sample);
113
+ }