@forgeax/engine-shader 0.1.19 → 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 +71 -22
- package/src/__tests__/fog-oracle.unit.test.ts +110 -114
- package/src/__tests__/fog-orthographic-ray.unit.test.ts +12 -37
- package/src/__tests__/fog-producer-matrix.integration.test.ts +16 -51
- package/src/__tests__/lighting-point-spot-volume.unit.test.ts +56 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +32 -6
- package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +26 -0
- package/src/__tests__/lighting-spot-volume.unit.test.ts +25 -0
- 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 +35 -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 +2 -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 +72 -73
- package/src/default-standard-pbr-skin.wgsl +31 -89
- package/src/default-standard-pbr.wgsl +94 -146
- package/src/fxaa.wgsl +27 -6
- package/src/hdrp-deferred-lighting.wgsl +3 -27
- package/src/ibl-sampling.wgsl +54 -0
- package/src/index.ts +6 -0
- package/src/lighting-attenuation.wgsl +58 -0
- package/src/lighting-directional.wgsl +188 -21
- package/src/lighting-punctual.wgsl +94 -19
- package/src/msdf-text.wgsl +4 -23
- package/src/scene-temporal.wgsl +14 -8
- package/src/shadow-pcf.wgsl +75 -11
- package/src/skybox.wgsl +2 -7
- package/src/sprite-lit.wgsl +38 -73
- package/src/sprite.wgsl +3 -22
- package/src/{hdrp-cluster-forward.wgsl → standard-cluster.wgsl} +74 -23
- package/src/tonemap.wgsl +8 -3
- package/src/unlit.wgsl +2 -24
- package/src/volume/volume-composite.wgsl +113 -0
- package/src/volume/volume-inject.wgsl +261 -0
- package/src/volume/volume-integrate.wgsl +337 -0
- package/src/volume/volume-temporal.wgsl +70 -0
|
@@ -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
|
|
@@ -238,6 +382,7 @@ fn _sampleShadowForCascade(
|
|
|
238
382
|
count : u32,
|
|
239
383
|
normal : vec3<f32>,
|
|
240
384
|
l : vec3<f32>,
|
|
385
|
+
useNormalBias : bool,
|
|
241
386
|
) -> f32 {
|
|
242
387
|
let lvp = _cascadeLightViewProj(layer);
|
|
243
388
|
let lightClip = lvp * vec4<f32>(worldPos, 1.0);
|
|
@@ -253,7 +398,7 @@ fn _sampleShadowForCascade(
|
|
|
253
398
|
// shadow fields carried in the View UBO -- normalBias scales the
|
|
254
399
|
// (1 - N.L) slope term, depthBias is the constant floor. Replaces the prior
|
|
255
400
|
// hardcoded max(0.05*(1-N.L), 0.005).
|
|
256
|
-
let bias = max(view.normalBias * (1.0 - dot(normal, l)), view.depthBias);
|
|
401
|
+
let bias = select(view.depthBias, max(view.normalBias * (1.0 - dot(normal, l)), view.depthBias), useNormalBias);
|
|
257
402
|
let adjustedDepth = currentDepth - bias;
|
|
258
403
|
// NaN-safe bounds: relational < and > do not reject NaN (NaN < 0 is
|
|
259
404
|
// false), so a zero / degenerate lightViewProj matrix that produces
|
|
@@ -274,21 +419,16 @@ fn _sampleShadowForCascade(
|
|
|
274
419
|
// [tileOrigin, tileOrigin+tileScale) (one texel inset) so taps stay in-tile.
|
|
275
420
|
let tileLo = tileOrigin + texel;
|
|
276
421
|
let tileHi = tileOrigin + tileScale - texel;
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
let
|
|
287
|
-
// Preserve the previous radius mapping for malformed even values:
|
|
288
|
-
// 1 -> 1x1, 2/3/4 -> 3x3, 5 -> 5x5. The authored component accepts only
|
|
289
|
-
// {1,3,5}, but keeping this normalization makes the refactor output-stable
|
|
290
|
-
// for raw UBO callers too.
|
|
291
|
-
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);
|
|
292
432
|
if (kernel == 1u) {
|
|
293
433
|
let lit = textureSampleCompareLevel(shadowMap, shadowSampler, clamp(uv, tileLo, tileHi), adjustedDepth);
|
|
294
434
|
return lit;
|
|
@@ -414,7 +554,7 @@ fn evalDirectionalShadowFactor(
|
|
|
414
554
|
return 1.0;
|
|
415
555
|
}
|
|
416
556
|
let layer = _pickCascadeLayer(viewDepth, count);
|
|
417
|
-
let shadowCurr = _sampleShadowForCascade(worldPos, layer, count, normal, l);
|
|
557
|
+
let shadowCurr = _sampleShadowForCascade(worldPos, layer, count, normal, l, true);
|
|
418
558
|
|
|
419
559
|
var shadow = shadowCurr;
|
|
420
560
|
if (view.cascadeBlend > 0.0 && layer + 1u < count) {
|
|
@@ -424,7 +564,7 @@ fn evalDirectionalShadowFactor(
|
|
|
424
564
|
let dist = spCurr - viewDepth;
|
|
425
565
|
let t = clamp(1.0 - dist / blendWidth, 0.0, 1.0);
|
|
426
566
|
if (t > 0.0) {
|
|
427
|
-
let shadowNext = _sampleShadowForCascade(worldPos, layer + 1u, count, normal, l);
|
|
567
|
+
let shadowNext = _sampleShadowForCascade(worldPos, layer + 1u, count, normal, l, true);
|
|
428
568
|
shadow = mix(shadowCurr, shadowNext, t);
|
|
429
569
|
}
|
|
430
570
|
}
|
|
@@ -432,6 +572,33 @@ fn evalDirectionalShadowFactor(
|
|
|
432
572
|
return shadow;
|
|
433
573
|
}
|
|
434
574
|
|
|
575
|
+
// Volume receivers are samples in a medium, not surfaces. They therefore use
|
|
576
|
+
// the same cascade selection, atlas placement, PCF, and shadow-distance SSOT
|
|
577
|
+
// but only the bounded constant receiver bias; a ray direction is never
|
|
578
|
+
// treated as a geometric normal for normalBias.
|
|
579
|
+
fn evalDirectionalVolumeShadowFactor(
|
|
580
|
+
worldPos : vec3<f32>,
|
|
581
|
+
viewDepth : f32,
|
|
582
|
+
) -> f32 {
|
|
583
|
+
if (view.cascadeCount < 1.0 || viewDepth > view.splitPlanes[u32(max(view.cascadeCount, 1.0)) - 1u].x) {
|
|
584
|
+
return 1.0;
|
|
585
|
+
}
|
|
586
|
+
let count = u32(max(view.cascadeCount, 1.0));
|
|
587
|
+
let layer = _pickCascadeLayer(viewDepth, count);
|
|
588
|
+
let lightDirection = normalize(-view.lightDir);
|
|
589
|
+
let current = _sampleShadowForCascade(worldPos, layer, count, vec3<f32>(0.0), lightDirection, false);
|
|
590
|
+
if (view.cascadeBlend <= 0.0 || layer + 1u >= count) {
|
|
591
|
+
return current;
|
|
592
|
+
}
|
|
593
|
+
let blendWidth = view.splitPlanes[layer].x * view.cascadeBlend;
|
|
594
|
+
if (blendWidth <= 0.0) {
|
|
595
|
+
return current;
|
|
596
|
+
}
|
|
597
|
+
let blend = clamp(1.0 - (view.splitPlanes[layer].x - viewDepth) / blendWidth, 0.0, 1.0);
|
|
598
|
+
let next = _sampleShadowForCascade(worldPos, layer + 1u, count, vec3<f32>(0.0), lightDirection, false);
|
|
599
|
+
return mix(current, next, blend);
|
|
600
|
+
}
|
|
601
|
+
|
|
435
602
|
// `evalDirectional` evaluates the GGX direct-lighting term for the single
|
|
436
603
|
// directional light carried in `view.lightDir / view.lightColor`. CSM
|
|
437
604
|
// pathway: pick cascade layer from viewZ + splitPlanes, sample the atlas
|
|
@@ -32,12 +32,13 @@
|
|
|
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, projectSpotUv}
|
|
35
36
|
// feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-5):
|
|
36
37
|
// spot shadow sampling reuses the shared 2D 9-tap PCF core (sample_shadow_2d)
|
|
37
38
|
// and the always-on `spotShadowMap` (binding 8) + `shadowSampler` (binding 4).
|
|
38
39
|
// `shadowSampler` is imported UNCONDITIONALLY here (spot is always-on, D-5):
|
|
39
40
|
// the point-shadow #ifdef block below must NOT re-import it (double import).
|
|
40
|
-
#import forgeax_pbr::shadow_pcf::{
|
|
41
|
+
#import forgeax_pbr::shadow_pcf::{sample_shadow_2d_kernel}
|
|
41
42
|
#import forgeax_view::common::{spotShadowMap, shadowSampler}
|
|
42
43
|
#ifdef POINT_SHADOW_AVAILABLE
|
|
43
44
|
#import forgeax_pbr::shadow_pcf::{sample_shadow_cube_hw2x2}
|
|
@@ -48,6 +49,35 @@
|
|
|
48
49
|
#import forgeax_view::common::{shadowAtlas}
|
|
49
50
|
#endif
|
|
50
51
|
|
|
52
|
+
// Volume lighting uses the same host-derived range and cone facts as surface
|
|
53
|
+
// lighting. These wrappers intentionally return radiance factors only; the
|
|
54
|
+
// volume integrator owns density, phase, transmittance, and accumulation.
|
|
55
|
+
fn evalVolumePoint(
|
|
56
|
+
lightPos : vec3<f32>,
|
|
57
|
+
colorTimesIntensity : vec3<f32>,
|
|
58
|
+
invRangeSquared : f32,
|
|
59
|
+
worldPos : vec3<f32>,
|
|
60
|
+
) -> vec3<f32> {
|
|
61
|
+
let toLight = lightPos - worldPos;
|
|
62
|
+
let dSquared = max(dot(toLight, toLight), 1e-4);
|
|
63
|
+
let safeDistance = max(dSquared, 1e-4);
|
|
64
|
+
return colorTimesIntensity * evalDistanceAttenuation(safeDistance, invRangeSquared);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn evalVolumeSpot(
|
|
68
|
+
lightPos : vec3<f32>,
|
|
69
|
+
lightDir : vec3<f32>,
|
|
70
|
+
colorTimesIntensity : vec3<f32>,
|
|
71
|
+
cosInner : f32,
|
|
72
|
+
cosOuter : f32,
|
|
73
|
+
invRangeSquared : f32,
|
|
74
|
+
worldPos : vec3<f32>,
|
|
75
|
+
) -> vec3<f32> {
|
|
76
|
+
return colorTimesIntensity * evalSpotAttenuation(
|
|
77
|
+
lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
51
81
|
// Shared punctual BRDF body returning (diffuse + specular) *
|
|
52
82
|
// colorTimesIntensity * nDotL * attenuation. Cone factor is applied by the
|
|
53
83
|
// caller (evalSpot only).
|
|
@@ -75,8 +105,7 @@ fn evalPunctualBody(
|
|
|
75
105
|
let specular = d_ggx(nDotH, alphaSq) * v_smith(nDotV, nDotL, alphaSq) * f;
|
|
76
106
|
let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);
|
|
77
107
|
let diffuse = kd * baseColor / 3.14159265;
|
|
78
|
-
let
|
|
79
|
-
let attenuation = factor * factor / dSquared;
|
|
108
|
+
let attenuation = evalDistanceAttenuation(dSquared, invRangeSquared);
|
|
80
109
|
return (diffuse + specular) * colorTimesIntensity * nDotL * attenuation;
|
|
81
110
|
}
|
|
82
111
|
|
|
@@ -99,6 +128,50 @@ fn evalPoint(
|
|
|
99
128
|
);
|
|
100
129
|
}
|
|
101
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
|
+
|
|
102
175
|
#ifdef POINT_SHADOW_AVAILABLE
|
|
103
176
|
// Shadow-modulated omnidirectional point light: same BRDF body * shadow factor.
|
|
104
177
|
//
|
|
@@ -136,13 +209,13 @@ fn evalPointShadowed(
|
|
|
136
209
|
lightPos, colorTimesIntensity, invRangeSquared,
|
|
137
210
|
worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
|
|
138
211
|
);
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
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.
|
|
142
216
|
let toLight = lightPos - worldPos;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
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);
|
|
146
219
|
// Reconstruct [0,1] NDC depth from world-space distance: largest-axis
|
|
147
220
|
// projection (research L0.5). Match the per-face perspective near / far
|
|
148
221
|
// configured by buildPointShadowMatrices (PointLightShadow.nearPlane /
|
|
@@ -185,10 +258,9 @@ fn evalSpot(
|
|
|
185
258
|
lightPos, colorTimesIntensity, invRangeSquared,
|
|
186
259
|
worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
|
|
187
260
|
);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return body * cone;
|
|
261
|
+
return body * evalSpotAttenuation(
|
|
262
|
+
lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
|
|
263
|
+
) / max(evalDistanceAttenuation(dot(lightPos - worldPos, lightPos - worldPos), invRangeSquared), 1e-4);
|
|
192
264
|
}
|
|
193
265
|
|
|
194
266
|
// feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-4 +
|
|
@@ -234,6 +306,8 @@ fn evalSpotShadowed(
|
|
|
234
306
|
shadowAtlasTile : i32,
|
|
235
307
|
depthBias : f32,
|
|
236
308
|
normalBias : f32,
|
|
309
|
+
pcfKernelSize : f32,
|
|
310
|
+
shadowIntensity : f32,
|
|
237
311
|
) -> vec3<f32> {
|
|
238
312
|
let body = evalSpot(
|
|
239
313
|
lightPos, lightDir, colorTimesIntensity, cosInner, cosOuter, invRangeSquared,
|
|
@@ -245,11 +319,8 @@ fn evalSpotShadowed(
|
|
|
245
319
|
// Perspective divide; guard a zero/near-zero w (fragment behind the light or
|
|
246
320
|
// a degenerate matrix) so the OOB gate below catches it as fully lit.
|
|
247
321
|
let invW = select(1.0 / splane.w, 0.0, abs(splane.w) < 1e-6);
|
|
248
|
-
let
|
|
322
|
+
let clipUv = projectSpotUv(lightViewProj, worldPos);
|
|
249
323
|
let depthRef = splane.z * invW;
|
|
250
|
-
// Clip-space [-1,1] -> texture UV [0,1] with the standard Y flip.
|
|
251
|
-
let clipUv = vec2<f32>(ndcXY.x * 0.5 + 0.5, ndcXY.y * -0.5 + 0.5);
|
|
252
|
-
|
|
253
324
|
// OOB / NaN gate: outside the light frustum (or NaN from a degenerate matrix)
|
|
254
325
|
// returns fully lit. Mirrors the directional `>= 0 && <= 1` NaN-safe form.
|
|
255
326
|
if (!(clipUv.x >= 0.0 && clipUv.x <= 1.0 && clipUv.y >= 0.0 && clipUv.y <= 1.0 && depthRef <= 1.0)) {
|
|
@@ -267,8 +338,12 @@ fn evalSpotShadowed(
|
|
|
267
338
|
let texel = vec2<f32>(1.0, 1.0) / atlasDims;
|
|
268
339
|
|
|
269
340
|
let nDotL = max(dot(normal, normalize(lightPos - worldPos)), 0.0);
|
|
270
|
-
let shadowFactor =
|
|
341
|
+
let shadowFactor = sample_shadow_2d_kernel(
|
|
271
342
|
spotShadowMap, shadowSampler, atlasUv, texel, depthRef, normalBias, depthBias, nDotL,
|
|
343
|
+
pcfKernelSize,
|
|
272
344
|
);
|
|
273
|
-
|
|
345
|
+
// Three's SpotLight.shadow.intensity is the opacity of the shadow, not a
|
|
346
|
+
// multiplier on the light's candela radiance. A value of 1 keeps the
|
|
347
|
+
// sampled visibility fully opaque; 0 leaves the unshadowed body intact.
|
|
348
|
+
return body * mix(1.0, shadowFactor, clamp(shadowIntensity, 0.0, 1.0));
|
|
274
349
|
}
|
package/src/msdf-text.wgsl
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
2
2
|
|
|
3
|
-
#import forgeax_view::common::{View,
|
|
4
|
-
#import forgeax_view::fog::{apply_fog}
|
|
3
|
+
#import forgeax_view::common::{View, Mesh, view, meshes, sampleMaterialTexture}
|
|
5
4
|
|
|
6
5
|
// @forgeax/engine-shader - msdf-text.wgsl
|
|
7
6
|
// (feat-20260531-world-space-msdf-text-rendering M5 / w20).
|
|
@@ -146,22 +145,6 @@ fn screen_px_range(uv : vec2<f32>) -> f32 {
|
|
|
146
145
|
return max(0.5 * dot(unit_range, screen_tex_size), 1.0);
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
150
|
-
var origin = viewParams.cameraPos;
|
|
151
|
-
var direction = normalize(worldPos - origin);
|
|
152
|
-
var rayDistance = length(worldPos - origin);
|
|
153
|
-
if (viewParams.temporalProjection.z >= 0.5) {
|
|
154
|
-
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
155
|
-
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
156
|
-
let nearPoint = nearH.xyz / nearH.w;
|
|
157
|
-
let farPoint = farH.xyz / farH.w;
|
|
158
|
-
direction = normalize(farPoint - nearPoint);
|
|
159
|
-
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
160
|
-
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
161
|
-
}
|
|
162
|
-
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
148
|
// fs_main_hdr: outputs linear premultiplied alpha for the rgba16float
|
|
166
149
|
// offscreen target (D-7 / R-7). The tonemap fullscreen pass handles sRGB
|
|
167
150
|
// encoding; writing hdrColor lets the bloom bright-pass catch the text.
|
|
@@ -176,8 +159,7 @@ fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
176
159
|
let alpha = clamp(dist + 0.5, 0.0, 1.0) * material.tintColor.a;
|
|
177
160
|
// Premultiplied output: rgb already multiplied by alpha for srcFactor=ONE /
|
|
178
161
|
// dstFactor=ONE_MINUS_SRC_ALPHA (wiki section 6 SSOT).
|
|
179
|
-
|
|
180
|
-
return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
162
|
+
return vec4<f32>(material.tintColor.rgb * alpha, alpha);
|
|
181
163
|
}
|
|
182
164
|
|
|
183
165
|
// linear_to_srgb: per-channel IEC 61966-2-1 transfer function for the LDR
|
|
@@ -197,12 +179,11 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
197
179
|
let sd = median(msd.r, msd.g, msd.b);
|
|
198
180
|
let dist = (sd - 0.5) * screen_px_range(in.uv);
|
|
199
181
|
let alpha = clamp(dist + 0.5, 0.0, 1.0) * material.tintColor.a;
|
|
200
|
-
let
|
|
201
|
-
let premult = fogged.rgb * fogged.a;
|
|
182
|
+
let premult = material.tintColor.rgb * alpha;
|
|
202
183
|
return vec4<f32>(
|
|
203
184
|
linear_to_srgb(premult.r),
|
|
204
185
|
linear_to_srgb(premult.g),
|
|
205
186
|
linear_to_srgb(premult.b),
|
|
206
|
-
|
|
187
|
+
alpha,
|
|
207
188
|
);
|
|
208
189
|
}
|
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
|
|
@@ -49,6 +92,22 @@ fn sample_shadow_2d(
|
|
|
49
92
|
normalBias : f32,
|
|
50
93
|
depthBias : f32,
|
|
51
94
|
nDotL : f32,
|
|
95
|
+
) -> f32 {
|
|
96
|
+
return sample_shadow_2d_kernel(
|
|
97
|
+
shadowMap, shadowSampler, uv, texel, depthRef, normalBias, depthBias, nDotL, 3.0,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn sample_shadow_2d_kernel(
|
|
102
|
+
shadowMap : texture_depth_2d,
|
|
103
|
+
shadowSampler: sampler_comparison,
|
|
104
|
+
uv : vec2<f32>,
|
|
105
|
+
texel : vec2<f32>,
|
|
106
|
+
depthRef : f32,
|
|
107
|
+
normalBias : f32,
|
|
108
|
+
depthBias : f32,
|
|
109
|
+
nDotL : f32,
|
|
110
|
+
kernelSize : f32,
|
|
52
111
|
) -> f32 {
|
|
53
112
|
// feat-20260621-merge-directionallightshadow-into-directionallight M3 / m3-t5:
|
|
54
113
|
// param names aligned to the D-1 directional convention -- the slope
|
|
@@ -60,17 +119,22 @@ fn sample_shadow_2d(
|
|
|
60
119
|
let bias = max(normalBias * (1.0 - nDotL), depthBias / 1000.0);
|
|
61
120
|
let adjustedDepth = depthRef - bias;
|
|
62
121
|
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
122
|
+
let halfWidth = clamp((i32(round(kernelSize)) - 1) / 2, 0, 2);
|
|
123
|
+
// Variable odd-kernel PCF. textureSampleCompareLevel returns 1.0 when
|
|
124
|
+
// adjustedDepth < sampledDepth; sampler clamp-to-edge handles OOB texels.
|
|
66
125
|
var blocked = 0.0;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
126
|
+
var samples = 0.0;
|
|
127
|
+
for (var y = -2; y <= 2; y = y + 1) {
|
|
128
|
+
for (var x = -2; x <= 2; x = x + 1) {
|
|
129
|
+
if (abs(x) <= halfWidth && abs(y) <= halfWidth) {
|
|
130
|
+
let offsetUv = uv + vec2<f32>(f32(x), f32(y)) * texel;
|
|
131
|
+
let lit = textureSampleCompareLevel(shadowMap, shadowSampler, offsetUv, adjustedDepth);
|
|
132
|
+
blocked = blocked + (1.0 - lit);
|
|
133
|
+
samples = samples + 1.0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
72
136
|
}
|
|
73
|
-
return 1.0 - blocked /
|
|
137
|
+
return 1.0 - blocked / max(samples, 1.0);
|
|
74
138
|
}
|
|
75
139
|
|
|
76
140
|
// === Cube point-light-shadow wrapper =========================================
|
|
@@ -78,7 +142,7 @@ fn sample_shadow_2d(
|
|
|
78
142
|
// `shadowAtlas` is @group(0) @binding(5) texture_depth_cube_array.
|
|
79
143
|
// `shadowSampler` is @group(0) @binding(4) sampler_comparison (shared with
|
|
80
144
|
// directional shadows — same sampler type, no dimension distinction).
|
|
81
|
-
// `lightLocal` is the
|
|
145
|
+
// `lightLocal` is the light-to-fragment direction vector in the cubemap's
|
|
82
146
|
// coordinate system (Bevy convention: left-handed cubemap; caller applies
|
|
83
147
|
// flip_z = vec3(1,1,-1) to convert right-hand world to left-hand cubemap).
|
|
84
148
|
// `layer` is the cube_array layer index (i32; 0..3 for 4 shadow-casting
|
|
@@ -117,4 +181,4 @@ fn sample_shadow_cube_hw2x2(
|
|
|
117
181
|
// handles face-boundary texel fetches. Returns 1.0 = fully lit, 0.0 = fully
|
|
118
182
|
// shadowed.
|
|
119
183
|
return textureSampleCompareLevel(shadowAtlas, shadowSampler, lightLocal, layer, adjustedDepth);
|
|
120
|
-
}
|
|
184
|
+
}
|