@forgeax/engine-shader 0.1.27 → 0.1.29

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 (73) hide show
  1. package/NOTICE +35 -0
  2. package/README.md +39 -0
  3. package/dist/ShaderRegistry.d.ts +8 -0
  4. package/dist/ShaderRegistry.d.ts.map +1 -1
  5. package/dist/index.d.ts +6 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.mjs +312 -71
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/material/artifact-types.d.ts +12 -2
  10. package/dist/material/artifact-types.d.ts.map +1 -1
  11. package/dist/material-schemas.d.ts +5 -0
  12. package/dist/material-schemas.d.ts.map +1 -1
  13. package/package.json +5 -4
  14. package/src/ShaderRegistry.ts +10 -0
  15. package/src/__tests__/auto-exposure-graph.unit.test.ts +59 -0
  16. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -15
  17. package/src/__tests__/builtin-texture-sampling-contract.test.ts +1 -1
  18. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +2 -1
  19. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +167 -6
  20. package/src/__tests__/direct-light-layout.unit.test.ts +1 -1
  21. package/src/__tests__/lighting-punctual.unit.test.ts +29 -1
  22. package/src/__tests__/ltc-provenance.unit.test.ts +13 -2
  23. package/src/__tests__/material-builtins.unit.test.ts +1 -0
  24. package/src/__tests__/material-contract.unit.test.ts +42 -9
  25. package/src/__tests__/material-derived-builtins.integration.test.ts +18 -2
  26. package/src/__tests__/probe-lighting-composition.unit.test.ts +1 -1
  27. package/src/__tests__/reflection-probe-sampling.unit.test.ts +23 -2
  28. package/src/__tests__/scene-temporal.unit.test.ts +10 -0
  29. package/src/__tests__/shader.unit.test.ts +1 -0
  30. package/src/__tests__/sprite-variants.unit.test.ts +1 -0
  31. package/src/__tests__/ssr-artifact.unit.test.ts +141 -0
  32. package/src/__tests__/ssr-bgl.integration.test.ts +185 -0
  33. package/src/__tests__/standard-output-domain.unit.test.ts +36 -0
  34. package/src/__tests__/standard-pbr-artifact-receipt.unit.test.ts +93 -0
  35. package/src/__tests__/standard-surface-pass-evaluation.integration.test.ts +16 -4
  36. package/src/__tests__/surface-v1.unit.test.ts +6 -0
  37. package/src/__tests__/taa-resolve.unit.test.ts +20 -9
  38. package/src/__tests__/transmission-thickness-scale.unit.test.ts +6 -1
  39. package/src/__tests__/transparent-pbr.unit.test.ts +1 -1
  40. package/src/__tests__/vertex-color-variant.unit.test.ts +4 -1
  41. package/src/atmosphere-background.wgsl +3 -3
  42. package/src/auto-exposure-meter.wgsl +179 -0
  43. package/src/color-lut.wgsl +17 -0
  44. package/src/common.wgsl +7 -5
  45. package/src/default-standard-pbr-skin.wgsl +142 -40
  46. package/src/default-standard-pbr.wgsl +198 -82
  47. package/src/default_standard_surface.wgsl +56 -19
  48. package/src/fxaa.wgsl +9 -5
  49. package/src/ibl-sampling.wgsl +30 -6
  50. package/src/index.ts +189 -0
  51. package/src/lighting-punctual.wgsl +5 -6
  52. package/src/material/artifact-types.ts +109 -63
  53. package/src/material-schemas.ts +84 -8
  54. package/src/output-encoding.wgsl +10 -0
  55. package/src/pbr-temporal.wgsl +9 -1
  56. package/src/scene-temporal.wgsl +2 -0
  57. package/src/shadow-pcf.wgsl +6 -8
  58. package/src/shadow-surface.wgsl +16 -0
  59. package/src/shadow_caster.wgsl +99 -61
  60. package/src/sprite-lit.wgsl +4 -4
  61. package/src/sprite.wgsl +3 -3
  62. package/src/ssr-compose.wgsl +129 -0
  63. package/src/ssr-hiz-reduce.wgsl +53 -0
  64. package/src/ssr-hiz.wgsl +66 -0
  65. package/src/ssr-temporal.wgsl +305 -0
  66. package/src/ssr-trace.wgsl +784 -0
  67. package/src/standard-cluster.wgsl +6 -4
  68. package/src/standard-surface.wgsl +696 -0
  69. package/src/surface_v1.wgsl +6 -0
  70. package/src/taa-resolve.wgsl +222 -28
  71. package/src/tbn.wgsl +1 -1
  72. package/src/tonemap.wgsl +37 -18
  73. package/src/unlit.wgsl +3 -3
