Skip to content

DDGI: camera-centered multi-cascade clipmap with probe relocation and scalable ray budgeting - #1674

Draft
Anthony-Gaudino wants to merge 21 commits into
turanszkij:masterfrom
Anthony-Gaudino:perf/ddgi
Draft

DDGI: camera-centered multi-cascade clipmap with probe relocation and scalable ray budgeting#1674
Anthony-Gaudino wants to merge 21 commits into
turanszkij:masterfrom
Anthony-Gaudino:perf/ddgi

Conversation

@Anthony-Gaudino

Copy link
Copy Markdown
Contributor

Summary

Reworks DDGI from a single grid fitted to the scene AABB into a camera-centered, toroidally-scrolling, N-cascade clipmap. This decouples probe density (fine, near the camera) from coverage (extended outward by cascades), so GI works in both small scenes and large multi-km levels without the old failure modes: coarse probe density in big scenes, probes drifting through geometry when the scene bounds changed.

On top of the new grid it adds a probe validity/relocation system (so probes cope with enclosed and moving geometry), and a set of performance/scalability changes that keep 6 cascades affordable. The whole DDGI update chain already runs on the async compute queue.

1. Grid architecture

  • Camera-centered grid with toroidal scrolling. The probe grid follows the main camera; when the camera crosses a cell boundary the grid snaps by whole cells and the buffers are addressed toroidally (a clipmap), so only the newly-entered slab of probes is reset while the rest keep their accumulated light. Signed modular addressing keeps this correct for any grid dimensions and scroll sign.
  • N concentric cascades (default 6). Cascade 0 is the fine near-camera grid; each coarser cascade keeps the same probe count but doubles cell spacing and coverage. Probes live in one shared set of buffers (concatenated by probe_offset); sampling picks the finest cascade containing the point and blends into the next coarser one over the outer edge for a seamless density transition. Per-cascade teleport reset (each cascade has its own reset flag) avoids wiping other cascades' history.
  • Shader-interop refactor. ShaderInterop_DDGI.h is now a leaf data header; GetScene()-based helpers moved to a new ddgiHF.hlsli. Debug rendering uses a dedicated DDGI-only low-poly icosphere header instead of the shared one.

2. Probe validity & relocation

  • Buried-probe detection. Probes enclosed in solid geometry are detected from backface/escape ray ratios, temporally smoothed into a confidence value with a dead-band to stop the per-frame verdict blinking; invalid probes are down-weighted when sampled and hidden in the debug view.
  • Ejection. Relocation now pushes a probe out through the nearest face when it is behind a surface, instead of deeper in, so a probe a moving object sweeps over is pushed back into open space rather than getting stuck dark inside.
  • Neighbor seeding + squeeze damping. A buried probe seeds its color from the neighbor in its ejection direction (avoiding brighter edge probes), and a probe pinched between two near surfaces is damped so it stops flickering.
  • Over-bright transient mitigation. Just-ejected probes stay down-weighted until their own rays land, and probes bordering a just-ejected one fast-reconverge, gated on ejection events, which only occur where a solid is moving, so static scenes are unaffected.

3. Performance & scalability

  • Staggered cascade updates. Cascade 0 updates every frame; coarser cascades round-robin one per frame, so per-frame trace/update cost is ~constant regardless of cascade count.
  • Ray-memory decoupled from cascade count. Rays are compacted into transient buffers sized for the probes refreshed in a single frame, not for every probe of every cascade, making ray VRAM independent of DDGI_CASCADE_COUNT.
  • Ray-budget shaping. Variance-driven per-probe budget, with buried and open-sky probes capped to a small fixed count, and a per-cascade budget (coarser cascades get fewer rays) that trims trace cost and shrinks the ray buffers a further ~25%.
  • Per-active-cascade dispatch. Ray allocation / update / update-depth dispatch only the two active cascades instead of all probes with an early-out, cutting ~2/3 of the group launches at 6 cascades.
  • Single-gather sampling deep inside a cascade (only the boundary regions gather two cascades), and frustum + distance culling of debug probes.

