render3d: the shadow array holds only the cascades a fog wall needs, and a thick fog takes the whole sky

A cascade that begins past the wall was fitted and cleared every frame for nothing: the array is now
made with the ones that begin inside it (2 at 30 m, 3 at 175 m, 5 without a fog) and made again when
the wall changes that count; shadow maps 80 -> 32 MB at 30 m, 48 MB at 175 m. A layer the array lacks
clamps to its last, read only past the wall where everything is fogged.

The sky: at walls of 110 m and nearer the whole sky is the fog's colour, overhead too, with a soft
glow at the sun or moon and no stars or clouds through it, easing out by 175 m; from 175 m on the
horizon band alone, and off as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 16:10:58 +03:00
parent 7024f42789
commit 4d485a1778
6 changed files with 40 additions and 3 deletions

View file

@ -199,6 +199,7 @@ export state Render3dState {
gvk_al_tag: []int = null # per allocation id: owner * 4 + image * 2 + host (vkmem_report.ludic) gvk_al_tag: []int = null # per allocation id: owner * 4 + image * 2 + host (vkmem_report.ludic)
gvk_tag: int = 0 # the owner allocations are made for now (VKM_*) gvk_tag: int = 0 # the owner allocations are made for now (VKM_*)
vkmem_at: int = 0 # R3D_VKMEM=<frame>: the report is printed at that frame vkmem_at: int = 0 # R3D_VKMEM=<frame>: the report is printed at that frame
sh_layers: int = 5 # the shadow array's layers: the cascades the fog wall needs (shadow_fog)
fog_imp_released: int = 0 # impostor atlases let go under a fog wall (fog_impostors.ludic) fog_imp_released: int = 0 # impostor atlases let go under a fog wall (fog_impostors.ludic)
fog_imp_rebaked: int = 0 fog_imp_rebaked: int = 0
fog_imp_rebake_us: int = 0 fog_imp_rebake_us: int = 0

View file

@ -431,7 +431,7 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
let span = (size + align - 1) / align * align let span = (size + align - 1) / align * align
# an impostor atlas has memory of its own: a fog wall lets it go (fog_impostors.ludic), and inside a # an impostor atlas has memory of its own: a fog wall lets it go (fog_impostors.ludic), and inside a
# shared block its room went back to nobody while the block's other tenants stayed # shared block its room went back to nobody while the block's other tenants stayed
if size <= gvk_own_over(render3d_st) and render3d_st.gvk_tag != VKM_IMPOSTOR { if size <= gvk_own_over(render3d_st) and render3d_st.gvk_tag != VKM_IMPOSTOR and render3d_st.gvk_tag != VKM_SHADOW {
var kind = t * 2 var kind = t * 2
if image { kind += 1 } if image { kind += 1 }
var i = 0 var i = 0

View file

@ -60,6 +60,7 @@ export function r3d_fog_wall(render3d_st: mut Render3dState, dist: float) -> voi
render3d_st.sc_view_gen += 1 render3d_st.sc_view_gen += 1
cam_update(render3d_st) cam_update(render3d_st)
fog_impostors(render3d_st) fog_impostors(render3d_st)
shadow_fog(render3d_st)
} }
# how far anything is drawn: the fog wall and a margin, or `far` (0: no limit of its own) without one # how far anything is drawn: the fog wall and a margin, or `far` (0: no limit of its own) without one
function r3d_reach(render3d_st: Render3dState, far: float) -> float { function r3d_reach(render3d_st: Render3dState, far: float) -> float {

View file

@ -117,6 +117,15 @@ void main() {
if (u_fog_wall > 0.0) { if (u_fog_wall > 0.0) {
float band = clamp(90.0 / u_fog_wall, 0.06, 0.75); float band = clamp(90.0 / u_fog_wall, 0.06, 0.75);
col = mix(col, fogWallCol(dir), 1.0 - smoothstep(-0.02, band, dir.y)); col = mix(col, fogWallCol(dir), 1.0 - smoothstep(-0.02, band, dir.y));
// a thick fog (110 m and nearer, easing out by 175 m) takes the whole sky, overhead too: no
// stars and no clouds through it, only a soft glow where the sun (in fogWallCol) or the moon is
float cover = 1.0 - smoothstep(110.0, 175.0, u_fog_wall);
if (cover > 0.0) {
vec3 thick = fogWallCol(dir);
float mg = pow(max(dot(dir, u_moon_dir), 0.0), 12.0) * u_moon_illum * night * step(-0.08, u_moon_dir.y);
thick += vec3(0.55, 0.6, 0.7) * 0.05 * mg;
col = mix(col, thick, cover);
}
} }
o_color = vec4(sane(col), 1.0); o_color = vec4(sane(col), 1.0);
} }

View file

@ -31,6 +31,7 @@ function shadow_init__t(render3d_st: mut Render3dState) -> void {
render3d_st.sh_split[0] = 16.0; render3d_st.sh_split[1] = 60.0; render3d_st.sh_split[2] = 250.0; render3d_st.sh_split[3] = 1100.0; render3d_st.sh_split[4] = 6000.0 render3d_st.sh_split[0] = 16.0; render3d_st.sh_split[1] = 60.0; render3d_st.sh_split[2] = 250.0; render3d_st.sh_split[3] = 1100.0; render3d_st.sh_split[4] = 6000.0
render3d_st.sh_tmp_proj = m4_new(); render3d_st.sh_tmp_vp = m4_new(); render3d_st.sh_tmp_inv = m4_new(); render3d_st.sh_tmp_view = m4_new() render3d_st.sh_tmp_proj = m4_new(); render3d_st.sh_tmp_vp = m4_new(); render3d_st.sh_tmp_inv = m4_new(); render3d_st.sh_tmp_view = m4_new()
render3d_st.sh_corner = floats(3) render3d_st.sh_corner = floats(3)
shadow_fog(render3d_st)
} }
# A shadow resolution setting: 1024, 2048 or 4096 per cascade. The depth layers are made again at # A shadow resolution setting: 1024, 2048 or 4096 per cascade. The depth layers are made again at
@ -64,7 +65,8 @@ function shadow_set_res(render3d_st: mut Render3dState, r: int) -> void {
function shadow_make_tex(render3d_st: mut Render3dState) -> void { function shadow_make_tex(render3d_st: mut Render3dState) -> void {
render3d_st.sh_tex = gpu_tex_new(render3d_st) render3d_st.sh_tex = gpu_tex_new(render3d_st)
gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, render3d_st.sh_tex) gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, render3d_st.sh_tex)
gpu_tex_image3d(render3d_st, GL_DEPTH_COMPONENT32F, render3d_st.shadow_res, render3d_st.shadow_res, SHADOW_CASCADES, GL_DEPTH_COMPONENT, GL_FLOAT, null) render3d_st.sh_layers = shadow_layers_for(render3d_st)
gpu_tex_image3d(render3d_st, GL_DEPTH_COMPONENT32F, render3d_st.shadow_res, render3d_st.shadow_res, render3d_st.sh_layers, GL_DEPTH_COMPONENT, GL_FLOAT, null)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR) gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR) gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER)
@ -159,6 +161,8 @@ function shadow_pass(render3d_st: mut Render3dState) -> void {
prof_cpu_mark(render3d_st, "shadow fit") prof_cpu_mark(render3d_st, "shadow fit")
let past = wall > 0.0 and near >= wall let past = wall > 0.0 and near >= wall
near = render3d_st.sh_split[c] near = render3d_st.sh_split[c]
# a cascade the fog's shadow array has no layer for: fitted (its uniforms stay sane), never drawn
if c >= render3d_st.sh_layers { continue }
gpu_fb_depth_layer(render3d_st, render3d_st.sh_tex, c) gpu_fb_depth_layer(render3d_st, render3d_st.sh_tex, c)
if render3d_st.r3d_debug and c == 0 { shadow_debug_fbo(render3d_st) } if render3d_st.r3d_debug and c == 0 { shadow_debug_fbo(render3d_st) }
gpu_clear(render3d_st, GL_DEPTH_BUFFER_BIT) gpu_clear(render3d_st, GL_DEPTH_BUFFER_BIT)
@ -179,7 +183,7 @@ function shadow_pass(render3d_st: mut Render3dState) -> void {
@alloc_ok("asked for by the player or a tool, not by the frame") @alloc_ok("asked for by the player or a tool, not by the frame")
function shadow_dump(render3d_st: mut Render3dState) -> void { function shadow_dump(render3d_st: mut Render3dState) -> void {
let n = render3d_st.shadow_res * render3d_st.shadow_res let n = render3d_st.shadow_res * render3d_st.shadow_res
let buf = floats(n * SHADOW_CASCADES) let buf = floats(n * render3d_st.sh_layers)
gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, render3d_st.sh_tex) gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, render3d_st.sh_tex)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_NONE) gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_NONE)
gpu_tex_read(render3d_st, GPU_TEX2D_ARRAY, GL_DEPTH_COMPONENT, GL_FLOAT, data_of(buf)) gpu_tex_read(render3d_st, GPU_TEX2D_ARRAY, GL_DEPTH_COMPONENT, GL_FLOAT, data_of(buf))
@ -300,3 +304,25 @@ function shadow_debug_range(render3d_st: mut Render3dState, prog: int) -> void {
# messages, each built in a function of its own so the path that says it holds no allocation # messages, each built in a function of its own so the path that says it holds no allocation
@alloc_ok("a message, built only when it is said: a failure, a warning or a debug switch") @alloc_ok("a message, built only when it is said: a failure, a warning or a debug switch")
function shadow_say_no_memory(r: int, was: int) -> void { print(`r3d: shadows: no memory for {r} x {r} cascades; keeping {was}`) } function shadow_say_no_memory(r: int, was: int) -> void { print(`r3d: shadows: no memory for {r} x {r} cascades; keeping {was}`) }
# ---- the fog wall -----------------------------------------------------------------------------
# A cascade that begins past the wall is never drawn, so the array holds only the ones that begin
# inside it (16 MB each at 2048): two at 30 m, three at 175 m, all five without a fog. A layer the
# array lacks clamps to its last, and what reads it lies past the wall, fully fogged.
function shadow_layers_for(render3d_st: Render3dState) -> int {
let wall = r3d_reach(render3d_st, 0.0)
if wall <= 0.0 or render3d_st.sh_split == null { return SHADOW_CASCADES }
var n = 1
while n < SHADOW_CASCADES and render3d_st.sh_split[n - 1] < wall { n += 1 }
return n
}
# the array made again when the wall changes how many cascades it needs
function shadow_fog(render3d_st: mut Render3dState) -> void {
if render3d_st.sh_tex == 0 or shadow_layers_for(render3d_st) == render3d_st.sh_layers { return }
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_SHADOW
gpu_tex_free(render3d_st, render3d_st.sh_tex)
shadow_make_tex(render3d_st)
render3d_st.gvk_tag = was
}