@forgeax/engine-shader 0.1.6 → 0.1.19

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 (99) hide show
  1. package/README.md +61 -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 +181 -0
  13. package/src/__tests__/lighting-punctual.unit.test.ts +1 -1
  14. package/src/__tests__/material-builtins.unit.test.ts +38 -1
  15. package/src/__tests__/material-contract.unit.test.ts +123 -0
  16. package/src/__tests__/motion-blur.unit.test.ts +44 -0
  17. package/src/__tests__/points-lines-shader.unit.test.ts +43 -0
  18. package/src/__tests__/scene-temporal.unit.test.ts +23 -5
  19. package/src/__tests__/shader-manifest-transmission.unit.test.ts +75 -0
  20. package/src/__tests__/shader.unit.test.ts +43 -3
  21. package/src/__tests__/sprite-variants.unit.test.ts +19 -0
  22. package/src/__tests__/taa-resolve.unit.test.ts +75 -3
  23. package/src/__tests__/transmission-thickness-scale.unit.test.ts +19 -0
  24. package/src/common.wgsl +30 -30
  25. package/src/default-standard-pbr-skin.wgsl +27 -9
  26. package/src/default-standard-pbr.wgsl +209 -28
  27. package/src/fxaa.wgsl +5 -6
  28. package/src/index.ts +3 -0
  29. package/src/material-schemas.ts +7 -0
  30. package/src/motion-blur.wgsl +77 -0
  31. package/src/pbr-temporal.wgsl +15 -2
  32. package/src/points-lines.wgsl +103 -0
  33. package/src/scene-temporal.wgsl +54 -0
  34. package/src/shadow_caster.wgsl +27 -2
  35. package/src/sprite-lit.wgsl +5 -4
  36. package/src/sprite.wgsl +5 -4
  37. package/src/taa-resolve.wgsl +76 -81
  38. package/src/tonemap.wgsl +7 -9
  39. package/src/unlit.wgsl +6 -5
  40. package/dist/__tests__/atmosphere-background.test.d.ts +0 -2
  41. package/dist/__tests__/atmosphere-background.test.d.ts.map +0 -1
  42. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts +0 -2
  43. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts.map +0 -1
  44. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts +0 -2
  45. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts.map +0 -1
  46. package/dist/__tests__/atmosphere-preetham-red.test.d.ts +0 -2
  47. package/dist/__tests__/atmosphere-preetham-red.test.d.ts.map +0 -1
  48. package/dist/__tests__/atmosphere-preetham.test.d.ts +0 -2
  49. package/dist/__tests__/atmosphere-preetham.test.d.ts.map +0 -1
  50. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts +0 -2
  51. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts.map +0 -1
  52. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts +0 -2
  53. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts.map +0 -1
  54. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts +0 -2
  55. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts.map +0 -1
  56. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts +0 -2
  57. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts.map +0 -1
  58. package/dist/__tests__/deferred-lighting-ssao.test.d.ts +0 -2
  59. package/dist/__tests__/deferred-lighting-ssao.test.d.ts.map +0 -1
  60. package/dist/__tests__/fog-oracle.unit.test.d.ts +0 -2
  61. package/dist/__tests__/fog-oracle.unit.test.d.ts.map +0 -1
  62. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts +0 -2
  63. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts.map +0 -1
  64. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts +0 -2
  65. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts.map +0 -1
  66. package/dist/__tests__/hdrp-ssao-compile.test.d.ts +0 -2
  67. package/dist/__tests__/hdrp-ssao-compile.test.d.ts.map +0 -1
  68. package/dist/__tests__/ibl-irradiance.unit.test.d.ts +0 -2
  69. package/dist/__tests__/ibl-irradiance.unit.test.d.ts.map +0 -1
  70. package/dist/__tests__/ibl-sampling.unit.test.d.ts +0 -2
  71. package/dist/__tests__/ibl-sampling.unit.test.d.ts.map +0 -1
  72. package/dist/__tests__/lighting-punctual.unit.test.d.ts +0 -2
  73. package/dist/__tests__/lighting-punctual.unit.test.d.ts.map +0 -1
  74. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts +0 -2
  75. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts.map +0 -1
  76. package/dist/__tests__/material-builtins.unit.test.d.ts +0 -2
  77. package/dist/__tests__/material-builtins.unit.test.d.ts.map +0 -1
  78. package/dist/__tests__/material-contract.unit.test.d.ts +0 -2
  79. package/dist/__tests__/material-contract.unit.test.d.ts.map +0 -1
  80. package/dist/__tests__/material-derived-builtins.integration.test.d.ts +0 -2
  81. package/dist/__tests__/material-derived-builtins.integration.test.d.ts.map +0 -1
  82. package/dist/__tests__/public-surface.unit.test.d.ts +0 -2
  83. package/dist/__tests__/public-surface.unit.test.d.ts.map +0 -1
  84. package/dist/__tests__/scene-temporal.unit.test.d.ts +0 -2
  85. package/dist/__tests__/scene-temporal.unit.test.d.ts.map +0 -1
  86. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts +0 -2
  87. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts.map +0 -1
  88. package/dist/__tests__/shader.unit.test.d.ts +0 -2
  89. package/dist/__tests__/shader.unit.test.d.ts.map +0 -1
  90. package/dist/__tests__/sprite-lit-shader.test.d.ts +0 -2
  91. package/dist/__tests__/sprite-lit-shader.test.d.ts.map +0 -1
  92. package/dist/__tests__/sprite-variants.unit.test.d.ts +0 -2
  93. package/dist/__tests__/sprite-variants.unit.test.d.ts.map +0 -1
  94. package/dist/__tests__/taa-resolve.unit.test.d.ts +0 -2
  95. package/dist/__tests__/taa-resolve.unit.test.d.ts.map +0 -1
  96. package/dist/__tests__/transparent-pbr.unit.test.d.ts +0 -2
  97. package/dist/__tests__/transparent-pbr.unit.test.d.ts.map +0 -1
  98. package/dist/__tests__/vertex-color-variant.unit.test.d.ts +0 -2
  99. package/dist/__tests__/vertex-color-variant.unit.test.d.ts.map +0 -1