4. Convergence quality

  • Blend-rate compensation for the staggered schedule: a cascade refreshed 1/K as often scales its temporal blend so it converges at the same wall-clock rate as cascade 0, fixing directional lag/darkening and a cascade-boundary pop while moving (color and depth tuned separately to avoid shimmer).

5. Cleanup

  • Stops serializing the DDGI probe/depth blobs, which the runtime always rebuilt on load, smaller scene files, with the stream layout kept compatible.

Known limitation

Fast-moving occluders still show a brief GI transient on their newly-revealed faces.


Only tested on Linux

Replace the scene-bounds-fitted DDGI grid with a
camera-centered grid that scrolls by whole cells
as the camera moves, addressing probe buffers
toroidally so only the newly-entered slab
reconverges. Fixes probes drifting through
geometry on camera movement and keeps a fixed
probe density near the viewer regardless of map
size.
Previously, the raytrace computed
surface.IsBackface() for every ray but discarded
the information, leaving no way to detect when a
probe was stuck inside a mesh. Now, backface hits
store negative depth (magnitude = distance) while
frontface hits store positive depth; the sign is
free to mean front/back.

The depth update pass reads this signal: a probe
is "inside" when >25% of its rays hit backfaces (a
ray only sees a backface from behind the surface).
Inside probes:
  - Relocate toward the nearest backface (the
    closest exit) instead of pushing off
    frontfaces, clamped to ±half cell
  - Are marked !VALID (0.02 down-weighting in
    sampling) so their tainted radiance doesn't
    leak

Self-correcting: once relocation moves a probe
into open space, the backface ratio drops below
25% and it goes VALID again. Probes too deep to
escape (beyond the offset clamp) stay
invalid/ignored, eliminating leaky dark blotches
from buried probes.
Collapse buried/invalid probes to a point in
ddgi_debugVS so they are not rasterized.
Declutters the debug view and skips their
overdraw. Toggle via the DDGI_DEBUG_SHOW_INVALID
constant to inspect all probes.
Invalid (buried) probes are down-weighted to
near-zero when sampled, yet their noisy backface
shading inflates the variance-driven ray budget.
Cap them at a fixed 32-ray floor instead. Enough
to keep re-measuring the backface ratio and
relocating (so exposed probes recover to full
budget next frame), but far below the up-to-512
they could otherwise consume.
…nalysis

Buried probe detection to handle edge cases: probe
clipping planes, large boxes, and flickering at
boundaries.

Key changes:
  - Hysteretic enclosure: enter buried at >=87.5%
    backfaces, exit at <25% backfaces or >=25%
    escapes, hold previous state in-between. Stops
    boundary flickering (ray counts jitter
    frame-to-frame).
  - Escape-count analysis: a ray "escapes" only
    when it's a miss or hits a far frontface
    (!is_backface && hit_dist >= max_range). A far
    backface is NOT an escape (probe still behind
    it). This separates a large solid box (few
    escapes, far walls are backfaces) from a large
    open room (many escapes, far walls are
    frontfaces), even when a plane clips through
    both and confuses the backface ratio.
  - Removed unstable backface-exit relocation that
    steered buried probes toward their nearest
    exit. That oscillated badly at dead ends (e.g.
    box resting on ground: probe steered down out
    -> sandwiched -> flips valid -> steered back
    in -> repeat). Relocation now only uses
    surface_push (push away from any near
    surface), flag-independent and stable.
  - Debug viz hides probes by luminance in
    addition to the valid flag: a probe sealed in
    a solid receives no light -> converges to
    black -> stable and visible independent of
    geometry. Catches plane-clip-through interior
    probes whose backface ratio was ambiguous.
  - Ray budget floor (32 rays) for invalid probes
    keeps re-measuring the backface ratio and
    relocation so exposed probes recover to full
    budget.
Probes at enclosure boundaries jitter between
valid/invalid each frame because the per-frame
decision (~32 re-randomized rays) is noisy. The
decision would cross thresholds unpredictably when
a probe sat right at the edge (e.g. inside a box
with a plane protruding, allowing variable
escapes).