@@ -6,7 +6,14 @@
6
6
  // interface remains the source of the scalar and texture values; the
7
7
  // Standard template owns physical-layer selection and lighting.
8
8
  fn surfaceUv(input : SurfaceInput, transform : vec4<f32>, metadata : vec4<f32>) -> vec2<f32> {
9
- let source = select(input.uv0, input.uv1, metadata.x >= 1.0);
9
+ var source = input.uv0;
10
+ if (metadata.x >= 1.0) { source = input.uv1; }
11
+ if (metadata.x >= 2.0) { source = input.uv2; }
12
+ if (metadata.x >= 3.0) { source = input.uv3; }
13
+ if (metadata.x >= 4.0) { source = input.uv4; }
14
+ if (metadata.x >= 5.0) { source = input.uv5; }
15
+ if (metadata.x >= 6.0) { source = input.uv6; }
16
+ if (metadata.x >= 7.0) { source = input.uv7; }
10
17
  let scaled = source * transform.zw;
11
18
  let c = cos(metadata.y);
12
19
  let s = sin(metadata.y);
@@ -31,58 +38,88 @@ fn surfaceNormal(input : SurfaceInput, encoded : vec4<f32>, normalScale : f32) -
31
38
  return normalize(tangent * tangentXY.x + bitangent * tangentXY.y + geometric * tangentZ);
32
39
  }
33
40
 
