@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.
- package/NOTICE +35 -0
- package/README.md +39 -0
- package/dist/ShaderRegistry.d.ts +8 -0
- package/dist/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +312 -71
- package/dist/index.mjs.map +1 -1
- package/dist/material/artifact-types.d.ts +12 -2
- package/dist/material/artifact-types.d.ts.map +1 -1
- package/dist/material-schemas.d.ts +5 -0
- package/dist/material-schemas.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/ShaderRegistry.ts +10 -0
- package/src/__tests__/auto-exposure-graph.unit.test.ts +59 -0
- package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -15
- package/src/__tests__/builtin-texture-sampling-contract.test.ts +1 -1
- package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +2 -1
- package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +167 -6
- package/src/__tests__/direct-light-layout.unit.test.ts +1 -1
- package/src/__tests__/lighting-punctual.unit.test.ts +29 -1
- package/src/__tests__/ltc-provenance.unit.test.ts +13 -2
- package/src/__tests__/material-builtins.unit.test.ts +1 -0
- package/src/__tests__/material-contract.unit.test.ts +42 -9
- package/src/__tests__/material-derived-builtins.integration.test.ts +18 -2
- package/src/__tests__/probe-lighting-composition.unit.test.ts +1 -1
- package/src/__tests__/reflection-probe-sampling.unit.test.ts +23 -2
- package/src/__tests__/scene-temporal.unit.test.ts +10 -0
- package/src/__tests__/shader.unit.test.ts +1 -0
- package/src/__tests__/sprite-variants.unit.test.ts +1 -0
- package/src/__tests__/ssr-artifact.unit.test.ts +141 -0
- package/src/__tests__/ssr-bgl.integration.test.ts +185 -0
- package/src/__tests__/standard-output-domain.unit.test.ts +36 -0
- package/src/__tests__/standard-pbr-artifact-receipt.unit.test.ts +93 -0
- package/src/__tests__/standard-surface-pass-evaluation.integration.test.ts +16 -4
- package/src/__tests__/surface-v1.unit.test.ts +6 -0
- package/src/__tests__/taa-resolve.unit.test.ts +20 -9
- package/src/__tests__/transmission-thickness-scale.unit.test.ts +6 -1
- package/src/__tests__/transparent-pbr.unit.test.ts +1 -1
- package/src/__tests__/vertex-color-variant.unit.test.ts +4 -1
- package/src/atmosphere-background.wgsl +3 -3
- package/src/auto-exposure-meter.wgsl +179 -0
- package/src/color-lut.wgsl +17 -0
- package/src/common.wgsl +7 -5
- package/src/default-standard-pbr-skin.wgsl +142 -40
- package/src/default-standard-pbr.wgsl +198 -82
- package/src/default_standard_surface.wgsl +56 -19
- package/src/fxaa.wgsl +9 -5
- package/src/ibl-sampling.wgsl +30 -6
- package/src/index.ts +189 -0
- package/src/lighting-punctual.wgsl +5 -6
- package/src/material/artifact-types.ts +109 -63
- package/src/material-schemas.ts +84 -8
- package/src/output-encoding.wgsl +10 -0
- package/src/pbr-temporal.wgsl +9 -1
- package/src/scene-temporal.wgsl +2 -0
- package/src/shadow-pcf.wgsl +6 -8
- package/src/shadow-surface.wgsl +16 -0
- package/src/shadow_caster.wgsl +99 -61
- package/src/sprite-lit.wgsl +4 -4
- package/src/sprite.wgsl +3 -3
- package/src/ssr-compose.wgsl +129 -0
- package/src/ssr-hiz-reduce.wgsl +53 -0
- package/src/ssr-hiz.wgsl +66 -0
- package/src/ssr-temporal.wgsl +305 -0
- package/src/ssr-trace.wgsl +784 -0
- package/src/standard-cluster.wgsl +6 -4
- package/src/standard-surface.wgsl +696 -0
- package/src/surface_v1.wgsl +6 -0
- package/src/taa-resolve.wgsl +222 -28
- package/src/tbn.wgsl +1 -1
- package/src/tonemap.wgsl +37 -18
- 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(
|
|
25
|
-
|
|
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
|
-
|
|
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('
|
|
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('
|
|
28
|
-
expect(source).toContain('clamp(rgbToYCoCg(history
|
|
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('
|
|
33
|
-
expect(source).toContain('
|
|
34
|
-
expect(source).toContain('
|
|
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: {
|
|
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
|
-
|
|
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, '../
|
|
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(
|
|
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
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
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
|
-
//
|
|
296
|
-
// slot N matches `PointLight.shadowAtlasLayer = N`.
|
|
297
|
-
//
|
|
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
|
-
#
|
|
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
|