Add temporal smoothing via an EMA of enclosure
confidence stored per probe:
  - DDGIProbe now holds enclosure_confidence [0,1]
    (repurposed the padding field, same size, no
    serialization change)
  - Per-frame verdict (enter_buried/clearly_open)
    feeds an EMA with 0.1 blend factor. Fresh
    probes classify immediately on their full ray
    budget.
  - VALID flag derived via dead-band: buried if
    conf>0.6, open if <0.4, else hold previous
    state. This prevents crossing back and forth
    at the midpoint.
  - Relaxed escape thresholds (boxed-in now <25%,
    open now >=40%) so a box with a grazing-escape
    plane still reads buried while open water
    stays valid.

Result: a boundary probe settles to a stable
confidence and state rather than blinking. EMA
rate (0.1) and dead-band (0.4/0.6) are tunable.
Extend the camera-centered DDGI grid to multiple
concentric cascades (currently DDGI_CASCADE_COUNT
= 2). Cascade 0 is the fine near-camera grid; each
higher cascade keeps the same probe dimensions but
doubles cell spacing and coverage, so probe
density is high near the camera and falls off with
distance while total covered area grows
exponentially with cascade count for only linear
probe cost.

Design: unified probe index space. All cascades
share the probe/variance/ray/ raycount/rayalloc
buffers (concatenated by probe_offset) and one
depth atlas (cascades stacked vertically at
depth_atlas_offset). Every compute pass and the
debug draw dispatch over total_probe_count; each
thread decodes its cascade from the global probe
index via ddgi_decode_probe(). All shader helpers
are now parameterized by cascade index.

Sampling: ddgi_sample_irradiance picks the finest
cascade containing the point and blends into the
next coarser one over the outer 15% of its volume,
so the density transition is seamless. Sampling
touches at most two cascades regardless of cascade
count, so shading cost is independent of
CASCADE_COUNT.

Per-cascade teleport reset: frame_index is shared
across cascades, so a mid-run teleport of one
cascade sets a per-cascade `reset` flag instead;
ddgi_scrollCS marks every probe in a reset cascade
FRESH (otherwise only the scrolled-in slab). The
scroll pass runs when any cascade scrolled or was
reset.

- ShaderInterop_Renderer.h: DDGI holds Cascade
  cascades[DDGI_CASCADE_COUNT] plus shared fields
  (probe_buffer, depth_texture, total_probe_count,
  ...)
- ShaderInterop_DDGI.h: cascade-parameterized
  helpers, ddgi_decode_probe, ddgi_gather_sh
  (out-param), cascade-blended
  ddgi_sample_irradiance
- wiScene.{h,cpp}: DDGI::Cascade +
  Compute_Cascade_Parameters /
  Get_Total_Probe_Count; total-probe buffer
  allocation; stacked depth atlas; per-cascade
  grid snapping; per-cascade shader upload
- wiScene_Serializers.cpp: serialize cascade 0
  config; size-guard probe/depth blobs against the
  new layout
- ddgi_scrollCS/rayallocationCS/raytraceCS/updateCS/debugVS:
  decode cascade from global probe index
- wiRenderer.cpp: dispatch over total_probe_count;
  scroll when any cascade scrolled/reset;
  debug-draw all cascades

Note: two DXC (spirv) crash workarounds in the
sampler - an ascending unrolled cascade-selection
loop (a descending one crashes), and an
unconditional second cascade gather (a
[branch]-guarded call with out params crashes).
The unused coarse gather is zeroed via its blend
weight.
Move the DDGI constant-buffer struct out of
ShaderInterop_Renderer.h into its own file,
matching how ShaderTerrain / ShaderVoxelGrid are
organized, and follow the engine's
ShaderInterop_*.h (shared data) + *HF.hlsli (GPU
helper functions) convention.

- ShaderInterop_DDGI.h becomes a leaf data header:
  constants, DDGI_CASCADE_COUNT, ShaderDDGI (+
  Cascade), push constants and packed ray/variance
  records. It no longer includes
  ShaderInterop_Renderer.h or references
  GetScene().
