@plasius/gpu-lighting 0.2.3 → 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 +86 -11
- package/README.md +85 -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 +2 -2
- 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
|
|
|
@@ -43,14 +46,86 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
43
46
|
- Added `sunlitBaseline` to environment lighting presets and wavefront
|
|
44
47
|
lighting options so renderers can apply a time-of-day daylight floor at
|
|
45
48
|
terminal path collisions without raising ambient residual colour.
|
|
46
|
-
-
|
|
49
|
+
- Added an Eames screenshot capture runbook and browser-runtime helpers so
|
|
50
|
+
validation scripts can attach to an existing WebGPU-capable Chrome over CDP
|
|
51
|
+
instead of only launching a fresh Playwright Chromium profile.
|
|
52
|
+
- Added generic glTF material forwarding in the Eames validation loader so
|
|
53
|
+
authored specular, sheen, clearcoat, transmission, emissive, and IOR values
|
|
54
|
+
can flow into the shared wavefront renderer without model-name overrides.
|
|
55
|
+
- Added Eames validation HUD/result diagnostics for GPU worker-job throughput
|
|
56
|
+
so validation runs can report compute-dispatch jobs per frame, per second,
|
|
57
|
+
and per command submission.
|
|
58
|
+
- Added adaptive Eames validation frame budgeting so motion-oriented runs can
|
|
59
|
+
request a `frameTimeBudgetMs` and inspect delivered `rendered/target spp`
|
|
60
|
+
in the HUD and result payload.
|
|
61
|
+
- Added `@plasius/gpu-performance`-governed adaptive SPP control to the Eames
|
|
62
|
+
validation harness so frame-budgeted runs degrade and recover through a
|
|
63
|
+
shared quality-ladder contract instead of a demo-local policy alone.
|
|
47
64
|
|
|
48
65
|
- **Changed**
|
|
49
66
|
- Reduced ambient residual strength for the grass-field, forest, warehouse,
|
|
50
67
|
and cavern environment preset families to avoid low-sample whitewash.
|
|
68
|
+
- Eames validation capture scripts now pin deterministic render settings by
|
|
69
|
+
default and write canvas-only PNG artifacts plus comparable black-pixel and
|
|
70
|
+
luminance metrics under `output/playwright/eames-environments/`.
|
|
71
|
+
- Eames validation page and capture entry points now accept up to `256 spp`,
|
|
72
|
+
and the validation boot timeout now scales with requested render workload
|
|
73
|
+
instead of failing at a fixed 60-second watchdog.
|
|
74
|
+
- `gpu-lighting` local typecheck coverage now includes the Eames validation
|
|
75
|
+
page, loader, and capture helpers instead of checking only the legacy demo
|
|
76
|
+
entry points.
|
|
77
|
+
- Eames validation mesh loading now preserves authored UVs and decoded
|
|
78
|
+
base-colour, metallic-roughness, normal, and occlusion maps so the shared
|
|
79
|
+
wavefront renderer can evaluate materially richer leather, wood, and chrome
|
|
80
|
+
surfaces.
|
|
81
|
+
- Eames validation meshes now forward decoded emissive maps as well so the
|
|
82
|
+
shared wavefront renderer can keep all material texture evaluation in the
|
|
83
|
+
GPU render path.
|
|
84
|
+
- Eames validation mesh building now preserves authored material values
|
|
85
|
+
generically instead of deriving chrome, leather, and wood behaviour from
|
|
86
|
+
material names.
|
|
87
|
+
- Eames validation page can now freeze its own canvas and POST the PNG back
|
|
88
|
+
to a local bridge endpoint, which provides a browser-driven screenshot
|
|
89
|
+
fallback when Playwright cannot own the Chromium process directly.
|
|
90
|
+
- Eames validation capture and reverse-pass debug entry points now share one
|
|
91
|
+
server-selection helper so port reuse, static serving, and bridge-ready
|
|
92
|
+
startup rules stay aligned across local runs and CI.
|
|
51
93
|
|
|
52
94
|
- **Fixed**
|
|
53
|
-
-
|
|
95
|
+
- Eames Playwright validation pages and capture scripts now surface import,
|
|
96
|
+
WebGPU bootstrap, and renderer startup diagnostics instead of hanging on the
|
|
97
|
+
initial HUD when a browser cannot complete setup.
|
|
98
|
+
- Eames validation renders now collect optional output-probe figures after the
|
|
99
|
+
frame render completes instead of coupling probe readback to the heavy
|
|
100
|
+
high-SPP render submission itself.
|
|
101
|
+
- Eames capture helpers now resolve browser profile and temporary output paths
|
|
102
|
+
through the host OS temp directory instead of assuming macOS-only
|
|
103
|
+
`/private/tmp`, which fixes Linux CI validation.
|
|
104
|
+
- Eames validation query parsing now preserves documented fallback defaults
|
|
105
|
+
when numeric URL params are omitted, which keeps standard captures out of
|
|
106
|
+
accidental reverse-pass debug mode.
|
|
107
|
+
- Eames validation motion renders now preserve the built-in adaptive
|
|
108
|
+
`frameTimeBudgetMs` default when the query parameter is omitted, so the
|
|
109
|
+
shared `@plasius/gpu-performance` quality ladder still engages on the
|
|
110
|
+
standard animated validation route.
|
|
111
|
+
- Animated source-marker captures now reuse the injected product-studio scene
|
|
112
|
+
helper during per-frame rebuilds instead of throwing when motion is enabled.
|
|
113
|
+
- Eames capture waits now scale with requested resolution, frame count, depth,
|
|
114
|
+
and SPP so higher-workload validation runs are not aborted at a stale fixed
|
|
115
|
+
timeout.
|
|
116
|
+
- Eames glTF validation materials now honor `KHR_texture_transform` offsets,
|
|
117
|
+
scales, and rotation by baking transformed texture maps during decode so
|
|
118
|
+
chair screenshots reflect the authored leather and wood layouts.
|
|
119
|
+
- The capture bridge now rejects non-loopback browser origins and confines
|
|
120
|
+
capture writes to `output/playwright/eames-environments/`, which closes the
|
|
121
|
+
browser-driven workspace overwrite path on the local upload endpoint.
|
|
122
|
+
- The Eames glTF loader now honors interleaved `bufferView.byteStride` values,
|
|
123
|
+
which keeps positions, normals, and UVs correct for legal strided assets.
|
|
124
|
+
- The capture bridge now serves static assets without a pre-stat/read race,
|
|
125
|
+
and browser-driven capture uploads are restricted to loopback bridge URLs.
|
|
126
|
+
- The standalone repo now ships the Eames demo asset set referenced by the
|
|
127
|
+
validation page so fresh checkouts can render the chair without external
|
|
128
|
+
workspace-only files.
|
|
54
129
|
|
|
55
130
|
- **Security**
|
|
56
131
|
- (placeholder)
|
|
@@ -389,7 +464,7 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
389
464
|
[0.1.16]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.1.16
|
|
390
465
|
[0.1.17]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.1.17
|
|
391
466
|
[0.1.19]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.1.19
|
|
392
|
-
[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
|
|
393
468
|
[0.2.0]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.2.0
|
|
394
469
|
[0.2.2]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.2.2
|
|
395
|
-
[0.2.
|
|
470
|
+
[0.2.5]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.2.5
|
package/README.md
CHANGED
|
@@ -42,6 +42,81 @@ For browser-only serving, the demo resolves `@plasius/gpu-shared` through an
|
|
|
42
42
|
import map so the page stays on the published package surface rather than a
|
|
43
43
|
package-private source path.
|
|
44
44
|
|
|
45
|
+
## Screenshot Capture
|
|
46
|
+
|
|
47
|
+
The repo also carries the Eames-chair environment validation harness under
|
|
48
|
+
`demo/eames-environments/` plus tracked Playwright helpers under
|
|
49
|
+
`scripts/eames-environments/`. The referenced chair asset is tracked under
|
|
50
|
+
`data/models/eames-lounge-chair-ottoman/` so fresh repo checkouts can run the
|
|
51
|
+
validation page without depending on a parent monorepo checkout. Build
|
|
52
|
+
`gpu-performance`, `gpu-renderer`, and `gpu-lighting` first, then run:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
node scripts/eames-environments/capture.mjs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For reverse-pass black-pixel diagnostics, run:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
node scripts/eames-environments/path-debug-capture.mjs
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If fresh Playwright Chromium bootstrap is unstable on macOS, start a
|
|
65
|
+
WebGPU-capable Chrome separately with remote debugging enabled and set
|
|
66
|
+
`PLASIUS_CAPTURE_CDP_URL=http://127.0.0.1:<port>` before running the capture
|
|
67
|
+
script. The validation page now reports bootstrap step, detail, and WebGPU
|
|
68
|
+
availability through `window.__plasiusCaptureState` and
|
|
69
|
+
`window.__plasiusCaptureError` so capture failures stop at a named phase rather
|
|
70
|
+
than hanging on the initial HUD.
|
|
71
|
+
|
|
72
|
+
For browser-controlled fallbacks, start
|
|
73
|
+
`node scripts/eames-environments/capture-bridge-server.mjs <port>` and open the
|
|
74
|
+
validation page with `captureBitmap=1` plus
|
|
75
|
+
`captureUploadPath=output/playwright/eames-environments/<name>.png`. If the page
|
|
76
|
+
is being served by a plain static server such as `python -m http.server`, also
|
|
77
|
+
pass
|
|
78
|
+
`captureUploadUrl=http://127.0.0.1:<port>/__plasius-capture`. The page will
|
|
79
|
+
freeze its own canvas and POST the PNG back to the bridge server once the
|
|
80
|
+
render completes. The browser-side upload helper now rejects non-loopback
|
|
81
|
+
capture endpoints so this fallback cannot be redirected at arbitrary remote
|
|
82
|
+
origins.
|
|
83
|
+
|
|
84
|
+
The main capture and reverse-pass debug capture entry points now share the same
|
|
85
|
+
server-selection helper, so local reuse, fresh static-server startup, and
|
|
86
|
+
bridge fallback all follow the same port and readiness rules across macOS and
|
|
87
|
+
Linux.
|
|
88
|
+
|
|
89
|
+
The capture scripts now pin deterministic validation settings unless explicitly
|
|
90
|
+
overridden:
|
|
91
|
+
|
|
92
|
+
- `PLASIUS_CAPTURE_MAX_DEPTH=8`
|
|
93
|
+
- `PLASIUS_CAPTURE_SPP=1`
|
|
94
|
+
- `PLASIUS_CAPTURE_FRAMES=1`
|
|
95
|
+
- `PLASIUS_CAPTURE_DENOISE=1`
|
|
96
|
+
- `PLASIUS_CAPTURE_MOTION=0`
|
|
97
|
+
- `PLASIUS_CAPTURE_FRAME_INDEX=777`
|
|
98
|
+
- `PLASIUS_CAPTURE_PROBE=1`
|
|
99
|
+
|
|
100
|
+
They save canvas-only PNGs plus per-capture JSON under
|
|
101
|
+
`output/playwright/eames-environments/`, including exact-black, near-black, and
|
|
102
|
+
average-luminance metrics so screenshot comparisons stay apples-to-apples. The
|
|
103
|
+
validation page now decouples optional probe readback from the heavy render
|
|
104
|
+
submission itself, which keeps higher-SPP screenshot validation more stable.
|
|
105
|
+
For motion or realtime validation, the page also accepts `frameTimeBudgetMs`
|
|
106
|
+
and will render at least one full-screen sample before adaptively spending the
|
|
107
|
+
rest of the per-frame budget on additional SPP passes. The HUD reports
|
|
108
|
+
`rendered/target spp` whenever the budgeted frame lands below the configured
|
|
109
|
+
ceiling. When `gpu-performance/dist/index.js` is available, the Eames harness
|
|
110
|
+
also routes that target through a `@plasius/gpu-performance` quality ladder fed
|
|
111
|
+
by `gpu-renderer`'s wavefront adaptive-sampling levels, so the requested SPP
|
|
112
|
+
becomes a release-grade ceiling rather than an ungoverned demo-local heuristic.
|
|
113
|
+
The Eames loader also preserves authored UVs, decoded base-colour,
|
|
114
|
+
metallic-roughness, normal, occlusion, and emissive maps, plus authored glTF
|
|
115
|
+
material factors such as clearcoat, sheen colour, specular colour,
|
|
116
|
+
transmission, and IOR when present. That keeps the validation scene on the
|
|
117
|
+
shared renderer path instead of relying on material-name-specific overrides for
|
|
118
|
+
leather, wood, and chrome.
|
|
119
|
+
|
|
45
120
|
## Usage (load one technique)
|
|
46
121
|
|
|
47
122
|
```js
|
|
@@ -250,6 +325,11 @@ The package now ships concrete WGSL contracts for:
|
|
|
250
325
|
- `hybrid.screenTrace`: first-hit reflection tracing over the shared hybrid scene contracts
|
|
251
326
|
- `hybrid.radianceCache`: irradiance history updates for cache-backed indirect reuse
|
|
252
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
|
|
253
333
|
- `pathtracer.pathTrace`: analytic scene tracing, bounce integration, and sky fallback
|
|
254
334
|
- `pathtracer.accumulate`: progressive history resolve with reset handling
|
|
255
335
|
- `pathtracer.denoise`: spatial-temporal bilateral filtering for reference previews
|
|
@@ -279,12 +359,12 @@ graph.
|
|
|
279
359
|
- `accumulate`
|
|
280
360
|
- `denoise`
|
|
281
361
|
- `volumetrics`
|
|
282
|
-
- `froxelIntegrate
|
|
283
|
-
- `volumetricShadow
|
|
362
|
+
- `froxelIntegrate`: accumulates participating-media scattering/extinction per froxel
|
|
363
|
+
- `volumetricShadow`: resolves directional shadow transmittance history per froxel
|
|
284
364
|
- `hdri`
|
|
285
|
-
- `irradianceConvolution
|
|
286
|
-
- `specularPrefilter
|
|
287
|
-
- `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
|
|
288
368
|
|
|
289
369
|
## Demo
|
|
290
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plasius/gpu-lighting",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Advanced lighting WGSL modules and planning profiles for @plasius/gpu-worker.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsup && cp -R src/techniques dist/techniques",
|
|
28
28
|
"demo": "python3 -m http.server --directory ..",
|
|
29
|
-
"typecheck": "node --check src/index.js && node --check demo/lighting-demo-config.js && node --check demo/main.js",
|
|
29
|
+
"typecheck": "node --check src/index.js && node --check demo/lighting-demo-config.js && node --check demo/main.js && node --check demo/eames-environments/page.js && node --check demo/eames-environments/eames-loader.js && node --check scripts/eames-environments/capture-runtime.mjs && node --check scripts/eames-environments/capture-server.mjs && node --check scripts/eames-environments/capture.mjs && node --check scripts/eames-environments/path-debug-capture.mjs && node --check scripts/eames-environments/capture-bridge-server.mjs",
|
|
30
30
|
"audit:eslint": "eslint . --max-warnings=0",
|
|
31
31
|
"audit:deps": "npm ls --all --omit=optional --omit=peer > /dev/null 2>&1 || true",
|
|
32
32
|
"audit:npm": "npm audit --audit-level=high --omit=dev",
|
|
@@ -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
|
}
|