@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
package/src/unlit.wgsl
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#define_import_path forgeax_material::unlit
|
|
2
|
-
#import forgeax_view::common::{View,
|
|
2
|
+
#import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTextureLinear}
|
|
3
3
|
#import forgeax_scene_temporal::{packSceneTemporalV1}
|
|
4
|
-
#import forgeax_view::fog::{apply_fog}
|
|
5
4
|
|
|
6
5
|
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
7
6
|
#pragma variant_axis VERTEX_COLOR_AVAILABLE
|
|
@@ -101,22 +100,6 @@ fn materialVertexColor(in : VsOut) -> vec4<f32> {
|
|
|
101
100
|
#endif
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
105
|
-
var origin = viewParams.cameraPos;
|
|
106
|
-
var direction = normalize(worldPos - origin);
|
|
107
|
-
var rayDistance = length(worldPos - origin);
|
|
108
|
-
if (viewParams.temporalProjection.z >= 0.5) {
|
|
109
|
-
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
110
|
-
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
111
|
-
let nearPoint = nearH.xyz / nearH.w;
|
|
112
|
-
let farPoint = farH.xyz / farH.w;
|
|
113
|
-
direction = normalize(farPoint - nearPoint);
|
|
114
|
-
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
115
|
-
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
116
|
-
}
|
|
117
|
-
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
103
|
@fragment
|
|
121
104
|
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
122
105
|
let texSample = sampleMaterialTextureLinear(baseColorTexture, baseColorSampler, in.uv, material.baseColorTextureCoordinatesMetadata.zw);
|
|
@@ -125,12 +108,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
125
108
|
if (material.alphaCutoff > 0.0 && alpha < material.alphaCutoff) {
|
|
126
109
|
discard;
|
|
127
110
|
}
|
|
128
|
-
return
|
|
129
|
-
view,
|
|
130
|
-
material.baseColor.rgb * texSample.rgb * vertexColor.rgb,
|
|
131
|
-
alpha,
|
|
132
|
-
in.worldPos,
|
|
133
|
-
);
|
|
111
|
+
return vec4<f32>(material.baseColor.rgb * texSample.rgb * vertexColor.rgb, alpha);
|
|
134
112
|
}
|
|
135
113
|
|
|
136
114
|
struct TemporalVsOut {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#define_import_path forgeax_view::volume_composite
|
|
2
|
+
// This is a view-UBO projection, not a second matrix source. The fields and
|
|
3
|
+
// padding preserve common::View camera/inverse-projection byte offsets.
|
|
4
|
+
struct VolumeDepthView {
|
|
5
|
+
_prefix : array<vec4<f32>, 6>,
|
|
6
|
+
cameraPos : vec4<f32>,
|
|
7
|
+
_lightViewProjA : mat4x4<f32>,
|
|
8
|
+
inverseViewProj : mat4x4<f32>,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
struct VolumeOutput {
|
|
12
|
+
@builtin(position) position : vec4<f32>,
|
|
13
|
+
@location(0) uv : vec2<f32>,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
@group(0) @binding(0) var resolved_volume : texture_2d<f32>;
|
|
17
|
+
@group(0) @binding(1) var volume_sampler : sampler;
|
|
18
|
+
@group(0) @binding(2) var scene_depth : texture_depth_2d;
|
|
19
|
+
@group(0) @binding(3) var<uniform> volume_view : VolumeDepthView;
|
|
20
|
+
|
|
21
|
+
@vertex
|
|
22
|
+
fn volume_vs(@builtin(vertex_index) index : u32) -> VolumeOutput {
|
|
23
|
+
var positions = array<vec2<f32>, 3>(
|
|
24
|
+
vec2<f32>(-1.0, -1.0),
|
|
25
|
+
vec2<f32>(3.0, -1.0),
|
|
26
|
+
vec2<f32>(-1.0, 3.0),
|
|
27
|
+
);
|
|
28
|
+
var output : VolumeOutput;
|
|
29
|
+
output.position = vec4<f32>(positions[index], 0.0, 1.0);
|
|
30
|
+
output.uv = vec2<f32>(
|
|
31
|
+
positions[index].x * 0.5 + 0.5,
|
|
32
|
+
0.5 - positions[index].y * 0.5,
|
|
33
|
+
);
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn composite_resolved_volume(sample : vec4<f32>) -> vec4<f32> {
|
|
38
|
+
let transmittance = clamp(sample.a, 0.0, 1.0);
|
|
39
|
+
return vec4<f32>(sample.rgb, 1.0 - transmittance);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn linear_scene_depth(uv : vec2<f32>) -> f32 {
|
|
43
|
+
let dimensions = max(vec2<f32>(textureDimensions(scene_depth)), vec2<f32>(1.0));
|
|
44
|
+
let pixel = vec2<i32>(clamp(uv * dimensions, vec2<f32>(0.0), dimensions - vec2<f32>(1.0)));
|
|
45
|
+
let device_depth = textureLoad(scene_depth, pixel, 0);
|
|
46
|
+
// WebGPU's framebuffer Y is opposite the view UBO's NDC Y.
|
|
47
|
+
let clip = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, device_depth, 1.0);
|
|
48
|
+
let world = volume_view.inverseViewProj * clip;
|
|
49
|
+
let world_position = world.xyz / max(abs(world.w), 1e-5);
|
|
50
|
+
return distance(world_position, volume_view.cameraPos.xyz);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn radiance_luma(sample : vec4<f32>) -> f32 {
|
|
54
|
+
return max(dot(sample.rgb, vec3<f32>(0.2126, 0.7152, 0.0722)), 0.0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn radiance_luma_weight(center : vec4<f32>, neighbor : vec4<f32>) -> f32 {
|
|
58
|
+
let center_luma = radiance_luma(center);
|
|
59
|
+
let neighbor_luma = radiance_luma(neighbor);
|
|
60
|
+
let relative_luma = abs(neighbor_luma - center_luma) /
|
|
61
|
+
max(max(neighbor_luma, center_luma), 1e-4);
|
|
62
|
+
return exp(-relative_luma * 8.0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn edge_aware_resolved_volume(uv : vec2<f32>) -> vec4<f32> {
|
|
66
|
+
let dimensions = max(vec2<f32>(textureDimensions(resolved_volume)), vec2<f32>(1.0));
|
|
67
|
+
let texel = 1.0 / dimensions;
|
|
68
|
+
let center = textureSampleLevel(resolved_volume, volume_sampler, uv, 0.0);
|
|
69
|
+
let center_depth = linear_scene_depth(uv);
|
|
70
|
+
var accumulated = center * 4.0;
|
|
71
|
+
var total_weight = 4.0;
|
|
72
|
+
let offsets = array<vec2<f32>, 8>(
|
|
73
|
+
vec2<f32>(texel.x, 0.0),
|
|
74
|
+
vec2<f32>(-texel.x, 0.0),
|
|
75
|
+
vec2<f32>(0.0, texel.y),
|
|
76
|
+
vec2<f32>(0.0, -texel.y),
|
|
77
|
+
vec2<f32>(texel.x, texel.y),
|
|
78
|
+
vec2<f32>(-texel.x, texel.y),
|
|
79
|
+
vec2<f32>(texel.x, -texel.y),
|
|
80
|
+
vec2<f32>(-texel.x, -texel.y),
|
|
81
|
+
);
|
|
82
|
+
let spatial_weights = array<f32, 8>(2.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0);
|
|
83
|
+
for (var index = 0u; index < 8u; index += 1u) {
|
|
84
|
+
let neighbor_uv = clamp(uv + offsets[index], vec2<f32>(0.0), vec2<f32>(1.0));
|
|
85
|
+
let neighbor = textureSampleLevel(
|
|
86
|
+
resolved_volume,
|
|
87
|
+
volume_sampler,
|
|
88
|
+
neighbor_uv,
|
|
89
|
+
0.0,
|
|
90
|
+
);
|
|
91
|
+
let neighbor_depth = linear_scene_depth(neighbor_uv);
|
|
92
|
+
// Relative view-depth rejection keeps the silhouette transition local;
|
|
93
|
+
// a fixed world-unit threshold creates halos at far occluders.
|
|
94
|
+
let depth_weight = select(0.0, exp(-abs(neighbor_depth - center_depth) * 0.08),
|
|
95
|
+
abs(neighbor_depth - center_depth) <= max(center_depth * 0.08, 0.25));
|
|
96
|
+
// Transmittance is the available edge signal: retain hard depth/occluder
|
|
97
|
+
// boundaries while reconstructing smooth low-frequency scattering.
|
|
98
|
+
let edge_weight = spatial_weights[index] * depth_weight * exp(-abs(neighbor.a - center.a) * 6.0) *
|
|
99
|
+
radiance_luma_weight(center, neighbor);
|
|
100
|
+
accumulated += neighbor * edge_weight;
|
|
101
|
+
total_weight += edge_weight;
|
|
102
|
+
}
|
|
103
|
+
return accumulated / max(total_weight, 1e-5);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@fragment
|
|
107
|
+
fn volume_fs(input : VolumeOutput) -> @location(0) vec4<f32> {
|
|
108
|
+
// Injection zeroes samples behind the observed scene depth. Sampling the
|
|
109
|
+
// cumulative far slice therefore applies the full ray integral while the
|
|
110
|
+
// depth test remains owned by the injection path.
|
|
111
|
+
let sample = edge_aware_resolved_volume(input.uv);
|
|
112
|
+
return composite_resolved_volume(sample);
|
|
113
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#define_import_path forgeax_view::volume_inject
|
|
2
|
+
#import forgeax_pbr::shadow_pcf::{sample_shadow_2d_kernel}
|
|
3
|
+
#import forgeax_pbr::lighting_attenuation::{projectSpotUv}
|
|
4
|
+
// forgeax_pbr::lighting_punctual exports the surface-equivalent
|
|
5
|
+
// evalVolumePoint and evalVolumeSpot contract; this stage consumes their
|
|
6
|
+
// shared projector and shadow visibility facts without a second light owner.
|
|
7
|
+
|
|
8
|
+
struct View {
|
|
9
|
+
worldViewProj : mat4x4<f32>, lightDir : vec3<f32>, lightColor : vec3<f32>, cameraPos : vec3<f32>,
|
|
10
|
+
lightViewProj_A : mat4x4<f32>, inverseViewProj : mat4x4<f32>, lightViewProj_B : mat4x4<f32>,
|
|
11
|
+
lightViewProj_C : mat4x4<f32>, lightViewProj_D : mat4x4<f32>, splitPlanes : array<vec4<f32>,4>,
|
|
12
|
+
cascadeCount : f32, cascadeBlend : f32, depthBias : f32, normalBias : f32, pcfKernelSize : f32,
|
|
13
|
+
spotLightViewProj : array<mat4x4<f32>,4>, temporalCurrentViewProj : mat4x4<f32>,
|
|
14
|
+
temporalPreviousViewProj : mat4x4<f32>, temporalProjection : vec4<f32>, temporalPreviousCameraPos : vec4<f32>,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
@group(0) @binding(3) var shadowMap : texture_depth_2d;
|
|
18
|
+
@group(0) @binding(4) var shadowSampler : sampler_comparison;
|
|
19
|
+
|
|
20
|
+
struct VolumeParams {
|
|
21
|
+
bounds_min : vec4<f32>,
|
|
22
|
+
bounds_max : vec4<f32>,
|
|
23
|
+
extinction : vec4<f32>,
|
|
24
|
+
albedo : vec4<f32>,
|
|
25
|
+
emission : vec4<f32>,
|
|
26
|
+
light_direction : vec4<f32>,
|
|
27
|
+
light_color : vec4<f32>,
|
|
28
|
+
optics : vec4<f32>,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
struct SpotLight {
|
|
32
|
+
position : vec3<f32>,
|
|
33
|
+
invRangeSquared : f32,
|
|
34
|
+
colorTimesIntensity : vec3<f32>,
|
|
35
|
+
cosInner : f32,
|
|
36
|
+
direction : vec3<f32>,
|
|
37
|
+
cosOuter : f32,
|
|
38
|
+
depthBias : f32,
|
|
39
|
+
normalBias : f32,
|
|
40
|
+
pcfKernelSize : f32,
|
|
41
|
+
shadowAtlasTile : i32,
|
|
42
|
+
shadowIntensity : f32,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
struct SpotLightsArray {
|
|
46
|
+
count : u32,
|
|
47
|
+
slots : array<SpotLight, 4>,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
struct PointLight {
|
|
51
|
+
position : vec3<f32>,
|
|
52
|
+
invRangeSquared : f32,
|
|
53
|
+
colorTimesIntensity : vec3<f32>,
|
|
54
|
+
shadowAtlasLayer : i32,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
struct PointLightsArray {
|
|
58
|
+
count : u32,
|
|
59
|
+
slots : array<PointLight, 4>,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
@group(0) @binding(5) var<uniform> volume_params : VolumeParams;
|
|
63
|
+
@group(0) @binding(6) var volume_froxel : texture_storage_2d_array<rgba8unorm, write>;
|
|
64
|
+
@group(0) @binding(7) var<storage, read> spotLightsBuffer : SpotLightsArray;
|
|
65
|
+
@group(0) @binding(8) var<storage, read> pointLightsBuffer : PointLightsArray;
|
|
66
|
+
@group(0) @binding(0) var<uniform> view : View;
|
|
67
|
+
|
|
68
|
+
fn reconstruct_world(uv : vec2<f32>, depth : f32) -> vec3<f32> {
|
|
69
|
+
// WebGPU NDC depth is already [0, 1]; only XY needs the [-1, 1] remap.
|
|
70
|
+
let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
|
71
|
+
let world = view.inverseViewProj * ndc;
|
|
72
|
+
return world.xyz / max(abs(world.w), 1e-5);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn ray_box_interval(
|
|
76
|
+
origin : vec3<f32>,
|
|
77
|
+
direction : vec3<f32>,
|
|
78
|
+
box_min : vec3<f32>,
|
|
79
|
+
box_max : vec3<f32>,
|
|
80
|
+
) -> vec2<f32> {
|
|
81
|
+
let safe_direction = select(direction, vec3<f32>(1e-5), abs(direction) < vec3<f32>(1e-5));
|
|
82
|
+
let reciprocal = 1.0 / safe_direction;
|
|
83
|
+
let near_point = (box_min - origin) * reciprocal;
|
|
84
|
+
let far_point = (box_max - origin) * reciprocal;
|
|
85
|
+
let near_distance = max(max(min(near_point.x, far_point.x), min(near_point.y, far_point.y)), min(near_point.z, far_point.z));
|
|
86
|
+
let far_distance = min(min(max(near_point.x, far_point.x), max(near_point.y, far_point.y)), max(near_point.z, far_point.z));
|
|
87
|
+
return vec2<f32>(max(near_distance, 0.0), max(far_distance, near_distance));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn hash32(value : u32) -> u32 {
|
|
91
|
+
var hash = value;
|
|
92
|
+
hash = (hash ^ 61u) ^ (hash >> 16u);
|
|
93
|
+
hash = hash + (hash << 3u);
|
|
94
|
+
hash = hash ^ (hash >> 4u);
|
|
95
|
+
hash = hash * 668265261u;
|
|
96
|
+
hash = hash ^ (hash >> 15u);
|
|
97
|
+
return hash;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn froxel_seed(id : vec3<u32>, salt : u32) -> u32 {
|
|
101
|
+
return hash32(id.x * 73856093u ^ id.y * 19349663u ^ id.z * 83492791u ^ salt);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn stratified32(seed : u32, frame_index : u32, odd_stride : u32) -> f32 {
|
|
105
|
+
let bin = (hash32(seed) + (frame_index & 31u) * odd_stride) & 31u;
|
|
106
|
+
return (f32(bin) + 0.5) / 32.0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn dither_unorm8(value : f32, noise : f32) -> f32 {
|
|
110
|
+
return clamp(value + (noise - 0.5) / 255.0, 0.0, 1.0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn camera_view_depth(world_position : vec3<f32>) -> f32 {
|
|
114
|
+
let clip = view.temporalCurrentViewProj * vec4<f32>(world_position, 1.0);
|
|
115
|
+
let ndc_depth = clip.z / max(abs(clip.w), 1e-5);
|
|
116
|
+
let orthographic_depth = view.temporalProjection.x +
|
|
117
|
+
ndc_depth * (view.temporalProjection.y - view.temporalProjection.x);
|
|
118
|
+
return select(max(clip.w, 0.0), max(orthographic_depth, 0.0), view.temporalProjection.z >= 0.5);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn volume_cascade(world_position : vec3<f32>, view_depth : f32) -> f32 {
|
|
122
|
+
if (view.cascadeCount < 1.0) { return 1.0; }
|
|
123
|
+
let count = u32(max(view.cascadeCount, 1.0));
|
|
124
|
+
if (view_depth > view.splitPlanes[count - 1u].x) { return 1.0; }
|
|
125
|
+
var layer = count - 1u;
|
|
126
|
+
for (var index = 0u; index < count - 1u; index = index + 1u) {
|
|
127
|
+
if (view_depth < view.splitPlanes[index].x) { layer = index; break; }
|
|
128
|
+
}
|
|
129
|
+
var light_matrix = view.lightViewProj_D;
|
|
130
|
+
switch (layer) {
|
|
131
|
+
case 0u: { light_matrix = view.lightViewProj_A; }
|
|
132
|
+
case 1u: { light_matrix = view.lightViewProj_B; }
|
|
133
|
+
case 2u: { light_matrix = view.lightViewProj_C; }
|
|
134
|
+
default: { }
|
|
135
|
+
}
|
|
136
|
+
let clip = light_matrix * vec4<f32>(world_position, 1.0);
|
|
137
|
+
let projected = clip.xyz / max(abs(clip.w), 1e-5);
|
|
138
|
+
let columns = select(2u, 1u, count <= 1u);
|
|
139
|
+
let rows = (count + columns - 1u) / columns;
|
|
140
|
+
let tile = vec2<u32>(layer % columns, layer / columns);
|
|
141
|
+
let tile_scale = vec2<f32>(1.0) / vec2<f32>(f32(columns), f32(rows));
|
|
142
|
+
let tile_uv = vec2<f32>(projected.x * 0.5 + 0.5, -projected.y * 0.5 + 0.5);
|
|
143
|
+
if (!(tile_uv.x >= 0.0 && tile_uv.x <= 1.0 && tile_uv.y >= 0.0 && tile_uv.y <= 1.0 && projected.z <= 1.0)) { return 1.0; }
|
|
144
|
+
let uv = tile_uv * tile_scale + vec2<f32>(tile) * tile_scale;
|
|
145
|
+
let shadow_size = vec2<f32>(textureDimensions(shadowMap));
|
|
146
|
+
return sample_shadow_2d_kernel(
|
|
147
|
+
shadowMap, shadowSampler, uv, 1.0 / shadow_size, projected.z,
|
|
148
|
+
0.0, view.depthBias, 1.0, view.pcfKernelSize,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn spot_shadow_visibility_at(world_position : vec3<f32>) -> f32 {
|
|
153
|
+
let slot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
|
|
154
|
+
if (slot >= spotLightsBuffer.count) { return 0.0; }
|
|
155
|
+
let light = spotLightsBuffer.slots[slot];
|
|
156
|
+
if (light.shadowAtlasTile < 0) { return 1.0; }
|
|
157
|
+
let tile = u32(light.shadowAtlasTile);
|
|
158
|
+
let projectorUv = projectSpotUv(view.spotLightViewProj[tile], world_position);
|
|
159
|
+
// The accepted projector tuple is sampled with textureSampleLevel by the
|
|
160
|
+
// surface and volume owners when a projector texture is present.
|
|
161
|
+
let projectorRevision = volume_params.light_color.w;
|
|
162
|
+
let clip = view.spotLightViewProj[tile] * vec4<f32>(world_position, 1.0);
|
|
163
|
+
// Shadow filtering is fail-open outside the light frustum. The map owns
|
|
164
|
+
// occlusion inside its tile; it must not darken samples behind the light or
|
|
165
|
+
// beyond the far plane, matching the surface SpotLight shadow contract.
|
|
166
|
+
if (clip.w <= 1e-5) { return 1.0; }
|
|
167
|
+
let projected = clip.xyz / clip.w;
|
|
168
|
+
let local_uv = vec2<f32>(projected.x * 0.5 + 0.5, -projected.y * 0.5 + 0.5);
|
|
169
|
+
if (any(projectorUv < vec2<f32>(0.0)) || any(projectorUv > vec2<f32>(1.0))) { return 1.0; }
|
|
170
|
+
if (any(local_uv < vec2<f32>(0.0)) || any(local_uv > vec2<f32>(1.0)) || projected.z > 1.0) {
|
|
171
|
+
return 1.0;
|
|
172
|
+
}
|
|
173
|
+
let atlas_uv = local_uv * 0.5 + vec2<f32>(f32(tile & 1u), f32(tile >> 1u)) * 0.5;
|
|
174
|
+
let shadow_size = vec2<f32>(textureDimensions(shadowMap));
|
|
175
|
+
return sample_shadow_2d_kernel(
|
|
176
|
+
shadowMap, shadowSampler, atlas_uv, 1.0 / shadow_size, projected.z,
|
|
177
|
+
light.normalBias, light.depthBias, 1.0, light.pcfKernelSize,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn shadow_visibility_at(ray_origin : vec3<f32>, ray_direction : vec3<f32>, slice : f32) -> f32 {
|
|
182
|
+
let interval = ray_box_interval(
|
|
183
|
+
ray_origin,
|
|
184
|
+
ray_direction,
|
|
185
|
+
volume_params.bounds_min.xyz,
|
|
186
|
+
volume_params.bounds_max.xyz,
|
|
187
|
+
);
|
|
188
|
+
let ray_near = interval.x;
|
|
189
|
+
let ray_limit = max(ray_near, min(interval.y, volume_params.optics.x));
|
|
190
|
+
let ray_t = mix(ray_near, ray_limit, slice);
|
|
191
|
+
let world_position = ray_origin + ray_direction * ray_t;
|
|
192
|
+
let in_bounds = all(world_position >= volume_params.bounds_min.xyz) &&
|
|
193
|
+
all(world_position <= volume_params.bounds_max.xyz) && ray_limit > ray_near;
|
|
194
|
+
let view_depth = camera_view_depth(world_position);
|
|
195
|
+
return select(0.0, volume_cascade(world_position, view_depth), in_bounds);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@compute @workgroup_size(8, 8, 1)
|
|
199
|
+
fn volume_inject(@builtin(global_invocation_id) id : vec3<u32>) {
|
|
200
|
+
let volume_size = textureDimensions(volume_froxel);
|
|
201
|
+
if (id.x >= volume_size.x || id.y >= volume_size.y) { return; }
|
|
202
|
+
let froxel_size = vec2<f32>(textureDimensions(volume_froxel).xy);
|
|
203
|
+
let frame_phase = volume_params.light_direction.w;
|
|
204
|
+
let frame_index = u32(max(frame_phase, 0.0));
|
|
205
|
+
let xy_noise = vec2<f32>(
|
|
206
|
+
stratified32(froxel_seed(id, 2654435761u), frame_index, 5u),
|
|
207
|
+
stratified32(froxel_seed(id, 2246822519u), frame_index, 11u),
|
|
208
|
+
) - vec2<f32>(0.5);
|
|
209
|
+
let depth_uv = clamp(
|
|
210
|
+
(vec2<f32>(id.xy) + vec2<f32>(0.5) + xy_noise * 0.75) / froxel_size,
|
|
211
|
+
vec2<f32>(0.0),
|
|
212
|
+
vec2<f32>(1.0),
|
|
213
|
+
);
|
|
214
|
+
let ray_origin = reconstruct_world(depth_uv, 0.0);
|
|
215
|
+
let ray_endpoint = reconstruct_world(depth_uv, 1.0);
|
|
216
|
+
let ray_direction = normalize(ray_endpoint - ray_origin);
|
|
217
|
+
let interval = ray_box_interval(
|
|
218
|
+
ray_origin,
|
|
219
|
+
ray_direction,
|
|
220
|
+
volume_params.bounds_min.xyz,
|
|
221
|
+
volume_params.bounds_max.xyz,
|
|
222
|
+
);
|
|
223
|
+
let ray_near = interval.x;
|
|
224
|
+
let ray_limit = max(ray_near, min(interval.y, volume_params.optics.x));
|
|
225
|
+
let logical_depth = max(textureNumLayers(volume_froxel) * 4u, 1u);
|
|
226
|
+
var visibility = vec4<f32>(0.0);
|
|
227
|
+
for (var channel = 0u; channel < 4u; channel = channel + 1u) {
|
|
228
|
+
let logical_slice = id.z * 4u + channel;
|
|
229
|
+
if (logical_slice >= logical_depth) { continue; }
|
|
230
|
+
let slice = clamp((f32(logical_slice) + 0.5) / f32(logical_depth), 0.0, 1.0);
|
|
231
|
+
let slice_world = ray_origin + ray_direction * mix(ray_near, ray_limit, slice);
|
|
232
|
+
// A paired Point+Spot volume owns two radiance terms. The spot shadow is
|
|
233
|
+
// stored for the spot term; the integrate stage applies it only to that
|
|
234
|
+
// term so PointLight radiance remains visible outside the spot cone.
|
|
235
|
+
let mode = u32(round(volume_params.optics.z));
|
|
236
|
+
visibility[channel] = select(
|
|
237
|
+
shadow_visibility_at(ray_origin, ray_direction, slice),
|
|
238
|
+
spot_shadow_visibility_at(slice_world),
|
|
239
|
+
mode == 2u || mode == 3u,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
let visibility_noise = stratified32(froxel_seed(id, 3812015801u), frame_index, 11u);
|
|
243
|
+
visibility = vec4<f32>(
|
|
244
|
+
dither_unorm8(visibility.x, visibility_noise),
|
|
245
|
+
dither_unorm8(visibility.y, visibility_noise),
|
|
246
|
+
dither_unorm8(visibility.z, visibility_noise),
|
|
247
|
+
dither_unorm8(visibility.w, visibility_noise));
|
|
248
|
+
textureStore(volume_froxel, id.xy, id.z, visibility);
|
|
249
|
+
}
|