- ShaderInterop_Renderer.h includes the data
  header and holds `ShaderDDGI ddgi;` instead of a
  large inline struct.
- New ddgiHF.hlsli holds every GetScene()-based
  helper (probe addressing,
  ddgi_sample_irradiance, MultiscaleMeanEstimator,
  depth-border table).
- DDGIProbe and its flags stay in
  ShaderInterop_Renderer.h: they depend on the SH
  namespace defined there, so moving them would
  create a circular include.
- Shaders that call the helpers include
  ddgiHF.hlsli; objectHF and
  ddgi_indirectprepareCS use only the data structs
  and keep ShaderInterop_DDGI.h.
Cull DDGI debug probe spheres that can't be seen,
reducing debug-view overdraw (which grows with
cascade count). Frustum culling (on by default)
collapses a probe's sphere to a point when it lies
entirely outside the camera frustum; an optional
DDGI_DEBUG_MAX_DISTANCE hides probes beyond a
world-space distance.

Both reuse the existing radius=0 hide mechanism
and are visualization-only - they do not affect
the GI.
DDGI debug probes were drawn as a 2880-vertex
uvsphere each; that is a huge amount of debug
geometry that tanks FPS when the overlay is on.

Add ddgi_debug_icosphere.hlsli - a 60-vertex
(20-face icosahedron) unit sphere, winding matched
to uvsphere/icosphere so it front-faces under
RSTYPE_FRONT. The DDGI debug VS uses it and the
draw is DrawInstanced(60, ...), a 48x reduction.

Kept in its own DDGI-scoped header (array
DDGI_DEBUG_ICOSPHERE) rather than the shared
icosphere.hlsli (240 verts, used by the sky and
volumetric-light shaders) so the probe gizmo can
be tuned without touching those. Visualization
only; does not affect GI.
Bump DDGI_CASCADE_COUNT to 6 (coverage now ~±2km
at the coarsest level) and add a staggered update
schedule so the extra cascades stay affordable.

Each cascade gets an `active` flag: cascade 0
refreshes every frame; cascades 1..N-1 round-robin
one per frame (c == 1 + frame_index % (N-1));
frame 0 refreshes all so the field converges at
once. Per-frame trace/update cost is therefore
~constant (cascade 0 + one coarse) regardless of
cascade count, and spike-free (unlike a 2^c
schedule which periodically aligns all cascades on
one frame).

An inactive cascade is frozen: the CPU snapping
loop skips it (grid does not scroll),
ddgi_rayallocationCS allocates it 0 rays, and
ddgi_updateCS (color and depth) early-returns - so
its probes simply hold their last result.
Raytracing is indirect off the ray allocation, so
it skips inactive cascades automatically.

Note: buffers are still sized for all probes at
DDGI_MAX_RAYCOUNT, so 6 cascades at 32x8x32 use
~600MB VRAM (ray buffer ~384MB). Staggering
reduces per-frame compute, not allocation.
…ount

The ray and ray-allocation buffers were sized for
every probe of every cascade at DDGI_MAX_RAYCOUNT
(~480MB at 6 cascades of 32x8x32), even though
they are transient (rays are traced and integrated
within one frame) and the staggered schedule only
refreshes cascade 0 + one coarse cascade per
frame.

Size them for the per-frame refresh worst case
instead (Get_Ray_Buffer_Capacity = min(2,
CASCADE_COUNT) x probes_per_cascade x
DDGI_MAX_RAYCOUNT) and compact rays contiguously:
the allocation pass reserves each probe's range
atomically, clamps to capacity (bucket alignment
preserved; overflow is impossible by schedule
design, the clamp is a safety net), and records
the base offset in a new per-probe raybase_buffer.
The raytrace writes each ray to its compacted slot
(the allocation record index), and the update
passes read rays at raybase + i. Indirect prepare
clamps the dispatch the same way.

