From 00df8839e68109cf8a4a28cbce212b42a50ad48c Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 15 Sep 2026 15:03:06 +0300 Subject: [PATCH] perf(render3d): the sky light baked once at start, the terrain load in four steps, actor cull bounds by height - sky_start_yaw: a game that turns the sky at boot sets it before r3d_init and the image-based light is baked once at that yaw; sky_set_yaw to a yaw already baked bakes nothing. - terrain_init_step: the height field and normals, the sun shadow, the materials, the patches and programs as separate steps; r3d_load_count is 8, so a loading bar moves through the terrain (half the start-up) instead of jumping over it. terrain_init runs the four in a row as before. - The per-cascade actor cull centres its sphere at half the model's height and sizes it by the larger of height and radius: centred at the radius, it dropped a head at a cascade's edge (11 px in town, found by the drawstats comparison). Lake was byte-identical before and after. OpenGL frames at the five viewpoints unchanged; the game's 59 self-tests pass. Co-Authored-By: Claude Opus 5 --- changes/sky-start-yaw.md | 6 ++++++ packages/ludic.render3d/actor.ludic | 8 ++++++-- packages/ludic.render3d/render.ludic | 24 ++++++++++++++---------- packages/ludic.render3d/sky.ludic | 12 +++++++++++- packages/ludic.render3d/terrain.ludic | 18 +++++++++++++++--- 5 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 changes/sky-start-yaw.md diff --git a/changes/sky-start-yaw.md b/changes/sky-start-yaw.md new file mode 100644 index 00000000..ea0845b3 --- /dev/null +++ b/changes/sky-start-yaw.md @@ -0,0 +1,6 @@ +bump: patch +type: perf +**The sky's light is baked once at start, not twice.** A game that turns the sky at boot sets +`sky_start_yaw` before `r3d_init`, and `sky_load` bakes the image-based light at that yaw; turning to +a yaw the light is already baked at (`sky_set_yaw`) no longer bakes it again. Without it, nothing +changes. diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index 8a32e4e9..e2c7884b 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -332,8 +332,12 @@ function actor_draw_casters(light_vp: words) -> void { let a = ac_actors[i] if not ac_visible(a, true) { continue } if a.radius != 0 { - let r = f_mul(a.radius, a.scale) - if not ac_in_light(light_vp, a.x, f_add(a.y, r), a.z, f_mul(r, fl(1.5))) { continue } + # the radius is the horizontal extent: a person is far taller than wide, so the sphere is + # centred at half the model's height and reaches the larger of the two, with a margin for + # the pose (a first version centred it at the radius and dropped a head at a cascade's edge) + let hh = f_mul(f_mul(a.model.height, F_HALF), a.scale) + let r = f_max(f_mul(a.radius, a.scale), hh) + if not ac_in_light(light_vp, a.x, f_add(a.y, hh), a.z, f_mul(r, fl(1.5))) { continue } } var ap = ac_sh if a.cutout { ap = ac_sh_cut } diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index 737c1d32..93bcb342 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -93,24 +93,28 @@ function r3d_open(w: int, h: int, title: string) -> bool { } # The load, as steps a loading screen can show between: 0 the sky and its image-based light, -# 1 the terrain (height, normals, shadow, materials), 2 the shadow maps and the screen targets, -# 3 the scattered cover and the actors, 4 the grass and the sky pass. The order is r3d_init's. -function r3d_load_count() -> int { return 5 } +# 1 - 4 the terrain (the height field and normals, its sun shadow, its materials, its patches and +# programs), 5 the shadow maps and the screen targets, 6 the scattered cover and the actors, 7 the +# grass and the sky pass. The order is r3d_init's. +function r3d_load_count() -> int { return 4 + TERRAIN_INIT_STEPS } function r3d_load_step(i: int) -> bool { + let t = i - 1 if i == 0 { if not sky_load(r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr") { return false } daylight_init() - } else if i == 1 { - if r3d_dem_path != null { terrain_use_dem(r3d_dem_path, r3d_dem_min, r3d_dem_max, r3d_dem_base, r3d_dem_ox, r3d_dem_oz) } - if r3d_ortho_path != null { terrain_use_ortho(r3d_ortho_path) } - terrain_init() - } else if i == 2 { + } else if t >= 0 and t < TERRAIN_INIT_STEPS { + if t == 0 { + if r3d_dem_path != null { terrain_use_dem(r3d_dem_path, r3d_dem_min, r3d_dem_max, r3d_dem_base, r3d_dem_ox, r3d_dem_oz) } + if r3d_ortho_path != null { terrain_use_ortho(r3d_ortho_path) } + } + terrain_init_step(t) + } else if i == 1 + TERRAIN_INIT_STEPS { shadow_init() post_init(gl_w, gl_h) - } else if i == 3 { + } else if i == 2 + TERRAIN_INIT_STEPS { scatter_init() actor_init() - } else if i == 4 { + } else if i == 3 + TERRAIN_INIT_STEPS { grass_init() r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "") r3d_fog_density = fl(0.00014) diff --git a/packages/ludic.render3d/sky.ludic b/packages/ludic.render3d/sky.ludic index 10e336f7..e46e3f80 100644 --- a/packages/ludic.render3d/sky.ludic +++ b/packages/ludic.render3d/sky.ludic @@ -21,6 +21,12 @@ var sky_sun_boost: int = 0x40133333 # 2.3: the photograph's thin cloud dims it var sky_rot_s: int = 0 var sky_rot_c: int = 0 var sun_hdri: words = null # the sun direction as found in the file +# The yaw to bake the light at the first time (radians, float bits). A game that turns the sky at +# start sets it before r3d_init, so the image-based light is baked once at that yaw rather than at +# 0 and then again - which is what sky_set_yaw at boot used to cost. +var sky_start_yaw: int = 0 +var sky_baked: bool = false # the IBL set exists, baked at sky_baked_yaw +var sky_baked_yaw: int = 0 var sky_p_irr: int = 0 var sky_p_pre: int = 0 var sky_p_brdf: int = 0 @@ -31,6 +37,8 @@ function sky_set_yaw(yaw: int) -> void { # world sun = rotY(sun_hdri, -yaw): the lookup rotates a world direction by +yaw let s = sun_hdri v3_set(sun_dir, f_sub(f_mul(sky_rot_c, s[0]), f_mul(sky_rot_s, s[2])), s[1], f_add(f_mul(sky_rot_s, s[0]), f_mul(sky_rot_c, s[2]))) + # the light is already baked at this yaw: turning to it again bakes nothing + if sky_baked and yaw == sky_baked_yaw { return } sky_precompute() } # turn only the visible sky image (cheap, per frame): the light and the convolved @@ -64,7 +72,7 @@ function sky_load(path: string) -> bool { print(`sun irradiance: {f_fx(hdr_sun_r)} {f_fx(hdr_sun_g)} {f_fx(hdr_sun_b)} (Q16.16), clip {f_fx(hdr_clip)}`) print(`sky: {sky_w}x{sky_h}, sun at texel {hdr_max_x},{hdr_max_y}`) sky_fullscreen = mesh_fullscreen() - sky_precompute() + if sky_start_yaw != 0 { sky_set_yaw(sky_start_yaw) } else { sky_precompute() } return true } @@ -124,6 +132,8 @@ function sky_precompute() -> void { let p_brdf = sky_p_brdf sky_brdf = tex_target(256, 256, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR) sky_convolve(p_brdf, sky_brdf, -1, 256, 256, F_ZERO) + sky_baked = true + sky_baked_yaw = sky_yaw gpu_check("sky precompute") } diff --git a/packages/ludic.render3d/terrain.ludic b/packages/ludic.render3d/terrain.ludic index 5be84556..f5efdd15 100644 --- a/packages/ludic.render3d/terrain.ludic +++ b/packages/ludic.render3d/terrain.ludic @@ -387,9 +387,21 @@ function terrain_load_textures() -> void { } function terrain_init() -> void { - terrain_generate() - terrain_bake_shadow() - terrain_load_textures() + for i in 0 .. TERRAIN_INIT_STEPS { terrain_init_step(i) } +} + +# The terrain's start-up as steps a loading screen can show between, in terrain_init's order: +# 0 the height field and its normals (and the read-back for placement), 1 the sun's height-field +# shadow, 2 the ground's materials, 3 the patch tree and the terrain programs. +const TERRAIN_INIT_STEPS: int = 4 +function terrain_init_step(i: int) -> void { + if i == 0 { terrain_generate(); return } + if i == 1 { terrain_bake_shadow(); return } + if i == 2 { terrain_load_textures(); return } + terrain_init_finish() +} + +function terrain_init_finish() -> void { cdlod_init() ter_wire = Os.has_env("R3D_WIRE") ter_force_far = Os.has_env("R3D_TFARONLY")