34
- fn evaluate_surface(input : SurfaceInput) -> SurfaceData {
35
- let baseSample = textureSample(
41
+ fn evaluate_standard_surface(input : SurfaceInput, materialValue : MaterialParameters) -> SurfaceData {
42
+ var baseSample = vec4<f32>(1.0);
43
+ #ifdef BASE_COLOR_TEXTURE_AVAILABLE
44
+ if (standardUsesBaseColorTexture()) {
45
+ baseSample = textureSample(
36
46
  baseColorTexture,
37
47
  baseColorTexture_sampler,
38
- surfaceUv(input, material.baseColorTextureCoordinatesTransform, material.baseColorTextureCoordinatesMetadata) * material.baseColorTextureCoordinatesMetadata.zw,
48
+ surfaceUv(input, materialValue.baseColorTextureCoordinatesTransform, materialValue.baseColorTextureCoordinatesMetadata) * materialValue.baseColorTextureCoordinatesMetadata.zw,
39
49
  );
40
- let metallicRoughnessSample = textureSample(
50
+ }
51
+ #endif
52
+ var metallicRoughnessSample = vec4<f32>(1.0);
53
+ #ifdef METALLIC_ROUGHNESS_TEXTURE_AVAILABLE
54
+ if (standardUsesMetallicRoughnessTexture()) {
55
+ metallicRoughnessSample = textureSample(
41
56
  metallicRoughnessTexture,
42
57
  metallicRoughnessTexture_sampler,
43
- surfaceUv(input, material.metallicRoughnessTextureCoordinatesTransform, material.metallicRoughnessTextureCoordinatesMetadata) * material.metallicRoughnessTextureCoordinatesMetadata.zw,
58
+ surfaceUv(input, materialValue.metallicRoughnessTextureCoordinatesTransform, materialValue.metallicRoughnessTextureCoordinatesMetadata) * materialValue.metallicRoughnessTextureCoordinatesMetadata.zw,
44
59
  );
60
+ }
61
+ #endif
62
+ var normal = normalize(input.geometricNormalWS);
63
+ #ifdef NORMAL_TEXTURE_AVAILABLE
64
+ if (standardUsesNormalTexture()) {
45
65
  let normalSample = textureSample(
46
66
  normalTexture,
47
67
  normalTexture_sampler,
48
- surfaceUv(input, material.normalTextureCoordinatesTransform, material.normalTextureCoordinatesMetadata) * material.normalTextureCoordinatesMetadata.zw,
68
+ surfaceUv(input, materialValue.normalTextureCoordinatesTransform, materialValue.normalTextureCoordinatesMetadata) * materialValue.normalTextureCoordinatesMetadata.zw,
49
69
  );
50
- let emissiveSample = textureSample(
70
+ normal = surfaceNormal(input, normalSample, materialValue.normalScale);
71
+ }
72
+ #endif
73
+ var emissiveSample = vec4<f32>(1.0);
74
+ #ifdef EMISSIVE_TEXTURE_AVAILABLE
75
+ if (standardUsesEmissiveTexture()) {
76
+ emissiveSample = textureSample(
51
77
  emissiveTexture,
52
78
  emissiveTexture_sampler,
53
- surfaceUv(input, material.emissiveTextureCoordinatesTransform, material.emissiveTextureCoordinatesMetadata) * material.emissiveTextureCoordinatesMetadata.zw,
79
+ surfaceUv(input, materialValue.emissiveTextureCoordinatesTransform, materialValue.emissiveTextureCoordinatesMetadata) * materialValue.emissiveTextureCoordinatesMetadata.zw,
54
80
  );
55
- let occlusionSample = textureSample(
81
+ }
82
+ #endif
83
+ var occlusionSample = vec4<f32>(1.0);
84
+ #ifdef OCCLUSION_TEXTURE_AVAILABLE
85
+ if (standardUsesOcclusionTexture()) {
86
+ occlusionSample = textureSample(
56
87
  occlusionTexture,
57
88
  occlusionTexture_sampler,
58
- surfaceUv(input, material.occlusionTextureCoordinatesTransform, material.occlusionTextureCoordinatesMetadata) * material.occlusionTextureCoordinatesMetadata.zw,
89
+ surfaceUv(input, materialValue.occlusionTextureCoordinatesTransform, materialValue.occlusionTextureCoordinatesMetadata) * materialValue.occlusionTextureCoordinatesMetadata.zw,
59
90
  );
91
+ }
92
+ #endif
60
93
  let vertexColor = input.vertexColor;
61
- let baseColor = material.baseColor.rgb * baseSample.rgb * vertexColor.rgb;
94
+ let baseColor = materialValue.baseColor.rgb * baseSample.rgb * vertexColor.rgb;
62
95
  let metallic = clamp(
63
- material.metallic * surfaceChannel(metallicRoughnessSample, u32(material.metallicChannel)),
96
+ materialValue.metallic * surfaceChannel(metallicRoughnessSample, u32(materialValue.metallicChannel)),
64
97
  0.0,
65
98
  1.0,
66
99
  );
67
100
  let roughness = clamp(
68
- material.roughness * surfaceChannel(metallicRoughnessSample, u32(material.roughnessChannel)),
101
+ materialValue.roughness * surfaceChannel(metallicRoughnessSample, u32(materialValue.roughnessChannel)),
69
102
  0.04,
70
103
  1.0,
71
104
  );
72
- let emissive = material.emissive * material.emissiveIntensity * emissiveSample.rgb;
105
+ let emissive = materialValue.emissive * materialValue.emissiveIntensity * emissiveSample.rgb;
73
106
  let occlusion = clamp(
74
- 1.0 + (occlusionSample.r - 1.0) * material.occlusionStrength,
107
+ 1.0 + (occlusionSample.r - 1.0) * materialValue.occlusionStrength,
75
108
  0.0,
76
109
  1.0,
77
110
  );
78
111
  return SurfaceData(
79
112
  baseColor,
80
- surfaceNormal(input, normalSample, material.normalScale),
113
+ normal,
81
114
  metallic,
82
115
  roughness,
83
116
  emissive,
84
117
  occlusion,
85
- clamp(material.baseColor.a * baseSample.a * vertexColor.a, 0.0, 1.0),
86
- clamp(material.alphaCutoff, 0.0, 1.0),
118
+ clamp(materialValue.baseColor.a * baseSample.a * vertexColor.a, 0.0, 1.0),
119
+ clamp(materialValue.alphaCutoff, 0.0, 1.0),
87
120
  );
88
121
  }
122
+
123
+ fn evaluate_surface(input : SurfaceInput) -> SurfaceData {
124
+ return evaluate_standard_surface(input, material);
125
+ }
package/src/fxaa.wgsl CHANGED
@@ -33,13 +33,15 @@
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 display-encoded input
36
+ // @binding(0) screenTexture : texture_2d<f32> -- sampled pass-domain input
37
37
  // @binding(1) samp : sampler -- linear filterable sampler
38
38
  // @binding(2) params : FxaaParams -- final-output policy
39
- // display-encoded: FXAA samples and writes display-encoded values; the Output
40
- // Transform owns the sole linear-to-sRGB conversion before this pass. FXAA is
41
- // the final Standard surface writer, so the shared bounded dither is applied
42
- // here, immediately before the surface's UNORM8 conversion.
39
+ // DOMAIN: the positive LUT path samples and writes linear-LDR values. Its
40
+ // ditherEnabled uniform is forced to zero because the following fs_encode_only
41
+ // surface writer owns the sole OETF and final dither. The legacy direct path
42
+ // may still enable the same bounded dither when FXAA is itself the surface
43
+ // writer; that legacy route remains display-encoded. FXAA never performs an
44
+ // OETF.
43
45
 
44
46
  #import forgeax_view::common::{FullscreenOutput, ditherUnorm8}
45
47
  #import forgeax_view::common::fullscreen_triangle
@@ -53,6 +55,8 @@ const ITERATIONS: i32 = 12;
53
55
  @group(0) @binding(1) var samp: sampler;
54
56
 
55
57
  struct FxaaParams {
58
+ // Zero for graph-owned linear-LDR intermediates; non-zero is reserved for a
59
+ // direct final surface writer and never performs color-space conversion.
56
60
  ditherEnabled: f32,
57
61
  // Keep the uniform block at one 16-byte slot. WebGL2/GLES uniform-buffer
58
62
  // validation uses the std140-aligned block size, while the host uploads a
@@ -21,6 +21,22 @@
21
21
 
22
22
  #import forgeax_pbr::ibl_shared::{fresnelSchlickRoughness, inverseRotateEnvironment}
23
23
 
24
+ // Decode the renderer's Skylight/probe scalar without touching the probe box
25
+ // metadata carried in the color lanes. A negative intensity is the
26
+ // ReflectionProbe sentinel `-(probeIntensity + 1)`; regular Skylights retain
27
+ // their linear RGB tint and intensity product. Both the PBR ambient specular
28
+ // term and its detached SSR fallback consume this one scale.
29
+ fn decodeSpecularEnvironmentScale(
30
+ skyColor: vec3<f32>,
31
+ intensity: f32,
32
+ ) -> vec3<f32> {
33
+ return select(
34
+ skyColor * intensity,
35
+ vec3<f32>(max(-intensity - 1.0, 0.0)),
36
+ intensity < 0.0,
37
+ );
38
+ }
39
+
24
40
  // Sample pre-convolved irradiance from the irradiance cubemap.
25
41
  // Y is negated to compensate for WebGPU's top-left texture origin vs the
26
42
  // OpenGL convention used during equirect-to-cube render passes.
@@ -37,6 +53,18 @@ fn sampleIblDiffuse(
37
53
  }
38
54
 
39
55
  // Split-sum specular IBL: prefiltered env * (F0 * scale + bias).
56
+ fn projectSpecularRadiance(
57
+ radiance: vec3<f32>, normal: vec3<f32>, view: vec3<f32>,
58
+ roughness: f32, F0: vec3<f32>, brdfLut: texture_2d<f32>, brdfLutSampler: sampler,
59
+ ) -> vec3<f32> {
60
+ let NdotV = max(dot(normal, view), 0.001);
61
+ // The preintegrated LUT has one level; explicit LOD remains valid when the
62
+ // Standard evaluator is selected by a non-uniform particle control path.
63
+ let envBRDF = textureSampleLevel(brdfLut, brdfLutSampler, vec2<f32>(NdotV, roughness), 0.0).rg;
64
+ let F = fresnelSchlickRoughness(NdotV, F0, roughness);
65
+ return radiance * (F * envBRDF.r + envBRDF.g);
66
+ }
67
+
40
68
  fn sampleIblSpecular(
41
69
  normal: vec3<f32>,
42
70
  view: vec3<f32>,
@@ -54,9 +82,7 @@ fn sampleIblSpecular(
54
82
  let Rflip = vec3<f32>(rotated.x, -rotated.y, rotated.z);
55
83
  let mip = roughness * 4.0;
56
84
  let prefilteredColor = textureSampleLevel(prefilterMap, prefilterSampler, Rflip, mip).rgb;
57
- let envBRDF = textureSample(brdfLut, brdfLutSampler, vec2<f32>(NdotV, roughness)).rg;
58
- let F = fresnelSchlickRoughness(NdotV, F0, roughness);
59
- return prefilteredColor * (F * envBRDF.r + envBRDF.g);
85
+ return projectSpecularRadiance(prefilteredColor, normal, view, roughness, F0, brdfLut, brdfLutSampler);
60
86
  }
61
87
 
62
88
  // Project a reflection ray from a point inside a probe box onto the box
@@ -106,7 +132,5 @@ fn sampleReflectionProbeSpecular(
106
132
  let probeDirection = vec3<f32>(rotated.x, -rotated.y, rotated.z);
107
133
  let mip = roughness * 4.0;
108
134
  let prefilteredColor = textureSampleLevel(probeMap, probeSampler, probeDirection, mip).rgb;
109
- let envBRDF = textureSample(brdfLut, brdfLutSampler, vec2<f32>(NdotV, roughness)).rg;
110
- let F = fresnelSchlickRoughness(NdotV, F0, roughness);
111
- return prefilteredColor * (F * envBRDF.r + envBRDF.g);
135
+ return projectSpecularRadiance(prefilteredColor, normal, view, roughness, F0, brdfLut, brdfLutSampler);
112
136
  }
package/src/index.ts CHANGED
@@ -69,6 +69,7 @@ export {
69
69
  DEFAULT_SPRITE_PARAM_SCHEMA,
70
70
  DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
71
71
  DEFAULT_UNLIT_PARAM_SCHEMA,
72
+ PARTICLE_MESH_SURFACE_PARAM_SCHEMA,
72
73
  STANDARD_BASE_PARAM_SCHEMA,
73
74
  STANDARD_PBR_ALPHA_CUTOFF_DEFAULT,
74
75
  STANDARD_PBR_ARTIFACT_RECEIPT,
@@ -76,8 +77,10 @@ export {
76
77
  STANDARD_PHYSICAL_LAYER_PARAM_SCHEMA,
77
78
  STANDARD_PHYSICAL_TEXTURE_FIELDS,
78
79
  STANDARD_PIPELINE_PARAM_SCHEMA,
80
+ STANDARD_TEXTURE_MASK_OVERRIDE,
79
81
  type StandardPhysicalTextureField,
80
82
  standardPhysicalTextureFields,
83
+ standardTextureMask,
81
84
  } from './material-schemas.js';
82
85
  export {
83
86
  registerDefaultSpriteLit,
@@ -95,6 +98,8 @@ export {
95
98
  type ShaderRegistryDevice as ShaderCatalogDevice,
96
99
  type ShaderRegistryOptions,
97
100
  type ShaderRegistryOptions as ShaderCatalogOptions,
101
+ SSR_SHADER_MODULES,
102
+ type SsrShaderModule,
98
103
  } from './ShaderRegistry.js';
99
104
  export {
100
105
  findVariantByKey,
@@ -119,6 +124,189 @@ export const DEFAULT_STANDARD_SURFACE_MODULE =
119
124
  /** Stable build-time module id for the temporal-v1 accessor owner. */
120
125
  export const SCENE_DATA_TEMPORAL_V1_SHADER_MODULE = 'forgeax_scene_temporal' as const;
121
126
 
127
+ /** Stable build-time module id for the single Standard output encoder. */
128
+ export const STANDARD_OUTPUT_ENCODING_SHADER_MODULE = 'forgeax_view::output_encoding' as const;
129
+
130
+ /** Runtime WGSL for the fixed-cohort, same-frame auto exposure meter. */
131
+ export const AUTO_EXPOSURE_METER_WGSL = /* wgsl */ `
132
+ struct AutoExposureParameters {
133
+ compensationEv: f32,
134
+ rangeMinEv: f32,
135
+ rangeMaxEv: f32,
136
+ upRate: f32,
137
+ downRate: f32,
138
+ deltaTime: f32,
139
+ fallback: f32,
140
+ generation: f32,
141
+ };
142
+ @group(0) @binding(0) var source: texture_2d<f32>;
143
+ @group(0) @binding(1) var<storage, read_write> histogram: array<atomic<u32>>;
144
+ @group(0) @binding(2) var<storage, read_write> state: array<vec4<f32>>;
145
+ @group(0) @binding(3) var<storage, read_write> candidate: array<vec4<f32>>;
146
+ @group(0) @binding(4) var<storage, read> parameters: AutoExposureParameters;
147
+
148
+ var<workgroup> localHistogram: array<atomic<u32>, 256>;
149
+ var<workgroup> totals: array<u32, 64>;
150
+ var<workgroup> sampleTotal: u32;
151
+
152
+ fn finite(value: f32) -> bool {
153
+ // Equality rejects NaN; the magnitude bound rejects +/-infinity.
154
+ return value == value && abs(value) <= 3.402823e+37;
155
+ }
156
+
157
+ fn centerWeight(
158
+ block: vec2<u32>,
159
+ center: vec2<f32>,
160
+ inverseExtentSquared: vec2<f32>,
161
+ ) -> u32 {
162
+ let delta = vec2<f32>(block) - center;
163
+ let distanceSquared =
164
+ delta.x * delta.x * inverseExtentSquared.x +
165
+ delta.y * delta.y * inverseExtentSquared.y;
166
+ if (distanceSquared <= 0.25) { return 3u; }
167
+ if (distanceSquared <= 0.75) { return 2u; }
168
+ return 1u;
169
+ }
170
+
171
+ fn accumulateSample(
172
+ block: vec2<u32>,
173
+ sampleGrid: vec2<u32>,
174
+ dimensions: vec2<u32>,
175
+ center: vec2<f32>,
176
+ extent: vec2<f32>,
177
+ ) {
178
+ if (block.x >= sampleGrid.x || block.y >= sampleGrid.y) { return; }
179
+ let pixel = min(block * vec2<u32>(4u) + vec2<u32>(2u), dimensions - vec2<u32>(1u));
180
+ let sample = textureLoad(source, vec2<i32>(pixel), 0);
181
+ if (!finite(sample.r) || !finite(sample.g) || !finite(sample.b)) { return; }
182
+ let luminance = dot(sample.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
183
+ if (!finite(luminance) || luminance <= 0.0) { return; }
184
+ let bin = min(u32(clamp(log2(luminance) + 12.0, 0.0, 23.999) * (256.0 / 24.0)), 255u);
185
+ atomicAdd(&localHistogram[bin], centerWeight(block, center, extent));
186
+ }
187
+
188
+ @compute @workgroup_size(256, 1, 1)
189
+ fn auto_exposure_clear(@builtin(local_invocation_index) localIndex: u32) {
190
+ atomicStore(&histogram[localIndex], 0u);
191
+ }
192
+
193
+ @compute @workgroup_size(256, 1, 1)
194
+ fn auto_exposure_histogram(
195
+ @builtin(local_invocation_index) localIndex: u32,
196
+ @builtin(workgroup_id) workgroupId: vec3<u32>,
197
+ @builtin(num_workgroups) numWorkgroups: vec3<u32>,
198
+ ) {
199
+ let dimensions = textureDimensions(source);
200
+ let sampleGrid = (dimensions.xy + vec2<u32>(3u)) / vec2<u32>(4u);
201
+ let grid = vec2<f32>(sampleGrid);
202
+ let center = (grid - vec2<f32>(1.0)) * 0.5;
203
+ let extent = max(center, vec2<f32>(1.0));
204
+ let inverseExtent = 1.0 / extent;
205
+ let inverseExtentSquared = inverseExtent * inverseExtent;
206
+
207
+ // Map the 256 lanes to a 16x16 logical tile. The fixed 4x8 dispatch grid
208
+ // then uses a two-dimensional tile stride to cover every 4x4 sample block.
209
+ atomicStore(&localHistogram[localIndex], 0u);
210
+ workgroupBarrier();
211
+ let localGrid = vec2<u32>(localIndex % 16u, localIndex / 16u);
212
+ let tileSize = vec2<u32>(16u);
213
+ let blockStart = workgroupId.xy * tileSize + localGrid;
214
+ let blockStride = numWorkgroups.xy * tileSize;
215
+ for (var blockY = blockStart.y; blockY < sampleGrid.y; blockY += blockStride.y) {
216
+ for (var blockX = blockStart.x; blockX < sampleGrid.x; blockX += blockStride.x) {
217
+ accumulateSample(
218
+ vec2<u32>(blockX, blockY),
219
+ sampleGrid,
220
+ dimensions,
221
+ center,
222
+ inverseExtentSquared,
223
+ );
224
+ }
225
+ }
226
+ workgroupBarrier();
227
+
228
+ // Every lane owns one bin. All local accumulation is complete before the
229
+ // owners atomically publish their finite result to the one global 1KiB map.
230
+ let localCount = atomicLoad(&localHistogram[localIndex]);
231
+ if (localCount > 0u) {
232
+ atomicAdd(&histogram[localIndex], localCount);
233
+ }
234
+ }
235
+
236
+ @compute @workgroup_size(64, 1, 1)
237
+ fn auto_exposure_adapt(@builtin(local_invocation_index) localIndex: u32) {
238
+ var total = 0u;
239
+ for (var index = localIndex; index < 256u; index += 64u) {
240
+ total += atomicLoad(&histogram[index]);
241
+ }
242
+ totals[localIndex] = total;
243
+ workgroupBarrier();
244
+ if (localIndex == 0u) {
245
+ var reducedTotal = 0u;
246
+ for (var lane = 0u; lane < 64u; lane += 1u) {
247
+ reducedTotal += totals[lane];
248
+ }
249
+ sampleTotal = reducedTotal;
250
+ }
251
+ workgroupBarrier();
252
+ if (localIndex == 0u) {
253
+ let validParameters =
254
+ finite(parameters.compensationEv) &&
255
+ finite(parameters.rangeMinEv) &&
256
+ finite(parameters.rangeMaxEv) &&
257
+ parameters.rangeMinEv <= parameters.rangeMaxEv &&
258
+ finite(parameters.upRate) &&
259
+ finite(parameters.downRate) &&
260
+ finite(parameters.deltaTime) &&
261
+ parameters.deltaTime >= 0.0 &&
262
+ finite(parameters.fallback) &&
263
+ parameters.fallback > 0.0 &&
264
+ finite(parameters.generation);
265
+ let safeFallback = select(
266
+ 1.0,
267
+ parameters.fallback,
268
+ finite(parameters.fallback) && parameters.fallback > 0.0,
269
+ );
270
+ let safeGeneration = select(0.0, parameters.generation, finite(parameters.generation));
271
+ let lowRank = min(sampleTotal, u32(f32(sampleTotal) * 0.05));
272
+ let highRank = min(sampleTotal, max(lowRank + 1u, u32(ceil(f32(sampleTotal) * 0.95))));
273
+ var cumulative = 0u;
274
+ var clippedWeightedLog = 0.0;
275
+ var clippedTotal = 0u;
276
+ for (var index = 0u; index < 256u; index += 1u) {
277
+ let count = atomicLoad(&histogram[index]);
278
+ let begin = cumulative;
279
+ cumulative += count;
280
+ let keptBegin = max(begin, lowRank);
281
+ let keptEnd = min(cumulative, highRank);
282
+ if (keptEnd > keptBegin) {
283
+ let kept = keptEnd - keptBegin;
284
+ clippedTotal += kept;
285
+ clippedWeightedLog += f32(kept) * (-12.0 + (f32(index) + 0.5) * (24.0 / 256.0));
286
+ }
287
+ }
288
+ let averageLog = select(0.0, clippedWeightedLog / f32(clippedTotal), clippedTotal > 0u);
289
+ let measuredExposure = exp2(-averageLog) * 0.18;
290
+ let targetExposure = select(
291
+ safeFallback,
292
+ exp2(clamp(log2(max(measuredExposure, 1e-6)) + parameters.compensationEv, parameters.rangeMinEv, parameters.rangeMaxEv)),
293
+ validParameters && clippedTotal > 0u && finite(measuredExposure),
294
+ );
295
+ let previous = state[0];
296
+ let hasPrevious = validParameters && previous.y > 0.5 && previous.z == safeGeneration && previous.w == safeFallback && finite(previous.x) && previous.x > 0.0;
297
+ let current = select(safeFallback, previous.x, hasPrevious);
298
+ let rate = select(parameters.downRate, parameters.upRate, targetExposure > current);
299
+ let safeRate = select(0.0, rate, finite(rate) && rate >= 0.0);
300
+ let dt = select(0.0, parameters.deltaTime, finite(parameters.deltaTime) && parameters.deltaTime >= 0.0);
301
+ let amount = 1.0 - exp(-safeRate * dt);
302
+ let adapted = select(current, current + (targetExposure - current) * amount, validParameters && finite(targetExposure) && finite(current) && dt > 0.0 && safeRate > 0.0);
303
+ let exposure = select(safeFallback, adapted, finite(adapted) && adapted > 0.0);
304
+ candidate[0] = vec4<f32>(exposure, 1.0, safeGeneration, safeFallback);
305
+ state[0] = candidate[0];
306
+ }
307
+ }
308
+ `;
309
+
122
310
  /**
123
311
  * Material modules whose shader source and parameter contract are owned by
124
312
  * the Engine. They live in the runtime ShaderRegistry, so a Pack containing
@@ -203,6 +391,7 @@ export function createBuiltinMaterialAsset(kind: BuiltinMaterialKind): MaterialA
203
391
  ? { moduleSlots: { surface: DEFAULT_STANDARD_SURFACE_MODULE } }
204
392
  : {}),
205
393
  },
394
+ renderState: { tags: { LightMode: 'Forward' } },
206
395
  },
207
396
  ],
208
397
  parameters:
@@ -218,13 +218,12 @@ fn evalPointShadowed(
218
218
  lightPos, colorTimesIntensity, invRangeSquared,
219
219
  worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
220
220
  );
221
- // Keep the fragment-to-light vector for BRDF and nDotL. Shadow matrices are
222
- // authored light-to-fragment (`lookAt(lightPos, fragment)`), so the cube
223
- // lookup must use the opposite direction. For a right-handed world, the
224
- // cubemap convention flips Z so the +Z face look direction matches.
221
+ // The atlas uses the shared CubeCamera face directions. The raster matrices
222
+ // reflect clip-space X to preserve authored front-face winding, so sampling
223
+ // must use the corresponding raw light-to-fragment direction.
225
224
  let toLight = lightPos - worldPos;
226
225
  let fromLight = worldPos - lightPos;
227
- let lightLocal = vec3<f32>(fromLight.x, fromLight.y, -fromLight.z);
226
+ let lightLocal = fromLight;
228
227
  // Reconstruct [0,1] NDC depth from world-space distance: largest-axis
229
228
  // projection (research L0.5). Match the per-face perspective near / far
230
229
  // configured by buildPointShadowMatrices (PointLightShadow.nearPlane /
@@ -239,7 +238,7 @@ fn evalPointShadowed(
239
238
  let nDotL = max(dot(normal, normalize(toLight)), 0.0);
240
239
  let shadowFactor = sample_shadow_cube_hw2x2(
241
240
  shadowAtlas, shadowSampler, lightLocal, shadowAtlasLayer,
242
- depthRef, depthBias, normalBias, nDotL,
241
+ depthRef, normalBias, depthBias, nDotL,
243
242
  );
244
243
  return lit * shadowFactor;
245
244
  }