Ray memory at 6 cascades: 384+96MB -> 128+32MB
(~320MB saved, DDGI total ~595MB -> ~275MB), and
it is now independent of DDGI_CASCADE_COUNT.

Frame 0 consequence: all cascades can no longer
refresh at once (they would not fit), so frame 0
refreshes the normal schedule while still snapping
every cascade's grid. A new
DDGIPROBE_FLAG_INITIALIZED (set after a probe's
first depth-pass update; absent in the zeroed
creation state) makes never-refreshed probes take
the full fresh path on their first round-robin
activation, so each coarse cascade converges in
one cycle (~CASCADE_COUNT-1 frames) after
creation; distant GI is briefly dark during
initial convergence.
ddgi_sample_irradiance always gathered two
cascades (fine + coarse) because a conditional
gather crashed DXC, so it paid ~2x the probe-cage
cost across the whole screen even where the coarse
cascade contributes nothing (blend == 0).

Gate the coarse gather via a data-dependent loop
bound instead of a branch: ddgi_gather_sh takes a
`bool enabled` and runs its 8-probe cage loop for
(enabled ? 8 : 0) iterations, so a disabled gather
does no probe-buffer reads or depth-atlas samples.
The out params are still written unconditionally
at the end (Zero) - the crash is triggered by ANY
conditional control flow tied to the SH out param
(conditional call, early return, or by-value
return called twice), but a varying loop trip
count compiles. The sampler enables the fine
gather always and the coarse gather only near a
cascade's outer edge (blend > 0 and a distinct
cascade), so a wave deep inside a cascade now pays
for a single gather.

Visualization/behaviour unchanged once converged;
~2x cheaper GI sampling over the interior of each
cascade, still blending across cascade boundaries.
Coarse cascades refresh once every CASCADE_COUNT-1
frames (staggered updates) but used the same
per-update temporal blend as cascade 0, so they
converged that many times slower in wall-clock
time. This showed as lighting that lags/darkens
while moving and a jump at cascade boundaries
(worst at cascade 2<->3).

Add a per-cascade blend_scale = update period (1
for cascade 0, CASCADE_COUNT-1 for the round-robin
cascades), stored in ShaderDDGI::Cascade and
uploaded with the other cascade parameters. The
update pass scales its temporal blend factors by
pow(period, strength), with separate strengths for
colour and depth (shader constants in
ddgi_updateCS.hlsl, tunable with a shader-only
recompile):
  - DDGI_BLEND_COMPENSATION_COLOR (0.5): kept
    gentle - pushing it harder lets more
    per-update radiance noise through on the
    coarse cascades = shimmer/flicker.
  - DDGI_BLEND_COMPENSATION_DEPTH (1.0): full -
    depth/visibility moments are smooth so faster
    convergence does not shimmer, and it speeds
the Chebyshev visibility test's recovery when
surfaces move. Fresh probes still bypass blending,
so only steady-state convergence is affected.

Fixes the cascade-boundary jump and the shimmer;
reduces (but does not eliminate) the transient
darkening on moving surfaces - the residual is a
separate probe relocation/validity issue.
Probe relocation pushed the probe away from every
nearby surface, front or back. For a probe sitting
just inside a face - e.g. the one a moving box
leaves behind on its trailing side - the nearest
surface is that face, hit as a backface, so "push
away" drove the probe deeper into the solid. It
stayed enclosed, was down-weighted to ~0 when
sampled, and the trailing face went black. The
leading face never showed this because an external
probe ahead of it is pinned against it by the same
push.

Make the push direction depend on which side of
the surface the probe is on: keep pushing away
from front faces (hold keep-distance on the
correct side), but push toward back faces so the
probe exits through the nearest face. Once it
pokes through it reads as a front face and the
away-push stabilizes it at keep-distance outside.
Deep inside a solid the opposite backfaces cancel,
so genuinely buried probes are not yanked toward
an arbitrary face, and the offset is still clamped
to half a cell.
…eighbors

Two relocation follow-ups to the backface-ejection
fix, both in the probe update push field, plus the
flag they share.

