Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2107,6 +2107,25 @@ traced pixel that the fast mean's buffer had free:
locked lettering of a still sign comes back from a motion's blur by the blend alone (the 0.2 on Kabukichō's
signs); a lamp that comes on arrives over a quarter of a second.

**Cleanup: experiments and switches that outlived their measurement (2026-11-13, after it).** Removed, each off by
default, so nothing a scene shows changes:

- the temporal pass's two round-3 experiment switches (`THREERT_TEMPORAL_FLAGS` 1: the à-trous stages gated by
history length alone; 2: the specular filter on mirror-like surfaces too) and the flag word that carried them;
- the emitter-reservoir experiment (`THREERT_RESTIR`, ReSTIR DI at camera hits; measured above: k = 1 0.608 → 0.613,
motion +0.1 ΔE, +0.44 ms): its kernel code, the reservoir buffer (group 1, binding 4: the kernel binds 19 storage
buffers as written, the merged layout's `MergedLive` six arrays) and five vec4 of `Params`;
- `THREERT_NO_MV` (no per-object motion vectors), `THREERT_LT_DYNAMIC=0` (emitters added in place by power share;
`bench-voxel-light`'s third row goes with it), `THREERT_EMIT_SYNC`, and the overrides of three constants
(`THREERT_EMITTER_CULL`, `THREERT_EMITTER_CULL_VIEW`, `THREERT_LIGHT_LOD`).

Kept: the switches the benches and tests use (`THREERT_PROFILE`, `_PROFILE_BLAS`, `_FREEZE_TIME`, `_EXPOSURE_SNAP`,
`_NO_GRAIN`, `_MOTION_BLUR`, `_EDIT_REBUILD`, `_INSTANCING`), the diagnostics (`_EDIT_LOG`, `_VIEW_MODE`, `_TL_HIDE`,
`_CACHE`, `_CACHE_FLAGS`), and two that are configuration (`_GOVERN_CLOCK`; `_RASTER`, the primary-visibility path
kept for devices without hardware ray tracing). Check: test-render, every output a run repeats to the byte, is
byte-identical to the build before; with `THREERT_MERGED=1` one volume test differs at a relative 2e-6 (the kernel
is compiled from a shorter source).

Not done here: filters sized by hit distance, which is what would let a short history stand without noise; the
Playground and the page still open a 1280 × 720 view; the live buffers are sized by the output (about 1.7 GB at
2560 × 1440), though the traced grid rarely fills it.
Expand Down
9 changes: 2 additions & 7 deletions renderer/shaders/live.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct LiveParams {
size: vec4<u32>, // width, height, frame, reset history (1)
misc: vec4<f32>, // exposure scale, max history, vignette intensity, bloom on (1) / off (0)
misc2: vec4<f32>, // denoise strength, sample clamp (display units at the current exposure), -, samples this frame
mode: vec4<u32>, // view mode (0 lit, 1 base color, 2 world normal, 3 lit no denoise, 4 raw sample, 5 history), -, the scene's lighting is changing (live.rs `live_lighting`), experiment flags
mode: vec4<u32>, // view mode (0 lit, 1 base color, 2 world normal, 3 lit no denoise, 4 raw sample, 5 history), -, the scene's lighting is changing (live.rs `live_lighting`), -
post: vec4<f32>, // chromatic aberration (UE scene fringe), film grain, bloom threshold (<= 0 off), -
ae: vec4<f32>, // auto exposure: low / high fraction, histogram log2 min / max
ae2: vec4<f32>, // compensation (2^bias; manual: the exposure), min / max average luminance, mode (1 = auto)
Expand Down Expand Up @@ -124,10 +124,6 @@ fn ycocg_rgb(y: vec3<f32>) -> vec3<f32> {
return vec3<f32>(y.x + y.y - y.z, y.x + y.z, y.x - y.y - y.z);
}

// Experiment switches (THREERT_TEMPORAL_FLAGS): 1 = a-trous gated by history length (round 3), 2 = specular spatial
// filter on mirror-like surfaces too (round 3).
fn xflag(b: u32) -> bool { return (L.mode.w & b) != 0u; }

fn lum(c: vec3<f32>) -> f32 {
return dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
}
Expand Down Expand Up @@ -592,7 +588,6 @@ fn atrous_gated(gid: vec3<u32>, stage: u32, step: i32, max_noise: f32) {
// every noisy night pixel, +1.9 ms on Kabukicho at 60 %).
let limits = array<f32, 5>(256.0, 128.0, 32.0, 8.0, 8.0);
var noisy = (count < 4.0 || sqrt(max(c.w, 0.0)) > max_noise * max(li, 1e-4)) && (stage < 2u || count < limits[stage]);
if (xflag(1u)) { noisy = count < limits[stage]; }
if (strength < 0.02 || !noisy || anim) { write_stage(stage, i, c); return; }
let g = traced_surface(i);
// Noise of the accumulated mean: temporal deviation / sqrt(frames). Fresh history has no reliable moments: assume
Expand Down Expand Up @@ -812,7 +807,7 @@ fn spec_filter_traced(gid: vec3<u32>, step: i32, from_a: bool) {
let g = gb_unpack(gb_out[i]);
let rough = g.rough;
let fresh = c.w < 4.0;
let reach = select(4.0 * clamp(rough * 8.0, 0.0, 1.0), 4.0, xflag(2u) || fresh);
let reach = select(4.0 * clamp(rough * 8.0, 0.0, 1.0), 4.0, fresh);
let npow = select(64.0, 8.0, fresh);
let strength = (0.25 + smoothstep(0.08, 0.5, rough)) / sqrt(max(c.w, 1.0)) * 4.0;
var outv = c.rgb;
Expand Down
182 changes: 3 additions & 179 deletions renderer/shaders/pathtrace.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const F_FOG: u32 = 64u;
const F_VOLUME: u32 = 128u;
const F_TEX: u32 = 256u;
const F_FUNC: u32 = 512u;
const F_RESTIR: u32 = 1024u;
const F_GLASS: u32 = 2048u;
const F_COAT: u32 = 4096u;
const F_HUGE: u32 = 8192u;
Expand Down Expand Up @@ -73,11 +72,6 @@ struct Params {
raster: vec4<f32>, // live: primary visibility from the raster pass (1 = on), this frame's sub-pixel jitter x, y (every camera ray's); ray tracing off (1)
cache: vec4<f32>, // live radiance cache (1 = on), history cap in samples, history cap in frames, -
motion: vec4<f32>, // live motion vectors: 1 = on, -, -, - (first dynamic triangle: group_first[1].x)
prev_pos: vec4<f32>, // live: the previous frame's camera (emitter reservoir reprojection), as cam_*
prev_fwd: vec4<f32>,
prev_right: vec4<f32>,
prev_up: vec4<f32>,
restir: vec4<f32>, // live emitter reservoirs: 0 off, 1 on without history, 2 on; previous traced width, height; this frame's half (0 / 1)
group_first: array<vec4<u32>, 2>, // first global triangle of each traversal group ([1].x = the first dynamic one)
light_lod: vec4<f32>, // live light LOD (lib.rs emitter_lod): reference position, d0 (0 = off)
emit_power: vec4<f32>, // x: the emitter CDF's total without light LOD (= misc.y when it is off); y: emitter cull radius (0 = off); z: its reach in the view cone
Expand Down Expand Up @@ -1856,9 +1850,7 @@ fn direct_light(s: Surface, lb: Lobes, v: vec3<f32>) -> vec3<f32> {
// Emissive triangles (incl. rect lights)
mis_x = o;
mis_n = s.n;
if (P.counts.y > 0u && restir_here()) {
sum += restir_emitters(s, lb, v, o);
} else if (P.counts.y > 0u) {
if (P.counts.y > 0u) {
var es: ES;
// With the light tree its candidates are already near the point: P.ltree.y of them at camera hits, one elsewhere.
if (lt_on()) { es = sample_emitter_ris(o, s.n, select(1u, u32(P.ltree.w), cur_depth == 0u), s, lb, v); } else if (seen_directly()) { es = sample_emitter_ris(o, s.n, select(EMIT_RIS_M / 2u, EMIT_RIS_M, cur_depth == 0u), s, lb, v); } else if (P.light_lod.w > 0.0) { es = sample_emitter_ris(o, s.n, 2u, s, lb, v); } else { es = sample_emitter(o, s.n); }
Expand Down Expand Up @@ -2472,27 +2464,7 @@ fn sphere_hit(x: vec3<f32>, l: vec3<f32>, hit: Hit) -> vec3<f32> {
}

// ------------------------------------------------------------------------------------------------
// Emitter reservoirs at live camera hits (ReSTIR DI, Bitterli et al. 2020; docs/ROADMAP.md § 1, step 1: direct light
// from the many emissive surfaces as clean as the exact light loop). Each pixel resamples RESTIR_M emitter points by
// their unshadowed contribution, then the previous frame's reservoir at its reprojected position and RESTIR_SPATIAL
// neighbours of it (same surface by normal and distance), and shades the winner with one shadow ray; occluded winners
// are stored with weight 0. The target is cheap (the emitter's factor and animation, not its texture); the winner gets
// the full emitted radiance, and its MIS weight against BSDF sampling uses the emitter CDF's pdf, as before. Reuse
// without visibility or pairwise MIS: slightly biased at shadow and surface boundaries. An experiment (THREERT_RESTIR=1):
// on Kabukicho it changed neither the k = 1 disocclusion frame (no history there) nor motion error (+0.1 ΔE), for
// +0.44 ms at 60 %; reuse among this frame's reservoirs needs a pass of its own.

const RESTIR_M: u32 = 4u;
const RESTIR_SPATIAL: u32 = 2u;
const RESTIR_RADIUS: f32 = 12.0; // traced pixels
const RESTIR_CAP: f32 = 80.0; // history, in candidates (20 x RESTIR_M)

// Two halves (this frame's, the previous one's) of 2 entries per traced pixel: (triangle, barycentrics unorm16x2,
// weight W, count M), (octahedral normal snorm16x2, distance to the camera, -, -).
@group(1) @binding(4) var<storage, read_write> resv: array<vec4<u32>>;

// The camera ray's pixel (traced grid) and whether this path vertex uses the reservoirs.
var<private> cur_pix: u32;
// What a live trace leaves the live passes (live.wgsl).

// Reflection resolve inputs (live; live.wgsl `refl`), 2 per traced pixel: the camera hit's specular-lobe ray
// (radiance it brought back, its lobe pdf; pdf 0 = the pixel sampled another lobe), (specular albedo E at the hit,
Expand All @@ -2515,160 +2487,13 @@ fn pdf_spec(s: Surface, lb: Lobes, v: vec3<f32>, l: vec3<f32>) -> f32 {
return g1(lb.a2, nov) * d_ggx(lb.a2, noh) / (4.0 * nov);
}

fn restir_here() -> bool {
return LIVE && has(F_RESTIR) && P.restir.x > 0.5 && cur_depth == 0u && P.misc.w < 0.5;
}

// A unit vector, octahedral (snorm16x2).
fn oct_enc(n: vec3<f32>) -> u32 {
let p = n.xy / (abs(n.x) + abs(n.y) + abs(n.z));
let q = select(p, (1.0 - abs(p.yx)) * select(vec2<f32>(-1.0), vec2<f32>(1.0), p >= vec2<f32>(0.0)), n.z < 0.0);
return pack2x16snorm(q);
}

fn oct_dec(u: u32) -> vec3<f32> {
let p = unpack2x16snorm(u);
var n = vec3<f32>(p, 1.0 - abs(p.x) - abs(p.y));
let t = max(-n.z, 0.0);
n.x += select(t, -t, n.x >= 0.0);
n.y += select(t, -t, n.y >= 0.0);
return normalize(n);
}

// An emitter point: triangle (0xffffffff: none), barycentrics, and its selection probability at the point that drew it
// (-1: unknown, a reused reservoir sample; emitter_eval asks the light tree then).
struct Cand { tri: u32, b: vec2<f32>, sel: f32 };

// One emitter point for x (normal n) by emitter_draw (the light tree or the CDF: area x emitted luminance).
fn emitter_point(x: vec3<f32>, n: vec3<f32>) -> Cand {
let pk = emitter_draw(x, n);
var b1 = rnd();
var b2 = rnd();
if (b1 + b2 > 1.0) { b1 = 1.0 - b1; b2 = 1.0 - b2; }
return Cand(select(0xffffffffu, bitcast<u32>(pk.e.x), pk.p > 0.0), vec2<f32>(b1, b2), pk.p);
}

// Emitter point c seen from x on surface s, unshadowed: target (luminance of factor x animation x BSDF x geometry x
// MIS weight), the CDF's area pdf, direction, distance, the BSDF's specular share; with `full`, the contribution with
// the emitted radiance (texture) in `f`.
struct EmitEval { f: vec3<f32>, ph: f32, pdf_a: f32, l: vec3<f32>, dist: f32, frac: vec3<f32> };

fn emitter_eval(s: Surface, lb: Lobes, v: vec3<f32>, x: vec3<f32>, c: Cand, full: bool) -> EmitEval {
var r: EmitEval;
if (c.tri == 0xffffffffu || c.tri >= arrayLength(&attrs)) { return r; }
let ti = c.tri;
let e1 = tri_verts[ti * 3u + 1u].xyz;
let e2 = tri_verts[ti * 3u + 2u].xyz;
let lp = tri_verts[ti * 3u].xyz + e1 * c.b.x + e2 * c.b.y;
let cr = cross(e1, e2);
let dl = lp - x;
let d2 = max(dot(dl, dl), 1e-8);
let dist = sqrt(d2);
let l = dl / dist;
let a = attrs[ti];
let mi = bitcast<u32>(a.uv.w);
let mat = materials[mi];
var cos_l = dot(normalize(cr), -l);
if (mat.tex1.z != MAT_EMITTER_CARD) { cos_l = abs(cos_l); }
if (cos_l <= 0.0) { return r; }
let f = eval_bsdf(s, lb, v, l);
if (all(f <= vec3<f32>(0.0))) { return r; }
r.frac = split_frac;
// Source pdf of the candidate (the CDF, light LOD included); MIS against the BSDF rays weighs by power alone (emit_mis).
var lum_p = luminance(mat.emissive.rgb) * obj_power(a);
// Particle emitters: nominal area (x.z) in the CDF, uniform over the actual one.
if (has(F_PEMIT) && a.x.z > 0.0) { lum_p *= a.x.z / max(0.5 * length(cr), 1e-12); }
var pdf_m = lum_p / P.emit_power.x;
if (lt_on()) {
// The light tree: its pmf at x per area, for the candidate's weight and for MIS (as emit_mis weighs).
var sel = c.sel;
if (sel < 0.0) { sel = emitter_sel(ti, x, s.n); }
r.pdf_a = sel / max(0.5 * length(cr), 1e-12);
pdf_m = r.pdf_a;
} else {
r.pdf_a = lum_p * emitter_lod(ti) / P.misc.y;
}
let m = power_heuristic(pdf_m * d2 / cos_l, pdf_bsdf(s, lb, v, l));
let g = cos_l / d2 * m;
r.ph = luminance(mat.emissive.rgb) * obj_power(a) * luminance(f) * g;
if (full) { r.f = emitter_radiance(ti, c.b.x, c.b.y, l) * f * g; }
r.l = l;
r.dist = dist;
return r;
}

fn restir_emitters(s: Surface, lb: Lobes, v: vec3<f32>, x: vec3<f32>) -> vec3<f32> {
var pick = Cand(0xffffffffu, vec2<f32>(0.0), -1.0);
var pick_ph = 0.0;
var wsum = 0.0;
var m = 0.0;
for (var k = 0u; k < RESTIR_M; k++) {
let c = emitter_point(x, s.n);
m += 1.0;
let ev = emitter_eval(s, lb, v, x, c, false);
if (ev.ph <= 0.0 || ev.pdf_a <= 0.0) { continue; }
let w = ev.ph / ev.pdf_a;
wsum += w;
if (rnd() * wsum < w) {
pick = c;
pick_ph = ev.ph;
}
}
let half = arrayLength(&resv) / 4u;
if (P.restir.x > 1.5) {
// Previous frame's traced-grid position of this surface point.
let dp = s.p - P.prev_pos.xyz;
let z = dot(dp, P.prev_fwd.xyz);
let dist = length(dp);
if (z > 1e-4) {
let pw = P.restir.y;
let ph = P.restir.z;
let c0 = vec2<f32>((dot(dp, P.prev_right.xyz) / (z * P.prev_pos.w * P.prev_fwd.w) * 0.5 + 0.5) * pw, (0.5 - dot(dp, P.prev_up.xyz) / (z * P.prev_pos.w) * 0.5) * ph);
let prev = (1u - u32(P.restir.w)) * half;
for (var j = 0u; j <= RESTIR_SPATIAL; j++) {
var q = c0;
if (j > 0u) {
let r = RESTIR_RADIUS * sqrt(rnd());
let a = 2.0 * PI * rnd();
q += r * vec2<f32>(cos(a), sin(a));
}
if (q.x < 0.0 || q.y < 0.0 || q.x >= pw || q.y >= ph) { continue; }
let qi = prev + u32(q.y) * u32(pw) + u32(q.x);
let e1 = resv[qi * 2u + 1u];
if (dot(oct_dec(e1.x), s.n) < 0.9 || abs(bitcast<f32>(e1.y) - dist) > 0.1 * dist) { continue; }
let e0 = resv[qi * 2u];
let mq = min(bitcast<f32>(e0.w), RESTIR_CAP);
let c = Cand(e0.x, unpack2x16unorm(e0.y), -1.0);
m += mq;
let ev = emitter_eval(s, lb, v, x, c, false);
let w = ev.ph * bitcast<f32>(e0.z) * mq;
if (!(w > 0.0)) { continue; }
wsum += w;
if (rnd() * wsum < w) {
pick = c;
pick_ph = ev.ph;
}
}
}
}
var wgt = 0.0;
var c = vec3<f32>(0.0);
if (pick_ph > 0.0) {
wgt = wsum / (m * pick_ph);
let ev = emitter_eval(s, lb, v, x, pick, true);
if (ev.ph > 0.0 && !occluded_any(x, ev.l, ev.dist * 0.999)) {
c = ev.f * wgt;
dl_spec += c * ev.frac;
} else {
wgt = 0.0;
}
}
if (non_finite(c)) { c = vec3<f32>(0.0); wgt = 0.0; }
let cur = u32(P.restir.w) * half + cur_pix;
resv[cur * 2u] = vec4<u32>(pick.tri, pack2x16unorm(pick.b), bitcast<u32>(wgt), bitcast<u32>(min(m, RESTIR_CAP)));
resv[cur * 2u + 1u] = vec4<u32>(oct_enc(s.n), bitcast<u32>(length(s.p - P.cam_pos.xyz)), 0u, 0u);
return c;
}

// ------------------------------------------------------------------------------------------------
// World-space radiance cache (live; docs/ROADMAP.md § 1, step 1). Spatially hashed voxels, sized by distance to the
// camera (CACHE_ANGLE, rounded down to a power of two), x 6 normal directions. Each holds the cosine-weighted incident
Expand Down Expand Up @@ -3007,7 +2832,6 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let h = P.size.y;
if (gid.x >= w || gid.y >= h) { return; }
let pix = gid.y * w + gid.x;
cur_pix = pix;
// Every pixel's deterministic direct light is written each frame (0 unless the camera hits a surface): a pixel that
// turned to sky must not keep what a surface left there.
if (LIVE && P.misc.w < 0.5 && arrayLength(&direct_out) >= w * h) { direct_out[pix] = vec4<f32>(0.0); }
Expand Down
Loading