From acbf40199da7df9bc94a3fb43604e5cb316209c0 Mon Sep 17 00:00:00 2001 From: "Yifeng \"Evan\" Wang" <7312949+doodlewind@users.noreply.github.com> Date: Sat, 14 Nov 2026 16:29:28 +0800 Subject: [PATCH] The last bounce lit by the directional lights alone: THREERT_LAST_DIRECT=sun (`last_sun`) A device whose budget cuts paths to one bounce lit that bounce's vertex by the sky and emitters alone unless `last_direct` was on (Android's default): on a Steam Deck Daylight Home's ceiling was near black, the sunlit floor lighting nothing. `last_direct` gives it back but costs every light's shadow rays at that vertex: on the Deck OpenStrikeRT's Foundry went from 21 to 29 ms a frame. `last_sun` takes the directional lights alone there (one shadow ray a light, `sun_light`, as `direct_light` takes them): Daylight Home lit as with `last_direct`, OpenStrikeRT 22 ms. - `KernelSwitches::last_sun`, flag `last_sun`; THREERT_LAST_DIRECT=sun sets it in place of `last_direct` (which includes it). Off unless set; the ThreeRT clients set it for a one-bounce budget off Android. - `sun_light` and the last vertex's branch are under `@if(last_sun && !last_direct)`: every other variant's WGSL is as it was. - Tests: the option parses on either platform; the variant has the function and the one branch; every variant stays valid WGSL. Co-Authored-By: Claude Opus 5.5 --- docs/RUNTIME.md | 2 +- renderer/shaders/pathtrace.wgsl | 41 +++++++++++++++++++++++++++++++-- renderer/src/gpu.rs | 30 ++++++++++++++++++------ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/docs/RUNTIME.md b/docs/RUNTIME.md index 4f88b11..1cc2f9c 100644 --- a/docs/RUNTIME.md +++ b/docs/RUNTIME.md @@ -1237,7 +1237,7 @@ The path-tracing kernel is one WESL module: `pathtrace.wgsl`, then `trace_units. translation (`@if(flag)` on a declaration or a statement, `@if(flag) { .. } @else { .. }` on a block of declarations). wesl-rs (WESL's compiler in Rust, natively and in a browser alike) makes a kernel's WGSL from a variant (gpu.rs `KernelVariant`, `kernel_wgsl`): the traversal (`hwrt`), a device's switches (`strict`, -`cutout_lite`, `cutout_lite2`, `last_direct`, `light_pick`), software traversal's units (`units`), the binding layout (`merged`, the browser's; `uniform_tables`, Android's) and the loop guard (`guard`). +`cutout_lite`, `cutout_lite2`, `last_direct`, `last_sun`, `light_pick`), software traversal's units (`units`), the binding layout (`merged`, the browser's; `uniform_tables`, Android's) and the loop guard (`guard`). pathtrace.wgsl's head names them; a flag the source names that a kernel is not given is an error. A scene's other features are bits of `FEAT`, a pipeline-overridable constant: one kernel source for all of them (`OBJECT_STRIDE` too is an override, from F_TINT). diff --git a/renderer/shaders/pathtrace.wgsl b/renderer/shaders/pathtrace.wgsl index 7592070..93a2a1b 100644 --- a/renderer/shaders/pathtrace.wgsl +++ b/renderer/shaders/pathtrace.wgsl @@ -9,7 +9,7 @@ // compiled for a device and a scene by WESL's conditional translation (`@if`, gpu.rs `kernel_wgsl`): a declaration // or a statement under `@if(x)` is there only where x holds. The flags: // hwrt hardware traversal (ray queries) in the software BVH walk's place, and the translucency pass -// strict, cutout_lite, cutout_lite2, last_direct, light_pick +// strict, cutout_lite, cutout_lite2, last_direct, last_sun, light_pick // a device's switches (gpu.rs `KernelSwitches`: what a GPU the kernel as written is wrong or // too slow on needs) // units software traversal's second level (gpu.rs `F_UNITS`); a scene's other features are bits @@ -1919,6 +1919,33 @@ fn hg_phase(g: f32, c: f32) -> f32 { return (1.0 - g * g) / (4.0 * PI * d * sqrt(d)); } +// gpu.rs `KernelSwitches::last_sun`: the directional lights alone, as `direct_light` takes them (one shadow ray a +// light), for the path's last vertex. +@if(last_sun && !last_direct) fn sun_light(s: Surface, lb: Lobes, v: vec3) -> vec3 { + var sum = vec3(0.0); + let o = offset_origin(s.p, s.ng); + for (var i = 0u; i < P.counts2.x; i++) { + let li = light_at(i); + if (all(li.color.rgb <= vec3(0.0))) { continue; } + let cos_max = li.dir.w; + let to_light = -li.dir.xyz; + let u = rng2(rng_sub(rng_dim(D_SUN), i)); + let cos_t = 1.0 - u.x * (1.0 - cos_max); + let sin_t = sqrt(max(0.0, 1.0 - cos_t * cos_t)); + let phi = 2.0 * PI * u.y; + let l = basis(to_light) * vec3(sin_t * cos(phi), sin_t * sin(phi), cos_t); + let f = eval_bsdf_k(s, lb, v, l, li.x.y); + if (any(f > vec3(0.0))) { + let vis = light_vis(li.ext.w >= 0.5, o, l, INF, rng_sub(rng_dim(D_SUN_VIS), i)); + if (any(vis > vec3(0.0))) { + let pdf_l = 1.0 / (2.0 * PI * (1.0 - cos_max)); + sum += li.color.rgb * f * power_heuristic(pdf_l, pdf_bsdf(s, lb, v, l)) * gtint(vis); + } + } + } + return sum; +} + fn direct_light(s: Surface, lb: Lobes, v: vec3) -> vec3 { var sum = vec3(0.0); dl_spec = vec3(0.0); @@ -3300,7 +3327,17 @@ fn main(@builtin(global_invocation_id) gid: vec3) { radiance += c; } if (s.card) { break; } - @if(!last_direct) if (depth == P.counts.z) { break; } + @if(!last_direct && !last_sun) if (depth == P.counts.z) { break; } + @if(last_sun && !last_direct) if (depth == P.counts.z) { + // gpu.rs `KernelSwitches::last_sun`: the last vertex's light from the directional lights alone. + if (depth > 0u) { + var dz = thr * sun_light(s, lobes(s, max(dot(s.n, -d), 1e-4)), -d); + let lum = luminance(dz); + if (lum > clamp_v && !straight) { dz *= clamp_v / lum; } + radiance += dz; + } + break; + } @if(last_direct) if (depth == P.counts.z) { // gpu.rs `KernelSwitches::last_direct`: the last vertex's direct light. if (depth > 0u) { diff --git a/renderer/src/gpu.rs b/renderer/src/gpu.rs index 4db4474..e892022 100644 --- a/renderer/src/gpu.rs +++ b/renderer/src/gpu.rs @@ -735,7 +735,7 @@ impl KernelVariant { } /// The flags of the kernel's conditional translation (pathtrace.wgsl's head). - pub fn flags(&self) -> [(&'static str, bool); 10] { + pub fn flags(&self) -> [(&'static str, bool); 11] { let (k, f) = (&self.switches, |bit: u32| self.features & bit != 0); [ ("hwrt", self.hwrt), @@ -743,6 +743,7 @@ impl KernelVariant { ("cutout_lite", k.cutout_lite >= 1), ("cutout_lite2", k.cutout_lite >= 2), ("last_direct", k.last_direct), + ("last_sun", k.last_sun), ("light_pick", k.light_pick), ("units", f(F_UNITS)), ("merged", self.merged.is_some()), @@ -1030,6 +1031,12 @@ pub struct KernelSwitches { /// takes its direct light. A device that affords one bounce and no radiance cache otherwise lights that bounce /// by sky and emitters alone: sunlit ground lit nothing, interiors were black. pub last_direct: bool, + /// THREERT_LAST_DIRECT=sun (off everywhere unless set; the ThreeRT clients set it for a budget of one bounce on + /// Linux). The path's last vertex takes the directional lights' direct light alone (one shadow ray a light): what + /// one bounce indoors lacks most (sunlit floor lighting the ceiling: Daylight Home's ceiling was black on a Steam + /// Deck), at a fraction of `last_direct`'s cost where a scene has many local lights (OpenStrikeRT's Foundry on the + /// Deck: +1 ms a frame, `last_direct` +8). `last_direct` includes it. + pub last_sun: bool, /// THREERT_LIGHT_PICK=1|0 (off everywhere unless set). Past the camera hit, `direct_light` takes the directional /// lights and one of the other kinds a scene has (local lights, emitters, the sky), weighted by their number, /// instead of all of them: two shadow rays a vertex instead of four. Unbiased; the indirect light is noisier. @@ -1044,7 +1051,8 @@ impl KernelSwitches { fn from_vars(var: &dyn Fn(&str) -> Option, android: bool) -> KernelSwitches { let num = |name: &str, default: u32| var(name).and_then(|v| v.parse::().ok()).unwrap_or(default); let on = android as u32; - KernelSwitches { strict: num("THREERT_KERNEL_STRICT", on) != 0, cutout_lite: num("THREERT_CUTOUT_LITE", on), last_direct: num("THREERT_LAST_DIRECT", on) != 0, light_pick: num("THREERT_LIGHT_PICK", 0) != 0 } + let sun = var("THREERT_LAST_DIRECT").is_some_and(|v| v.trim() == "sun"); + KernelSwitches { strict: num("THREERT_KERNEL_STRICT", on) != 0, cutout_lite: num("THREERT_CUTOUT_LITE", on), last_direct: !sun && num("THREERT_LAST_DIRECT", on) != 0, last_sun: sun, light_pick: num("THREERT_LIGHT_PICK", 0) != 0 } } } @@ -3388,13 +3396,14 @@ mod tests { let sha = |s: &str| Sha256::digest(s.as_bytes()).iter().map(|b| format!("{b:02x}")).collect::(); assert_eq!(crate::live::live_source(), include_str!("../shaders/live.wgsl")); let template = kernel_template(); - let all = KernelSwitches { strict: true, cutout_lite: 2, last_direct: true, light_pick: true }; + let all = KernelSwitches { strict: true, cutout_lite: 2, last_direct: true, last_sun: false, light_pick: true }; let switches = [ KernelSwitches::default(), KernelSwitches { strict: true, ..Default::default() }, KernelSwitches { cutout_lite: 1, ..Default::default() }, KernelSwitches { cutout_lite: 2, ..Default::default() }, KernelSwitches { last_direct: true, ..Default::default() }, + KernelSwitches { last_sun: true, ..Default::default() }, KernelSwitches { light_pick: true, ..Default::default() }, all, ]; @@ -3465,13 +3474,16 @@ mod tests { let vars = |set: &'static [(&'static str, &'static str)]| move |name: &str| set.iter().find(|(n, _)| *n == name).map(|(_, v)| v.to_string()); let desktop = GpuOptions::from_vars(&vars(&[]), false); assert_eq!(desktop, GpuOptions::default()); - assert_eq!(desktop.kernel, KernelSwitches { strict: false, cutout_lite: 0, last_direct: false, light_pick: false }); + assert_eq!(desktop.kernel, KernelSwitches { strict: false, cutout_lite: 0, last_direct: false, last_sun: false, light_pick: false }); assert!(!desktop.uniform_tables_on(false, false)); for hwrt in [false, true] { assert!(KernelVariant::new(hwrt, desktop.kernel, 0).flags().iter().all(|&(name, on)| on == (name == "hwrt" && hwrt))); } let android = GpuOptions::from_vars(&vars(&[]), true); - assert_eq!(android.kernel, KernelSwitches { strict: true, cutout_lite: 1, last_direct: true, light_pick: false }); + assert_eq!(android.kernel, KernelSwitches { strict: true, cutout_lite: 1, last_direct: true, last_sun: false, light_pick: false }); + // `sun`: the directional lights alone at the last vertex, in place of all of them, on either platform. + let sun = GpuOptions::from_vars(&vars(&[("THREERT_LAST_DIRECT", "sun")]), true); + assert_eq!((sun.kernel.last_direct, sun.kernel.last_sun), (false, true)); assert_eq!((android.uniform_tables, android.pipeline_cache.clone()), (None, None)); assert!(android.uniform_tables_on(false, true) && !android.uniform_tables_on(true, true)); // Set: what is asked, on either platform. @@ -3479,7 +3491,7 @@ mod tests { assert_eq!((off.kernel, off.uniform_tables_on(false, true)), (KernelSwitches::default(), false)); let on = GpuOptions::from_vars(&vars(&[("THREERT_UNIFORM_TABLES", "1"), ("THREERT_CUTOUT_LITE", "2"), ("THREERT_LIGHT_PICK", "1"), ("THREERT_PIPELINE_CACHE", "/data/cache/pipelines")]), false); assert!(on.uniform_tables_on(false, false) && !on.uniform_tables_on(true, false)); - assert_eq!(on.kernel, KernelSwitches { strict: false, cutout_lite: 2, last_direct: false, light_pick: true }); + assert_eq!(on.kernel, KernelSwitches { strict: false, cutout_lite: 2, last_direct: false, last_sun: false, light_pick: true }); assert_eq!(on.pipeline_cache.as_deref(), Some(std::path::Path::new("/data/cache/pipelines"))); assert_eq!(GpuOptions::from_vars(&vars(&[("THREERT_PIPELINE_CACHE", "")]), true).pipeline_cache, None); // (And the environment map's light sampling, which is the scene's preparation's: lib.rs `env_nee_on`.) @@ -3512,8 +3524,12 @@ mod tests { assert_eq!(strict.matches("ray_bad(o, d, tmax").count(), queries, "every ray query is guarded"); assert_eq!(strict.matches("fn ray_bad(").count(), hwrt as usize); assert!(!strict.contains("env_cdf[y - 1u]") && !strict.contains("env_cdf[base + x - 1u]") && plain.contains("env_cdf[y - 1u]")); - let lit = v(hwrt, KernelSwitches { strict: true, cutout_lite: 2, last_direct: true, light_pick: true }); + let lit = v(hwrt, KernelSwitches { strict: true, cutout_lite: 2, last_direct: true, last_sun: false, light_pick: true }); assert_eq!(lit.matches("radiance += dz;").count(), 1); + assert!(!lit.contains("fn sun_light("), "last_direct takes the directional lights already"); + let sun = v(hwrt, KernelSwitches { last_sun: true, ..Default::default() }); + assert_eq!((sun.matches("radiance += dz;").count(), sun.matches("fn sun_light(").count(), sun.matches("sun_light(s, lobes(").count()), (1, 1, 1)); + assert!(!plain.contains("sun_light")); assert_eq!(lit.matches("env_on = take_env && has_env;").count(), 1); assert_eq!(lit.contains("let steps = 4u;"), hwrt); assert_eq!(v(hwrt, KernelSwitches { cutout_lite: 2, ..Default::default() }) == plain, !hwrt);