@forgeax/engine-shader 0.1.19 → 0.1.20
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/dist/.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +23 -19
- 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 +48 -0
- 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__/shader.unit.test.ts +1 -0
- package/src/__tests__/sprite-variants.unit.test.ts +1 -0
- package/src/common.wgsl +30 -61
- package/src/default-standard-pbr-skin.wgsl +2 -19
- package/src/default-standard-pbr.wgsl +50 -31
- package/src/hdrp-cluster-forward.wgsl +41 -3
- package/src/hdrp-deferred-lighting.wgsl +3 -27
- package/src/lighting-attenuation.wgsl +58 -0
- package/src/lighting-directional.wgsl +31 -3
- package/src/lighting-punctual.wgsl +57 -12
- package/src/msdf-text.wgsl +4 -23
- package/src/shadow-pcf.wgsl +31 -10
- package/src/skybox.wgsl +2 -7
- package/src/sprite-lit.wgsl +3 -22
- package/src/sprite.wgsl +3 -22
- package/src/unlit.wgsl +2 -24
- package/src/volume/volume-composite.wgsl +113 -0
- package/src/volume/volume-inject.wgsl +249 -0
- package/src/volume/volume-integrate.wgsl +342 -0
- package/src/volume/volume-temporal.wgsl +70 -0
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
#define_import_path forgeax_hdrp::cluster_forward
|
|
8
8
|
|
|
9
|
-
#import forgeax_pbr::lighting_punctual::{evalPoint, evalSpot, evalSpotShadowed}
|
|
9
|
+
#import forgeax_pbr::lighting_punctual::{evalPoint, evalSpot, evalSpotShadowed, projectSpotUv}
|
|
10
10
|
#import forgeax_view::common::{view}
|
|
11
|
+
#ifdef PROJECTOR_AVAILABLE
|
|
12
|
+
#import forgeax_view::common::{projectorTexture, projectorSampler}
|
|
13
|
+
#endif
|
|
11
14
|
#ifdef POINT_SHADOW_AVAILABLE
|
|
12
15
|
#import forgeax_pbr::lighting_punctual::{evalPointShadowed}
|
|
13
16
|
#endif
|
|
@@ -16,13 +19,15 @@
|
|
|
16
19
|
|
|
17
20
|
const KIND_POINT: u32 = 0u;
|
|
18
21
|
const KIND_SPOT: u32 = 1u;
|
|
22
|
+
const PROJECTOR_ONLY_TILE: i32 = -2;
|
|
19
23
|
|
|
20
24
|
// LightSlot is the byte-frozen 64B std430/std140 transport contract.
|
|
21
25
|
// [0..2] position, [3] invRangeSquared
|
|
22
26
|
// [4..6] colorTimesIntensity, [7] cosInner
|
|
23
27
|
// [8..10] direction, [11] cosOuter
|
|
24
28
|
// [12] raw kind u32, [13] shadow identity i32
|
|
25
|
-
// [14
|
|
29
|
+
// [14] point near / spot shadow intensity f32 bits
|
|
30
|
+
// [15] point far / spot PCF kernel width f32 bits
|
|
26
31
|
struct LightSlot {
|
|
27
32
|
position : vec4<f32>,
|
|
28
33
|
color : vec4<f32>,
|
|
@@ -32,6 +37,23 @@ struct LightSlot {
|
|
|
32
37
|
|
|
33
38
|
const LIGHTSLOT_BYTE_SIZE: u32 = 64u;
|
|
34
39
|
|
|
40
|
+
fn sampleClusterSpotProjector(lightViewProj : mat4x4<f32>, worldPos : vec3<f32>) -> vec3<f32> {
|
|
41
|
+
#ifdef PROJECTOR_AVAILABLE
|
|
42
|
+
let clip = lightViewProj * vec4<f32>(worldPos, 1.0);
|
|
43
|
+
// The projector is a cookie, not an extra cone. A fragment that cannot be
|
|
44
|
+
// projected (or lies outside the tile) keeps the analytic Spot contribution
|
|
45
|
+
// intact, matching Three's SpotLightNode fail-open map gate.
|
|
46
|
+
if (abs(clip.w) < 1e-6) { return vec3<f32>(1.0); }
|
|
47
|
+
let uv = projectSpotUv(lightViewProj, worldPos);
|
|
48
|
+
if (any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0))) {
|
|
49
|
+
return vec3<f32>(1.0);
|
|
50
|
+
}
|
|
51
|
+
return textureSampleLevel(projectorTexture, projectorSampler, uv, 0.0).rgb;
|
|
52
|
+
#else
|
|
53
|
+
return vec3<f32>(1.0);
|
|
54
|
+
#endif
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
struct ClusterUniform {
|
|
36
58
|
grid : vec4<u32>,
|
|
37
59
|
near_far_log : vec4<f32>,
|
|
@@ -119,6 +141,7 @@ fn evaluate_cluster_light(
|
|
|
119
141
|
if (kind == KIND_SPOT) {
|
|
120
142
|
let tile = bitcast<i32>(light.kind_and_shadow.y);
|
|
121
143
|
if (tile >= 0) {
|
|
144
|
+
let projector = sampleClusterSpotProjector(view.spotLightViewProj[tile], world_pos);
|
|
122
145
|
return evalSpotShadowed(
|
|
123
146
|
light.position.xyz, light.direction.xyz, light.color.xyz,
|
|
124
147
|
// LightSlot packs cosInner in color.w and cosOuter in direction.w;
|
|
@@ -126,7 +149,22 @@ fn evaluate_cluster_light(
|
|
|
126
149
|
light.color.w, light.direction.w, light.position.w,
|
|
127
150
|
world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
128
151
|
view.spotLightViewProj[tile], tile, 0.005, 0.05,
|
|
129
|
-
|
|
152
|
+
clamp(
|
|
153
|
+
round(select(3.0, bitcast<f32>(light.kind_and_shadow.w),
|
|
154
|
+
bitcast<f32>(light.kind_and_shadow.w) > 0.5)),
|
|
155
|
+
1.0,
|
|
156
|
+
5.0,
|
|
157
|
+
),
|
|
158
|
+
clamp(bitcast<f32>(light.kind_and_shadow.z), 0.0, 1.0),
|
|
159
|
+
) * projector;
|
|
160
|
+
}
|
|
161
|
+
if (tile == PROJECTOR_ONLY_TILE) {
|
|
162
|
+
let projector = sampleClusterSpotProjector(view.spotLightViewProj[0], world_pos);
|
|
163
|
+
return evalSpot(
|
|
164
|
+
light.position.xyz, light.direction.xyz, light.color.xyz,
|
|
165
|
+
light.color.w, light.direction.w, light.position.w,
|
|
166
|
+
world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
167
|
+
) * projector;
|
|
130
168
|
}
|
|
131
169
|
return evalSpot(
|
|
132
170
|
light.position.xyz, light.direction.xyz, light.color.xyz,
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
#define_import_path forgeax_view::hdrp_deferred_lighting
|
|
2
2
|
|
|
3
3
|
#import forgeax_view::common::FullscreenOutput
|
|
4
|
-
#import forgeax_view::common::
|
|
4
|
+
#import forgeax_view::common::View
|
|
5
5
|
#import forgeax_view::common::fullscreen_triangle
|
|
6
|
-
#import forgeax_view::fog::{apply_fog}
|
|
7
6
|
|
|
8
7
|
// HDRP deferred lighting producer. The lighting pass consumes the material
|
|
9
8
|
// G-buffer and current-frame depth, reconstructs the fragment world position,
|
|
10
|
-
// then
|
|
9
|
+
// then writes linear radiance for the shared volume composite pass.
|
|
11
10
|
|
|
12
11
|
@group(0) @binding(0) var<uniform> view : View;
|
|
13
12
|
|
|
@@ -24,28 +23,6 @@ fn vs_main(@builtin(vertex_index) vertex_index : u32) -> FullscreenOutput {
|
|
|
24
23
|
return fullscreen_triangle(vertex_index);
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
fn reconstructWorldPosition(uv : vec2<f32>, depth : f32) -> vec3<f32> {
|
|
28
|
-
let clip = vec4<f32>(uv * 2.0 - vec2<f32>(1.0), depth, 1.0);
|
|
29
|
-
let worldH = view.inverseViewProj * clip;
|
|
30
|
-
return worldH.xyz / worldH.w;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
fn fogDeferredRadiance(color : vec4<f32>, worldPos : vec3<f32>) -> vec4<f32> {
|
|
34
|
-
var origin = view.cameraPos;
|
|
35
|
-
var direction = normalize(worldPos - origin);
|
|
36
|
-
var rayDistance = length(worldPos - origin);
|
|
37
|
-
if (view.temporalProjection.z >= 0.5) {
|
|
38
|
-
let nearH = view.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
39
|
-
let farH = view.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
40
|
-
let nearPoint = nearH.xyz / nearH.w;
|
|
41
|
-
let farPoint = farH.xyz / farH.w;
|
|
42
|
-
direction = normalize(farPoint - nearPoint);
|
|
43
|
-
origin = worldPos - direction * dot(worldPos - view.cameraPos, direction);
|
|
44
|
-
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
45
|
-
}
|
|
46
|
-
return apply_fog(view.fog, FogRay(origin, direction, rayDistance), color);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
26
|
@fragment
|
|
50
27
|
fn fs_lighting(in : FullscreenOutput) -> @location(0) vec4<f32> {
|
|
51
28
|
// The lighting pass runs before the forward lane and shares its cleared
|
|
@@ -64,6 +41,5 @@ fn fs_lighting(in : FullscreenOutput) -> @location(0) vec4<f32> {
|
|
|
64
41
|
let light = max(dot(normal, normalize(-view.lightDir)), 0.0);
|
|
65
42
|
let direct = material.rgb * view.lightColor * light * clamp(emissiveSample.a, 0.0, 1.0);
|
|
66
43
|
let radiance = vec4<f32>(direct + emissiveSample.rgb, material.a);
|
|
67
|
-
|
|
68
|
-
return fogDeferredRadiance(radiance, worldPos);
|
|
44
|
+
return radiance;
|
|
69
45
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::lighting_attenuation
|
|
2
|
+
|
|
3
|
+
// Shared punctual light optics. Range is represented by the host-derived
|
|
4
|
+
// inverse squared range; the shader never reconstructs authoring units.
|
|
5
|
+
fn evalDistanceAttenuation(dSquared : f32, invRangeSquared : f32) -> f32 {
|
|
6
|
+
let safeDistance = max(dSquared, 1e-4);
|
|
7
|
+
let window = clamp(1.0 - (safeDistance * invRangeSquared) * (safeDistance * invRangeSquared), 0.0, 1.0);
|
|
8
|
+
return window * window / safeDistance;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fn evalSpotAttenuation(
|
|
12
|
+
lightPos : vec3<f32>,
|
|
13
|
+
lightDir : vec3<f32>,
|
|
14
|
+
worldPos : vec3<f32>,
|
|
15
|
+
cosInner : f32,
|
|
16
|
+
cosOuter : f32,
|
|
17
|
+
invRangeSquared : f32,
|
|
18
|
+
) -> f32 {
|
|
19
|
+
let toLight = lightPos - worldPos;
|
|
20
|
+
let dSquared = dot(toLight, toLight);
|
|
21
|
+
let distance = evalDistanceAttenuation(dSquared, invRangeSquared);
|
|
22
|
+
let direction = normalize(select(vec3<f32>(0.0, 0.0, -1.0), toLight, dSquared > 1e-4));
|
|
23
|
+
let cone = smoothstep(cosOuter, cosInner, dot(direction, -normalize(lightDir)));
|
|
24
|
+
return distance * cone;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Isolated volume radiance helpers: this module has no resource bindings, so
|
|
28
|
+
// compute stages can reuse the exact surface attenuation and cone facts.
|
|
29
|
+
fn evalVolumePointRadiance(
|
|
30
|
+
lightPos : vec3<f32>,
|
|
31
|
+
colorTimesIntensity : vec3<f32>,
|
|
32
|
+
invRangeSquared : f32,
|
|
33
|
+
worldPos : vec3<f32>,
|
|
34
|
+
) -> vec3<f32> {
|
|
35
|
+
let toLight = lightPos - worldPos;
|
|
36
|
+
let dSquared = max(dot(toLight, toLight), 1e-4);
|
|
37
|
+
return colorTimesIntensity * evalDistanceAttenuation(dSquared, invRangeSquared);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn evalVolumeSpotRadiance(
|
|
41
|
+
lightPos : vec3<f32>,
|
|
42
|
+
lightDir : vec3<f32>,
|
|
43
|
+
colorTimesIntensity : vec3<f32>,
|
|
44
|
+
cosInner : f32,
|
|
45
|
+
cosOuter : f32,
|
|
46
|
+
invRangeSquared : f32,
|
|
47
|
+
worldPos : vec3<f32>,
|
|
48
|
+
) -> vec3<f32> {
|
|
49
|
+
return colorTimesIntensity * evalSpotAttenuation(
|
|
50
|
+
lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn projectSpotUv(lightViewProj : mat4x4<f32>, worldPos : vec3<f32>) -> vec2<f32> {
|
|
55
|
+
let clip = lightViewProj * vec4<f32>(worldPos, 1.0);
|
|
56
|
+
let invW = select(1.0 / clip.w, 0.0, abs(clip.w) < 1e-6);
|
|
57
|
+
return vec2<f32>(clip.x * invW * 0.5 + 0.5, clip.y * invW * -0.5 + 0.5);
|
|
58
|
+
}
|
|
@@ -238,6 +238,7 @@ fn _sampleShadowForCascade(
|
|
|
238
238
|
count : u32,
|
|
239
239
|
normal : vec3<f32>,
|
|
240
240
|
l : vec3<f32>,
|
|
241
|
+
useNormalBias : bool,
|
|
241
242
|
) -> f32 {
|
|
242
243
|
let lvp = _cascadeLightViewProj(layer);
|
|
243
244
|
let lightClip = lvp * vec4<f32>(worldPos, 1.0);
|
|
@@ -253,7 +254,7 @@ fn _sampleShadowForCascade(
|
|
|
253
254
|
// shadow fields carried in the View UBO -- normalBias scales the
|
|
254
255
|
// (1 - N.L) slope term, depthBias is the constant floor. Replaces the prior
|
|
255
256
|
// hardcoded max(0.05*(1-N.L), 0.005).
|
|
256
|
-
let bias = max(view.normalBias * (1.0 - dot(normal, l)), view.depthBias);
|
|
257
|
+
let bias = select(view.depthBias, max(view.normalBias * (1.0 - dot(normal, l)), view.depthBias), useNormalBias);
|
|
257
258
|
let adjustedDepth = currentDepth - bias;
|
|
258
259
|
// NaN-safe bounds: relational < and > do not reject NaN (NaN < 0 is
|
|
259
260
|
// false), so a zero / degenerate lightViewProj matrix that produces
|
|
@@ -414,7 +415,7 @@ fn evalDirectionalShadowFactor(
|
|
|
414
415
|
return 1.0;
|
|
415
416
|
}
|
|
416
417
|
let layer = _pickCascadeLayer(viewDepth, count);
|
|
417
|
-
let shadowCurr = _sampleShadowForCascade(worldPos, layer, count, normal, l);
|
|
418
|
+
let shadowCurr = _sampleShadowForCascade(worldPos, layer, count, normal, l, true);
|
|
418
419
|
|
|
419
420
|
var shadow = shadowCurr;
|
|
420
421
|
if (view.cascadeBlend > 0.0 && layer + 1u < count) {
|
|
@@ -424,7 +425,7 @@ fn evalDirectionalShadowFactor(
|
|
|
424
425
|
let dist = spCurr - viewDepth;
|
|
425
426
|
let t = clamp(1.0 - dist / blendWidth, 0.0, 1.0);
|
|
426
427
|
if (t > 0.0) {
|
|
427
|
-
let shadowNext = _sampleShadowForCascade(worldPos, layer + 1u, count, normal, l);
|
|
428
|
+
let shadowNext = _sampleShadowForCascade(worldPos, layer + 1u, count, normal, l, true);
|
|
428
429
|
shadow = mix(shadowCurr, shadowNext, t);
|
|
429
430
|
}
|
|
430
431
|
}
|
|
@@ -432,6 +433,33 @@ fn evalDirectionalShadowFactor(
|
|
|
432
433
|
return shadow;
|
|
433
434
|
}
|
|
434
435
|
|
|
436
|
+
// Volume receivers are samples in a medium, not surfaces. They therefore use
|
|
437
|
+
// the same cascade selection, atlas placement, PCF, and shadow-distance SSOT
|
|
438
|
+
// but only the bounded constant receiver bias; a ray direction is never
|
|
439
|
+
// treated as a geometric normal for normalBias.
|
|
440
|
+
fn evalDirectionalVolumeShadowFactor(
|
|
441
|
+
worldPos : vec3<f32>,
|
|
442
|
+
viewDepth : f32,
|
|
443
|
+
) -> f32 {
|
|
444
|
+
if (view.cascadeCount < 1.0 || viewDepth > view.splitPlanes[u32(max(view.cascadeCount, 1.0)) - 1u].x) {
|
|
445
|
+
return 1.0;
|
|
446
|
+
}
|
|
447
|
+
let count = u32(max(view.cascadeCount, 1.0));
|
|
448
|
+
let layer = _pickCascadeLayer(viewDepth, count);
|
|
449
|
+
let lightDirection = normalize(-view.lightDir);
|
|
450
|
+
let current = _sampleShadowForCascade(worldPos, layer, count, vec3<f32>(0.0), lightDirection, false);
|
|
451
|
+
if (view.cascadeBlend <= 0.0 || layer + 1u >= count) {
|
|
452
|
+
return current;
|
|
453
|
+
}
|
|
454
|
+
let blendWidth = view.splitPlanes[layer].x * view.cascadeBlend;
|
|
455
|
+
if (blendWidth <= 0.0) {
|
|
456
|
+
return current;
|
|
457
|
+
}
|
|
458
|
+
let blend = clamp(1.0 - (view.splitPlanes[layer].x - viewDepth) / blendWidth, 0.0, 1.0);
|
|
459
|
+
let next = _sampleShadowForCascade(worldPos, layer + 1u, count, vec3<f32>(0.0), lightDirection, false);
|
|
460
|
+
return mix(current, next, blend);
|
|
461
|
+
}
|
|
462
|
+
|
|
435
463
|
// `evalDirectional` evaluates the GGX direct-lighting term for the single
|
|
436
464
|
// directional light carried in `view.lightDir / view.lightColor`. CSM
|
|
437
465
|
// 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}
|
|
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,45 @@
|
|
|
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
|
+
|
|
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
|
+
|
|
51
91
|
// Shared punctual BRDF body returning (diffuse + specular) *
|
|
52
92
|
// colorTimesIntensity * nDotL * attenuation. Cone factor is applied by the
|
|
53
93
|
// caller (evalSpot only).
|
|
@@ -75,8 +115,7 @@ fn evalPunctualBody(
|
|
|
75
115
|
let specular = d_ggx(nDotH, alphaSq) * v_smith(nDotV, nDotL, alphaSq) * f;
|
|
76
116
|
let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);
|
|
77
117
|
let diffuse = kd * baseColor / 3.14159265;
|
|
78
|
-
let
|
|
79
|
-
let attenuation = factor * factor / dSquared;
|
|
118
|
+
let attenuation = evalDistanceAttenuation(dSquared, invRangeSquared);
|
|
80
119
|
return (diffuse + specular) * colorTimesIntensity * nDotL * attenuation;
|
|
81
120
|
}
|
|
82
121
|
|
|
@@ -185,10 +224,9 @@ fn evalSpot(
|
|
|
185
224
|
lightPos, colorTimesIntensity, invRangeSquared,
|
|
186
225
|
worldPos, normal, viewDir, baseColor, metallic, alphaSq, F0,
|
|
187
226
|
);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return body * cone;
|
|
227
|
+
return body * evalSpotAttenuation(
|
|
228
|
+
lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
|
|
229
|
+
) / max(evalDistanceAttenuation(dot(lightPos - worldPos, lightPos - worldPos), invRangeSquared), 1e-4);
|
|
192
230
|
}
|
|
193
231
|
|
|
194
232
|
// feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-3 + D-4 +
|
|
@@ -234,6 +272,8 @@ fn evalSpotShadowed(
|
|
|
234
272
|
shadowAtlasTile : i32,
|
|
235
273
|
depthBias : f32,
|
|
236
274
|
normalBias : f32,
|
|
275
|
+
pcfKernelSize : f32,
|
|
276
|
+
shadowIntensity : f32,
|
|
237
277
|
) -> vec3<f32> {
|
|
238
278
|
let body = evalSpot(
|
|
239
279
|
lightPos, lightDir, colorTimesIntensity, cosInner, cosOuter, invRangeSquared,
|
|
@@ -245,10 +285,11 @@ fn evalSpotShadowed(
|
|
|
245
285
|
// Perspective divide; guard a zero/near-zero w (fragment behind the light or
|
|
246
286
|
// a degenerate matrix) so the OOB gate below catches it as fully lit.
|
|
247
287
|
let invW = select(1.0 / splane.w, 0.0, abs(splane.w) < 1e-6);
|
|
248
|
-
let
|
|
288
|
+
let clipUv = projectSpotUv(lightViewProj, worldPos);
|
|
249
289
|
let depthRef = splane.z * invW;
|
|
250
|
-
//
|
|
251
|
-
|
|
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;
|
|
252
293
|
|
|
253
294
|
// OOB / NaN gate: outside the light frustum (or NaN from a degenerate matrix)
|
|
254
295
|
// returns fully lit. Mirrors the directional `>= 0 && <= 1` NaN-safe form.
|
|
@@ -267,8 +308,12 @@ fn evalSpotShadowed(
|
|
|
267
308
|
let texel = vec2<f32>(1.0, 1.0) / atlasDims;
|
|
268
309
|
|
|
269
310
|
let nDotL = max(dot(normal, normalize(lightPos - worldPos)), 0.0);
|
|
270
|
-
let shadowFactor =
|
|
311
|
+
let shadowFactor = sample_shadow_2d_kernel(
|
|
271
312
|
spotShadowMap, shadowSampler, atlasUv, texel, depthRef, normalBias, depthBias, nDotL,
|
|
313
|
+
pcfKernelSize,
|
|
272
314
|
);
|
|
273
|
-
|
|
315
|
+
// Three's SpotLight.shadow.intensity is the opacity of the shadow, not a
|
|
316
|
+
// multiplier on the light's candela radiance. A value of 1 keeps the
|
|
317
|
+
// sampled visibility fully opaque; 0 leaves the unshadowed body intact.
|
|
318
|
+
return body * mix(1.0, shadowFactor, clamp(shadowIntensity, 0.0, 1.0));
|
|
274
319
|
}
|
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/shadow-pcf.wgsl
CHANGED
|
@@ -49,6 +49,22 @@ fn sample_shadow_2d(
|
|
|
49
49
|
normalBias : f32,
|
|
50
50
|
depthBias : f32,
|
|
51
51
|
nDotL : f32,
|
|
52
|
+
) -> f32 {
|
|
53
|
+
return sample_shadow_2d_kernel(
|
|
54
|
+
shadowMap, shadowSampler, uv, texel, depthRef, normalBias, depthBias, nDotL, 3.0,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn sample_shadow_2d_kernel(
|
|
59
|
+
shadowMap : texture_depth_2d,
|
|
60
|
+
shadowSampler: sampler_comparison,
|
|
61
|
+
uv : vec2<f32>,
|
|
62
|
+
texel : vec2<f32>,
|
|
63
|
+
depthRef : f32,
|
|
64
|
+
normalBias : f32,
|
|
65
|
+
depthBias : f32,
|
|
66
|
+
nDotL : f32,
|
|
67
|
+
kernelSize : f32,
|
|
52
68
|
) -> f32 {
|
|
53
69
|
// feat-20260621-merge-directionallightshadow-into-directionallight M3 / m3-t5:
|
|
54
70
|
// param names aligned to the D-1 directional convention -- the slope
|
|
@@ -60,17 +76,22 @@ fn sample_shadow_2d(
|
|
|
60
76
|
let bias = max(normalBias * (1.0 - nDotL), depthBias / 1000.0);
|
|
61
77
|
let adjustedDepth = depthRef - bias;
|
|
62
78
|
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
79
|
+
let halfWidth = clamp((i32(round(kernelSize)) - 1) / 2, 0, 2);
|
|
80
|
+
// Variable odd-kernel PCF. textureSampleCompareLevel returns 1.0 when
|
|
81
|
+
// adjustedDepth < sampledDepth; sampler clamp-to-edge handles OOB texels.
|
|
66
82
|
var blocked = 0.0;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
var samples = 0.0;
|
|
84
|
+
for (var y = -2; y <= 2; y = y + 1) {
|
|
85
|
+
for (var x = -2; x <= 2; x = x + 1) {
|
|
86
|
+
if (abs(x) <= halfWidth && abs(y) <= halfWidth) {
|
|
87
|
+
let offsetUv = uv + vec2<f32>(f32(x), f32(y)) * texel;
|
|
88
|
+
let lit = textureSampleCompareLevel(shadowMap, shadowSampler, offsetUv, adjustedDepth);
|
|
89
|
+
blocked = blocked + (1.0 - lit);
|
|
90
|
+
samples = samples + 1.0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
72
93
|
}
|
|
73
|
-
return 1.0 - blocked /
|
|
94
|
+
return 1.0 - blocked / max(samples, 1.0);
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
// === Cube point-light-shadow wrapper =========================================
|
|
@@ -117,4 +138,4 @@ fn sample_shadow_cube_hw2x2(
|
|
|
117
138
|
// handles face-boundary texel fetches. Returns 1.0 = fully lit, 0.0 = fully
|
|
118
139
|
// shadowed.
|
|
119
140
|
return textureSampleCompareLevel(shadowAtlas, shadowSampler, lightLocal, layer, adjustedDepth);
|
|
120
|
-
}
|
|
141
|
+
}
|
package/src/skybox.wgsl
CHANGED
|
@@ -23,9 +23,8 @@
|
|
|
23
23
|
// @binding(3) rotation : SkyboxRotation (UBO)
|
|
24
24
|
|
|
25
25
|
#import forgeax_view::common::FullscreenOutput
|
|
26
|
-
#import forgeax_view::common::
|
|
26
|
+
#import forgeax_view::common::View
|
|
27
27
|
#import forgeax_view::common::fullscreen_triangle
|
|
28
|
-
#import forgeax_view::fog::{apply_fog}
|
|
29
28
|
#import forgeax_pbr::ibl_shared::{inverseRotateEnvironment}
|
|
30
29
|
|
|
31
30
|
@group(0) @binding(0) var cubemap : texture_cube<f32>;
|
|
@@ -67,9 +66,5 @@ fn skyboxDirection(uv : vec2<f32>) -> vec3<f32> {
|
|
|
67
66
|
fn skybox_fs(in : FullscreenOutput) -> @location(0) vec4<f32> {
|
|
68
67
|
let dir = skyboxDirection(in.uv);
|
|
69
68
|
let color = textureSample(cubemap, cubemapSampler, dir).rgb;
|
|
70
|
-
return
|
|
71
|
-
view.fog,
|
|
72
|
-
FogRay(view.cameraPos, dir, max(view.temporalProjection.y, 0.0)),
|
|
73
|
-
vec4<f32>(color, 1.0),
|
|
74
|
-
);
|
|
69
|
+
return vec4<f32>(color, 1.0);
|
|
75
70
|
}
|
package/src/sprite-lit.wgsl
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
2
2
|
#define_import_path forgeax_material::sprite-lit
|
|
3
3
|
|
|
4
|
-
#import forgeax_view::common::{View,
|
|
4
|
+
#import forgeax_view::common::{View, Mesh, InstanceData, PointLight, SpotLight, view, meshes, instances, pointLightsBuffer, spotLightsBuffer, sampleMaterialTexture}
|
|
5
5
|
#import forgeax_scene_temporal::{packSceneTemporalV1}
|
|
6
|
-
#import forgeax_view::fog::{apply_fog}
|
|
7
6
|
|
|
8
7
|
// @forgeax/engine-shader - sprite-lit.wgsl
|
|
9
8
|
// (tweak-20260701-sprite-lit-flat-default-drop-ndotl-for-2d).
|
|
@@ -222,22 +221,6 @@ fn spriteLitShadeAccum(albedo : vec3<f32>, worldPos : vec3<f32>) -> vec3<f32> {
|
|
|
222
221
|
return lit;
|
|
223
222
|
}
|
|
224
223
|
|
|
225
|
-
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
226
|
-
var origin = viewParams.cameraPos;
|
|
227
|
-
var direction = normalize(worldPos - origin);
|
|
228
|
-
var rayDistance = length(worldPos - origin);
|
|
229
|
-
if (viewParams.temporalProjection.z >= 0.5) {
|
|
230
|
-
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
231
|
-
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
232
|
-
let nearPoint = nearH.xyz / nearH.w;
|
|
233
|
-
let farPoint = farH.xyz / farH.w;
|
|
234
|
-
direction = normalize(farPoint - nearPoint);
|
|
235
|
-
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
236
|
-
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
237
|
-
}
|
|
238
|
-
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
224
|
@fragment
|
|
242
225
|
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
243
226
|
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
@@ -246,8 +229,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
246
229
|
// Strict clamp 0..1 before premultiplied alpha multiply keeps the LDR
|
|
247
230
|
// path bounded even when a scene stacks many lights.
|
|
248
231
|
let lit_rgba = clamp(vec4<f32>(lit, albedo4.a), vec4<f32>(0.0), vec4<f32>(1.0));
|
|
249
|
-
let
|
|
250
|
-
let premult = vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
232
|
+
let premult = vec4<f32>(lit_rgba.rgb * lit_rgba.a, lit_rgba.a);
|
|
251
233
|
// LDR target is bgra8unorm; encode rgb via the sRGB transfer in-shader.
|
|
252
234
|
return vec4<f32>(
|
|
253
235
|
linear_to_srgb(premult.r),
|
|
@@ -266,8 +248,7 @@ fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
266
248
|
// pass absorb HDR values > 1. Alpha stays clamped because the premult
|
|
267
249
|
// math requires alpha in [0, 1].
|
|
268
250
|
let alpha = clamp(albedo4.a, 0.0, 1.0);
|
|
269
|
-
|
|
270
|
-
return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
251
|
+
return vec4<f32>(lit * alpha, alpha);
|
|
271
252
|
}
|
|
272
253
|
|
|
273
254
|
struct TemporalVsOut {
|
package/src/sprite.wgsl
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
3
3
|
#pragma variant_axis PER_INSTANCE_REGION
|
|
4
4
|
|
|
5
|
-
#import forgeax_view::common::{View,
|
|
5
|
+
#import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture}
|
|
6
6
|
#import forgeax_scene_temporal::{packSceneTemporalV1}
|
|
7
|
-
#import forgeax_view::fog::{apply_fog}
|
|
8
7
|
|
|
9
8
|
// @forgeax/engine-shader - sprite.wgsl (feat-20260520-2d-sprite-layer-mvp /
|
|
10
9
|
// M-3 / w19). 2D sprite material — third variant of the MaterialAsset
|
|
@@ -271,22 +270,6 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index)
|
|
|
271
270
|
return out;
|
|
272
271
|
}
|
|
273
272
|
|
|
274
|
-
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
275
|
-
var origin = viewParams.cameraPos;
|
|
276
|
-
var direction = normalize(worldPos - origin);
|
|
277
|
-
var rayDistance = length(worldPos - origin);
|
|
278
|
-
if (viewParams.temporalProjection.z >= 0.5) {
|
|
279
|
-
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
280
|
-
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
281
|
-
let nearPoint = nearH.xyz / nearH.w;
|
|
282
|
-
let farPoint = farH.xyz / farH.w;
|
|
283
|
-
direction = normalize(farPoint - nearPoint);
|
|
284
|
-
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
285
|
-
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
286
|
-
}
|
|
287
|
-
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
288
|
-
}
|
|
289
|
-
|
|
290
273
|
// linear_to_srgb: per-channel IEC 61966-2-1 transfer function used by the
|
|
291
274
|
// LDR fragment entry to encode linear premultiplied RGB into the bgra8unorm
|
|
292
275
|
// swap-chain storage. The bgra8unorm target is not hardware-sRGB-encoded
|
|
@@ -304,8 +287,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
304
287
|
let rgba = clamp(texel * material.colorTint, vec4<f32>(0.0), vec4<f32>(1.0));
|
|
305
288
|
// Premultiplied alpha: rgb pre-multiplied by alpha for srcFactor='one' /
|
|
306
289
|
// dstFactor='one-minus-src-alpha' blend.
|
|
307
|
-
let
|
|
308
|
-
let premult = vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
290
|
+
let premult = vec4<f32>(rgba.rgb * rgba.a, rgba.a);
|
|
309
291
|
// LDR target is bgra8unorm (blendable per WebGPU spec); hardware does not
|
|
310
292
|
// sRGB-encode bgra8unorm, so apply the transfer function in the shader.
|
|
311
293
|
// Only RGB channels are encoded; alpha is linear throughout the blend.
|
|
@@ -324,8 +306,7 @@ fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
324
306
|
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
325
307
|
// R6 mitigation: strict clamp 0..1 before premultiplied alpha multiply.
|
|
326
308
|
let rgba = clamp(texel * material.colorTint, vec4<f32>(0.0), vec4<f32>(1.0));
|
|
327
|
-
|
|
328
|
-
return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
309
|
+
return vec4<f32>(rgba.rgb * rgba.a, rgba.a);
|
|
329
310
|
}
|
|
330
311
|
|
|
331
312
|
struct TemporalVsOut {
|