@forgeax/engine-shader 0.1.28 → 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
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createStandardPbrArtifactReceipt } from '../material/artifact-types.js';
3
+
4
+ const BASE_VERTEX_INPUTS = [
5
+ { semantic: 'position', location: 0, format: 'float32x3' },
6
+ { semantic: 'normal', location: 1, format: 'float32x3' },
7
+ { semantic: 'uv', location: 2, format: 'float32x2' },
8
+ { semantic: 'tangent', location: 3, format: 'float32x4' },
9
+ ] as const;
10
+
11
+ const SKIN_VERTEX_INPUTS = [
12
+ { semantic: 'skinIndex', location: 4, format: 'uint16x4' },
13
+ { semantic: 'skinWeight', location: 5, format: 'float32x4' },
14
+ ] as const;
15
+
16
+ const COLOR_VERTEX_INPUT = { semantic: 'color', location: 13, format: 'float32x4' } as const;
17
+
18
+ const CASES = [
19
+ {
20
+ name: 'rigid uncolored',
21
+ skinned: false,
22
+ vertexColorAvailable: false,
23
+ vertexInputs: BASE_VERTEX_INPUTS,
24
+ },
25
+ {
26
+ name: 'rigid colored',
27
+ skinned: false,
28
+ vertexColorAvailable: true,
29
+ vertexInputs: [...BASE_VERTEX_INPUTS, COLOR_VERTEX_INPUT],
30
+ },
31
+ {
32
+ name: 'skinned uncolored',
33
+ skinned: true,
34
+ vertexColorAvailable: false,
35
+ vertexInputs: [...BASE_VERTEX_INPUTS, ...SKIN_VERTEX_INPUTS],
36
+ },
37
+ {
38
+ name: 'skinned colored',
39
+ skinned: true,
40
+ vertexColorAvailable: true,
41
+ vertexInputs: [...BASE_VERTEX_INPUTS, ...SKIN_VERTEX_INPUTS, COLOR_VERTEX_INPUT],
42
+ },
43
+ ] as const;
44
+
45
+ describe('Standard PBR artifact receipt vertex ABI', () => {
46
+ it.each(CASES)('publishes the $name input sequence in location order', (testCase) => {
47
+ const receipt = createStandardPbrArtifactReceipt(
48
+ testCase.skinned,
49
+ testCase.vertexColorAvailable,
50
+ );
51
+
52
+ expect(receipt.vertexInputs).toEqual(testCase.vertexInputs);
53
+ expect(receipt.reflection.vertexInputs).toEqual(testCase.vertexInputs);
54
+ expect(receipt.vertexInputs.map((input) => input.location)).toEqual(
55
+ testCase.vertexInputs.map((input) => input.location),
56
+ );
57
+ expect(receipt.skinPaletteAddress).toEqual(
58
+ testCase.skinned ? { group: 2, binding: 1, stride: 64 } : undefined,
59
+ );
60
+ });
61
+
62
+ it('shares material row and resource ABI across all rigid and skinned color variants', () => {
63
+ const receipts = CASES.map((testCase) =>
64
+ createStandardPbrArtifactReceipt(testCase.skinned, testCase.vertexColorAvailable),
65
+ );
66
+ const [baseline, ...variants] = receipts;
67
+
68
+ for (const receipt of variants) {
69
+ expect(receipt.materialRow).toEqual(baseline.materialRow);
70
+ expect(receipt.resourceSlots).toEqual(baseline.resourceSlots);
71
+ expect(receipt.reflection.resourceSlots).toEqual(baseline.reflection.resourceSlots);
72
+ expect(receipt.uvSets).toEqual(baseline.uvSets);
73
+ expect(receipt.alphaMask).toEqual(baseline.alphaMask);
74
+ }
75
+ });
76
+
77
+ it('separates colored and uncolored identities while retaining rigid and skin compatibility', () => {
78
+ const rigid = createStandardPbrArtifactReceipt(false, false);
79
+ const rigidColor = createStandardPbrArtifactReceipt(false, true);
80
+ const skin = createStandardPbrArtifactReceipt(true, false);
81
+ const skinColor = createStandardPbrArtifactReceipt(true, true);
82
+
83
+ expect(rigid.receiptIdentity).toBe('standard-pbr/material-row-v2');
84
+ expect(rigid.reflection.layoutIdentity).toBe(rigid.receiptIdentity);
85
+ expect(skin.receiptIdentity).toBe(rigid.receiptIdentity);
86
+ expect(skin.reflection.layoutIdentity).toBe(skin.receiptIdentity);
87
+ expect(rigidColor.receiptIdentity).toBe('standard-pbr/material-row-v2/vertex-color');
88
+ expect(rigidColor.reflection.layoutIdentity).toBe(rigidColor.receiptIdentity);
89
+ expect(skinColor.receiptIdentity).toBe(rigidColor.receiptIdentity);
90
+ expect(skinColor.reflection.layoutIdentity).toBe(skinColor.receiptIdentity);
91
+ expect(new Set([rigid.receiptIdentity, rigidColor.receiptIdentity])).toHaveLength(2);
92
+ });
93
+ });
@@ -21,16 +21,28 @@ describe('Standard Surface pass evaluation', () => {
21
21
  for (const entry of ['fs_main', 'fs_gbuffer']) {
22
22
  const body = entryBody(shader, entry);
23
23
  expect(body, `${name}:${entry} must exist`).toContain(`fn ${entry}`);
24
- expect(body, `${name}:${entry}`).toMatch(
25
- /(?:evaluateStandardSurface|evaluate_surface|forgeax_evaluate_surface_from_fragments)\s*\(/,
26
- );
24
+ expect(
25
+ body.match(
26
+ /(?:evaluateStandardSurface|evaluate_surface|forgeax_evaluate_surface_from_fragments)\s*\(/g,
27
+ ),
28
+ `${name}:${entry}`,
29
+ ).toHaveLength(1);
27
30
  expect(body).toContain('alphaTestSurface(surface)');
28
31
  if (entry === 'fs_main') expect(body).toContain('surface.opacity');
29
32
  expect(body).toContain('surface.baseColor');
30
33
  expect(body).toContain('surface.normalWS');
31
34
  expect(body).toContain('surface.metallic');
32
35
  expect(body).toContain('surface.roughness');
33
- expect(body).toContain('surface.emissive');
36
+ if (entry === 'fs_gbuffer' && name === 'default-standard-pbr.wgsl') {
37
+ // The rigid SSR G-buffer stores the material's unit-radiance response.
38
+ // Emissive stays in fs_main; skin remains outside SSR admission.
39
+ expect(body).toContain('out.specular_response_ao');
40
+ expect(body).toContain('projectSpecularRadiance(vec3<f32>(1.0)');
41
+ expect(body).toContain('standardSurfaceF0(in, surface)');
42
+ expect(body).not.toContain('surface.emissive');
43
+ } else {
44
+ expect(body).toContain('surface.emissive');
45
+ }
34
46
  expect(body).toContain('surface.occlusion');
35
47
  }
36
48
  }
@@ -19,6 +19,12 @@ const inputFields = [
19
19
  ['viewDirectionWS', 'vec3<f32>'],
20
20
  ['uv0', 'vec2<f32>'],
21
21
  ['uv1', 'vec2<f32>'],
22
+ ['uv2', 'vec2<f32>'],
23
+ ['uv3', 'vec2<f32>'],
24
+ ['uv4', 'vec2<f32>'],
25
+ ['uv5', 'vec2<f32>'],
26
+ ['uv6', 'vec2<f32>'],
27
+ ['uv7', 'vec2<f32>'],
22
28
  ['vertexColor', 'vec4<f32>'],
23
29
  ['frontFacing', 'bool'],
24
30
  ] as const;
@@ -7,15 +7,18 @@ const pbrTemporal = readFileSync(resolve(import.meta.dirname, '../pbr-temporal.w
7
7
  const sceneTemporal = readFileSync(resolve(import.meta.dirname, '../scene-temporal.wgsl'), 'utf8');
8
8
 
9
9
  describe('taa-resolve.wgsl', () => {
10
- it('keeps the fixed packed temporal and two-attachment ABI', () => {
10
+ it('preserves receiver motion/depth, merges secondary reactivity and stores stability separately', () => {
11
11
  expect(source).toContain('struct TaaResolveParams');
12
12
  expect(source).toContain('@location(0) color');
13
13
  expect(source).toContain('@location(1) temporal');
14
+ expect(source).toContain('@location(2) stability');
14
15
  expect(source).toContain('return TaaResolveOutput(');
15
16
  expect(source).toContain('temporal,');
16
17
  expect(source).toContain('current.a');
17
18
  expect(source).toContain('1.0 - clamp(temporal.w');
18
19
  expect(source).not.toContain('1.0 - clamp(current.a');
20
+ expect(source).toContain('vec4<f32>(sceneTemporal.xyz,');
21
+ expect(source).toContain('max(sceneTemporal.w, sampleSecondaryReactivity(currentUv))');
19
22
  });
20
23
 
21
24
  it('contains every v1 rejection and adaptive weighting term', () => {
@@ -24,14 +27,18 @@ describe('taa-resolve.wgsl', () => {
24
27
  expect(source).toContain('closestCurrentTemporal(pixel, dimensions)');
25
28
  expect(source).toContain('historyInBounds');
26
29
  expect(source).toContain('depthDelta > depthThreshold');
27
- expect(source).toContain('neighborhoodSquareMean');
28
- expect(source).toContain('clamp(rgbToYCoCg(history.rgb), clipMin, clipMax)');
30
+ expect(source).toContain('taaNeighborhood');
31
+ expect(source).toContain('clamp(rgbToYCoCg(history), clipMin, clipMax)');
29
32
  expect(source).toContain('reactiveFactor');
30
33
  expect(source).toContain('velocityFactor');
31
- expect(source).toContain('depthFactor');
32
- expect(source).toContain('lumaFactor');
33
- expect(source).toContain('mix(0.88, 0.97, lumaFactor)');
34
- expect(source).toContain('params.temporalFrameIndex < 8u');
34
+ expect(source).not.toContain('depthFactor');
35
+ expect(source).toContain('progressiveWeight * reactiveFactor * velocityFactor');
36
+ expect(source).not.toContain('unbiasedLumaDelta');
37
+ expect(source).toContain('blendTaaHistory(current.rgb, clippedHistoryRgb, historyWeight)');
38
+ expect(source).toContain('taaAccumulationWeight(params.temporalFrameIndex, steadyWeight)');
39
+ expect(source).toContain(
40
+ 'mix(0.95, 0.99, smoothstep(64.0, TAA_HISTORY_SETTLE_FRAMES, stableAge))',
41
+ );
35
42
  expect(source).toContain('progressiveWeight');
36
43
  expect(source).toContain('let dimensions = vec2<i32>(textureDimensions(currentColor, 0));');
37
44
  expect(source).toContain('closestCurrentTemporal(pixel, dimensions)');
@@ -63,7 +70,7 @@ ${pbrTemporal.replace(/^#define_import_path.*$/gm, '').replace(/^#import.*$/gm,
63
70
  @fragment
64
71
  fn fs_temporal_probe() -> @location(0) vec4<f32> {
65
72
  return projectPbrSceneTemporal(
66
- 0.75, 0.5, colorTexture, colorSampler,
73
+ 0.75, 0.5, true, colorTexture, colorSampler,
67
74
  vec4<f32>(0.0, 0.0, 1.0, 1.0), vec4<f32>(0.0),
68
75
  vec4<f32>(0.0, 0.0, 1.0, 1.0), vec4<f32>(0.0),
69
76
  vec4<f32>(0.0, 0.0, 0.0, 0.0), 0.0,
@@ -74,7 +81,11 @@ fn fs_temporal_probe() -> @location(0) vec4<f32> {
74
81
  `;
75
82
  const compiled = await compileShader(entry, {
76
83
  id: 'test::pbr-temporal-reactive',
77
- defines: { PER_INSTANCE_REGION: false, STORAGE_BUFFER_AVAILABLE: true },
84
+ defines: {
85
+ PER_INSTANCE_REGION: false,
86
+ STORAGE_BUFFER_AVAILABLE: true,
87
+ BASE_COLOR_TEXTURE_AVAILABLE: true,
88
+ },
78
89
  });
79
90
  expect(compiled.ok, compiled.ok ? undefined : String(compiled.error)).toBe(true);
80
91
  if (!compiled.ok) return;
@@ -8,7 +8,12 @@ describe('standard transmission thickness scale contract', () => {
8
8
  it('converts local authored thickness through the combined local-to-world basis', () => {
9
9
  expect(shader).toContain('@location(4) @interpolate(flat) transmissionBasis0 : vec4<f32>');
10
10
  expect(shader).toContain('@location(13) @interpolate(flat) transmissionBasis1 : vec4<f32>');
11
- expect(shader).not.toContain('@location(15)');
11
+ // Scene-index Standard uses location 15 for its flat material-row index
12
+ // when transmission is disabled. Keep this assertion scoped to the
13
+ // transmission ABI: it must not claim that the unrelated scene-index
14
+ // varying is absent from the shared source template.
15
+ expect(shader).toMatch(/@location\(15\)\s+@interpolate\(flat\)\s+materialIndex\s+:\s+u32/);
16
+ expect(shader).not.toMatch(/@location\(15\)[^\n]*thickness/i);
12
17
  expect(shader).toContain('let localToWorld0 = in.transmissionBasis0.xyz;');
13
18
  expect(shader).toContain('let localToWorld1 = vec3<f32>(');
14
19
  expect(shader).toContain('let localToWorld2 = vec3<f32>(');
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { describe, expect, it } from 'vitest';
4
4
 
5
- const shader = readFileSync(resolve(import.meta.dirname, '../default-standard-pbr.wgsl'), 'utf8');
5
+ const shader = readFileSync(resolve(import.meta.dirname, '../standard-surface.wgsl'), 'utf8');
6
6
 
7
7
  describe('transparent PBR color domain contract', () => {
8
8
  it('mixes transparent source and destination in the linear domain', () => {
@@ -42,7 +42,10 @@ describe('M4 vertex-color shader contract', () => {
42
42
  expect(text).toMatch(/baseColor\.rgb\s*\*\s*texSample\.rgb\s*\*\s*vertexColor\.rgb/);
43
43
  expect(text).toMatch(/baseColor\.a\s*\*\s*texSample\.a\s*\*\s*vertexColor\.a/);
44
44
  } else {
45
- expect(text).toContain('#import forgeax_material::slot::surface::{evaluate_surface}');
45
+ expect(text).toContain(
46
+ '#import forgeax_material::slot::surface::{evaluate_surface, evaluate_standard_surface}',
47
+ );
48
+ expect(text).toContain('materialVertexColor(in), frontFacing');
46
49
  }
47
50
  expect(text).toMatch(/alphaCutoff/);
48
51
  expect(text).toMatch(/fs_temporal/);
@@ -48,9 +48,9 @@ fn atmosphere_sun_disc_radiance(
48
48
 
49
49
  @vertex
50
50
  fn atmosphere_background_vs(@builtin(vertex_index) vertexIndex: u32) -> FullscreenOutput {
51
- // Keep the fullscreen primitive at the far plane. The render graph binds
52
- // the existing depth authority read-only, so fixed-function less-equal
53
- // rejects background fragments covered by geometry without sampling depth.
51
+ // Initialize the far-plane background before scene draws. The shared
52
+ // geometry depth authority preserves occlusion and transparent blending;
53
+ // background never samples depth or overwrites a later foreground.
54
54
  var output = fullscreen_triangle(vertexIndex);
55
55
  output.position.z = 1.0;
56
56
  return output;
@@ -0,0 +1,179 @@
1
+ // Auto exposure meter module. Runtime uses the cooked WGSL string directly.
2
+ // The three entry points execute as ordered dispatches in one meter pass.
3
+ struct AutoExposureParameters {
4
+ compensationEv: f32,
5
+ rangeMinEv: f32,
6
+ rangeMaxEv: f32,
7
+ upRate: f32,
8
+ downRate: f32,
9
+ deltaTime: f32,
10
+ fallback: f32,
11
+ generation: f32,
12
+ };
13
+
14
+ @group(0) @binding(0) var source: texture_2d<f32>;
15
+ @group(0) @binding(1) var<storage, read_write> histogram: array<atomic<u32>>;
16
+ @group(0) @binding(2) var<storage, read_write> state: array<vec4<f32>>;
17
+ @group(0) @binding(3) var<storage, read_write> candidate: array<vec4<f32>>;
18
+ @group(0) @binding(4) var<storage, read> parameters: AutoExposureParameters;
19
+
20
+ var<workgroup> localHistogram: array<atomic<u32>, 256>;
21
+ var<workgroup> totals: array<u32, 64>;
22
+ var<workgroup> sampleTotal: u32;
23
+
24
+ fn finite(value: f32) -> bool {
25
+ // Equality rejects NaN; the magnitude bound rejects +/-infinity.
26
+ return value == value && abs(value) <= 3.402823e+37;
27
+ }
28
+
29
+ fn centerWeight(
30
+ block: vec2<u32>,
31
+ center: vec2<f32>,
32
+ inverseExtentSquared: vec2<f32>,
33
+ ) -> u32 {
34
+ let delta = vec2<f32>(block) - center;
35
+ let distanceSquared =
36
+ delta.x * delta.x * inverseExtentSquared.x +
37
+ delta.y * delta.y * inverseExtentSquared.y;
38
+ if (distanceSquared <= 0.25) { return 3u; }
39
+ if (distanceSquared <= 0.75) { return 2u; }
40
+ return 1u;
41
+ }
42
+
43
+ fn accumulateSample(
44
+ block: vec2<u32>,
45
+ sampleGrid: vec2<u32>,
46
+ dimensions: vec2<u32>,
47
+ center: vec2<f32>,
48
+ extent: vec2<f32>,
49
+ ) {
50
+ if (block.x >= sampleGrid.x || block.y >= sampleGrid.y) { return; }
51
+ let pixel = min(block * vec2<u32>(4u) + vec2<u32>(2u), dimensions - vec2<u32>(1u));
52
+ let sample = textureLoad(source, vec2<i32>(pixel), 0);
53
+ if (!finite(sample.r) || !finite(sample.g) || !finite(sample.b)) { return; }
54
+ let luminance = dot(sample.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
55
+ if (!finite(luminance) || luminance <= 0.0) { return; }
56
+ let bin = min(u32(clamp(log2(luminance) + 12.0, 0.0, 23.999) * (256.0 / 24.0)), 255u);
57
+ atomicAdd(&localHistogram[bin], centerWeight(block, center, extent));
58
+ }
59
+
60
+ @compute @workgroup_size(256, 1, 1)
61
+ fn auto_exposure_clear(@builtin(local_invocation_index) localIndex: u32) {
62
+ atomicStore(&histogram[localIndex], 0u);
63
+ }
64
+
65
+ @compute @workgroup_size(256, 1, 1)
66
+ fn auto_exposure_histogram(
67
+ @builtin(local_invocation_index) localIndex: u32,
68
+ @builtin(workgroup_id) workgroupId: vec3<u32>,
69
+ @builtin(num_workgroups) numWorkgroups: vec3<u32>,
70
+ ) {
71
+ let dimensions = textureDimensions(source);
72
+ let sampleGrid = (dimensions.xy + vec2<u32>(3u)) / vec2<u32>(4u);
73
+ let grid = vec2<f32>(sampleGrid);
74
+ let center = (grid - vec2<f32>(1.0)) * 0.5;
75
+ let extent = max(center, vec2<f32>(1.0));
76
+ let inverseExtent = 1.0 / extent;
77
+ let inverseExtentSquared = inverseExtent * inverseExtent;
78
+
79
+ // Map the 256 lanes to a 16x16 logical tile. The fixed 4x8 dispatch grid
80
+ // then uses a two-dimensional tile stride to cover every 4x4 sample block.
81
+ atomicStore(&localHistogram[localIndex], 0u);
82
+ workgroupBarrier();
83
+ let localGrid = vec2<u32>(localIndex % 16u, localIndex / 16u);
84
+ let tileSize = vec2<u32>(16u);
85
+ let blockStart = workgroupId.xy * tileSize + localGrid;
86
+ let blockStride = numWorkgroups.xy * tileSize;
87
+ for (var blockY = blockStart.y; blockY < sampleGrid.y; blockY += blockStride.y) {
88
+ for (var blockX = blockStart.x; blockX < sampleGrid.x; blockX += blockStride.x) {
89
+ accumulateSample(
90
+ vec2<u32>(blockX, blockY),
91
+ sampleGrid,
92
+ dimensions,
93
+ center,
94
+ inverseExtentSquared,
95
+ );
96
+ }
97
+ }
98
+ workgroupBarrier();
99
+
100
+ // Every lane owns one bin. All local accumulation is complete before the
101
+ // owners atomically publish their finite result to the one global 1KiB map.
102
+ let localCount = atomicLoad(&localHistogram[localIndex]);
103
+ if (localCount > 0u) {
104
+ atomicAdd(&histogram[localIndex], localCount);
105
+ }
106
+ }
107
+
108
+ @compute @workgroup_size(64, 1, 1)
109
+ fn auto_exposure_adapt(@builtin(local_invocation_index) localIndex: u32) {
110
+ var total = 0u;
111
+ for (var index = localIndex; index < 256u; index += 64u) {
112
+ total += atomicLoad(&histogram[index]);
113
+ }
114
+ totals[localIndex] = total;
115
+ workgroupBarrier();
116
+ if (localIndex == 0u) {
117
+ var reducedTotal = 0u;
118
+ for (var lane = 0u; lane < 64u; lane += 1u) {
119
+ reducedTotal += totals[lane];
120
+ }
121
+ sampleTotal = reducedTotal;
122
+ }
123
+ workgroupBarrier();
124
+ if (localIndex == 0u) {
125
+ let validParameters =
126
+ finite(parameters.compensationEv) &&
127
+ finite(parameters.rangeMinEv) &&
128
+ finite(parameters.rangeMaxEv) &&
129
+ parameters.rangeMinEv <= parameters.rangeMaxEv &&
130
+ finite(parameters.upRate) &&
131
+ finite(parameters.downRate) &&
132
+ finite(parameters.deltaTime) &&
133
+ parameters.deltaTime >= 0.0 &&
134
+ finite(parameters.fallback) &&
135
+ parameters.fallback > 0.0 &&
136
+ finite(parameters.generation);
137
+ let safeFallback = select(
138
+ 1.0,
139
+ parameters.fallback,
140
+ finite(parameters.fallback) && parameters.fallback > 0.0,
141
+ );
142
+ let safeGeneration = select(0.0, parameters.generation, finite(parameters.generation));
143
+ let lowRank = min(sampleTotal, u32(f32(sampleTotal) * 0.05));
144
+ let highRank = min(sampleTotal, max(lowRank + 1u, u32(ceil(f32(sampleTotal) * 0.95))));
145
+ var cumulative = 0u;
146
+ var clippedWeightedLog = 0.0;
147
+ var clippedTotal = 0u;
148
+ for (var index = 0u; index < 256u; index += 1u) {
149
+ let count = atomicLoad(&histogram[index]);
150
+ let begin = cumulative;
151
+ cumulative += count;
152
+ let keptBegin = max(begin, lowRank);
153
+ let keptEnd = min(cumulative, highRank);
154
+ if (keptEnd > keptBegin) {
155
+ let kept = keptEnd - keptBegin;
156
+ clippedTotal += kept;
157
+ clippedWeightedLog += f32(kept) * (-12.0 + (f32(index) + 0.5) * (24.0 / 256.0));
158
+ }
159
+ }
160
+ let averageLog = select(0.0, clippedWeightedLog / f32(clippedTotal), clippedTotal > 0u);
161
+ let measuredExposure = exp2(-averageLog) * 0.18;
162
+ let targetExposure = select(
163
+ safeFallback,
164
+ exp2(clamp(log2(max(measuredExposure, 1e-6)) + parameters.compensationEv, parameters.rangeMinEv, parameters.rangeMaxEv)),
165
+ validParameters && clippedTotal > 0u && finite(measuredExposure),
166
+ );
167
+ let previous = state[0];
168
+ let hasPrevious = validParameters && previous.y > 0.5 && previous.z == safeGeneration && previous.w == safeFallback && finite(previous.x) && previous.x > 0.0;
169
+ let current = select(safeFallback, previous.x, hasPrevious);
170
+ let rate = select(parameters.downRate, parameters.upRate, targetExposure > current);
171
+ let safeRate = select(0.0, rate, finite(rate) && rate >= 0.0);
172
+ let dt = select(0.0, parameters.deltaTime, finite(parameters.deltaTime) && parameters.deltaTime >= 0.0);
173
+ let amount = 1.0 - exp(-safeRate * dt);
174
+ let adapted = select(current, current + (targetExposure - current) * amount, validParameters && finite(targetExposure) && finite(current) && dt > 0.0 && safeRate > 0.0);
175
+ let exposure = select(safeFallback, adapted, finite(adapted) && adapted > 0.0);
176
+ candidate[0] = vec4<f32>(exposure, 1.0, safeGeneration, safeFallback);
177
+ state[0] = candidate[0];
178
+ }
179
+ }
@@ -0,0 +1,17 @@
1
+ // Standard output 3D LUT sampling. The sampler is fixed to clamp and linear.
2
+
3
+ struct ColorLutParams {
4
+ strength: f32,
5
+ };
6
+
7
+ fn sampleColorLut(
8
+ source: texture_3d<f32>,
9
+ lutSampler: sampler,
10
+ color: vec3<f32>,
11
+ params: ColorLutParams,
12
+ ) -> vec4<f32> {
13
+ let clamped = clamp(color, vec3<f32>(0.0), vec3<f32>(1.0));
14
+ let sampled = textureSampleLevel(source, lutSampler, clamped, 0.0);
15
+ let amount = clamp(params.strength, 0.0, 1.0);
16
+ return vec4<f32>(mix(color, sampled.rgb, amount), sampled.a);
17
+ }
package/src/common.wgsl CHANGED
@@ -183,6 +183,9 @@ struct View {
183
183
  // Accepted camera origin at the previous submitted frame. This occupies
184
184
  // the existing tail padding and keeps temporal reprojection on one SSOT.
185
185
  temporalPreviousCameraPos : vec4<f32>,
186
+ // SSR authoring facts share the fixed View UBO tail. x/y/z are the validated
187
+ // maxDistance/thickness/maxRoughness values; w is a finite enabled sentinel.
188
+ ssrParams : vec4<f32>,
186
189
  };
187
190
 
188
191
  // Per-instance mesh slot (feat-20260518-pbr-direct-lighting-mvp M2 / w8.5,
@@ -292,10 +295,9 @@ struct DirectLightSlot {
292
295
  // Per-light shadow params (binding 6):
293
296
  // `array<vec4<f32>, 4>` carrying the URP-side proj constants for cube
294
297
  // depth-ref reconstruction (research L0.5 + L1.13). Each lane stores the
295
- // shader-side reconstruction constants for one shadow-casting point light;
296
- // slot N matches `PointLight.shadowAtlasLayer = N`. HDRP rides the same
297
- // constants on direct-light metadata so binding 6 stays unbound on
298
- // the HDRP path.
298
+ // (near, far, depthBias, normalBias) for one shadow-casting point light;
299
+ // slot N matches `PointLight.shadowAtlasLayer = N`. Direct and clustered
300
+ // Standard consumers share this exact binding and authored bias payload.
299
301
  #ifdef POINT_SHADOW_AVAILABLE
300
302
  @group(0) @binding(5) var shadowAtlas : texture_depth_cube_array;
301
303
  @group(0) @binding(6) var<uniform> shadowParams : array<vec4<f32>, 4>;
@@ -383,7 +385,7 @@ struct ShadowCasterCascade {
383
385
  @group(0) @binding(15) var<uniform> cookieMatrices : array<mat4x4<f32>, 32>;
384
386
  #endif
385
387
  #ifdef PROJECTOR_AVAILABLE
386
- #ifndef EXTENDED_LIGHTING_AVAILABLE
388
+ #if EXTENDED_LIGHTING_AVAILABLE == false
387
389
  // Optional authored SpotLight projector. Surface and volume bind the same
388
390
  // accepted TextureAsset view and linear-clamp sampler; an unprojected spot
389
391
  // receives the renderer-owned white fallback. The declaration is capability