@plasius/gpu-lighting 0.2.4 → 0.2.5
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/CHANGELOG.md +12 -9
- package/README.md +10 -5
- package/dist/techniques/techniques/hdri/brdf-lut.job.wgsl +70 -2
- package/dist/techniques/techniques/hdri/irradiance-convolution.job.wgsl +91 -2
- package/dist/techniques/techniques/hdri/specular-prefilter.job.wgsl +110 -2
- package/dist/techniques/techniques/volumetrics/froxel-integrate.job.wgsl +105 -2
- package/dist/techniques/techniques/volumetrics/volumetric-shadow.job.wgsl +96 -2
- package/package.json +1 -1
- package/src/techniques/hdri/brdf-lut.job.wgsl +70 -2
- package/src/techniques/hdri/irradiance-convolution.job.wgsl +91 -2
- package/src/techniques/hdri/specular-prefilter.job.wgsl +110 -2
- package/src/techniques/volumetrics/froxel-integrate.job.wgsl +105 -2
- package/src/techniques/volumetrics/volumetric-shadow.job.wgsl +96 -2
package/CHANGELOG.md
CHANGED
|
@@ -20,19 +20,22 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
20
20
|
- **Security**
|
|
21
21
|
- (placeholder)
|
|
22
22
|
|
|
23
|
-
## [0.2.
|
|
23
|
+
## [0.2.5] - 2026-06-15
|
|
24
24
|
|
|
25
25
|
- **Added**
|
|
26
|
-
-
|
|
26
|
+
- Added concrete volumetric WGSL kernels for `volumetricShadow` and
|
|
27
|
+
`froxelIntegrate`, covering froxel shadow history plus scattering/extinction
|
|
28
|
+
integration for the published realtime and reference profiles.
|
|
29
|
+
- Added concrete HDRI/IBL WGSL kernels for `irradianceConvolution`,
|
|
30
|
+
`specularPrefilter`, and `brdfLut`.
|
|
27
31
|
|
|
28
32
|
- **Changed**
|
|
29
|
-
-
|
|
33
|
+
- README now documents the delivered volumetrics and HDRI kernel scope with
|
|
34
|
+
technique-level descriptions instead of leaving those exported jobs implied.
|
|
30
35
|
|
|
31
36
|
- **Fixed**
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
- **Security**
|
|
35
|
-
- (placeholder)
|
|
37
|
+
- Package tests now fail if any exported `hybrid`, `volumetrics`, or `hdri`
|
|
38
|
+
job regresses to placeholder text or an empty/no-op `process_job` body.
|
|
36
39
|
|
|
37
40
|
## [0.2.2] - 2026-06-11
|
|
38
41
|
|
|
@@ -461,7 +464,7 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
461
464
|
[0.1.16]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.1.16
|
|
462
465
|
[0.1.17]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.1.17
|
|
463
466
|
[0.1.19]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.1.19
|
|
464
|
-
[Unreleased]: https://github.com/Plasius-LTD/gpu-lighting/compare/v0.2.
|
|
467
|
+
[Unreleased]: https://github.com/Plasius-LTD/gpu-lighting/compare/v0.2.5...HEAD
|
|
465
468
|
[0.2.0]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.2.0
|
|
466
469
|
[0.2.2]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.2.2
|
|
467
|
-
[0.2.
|
|
470
|
+
[0.2.5]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.2.5
|
package/README.md
CHANGED
|
@@ -325,6 +325,11 @@ The package now ships concrete WGSL contracts for:
|
|
|
325
325
|
- `hybrid.screenTrace`: first-hit reflection tracing over the shared hybrid scene contracts
|
|
326
326
|
- `hybrid.radianceCache`: irradiance history updates for cache-backed indirect reuse
|
|
327
327
|
- `hybrid.finalGather`: cache + trace composition with temporal reuse for the hybrid GI path
|
|
328
|
+
- `volumetrics.volumetricShadow`: slice-aware Beer-Lambert shadow history for fog and shafts
|
|
329
|
+
- `volumetrics.froxelIntegrate`: froxel scattering/extinction integration with temporal stability
|
|
330
|
+
- `hdri.irradianceConvolution`: cosine-weighted diffuse environment convolution
|
|
331
|
+
- `hdri.specularPrefilter`: roughness-aware environment prefiltering for glossy IBL
|
|
332
|
+
- `hdri.brdfLut`: split-sum BRDF LUT integration for image-based lighting
|
|
328
333
|
- `pathtracer.pathTrace`: analytic scene tracing, bounce integration, and sky fallback
|
|
329
334
|
- `pathtracer.accumulate`: progressive history resolve with reset handling
|
|
330
335
|
- `pathtracer.denoise`: spatial-temporal bilateral filtering for reference previews
|
|
@@ -354,12 +359,12 @@ graph.
|
|
|
354
359
|
- `accumulate`
|
|
355
360
|
- `denoise`
|
|
356
361
|
- `volumetrics`
|
|
357
|
-
- `froxelIntegrate
|
|
358
|
-
- `volumetricShadow
|
|
362
|
+
- `froxelIntegrate`: accumulates participating-media scattering/extinction per froxel
|
|
363
|
+
- `volumetricShadow`: resolves directional shadow transmittance history per froxel
|
|
359
364
|
- `hdri`
|
|
360
|
-
- `irradianceConvolution
|
|
361
|
-
- `specularPrefilter
|
|
362
|
-
- `brdfLut
|
|
365
|
+
- `irradianceConvolution`: builds diffuse irradiance from the environment source
|
|
366
|
+
- `specularPrefilter`: builds roughness-aware glossy environment mip data
|
|
367
|
+
- `brdfLut`: integrates the split-sum BRDF lookup surface for IBL
|
|
363
368
|
|
|
364
369
|
## Demo
|
|
365
370
|
|
|
@@ -1,3 +1,71 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
@group(0) @binding(0) var<uniform> iblPrecomputeParams: IblPrecomputeParams;
|
|
2
|
+
@group(0) @binding(1) var<storage, read_write> hdriBrdfLutOutput: array<vec4<f32>>;
|
|
3
|
+
|
|
4
|
+
fn lut_extent() -> u32 {
|
|
5
|
+
return max(iblPrecomputeParams.sample_count, 1u);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
fn lut_index(pixel: vec2<u32>) -> u32 {
|
|
9
|
+
let extent = lut_extent();
|
|
10
|
+
return pixel.y * extent + pixel.x;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn saturate(value: f32) -> f32 {
|
|
14
|
+
return clamp(value, 0.0, 1.0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn geometry_schlick_ggx(ndotv: f32, roughness: f32) -> f32 {
|
|
18
|
+
let alpha = max(roughness * roughness, 0.001);
|
|
19
|
+
let k = (alpha + 1.0) * (alpha + 1.0) / 8.0;
|
|
20
|
+
return ndotv / max(ndotv * (1.0 - k) + k, 0.001);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn geometry_smith(ndotv: f32, ndotl: f32, roughness: f32) -> f32 {
|
|
24
|
+
return geometry_schlick_ggx(ndotv, roughness) * geometry_schlick_ggx(ndotl, roughness);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn fresnel_schlick(cos_theta: f32) -> f32 {
|
|
28
|
+
return pow(1.0 - saturate(cos_theta), 5.0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@compute @workgroup_size(8, 8, 1)
|
|
32
|
+
fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
33
|
+
let extent = lut_extent();
|
|
34
|
+
if (global_id.x >= extent || global_id.y >= extent) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let uv = (vec2<f32>(global_id.xy) + vec2<f32>(0.5)) / max(vec2<f32>(f32(extent)), vec2<f32>(1.0));
|
|
39
|
+
let ndotv = saturate(uv.x);
|
|
40
|
+
let roughness = clamp_roughness(uv.y + iblPrecomputeParams.roughness * 0.15);
|
|
41
|
+
let sample_total = max(iblPrecomputeParams.sample_count, 1u);
|
|
42
|
+
var integrated_brdf = vec2<f32>(0.0);
|
|
43
|
+
var sample_index = 0u;
|
|
44
|
+
loop {
|
|
45
|
+
if (sample_index >= sample_total) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let sample_u = (f32(sample_index) + 0.5) / f32(sample_total);
|
|
50
|
+
let sample_v = fract(f32(sample_index) * 0.61803398875);
|
|
51
|
+
let ndotl = saturate(sqrt(sample_u));
|
|
52
|
+
let vdoth = saturate(mix(ndotv, 1.0, sample_v));
|
|
53
|
+
let geometry = geometry_smith(ndotv, ndotl, roughness);
|
|
54
|
+
let fresnel = fresnel_schlick(vdoth);
|
|
55
|
+
integrated_brdf = integrated_brdf + vec2<f32>(
|
|
56
|
+
(1.0 - fresnel) * geometry,
|
|
57
|
+
fresnel * geometry
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
sample_index = sample_index + 1u;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
integrated_brdf =
|
|
64
|
+
integrated_brdf / f32(sample_total) * max(iblPrecomputeParams.exposure_bias, 0.0001);
|
|
65
|
+
hdriBrdfLutOutput[lut_index(global_id.xy)] = vec4<f32>(
|
|
66
|
+
integrated_brdf.x,
|
|
67
|
+
integrated_brdf.y,
|
|
68
|
+
roughness,
|
|
69
|
+
ndotv
|
|
70
|
+
);
|
|
3
71
|
}
|
|
@@ -1,3 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
struct HdriConvolutionSample {
|
|
2
|
+
direction_radiance: vec4<f32>,
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
@group(0) @binding(0) var<uniform> iblPrecomputeParams: IblPrecomputeParams;
|
|
6
|
+
@group(0) @binding(1) var<storage, read> hdriEnvironmentInput: array<HdriConvolutionSample>;
|
|
7
|
+
@group(0) @binding(2) var<storage, read_write> hdriIrradianceOutput: array<vec4<f32>>;
|
|
8
|
+
|
|
9
|
+
fn face_extent() -> u32 {
|
|
10
|
+
return max(iblPrecomputeParams.sample_count, 1u);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn face_pixel_index(pixel: vec2<u32>) -> u32 {
|
|
14
|
+
let extent = face_extent();
|
|
15
|
+
return pixel.y * extent + pixel.x;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn safe_normalize(value: vec3<f32>) -> vec3<f32> {
|
|
19
|
+
let length_value = length(value);
|
|
20
|
+
if (length_value <= 0.000001) {
|
|
21
|
+
return vec3<f32>(0.0, 1.0, 0.0);
|
|
22
|
+
}
|
|
23
|
+
return value / length_value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn direction_from_texel(pixel: vec2<u32>, extent: u32) -> vec3<f32> {
|
|
27
|
+
let uv = (vec2<f32>(pixel) + vec2<f32>(0.5)) / max(vec2<f32>(f32(extent)), vec2<f32>(1.0));
|
|
28
|
+
let phi = (uv.x * 2.0 - 1.0) * 3.141592653589793;
|
|
29
|
+
let theta = uv.y * 3.141592653589793;
|
|
30
|
+
let sin_theta = sin(theta);
|
|
31
|
+
return safe_normalize(
|
|
32
|
+
vec3<f32>(
|
|
33
|
+
cos(phi) * sin_theta,
|
|
34
|
+
cos(theta),
|
|
35
|
+
sin(phi) * sin_theta
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn direction_to_index(direction: vec3<f32>, extent: u32) -> u32 {
|
|
41
|
+
let dir = safe_normalize(direction);
|
|
42
|
+
let phi = atan2(dir.z, dir.x);
|
|
43
|
+
let theta = acos(clamp(dir.y, -1.0, 1.0));
|
|
44
|
+
let u = fract(phi / (2.0 * 3.141592653589793) + 0.5);
|
|
45
|
+
let v = clamp(theta / 3.141592653589793, 0.0, 0.999999);
|
|
46
|
+
let pixel = vec2<u32>(
|
|
47
|
+
min(u32(floor(u * f32(extent))), extent - 1u),
|
|
48
|
+
min(u32(floor(v * f32(extent))), extent - 1u)
|
|
49
|
+
);
|
|
50
|
+
return face_pixel_index(pixel);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn sample_environment(direction: vec3<f32>) -> vec3<f32> {
|
|
54
|
+
let index = direction_to_index(direction, face_extent());
|
|
55
|
+
return hdriEnvironmentInput[index].direction_radiance.xyz;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@compute @workgroup_size(8, 8, 1)
|
|
59
|
+
fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
60
|
+
let extent = face_extent();
|
|
61
|
+
if (global_id.x >= extent || global_id.y >= extent) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let normal = direction_from_texel(global_id.xy, extent);
|
|
66
|
+
var accumulated_irradiance = vec3<f32>(0.0);
|
|
67
|
+
var total_weight = 0.0;
|
|
68
|
+
var sample_index = 0u;
|
|
69
|
+
loop {
|
|
70
|
+
if (sample_index >= extent) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let sample_pixel = vec2<u32>(
|
|
75
|
+
(global_id.x + sample_index) % extent,
|
|
76
|
+
(global_id.y + sample_index * 3u) % extent
|
|
77
|
+
);
|
|
78
|
+
let sample_direction = direction_from_texel(sample_pixel, extent);
|
|
79
|
+
let cosine_weight = max(dot(normal, sample_direction), 0.0);
|
|
80
|
+
if (cosine_weight > 0.0) {
|
|
81
|
+
accumulated_irradiance =
|
|
82
|
+
accumulated_irradiance + sample_environment(sample_direction) * cosine_weight;
|
|
83
|
+
total_weight = total_weight + cosine_weight;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sample_index = sample_index + 1u;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let irradiance =
|
|
90
|
+
accumulated_irradiance / max(total_weight, 0.0001) * max(iblPrecomputeParams.exposure_bias, 0.0001);
|
|
91
|
+
hdriIrradianceOutput[face_pixel_index(global_id.xy)] = vec4<f32>(irradiance, 1.0);
|
|
3
92
|
}
|
|
@@ -1,3 +1,111 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
struct HdriSpecularSample {
|
|
2
|
+
direction_radiance: vec4<f32>,
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
@group(0) @binding(0) var<uniform> iblPrecomputeParams: IblPrecomputeParams;
|
|
6
|
+
@group(0) @binding(1) var<storage, read> hdriEnvironmentInput: array<HdriSpecularSample>;
|
|
7
|
+
@group(0) @binding(2) var<storage, read_write> hdriSpecularOutput: array<vec4<f32>>;
|
|
8
|
+
|
|
9
|
+
fn face_extent() -> u32 {
|
|
10
|
+
return max(iblPrecomputeParams.sample_count, 1u);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn face_pixel_index(pixel: vec2<u32>) -> u32 {
|
|
14
|
+
let extent = face_extent();
|
|
15
|
+
return pixel.y * extent + pixel.x;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn safe_normalize(value: vec3<f32>) -> vec3<f32> {
|
|
19
|
+
let length_value = length(value);
|
|
20
|
+
if (length_value <= 0.000001) {
|
|
21
|
+
return vec3<f32>(0.0, 1.0, 0.0);
|
|
22
|
+
}
|
|
23
|
+
return value / length_value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn direction_from_texel(pixel: vec2<u32>, extent: u32) -> vec3<f32> {
|
|
27
|
+
let uv = (vec2<f32>(pixel) + vec2<f32>(0.5)) / max(vec2<f32>(f32(extent)), vec2<f32>(1.0));
|
|
28
|
+
let phi = (uv.x * 2.0 - 1.0) * 3.141592653589793;
|
|
29
|
+
let theta = uv.y * 3.141592653589793;
|
|
30
|
+
let sin_theta = sin(theta);
|
|
31
|
+
return safe_normalize(
|
|
32
|
+
vec3<f32>(
|
|
33
|
+
cos(phi) * sin_theta,
|
|
34
|
+
cos(theta),
|
|
35
|
+
sin(phi) * sin_theta
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn direction_to_index(direction: vec3<f32>, extent: u32) -> u32 {
|
|
41
|
+
let dir = safe_normalize(direction);
|
|
42
|
+
let phi = atan2(dir.z, dir.x);
|
|
43
|
+
let theta = acos(clamp(dir.y, -1.0, 1.0));
|
|
44
|
+
let u = fract(phi / (2.0 * 3.141592653589793) + 0.5);
|
|
45
|
+
let v = clamp(theta / 3.141592653589793, 0.0, 0.999999);
|
|
46
|
+
let pixel = vec2<u32>(
|
|
47
|
+
min(u32(floor(u * f32(extent))), extent - 1u),
|
|
48
|
+
min(u32(floor(v * f32(extent))), extent - 1u)
|
|
49
|
+
);
|
|
50
|
+
return face_pixel_index(pixel);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn sample_environment(direction: vec3<f32>) -> vec3<f32> {
|
|
54
|
+
let index = direction_to_index(direction, face_extent());
|
|
55
|
+
return hdriEnvironmentInput[index].direction_radiance.xyz;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn importance_sample_hemisphere(normal: vec3<f32>, xi: vec2<f32>, roughness: f32) -> vec3<f32> {
|
|
59
|
+
let phi = 2.0 * 3.141592653589793 * xi.x;
|
|
60
|
+
let alpha = max(roughness * roughness, 0.001);
|
|
61
|
+
let cos_theta = sqrt((1.0 - xi.y) / max(1.0 + (alpha * alpha - 1.0) * xi.y, 0.001));
|
|
62
|
+
let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0));
|
|
63
|
+
let tangent_seed = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.y) > 0.9);
|
|
64
|
+
let tangent = safe_normalize(cross(tangent_seed, normal));
|
|
65
|
+
let bitangent = cross(normal, tangent);
|
|
66
|
+
return safe_normalize(
|
|
67
|
+
tangent * cos(phi) * sin_theta +
|
|
68
|
+
bitangent * sin(phi) * sin_theta +
|
|
69
|
+
normal * cos_theta
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@compute @workgroup_size(8, 8, 1)
|
|
74
|
+
fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
75
|
+
let extent = face_extent();
|
|
76
|
+
if (global_id.x >= extent || global_id.y >= extent) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let normal = direction_from_texel(global_id.xy, extent);
|
|
81
|
+
let view_direction = normal;
|
|
82
|
+
let roughness = clamp_roughness(iblPrecomputeParams.roughness);
|
|
83
|
+
var prefiltered_radiance = vec3<f32>(0.0);
|
|
84
|
+
var importance_weight = 0.0;
|
|
85
|
+
var sample_index = 0u;
|
|
86
|
+
loop {
|
|
87
|
+
if (sample_index >= extent) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let xi = vec2<f32>(
|
|
92
|
+
(f32(sample_index) + 0.5) / f32(extent),
|
|
93
|
+
fract((f32(sample_index) * 0.7548776662466927) + (f32(global_id.x) + f32(global_id.y)) * 0.01)
|
|
94
|
+
);
|
|
95
|
+
let half_vector = importance_sample_hemisphere(normal, xi, roughness);
|
|
96
|
+
let sample_direction = reflect(-view_direction, half_vector);
|
|
97
|
+
let ndotl = max(dot(normal, sample_direction), 0.0);
|
|
98
|
+
if (ndotl > 0.0) {
|
|
99
|
+
let weight = mix(ndotl, pow(ndotl, 1.0 / max(roughness + 0.05, 0.05)), roughness);
|
|
100
|
+
prefiltered_radiance =
|
|
101
|
+
prefiltered_radiance + sample_environment(sample_direction) * weight;
|
|
102
|
+
importance_weight = importance_weight + weight;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
sample_index = sample_index + 1u;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let resolved_radiance =
|
|
109
|
+
prefiltered_radiance / max(importance_weight, 0.0001) * max(iblPrecomputeParams.exposure_bias, 0.0001);
|
|
110
|
+
hdriSpecularOutput[face_pixel_index(global_id.xy)] = vec4<f32>(resolved_radiance, roughness);
|
|
3
111
|
}
|
|
@@ -1,3 +1,106 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
struct VolumetricIntegrationParams {
|
|
2
|
+
light_direction: vec3<f32>,
|
|
3
|
+
history_blend: f32,
|
|
4
|
+
extinction_bias: f32,
|
|
5
|
+
ambient_boost: f32,
|
|
6
|
+
integration_step_scale: f32,
|
|
7
|
+
phase_bias: f32,
|
|
8
|
+
temporal_stability: f32,
|
|
9
|
+
reserved: f32,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
struct FroxelMediumVoxel {
|
|
13
|
+
extinction_density: vec4<f32>,
|
|
14
|
+
inscattering_anisotropy: vec4<f32>,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
struct VolumetricShadowHistory {
|
|
18
|
+
shadow_transmittance: vec4<f32>,
|
|
19
|
+
depth_phase: vec4<f32>,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
struct FroxelIntegratedVoxel {
|
|
23
|
+
integrated_scattering: vec4<f32>,
|
|
24
|
+
integrated_extinction: vec4<f32>,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
@group(0) @binding(0) var<uniform> froxelGridParams: FroxelGridParams;
|
|
28
|
+
@group(0) @binding(1) var<uniform> volumetricIntegrationParams: VolumetricIntegrationParams;
|
|
29
|
+
@group(0) @binding(2) var<storage, read> froxelMediumInput: array<FroxelMediumVoxel>;
|
|
30
|
+
@group(0) @binding(3) var<storage, read> froxelShadowInput: array<VolumetricShadowHistory>;
|
|
31
|
+
@group(0) @binding(4) var<storage, read_write> froxelIntegratedOutput: array<FroxelIntegratedVoxel>;
|
|
32
|
+
|
|
33
|
+
fn froxel_integrate_index(coord: vec3<u32>) -> u32 {
|
|
34
|
+
let plane = max(froxelGridParams.grid_width, 1u) * max(froxelGridParams.grid_height, 1u);
|
|
35
|
+
return coord.z * plane + coord.y * max(froxelGridParams.grid_width, 1u) + coord.x;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn safe_normalize(value: vec3<f32>) -> vec3<f32> {
|
|
39
|
+
let length_value = length(value);
|
|
40
|
+
if (length_value <= 0.000001) {
|
|
41
|
+
return vec3<f32>(0.0, 1.0, 0.0);
|
|
42
|
+
}
|
|
43
|
+
return value / length_value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn henyey_greenstein(cos_theta: f32, anisotropy: f32) -> f32 {
|
|
47
|
+
let g = clamp(anisotropy, -0.85, 0.85);
|
|
48
|
+
let denominator = pow(max(1.0 + g * g - 2.0 * g * cos_theta, 0.001), 1.5);
|
|
49
|
+
return (1.0 - g * g) / max(12.566370614359172 * denominator, 0.001);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@compute @workgroup_size(4, 4, 4)
|
|
53
|
+
fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
54
|
+
if (
|
|
55
|
+
global_id.x >= froxelGridParams.grid_width ||
|
|
56
|
+
global_id.y >= froxelGridParams.grid_height ||
|
|
57
|
+
global_id.z >= froxelGridParams.grid_depth
|
|
58
|
+
) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let index = froxel_integrate_index(global_id);
|
|
63
|
+
let medium = froxelMediumInput[index];
|
|
64
|
+
let shadow = froxelShadowInput[index];
|
|
65
|
+
let previous = froxelIntegratedOutput[index];
|
|
66
|
+
let light_direction = safe_normalize(volumetricIntegrationParams.light_direction);
|
|
67
|
+
let slice_depth = (f32(global_id.z) + 0.5) / max(f32(froxelGridParams.grid_depth), 1.0);
|
|
68
|
+
let density = max(medium.extinction_density.w + volumetricIntegrationParams.extinction_bias, 0.0);
|
|
69
|
+
let extinction = max(medium.extinction_density.xyz, vec3<f32>(0.0001));
|
|
70
|
+
let albedo = clamp(medium.inscattering_anisotropy.xyz, vec3<f32>(0.0), vec3<f32>(4.0));
|
|
71
|
+
let anisotropy = medium.inscattering_anisotropy.w;
|
|
72
|
+
let phase = henyey_greenstein(light_direction.y, anisotropy + volumetricIntegrationParams.phase_bias * 0.1);
|
|
73
|
+
let step_length =
|
|
74
|
+
max(volumetricIntegrationParams.integration_step_scale, 0.2) /
|
|
75
|
+
max(f32(froxelGridParams.grid_depth), 1.0);
|
|
76
|
+
let shadow_visibility = shadow.shadow_transmittance.w;
|
|
77
|
+
let ambient_term = vec3<f32>(0.08, 0.1, 0.14) * max(volumetricIntegrationParams.ambient_boost, 0.0);
|
|
78
|
+
let sample = MediumSample(
|
|
79
|
+
extinction,
|
|
80
|
+
(albedo * (0.25 + slice_depth * 0.5) + ambient_term) * shadow_visibility * phase
|
|
81
|
+
);
|
|
82
|
+
let transmittance = exp(-sample.extinction * density * step_length);
|
|
83
|
+
let integrated_scattering =
|
|
84
|
+
sample.inscattering *
|
|
85
|
+
froxelGridParams.scattering_strength *
|
|
86
|
+
(1.0 - transmittance) *
|
|
87
|
+
(1.0 + shadow.depth_phase.z * 0.2);
|
|
88
|
+
let integrated_extinction = sample.extinction * density * step_length;
|
|
89
|
+
let stability = clamp(
|
|
90
|
+
shadow.depth_phase.w * volumetricIntegrationParams.temporal_stability,
|
|
91
|
+
0.0,
|
|
92
|
+
1.0
|
|
93
|
+
);
|
|
94
|
+
let blend = clamp(volumetricIntegrationParams.history_blend, 0.0, 0.98) * stability;
|
|
95
|
+
let resolved_scattering =
|
|
96
|
+
previous.integrated_scattering.xyz * blend +
|
|
97
|
+
integrated_scattering * (1.0 - blend);
|
|
98
|
+
let resolved_extinction =
|
|
99
|
+
previous.integrated_extinction.xyz * blend +
|
|
100
|
+
integrated_extinction * (1.0 - blend);
|
|
101
|
+
|
|
102
|
+
froxelIntegratedOutput[index] = FroxelIntegratedVoxel(
|
|
103
|
+
vec4<f32>(resolved_scattering, shadow_visibility),
|
|
104
|
+
vec4<f32>(resolved_extinction, stability)
|
|
105
|
+
);
|
|
3
106
|
}
|
|
@@ -1,3 +1,97 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
struct VolumetricLightParams {
|
|
2
|
+
light_direction: vec3<f32>,
|
|
3
|
+
history_blend: f32,
|
|
4
|
+
light_color: vec3<f32>,
|
|
5
|
+
shadow_strength: f32,
|
|
6
|
+
slice_depth_scale: f32,
|
|
7
|
+
density_bias: f32,
|
|
8
|
+
phase_bias: f32,
|
|
9
|
+
jitter_amount: f32,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
struct FroxelMediumVoxel {
|
|
13
|
+
extinction_density: vec4<f32>,
|
|
14
|
+
inscattering_anisotropy: vec4<f32>,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
struct VolumetricShadowHistory {
|
|
18
|
+
shadow_transmittance: vec4<f32>,
|
|
19
|
+
depth_phase: vec4<f32>,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
@group(0) @binding(0) var<uniform> froxelGridParams: FroxelGridParams;
|
|
23
|
+
@group(0) @binding(1) var<uniform> volumetricLightParams: VolumetricLightParams;
|
|
24
|
+
@group(0) @binding(2) var<storage, read> froxelMediumInput: array<FroxelMediumVoxel>;
|
|
25
|
+
@group(0) @binding(3) var<storage, read> froxelShadowHistory: array<VolumetricShadowHistory>;
|
|
26
|
+
@group(0) @binding(4) var<storage, read_write> froxelShadowOutput: array<VolumetricShadowHistory>;
|
|
27
|
+
|
|
28
|
+
fn froxel_shadow_index(coord: vec3<u32>) -> u32 {
|
|
29
|
+
let plane = max(froxelGridParams.grid_width, 1u) * max(froxelGridParams.grid_height, 1u);
|
|
30
|
+
return coord.z * plane + coord.y * max(froxelGridParams.grid_width, 1u) + coord.x;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn safe_normalize(value: vec3<f32>) -> vec3<f32> {
|
|
34
|
+
let length_value = length(value);
|
|
35
|
+
if (length_value <= 0.000001) {
|
|
36
|
+
return vec3<f32>(0.0, 1.0, 0.0);
|
|
37
|
+
}
|
|
38
|
+
return value / length_value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn henyey_greenstein(cos_theta: f32, anisotropy: f32) -> f32 {
|
|
42
|
+
let g = clamp(anisotropy, -0.85, 0.85);
|
|
43
|
+
let denominator = pow(max(1.0 + g * g - 2.0 * g * cos_theta, 0.001), 1.5);
|
|
44
|
+
return (1.0 - g * g) / max(12.566370614359172 * denominator, 0.001);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@compute @workgroup_size(4, 4, 4)
|
|
48
|
+
fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
49
|
+
if (
|
|
50
|
+
global_id.x >= froxelGridParams.grid_width ||
|
|
51
|
+
global_id.y >= froxelGridParams.grid_height ||
|
|
52
|
+
global_id.z >= froxelGridParams.grid_depth
|
|
53
|
+
) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let index = froxel_shadow_index(global_id);
|
|
58
|
+
let medium = froxelMediumInput[index];
|
|
59
|
+
let previous = froxelShadowHistory[index];
|
|
60
|
+
let light_direction = safe_normalize(volumetricLightParams.light_direction);
|
|
61
|
+
let slice_depth = (f32(global_id.z) + 0.5) / max(f32(froxelGridParams.grid_depth), 1.0);
|
|
62
|
+
let density = max(medium.extinction_density.w + volumetricLightParams.density_bias, 0.0);
|
|
63
|
+
let extinction = max(dot(medium.extinction_density.xyz, vec3<f32>(0.2126, 0.7152, 0.0722)), 0.0001);
|
|
64
|
+
let anisotropy = medium.inscattering_anisotropy.w;
|
|
65
|
+
let phase_weight = henyey_greenstein(light_direction.y, anisotropy);
|
|
66
|
+
let depth_scale = max(volumetricLightParams.slice_depth_scale, 0.25);
|
|
67
|
+
let shadow_distance = depth_scale * mix(0.35, 1.35, slice_depth);
|
|
68
|
+
let optical_depth = (density * 0.7 + extinction * 0.3) * shadow_distance;
|
|
69
|
+
let raw_transmittance = exp(-optical_depth);
|
|
70
|
+
let horizon_wrap = 0.35 + 0.65 * saturate(light_direction.y * 0.5 + 0.5);
|
|
71
|
+
let jitter = fract(
|
|
72
|
+
f32(global_id.x * 19u + global_id.y * 47u + global_id.z * 73u) * 0.61803398875
|
|
73
|
+
);
|
|
74
|
+
let visibility = clamp(
|
|
75
|
+
raw_transmittance * horizon_wrap * (1.0 - volumetricLightParams.jitter_amount * (jitter - 0.5)),
|
|
76
|
+
0.0,
|
|
77
|
+
1.0
|
|
78
|
+
);
|
|
79
|
+
let history_visibility = previous.shadow_transmittance.w;
|
|
80
|
+
let blend = clamp(volumetricLightParams.history_blend, 0.0, 0.98);
|
|
81
|
+
let shadow_transmittance = mix(visibility, history_visibility, blend);
|
|
82
|
+
let light_radiance =
|
|
83
|
+
volumetricLightParams.light_color *
|
|
84
|
+
shadow_transmittance *
|
|
85
|
+
max(volumetricLightParams.shadow_strength, 0.0) *
|
|
86
|
+
(0.55 + phase_weight * 0.45);
|
|
87
|
+
let depth_confidence = clamp(
|
|
88
|
+
slice_depth * 0.45 + shadow_transmittance * 0.35 + (1.0 - density) * 0.2,
|
|
89
|
+
0.0,
|
|
90
|
+
1.0
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
froxelShadowOutput[index] = VolumetricShadowHistory(
|
|
94
|
+
vec4<f32>(light_radiance, shadow_transmittance),
|
|
95
|
+
vec4<f32>(slice_depth, optical_depth, phase_weight, depth_confidence)
|
|
96
|
+
);
|
|
3
97
|
}
|
package/package.json
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
@group(0) @binding(0) var<uniform> iblPrecomputeParams: IblPrecomputeParams;
|
|
2
|
+
@group(0) @binding(1) var<storage, read_write> hdriBrdfLutOutput: array<vec4<f32>>;
|
|
3
|
+
|
|
4
|
+
fn lut_extent() -> u32 {
|
|
5
|
+
return max(iblPrecomputeParams.sample_count, 1u);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
fn lut_index(pixel: vec2<u32>) -> u32 {
|
|
9
|
+
let extent = lut_extent();
|
|
10
|
+
return pixel.y * extent + pixel.x;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn saturate(value: f32) -> f32 {
|
|
14
|
+
return clamp(value, 0.0, 1.0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn geometry_schlick_ggx(ndotv: f32, roughness: f32) -> f32 {
|
|
18
|
+
let alpha = max(roughness * roughness, 0.001);
|
|
19
|
+
let k = (alpha + 1.0) * (alpha + 1.0) / 8.0;
|
|
20
|
+
return ndotv / max(ndotv * (1.0 - k) + k, 0.001);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn geometry_smith(ndotv: f32, ndotl: f32, roughness: f32) -> f32 {
|
|
24
|
+
return geometry_schlick_ggx(ndotv, roughness) * geometry_schlick_ggx(ndotl, roughness);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn fresnel_schlick(cos_theta: f32) -> f32 {
|
|
28
|
+
return pow(1.0 - saturate(cos_theta), 5.0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@compute @workgroup_size(8, 8, 1)
|
|
32
|
+
fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
33
|
+
let extent = lut_extent();
|
|
34
|
+
if (global_id.x >= extent || global_id.y >= extent) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let uv = (vec2<f32>(global_id.xy) + vec2<f32>(0.5)) / max(vec2<f32>(f32(extent)), vec2<f32>(1.0));
|
|
39
|
+
let ndotv = saturate(uv.x);
|
|
40
|
+
let roughness = clamp_roughness(uv.y + iblPrecomputeParams.roughness * 0.15);
|
|
41
|
+
let sample_total = max(iblPrecomputeParams.sample_count, 1u);
|
|
42
|
+
var integrated_brdf = vec2<f32>(0.0);
|
|
43
|
+
var sample_index = 0u;
|
|
44
|
+
loop {
|
|
45
|
+
if (sample_index >= sample_total) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let sample_u = (f32(sample_index) + 0.5) / f32(sample_total);
|
|
50
|
+
let sample_v = fract(f32(sample_index) * 0.61803398875);
|
|
51
|
+
let ndotl = saturate(sqrt(sample_u));
|
|
52
|
+
let vdoth = saturate(mix(ndotv, 1.0, sample_v));
|
|
53
|
+
let geometry = geometry_smith(ndotv, ndotl, roughness);
|
|
54
|
+
let fresnel = fresnel_schlick(vdoth);
|
|
55
|
+
integrated_brdf = integrated_brdf + vec2<f32>(
|
|
56
|
+
(1.0 - fresnel) * geometry,
|
|
57
|
+
fresnel * geometry
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
sample_index = sample_index + 1u;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
integrated_brdf =
|
|
64
|
+
integrated_brdf / f32(sample_total) * max(iblPrecomputeParams.exposure_bias, 0.0001);
|
|
65
|
+
hdriBrdfLutOutput[lut_index(global_id.xy)] = vec4<f32>(
|
|
66
|
+
integrated_brdf.x,
|
|
67
|
+
integrated_brdf.y,
|
|
68
|
+
roughness,
|
|
69
|
+
ndotv
|
|
70
|
+
);
|
|
3
71
|
}
|
|
@@ -1,3 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
struct HdriConvolutionSample {
|
|
2
|
+
direction_radiance: vec4<f32>,
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
@group(0) @binding(0) var<uniform> iblPrecomputeParams: IblPrecomputeParams;
|
|
6
|
+
@group(0) @binding(1) var<storage, read> hdriEnvironmentInput: array<HdriConvolutionSample>;
|
|
7
|
+
@group(0) @binding(2) var<storage, read_write> hdriIrradianceOutput: array<vec4<f32>>;
|
|
8
|
+
|
|
9
|
+
fn face_extent() -> u32 {
|
|
10
|
+
return max(iblPrecomputeParams.sample_count, 1u);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn face_pixel_index(pixel: vec2<u32>) -> u32 {
|
|
14
|
+
let extent = face_extent();
|
|
15
|
+
return pixel.y * extent + pixel.x;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn safe_normalize(value: vec3<f32>) -> vec3<f32> {
|
|
19
|
+
let length_value = length(value);
|
|
20
|
+
if (length_value <= 0.000001) {
|
|
21
|
+
return vec3<f32>(0.0, 1.0, 0.0);
|
|
22
|
+
}
|
|
23
|
+
return value / length_value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn direction_from_texel(pixel: vec2<u32>, extent: u32) -> vec3<f32> {
|
|
27
|
+
let uv = (vec2<f32>(pixel) + vec2<f32>(0.5)) / max(vec2<f32>(f32(extent)), vec2<f32>(1.0));
|
|
28
|
+
let phi = (uv.x * 2.0 - 1.0) * 3.141592653589793;
|
|
29
|
+
let theta = uv.y * 3.141592653589793;
|
|
30
|
+
let sin_theta = sin(theta);
|
|
31
|
+
return safe_normalize(
|
|
32
|
+
vec3<f32>(
|
|
33
|
+
cos(phi) * sin_theta,
|
|
34
|
+
cos(theta),
|
|
35
|
+
sin(phi) * sin_theta
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn direction_to_index(direction: vec3<f32>, extent: u32) -> u32 {
|
|
41
|
+
let dir = safe_normalize(direction);
|
|
42
|
+
let phi = atan2(dir.z, dir.x);
|
|
43
|
+
let theta = acos(clamp(dir.y, -1.0, 1.0));
|
|
44
|
+
let u = fract(phi / (2.0 * 3.141592653589793) + 0.5);
|
|
45
|
+
let v = clamp(theta / 3.141592653589793, 0.0, 0.999999);
|
|
46
|
+
let pixel = vec2<u32>(
|
|
47
|
+
min(u32(floor(u * f32(extent))), extent - 1u),
|
|
48
|
+
min(u32(floor(v * f32(extent))), extent - 1u)
|
|
49
|
+
);
|
|
50
|
+
return face_pixel_index(pixel);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn sample_environment(direction: vec3<f32>) -> vec3<f32> {
|
|
54
|
+
let index = direction_to_index(direction, face_extent());
|
|
55
|
+
return hdriEnvironmentInput[index].direction_radiance.xyz;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@compute @workgroup_size(8, 8, 1)
|
|
59
|
+
fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
60
|
+
let extent = face_extent();
|
|
61
|
+
if (global_id.x >= extent || global_id.y >= extent) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let normal = direction_from_texel(global_id.xy, extent);
|
|
66
|
+
var accumulated_irradiance = vec3<f32>(0.0);
|
|
67
|
+
var total_weight = 0.0;
|
|
68
|
+
var sample_index = 0u;
|
|
69
|
+
loop {
|
|
70
|
+
if (sample_index >= extent) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let sample_pixel = vec2<u32>(
|
|
75
|
+
(global_id.x + sample_index) % extent,
|
|
76
|
+
(global_id.y + sample_index * 3u) % extent
|
|
77
|
+
);
|
|
78
|
+
let sample_direction = direction_from_texel(sample_pixel, extent);
|
|
79
|
+
let cosine_weight = max(dot(normal, sample_direction), 0.0);
|
|
80
|
+
if (cosine_weight > 0.0) {
|
|
81
|
+
accumulated_irradiance =
|
|
82
|
+
accumulated_irradiance + sample_environment(sample_direction) * cosine_weight;
|
|
83
|
+
total_weight = total_weight + cosine_weight;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sample_index = sample_index + 1u;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let irradiance =
|
|
90
|
+
accumulated_irradiance / max(total_weight, 0.0001) * max(iblPrecomputeParams.exposure_bias, 0.0001);
|
|
91
|
+
hdriIrradianceOutput[face_pixel_index(global_id.xy)] = vec4<f32>(irradiance, 1.0);
|
|
3
92
|
}
|
|
@@ -1,3 +1,111 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
struct HdriSpecularSample {
|
|
2
|
+
direction_radiance: vec4<f32>,
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
@group(0) @binding(0) var<uniform> iblPrecomputeParams: IblPrecomputeParams;
|
|
6
|
+
@group(0) @binding(1) var<storage, read> hdriEnvironmentInput: array<HdriSpecularSample>;
|
|
7
|
+
@group(0) @binding(2) var<storage, read_write> hdriSpecularOutput: array<vec4<f32>>;
|
|
8
|
+
|
|
9
|
+
fn face_extent() -> u32 {
|
|
10
|
+
return max(iblPrecomputeParams.sample_count, 1u);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn face_pixel_index(pixel: vec2<u32>) -> u32 {
|
|
14
|
+
let extent = face_extent();
|
|
15
|
+
return pixel.y * extent + pixel.x;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn safe_normalize(value: vec3<f32>) -> vec3<f32> {
|
|
19
|
+
let length_value = length(value);
|
|
20
|
+
if (length_value <= 0.000001) {
|
|
21
|
+
return vec3<f32>(0.0, 1.0, 0.0);
|
|
22
|
+
}
|
|
23
|
+
return value / length_value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn direction_from_texel(pixel: vec2<u32>, extent: u32) -> vec3<f32> {
|
|
27
|
+
let uv = (vec2<f32>(pixel) + vec2<f32>(0.5)) / max(vec2<f32>(f32(extent)), vec2<f32>(1.0));
|
|
28
|
+
let phi = (uv.x * 2.0 - 1.0) * 3.141592653589793;
|
|
29
|
+
let theta = uv.y * 3.141592653589793;
|
|
30
|
+
let sin_theta = sin(theta);
|
|
31
|
+
return safe_normalize(
|
|
32
|
+
vec3<f32>(
|
|
33
|
+
cos(phi) * sin_theta,
|
|
34
|
+
cos(theta),
|
|
35
|
+
sin(phi) * sin_theta
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn direction_to_index(direction: vec3<f32>, extent: u32) -> u32 {
|
|
41
|
+
let dir = safe_normalize(direction);
|
|
42
|
+
let phi = atan2(dir.z, dir.x);
|
|
43
|
+
let theta = acos(clamp(dir.y, -1.0, 1.0));
|
|
44
|
+
let u = fract(phi / (2.0 * 3.141592653589793) + 0.5);
|
|
45
|
+
let v = clamp(theta / 3.141592653589793, 0.0, 0.999999);
|
|
46
|
+
let pixel = vec2<u32>(
|
|
47
|
+
min(u32(floor(u * f32(extent))), extent - 1u),
|
|
48
|
+
min(u32(floor(v * f32(extent))), extent - 1u)
|
|
49
|
+
);
|
|
50
|
+
return face_pixel_index(pixel);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn sample_environment(direction: vec3<f32>) -> vec3<f32> {
|
|
54
|
+
let index = direction_to_index(direction, face_extent());
|
|
55
|
+
return hdriEnvironmentInput[index].direction_radiance.xyz;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn importance_sample_hemisphere(normal: vec3<f32>, xi: vec2<f32>, roughness: f32) -> vec3<f32> {
|
|
59
|
+
let phi = 2.0 * 3.141592653589793 * xi.x;
|
|
60
|
+
let alpha = max(roughness * roughness, 0.001);
|
|
61
|
+
let cos_theta = sqrt((1.0 - xi.y) / max(1.0 + (alpha * alpha - 1.0) * xi.y, 0.001));
|
|
62
|
+
let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0));
|
|
63
|
+
let tangent_seed = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.y) > 0.9);
|
|
64
|
+
let tangent = safe_normalize(cross(tangent_seed, normal));
|
|
65
|
+
let bitangent = cross(normal, tangent);
|
|
66
|
+
return safe_normalize(
|
|
67
|
+
tangent * cos(phi) * sin_theta +
|
|
68
|
+
bitangent * sin(phi) * sin_theta +
|
|
69
|
+
normal * cos_theta
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@compute @workgroup_size(8, 8, 1)
|
|
74
|
+
fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
75
|
+
let extent = face_extent();
|
|
76
|
+
if (global_id.x >= extent || global_id.y >= extent) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let normal = direction_from_texel(global_id.xy, extent);
|
|
81
|
+
let view_direction = normal;
|
|
82
|
+
let roughness = clamp_roughness(iblPrecomputeParams.roughness);
|
|
83
|
+
var prefiltered_radiance = vec3<f32>(0.0);
|
|
84
|
+
var importance_weight = 0.0;
|
|
85
|
+
var sample_index = 0u;
|
|
86
|
+
loop {
|
|
87
|
+
if (sample_index >= extent) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let xi = vec2<f32>(
|
|
92
|
+
(f32(sample_index) + 0.5) / f32(extent),
|
|
93
|
+
fract((f32(sample_index) * 0.7548776662466927) + (f32(global_id.x) + f32(global_id.y)) * 0.01)
|
|
94
|
+
);
|
|
95
|
+
let half_vector = importance_sample_hemisphere(normal, xi, roughness);
|
|
96
|
+
let sample_direction = reflect(-view_direction, half_vector);
|
|
97
|
+
let ndotl = max(dot(normal, sample_direction), 0.0);
|
|
98
|
+
if (ndotl > 0.0) {
|
|
99
|
+
let weight = mix(ndotl, pow(ndotl, 1.0 / max(roughness + 0.05, 0.05)), roughness);
|
|
100
|
+
prefiltered_radiance =
|
|
101
|
+
prefiltered_radiance + sample_environment(sample_direction) * weight;
|
|
102
|
+
importance_weight = importance_weight + weight;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
sample_index = sample_index + 1u;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let resolved_radiance =
|
|
109
|
+
prefiltered_radiance / max(importance_weight, 0.0001) * max(iblPrecomputeParams.exposure_bias, 0.0001);
|
|
110
|
+
hdriSpecularOutput[face_pixel_index(global_id.xy)] = vec4<f32>(resolved_radiance, roughness);
|
|
3
111
|
}
|
|
@@ -1,3 +1,106 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
struct VolumetricIntegrationParams {
|
|
2
|
+
light_direction: vec3<f32>,
|
|
3
|
+
history_blend: f32,
|
|
4
|
+
extinction_bias: f32,
|
|
5
|
+
ambient_boost: f32,
|
|
6
|
+
integration_step_scale: f32,
|
|
7
|
+
phase_bias: f32,
|
|
8
|
+
temporal_stability: f32,
|
|
9
|
+
reserved: f32,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
struct FroxelMediumVoxel {
|
|
13
|
+
extinction_density: vec4<f32>,
|
|
14
|
+
inscattering_anisotropy: vec4<f32>,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
struct VolumetricShadowHistory {
|
|
18
|
+
shadow_transmittance: vec4<f32>,
|
|
19
|
+
depth_phase: vec4<f32>,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
struct FroxelIntegratedVoxel {
|
|
23
|
+
integrated_scattering: vec4<f32>,
|
|
24
|
+
integrated_extinction: vec4<f32>,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
@group(0) @binding(0) var<uniform> froxelGridParams: FroxelGridParams;
|
|
28
|
+
@group(0) @binding(1) var<uniform> volumetricIntegrationParams: VolumetricIntegrationParams;
|
|
29
|
+
@group(0) @binding(2) var<storage, read> froxelMediumInput: array<FroxelMediumVoxel>;
|
|
30
|
+
@group(0) @binding(3) var<storage, read> froxelShadowInput: array<VolumetricShadowHistory>;
|
|
31
|
+
@group(0) @binding(4) var<storage, read_write> froxelIntegratedOutput: array<FroxelIntegratedVoxel>;
|
|
32
|
+
|
|
33
|
+
fn froxel_integrate_index(coord: vec3<u32>) -> u32 {
|
|
34
|
+
let plane = max(froxelGridParams.grid_width, 1u) * max(froxelGridParams.grid_height, 1u);
|
|
35
|
+
return coord.z * plane + coord.y * max(froxelGridParams.grid_width, 1u) + coord.x;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn safe_normalize(value: vec3<f32>) -> vec3<f32> {
|
|
39
|
+
let length_value = length(value);
|
|
40
|
+
if (length_value <= 0.000001) {
|
|
41
|
+
return vec3<f32>(0.0, 1.0, 0.0);
|
|
42
|
+
}
|
|
43
|
+
return value / length_value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn henyey_greenstein(cos_theta: f32, anisotropy: f32) -> f32 {
|
|
47
|
+
let g = clamp(anisotropy, -0.85, 0.85);
|
|
48
|
+
let denominator = pow(max(1.0 + g * g - 2.0 * g * cos_theta, 0.001), 1.5);
|
|
49
|
+
return (1.0 - g * g) / max(12.566370614359172 * denominator, 0.001);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@compute @workgroup_size(4, 4, 4)
|
|
53
|
+
fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
54
|
+
if (
|
|
55
|
+
global_id.x >= froxelGridParams.grid_width ||
|
|
56
|
+
global_id.y >= froxelGridParams.grid_height ||
|
|
57
|
+
global_id.z >= froxelGridParams.grid_depth
|
|
58
|
+
) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let index = froxel_integrate_index(global_id);
|
|
63
|
+
let medium = froxelMediumInput[index];
|
|
64
|
+
let shadow = froxelShadowInput[index];
|
|
65
|
+
let previous = froxelIntegratedOutput[index];
|
|
66
|
+
let light_direction = safe_normalize(volumetricIntegrationParams.light_direction);
|
|
67
|
+
let slice_depth = (f32(global_id.z) + 0.5) / max(f32(froxelGridParams.grid_depth), 1.0);
|
|
68
|
+
let density = max(medium.extinction_density.w + volumetricIntegrationParams.extinction_bias, 0.0);
|
|
69
|
+
let extinction = max(medium.extinction_density.xyz, vec3<f32>(0.0001));
|
|
70
|
+
let albedo = clamp(medium.inscattering_anisotropy.xyz, vec3<f32>(0.0), vec3<f32>(4.0));
|
|
71
|
+
let anisotropy = medium.inscattering_anisotropy.w;
|
|
72
|
+
let phase = henyey_greenstein(light_direction.y, anisotropy + volumetricIntegrationParams.phase_bias * 0.1);
|
|
73
|
+
let step_length =
|
|
74
|
+
max(volumetricIntegrationParams.integration_step_scale, 0.2) /
|
|
75
|
+
max(f32(froxelGridParams.grid_depth), 1.0);
|
|
76
|
+
let shadow_visibility = shadow.shadow_transmittance.w;
|
|
77
|
+
let ambient_term = vec3<f32>(0.08, 0.1, 0.14) * max(volumetricIntegrationParams.ambient_boost, 0.0);
|
|
78
|
+
let sample = MediumSample(
|
|
79
|
+
extinction,
|
|
80
|
+
(albedo * (0.25 + slice_depth * 0.5) + ambient_term) * shadow_visibility * phase
|
|
81
|
+
);
|
|
82
|
+
let transmittance = exp(-sample.extinction * density * step_length);
|
|
83
|
+
let integrated_scattering =
|
|
84
|
+
sample.inscattering *
|
|
85
|
+
froxelGridParams.scattering_strength *
|
|
86
|
+
(1.0 - transmittance) *
|
|
87
|
+
(1.0 + shadow.depth_phase.z * 0.2);
|
|
88
|
+
let integrated_extinction = sample.extinction * density * step_length;
|
|
89
|
+
let stability = clamp(
|
|
90
|
+
shadow.depth_phase.w * volumetricIntegrationParams.temporal_stability,
|
|
91
|
+
0.0,
|
|
92
|
+
1.0
|
|
93
|
+
);
|
|
94
|
+
let blend = clamp(volumetricIntegrationParams.history_blend, 0.0, 0.98) * stability;
|
|
95
|
+
let resolved_scattering =
|
|
96
|
+
previous.integrated_scattering.xyz * blend +
|
|
97
|
+
integrated_scattering * (1.0 - blend);
|
|
98
|
+
let resolved_extinction =
|
|
99
|
+
previous.integrated_extinction.xyz * blend +
|
|
100
|
+
integrated_extinction * (1.0 - blend);
|
|
101
|
+
|
|
102
|
+
froxelIntegratedOutput[index] = FroxelIntegratedVoxel(
|
|
103
|
+
vec4<f32>(resolved_scattering, shadow_visibility),
|
|
104
|
+
vec4<f32>(resolved_extinction, stability)
|
|
105
|
+
);
|
|
3
106
|
}
|
|
@@ -1,3 +1,97 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
struct VolumetricLightParams {
|
|
2
|
+
light_direction: vec3<f32>,
|
|
3
|
+
history_blend: f32,
|
|
4
|
+
light_color: vec3<f32>,
|
|
5
|
+
shadow_strength: f32,
|
|
6
|
+
slice_depth_scale: f32,
|
|
7
|
+
density_bias: f32,
|
|
8
|
+
phase_bias: f32,
|
|
9
|
+
jitter_amount: f32,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
struct FroxelMediumVoxel {
|
|
13
|
+
extinction_density: vec4<f32>,
|
|
14
|
+
inscattering_anisotropy: vec4<f32>,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
struct VolumetricShadowHistory {
|
|
18
|
+
shadow_transmittance: vec4<f32>,
|
|
19
|
+
depth_phase: vec4<f32>,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
@group(0) @binding(0) var<uniform> froxelGridParams: FroxelGridParams;
|
|
23
|
+
@group(0) @binding(1) var<uniform> volumetricLightParams: VolumetricLightParams;
|
|
24
|
+
@group(0) @binding(2) var<storage, read> froxelMediumInput: array<FroxelMediumVoxel>;
|
|
25
|
+
@group(0) @binding(3) var<storage, read> froxelShadowHistory: array<VolumetricShadowHistory>;
|
|
26
|
+
@group(0) @binding(4) var<storage, read_write> froxelShadowOutput: array<VolumetricShadowHistory>;
|
|
27
|
+
|
|
28
|
+
fn froxel_shadow_index(coord: vec3<u32>) -> u32 {
|
|
29
|
+
let plane = max(froxelGridParams.grid_width, 1u) * max(froxelGridParams.grid_height, 1u);
|
|
30
|
+
return coord.z * plane + coord.y * max(froxelGridParams.grid_width, 1u) + coord.x;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn safe_normalize(value: vec3<f32>) -> vec3<f32> {
|
|
34
|
+
let length_value = length(value);
|
|
35
|
+
if (length_value <= 0.000001) {
|
|
36
|
+
return vec3<f32>(0.0, 1.0, 0.0);
|
|
37
|
+
}
|
|
38
|
+
return value / length_value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn henyey_greenstein(cos_theta: f32, anisotropy: f32) -> f32 {
|
|
42
|
+
let g = clamp(anisotropy, -0.85, 0.85);
|
|
43
|
+
let denominator = pow(max(1.0 + g * g - 2.0 * g * cos_theta, 0.001), 1.5);
|
|
44
|
+
return (1.0 - g * g) / max(12.566370614359172 * denominator, 0.001);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@compute @workgroup_size(4, 4, 4)
|
|
48
|
+
fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
49
|
+
if (
|
|
50
|
+
global_id.x >= froxelGridParams.grid_width ||
|
|
51
|
+
global_id.y >= froxelGridParams.grid_height ||
|
|
52
|
+
global_id.z >= froxelGridParams.grid_depth
|
|
53
|
+
) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let index = froxel_shadow_index(global_id);
|
|
58
|
+
let medium = froxelMediumInput[index];
|
|
59
|
+
let previous = froxelShadowHistory[index];
|
|
60
|
+
let light_direction = safe_normalize(volumetricLightParams.light_direction);
|
|
61
|
+
let slice_depth = (f32(global_id.z) + 0.5) / max(f32(froxelGridParams.grid_depth), 1.0);
|
|
62
|
+
let density = max(medium.extinction_density.w + volumetricLightParams.density_bias, 0.0);
|
|
63
|
+
let extinction = max(dot(medium.extinction_density.xyz, vec3<f32>(0.2126, 0.7152, 0.0722)), 0.0001);
|
|
64
|
+
let anisotropy = medium.inscattering_anisotropy.w;
|
|
65
|
+
let phase_weight = henyey_greenstein(light_direction.y, anisotropy);
|
|
66
|
+
let depth_scale = max(volumetricLightParams.slice_depth_scale, 0.25);
|
|
67
|
+
let shadow_distance = depth_scale * mix(0.35, 1.35, slice_depth);
|
|
68
|
+
let optical_depth = (density * 0.7 + extinction * 0.3) * shadow_distance;
|
|
69
|
+
let raw_transmittance = exp(-optical_depth);
|
|
70
|
+
let horizon_wrap = 0.35 + 0.65 * saturate(light_direction.y * 0.5 + 0.5);
|
|
71
|
+
let jitter = fract(
|
|
72
|
+
f32(global_id.x * 19u + global_id.y * 47u + global_id.z * 73u) * 0.61803398875
|
|
73
|
+
);
|
|
74
|
+
let visibility = clamp(
|
|
75
|
+
raw_transmittance * horizon_wrap * (1.0 - volumetricLightParams.jitter_amount * (jitter - 0.5)),
|
|
76
|
+
0.0,
|
|
77
|
+
1.0
|
|
78
|
+
);
|
|
79
|
+
let history_visibility = previous.shadow_transmittance.w;
|
|
80
|
+
let blend = clamp(volumetricLightParams.history_blend, 0.0, 0.98);
|
|
81
|
+
let shadow_transmittance = mix(visibility, history_visibility, blend);
|
|
82
|
+
let light_radiance =
|
|
83
|
+
volumetricLightParams.light_color *
|
|
84
|
+
shadow_transmittance *
|
|
85
|
+
max(volumetricLightParams.shadow_strength, 0.0) *
|
|
86
|
+
(0.55 + phase_weight * 0.45);
|
|
87
|
+
let depth_confidence = clamp(
|
|
88
|
+
slice_depth * 0.45 + shadow_transmittance * 0.35 + (1.0 - density) * 0.2,
|
|
89
|
+
0.0,
|
|
90
|
+
1.0
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
froxelShadowOutput[index] = VolumetricShadowHistory(
|
|
94
|
+
vec4<f32>(light_radiance, shadow_transmittance),
|
|
95
|
+
vec4<f32>(slice_depth, optical_depth, phase_weight, depth_confidence)
|
|
96
|
+
);
|
|
3
97
|
}
|