Squeeze flicker: a probe pinched between two
nearby FRONT faces (a thin gap) gets opposing
pushes that cancel to a small, noisy net vector,
so the re-randomized rays swing its sign each
frame and the probe oscillates between the faces.
Track the summed push magnitude alongside the
vector push and scale the push by its coherence
(net length / summed magnitude): ~1 when a single
surface dominates (ejection and keep-distance
unaffected), ~0 when surfaces oppose, so a pinched
probe settles at the balance point instead of
chasing an unreachable keep-distance from both.
This also damps deep-buried jitter (opposite
backfaces cancel).

Black patch on ejection: a buried probe has no
valid colour - integrating its backface-only rays
drives it to black. When such a probe was ejected
into open space its whole cage could be black and
the revealed surface showed a black patch until it
slowly reconverged. Give buried probes a real
colour source instead:
- While invalid, a probe copies the radiance of
  its nearest valid neighbour (6 straight then 12
  diagonal), so the region a solid occupies shows
  surrounding GI rather than black, both while
  buried and during ejection. Deep-interior probes
  with no valid neighbour are left unchanged (not
  meaningfully sampled).
- On the invalid->valid transition the depth pass
  sets a one-shot DDGIPROBE_FLAG_COLOR_RESET
  (distinct from FRESH so it does NOT reset the
  offset and undo the ejection). The ray
  allocation pass then hands the probe the full
  ray budget and the colour pass takes that result
  directly as its mean, so it converges from its
  new open position in one step instead of
  trickling up from black.
The ray allocation, update, and update-depth
passes dispatched one group per probe over every
cascade (total_probe_count, ~49k at 6 cascades)
and had inactive cascades early-out. With
staggered updates only two cascades run per frame,
so ~2/3 of those group launches did nothing but
return - millions of update-depth threads (256 per
group) launched to immediately exit.

Dispatch only the active cascades instead.
wiRenderer loops the cascades and, for each active
one, dispatches its probe_count groups starting at
its probe_offset, passed via a new
DDGIPushConstants::probeIndexOffset; the shaders
compute probeIndex = SV_GroupID.x +
probeIndexOffset and the active early-outs are
removed. This scales with cascade count - adding
cascades no longer adds launches to these passes.

The compacted-ray InterlockedAdd counter is
cleared once per frame and accumulates across the
two active-cascade dispatches (disjoint,
capacity-clamped ranges), so no barrier between
them is needed. The per-probe raycount/raybase
buffers were previously zeroed for inactive probes
by the removed early-out, so they are now
host-cleared every frame before ray allocation.
Raytrace is unchanged (indirect, reads the probe
index from the allocation records); the scroll
pass and debug draw still cover all probes.
The DDGI probe buffer and depth atlas were
serialized as raw GPU dumps, but the runtime
rebuilds them zero-initialized whenever a
serialized scene loads (the transient ray_buffer
is never serialized, so Scene::Update recreates
every DDGI resource and resets frame_index). The
blobs were always discarded on load yet bloated
every scene file.

Read now consumes the blob vectors for stream
compatibility and discards them instead of
creating GPU resources; write emits empty vectors
instead of dumping the buffers. The scalar fields
are unchanged, so the stream layout stays stable
and old/new scene files interoperate both ways.
A probe with open space in every direction (no
geometry within max_distance) sees only the smooth
sky and lights no surface within range, yet
per-frame sun/sky sampling noise could inflate its
variance-driven ray budget - the same failure mode
the buried-probe cap already handles. The depth
pass flags such probes (DDGIPROBE_FLAG_OPEN, set
when every ray escapes) and ray allocation caps
them to 32 rays (min, so a quiet converged sky
keeps its lower variance-driven value). Visually
free: a fully-open probe is never in a shaded
point's interpolation cage.
Follow-ups to the neighbor-seed black-face fix,
all targeting the transient brightening on a
moving object's newly-revealed faces. They reduce
(do not eliminate) it - the residual is inherent
to DDGI's world-static probe grid vs. moving
geometry.

