@forgeax/engine-shader 0.1.20 → 0.1.21
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/README.md +10 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +52 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/ShaderRegistry.ts +65 -0
- package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -4
- package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +7 -0
- package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +69 -24
- package/src/__tests__/lighting-point-spot-volume.unit.test.ts +8 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +32 -6
- package/src/__tests__/material-derived-builtins.integration.test.ts +27 -0
- package/src/__tests__/reflection-probe-sampling.unit.test.ts +23 -0
- package/src/__tests__/scene-temporal.unit.test.ts +20 -0
- package/src/__tests__/shader.unit.test.ts +34 -13
- package/src/__tests__/shadow-pcss.unit.test.ts +131 -0
- package/src/__tests__/sprite-lit-shader.test.ts +24 -11
- package/src/__tests__/sprite-variants.unit.test.ts +1 -0
- package/src/__tests__/transmission-thickness-scale.unit.test.ts +7 -1
- package/src/__tests__/transparent-pbr.unit.test.ts +7 -0
- package/src/common.wgsl +42 -12
- package/src/default-standard-pbr-skin.wgsl +30 -71
- package/src/default-standard-pbr.wgsl +92 -163
- package/src/fxaa.wgsl +27 -6
- package/src/ibl-sampling.wgsl +54 -0
- package/src/index.ts +6 -0
- package/src/lighting-directional.wgsl +157 -18
- package/src/lighting-punctual.wgsl +51 -21
- package/src/scene-temporal.wgsl +14 -8
- package/src/shadow-pcf.wgsl +44 -1
- package/src/sprite-lit.wgsl +36 -52
- package/src/{hdrp-cluster-forward.wgsl → standard-cluster.wgsl} +73 -60
- package/src/tonemap.wgsl +8 -3
- package/src/volume/volume-inject.wgsl +14 -2
- package/src/volume/volume-integrate.wgsl +1 -6
package/src/fxaa.wgsl
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// FXAA 3.11 fragment shader -- faithful port of the canonical Simon
|
|
8
8
|
// Rodriguez reference (same lineage as Bevy's WGSL port), adapted to
|
|
9
9
|
// forgeax naga_oil conventions. Hardcoded "High" quality preset: 12-step
|
|
10
|
-
// edge search with compile-time step multipliers (
|
|
11
|
-
// branches).
|
|
10
|
+
// edge search with compile-time step multipliers (the only UBO field is the
|
|
11
|
+
// final-output dither switch; there are no quality #ifdef branches).
|
|
12
12
|
//
|
|
13
13
|
// Vertex stage: imports the SSOT fullscreen_triangle() from
|
|
14
14
|
// forgeax_view::common (single large triangle, research Finding 1).
|
|
@@ -35,10 +35,13 @@
|
|
|
35
35
|
// Bindings (group 0):
|
|
36
36
|
// @binding(0) screenTexture : texture_2d<f32> -- sampled display-encoded input
|
|
37
37
|
// @binding(1) samp : sampler -- linear filterable sampler
|
|
38
|
+
// @binding(2) params : FxaaParams -- final-output policy
|
|
38
39
|
// display-encoded: FXAA samples and writes display-encoded values; the Output
|
|
39
|
-
// Transform owns the sole linear-to-sRGB conversion before this pass.
|
|
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.
|
|
40
43
|
|
|
41
|
-
#import forgeax_view::common::FullscreenOutput
|
|
44
|
+
#import forgeax_view::common::{FullscreenOutput, ditherUnorm8}
|
|
42
45
|
#import forgeax_view::common::fullscreen_triangle
|
|
43
46
|
|
|
44
47
|
const EDGE_THRESHOLD_MIN: f32 = 0.0312;
|
|
@@ -49,6 +52,18 @@ const ITERATIONS: i32 = 12;
|
|
|
49
52
|
@group(0) @binding(0) var screenTexture: texture_2d<f32>;
|
|
50
53
|
@group(0) @binding(1) var samp: sampler;
|
|
51
54
|
|
|
55
|
+
struct FxaaParams {
|
|
56
|
+
ditherEnabled: f32,
|
|
57
|
+
// Keep the uniform block at one 16-byte slot. WebGL2/GLES uniform-buffer
|
|
58
|
+
// validation uses the std140-aligned block size, while the host uploads a
|
|
59
|
+
// four-f32 (16-byte) payload.
|
|
60
|
+
_pad0: f32,
|
|
61
|
+
_pad1: f32,
|
|
62
|
+
_pad2: f32,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
@group(0) @binding(2) var<uniform> params: FxaaParams;
|
|
66
|
+
|
|
52
67
|
fn rgb2luma(rgb: vec3<f32>) -> f32 {
|
|
53
68
|
return sqrt(dot(rgb, vec3<f32>(0.299, 0.587, 0.114)));
|
|
54
69
|
}
|
|
@@ -105,7 +120,10 @@ fn fs_main(in: FullscreenOutput) -> @location(0) vec4<f32> {
|
|
|
105
120
|
|
|
106
121
|
// Not on an edge (or in a near-uniform region): pass the centre through.
|
|
107
122
|
if lumaRange < max(EDGE_THRESHOLD_MIN, lumaMax * EDGE_THRESHOLD_MAX) {
|
|
108
|
-
return vec4<f32>(
|
|
123
|
+
return vec4<f32>(
|
|
124
|
+
select(centerColor, ditherUnorm8(centerColor, in.position.xy), params.ditherEnabled > 0.5),
|
|
125
|
+
1.0,
|
|
126
|
+
);
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
// Lumas of the 4 corners.
|
|
@@ -254,5 +272,8 @@ fn fs_main(in: FullscreenOutput) -> @location(0) vec4<f32> {
|
|
|
254
272
|
}
|
|
255
273
|
|
|
256
274
|
let finalColor = sampleColor(finalUv);
|
|
257
|
-
return vec4<f32>(
|
|
275
|
+
return vec4<f32>(
|
|
276
|
+
select(finalColor, ditherUnorm8(finalColor, in.position.xy), params.ditherEnabled > 0.5),
|
|
277
|
+
1.0,
|
|
278
|
+
);
|
|
258
279
|
}
|
package/src/ibl-sampling.wgsl
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
// - sampleIblDiffuse(N, irradianceMap, irradianceSampler)
|
|
17
17
|
// - sampleIblSpecular(N, V, roughness, F0, prefilterMap, prefilterSampler,
|
|
18
18
|
// brdfLut, brdfLutSampler)
|
|
19
|
+
// - box_project(worldPosition, direction, boxCenter, boxExtents)
|
|
20
|
+
// - sampleReflectionProbeSpecular(..., probeMap, probeSampler, ...)
|
|
19
21
|
|
|
20
22
|
#import forgeax_pbr::ibl_shared::{PI, fresnelSchlickRoughness, inverseRotateEnvironment}
|
|
21
23
|
|
|
@@ -56,3 +58,55 @@ fn sampleIblSpecular(
|
|
|
56
58
|
let F = fresnelSchlickRoughness(NdotV, F0, roughness);
|
|
57
59
|
return prefilteredColor * (F * envBRDF.r + envBRDF.g);
|
|
58
60
|
}
|
|
61
|
+
|
|
62
|
+
// Project a reflection ray from a point inside a probe box onto the box
|
|
63
|
+
// boundary. The helper is binding-free so the scene table and the material
|
|
64
|
+
// resource projection remain owned by the host record stage.
|
|
65
|
+
fn box_project(
|
|
66
|
+
worldPosition: vec3<f32>,
|
|
67
|
+
direction: vec3<f32>,
|
|
68
|
+
boxCenter: vec3<f32>,
|
|
69
|
+
boxExtents: vec3<f32>,
|
|
70
|
+
) -> vec3<f32> {
|
|
71
|
+
let safeExtents = max(boxExtents, vec3<f32>(0.0001));
|
|
72
|
+
let safeDirection = select(
|
|
73
|
+
vec3<f32>(0.0001),
|
|
74
|
+
direction,
|
|
75
|
+
abs(direction) >= vec3<f32>(0.0001),
|
|
76
|
+
);
|
|
77
|
+
let localPosition = worldPosition - boxCenter;
|
|
78
|
+
let edgeSign = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
|
|
79
|
+
let edge = edgeSign * safeExtents;
|
|
80
|
+
let distances = (edge - localPosition) / safeDirection;
|
|
81
|
+
let travel = min(distances.x, min(distances.y, distances.z));
|
|
82
|
+
return normalize(localPosition + direction * max(travel, 0.0));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Split-sum probe sample. The probe texture is supplied by the caller so a
|
|
86
|
+
// missing scene candidate can keep using sampleIblSpecular with the Skylight
|
|
87
|
+
// resources already present in the Standard material layout.
|
|
88
|
+
fn sampleReflectionProbeSpecular(
|
|
89
|
+
normal: vec3<f32>,
|
|
90
|
+
view: vec3<f32>,
|
|
91
|
+
roughness: f32,
|
|
92
|
+
F0: vec3<f32>,
|
|
93
|
+
worldPosition: vec3<f32>,
|
|
94
|
+
boxCenter: vec3<f32>,
|
|
95
|
+
boxExtents: vec3<f32>,
|
|
96
|
+
rotation: vec4<f32>,
|
|
97
|
+
probeMap: texture_cube<f32>,
|
|
98
|
+
probeSampler: sampler,
|
|
99
|
+
brdfLut: texture_2d<f32>,
|
|
100
|
+
brdfLutSampler: sampler,
|
|
101
|
+
) -> vec3<f32> {
|
|
102
|
+
let NdotV = max(dot(normal, view), 0.001);
|
|
103
|
+
let reflection = reflect(-view, normal);
|
|
104
|
+
let projected = box_project(worldPosition, reflection, boxCenter, boxExtents);
|
|
105
|
+
let rotated = inverseRotateEnvironment(projected, rotation);
|
|
106
|
+
let probeDirection = vec3<f32>(rotated.x, -rotated.y, rotated.z);
|
|
107
|
+
let mip = roughness * 4.0;
|
|
108
|
+
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);
|
|
112
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -108,6 +108,12 @@ export const ENGINE_MATERIAL_MODULES = [
|
|
|
108
108
|
'forgeax_material::sprite-lit',
|
|
109
109
|
] as const;
|
|
110
110
|
|
|
111
|
+
/** Names of the zero-binding Standard reflection-probe shader helpers. */
|
|
112
|
+
export const REFLECTION_PROBE_SHADER_HELPERS = Object.freeze([
|
|
113
|
+
'box_project',
|
|
114
|
+
'sampleReflectionProbeSpecular',
|
|
115
|
+
] as const);
|
|
116
|
+
|
|
111
117
|
export function isEngineMaterialModule(module: string): boolean {
|
|
112
118
|
return (ENGINE_MATERIAL_MODULES as readonly string[]).includes(module);
|
|
113
119
|
}
|
|
@@ -48,14 +48,38 @@
|
|
|
48
48
|
#import forgeax_view::common::{view, shadowMap, shadowSampler}
|
|
49
49
|
#import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
|
|
50
50
|
#import forgeax_pbr::shadow_pcf::{sample_shadow_2d}
|
|
51
|
+
#ifdef DIRECTIONAL_PCSS_AVAILABLE
|
|
52
|
+
#import forgeax_pbr::shadow_pcf::{shadow_biased_receiver_depth, shadow_clamp_texel_to_tile, shadow_load_raw_depth, shadow_sample_compare}
|
|
53
|
+
#endif
|
|
51
54
|
|
|
52
55
|
// feat-20260621-learn-render-5-3-production-shadow-demos M0 / AC-14:
|
|
53
56
|
// compile-time upper bound on the PCF half-extent so the WGSL tap loops keep
|
|
54
57
|
// a constant trip count (no dynamic loop bounds / shader variants). half=2
|
|
55
|
-
// covers
|
|
56
|
-
// the runtime radius
|
|
58
|
+
// covers the PCF1/3/5 profiles -> {1,9,25} taps; the profile carrier selects
|
|
59
|
+
// the runtime radius while keeping the receiver variant-free.
|
|
57
60
|
const MAX_PCF_HALF : u32 = 2u;
|
|
58
61
|
|
|
62
|
+
#ifdef DIRECTIONAL_PCSS_AVAILABLE
|
|
63
|
+
const PCSS_MEDIUM_RAW_TAPS : u32 = 8u;
|
|
64
|
+
const PCSS_MEDIUM_COMPARE_TAPS : u32 = 16u;
|
|
65
|
+
const PCSS_HIGH_RAW_TAPS : u32 = 16u;
|
|
66
|
+
const PCSS_HIGH_COMPARE_TAPS : u32 = 32u;
|
|
67
|
+
|
|
68
|
+
// Cascade-local disk points are fixed, bounded, and shared by raw and compare
|
|
69
|
+
// phases. A rotation derived from the integer receiver texel and cascade keeps
|
|
70
|
+
// the pattern stable without a temporal input.
|
|
71
|
+
const PCSS_DISK_OFFSETS : array<vec2<f32>, 32> = array<vec2<f32>, 32>(
|
|
72
|
+
vec2<f32>(-0.326, -0.945), vec2<f32>(0.236, -0.873), vec2<f32>(0.891, -0.404), vec2<f32>(-0.761, -0.581),
|
|
73
|
+
vec2<f32>(0.612, 0.146), vec2<f32>(-0.148, 0.514), vec2<f32>(-0.527, -0.109), vec2<f32>(0.074, 0.911),
|
|
74
|
+
vec2<f32>(-0.944, 0.238), vec2<f32>(0.444, -0.736), vec2<f32>(0.707, 0.641), vec2<f32>(-0.184, -0.342),
|
|
75
|
+
vec2<f32>(0.318, 0.382), vec2<f32>(-0.638, 0.526), vec2<f32>(0.955, -0.083), vec2<f32>(-0.401, 0.816),
|
|
76
|
+
vec2<f32>(-0.083, -0.632), vec2<f32>(0.539, -0.262), vec2<f32>(-0.735, -0.168), vec2<f32>(0.162, 0.719),
|
|
77
|
+
vec2<f32>(-0.841, 0.003), vec2<f32>(0.791, 0.332), vec2<f32>(-0.291, -0.791), vec2<f32>(0.021, -0.224),
|
|
78
|
+
vec2<f32>(0.386, 0.799), vec2<f32>(-0.558, 0.134), vec2<f32>(0.638, -0.555), vec2<f32>(-0.189, 0.957),
|
|
79
|
+
vec2<f32>(-0.977, -0.117), vec2<f32>(0.271, 0.589), vec2<f32>(0.819, -0.719), vec2<f32>(-0.472, -0.409),
|
|
80
|
+
);
|
|
81
|
+
#endif
|
|
82
|
+
|
|
59
83
|
// Source: Three.js r184 src/nodes/functions/BSDF/DFGLUT.js. The full 16x16
|
|
60
84
|
// RG16F table is decoded to exact f32 constants here; the lookup below mirrors
|
|
61
85
|
// the source DataTexture's linear filtering and clamp-to-edge sampling without
|
|
@@ -226,8 +250,128 @@ fn _atlasTileOrigin(layer : u32, count : u32) -> vec2<f32> {
|
|
|
226
250
|
return vec2<f32>(tile) / vec2<f32>(grid);
|
|
227
251
|
}
|
|
228
252
|
|
|
253
|
+
#ifdef DIRECTIONAL_PCSS_AVAILABLE
|
|
254
|
+
fn _pcssDiskRotation(texel : vec2<i32>, layer : u32) -> f32 {
|
|
255
|
+
let hash = u32(texel.x) * 1664525u + u32(texel.y) * 1013904223u + (layer + 1u) * 374761393u;
|
|
256
|
+
return f32(hash % 6283u) * 0.001;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn _pcssDiskPoint(index : u32, angle : f32, radius : f32) -> vec2<f32> {
|
|
260
|
+
let point = PCSS_DISK_OFFSETS[index];
|
|
261
|
+
let cs = cos(angle);
|
|
262
|
+
let sn = sin(angle);
|
|
263
|
+
return vec2<f32>(point.x * cs - point.y * sn, point.x * sn + point.y * cs) * radius;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn _pcssProjectedSample(
|
|
267
|
+
shadowMapSize : vec2<u32>,
|
|
268
|
+
tileOrigin : vec2<u32>,
|
|
269
|
+
tileSize : vec2<u32>,
|
|
270
|
+
baseTexel : vec2<i32>,
|
|
271
|
+
offset : vec2<f32>,
|
|
272
|
+
) -> vec2<f32> {
|
|
273
|
+
let texel = shadow_clamp_texel_to_tile(
|
|
274
|
+
baseTexel + vec2<i32>(round(offset)),
|
|
275
|
+
vec2<i32>(tileOrigin),
|
|
276
|
+
vec2<i32>(tileSize),
|
|
277
|
+
1,
|
|
278
|
+
);
|
|
279
|
+
return (vec2<f32>(texel) + vec2<f32>(0.5)) / vec2<f32>(shadowMapSize);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fn _samplePcssForCascade(
|
|
283
|
+
worldPos : vec3<f32>,
|
|
284
|
+
layer : u32,
|
|
285
|
+
count : u32,
|
|
286
|
+
normal : vec3<f32>,
|
|
287
|
+
l : vec3<f32>,
|
|
288
|
+
profile : u32,
|
|
289
|
+
) -> f32 {
|
|
290
|
+
let lightClip = _cascadeLightViewProj(layer) * vec4<f32>(worldPos, 1.0);
|
|
291
|
+
if (!(lightClip.w > 0.0 || lightClip.w < 0.0)) {
|
|
292
|
+
return 1.0;
|
|
293
|
+
}
|
|
294
|
+
let projCoords = lightClip.xyz / lightClip.w;
|
|
295
|
+
let tileUv = vec2<f32>(projCoords.x * 0.5 + 0.5, -projCoords.y * 0.5 + 0.5);
|
|
296
|
+
if (!(tileUv.x >= 0.0 && tileUv.x <= 1.0 && tileUv.y >= 0.0 && tileUv.y <= 1.0 && projCoords.z >= 0.0 && projCoords.z <= 1.0)) {
|
|
297
|
+
return 1.0;
|
|
298
|
+
}
|
|
299
|
+
let nDotL = dot(normal, l);
|
|
300
|
+
if (!(nDotL >= -1.0 && nDotL <= 1.0)) {
|
|
301
|
+
return 1.0;
|
|
302
|
+
}
|
|
303
|
+
let shadowMapSize = textureDimensions(shadowMap, 0);
|
|
304
|
+
let grid = _atlasTileGrid(count);
|
|
305
|
+
let tileSize = shadowMapSize / grid;
|
|
306
|
+
let tileOrigin = vec2<u32>(layer % grid.x, layer / grid.x) * tileSize;
|
|
307
|
+
let baseTexel = vec2<i32>(tileOrigin) + vec2<i32>(floor(tileUv * vec2<f32>(tileSize)));
|
|
308
|
+
let angle = _pcssDiskRotation(baseTexel, layer);
|
|
309
|
+
let biasedDepth = shadow_biased_receiver_depth(
|
|
310
|
+
projCoords.z,
|
|
311
|
+
view.normalBias,
|
|
312
|
+
view.depthBias,
|
|
313
|
+
nDotL,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
let rawRadius = select(2.0, 3.0, profile == 5u);
|
|
317
|
+
var blockerDepth = 0.0;
|
|
318
|
+
var blockerCount = 0u;
|
|
319
|
+
if (profile == 4u) {
|
|
320
|
+
for (var i = 0u; i < PCSS_MEDIUM_RAW_TAPS; i = i + 1u) {
|
|
321
|
+
let rawUv = _pcssProjectedSample(shadowMapSize, tileOrigin, tileSize, baseTexel, _pcssDiskPoint(i, angle, rawRadius));
|
|
322
|
+
let rawTexel = vec2<i32>(rawUv * vec2<f32>(shadowMapSize) - vec2<f32>(0.5));
|
|
323
|
+
let rawDepth = shadow_load_raw_depth(shadowMap, rawTexel);
|
|
324
|
+
if (rawDepth >= 0.0 && rawDepth < biasedDepth) {
|
|
325
|
+
blockerDepth = blockerDepth + rawDepth;
|
|
326
|
+
blockerCount = blockerCount + 1u;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
for (var i = 0u; i < PCSS_HIGH_RAW_TAPS; i = i + 1u) {
|
|
331
|
+
let rawUv = _pcssProjectedSample(shadowMapSize, tileOrigin, tileSize, baseTexel, _pcssDiskPoint(i, angle, rawRadius));
|
|
332
|
+
let rawTexel = vec2<i32>(rawUv * vec2<f32>(shadowMapSize) - vec2<f32>(0.5));
|
|
333
|
+
let rawDepth = shadow_load_raw_depth(shadowMap, rawTexel);
|
|
334
|
+
if (rawDepth >= 0.0 && rawDepth < biasedDepth) {
|
|
335
|
+
blockerDepth = blockerDepth + rawDepth;
|
|
336
|
+
blockerCount = blockerCount + 1u;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (blockerCount == 0u) {
|
|
341
|
+
return 1.0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let averageBlockerDepth = blockerDepth / f32(blockerCount);
|
|
345
|
+
let lightDepthWorldSpan = max(view.splitPlanes[layer].z, 0.0);
|
|
346
|
+
let worldUnitsPerTexel = max(view.splitPlanes[layer].y, 0.0);
|
|
347
|
+
if (!(lightDepthWorldSpan > 0.0 && worldUnitsPerTexel > 0.0 && view.directionalShadowFilter.y >= 0.0 && view.directionalShadowFilter.z >= 0.0)) {
|
|
348
|
+
return 1.0;
|
|
349
|
+
}
|
|
350
|
+
let worldDistance = max(0.0, biasedDepth - averageBlockerDepth) * lightDepthWorldSpan;
|
|
351
|
+
let penumbraTexels = clamp(
|
|
352
|
+
worldDistance * tan(view.directionalShadowFilter.y) / worldUnitsPerTexel,
|
|
353
|
+
0.0,
|
|
354
|
+
view.directionalShadowFilter.z,
|
|
355
|
+
);
|
|
356
|
+
let compareRadius = max(0.5, penumbraTexels);
|
|
357
|
+
var litSum = 0.0;
|
|
358
|
+
if (profile == 4u) {
|
|
359
|
+
for (var i = 0u; i < PCSS_MEDIUM_COMPARE_TAPS; i = i + 1u) {
|
|
360
|
+
let compareUv = _pcssProjectedSample(shadowMapSize, tileOrigin, tileSize, baseTexel, _pcssDiskPoint(i, angle, compareRadius));
|
|
361
|
+
litSum = litSum + shadow_sample_compare(shadowMap, shadowSampler, compareUv, biasedDepth);
|
|
362
|
+
}
|
|
363
|
+
return litSum / f32(PCSS_MEDIUM_COMPARE_TAPS);
|
|
364
|
+
}
|
|
365
|
+
for (var i = 0u; i < PCSS_HIGH_COMPARE_TAPS; i = i + 1u) {
|
|
366
|
+
let compareUv = _pcssProjectedSample(shadowMapSize, tileOrigin, tileSize, baseTexel, _pcssDiskPoint(i, angle, compareRadius));
|
|
367
|
+
litSum = litSum + shadow_sample_compare(shadowMap, shadowSampler, compareUv, biasedDepth);
|
|
368
|
+
}
|
|
369
|
+
return litSum / f32(PCSS_HIGH_COMPARE_TAPS);
|
|
370
|
+
}
|
|
371
|
+
#endif
|
|
372
|
+
|
|
229
373
|
// Sample the shadow atlas with the LO 3.1.3 slope-scaled bias + dynamic PCF
|
|
230
|
-
// kernel (driven by
|
|
374
|
+
// kernel (driven by the directional filter profile, MAX_PCF_HALF=2),
|
|
231
375
|
// against the lightViewProj for the chosen cascade. The shader maps NDC
|
|
232
376
|
// xy to that cascade's atlas tile in fragment space (matrix carries
|
|
233
377
|
// clip-space; tile placement happens here so shadow_caster.gl_Position
|
|
@@ -275,21 +419,16 @@ fn _sampleShadowForCascade(
|
|
|
275
419
|
// [tileOrigin, tileOrigin+tileScale) (one texel inset) so taps stay in-tile.
|
|
276
420
|
let tileLo = tileOrigin + texel;
|
|
277
421
|
let tileHi = tileOrigin + tileScale - texel;
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
let
|
|
288
|
-
// Preserve the previous radius mapping for malformed even values:
|
|
289
|
-
// 1 -> 1x1, 2/3/4 -> 3x3, 5 -> 5x5. The authored component accepts only
|
|
290
|
-
// {1,3,5}, but keeping this normalization makes the refactor output-stable
|
|
291
|
-
// for raw UBO callers too.
|
|
292
|
-
let kernel = select(select(5u, 3u, requestedKernel <= 4u), 1u, requestedKernel == 1u);
|
|
422
|
+
// PCF1/3/5 profiles 1/2/3 retain their existing receiver. PCSS profiles 4/5
|
|
423
|
+
// use the bounded three-stage receiver below without a material or variant
|
|
424
|
+
// axis; the carrier y/z lanes provide angular radius and penumbra limit.
|
|
425
|
+
let filterProfile = clamp(u32(round(view.directionalShadowFilter.x)), 1u, 5u);
|
|
426
|
+
#ifdef DIRECTIONAL_PCSS_AVAILABLE
|
|
427
|
+
if (filterProfile >= 4u) {
|
|
428
|
+
return _samplePcssForCascade(worldPos, layer, count, normal, l, filterProfile);
|
|
429
|
+
}
|
|
430
|
+
#endif
|
|
431
|
+
let kernel = select(select(3u, 5u, filterProfile == 3u), 1u, filterProfile == 1u);
|
|
293
432
|
if (kernel == 1u) {
|
|
294
433
|
let lit = textureSampleCompareLevel(shadowMap, shadowSampler, clamp(uv, tileLo, tileHi), adjustedDepth);
|
|
295
434
|
return lit;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// invRangeSquared, ...) -> vec3<f32>
|
|
33
33
|
|
|
34
34
|
#import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
|
|
35
|
-
#import forgeax_pbr::lighting_attenuation::{evalDistanceAttenuation, evalSpotAttenuation}
|
|
35
|
+
#import forgeax_pbr::lighting_attenuation::{evalDistanceAttenuation, evalSpotAttenuation, projectSpotUv}
|
|
36
36
|
// feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-5):
|
|
37
37
|
// spot shadow sampling reuses the shared 2D 9-tap PCF core (sample_shadow_2d)
|
|
38
38
|
// and the always-on `spotShadowMap` (binding 8) + `shadowSampler` (binding 4).
|
|
@@ -78,16 +78,6 @@ fn evalVolumeSpot(
|
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// One projector UV helper is shared by surface and volume. The accepted
|
|
82
|
-
// projectorRevision travels in the host tuple; it is deliberately not a
|
|
83
|
-
// second shader registry or a device-side identity.
|
|
84
|
-
fn projectSpotUv(lightViewProj : mat4x4<f32>, worldPos : vec3<f32>) -> vec2<f32> {
|
|
85
|
-
let clip = lightViewProj * vec4<f32>(worldPos, 1.0);
|
|
86
|
-
let invW = select(1.0 / clip.w, 0.0, abs(clip.w) < 1e-6);
|
|
87
|
-
let projectorUv = vec2<f32>(clip.x * invW * 0.5 + 0.5, clip.y * invW * -0.5 + 0.5);
|
|
88
|
-
return projectorUv;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
81
|
// Shared punctual BRDF body returning (diffuse + specular) *
|
|
92
82
|
// colorTimesIntensity * nDotL * attenuation. Cone factor is applied by the
|
|
93
83
|
// caller (evalSpot only).
|
|
@@ -138,6 +128,50 @@ fn evalPoint(
|
|
|
138
128
|
);
|
|
139
129
|
}
|
|
140
130
|
|
|
131
|
+
// Flat 2D punctual evaluators share the range/cone math with the compatibility
|
|
132
|
+
// path and the clustered path, but intentionally skip the 3D BRDF normal term.
|
|
133
|
+
// Sprite-lit treats every quad as an omnidirectional receiver; keeping this
|
|
134
|
+
// owner here prevents URP and Cluster from drifting when the light lies in the
|
|
135
|
+
// sprite plane (the common 2D flashlight setup).
|
|
136
|
+
fn evalFlatRangeAttenuation(dSquared : f32, invRangeSquared : f32) -> f32 {
|
|
137
|
+
let factor = max(min(1.0 - (dSquared * invRangeSquared) * (dSquared * invRangeSquared), 1.0), 0.0);
|
|
138
|
+
return factor / dSquared;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn evalPointFlat(
|
|
142
|
+
lightPos : vec3<f32>,
|
|
143
|
+
colorTimesIntensity : vec3<f32>,
|
|
144
|
+
invRangeSquared : f32,
|
|
145
|
+
worldPos : vec3<f32>,
|
|
146
|
+
baseColor : vec3<f32>,
|
|
147
|
+
) -> vec3<f32> {
|
|
148
|
+
// The flat 2D path shares the same finite-range attenuation owner as the
|
|
149
|
+
// clustered and direct Standard paths; its only intentional difference is
|
|
150
|
+
// that a sprite is an omnidirectional receiver.
|
|
151
|
+
let toLight = lightPos - worldPos;
|
|
152
|
+
let dSquared = max(dot(toLight, toLight), 1e-4);
|
|
153
|
+
return baseColor * colorTimesIntensity * evalFlatRangeAttenuation(dSquared, invRangeSquared);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn evalSpotFlat(
|
|
157
|
+
lightPos : vec3<f32>,
|
|
158
|
+
lightDir : vec3<f32>,
|
|
159
|
+
colorTimesIntensity : vec3<f32>,
|
|
160
|
+
cosInner : f32,
|
|
161
|
+
cosOuter : f32,
|
|
162
|
+
invRangeSquared : f32,
|
|
163
|
+
worldPos : vec3<f32>,
|
|
164
|
+
baseColor : vec3<f32>,
|
|
165
|
+
) -> vec3<f32> {
|
|
166
|
+
let toLight = lightPos - worldPos;
|
|
167
|
+
let dSquared = max(dot(toLight, toLight), 1e-4);
|
|
168
|
+
let l = toLight / sqrt(dSquared);
|
|
169
|
+
let cone = smoothstep(cosOuter, cosInner, dot(l, -lightDir));
|
|
170
|
+
return evalPointFlat(
|
|
171
|
+
lightPos, colorTimesIntensity, invRangeSquared, worldPos, baseColor,
|
|
172
|
+
) * cone;
|
|
173
|
+
}
|
|
174
|
+
|
|
141
175
|
#ifdef POINT_SHADOW_AVAILABLE
|
|
142
176
|
// Shadow-modulated omnidirectional point light: same BRDF body * shadow factor.
|
|
143
177
|
//
|
|
@@ -175,13 +209,13 @@ fn evalPointShadowed(
|
|
|
175
209
|
lightPos, colorTimesIntensity, invRangeSquared,
|
|
176
210
|
worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
|
|
177
211
|
);
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
212
|
+
// Keep the fragment-to-light vector for BRDF and nDotL. Shadow matrices are
|
|
213
|
+
// authored light-to-fragment (`lookAt(lightPos, fragment)`), so the cube
|
|
214
|
+
// lookup must use the opposite direction. For a right-handed world, the
|
|
215
|
+
// cubemap convention flips Z so the +Z face look direction matches.
|
|
181
216
|
let toLight = lightPos - worldPos;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
let lightLocal = vec3<f32>(toLight.x, toLight.y, -toLight.z);
|
|
217
|
+
let fromLight = worldPos - lightPos;
|
|
218
|
+
let lightLocal = vec3<f32>(fromLight.x, fromLight.y, -fromLight.z);
|
|
185
219
|
// Reconstruct [0,1] NDC depth from world-space distance: largest-axis
|
|
186
220
|
// projection (research L0.5). Match the per-face perspective near / far
|
|
187
221
|
// configured by buildPointShadowMatrices (PointLightShadow.nearPlane /
|
|
@@ -287,10 +321,6 @@ fn evalSpotShadowed(
|
|
|
287
321
|
let invW = select(1.0 / splane.w, 0.0, abs(splane.w) < 1e-6);
|
|
288
322
|
let clipUv = projectSpotUv(lightViewProj, worldPos);
|
|
289
323
|
let depthRef = splane.z * invW;
|
|
290
|
-
// The accepted projectorRevision selects the same texture tuple for
|
|
291
|
-
// surface and volume; a bound projector uses textureSampleLevel here.
|
|
292
|
-
let projectorRevision = 0.0;
|
|
293
|
-
|
|
294
324
|
// OOB / NaN gate: outside the light frustum (or NaN from a degenerate matrix)
|
|
295
325
|
// returns fully lit. Mirrors the directional `>= 0 && <= 1` NaN-safe form.
|
|
296
326
|
if (!(clipUv.x >= 0.0 && clipUv.x <= 1.0 && clipUv.y >= 0.0 && clipUv.y <= 1.0 && depthRef <= 1.0)) {
|
package/src/scene-temporal.wgsl
CHANGED
|
@@ -27,17 +27,23 @@ fn sceneTemporalUv(clip : vec4<f32>) -> vec2<f32> {
|
|
|
27
27
|
return vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// Signed view-space Z shared by temporal reconstruction and Standard Cluster
|
|
31
|
+
// lookup. Both CPU and WGSL binners consume the negative camera-space depth:
|
|
32
|
+
// perspective uses -clip.w, while orthographic depth is reconstructed from
|
|
33
|
+
// the projection's near/far payload rather than Euclidean world distance.
|
|
34
|
+
fn sceneViewZ(clip : vec4<f32>, temporalProjection : vec4<f32>) -> f32 {
|
|
32
35
|
let ndcDepth = clip.z / max(abs(clip.w), 1e-6);
|
|
33
|
-
let
|
|
34
|
-
ndcDepth * (temporalProjection.y - temporalProjection.x);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
let orthographicViewZ = -(temporalProjection.x +
|
|
37
|
+
ndcDepth * (temporalProjection.y - temporalProjection.x));
|
|
38
|
+
return select(
|
|
39
|
+
-clip.w,
|
|
40
|
+
orthographicViewZ,
|
|
38
41
|
temporalProjection.z >= 0.5,
|
|
39
42
|
);
|
|
40
|
-
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn sceneTemporalViewDepth(clip : vec4<f32>, temporalProjection : vec4<f32>) -> f32 {
|
|
46
|
+
return log2(1.0 + max(-sceneViewZ(clip, temporalProjection), 0.0));
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
fn packSceneTemporalV1(
|
package/src/shadow-pcf.wgsl
CHANGED
|
@@ -20,6 +20,49 @@
|
|
|
20
20
|
//
|
|
21
21
|
// Return: 1.0 = fully lit, 0.0 = fully shadowed.
|
|
22
22
|
|
|
23
|
+
// === Directional PCSS sampling primitives ====================================
|
|
24
|
+
//
|
|
25
|
+
// These functions deliberately accept tile bounds from their caller. They own
|
|
26
|
+
// only integer addressing, raw depth, comparison sampling, and the shared
|
|
27
|
+
// receiver-bias equation; cascade selection, profile selection, blocker
|
|
28
|
+
// averaging, penumbra sizing, and cascade blending remain Directional policy.
|
|
29
|
+
|
|
30
|
+
fn shadow_clamp_texel_to_tile(
|
|
31
|
+
texel : vec2<i32>,
|
|
32
|
+
tileOrigin : vec2<i32>,
|
|
33
|
+
tileSize : vec2<i32>,
|
|
34
|
+
inset : i32,
|
|
35
|
+
) -> vec2<i32> {
|
|
36
|
+
let tileMin = tileOrigin + vec2<i32>(inset);
|
|
37
|
+
let tileMax = tileOrigin + max(vec2<i32>(inset), tileSize - vec2<i32>(1 + inset));
|
|
38
|
+
return min(tileMax, max(tileMin, texel));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn shadow_load_raw_depth(
|
|
42
|
+
shadowMap : texture_depth_2d,
|
|
43
|
+
texel : vec2<i32>,
|
|
44
|
+
) -> f32 {
|
|
45
|
+
return textureLoad(shadowMap, texel, 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn shadow_sample_compare(
|
|
49
|
+
shadowMap : texture_depth_2d,
|
|
50
|
+
shadowSampler: sampler_comparison,
|
|
51
|
+
uv : vec2<f32>,
|
|
52
|
+
depthRef : f32,
|
|
53
|
+
) -> f32 {
|
|
54
|
+
return textureSampleCompareLevel(shadowMap, shadowSampler, uv, depthRef);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn shadow_biased_receiver_depth(
|
|
58
|
+
receiverDepth : f32,
|
|
59
|
+
normalBias : f32,
|
|
60
|
+
depthBias : f32,
|
|
61
|
+
nDotL : f32,
|
|
62
|
+
) -> f32 {
|
|
63
|
+
return receiverDepth - max(normalBias * (1.0 - nDotL), depthBias);
|
|
64
|
+
}
|
|
65
|
+
|
|
23
66
|
// 3x3 integer offset table for PCF kernel (9 taps).
|
|
24
67
|
// Extracted from lighting-directional.wgsl inline loop (T-M2-4);
|
|
25
68
|
// `sample_shadow_2d` walks all 9 taps; `sample_shadow_cube_hw2x2` does NOT
|
|
@@ -99,7 +142,7 @@ fn sample_shadow_2d_kernel(
|
|
|
99
142
|
// `shadowAtlas` is @group(0) @binding(5) texture_depth_cube_array.
|
|
100
143
|
// `shadowSampler` is @group(0) @binding(4) sampler_comparison (shared with
|
|
101
144
|
// directional shadows — same sampler type, no dimension distinction).
|
|
102
|
-
// `lightLocal` is the
|
|
145
|
+
// `lightLocal` is the light-to-fragment direction vector in the cubemap's
|
|
103
146
|
// coordinate system (Bevy convention: left-handed cubemap; caller applies
|
|
104
147
|
// flip_z = vec3(1,1,-1) to convert right-hand world to left-hand cubemap).
|
|
105
148
|
// `layer` is the cube_array layer index (i32; 0..3 for 4 shadow-casting
|