From dcf6c55a4e1fda50c71b2012fad87380f00077ee Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 15 Sep 2026 14:52:39 +0300 Subject: [PATCH] perf(render3d): an actor casts only into the shadow cascades its bounds reach actor_draw_casters drew every visible actor within 300 m into all five cascades. The draw-call count (R3D_DRAWSTATS) found about 1500 skinned shadow draws a frame in town against 69 in the lit pass. The cascade is an orthographic box, so a bounding sphere (1.5x the actor's radius) outside its clip x/y casts nothing into that layer and is skipped. OpenGL frames at the five viewpoints are unchanged; the game's 59 self-tests pass. Co-Authored-By: Claude Opus 5 --- packages/ludic.render3d/actor.ludic | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index d7e6e0b0..8a32e4e9 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -300,6 +300,30 @@ function actor_draw_outline_one(a: Actor, ap: AcProg) -> void { mesh_draw(pr.mesh) } } +# Is a sphere inside this cascade's light box? The projection is orthographic, so a point's +# clip x and y are the matrix applied to it, and the sphere's reach along each is bounded by the +# three axis offsets. An actor outside the box casts nothing into that layer: skipping it changes no +# depth, only the draw. (Each actor used to draw into every cascade out to 300 m - in town that was +# about 1500 skinned shadow draws a frame against 69 in the lit pass.) +var ac_lp0: words = null +var ac_lpx: words = null +var ac_lpy: words = null +var ac_lpz: words = null +function ac_in_light(vp: words, x: int, y: int, z: int, r: int) -> bool { + if ac_lp0 == null { ac_lp0 = words(3); ac_lpx = words(3); ac_lpy = words(3); ac_lpz = words(3) } + m4_xform_point(ac_lp0, vp, x, y, z) + m4_xform_point(ac_lpx, vp, f_add(x, r), y, z) + m4_xform_point(ac_lpy, vp, x, f_add(y, r), z) + m4_xform_point(ac_lpz, vp, x, y, f_add(z, r)) + for k in 0 .. 2 { + let c = ac_lp0[k] + let e = f_add(f_add(ac_abs(f_sub(ac_lpx[k], c)), ac_abs(f_sub(ac_lpy[k], c))), ac_abs(f_sub(ac_lpz[k], c))) + if f_gt(f_sub(ac_abs(c), e), F_ONE) { return false } + } + return true +} +function ac_abs(v: int) -> int { return f_max(v, f_neg(v)) } + function actor_draw_casters(light_vp: words) -> void { if ac_actors == null { return } gpu_use_program(ac_sh.prog); u_mat4(ac_sh.l_lvp, light_vp) @@ -307,6 +331,10 @@ function actor_draw_casters(light_vp: words) -> void { for i in 0 .. len(ac_actors) { 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 } + } var ap = ac_sh if a.cutout { ap = ac_sh_cut } actor_draw_one(a, ap, true)