- Seed direction: a buried probe now seeds its
  color from the neighbor in its ejection
  (relocation-offset) direction - the probe
  directly out from the face it is escaping - and
  skips diagonal (edge/corner) neighbors, instead
  of taking the first valid neighbor, which near
  an object's edge grabbed a much brighter probe
  sitting past the edge in open space.

- Trust gate: a probe that was just ejected
  (COLOR_RESET pending) is still carrying its
  provisional borrowed color, so it stays
  down-weighted like a buried probe when sampled
  until its own rays land, instead of being
  trusted at full weight.

- Ejected-neighbor propagation: a new one-frame
  DDGIPROBE_FLAG_EJECTED marks a probe that just
  transitioned buried->valid, and a valid probe
  bordering such a marker also takes the fast
  color-reset path - the exterior probes lighting
  the object's newly-covered faces would otherwise
  lag through the noise-damped estimator. Gated on
  ejection events, which only occur where a solid
  is moving, so static geometry is never affected.
Coarser cascades cover distant, low-frequency
areas and refresh only once every CASCADE_COUNT-1
frames, yet each probe could still allocate the
full DDGI_MAX_RAYCOUNT on a fresh/scroll frame,
and the transient ray buffers were sized for two
cascades at that full budget.

Add a shared ddgi_cascade_ray_budget(cascade) =
max(DDGI_MAX_RAYCOUNT >> cascade,
DDGI_MIN_RAYCOUNT): cascade 0 keeps the full 512,
each coarser cascade halves down to a 64-ray
floor. The ray allocation pass caps the
variance-driven and fresh-probe budgets by it, and
Get_Ray_Buffer_Capacity sizes for the heaviest
single-frame pairing (cascade 0 + cascade 1)
instead of two full-budget cascades - shrinking
the ray and allocation buffers ~25% and trimming
raytrace cost on scroll/teleport frames. Cascade 0
(what the viewer sees most) is unchanged, and
coarse cascades are distant and blur-blended, so
quality is unaffected. Every cap is a power of two
>= the floor, so all budgets, the capacity, and
every compacted ray base stay
DDGI_RAY_BUCKET_COUNT-aligned.
@turanszkij

Copy link
Copy Markdown
Owner

Didn't spend a lot of time to check yet, but few things I noticed:

  • it is much slower than the old one
  • if you discard depth data on serialize then it will not be useable as a static DDGI because depth needs to be rebuilt at runtime

I'd like DDGI to remain a more light-weight solution for fallback effects and especially useable as completely baked. I think instead of cascades it should be supplemented with a good screen space probe GI. SSGI rewrite for probe-based one is planned, however there could be a version of that with screen-probes + raytrace.

@Anthony-Gaudino

Copy link
Copy Markdown
Contributor Author

It's not slower on my system. Reducing the number of cascades would probably help, 6 cascades create a very large number of probes, enough to cover massive worlds. Using 3 or 4 cascades instead would likely provide a significant performance improvement.

You're also right that baking wouldn't work with the current cascade implementation, since the probe density is tied to the camera position. My goal was to move toward something similar to [UE 5.8](https://www.youtube.com/watch?v=pJhWyxVBMX8): a fully dynamic solution. However, my implementation isn't complete yet, and I think there's still plenty of room for improvement. From what I can tell, Unreal uses clipmaps rather than cascades, and it only places or activates probes around nearby geometry, which greatly improves performance.

If the goal is baked DDGI, I think a better approach would be to let users define DDGI volumes and configure the DDGI settings independently for each volume. That would allow, for example, placing high-density volumes in interior rooms while using different settings elsewhere. The current approach of fitting a single DDGI volume to the entire scene AABB has a few drawbacks:

  1. The maximum probe count is specified as X, Y, and Z dimensions in the UI. For large scenes, this often results in probe densities that are either too low to provide good lighting or unnecessarily high.
  2. With procedural terrain, if automatic tile generation is enabled, moving through the world continuously expands the scene AABB as new tiles are created. This shifts the entire probe grid, causing all probes to move, which is undesirable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants