@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
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#define_import_path forgeax_view::volume_integrate
|
|
2
|
+
// forgeax_pbr::lighting_punctual remains the surface contract; the isolated
|
|
3
|
+
// attenuation module below provides its resource-free volume equivalents.
|
|
4
|
+
|
|
5
|
+
struct VolumeView {
|
|
6
|
+
_prefix : array<vec4<f32>, 6>,
|
|
7
|
+
cameraPos : vec4<f32>,
|
|
8
|
+
_lightViewProjA : mat4x4<f32>,
|
|
9
|
+
inverseViewProj : mat4x4<f32>,
|
|
10
|
+
_viewTail : array<vec4<f32>, 18>,
|
|
11
|
+
spotLightViewProj : array<mat4x4<f32>, 4>,
|
|
12
|
+
};
|
|
13
|
+
struct SpotLight {
|
|
14
|
+
position : vec3<f32>, invRangeSquared : f32,
|
|
15
|
+
colorTimesIntensity : vec3<f32>, cosInner : f32,
|
|
16
|
+
direction : vec3<f32>, cosOuter : f32,
|
|
17
|
+
depthBias : f32, normalBias : f32, pcfKernelSize : f32,
|
|
18
|
+
shadowAtlasTile : i32,
|
|
19
|
+
shadowIntensity : f32,
|
|
20
|
+
};
|
|
21
|
+
struct SpotLightsArray { count : u32, slots : array<SpotLight, 4>, };
|
|
22
|
+
struct PointLight {
|
|
23
|
+
position : vec3<f32>, invRangeSquared : f32,
|
|
24
|
+
colorTimesIntensity : vec3<f32>, shadowAtlasLayer : i32,
|
|
25
|
+
};
|
|
26
|
+
struct PointLightsArray { count : u32, slots : array<PointLight, 4>, };
|
|
27
|
+
struct VolumeParams {
|
|
28
|
+
bounds_min : vec4<f32>, bounds_max : vec4<f32>, extinction : vec4<f32>, albedo : vec4<f32>,
|
|
29
|
+
emission : vec4<f32>, light_direction : vec4<f32>, light_color : vec4<f32>, optics : vec4<f32>,
|
|
30
|
+
};
|
|
31
|
+
const EPSILON : f32 = 1e-5;
|
|
32
|
+
const FOUR_PI : f32 = 12.566370614359172;
|
|
33
|
+
// The punctual light buffers carry the integrated surface-light value
|
|
34
|
+
// (color * candela * inverse-square attenuation). The normalized HG phase
|
|
35
|
+
// below is a per-steradian distribution, so the volume boundary converts the
|
|
36
|
+
// shared light value into that basis exactly once. This keeps public light
|
|
37
|
+
// units unchanged while matching the Three.js volume-lighting convention,
|
|
38
|
+
// which accumulates the same direct-light value before applying an
|
|
39
|
+
// unnormalized phase.
|
|
40
|
+
const VOLUME_LIGHT_PHASE_SCALE : f32 = FOUR_PI;
|
|
41
|
+
|
|
42
|
+
fn hash32(value : u32) -> u32 {
|
|
43
|
+
var hash = value;
|
|
44
|
+
hash = (hash ^ 61u) ^ (hash >> 16u);
|
|
45
|
+
hash = hash + (hash << 3u);
|
|
46
|
+
hash = hash ^ (hash >> 4u);
|
|
47
|
+
hash = hash * 668265261u;
|
|
48
|
+
hash = hash ^ (hash >> 15u);
|
|
49
|
+
return hash;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn froxel_seed(id : vec3<u32>, salt : u32) -> u32 {
|
|
53
|
+
return hash32(id.x * 73856093u ^ id.y * 19349663u ^ id.z * 83492791u ^ salt);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn stratified32(seed : u32, frame_index : u32, odd_stride : u32) -> f32 {
|
|
57
|
+
let bin = (hash32(seed) + (frame_index & 31u) * odd_stride) & 31u;
|
|
58
|
+
return (f32(bin) + 0.5) / 32.0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@group(0) @binding(0) var froxel : texture_2d_array<f32>;
|
|
62
|
+
@group(0) @binding(1) var scene_depth : texture_depth_2d;
|
|
63
|
+
@group(0) @binding(2) var resolved : texture_storage_2d<rgba16float, write>;
|
|
64
|
+
@group(0) @binding(3) var<uniform> volume_params : VolumeParams;
|
|
65
|
+
@group(0) @binding(4) var<uniform> volume_view : VolumeView;
|
|
66
|
+
@group(0) @binding(5) var froxel_sampler : sampler;
|
|
67
|
+
@group(0) @binding(6) var density : texture_3d<f32>;
|
|
68
|
+
@group(0) @binding(7) var density_sampler : sampler;
|
|
69
|
+
@group(0) @binding(8) var history_seed : texture_storage_2d<rgba16float, write>;
|
|
70
|
+
@group(0) @binding(9) var temporal_seed : texture_storage_2d<rgba16float, write>;
|
|
71
|
+
@group(0) @binding(10) var<storage, read> spotLightsBuffer : SpotLightsArray;
|
|
72
|
+
@group(0) @binding(11) var<storage, read> pointLightsBuffer : PointLightsArray;
|
|
73
|
+
@group(0) @binding(12) var projectorTexture : texture_2d<f32>;
|
|
74
|
+
@group(0) @binding(13) var projectorSampler : sampler;
|
|
75
|
+
|
|
76
|
+
fn hg(cos_theta : f32, anisotropy : f32) -> f32 {
|
|
77
|
+
let g = clamp(anisotropy, -0.999, 0.999);
|
|
78
|
+
let denominator = max(1.0 + g * g - 2.0 * g * clamp(cos_theta, -1.0, 1.0), EPSILON);
|
|
79
|
+
return (1.0 - g * g) / (FOUR_PI * pow(denominator, 1.5));
|
|
80
|
+
}
|
|
81
|
+
fn reconstruct_world(uv : vec2<f32>, depth : f32) -> vec3<f32> {
|
|
82
|
+
let clip = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
|
83
|
+
let world = volume_view.inverseViewProj * clip;
|
|
84
|
+
return world.xyz / max(abs(world.w), EPSILON);
|
|
85
|
+
}
|
|
86
|
+
fn ray_box_interval(origin : vec3<f32>, direction : vec3<f32>) -> vec2<f32> {
|
|
87
|
+
let safe_direction = select(direction, vec3<f32>(EPSILON), abs(direction) < vec3<f32>(EPSILON));
|
|
88
|
+
let reciprocal = 1.0 / safe_direction;
|
|
89
|
+
let a = (volume_params.bounds_min.xyz - origin) * reciprocal;
|
|
90
|
+
let b = (volume_params.bounds_max.xyz - origin) * reciprocal;
|
|
91
|
+
let near_distance = max(max(min(a.x, b.x), min(a.y, b.y)), min(a.z, b.z));
|
|
92
|
+
let far_distance = min(min(max(a.x, b.x), max(a.y, b.y)), max(a.z, b.z));
|
|
93
|
+
return vec2<f32>(max(near_distance, 0.0), max(far_distance, near_distance));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn evalSpotAttenuation(
|
|
97
|
+
lightPos : vec3<f32>, lightDir : vec3<f32>, worldPos : vec3<f32>,
|
|
98
|
+
cosInner : f32, cosOuter : f32, invRangeSquared : f32,
|
|
99
|
+
) -> f32 {
|
|
100
|
+
let toLight = lightPos - worldPos;
|
|
101
|
+
let dSquared = dot(toLight, toLight);
|
|
102
|
+
let safeDistance = max(dSquared, EPSILON);
|
|
103
|
+
let window = clamp(1.0 - (safeDistance * invRangeSquared) * (safeDistance * invRangeSquared), 0.0, 1.0);
|
|
104
|
+
let distance = window * window / safeDistance;
|
|
105
|
+
let direction = normalize(select(vec3<f32>(0.0, 0.0, -1.0), toLight, dSquared > EPSILON));
|
|
106
|
+
return distance * smoothstep(cosOuter, cosInner, dot(direction, -normalize(lightDir)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn projectSpotUv(lightViewProj : mat4x4<f32>, worldPos : vec3<f32>) -> vec2<f32> {
|
|
110
|
+
let clip = lightViewProj * vec4<f32>(worldPos, 1.0);
|
|
111
|
+
let invW = select(1.0 / clip.w, 0.0, abs(clip.w) < 1e-6);
|
|
112
|
+
return vec2<f32>(clip.x * invW * 0.5 + 0.5, clip.y * invW * -0.5 + 0.5);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn spotProjectorMatrix(light : SpotLight) -> mat4x4<f32> {
|
|
116
|
+
if (light.shadowAtlasTile >= 0) {
|
|
117
|
+
return volume_view.spotLightViewProj[light.shadowAtlasTile];
|
|
118
|
+
}
|
|
119
|
+
return volume_view.spotLightViewProj[0];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn spot_attenuation_at(world_position : vec3<f32>) -> f32 {
|
|
123
|
+
let slot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
|
|
124
|
+
if (slot >= spotLightsBuffer.count) { return 0.0; }
|
|
125
|
+
let light = spotLightsBuffer.slots[slot];
|
|
126
|
+
let projectorUv = projectSpotUv(spotProjectorMatrix(light), world_position);
|
|
127
|
+
if (any(projectorUv < vec2<f32>(0.0)) || any(projectorUv > vec2<f32>(1.0))) { return 0.0; }
|
|
128
|
+
return evalSpotAttenuation(
|
|
129
|
+
light.position,
|
|
130
|
+
light.direction,
|
|
131
|
+
world_position,
|
|
132
|
+
light.cosInner,
|
|
133
|
+
light.cosOuter,
|
|
134
|
+
light.invRangeSquared,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn evalVolumePoint(
|
|
139
|
+
lightPos : vec3<f32>, colorTimesIntensity : vec3<f32>,
|
|
140
|
+
invRangeSquared : f32, worldPos : vec3<f32>,
|
|
141
|
+
) -> vec3<f32> {
|
|
142
|
+
let toLight = lightPos - worldPos;
|
|
143
|
+
let dSquared = max(dot(toLight, toLight), EPSILON);
|
|
144
|
+
let safeDistance = max(dSquared, EPSILON);
|
|
145
|
+
let window = clamp(1.0 - (safeDistance * invRangeSquared) * (safeDistance * invRangeSquared), 0.0, 1.0);
|
|
146
|
+
return colorTimesIntensity * (window * window / safeDistance);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn evalVolumeSpot(
|
|
150
|
+
lightPos : vec3<f32>, lightDir : vec3<f32>, colorTimesIntensity : vec3<f32>,
|
|
151
|
+
cosInner : f32, cosOuter : f32, invRangeSquared : f32, worldPos : vec3<f32>,
|
|
152
|
+
) -> vec3<f32> {
|
|
153
|
+
return colorTimesIntensity * evalSpotAttenuation(
|
|
154
|
+
lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn volume_point_radiance(world_position : vec3<f32>) -> vec3<f32> {
|
|
159
|
+
let pointSlot = u32(clamp(round(volume_params.emission.w), 0.0, 3.0));
|
|
160
|
+
if (pointSlot >= pointLightsBuffer.count) { return vec3<f32>(0.0); }
|
|
161
|
+
let point = pointLightsBuffer.slots[pointSlot];
|
|
162
|
+
return evalVolumePoint(
|
|
163
|
+
point.position, point.colorTimesIntensity, point.invRangeSquared, world_position,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn volume_spot_radiance(world_position : vec3<f32>) -> vec3<f32> {
|
|
168
|
+
let spotSlot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
|
|
169
|
+
if (spotSlot >= spotLightsBuffer.count) { return vec3<f32>(0.0); }
|
|
170
|
+
let spot = spotLightsBuffer.slots[spotSlot];
|
|
171
|
+
let radiance = evalVolumeSpot(
|
|
172
|
+
spot.position, spot.direction, spot.colorTimesIntensity,
|
|
173
|
+
spot.cosInner, spot.cosOuter, spot.invRangeSquared, world_position,
|
|
174
|
+
);
|
|
175
|
+
let projectorUv = projectSpotUv(spotProjectorMatrix(spot), world_position);
|
|
176
|
+
if (!(projectorUv.x >= 0.0 && projectorUv.x <= 1.0 && projectorUv.y >= 0.0 && projectorUv.y <= 1.0)) {
|
|
177
|
+
// A SpotLight map modulates the light only inside its projected tile.
|
|
178
|
+
// Three keeps the cone/attenuation result outside that tile; the map is
|
|
179
|
+
// not a second frustum mask.
|
|
180
|
+
return radiance;
|
|
181
|
+
}
|
|
182
|
+
return radiance * textureSampleLevel(projectorTexture, projectorSampler, projectorUv, 0.0).rgb;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fn volume_spot_shadow_intensity() -> f32 {
|
|
186
|
+
let spotSlot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
|
|
187
|
+
if (spotSlot >= spotLightsBuffer.count) { return 1.0; }
|
|
188
|
+
return clamp(spotLightsBuffer.slots[spotSlot].shadowIntensity, 0.0, 1.0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fn volume_light_radiance(world_position : vec3<f32>) -> vec3<f32> {
|
|
192
|
+
let mode = u32(round(volume_params.optics.z));
|
|
193
|
+
if (mode == 0u) { return volume_params.light_color.xyz; }
|
|
194
|
+
var radiance = vec3<f32>(0.0);
|
|
195
|
+
if (mode == 1u || mode == 3u) {
|
|
196
|
+
radiance = radiance + volume_point_radiance(world_position);
|
|
197
|
+
}
|
|
198
|
+
if (mode == 2u || mode == 3u) {
|
|
199
|
+
radiance = radiance + volume_spot_radiance(world_position);
|
|
200
|
+
}
|
|
201
|
+
return radiance;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn volume_light_radiance_pair(world_position : vec3<f32>, spot_visibility : f32) -> vec3<f32> {
|
|
205
|
+
// Keep the pair's PointLight term independent from the SpotLight shadow.
|
|
206
|
+
return volume_point_radiance(world_position) +
|
|
207
|
+
volume_spot_radiance(world_position) *
|
|
208
|
+
mix(1.0, spot_visibility, volume_spot_shadow_intensity());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn isFinite(value : f32) -> bool {
|
|
212
|
+
return value == value && abs(value) < 3.402823e+38;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn finiteVec3(value : vec3<f32>) -> bool {
|
|
216
|
+
return isFinite(value.x) && isFinite(value.y) && isFinite(value.z);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Match the pinned Three.js VolumeNodeMaterial density expression while
|
|
220
|
+
// keeping the authored TextureAsset and its renderer-owned sampler intact.
|
|
221
|
+
fn volume_sample_grain(
|
|
222
|
+
position : vec3<f32>,
|
|
223
|
+
scale : f32,
|
|
224
|
+
time_scaled : vec3<f32>,
|
|
225
|
+
time_scale : f32,
|
|
226
|
+
) -> f32 {
|
|
227
|
+
let coordinate = fract((position + time_scaled * time_scale) * scale);
|
|
228
|
+
return textureSampleLevel(density, density_sampler, coordinate, 0.0).r + 0.5;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn volume_scattering_density(position : vec3<f32>, frame_index : u32) -> f32 {
|
|
232
|
+
let time = f32(frame_index) / 60.0;
|
|
233
|
+
let time_scaled = vec3<f32>(time, 0.0, time * 0.3);
|
|
234
|
+
var grain = volume_sample_grain(position, 0.1, time_scaled, 1.0);
|
|
235
|
+
grain = grain * volume_sample_grain(position, 0.05, time_scaled, 1.0);
|
|
236
|
+
grain = grain * volume_sample_grain(position, 0.02, time_scaled, 2.0);
|
|
237
|
+
// TSL's smokeAmount.mix(1, grain) expands to mix(1, grain, 2).
|
|
238
|
+
return 2.0 * grain - 1.0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn packed_visibility_at(previous : vec4<f32>, current : vec4<f32>, next : vec4<f32>, channel : i32) -> f32 {
|
|
242
|
+
if (channel < 0) { return previous.w; }
|
|
243
|
+
if (channel > 3) { return next.x; }
|
|
244
|
+
return current[channel];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@compute @workgroup_size(8, 8, 1)
|
|
248
|
+
fn volume_integrate(@builtin(global_invocation_id) id : vec3<u32>) {
|
|
249
|
+
let volume_size = textureDimensions(resolved);
|
|
250
|
+
let output_size = textureDimensions(resolved);
|
|
251
|
+
if (any(id.xy >= output_size)) { return; }
|
|
252
|
+
let uv = (vec2<f32>(id.xy) + vec2<f32>(0.5)) / vec2<f32>(output_size);
|
|
253
|
+
let origin = reconstruct_world(uv, 0.0);
|
|
254
|
+
let far_point = reconstruct_world(uv, 1.0);
|
|
255
|
+
let ray_direction = normalize(far_point - origin);
|
|
256
|
+
let interval = ray_box_interval(origin, ray_direction);
|
|
257
|
+
let ray_near = interval.x;
|
|
258
|
+
let ray_far = min(interval.y, volume_params.optics.x);
|
|
259
|
+
let depth_size = textureDimensions(scene_depth);
|
|
260
|
+
let pixel = min(vec2<u32>(uv * vec2<f32>(depth_size)), max(depth_size, vec2<u32>(1u)) - vec2<u32>(1u));
|
|
261
|
+
let scene_depth_value = textureLoad(scene_depth, vec2<i32>(pixel), 0);
|
|
262
|
+
let scene_world = reconstruct_world(uv, scene_depth_value);
|
|
263
|
+
let scene_distance = max(dot(scene_world - origin, ray_direction), 0.0);
|
|
264
|
+
let clipped_far = min(ray_far, scene_distance);
|
|
265
|
+
let visibility_depth = max(textureNumLayers(froxel) * 4u, 1u);
|
|
266
|
+
let ray_step_count = 12u;
|
|
267
|
+
// The pinned Three.js scene evaluates twelve lighting steps. The packed
|
|
268
|
+
// froxel remains deeper so shadow visibility can be interpolated at each
|
|
269
|
+
// ray sample without making the integration denser than the oracle.
|
|
270
|
+
let full_step = max(ray_far - ray_near, 0.0) / f32(visibility_depth);
|
|
271
|
+
let ray_step = max(ray_far - ray_near, 0.0) / f32(ray_step_count);
|
|
272
|
+
let volume_extent = max(volume_params.bounds_max.xyz - volume_params.bounds_min.xyz, vec3<f32>(EPSILON));
|
|
273
|
+
var transmittance = 1.0;
|
|
274
|
+
var scattering = vec3<f32>(0.0);
|
|
275
|
+
let phase = hg(dot(-normalize(volume_params.light_direction.xyz), ray_direction), volume_params.optics.y);
|
|
276
|
+
let sigma_scale = max(dot(volume_params.extinction.xyz, vec3<f32>(0.3333333)), 0.0);
|
|
277
|
+
let raw_froxel_size = max(textureDimensions(froxel).xy, vec2<u32>(1u));
|
|
278
|
+
let raw_froxel_coord = min(
|
|
279
|
+
vec2<u32>(uv * vec2<f32>(raw_froxel_size)),
|
|
280
|
+
raw_froxel_size - vec2<u32>(1u),
|
|
281
|
+
);
|
|
282
|
+
let frame_index = u32(max(volume_params.light_direction.w, 0.0));
|
|
283
|
+
for (var step = 0u; step < ray_step_count; step = step + 1u) {
|
|
284
|
+
let segment_start = ray_near + f32(step) * ray_step;
|
|
285
|
+
let segment_length = clamp(clipped_far - segment_start, 0.0, ray_step);
|
|
286
|
+
if (segment_length > 0.0) {
|
|
287
|
+
let depth_phase = stratified32(
|
|
288
|
+
froxel_seed(vec3<u32>(raw_froxel_coord, step), 3266489917u),
|
|
289
|
+
frame_index,
|
|
290
|
+
17u,
|
|
291
|
+
);
|
|
292
|
+
let jittered_ray_t = segment_start + segment_length * depth_phase;
|
|
293
|
+
let world_position = origin + ray_direction * jittered_ray_t;
|
|
294
|
+
// The exact Three.js smokeAmount transform can produce negative values
|
|
295
|
+
// from its signed noise domain. Beer-Lambert optical depth is physical
|
|
296
|
+
// extinction, so the renderer clamps only this consumed density while
|
|
297
|
+
// retaining the authored source expression and its signed diagnostics.
|
|
298
|
+
let density_value = max(volume_scattering_density(world_position, frame_index), 0.0);
|
|
299
|
+
let sample_position = clamp(
|
|
300
|
+
(jittered_ray_t - ray_near) / max(full_step, EPSILON) - 0.5,
|
|
301
|
+
0.0,
|
|
302
|
+
f32(visibility_depth - 1u),
|
|
303
|
+
);
|
|
304
|
+
let lower_position = u32(floor(sample_position));
|
|
305
|
+
let upper_position = min(lower_position + 1u, visibility_depth - 1u);
|
|
306
|
+
let interpolation = sample_position - f32(lower_position);
|
|
307
|
+
let slice_group = lower_position / 4u;
|
|
308
|
+
let upper_group = upper_position / 4u;
|
|
309
|
+
let lower_packed = textureSampleLevel(froxel, froxel_sampler, uv, i32(slice_group), 0.0);
|
|
310
|
+
let upper_packed = textureSampleLevel(froxel, froxel_sampler, uv, i32(upper_group), 0.0);
|
|
311
|
+
let lower_visibility = lower_packed[i32(lower_position % 4u)];
|
|
312
|
+
let upper_visibility = upper_packed[i32(upper_position % 4u)];
|
|
313
|
+
let shadow_visibility = clamp(mix(lower_visibility, upper_visibility, interpolation), 0.0, 1.0);
|
|
314
|
+
// Apply the packed SpotLight shadow only to the spot contribution.
|
|
315
|
+
// PointLight distance attenuation remains independent of the spot cone.
|
|
316
|
+
let mode = u32(round(volume_params.optics.z));
|
|
317
|
+
let visibility = select(
|
|
318
|
+
shadow_visibility,
|
|
319
|
+
mix(1.0, shadow_visibility, volume_spot_shadow_intensity()),
|
|
320
|
+
mode == 2u || mode == 3u,
|
|
321
|
+
);
|
|
322
|
+
let radiance = select(
|
|
323
|
+
volume_light_radiance(world_position) * visibility,
|
|
324
|
+
volume_light_radiance_pair(world_position, visibility),
|
|
325
|
+
mode == 3u,
|
|
326
|
+
);
|
|
327
|
+
let local_transmittance = exp(-sigma_scale * density_value * segment_length);
|
|
328
|
+
let local_scatter = radiance * volume_params.albedo.xyz * phase *
|
|
329
|
+
VOLUME_LIGHT_PHASE_SCALE * (1.0 - local_transmittance) +
|
|
330
|
+
volume_params.emission.xyz * density_value * segment_length;
|
|
331
|
+
scattering = scattering + transmittance * local_scatter;
|
|
332
|
+
transmittance = transmittance * local_transmittance;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
let finite = isFinite(transmittance) && finiteVec3(scattering);
|
|
336
|
+
let result = select(vec4<f32>(0.0, 0.0, 0.0, 1.0), vec4<f32>(scattering, transmittance), finite);
|
|
337
|
+
textureStore(resolved, id.xy, result);
|
|
338
|
+
if (volume_params.optics.w < 0.5) {
|
|
339
|
+
textureStore(history_seed, id.xy, result);
|
|
340
|
+
textureStore(temporal_seed, id.xy, result);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#define_import_path forgeax_view::volume_temporal
|
|
2
|
+
|
|
3
|
+
struct VolumeTemporalView {
|
|
4
|
+
_prefix : array<vec4<f32>, 6>, cameraPos : vec4<f32>, _lightViewProjA : mat4x4<f32>, inverseViewProj : mat4x4<f32>,
|
|
5
|
+
_middle : array<vec4<f32>, 34>, temporalCurrentViewProj : mat4x4<f32>, temporalPreviousViewProj : mat4x4<f32>,
|
|
6
|
+
temporalProjection : vec4<f32>, temporalPreviousCameraPos : vec4<f32>,
|
|
7
|
+
};
|
|
8
|
+
struct VolumeParams {
|
|
9
|
+
bounds_min : vec4<f32>, bounds_max : vec4<f32>, extinction : vec4<f32>, albedo : vec4<f32>,
|
|
10
|
+
emission : vec4<f32>, light_direction : vec4<f32>, light_color : vec4<f32>, optics : vec4<f32>,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
@group(0) @binding(0) var current : texture_2d<f32>;
|
|
14
|
+
@group(0) @binding(1) var accepted : texture_2d<f32>;
|
|
15
|
+
@group(0) @binding(2) var pending : texture_storage_2d<rgba16float, write>;
|
|
16
|
+
@group(0) @binding(3) var<uniform> volume_params : VolumeParams;
|
|
17
|
+
@group(0) @binding(4) var<uniform> volume_view : VolumeTemporalView;
|
|
18
|
+
|
|
19
|
+
fn bilinear(uv : vec2<f32>, size : vec2<u32>) -> vec4<f32> {
|
|
20
|
+
let p = uv * vec2<f32>(size) - vec2<f32>(0.5);
|
|
21
|
+
let base = vec2<i32>(floor(p));
|
|
22
|
+
let f = fract(p);
|
|
23
|
+
let max_coord = vec2<i32>(size) - vec2<i32>(1);
|
|
24
|
+
let c00 = textureLoad(accepted, clamp(base, vec2<i32>(0), max_coord), 0);
|
|
25
|
+
let c10 = textureLoad(accepted, clamp(base + vec2<i32>(1, 0), vec2<i32>(0), max_coord), 0);
|
|
26
|
+
let c01 = textureLoad(accepted, clamp(base + vec2<i32>(0, 1), vec2<i32>(0), max_coord), 0);
|
|
27
|
+
let c11 = textureLoad(accepted, clamp(base + vec2<i32>(1), vec2<i32>(0), max_coord), 0);
|
|
28
|
+
return mix(mix(c00, c10, f.x), mix(c01, c11, f.x), f.y);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn reproject_uv(uv : vec2<f32>) -> vec2<f32> {
|
|
32
|
+
let current_ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.5, 1.0);
|
|
33
|
+
let world_h = volume_view.inverseViewProj * current_ndc;
|
|
34
|
+
let world = world_h.xyz / max(abs(world_h.w), 1e-5);
|
|
35
|
+
let previous_clip = volume_view.temporalPreviousViewProj * vec4<f32>(world, 1.0);
|
|
36
|
+
if (previous_clip.w <= 1e-5) {
|
|
37
|
+
return vec2<f32>(-1.0);
|
|
38
|
+
}
|
|
39
|
+
let previous_ndc = previous_clip.xyz / previous_clip.w;
|
|
40
|
+
return vec2<f32>(previous_ndc.x * 0.5 + 0.5, 1.0 - (previous_ndc.y * 0.5 + 0.5));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@compute @workgroup_size(8, 8, 1)
|
|
44
|
+
fn volume_temporal(@builtin(global_invocation_id) id : vec3<u32>) {
|
|
45
|
+
let size = textureDimensions(current);
|
|
46
|
+
if (any(id.xy >= size)) { return; }
|
|
47
|
+
let uv = (vec2<f32>(id.xy) + vec2<f32>(0.5)) / vec2<f32>(size);
|
|
48
|
+
let center = textureLoad(current, vec2<i32>(id.xy), 0);
|
|
49
|
+
var lower = center;
|
|
50
|
+
var upper = center;
|
|
51
|
+
for (var oy = -1i; oy <= 1i; oy = oy + 1i) {
|
|
52
|
+
for (var ox = -1i; ox <= 1i; ox = ox + 1i) {
|
|
53
|
+
let coord = clamp(vec2<i32>(id.xy) + vec2<i32>(ox, oy), vec2<i32>(0), vec2<i32>(size) - vec2<i32>(1));
|
|
54
|
+
let neighbor = textureLoad(current, coord, 0);
|
|
55
|
+
lower = min(lower, neighbor);
|
|
56
|
+
upper = max(upper, neighbor);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
var output = center;
|
|
60
|
+
let historyValid = volume_params.optics.w >= 0.5 && volume_view.temporalPreviousCameraPos.w >= 0.5;
|
|
61
|
+
if (historyValid) {
|
|
62
|
+
let previous_uv = reproject_uv(uv);
|
|
63
|
+
if (all(previous_uv >= vec2<f32>(0.0)) && all(previous_uv <= vec2<f32>(1.0))) {
|
|
64
|
+
let prior = bilinear(previous_uv, size);
|
|
65
|
+
let clamped = clamp(prior, lower, upper);
|
|
66
|
+
output = mix(center, clamped, 0.875);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
textureStore(pending, id.xy, output);
|
|
70
|
+
}
|