@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
package/src/shadow_caster.wgsl
CHANGED
|
@@ -1,43 +1,32 @@
|
|
|
1
1
|
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
2
2
|
#pragma variant_axis SKINNING_DISABLED
|
|
3
|
+
#pragma variant_axis GPU_DRIVEN_SCENE_INDEX_AVAILABLE
|
|
4
|
+
#pragma variant_axis GPU_DRIVEN_SCENE_INDEX_EXPLICIT
|
|
5
|
+
#pragma variant_axis ALPHA_MASK
|
|
3
6
|
#pragma material_slot surface
|
|
4
7
|
#define_import_path forgeax::default-shadow-caster
|
|
5
|
-
#import forgeax_material::slot::surface::{evaluate_surface}
|
|
8
|
+
#import forgeax_material::slot::surface::{evaluate_surface, evaluate_standard_surface}
|
|
6
9
|
#import forgeax_material::surface_v1::{SurfaceInput, SurfaceData}
|
|
7
10
|
#import forgeax_material::parameters::{material}
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
// feat-20260520-directional-light-shadow-mapping M1c / w9 (D-9 / AC-09):
|
|
11
|
-
// depth pass for directional shadow map. The fragment stage evaluates the
|
|
12
|
-
// selected Standard Surface so opacity/alpha-clip stays identical to Forward
|
|
13
|
-
// and Deferred (depth32float is still the only render target).
|
|
14
|
-
//
|
|
15
|
-
// feat-20260613-csm-cascaded-shadow-maps M5 / w28: per-cascade
|
|
16
|
-
// lightViewProj selection. Each cascade pass writes a different
|
|
17
|
-
// `shadowCasterCascade.index` (0..3) before encoder submit; the vertex
|
|
18
|
-
// shader reads it to pick `view.lightViewProj_A..D`. The atlas tile UV
|
|
19
|
-
// inset is already baked into each lightViewProj host-side
|
|
20
|
-
// (render-system-extract.ts), so the per-cascade viewport on the depth
|
|
21
|
-
// pass clips rasterization to the correct atlas tile while the matrix
|
|
22
|
-
// itself maps NDC straight into atlas-space [0,1]^2.
|
|
23
|
-
//
|
|
24
|
-
// Reuses:
|
|
25
|
-
// @group(0) binding(0) view : View -- common.wgsl
|
|
26
|
-
// @group(0) binding(5) shadowCasterCascade -- common.wgsl
|
|
27
|
-
// @group(2) binding(0) meshes : array<Mesh> -- common.wgsl
|
|
28
|
-
// @group(3) binding(0) instances : array<InstanceData> -- common.wgsl
|
|
29
|
-
//
|
|
30
|
-
// SKINNING_DISABLED=true consumes the ordinary 12F mesh layout; the explicit
|
|
31
|
-
// false variant consumes JOINTS_0 / WEIGHTS_0 from the same 18F layout as pbr-skin and
|
|
32
|
-
// projects the animated world-space palette result into the shadow view.
|
|
12
|
+
#import forgeax_view::common::{View, Mesh, InstanceData, ShadowCasterCascade, view, shadowCasterCascade, meshes, instances, sampleMaterialTexture}
|
|
33
13
|
|
|
34
|
-
|
|
14
|
+
// The depth pass shares the Standard Surface contract with Forward/Deferred.
|
|
15
|
+
// Alpha-mask and GPU scene-index variants add only producer-owned bindings;
|
|
16
|
+
// ordinary casters keep the same four-slot pipeline layout.
|
|
35
17
|
|
|
36
18
|
struct VsInput {
|
|
37
19
|
@location(0) position : vec3<f32>,
|
|
38
20
|
@location(1) normal : vec3<f32>,
|
|
39
21
|
@location(2) uv : vec2<f32>,
|
|
40
22
|
@location(3) tangent : vec4<f32>,
|
|
23
|
+
@location(6) uv1 : vec2<f32>,
|
|
24
|
+
@location(7) uv2 : vec2<f32>,
|
|
25
|
+
@location(8) uv3 : vec2<f32>,
|
|
26
|
+
@location(9) uv4 : vec2<f32>,
|
|
27
|
+
@location(10) uv5 : vec2<f32>,
|
|
28
|
+
@location(11) uv6 : vec2<f32>,
|
|
29
|
+
@location(12) uv7 : vec2<f32>,
|
|
41
30
|
#if SKINNING_DISABLED == false
|
|
42
31
|
@location(4) skinIndex : vec4<u32>,
|
|
43
32
|
@location(5) skinWeight : vec4<f32>,
|
|
@@ -51,9 +40,22 @@ struct VsOut {
|
|
|
51
40
|
@location(2) normalWS : vec3<f32>,
|
|
52
41
|
@location(3) tangentWS : vec4<f32>,
|
|
53
42
|
@location(4) surfaceUv : vec2<f32>,
|
|
54
|
-
@location(5)
|
|
43
|
+
@location(5) uv1 : vec2<f32>,
|
|
44
|
+
@location(6) uv2 : vec2<f32>,
|
|
45
|
+
@location(7) uv3 : vec2<f32>,
|
|
46
|
+
@location(8) uv4 : vec2<f32>,
|
|
47
|
+
@location(9) uv5 : vec2<f32>,
|
|
48
|
+
@location(10) uv6And7 : vec4<f32>,
|
|
49
|
+
#if GPU_DRIVEN_SCENE_INDEX_AVAILABLE == true
|
|
50
|
+
@location(11) @interpolate(flat) materialIndex : u32,
|
|
51
|
+
#endif
|
|
55
52
|
};
|
|
56
53
|
|
|
54
|
+
#if GPU_DRIVEN_SCENE_INDEX_AVAILABLE == true
|
|
55
|
+
@group(1) @binding(46) var<storage, read> sceneMaterials : array<MaterialParameters>;
|
|
56
|
+
@group(3) @binding(1) var<storage, read> visibleItems : array<vec2<u32>>;
|
|
57
|
+
#endif
|
|
58
|
+
|
|
57
59
|
#if SKINNING_DISABLED == false
|
|
58
60
|
#if STORAGE_BUFFER_AVAILABLE == true
|
|
59
61
|
@group(2) @binding(1) var<storage, read> palette : array<mat4x4<f32>>;
|
|
@@ -71,67 +73,102 @@ fn _cascadeLightViewProj(layer : u32) -> mat4x4<f32> {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
fn shadowVertex(in : VsInput, idx : u32) -> VsOut {
|
|
77
|
+
var meshIndex = 0u;
|
|
78
|
+
var instanceIndex = idx;
|
|
79
|
+
var materialIndex = 0u;
|
|
80
|
+
var paletteBase = 0u;
|
|
81
|
+
#if GPU_DRIVEN_SCENE_INDEX_AVAILABLE == true
|
|
82
|
+
#if GPU_DRIVEN_SCENE_INDEX_EXPLICIT == false
|
|
83
|
+
let visible = visibleItems[idx];
|
|
84
|
+
meshIndex = visible.x;
|
|
85
|
+
instanceIndex = 0u;
|
|
86
|
+
paletteBase = visible.y;
|
|
87
|
+
materialIndex = visible.y;
|
|
88
|
+
#endif
|
|
89
|
+
#endif
|
|
76
90
|
#if SKINNING_DISABLED == false
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
#if STORAGE_BUFFER_AVAILABLE == true
|
|
92
|
+
// GPU skin rows carry the material row in the Mesh temporal metadata lane.
|
|
93
|
+
materialIndex = bitcast<u32>(meshes[meshIndex].temporal.w);
|
|
94
|
+
#endif
|
|
95
|
+
let skinMatrix = palette[paletteBase + in.skinIndex.x] * in.skinWeight.x +
|
|
96
|
+
palette[paletteBase + in.skinIndex.y] * in.skinWeight.y +
|
|
97
|
+
palette[paletteBase + in.skinIndex.z] * in.skinWeight.z +
|
|
98
|
+
palette[paletteBase + in.skinIndex.w] * in.skinWeight.w;
|
|
84
99
|
let worldPos = skinMatrix * vec4<f32>(in.position, 1.0);
|
|
85
100
|
let worldNormal = normalize((skinMatrix * vec4<f32>(in.normal, 0.0)).xyz);
|
|
86
101
|
let worldTangent = normalize((skinMatrix * vec4<f32>(in.tangent.xyz, 0.0)).xyz);
|
|
87
102
|
#else
|
|
88
|
-
let
|
|
89
|
-
let worldMatrix = meshes[0].worldFromLocal * instanceLocal;
|
|
103
|
+
let worldMatrix = meshes[meshIndex].worldFromLocal * instances[instanceIndex].localFromInstance;
|
|
90
104
|
let worldPos = worldMatrix * vec4<f32>(in.position, 1.0);
|
|
91
105
|
let worldNormal = normalize((worldMatrix * vec4<f32>(in.normal, 0.0)).xyz);
|
|
92
106
|
let worldTangent = normalize((worldMatrix * vec4<f32>(in.tangent.xyz, 0.0)).xyz);
|
|
93
107
|
#endif
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// `spotLightViewProj`; directional cascade passes keep `isSpot = 0u` and read
|
|
97
|
-
// `view.lightViewProj_A..D` via `index`. Routing on the discriminant keeps
|
|
98
|
-
// the spot matrix out of the directional View UBO (no same-frame contention).
|
|
108
|
+
|
|
109
|
+
var out : VsOut;
|
|
99
110
|
if (shadowCasterCascade.isSpot == 1u) {
|
|
100
|
-
var out : VsOut;
|
|
101
111
|
out.clip = shadowCasterCascade.spotLightViewProj * worldPos;
|
|
102
|
-
|
|
103
|
-
out.
|
|
104
|
-
out.normalWS = worldNormal;
|
|
105
|
-
out.tangentWS = vec4<f32>(worldTangent, in.tangent.w);
|
|
106
|
-
out.surfaceUv = in.uv;
|
|
107
|
-
out.vertexColor = vec4<f32>(1.0);
|
|
108
|
-
return out;
|
|
112
|
+
} else {
|
|
113
|
+
out.clip = _cascadeLightViewProj(shadowCasterCascade.index) * worldPos;
|
|
109
114
|
}
|
|
110
|
-
let lvp = _cascadeLightViewProj(shadowCasterCascade.index);
|
|
111
|
-
var out : VsOut;
|
|
112
|
-
out.clip = lvp * worldPos;
|
|
113
115
|
out.positionOS = in.position;
|
|
114
116
|
out.positionWS = worldPos.xyz;
|
|
115
117
|
out.normalWS = worldNormal;
|
|
116
118
|
out.tangentWS = vec4<f32>(worldTangent, in.tangent.w);
|
|
117
119
|
out.surfaceUv = in.uv;
|
|
118
|
-
out.
|
|
120
|
+
out.uv1 = in.uv1;
|
|
121
|
+
out.uv2 = in.uv2;
|
|
122
|
+
out.uv3 = in.uv3;
|
|
123
|
+
out.uv4 = in.uv4;
|
|
124
|
+
out.uv5 = in.uv5;
|
|
125
|
+
out.uv6And7 = vec4<f32>(in.uv6, in.uv7);
|
|
126
|
+
#if GPU_DRIVEN_SCENE_INDEX_AVAILABLE == true
|
|
127
|
+
out.materialIndex = materialIndex;
|
|
128
|
+
#endif
|
|
119
129
|
return out;
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
@vertex
|
|
133
|
+
fn vs_main(in : VsInput, @builtin(instance_index) idx : u32) -> VsOut {
|
|
134
|
+
return shadowVertex(in, idx);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Scene-index shadow batches use a receipt-owned entry point so the indirect
|
|
138
|
+
// lane cannot silently reinterpret the visible stream as instance indices.
|
|
139
|
+
@vertex
|
|
140
|
+
fn vs_scene_index(in : VsInput, @builtin(instance_index) idx : u32) -> VsOut {
|
|
141
|
+
return shadowVertex(in, idx);
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
fn evaluateShadowSurface(in : VsOut, frontFacing : bool) -> SurfaceData {
|
|
123
145
|
let viewDirectionWS = normalize(view.cameraPos - in.positionWS);
|
|
124
|
-
|
|
146
|
+
let input = SurfaceInput(
|
|
125
147
|
in.positionOS,
|
|
126
148
|
in.positionWS,
|
|
127
149
|
in.normalWS,
|
|
128
150
|
in.tangentWS,
|
|
129
151
|
viewDirectionWS,
|
|
130
152
|
in.surfaceUv,
|
|
131
|
-
in.
|
|
132
|
-
in.
|
|
153
|
+
in.uv1,
|
|
154
|
+
in.uv2,
|
|
155
|
+
in.uv3,
|
|
156
|
+
in.uv4,
|
|
157
|
+
in.uv5,
|
|
158
|
+
in.uv6And7.xy,
|
|
159
|
+
in.uv6And7.zw,
|
|
160
|
+
vec4<f32>(1.0),
|
|
133
161
|
frontFacing,
|
|
134
|
-
)
|
|
162
|
+
);
|
|
163
|
+
#if GPU_DRIVEN_SCENE_INDEX_AVAILABLE == true
|
|
164
|
+
#if ALPHA_MASK == true
|
|
165
|
+
return evaluate_standard_surface(input, sceneMaterials[in.materialIndex]);
|
|
166
|
+
#else
|
|
167
|
+
return evaluate_surface(input);
|
|
168
|
+
#endif
|
|
169
|
+
#else
|
|
170
|
+
return evaluate_surface(input);
|
|
171
|
+
#endif
|
|
135
172
|
}
|
|
136
173
|
|
|
137
174
|
fn alphaTestShadowSurface(surface : SurfaceData) {
|
|
@@ -140,13 +177,14 @@ fn alphaTestShadowSurface(surface : SurfaceData) {
|
|
|
140
177
|
}
|
|
141
178
|
}
|
|
142
179
|
|
|
143
|
-
// The depth-only
|
|
144
|
-
//
|
|
180
|
+
// The depth-only pass has no color target; alpha clipping still runs through
|
|
181
|
+
// the selected Standard Surface before depth is committed.
|
|
145
182
|
@fragment
|
|
146
183
|
fn fs_shadow(in : VsOut, @builtin(front_facing) frontFacing : bool) {
|
|
147
184
|
alphaTestShadowSurface(evaluateShadowSurface(in, frontFacing));
|
|
148
185
|
}
|
|
149
186
|
|
|
187
|
+
// Keep fs_main available for material consumers that explicitly request it.
|
|
150
188
|
@fragment
|
|
151
189
|
fn fs_main(in : VsOut, @builtin(front_facing) frontFacing : bool) {
|
|
152
190
|
alphaTestShadowSurface(evaluateShadowSurface(in, frontFacing));
|
package/src/sprite-lit.wgsl
CHANGED
|
@@ -90,7 +90,7 @@ struct VsIn {
|
|
|
90
90
|
// a valid source for NDC cluster lookup; keep the perspective divide here and
|
|
91
91
|
// interpolate the result alongside worldPos.
|
|
92
92
|
struct VsOut {
|
|
93
|
-
@builtin(position) clip : vec4<f32>,
|
|
93
|
+
@builtin(position) @invariant clip : vec4<f32>,
|
|
94
94
|
@location(0) uv_atlas : vec2<f32>,
|
|
95
95
|
@location(1) worldPos : vec3<f32>,
|
|
96
96
|
@location(2) ndc : vec3<f32>,
|
|
@@ -199,7 +199,7 @@ fn spriteLitShadeAccum(
|
|
|
199
199
|
let viewDir = normalize(view.cameraPos - worldPos);
|
|
200
200
|
lit = lit + evaluateStandardClusterLights(
|
|
201
201
|
ndc, viewZ, worldPos, vec3<f32>(0.0, 0.0, 1.0),
|
|
202
|
-
viewDir, albedo, 0.0, 1.0, vec3<f32>(0.04), true,
|
|
202
|
+
viewDir, albedo, 0.0, 1.0, vec3<f32>(0.04), true, true,
|
|
203
203
|
);
|
|
204
204
|
#endif
|
|
205
205
|
return lit;
|
|
@@ -236,7 +236,7 @@ fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
struct TemporalVsOut {
|
|
239
|
-
@builtin(position) clip : vec4<f32>,
|
|
239
|
+
@builtin(position) @invariant clip : vec4<f32>,
|
|
240
240
|
@location(0) uvAtlas : vec2<f32>,
|
|
241
241
|
@location(1) @interpolate(perspective) currentClip : vec4<f32>,
|
|
242
242
|
@location(2) @interpolate(perspective) previousClip : vec4<f32>,
|
|
@@ -254,7 +254,7 @@ fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_in
|
|
|
254
254
|
#endif
|
|
255
255
|
var out : TemporalVsOut;
|
|
256
256
|
out.currentClip = view.temporalCurrentViewProj * currentWorld;
|
|
257
|
-
out.clip =
|
|
257
|
+
out.clip = view.worldViewProj * currentWorld;
|
|
258
258
|
out.previousClip = view.temporalPreviousViewProj * previousWorld;
|
|
259
259
|
out.uvAtlas = vertex.uvAtlas;
|
|
260
260
|
return out;
|
package/src/sprite.wgsl
CHANGED
|
@@ -130,7 +130,7 @@ struct VsIn {
|
|
|
130
130
|
};
|
|
131
131
|
|
|
132
132
|
struct VsOut {
|
|
133
|
-
@builtin(position) clip : vec4<f32>,
|
|
133
|
+
@builtin(position) @invariant clip : vec4<f32>,
|
|
134
134
|
// Atlas-space UV: already folded through region origin + size by the
|
|
135
135
|
// vertex stage so the fragment can sample the bound material texture at
|
|
136
136
|
// `in.uv_atlas` without any host-side region
|
|
@@ -310,7 +310,7 @@ fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
struct TemporalVsOut {
|
|
313
|
-
@builtin(position) clip : vec4<f32>,
|
|
313
|
+
@builtin(position) @invariant clip : vec4<f32>,
|
|
314
314
|
@location(0) uvAtlas : vec2<f32>,
|
|
315
315
|
@location(1) @interpolate(perspective) currentClip : vec4<f32>,
|
|
316
316
|
@location(2) @interpolate(perspective) previousClip : vec4<f32>,
|
|
@@ -328,7 +328,7 @@ fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_in
|
|
|
328
328
|
#endif
|
|
329
329
|
var out : TemporalVsOut;
|
|
330
330
|
out.currentClip = view.temporalCurrentViewProj * currentWorld;
|
|
331
|
-
out.clip =
|
|
331
|
+
out.clip = view.worldViewProj * currentWorld;
|
|
332
332
|
out.previousClip = view.temporalPreviousViewProj * previousWorld;
|
|
333
333
|
out.uvAtlas = vertex.uvAtlas;
|
|
334
334
|
return out;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#define_import_path forgeax_ssr::compose
|
|
2
|
+
|
|
3
|
+
struct ComposeVertex {
|
|
4
|
+
@builtin(position) position : vec4<f32>,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
@group(0) @binding(0) var radiance : texture_2d<f32>;
|
|
8
|
+
@group(0) @binding(1) var fallback : texture_2d<f32>;
|
|
9
|
+
@group(0) @binding(2) var response : texture_2d<f32>;
|
|
10
|
+
@group(0) @binding(3) var normalRoughness : texture_2d<f32>;
|
|
11
|
+
@group(0) @binding(4) var radianceSampler : sampler;
|
|
12
|
+
#import forgeax_view::common::View
|
|
13
|
+
@group(0) @binding(5) var<uniform> view : View;
|
|
14
|
+
|
|
15
|
+
struct SsrReceiverSample {
|
|
16
|
+
radiance : vec4<f32>,
|
|
17
|
+
coverage : f32,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
@vertex
|
|
21
|
+
fn vs_ssr_compose(@builtin(vertex_index) index : u32) -> ComposeVertex {
|
|
22
|
+
let x = select(-1.0, 3.0, index == 1u);
|
|
23
|
+
let y = select(-1.0, 3.0, index == 2u);
|
|
24
|
+
return ComposeVertex(vec4<f32>(x, y, 0.0, 1.0));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Reconstruct the half-resolution lattice without borrowing another face's
|
|
28
|
+
// ray. Renormalize compatible taps instead of rejecting the entire pixel
|
|
29
|
+
// solely because its nearest trace sample crossed a subpixel tile edge.
|
|
30
|
+
fn ssrReceiverSample(uv : vec2<f32>, normal : vec3<f32>) -> SsrReceiverSample {
|
|
31
|
+
let size = vec2<i32>(textureDimensions(radiance, 0));
|
|
32
|
+
let coordinate = uv * vec2<f32>(size) - vec2<f32>(0.5);
|
|
33
|
+
let first = vec2<i32>(floor(coordinate));
|
|
34
|
+
let fraction = fract(coordinate);
|
|
35
|
+
var sum = vec4<f32>(0.0);
|
|
36
|
+
var totalWeight = 0.0;
|
|
37
|
+
let pixel00 = clamp(first + vec2<i32>(0, 0), vec2<i32>(0), size - vec2<i32>(1));
|
|
38
|
+
let tracedNormal00 = textureLoad(normalRoughness, pixel00 * 2, 0).xyz * 2.0 - vec3<f32>(1.0);
|
|
39
|
+
let weight00 = select(1.0 - fraction.x, fraction.x, false) * select(1.0 - fraction.y, fraction.y, false)
|
|
40
|
+
* ssrReceiverWeight(normal, tracedNormal00);
|
|
41
|
+
// The presentation pyramid is premultiplied by confidence. Preserve
|
|
42
|
+
// that representation during receiver reconstruction and unpremultiply
|
|
43
|
+
// only once after the roughness filter has selected its LOD.
|
|
44
|
+
let sample00 = textureLoad(radiance, pixel00, 0);
|
|
45
|
+
sum += sample00 * weight00;
|
|
46
|
+
totalWeight += weight00;
|
|
47
|
+
|
|
48
|
+
let pixel10 = clamp(first + vec2<i32>(1, 0), vec2<i32>(0), size - vec2<i32>(1));
|
|
49
|
+
let tracedNormal10 = textureLoad(normalRoughness, pixel10 * 2, 0).xyz * 2.0 - vec3<f32>(1.0);
|
|
50
|
+
let weight10 = select(1.0 - fraction.x, fraction.x, true) * select(1.0 - fraction.y, fraction.y, false)
|
|
51
|
+
* ssrReceiverWeight(normal, tracedNormal10);
|
|
52
|
+
let sample10 = textureLoad(radiance, pixel10, 0);
|
|
53
|
+
sum += sample10 * weight10;
|
|
54
|
+
totalWeight += weight10;
|
|
55
|
+
|
|
56
|
+
let pixel01 = clamp(first + vec2<i32>(0, 1), vec2<i32>(0), size - vec2<i32>(1));
|
|
57
|
+
let tracedNormal01 = textureLoad(normalRoughness, pixel01 * 2, 0).xyz * 2.0 - vec3<f32>(1.0);
|
|
58
|
+
let weight01 = select(1.0 - fraction.x, fraction.x, false) * select(1.0 - fraction.y, fraction.y, true)
|
|
59
|
+
* ssrReceiverWeight(normal, tracedNormal01);
|
|
60
|
+
let sample01 = textureLoad(radiance, pixel01, 0);
|
|
61
|
+
sum += sample01 * weight01;
|
|
62
|
+
totalWeight += weight01;
|
|
63
|
+
|
|
64
|
+
let pixel11 = clamp(first + vec2<i32>(1, 1), vec2<i32>(0), size - vec2<i32>(1));
|
|
65
|
+
let tracedNormal11 = textureLoad(normalRoughness, pixel11 * 2, 0).xyz * 2.0 - vec3<f32>(1.0);
|
|
66
|
+
let weight11 = select(1.0 - fraction.x, fraction.x, true) * select(1.0 - fraction.y, fraction.y, true)
|
|
67
|
+
* ssrReceiverWeight(normal, tracedNormal11);
|
|
68
|
+
let sample11 = textureLoad(radiance, pixel11, 0);
|
|
69
|
+
sum += sample11 * weight11;
|
|
70
|
+
totalWeight += weight11;
|
|
71
|
+
return SsrReceiverSample(sum / max(totalWeight, 1e-6), totalWeight);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn ssrReflectionLod(roughness : f32) -> f32 {
|
|
75
|
+
return roughness * roughness * f32(textureNumLevels(radiance) - 1u);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn sampleRoughSsr(uv : vec2<f32>, roughness : f32) -> vec4<f32> {
|
|
79
|
+
let lod = ssrReflectionLod(roughness);
|
|
80
|
+
// The presentation pyramid is premultiplied by confidence, so hardware
|
|
81
|
+
// bilinear/trilinear filtering preserves coverage without the four manual
|
|
82
|
+
// loads previously needed for every low-LOD sample.
|
|
83
|
+
return textureSampleLevel(radiance, radianceSampler, uv, lod);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn ssrReceiverWeight(current : vec3<f32>, traced : vec3<f32>) -> f32 {
|
|
87
|
+
let agreement = dot(current, traced) * inverseSqrt(max(dot(current, current) * dot(traced, traced), 1e-8));
|
|
88
|
+
return smoothstep(0.9, 0.99, agreement);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@fragment
|
|
92
|
+
fn fs_ssr_compose(in : ComposeVertex) -> @location(0) vec4<f32> {
|
|
93
|
+
let pixel = vec2<i32>(in.position.xy);
|
|
94
|
+
// Trace samples full-resolution pixel 2*p, not the center of a 2x2 box.
|
|
95
|
+
let uv = (in.position.xy + vec2<f32>(0.5)) / vec2<f32>(textureDimensions(fallback, 0));
|
|
96
|
+
let receiver = textureLoad(normalRoughness, pixel, 0);
|
|
97
|
+
let environment = textureLoad(fallback, pixel, 0);
|
|
98
|
+
let roughnessLimit = clamp(view.ssrParams.z, 0.0, 1.0);
|
|
99
|
+
if (!(view.ssrParams.w > 0.5 && view.ssrParams.w < 3.402823e+38) ||
|
|
100
|
+
!(receiver.a >= 0.0 && receiver.a < roughnessLimit) || environment.a <= 0.0) {
|
|
101
|
+
return vec4<f32>(0.0);
|
|
102
|
+
}
|
|
103
|
+
let reconstructed = ssrReceiverSample(uv, receiver.xyz * 2.0 - vec3<f32>(1.0));
|
|
104
|
+
if (reconstructed.coverage <= 1e-6) { return vec4<f32>(0.0); }
|
|
105
|
+
let base = reconstructed.radiance;
|
|
106
|
+
let roughness = clamp(receiver.a, 0.04, 1.0);
|
|
107
|
+
let lod = ssrReflectionLod(roughness);
|
|
108
|
+
var filtered = base;
|
|
109
|
+
if (lod < 1.0) {
|
|
110
|
+
// Preserve the normal-compatible receiver reconstruction at the sharp
|
|
111
|
+
// end while using one hardware-filtered read for the first roughness mip.
|
|
112
|
+
// This keeps the transition continuous at lod == 1 and avoids the four
|
|
113
|
+
// explicit footprint loads used by the old mip helper.
|
|
114
|
+
if (textureNumLevels(radiance) > 1u) {
|
|
115
|
+
filtered = mix(base, textureSampleLevel(radiance, radianceSampler, uv, 1.0), lod);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
filtered = sampleRoughSsr(uv, roughness);
|
|
119
|
+
}
|
|
120
|
+
let sample = vec4<f32>(filtered.rgb / max(filtered.a, 1e-6), filtered.a);
|
|
121
|
+
let materialResponse = textureLoad(response, pixel, 0).rgb;
|
|
122
|
+
// Receiver admission is independent of the central mirror ray. A rough
|
|
123
|
+
// lobe can contain a visible wall even when that one ray misses its edge.
|
|
124
|
+
// Preserve the current material's cutoff instead of borrowing a neighbor's
|
|
125
|
+
// roughness policy from the filtered reflection pyramid.
|
|
126
|
+
let receiverConfidence = 1.0 - smoothstep(roughnessLimit * 0.8, roughnessLimit, receiver.a);
|
|
127
|
+
let confidence = clamp(min(receiverConfidence, sample.a), 0.0, 1.0) * environment.a;
|
|
128
|
+
return vec4<f32>(confidence * (sample.rgb * materialResponse - environment.rgb), 0.0);
|
|
129
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#define_import_path forgeax_ssr::hiz_reduce
|
|
2
|
+
|
|
3
|
+
// Hi-Z reduction consumes the previous r32float mip. It is deliberately a
|
|
4
|
+
// separate module from the depth seed: WebGPU depth sample types cannot bind
|
|
5
|
+
// an r32float view, while the seed must retain the depth-only binding.
|
|
6
|
+
const SSR_HIZ_EMPTY_DEPTH : f32 = 3.402823e+38;
|
|
7
|
+
|
|
8
|
+
@group(0) @binding(0) var sourceHiZ : texture_2d<f32>;
|
|
9
|
+
@group(0) @binding(1) var hizOutput : texture_storage_2d<r32float, write>;
|
|
10
|
+
|
|
11
|
+
fn isFinite(value : f32) -> bool {
|
|
12
|
+
return value == value && abs(value) < 3.402823e+38;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn normalizeSsrHiZDepth(value : f32) -> f32 {
|
|
16
|
+
return select(SSR_HIZ_EMPTY_DEPTH, value, isFinite(value) && value > 0.0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Physical WebGPU mip dimensions are floor-halved. Partition the source by
|
|
20
|
+
// integer normalized boundaries and ceil the end boundary so adjacent
|
|
21
|
+
// footprints overlap at odd boundaries; a 3-wide source reduced to one texel
|
|
22
|
+
// must inspect all three source texels rather than a clamped 2x2 prefix.
|
|
23
|
+
fn reduceSsrHiZFootprint(
|
|
24
|
+
destinationCoordinate : vec2<u32>,
|
|
25
|
+
sourceSize : vec2<u32>,
|
|
26
|
+
destinationSize : vec2<u32>,
|
|
27
|
+
) -> f32 {
|
|
28
|
+
let sourceStart = destinationCoordinate * sourceSize / destinationSize;
|
|
29
|
+
let sourceEnd = min(
|
|
30
|
+
((destinationCoordinate + vec2<u32>(1u)) * sourceSize + destinationSize -
|
|
31
|
+
vec2<u32>(1u)) / destinationSize,
|
|
32
|
+
sourceSize,
|
|
33
|
+
);
|
|
34
|
+
var minimum = SSR_HIZ_EMPTY_DEPTH;
|
|
35
|
+
for (var y = sourceStart.y; y < sourceEnd.y; y += 1u) {
|
|
36
|
+
for (var x = sourceStart.x; x < sourceEnd.x; x += 1u) {
|
|
37
|
+
let depth = normalizeSsrHiZDepth(textureLoad(sourceHiZ, vec2<i32>(vec2<u32>(x, y)), 0).r);
|
|
38
|
+
minimum = min(minimum, depth);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return minimum;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@compute @workgroup_size(8, 8, 1)
|
|
45
|
+
fn ssr_hiz_reduce(@builtin(global_invocation_id) globalId : vec3<u32>) {
|
|
46
|
+
let sourceSize = textureDimensions(sourceHiZ, 0);
|
|
47
|
+
let destinationSize = textureDimensions(hizOutput);
|
|
48
|
+
if (globalId.x >= destinationSize.x || globalId.y >= destinationSize.y) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let reduced = reduceSsrHiZFootprint(globalId.xy, sourceSize, destinationSize);
|
|
52
|
+
textureStore(hizOutput, vec2<i32>(globalId.xy), vec4<f32>(reduced, 0.0, 0.0, 1.0));
|
|
53
|
+
}
|
package/src/ssr-hiz.wgsl
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#define_import_path forgeax_ssr::hiz
|
|
2
|
+
|
|
3
|
+
// The SSR Hi-Z producer uses one r32float view for each graph mip.
|
|
4
|
+
const SSR_HIZ_FORMAT : u32 = 1u;
|
|
5
|
+
const SSR_HIZ_EMPTY_DEPTH : f32 = 3.402823e+38;
|
|
6
|
+
|
|
7
|
+
@group(0) @binding(0) var sourceDepth : texture_depth_2d;
|
|
8
|
+
@group(0) @binding(1) var hizOutput : texture_storage_2d<r32float, write>;
|
|
9
|
+
#import forgeax_view::common::View
|
|
10
|
+
@group(0) @binding(2) var<uniform> view : View;
|
|
11
|
+
|
|
12
|
+
fn isFinite(value : f32) -> bool {
|
|
13
|
+
return value == value && abs(value) < 3.402823e+38;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// WebGPU depth attachments expose perspective depth, not a linear distance.
|
|
17
|
+
// Hi-Z stores the positive view distance consumed by the bounded SSR ray
|
|
18
|
+
// marcher. The shared View UBO carries the active projection range, so this
|
|
19
|
+
// conversion does not create a second camera or projection owner.
|
|
20
|
+
fn linearizeSsrDepth(value : f32) -> f32 {
|
|
21
|
+
if (!isFinite(value) || value <= 0.0 || value >= 1.0) {
|
|
22
|
+
return SSR_HIZ_EMPTY_DEPTH;
|
|
23
|
+
}
|
|
24
|
+
let near = view.temporalProjection.x;
|
|
25
|
+
let far = view.temporalProjection.y;
|
|
26
|
+
if (!isFinite(near) || !isFinite(far) || near <= 0.0 || far <= near) {
|
|
27
|
+
return SSR_HIZ_EMPTY_DEPTH;
|
|
28
|
+
}
|
|
29
|
+
let orthographic = view.temporalProjection.z > 0.5;
|
|
30
|
+
let distance = select(
|
|
31
|
+
(near * far) / max(far - value * (far - near), 1e-5),
|
|
32
|
+
near + value * (far - near),
|
|
33
|
+
orthographic,
|
|
34
|
+
);
|
|
35
|
+
return select(SSR_HIZ_EMPTY_DEPTH, distance, isFinite(distance) && distance > 0.0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@compute @workgroup_size(8, 8, 1)
|
|
39
|
+
fn ssr_hiz_seed(@builtin(global_invocation_id) globalId : vec3<u32>) {
|
|
40
|
+
let sourceSize = textureDimensions(sourceDepth, 0);
|
|
41
|
+
let outputSize = textureDimensions(hizOutput);
|
|
42
|
+
if (any(globalId.xy >= outputSize)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Match the reduction coverage contract: floor the start and ceil the end
|
|
46
|
+
// so an odd source boundary is conservatively present in both neighbors.
|
|
47
|
+
let first = globalId.xy * sourceSize / outputSize;
|
|
48
|
+
let end = min(
|
|
49
|
+
((globalId.xy + vec2<u32>(1u)) * sourceSize + outputSize - vec2<u32>(1u)) /
|
|
50
|
+
outputSize,
|
|
51
|
+
sourceSize,
|
|
52
|
+
);
|
|
53
|
+
var nearestDepth = 1.0;
|
|
54
|
+
for (var y = first.y; y < end.y; y++) {
|
|
55
|
+
for (var x = first.x; x < end.x; x++) {
|
|
56
|
+
let sample = textureLoad(sourceDepth, vec2<i32>(vec2<u32>(x, y)), 0);
|
|
57
|
+
if (isFinite(sample) && sample > 0.0 && sample < 1.0) {
|
|
58
|
+
nearestDepth = min(nearestDepth, sample);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Both supported depth projections are monotonic, so linearize once after
|
|
63
|
+
// selecting the nearest valid source sample.
|
|
64
|
+
let depth = linearizeSsrDepth(nearestDepth);
|
|
65
|
+
textureStore(hizOutput, vec2<i32>(globalId.xy), vec4<f32>(depth, 0.0, 0.0, 1.0));
|
|
66
|
+
}
|