@@ -18,6 +18,7 @@
18
18
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
19
19
  #pragma variant_axis CLUSTER_FORWARD_AVAILABLE
20
20
  #pragma variant_axis VERTEX_COLOR_AVAILABLE
21
+ #pragma variant_axis TRANSMISSION_AVAILABLE
21
22
 
22
23
  // @forgeax/engine-shader - default-standard-pbr.wgsl
23
24
  // (feat-20260523-shader-template-instance-split M5 / T04).
@@ -32,8 +33,8 @@
32
33
  //
33
34
  // Bindings (4 BG layout slots; View + Mesh bindings inherited from
34
35
  // forgeax_view::common; shadow map + comparison sampler at view BG
35
- // @binding(3..4) per shadow-mapping feat; Skylight 7 bindings merged into
36
- // the PBR material BGL at @binding(7..13) per D-5 round-4):
36
+ // @binding(3..4) per shadow-mapping feat; Skylight 7 bindings merged after
37
+ // the Standard user region per D-5 round-4:
37
38
  //
38
39
  // @group(0) @binding(0) view uniform (see common.wgsl)
39
40
  // @group(1) @binding(0) material uniform (baseColor vec4
@@ -112,6 +113,11 @@ struct Material {
112
113
  clearcoatRoughness : f32,
113
114
  specularTint : vec3<f32>,
114
115
  normalScale : f32,
116
+ transmission : f32,
117
+ ior : f32,
118
+ thickness : f32,
119
+ attenuationColor : vec3<f32>,
120
+ attenuationDistance : f32,
115
121
  baseColorTextureCoordinatesTransform : vec4<f32>,
116
122
  baseColorTextureCoordinatesMetadata : vec4<f32>,
117
123
  metallicRoughnessTextureCoordinatesTransform : vec4<f32>,
@@ -124,6 +130,10 @@ struct Material {
124
130
  emissiveTextureCoordinatesMetadata : vec4<f32>,
125
131
  occlusionTextureCoordinatesTransform : vec4<f32>,
126
132
  occlusionTextureCoordinatesMetadata : vec4<f32>,
133
+ transmissionTextureCoordinatesTransform : vec4<f32>,
134
+ transmissionTextureCoordinatesMetadata : vec4<f32>,
135
+ thicknessTextureCoordinatesTransform : vec4<f32>,
136
+ thicknessTextureCoordinatesMetadata : vec4<f32>,
127
137
  };
128
138
 
129
139
  @group(1) @binding(0) var<uniform> material : Material;
@@ -139,6 +149,14 @@ struct Material {
139
149
  @group(1) @binding(10) var emissiveTexture : texture_2d<f32>;
140
150
  @group(1) @binding(11) var occlusionSampler : sampler;
141
151
  @group(1) @binding(12) var occlusionTexture : texture_2d<f32>;
152
+ #ifdef TRANSMISSION_AVAILABLE
153
+ @group(1) @binding(13) var transmissionSampler : sampler;
154
+ @group(1) @binding(14) var transmissionTexture : texture_2d<f32>;
155
+ @group(1) @binding(15) var thicknessSampler : sampler;
156
+ @group(1) @binding(16) var thicknessTexture : texture_2d<f32>;
157
+ @group(1) @binding(24) var transmissionBackdropSampler : sampler;
158
+ @group(1) @binding(25) var transmissionBackdropTexture : texture_2d<f32>;
159
+ #endif
142
160
 
143
161
  // Naga reflection does not retain filtering usage through the generic shared
144
162
  // helper. These compile-time-only witnesses retain the binding contract while
@@ -156,6 +174,12 @@ fn materialTextureFilteringWitness() {
156
174
  let specularTintWitness = textureSample(specularTint, specularTintSampler, vec2<f32>(0.0));
157
175
  let emissiveWitness = textureSample(emissive, emissiveSampler, vec2<f32>(0.0));
158
176
  let occlusionWitness = textureSample(occlusion, occlusionSampler, vec2<f32>(0.0));
177
+ #ifdef TRANSMISSION_AVAILABLE
178
+ let transmission = transmissionTexture;
179
+ let thickness = thicknessTexture;
180
+ let transmissionWitness = textureSample(transmission, transmissionSampler, vec2<f32>(0.0));
181
+ let thicknessWitness = textureSample(thickness, thicknessSampler, vec2<f32>(0.0));
182
+ #endif
159
183
  }
160
184
 
161
185
  // Skylight bindings merged into the PBR material BGL
@@ -163,10 +187,10 @@ fn materialTextureFilteringWitness() {
163
187
  // round-4 REVISED). The round-2 stand-alone group 4 Skylight BGL collided
164
188
  // with WebGPU's default maxBindGroups=4 in chrome-beta and blocked pbr-pl
165
189
  // pipeline-layout creation; round-4 appends the 7 Skylight entries to the
166
- // PBR material BindGroupLayout at binding 7..13. The material BG factory
190
+ // PBR material BindGroupLayout after the eight Standard texture pairs. The material BG factory
167
191
  // (mergeSkylightIntoMaterialBgl in
168
192
  // packages/runtime/src/ibl/skylight-bind-group.ts) extends the layout to
169
- // 14 entries; render-system-record assembles a single merged material
193
+ // merged entries; render-system-record assembles a single merged material
170
194
  // BindGroup (no extra setBindGroup(4) call). Identity (default) resources
171
195
  // produce ambient = 0; with a real Skylight, ibl_sampling helpers project
172
196
  // the IBL irradiance + split-sum specular into `ambient` below.
@@ -182,13 +206,13 @@ struct SkylightUniforms {
182
206
  colorB : f32,
183
207
  rotation : vec4<f32>,
184
208
  };
185
- @group(1) @binding(13) var irradianceMap : texture_cube<f32>;
186
- @group(1) @binding(14) var irradianceSampler : sampler;
187
- @group(1) @binding(15) var prefilterMap : texture_cube<f32>;
188
- @group(1) @binding(16) var prefilterSampler : sampler;
189
- @group(1) @binding(17) var brdfLut : texture_2d<f32>;
190
- @group(1) @binding(18) var brdfLutSampler : sampler;
191
- @group(1) @binding(19) var<uniform> skylight : SkylightUniforms;
209
+ @group(1) @binding(17) var irradianceMap : texture_cube<f32>;
210
+ @group(1) @binding(18) var irradianceSampler : sampler;
211
+ @group(1) @binding(19) var prefilterMap : texture_cube<f32>;
212
+ @group(1) @binding(20) var prefilterSampler : sampler;
213
+ @group(1) @binding(21) var brdfLut : texture_2d<f32>;
214
+ @group(1) @binding(22) var brdfLutSampler : sampler;
215
+ @group(1) @binding(23) var<uniform> skylight : SkylightUniforms;
192
216
 
193
217
  // feat-20260612-hdrp-ssao M7 (round 2) D-B + D-C, scope-amend-webgl2-ubo:
194
218
  // SSAO sampling lives on the HDRP unified BGL @group(2) alongside the
@@ -305,7 +329,8 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
305
329
  // Combine: entity_world * per_instance_local.
306
330
  let instanceLocal = instances[idx].localFromInstance;
307
331
  let entityWorld = meshes[0].worldFromLocal;
308
- let world = entityWorld * instanceLocal * vec4<f32>(in.pos, 1.0);
332
+ let localToWorld = entityWorld * instanceLocal;
333
+ let world = localToWorld * vec4<f32>(in.pos, 1.0);
309
334
  var out : VsOut;
310
335
  out.clip = view.worldViewProj * world;
311
336
  out.worldPos = world.xyz;
@@ -387,6 +412,19 @@ fn materialVertexColor(in : VsOut) -> vec4<f32> {
387
412
  #endif
388
413
  }
389
414
 
415
+ fn finiteScalar(value : f32, fallback : f32) -> f32 {
416
+ let bounded = clamp(value, -65504.0, 65504.0);
417
+ return select(fallback, bounded, value == value);
418
+ }
419
+
420
+ fn finiteColor(value : vec3<f32>, fallback : vec3<f32>) -> vec3<f32> {
421
+ return vec3<f32>(
422
+ finiteScalar(value.x, fallback.x),
423
+ finiteScalar(value.y, fallback.y),
424
+ finiteScalar(value.z, fallback.z),
425
+ );
426
+ }
427
+
390
428
  // linearColorDomain: transparent source and destination values are blended
391
429
  // before any display encoding. The fixed-function material blend state uses
392
430
  // this same equation for the render target's declared linear domain.
@@ -423,8 +461,17 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
423
461
  // at the pick_channel call site; values stay in {0,1,2,3}.
424
462
  let mrUv = transformedMaterialUv(material.metallicRoughnessTextureCoordinatesTransform, material.metallicRoughnessTextureCoordinatesMetadata, in);
425
463
  let mrSample = sampleMaterialTexture(metallicRoughnessTexture, metallicRoughnessSampler, mrUv, material.metallicRoughnessTextureCoordinatesMetadata.zw);
426
- let metallic = material.metallic * pick_channel(mrSample, u32(material.metallicChannel));
427
- let roughnessTex = pick_channel(mrSample, u32(material.roughnessChannel));
464
+ let metallic = clamp(
465
+ finiteScalar(material.metallic, 0.0) *
466
+ finiteScalar(pick_channel(mrSample, u32(material.metallicChannel)), 1.0),
467
+ 0.0,
468
+ 1.0,
469
+ );
470
+ let roughnessTex = clamp(
471
+ finiteScalar(pick_channel(mrSample, u32(material.roughnessChannel)), 1.0),
472
+ 0.0,
473
+ 1.0,
474
+ );
428
475
 
429
476
  // Layer-2 fail-fast: shader internal clamp keeps D_GGX finite for
430
477
  // roughness=0 even if the asset somehow bypasses layer-1 register
@@ -433,7 +480,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
433
480
  // so the final roughness >= 0.04 floor is preserved (texture sample is
434
481
  // a 1.0-default white when metallicRoughnessTexture is absent, so
435
482
  // multiplying after the clamp keeps the floor intact).
436
- var a = max(material.roughness, 0.04);
483
+ var a = max(finiteScalar(material.roughness, 0.5), 0.04);
437
484
  a = a * roughnessTex;
438
485
  a = a * a;
439
486
 
@@ -452,7 +499,57 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
452
499
  let specularTint = material.specularTint * sampleMaterialTexture(
453
500
  specularTintTexture, specularTintSampler, specularUv, material.specularTintTextureCoordinatesMetadata.zw,
454
501
  ).rgb;
455
- let f0 = mix(vec3<f32>(0.04) * specularTint, albedo, metallic);
502
+ let safeIor = max(finiteScalar(material.ior, 1.5), 1.0);
503
+ let dielectricF0 = pow((safeIor - 1.0) / (safeIor + 1.0), 2.0);
504
+ let f0 = mix(vec3<f32>(dielectricF0) * specularTint, albedo, metallic);
505
+ var diffuseAlbedo = albedo;
506
+ #ifdef TRANSMISSION_AVAILABLE
507
+ let transmissionUv = transformedMaterialUv(
508
+ material.transmissionTextureCoordinatesTransform,
509
+ material.transmissionTextureCoordinatesMetadata,
510
+ in,
511
+ );
512
+ let thicknessUv = transformedMaterialUv(
513
+ material.thicknessTextureCoordinatesTransform,
514
+ material.thicknessTextureCoordinatesMetadata,
515
+ in,
516
+ );
517
+ let transmissionSample = sampleMaterialTexture(
518
+ transmissionTexture,
519
+ transmissionSampler,
520
+ transmissionUv,
521
+ material.transmissionTextureCoordinatesMetadata.zw,
522
+ ).r;
523
+ let thicknessSample = sampleMaterialTexture(
524
+ thicknessTexture,
525
+ thicknessSampler,
526
+ thicknessUv,
527
+ material.thicknessTextureCoordinatesMetadata.zw,
528
+ ).g;
529
+ let transmissionFactor = clamp(
530
+ finiteScalar(material.transmission, 0.0) * finiteScalar(transmissionSample, 1.0),
531
+ 0.0,
532
+ 1.0,
533
+ );
534
+ let viewDot = finiteScalar(dot(n, v), 0.0);
535
+ let refractionFromInside = viewDot < 0.0;
536
+ let refractionNormal = select(n, -n, refractionFromInside);
537
+ let refractionEta = select(1.0 / safeIor, safeIor, refractionFromInside);
538
+ let incident = -v;
539
+ let refracted = refract(incident, refractionNormal, refractionEta);
540
+ let refractedLengthSquared = dot(refracted, refracted);
541
+ let viewCos = clamp(abs(viewDot), 0.0, 1.0);
542
+ let fresnel = clamp(f_schlick(viewCos, vec3<f32>(dielectricF0)).x, 0.0, 1.0);
543
+ // `refract` returns the zero vector for total internal reflection. Keep the
544
+ // reflection/IBL term above authoritative and remove transmission energy
545
+ // instead of sampling and adding a second reflected environment.
546
+ let transmittedEnergy = select(
547
+ 0.0,
548
+ transmissionFactor * (1.0 - metallic) * (1.0 - fresnel),
549
+ refractedLengthSquared > 1e-6,
550
+ );
551
+ diffuseAlbedo = albedo * (1.0 - transmittedEnergy);
552
+ #endif
456
553
  let coatRoughness = max(material.clearcoatRoughness, 0.04);
457
554
  let coatAlpha = coatRoughness * coatRoughness;
458
555
  let coatF = f_schlick(max(dot(n, v), 0.0), vec3<f32>(0.04)) * material.clearcoat;
@@ -487,7 +584,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
487
584
  // through the per-cascade light-space matrix in the fragment stage for
488
585
  // shadow-map PCF lookup (CSM, feat-20260613).
489
586
  let kD = (vec3<f32>(1.0) - f_schlick(max(dot(n, v), 0.0), f0)) * (1.0 - metallic);
490
- let iblRoughness = max(material.roughness, 0.04) * roughnessTex;
587
+ let iblRoughness = clamp(max(finiteScalar(material.roughness, 0.5), 0.04) * roughnessTex, 0.04, 1.0);
491
588
  let irradiance = sampleIblDiffuse(n, skylight.rotation, irradianceMap, irradianceSampler);
492
589
  let specularIbl = sampleIblSpecular(
493
590
  n, v, iblRoughness, f0,
@@ -501,7 +598,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
501
598
  // CLUSTER_FORWARD_AVAILABLE branch below can `ambient *=` the SSAO
502
599
  // factor. The non-HDRP path leaves ambient untouched.
503
600
  let skyColor = vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB);
504
- var ambient = (kD * irradiance * albedo + specularIbl) * (vec3<f32>(1.0) - coatF);
601
+ var ambient = (kD * irradiance * diffuseAlbedo + specularIbl) * (vec3<f32>(1.0) - coatF);
505
602
  if (material.clearcoat != 0.0) {
506
603
  let clearcoatIbl = sampleIblSpecular(
507
604
  n, v, coatRoughness, vec3<f32>(0.04),
@@ -529,8 +626,91 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
529
626
  ambient *= mix(1.0, ssaoFactor * ao, ssaoIntensity);
530
627
  #endif
531
628
  var color = ambient;
629
+ #ifdef TRANSMISSION_AVAILABLE
630
+ let screenUv = in.ndc.xy * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5);
631
+ // Read the same renderer-owned transform tables that the vertex stage uses
632
+ // instead of adding six more interstage varyings. WebGPU guarantees only 16
633
+ // user-defined vertex output locations; keeping the affine conversion in
634
+ // the fragment stage preserves that contract and keeps thickness in the
635
+ // glTF unit-scale mesh space before measuring the refracted segment in world
636
+ // metres.
637
+ let localToWorld = meshes[0].worldFromLocal * instances[in.instanceIdx].localFromInstance;
638
+ let localToWorld0 = localToWorld[0].xyz;
639
+ let localToWorld1 = localToWorld[1].xyz;
640
+ let localToWorld2 = localToWorld[2].xyz;
641
+ let inverseCofactor0 = cross(localToWorld1, localToWorld2);
642
+ let inverseCofactor1 = cross(localToWorld2, localToWorld0);
643
+ let inverseCofactor2 = cross(localToWorld0, localToWorld1);
644
+ let localToWorldDet = dot(localToWorld0, inverseCofactor0);
645
+ let safeLocalToWorldDet = select(1.0, localToWorldDet, abs(localToWorldDet) >= 1e-6);
646
+ let worldToLocal0 = vec3<f32>(
647
+ inverseCofactor0.x,
648
+ inverseCofactor1.x,
649
+ inverseCofactor2.x,
650
+ ) / safeLocalToWorldDet;
651
+ let worldToLocal1 = vec3<f32>(
652
+ inverseCofactor0.y,
653
+ inverseCofactor1.y,
654
+ inverseCofactor2.y,
655
+ ) / safeLocalToWorldDet;
656
+ let worldToLocal2 = vec3<f32>(
657
+ inverseCofactor0.z,
658
+ inverseCofactor1.z,
659
+ inverseCofactor2.z,
660
+ ) / safeLocalToWorldDet;
661
+ let refractedLocal = normalize(
662
+ worldToLocal0 * refracted.x +
663
+ worldToLocal1 * refracted.y +
664
+ worldToLocal2 * refracted.z,
665
+ );
666
+ let worldRefractedDirection =
667
+ localToWorld0 * refractedLocal.x +
668
+ localToWorld1 * refractedLocal.y +
669
+ localToWorld2 * refractedLocal.z;
670
+ let worldThickness = max(
671
+ finiteScalar(material.thickness, 0.0) * length(worldRefractedDirection) *
672
+ finiteScalar(thicknessSample, 1.0),
673
+ 0.0,
674
+ );
675
+ let refractedUv = screenUv + (refracted.xy - incident.xy) * worldThickness * 0.25;
676
+ let guardBand = 0.02;
677
+ let insideGuardBand = all(refractedUv >= vec2<f32>(guardBand)) &&
678
+ all(refractedUv <= vec2<f32>(1.0 - guardBand));
679
+ let backdropMipCount = textureNumLevels(transmissionBackdropTexture);
680
+ let backdropMaxLod = max(f32(backdropMipCount) - 1.0, 0.0);
681
+ let backdropLod = clamp(iblRoughness * iblRoughness * backdropMaxLod, 0.0, backdropMaxLod);
682
+ let unrefractedBackdrop = textureSampleLevel(
683
+ transmissionBackdropTexture,
684
+ transmissionBackdropSampler,
685
+ clamp(screenUv, vec2<f32>(0.0), vec2<f32>(1.0)),
686
+ backdropLod,
687
+ ).rgb;
688
+ var transmittedBackdrop = unrefractedBackdrop;
689
+ if (refractedLengthSquared > 1e-6 && insideGuardBand) {
690
+ transmittedBackdrop = textureSampleLevel(
691
+ transmissionBackdropTexture,
692
+ transmissionBackdropSampler,
693
+ clamp(refractedUv, vec2<f32>(0.0), vec2<f32>(1.0)),
694
+ backdropLod,
695
+ ).rgb;
696
+ }
697
+ let safeAttenuationColor = clamp(
698
+ finiteColor(material.attenuationColor, vec3<f32>(1.0)),
699
+ vec3<f32>(0.0),
700
+ vec3<f32>(1.0),
701
+ );
702
+ let safeAttenuationDistance = max(finiteScalar(material.attenuationDistance, 0.0), 0.0);
703
+ let attenuationExponent = worldThickness / max(safeAttenuationDistance, 1e-6);
704
+ let beerAttenuation = select(
705
+ vec3<f32>(1.0),
706
+ pow(safeAttenuationColor, vec3<f32>(attenuationExponent)),
707
+ safeAttenuationDistance > 1e-6 && worldThickness > 0.0,
708
+ );
709
+ color = color + finiteColor(transmittedBackdrop, vec3<f32>(0.0)) *
710
+ transmittedEnergy * beerAttenuation;
711
+ #endif
532
712
  let directionalShadow = evalDirectionalShadowFactor(n, in.worldPos, in.viewZ);
533
- let directionalBase = evalDirectionalNoShadow(n, v, albedo, metallic, a, f0);
713
+ let directionalBase = evalDirectionalNoShadow(n, v, diffuseAlbedo, metallic, a, f0);
534
714
  color = color + directionalShadow * directionalBase;
535
715
  if (material.clearcoat != 0.0) {
536
716
  let directionalClearcoat = evalDirectionalNoShadow(
@@ -541,7 +721,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
541
721
  #ifdef CLUSTER_FORWARD_AVAILABLE
542
722
  // NDC from vertex shader (perspective-divided clip-space, interpolated).
543
723
  // view_z: NDC depth for cluster Z-slice lookup.
544
- color = color + evaluate_cluster_lights(in.ndc, in.viewZ, in.worldPos, n, v, albedo, metallic, a, f0);
724
+ color = color + evaluate_cluster_lights(in.ndc, in.viewZ, in.worldPos, n, v, diffuseAlbedo, metallic, a, f0);
545
725
  if (material.clearcoat != 0.0) {
546
726
  color = color + material.clearcoat * evaluate_cluster_lights(
547
727
  in.ndc, in.viewZ, in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
@@ -562,7 +742,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
562
742
  let lane = shadowParams[p.shadowAtlasLayer];
563
743
  color = color + evalPointShadowed(
564
744
  p.position, p.colorTimesIntensity, p.invRangeSquared,
565
- in.worldPos, n, v, albedo, metallic, a, f0,
745
+ in.worldPos, n, v, diffuseAlbedo, metallic, a, f0,
566
746
  p.shadowAtlasLayer, lane.x, lane.y, 0.005, 0.05,
567
747
  );
568
748
  if (material.clearcoat != 0.0) {
@@ -575,7 +755,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
575
755
  } else {
576
756
  color = color + evalPoint(
577
757
  p.position, p.colorTimesIntensity, p.invRangeSquared,
578
- in.worldPos, n, v, albedo, metallic, a, f0,
758
+ in.worldPos, n, v, diffuseAlbedo, metallic, a, f0,
579
759
  );
580
760
  if (material.clearcoat != 0.0) {
581
761
  color = color + material.clearcoat * evalPoint(
@@ -587,7 +767,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
587
767
  #else
588
768
  color = color + evalPoint(
589
769
  p.position, p.colorTimesIntensity, p.invRangeSquared,
590
- in.worldPos, n, v, albedo, metallic, a, f0,
770
+ in.worldPos, n, v, diffuseAlbedo, metallic, a, f0,
591
771
  );
592
772
  if (material.clearcoat != 0.0) {
593
773
  color = color + material.clearcoat * evalPoint(
@@ -614,7 +794,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
614
794
  color = color + evalSpotShadowed(
615
795
  s.position, s.direction, s.colorTimesIntensity,
616
796
  s.cosInner, s.cosOuter, s.invRangeSquared,
617
- in.worldPos, n, v, albedo, metallic, a, f0,
797
+ in.worldPos, n, v, diffuseAlbedo, metallic, a, f0,
618
798
  view.spotLightViewProj[s.shadowAtlasTile], s.shadowAtlasTile, 0.005, 0.05,
619
799
  );
620
800
  if (material.clearcoat != 0.0) {
@@ -629,7 +809,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
629
809
  color = color + evalSpot(
630
810
  s.position, s.direction, s.colorTimesIntensity,
631
811
  s.cosInner, s.cosOuter, s.invRangeSquared,
632
- in.worldPos, n, v, albedo, metallic, a, f0,
812
+ in.worldPos, n, v, diffuseAlbedo, metallic, a, f0,
633
813
  );
634
814
  if (material.clearcoat != 0.0) {
635
815
  color = color + material.clearcoat * evalSpot(
@@ -730,8 +910,8 @@ struct TemporalVsOut {
730
910
  @location(5) uv5 : vec2<f32>,
731
911
  @location(6) uv6 : vec2<f32>,
732
912
  @location(7) uv7 : vec2<f32>,
733
- @location(8) @interpolate(linear) currentClip : vec4<f32>,
734
- @location(9) @interpolate(linear) previousClip : vec4<f32>,
913
+ @location(8) @interpolate(perspective) currentClip : vec4<f32>,
914
+ @location(9) @interpolate(perspective) previousClip : vec4<f32>,
735
915
  #ifdef VERTEX_COLOR_AVAILABLE
736
916
  @location(14) color : vec4<f32>,
737
917
  #endif
@@ -774,7 +954,7 @@ fn temporalVertexAlpha(in : TemporalVsOut) -> f32 {
774
954
 
775
955
  @fragment
776
956
  fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
777
- var reactive = 1.0;
957
+ var reactive = 0.0;
778
958
  #if STORAGE_BUFFER_AVAILABLE == true
779
959
  reactive = meshes[0].temporal.x;
780
960
  #endif
@@ -787,6 +967,7 @@ fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
787
967
  material.baseColorTextureCoordinatesMetadata,
788
968
  in.currentClip,
789
969
  in.previousClip,
970
+ view.temporalProjection,
790
971
  reactive,
791
972
  in.uv, in.uv1, in.uv2, in.uv3,
792
973
  in.uv4, in.uv5, in.uv6, in.uv7,
package/src/fxaa.wgsl CHANGED
@@ -33,14 +33,13 @@
33
33
  // holes in shaded surfaces -- the bug this rewrite fixes.
34
34
  //
35
35
  // Bindings (group 0):
36
- // @binding(0) screenTexture : texture_2d<f32> -- sampled LDR input
36
+ // @binding(0) screenTexture : texture_2d<f32> -- sampled display-encoded input
37
37
  // @binding(1) samp : sampler -- linear filterable sampler
38
- // linearLdrColorDomain: FXAA samples and writes linear LDR values; display
39
- // encoding happens only at the final output surface.
38
+ // display-encoded: FXAA samples and writes display-encoded values; the Output
39
+ // Transform owns the sole linear-to-sRGB conversion before this pass.
40
40
 
41
41
  #import forgeax_view::common::FullscreenOutput
42
42
  #import forgeax_view::common::fullscreen_triangle
43
- #import forgeax_view::common::linearToSrgbOetf
44
43
 
45
44
  const EDGE_THRESHOLD_MIN: f32 = 0.0312;
46
45
  const EDGE_THRESHOLD_MAX: f32 = 0.125;
@@ -106,7 +105,7 @@ fn fs_main(in: FullscreenOutput) -> @location(0) vec4<f32> {
106
105
 
107
106
  // Not on an edge (or in a near-uniform region): pass the centre through.
108
107
  if lumaRange < max(EDGE_THRESHOLD_MIN, lumaMax * EDGE_THRESHOLD_MAX) {
109
- return vec4<f32>(linearToSrgbOetf(centerColor), 1.0);
108
+ return vec4<f32>(centerColor, 1.0);
110
109
  }
111
110
 
112
111
  // Lumas of the 4 corners.
@@ -255,5 +254,5 @@ fn fs_main(in: FullscreenOutput) -> @location(0) vec4<f32> {
255
254
  }
256
255
 
257
256
  let finalColor = sampleColor(finalUv);
258
- return vec4<f32>(linearToSrgbOetf(finalColor), 1.0);
257
+ return vec4<f32>(finalColor, 1.0);
259
258
  }
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
@@ -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,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
  }