From a1a90e0626942b96e480a4761c4dadb0fc35c9fd Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 12:37:24 +0300 Subject: [PATCH 1/2] render3d/shadow: a dressed figure casts one capsule in the far cascades In cascades 2 and out (receivers from 60 m) a skinned figure of more than eight pieces - a dressed person, up to 86 of them - casts one capsule its own height (radius 13% of it) instead of every visible piece: past 60 m nobody can tell a garment's shadow from the body's. Animals (a piece or two), rigid props and the near two cascades are unchanged. The capsule is made once. Aspen view, same main (30ec1722) and foundations (15f1020), 240 frames, twice a side: shadow draws 1889 -> 1799; shadow GPU 4808/4819 -> 4710/4714 us; median frame 17200/17221 -> 16910/17128 us. Most of the far cascades' actor draws turned out to be rigid props (the census's "skinned" counts draws, not actors), so this is the smaller of the two. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/actor.ludic | 86 ++++++++++++++++++++++++++++- packages/ludic.render3d/env.ludic | 3 + 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index 199be570..7f37a0c8 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -411,7 +411,7 @@ function actor_draw_casters(render3d_st: mut Render3dState, light_vp: floats) -> var ap = render3d_st.ac_sh if a.cutout { ap = render3d_st.ac_sh_cut } if render3d_st.ac_census_at > 0 and render3d_st.ac_frame == render3d_st.ac_census_at { ac_caster_count(render3d_st, a) } - actor_draw_one(render3d_st, a, ap, true) + if ac_proxy_figure(render3d_st, a) { actor_draw_proxy(render3d_st, a) } else { actor_draw_one(render3d_st, a, ap, true) } } if render3d_st.ac_census_at > 0 and render3d_st.ac_frame == render3d_st.ac_census_at { print(`actor census, cascade {render3d_st.sh_cascade}: {render3d_st.ac_cc_actors} actors cast {render3d_st.ac_cc_draws} draws ({render3d_st.ac_cc_skinned} of them skinned, {render3d_st.ac_cc_cut} cutout)`) @@ -492,3 +492,87 @@ function actor_room(l: []Actor, n: int) -> void { for i in 0 .. n { push(l, null) } while len(l) > have { List.pop(l) } } + +# ---- a figure's shadow past 60 m: one capsule --------------------------------------------------- +# A dressed figure is up to 86 skinned pieces, and the far cascades (2 and out: receivers from 60 m) +# drew each piece of every walker in them - most of a forest frame's shadow pass. Nobody can tell a +# garment's shadow from the body's there, so a figure casts one capsule its own height instead. +# Animals (a piece or two) and anything rigid keep their own shape. +const AC_PROXY_CASCADE: int = 2 +const AC_PROXY_SIDES: int = 8 +const AC_PROXY_R: float = 0.13 # the capsule's radius, as a fraction of the figure's height +function ac_proxy_figure(render3d_st: Render3dState, a: Actor) -> bool { + if render3d_st.sh_cascade < AC_PROXY_CASCADE { return false } + if a.skin == null and a.model.skin == null { return false } + return len(a.model.prims) > 8 +} +function actor_draw_proxy(render3d_st: mut Render3dState, a: Actor) -> void { + let m = ac_proxy_mesh(render3d_st) + let ap = render3d_st.ac_sh + gpu_use_program(render3d_st, ap.prog) + let h = a.model.height + m4_scaling(render3d_st.ac_proxy_s, h * AC_PROXY_R, h, h * AC_PROXY_R) + m4_mul(render3d_st.ac_proxy_m, a.mat, render3d_st.ac_proxy_s) + u_mat4(render3d_st, ap.l_model, render3d_st.ac_proxy_m) + u_f(render3d_st, ap.l_skin, 0.0) + gpu_cull(render3d_st, false) + mesh_draw(render3d_st, m) +} +# a unit capsule (radius 1, height 1, feet at the origin): a ring of AC_PROXY_SIDES at each end of the +# body and a cone to a point below and above them +@alloc_ok("made once: the capsule every far figure casts, and its two matrices") +function ac_proxy_mesh(render3d_st: mut Render3dState) -> Mesh { + if render3d_st.ac_proxy != null { return render3d_st.ac_proxy } + render3d_st.ac_proxy_s = m4_new() + render3d_st.ac_proxy_m = m4_new() + let n = AC_PROXY_SIDES + let nv = 2 + 2 * n + let pos = gl_floats(nv * 3) + let nrm = gl_floats(nv * 3) + let uv = gl_floats(nv * 2) + ac_proxy_vert(pos, nrm, 0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0) + ac_proxy_vert(pos, nrm, 1, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0) + for i in 0 .. n { + let t = float(i) * 6.2831853 / float(n) + let cx = Math.cos(t) + let sz = Math.sin(t) + ac_proxy_vert(pos, nrm, 2 + i, cx, AC_PROXY_R, sz, cx, 0.0, sz) + ac_proxy_vert(pos, nrm, 2 + n + i, cx, 1.0 - AC_PROXY_R, sz, cx, 0.0, sz) + } + for i in 0 .. nv * 2 { gl_put_bits(uv, i, float_bits(0.0)) } + let m = gpu_mesh_new(render3d_st) + gpu_mesh_vertices(render3d_st, m, pos, gl_bytes_of(nv * 3), GPU_STATIC) + gpu_mesh_attr(render3d_st, m, 0, 3, GPU_F32, 0, 0, false) + gpu_mesh_vertices(render3d_st, m, nrm, gl_bytes_of(nv * 3), GPU_STATIC) + gpu_mesh_attr(render3d_st, m, 1, 3, GPU_F32, 0, 0, false) + gpu_mesh_vertices(render3d_st, m, uv, gl_bytes_of(nv * 2), GPU_STATIC) + gpu_mesh_attr(render3d_st, m, 2, 2, GPU_F32, 0, 0, false) + free(pos) + free(nrm) + free(uv) + let ni = n * 12 + let idx = words(ni) + var k = 0 + for i in 0 .. n { + let j = (i + 1) % n + let b0 = 2 + i + let b1 = 2 + j + let t0 = 2 + n + i + let t1 = 2 + n + j + idx[k] = 0; idx[k + 1] = b0; idx[k + 2] = b1 + idx[k + 3] = b0; idx[k + 4] = t0; idx[k + 5] = t1 + idx[k + 6] = b0; idx[k + 7] = t1; idx[k + 8] = b1 + idx[k + 9] = 1; idx[k + 10] = t1; idx[k + 11] = t0 + k += 12 + } + gpu_mesh_indices(render3d_st, m, data_of(idx), ni * 4, 4) + free(idx) + m.count = ni + gpu_mesh_done(render3d_st, m) + render3d_st.ac_proxy = m + return m +} +function ac_proxy_vert(pos: pointer, nrm: pointer, v: int, x: float, y: float, z: float, nx: float, ny: float, nz: float) -> void { + gl_put_bits(pos, v * 3, float_bits(x)); gl_put_bits(pos, v * 3 + 1, float_bits(y)); gl_put_bits(pos, v * 3 + 2, float_bits(z)) + gl_put_bits(nrm, v * 3, float_bits(nx)); gl_put_bits(nrm, v * 3 + 1, float_bits(ny)); gl_put_bits(nrm, v * 3 + 2, float_bits(nz)) +} diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index bac82d23..740379da 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -19,6 +19,9 @@ export state Render3dState { ac_oq_n: int = 0 # live entries; the array is kept and reused ac_oq_closed: bool = true ac_census_at: int = -2 + ac_proxy: Mesh = null # the capsule a figure casts in the far cascades (actor.ludic) + ac_proxy_s: floats = null # its scale and its world matrix, made with it + ac_proxy_m: floats = null ac_lp0: floats = floats(3) ac_lpx: floats = floats(3) ac_lpy: floats = floats(3) From e691f072aea2df026ea36d6b1839cef7e4787c71 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 12:37:24 +0300 Subject: [PATCH 2/2] render3d/shadow: tree impostors cast only into the far cascades Every tree on the map was drawn as an impostor card into all the cascades, the near two included, whose receivers (within 60 m) are shaded by the near trees' own LOD2 meshes cast beside them. The cards now cast from cascade 2 on. Aspen view, same main and foundations, 240 frames, with the figure capsule: shadow instances 323,587 -> 202,743; shadow GPU 4808-4942 -> 4579/4578 us; median frame 17200-17693 -> 17178/17205 us. The look held: shots of the view taken twice a side are identical within a side (0.00% of pixels past 8) and differ by 0.04% across (every pairing). Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/scatter.ludic | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 48d70f6a..fcbea5de 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -1058,6 +1058,7 @@ function scatter_draw(render3d_st: mut Render3dState) -> void { # whole class of caster vanish at a fixed distance, which is exactly the popping. A layer # with an impostor now casts its entire instance list from the card in every cascade; # only layers that have no impostor at all fall back to the mesh. +const SC_IMP_CAST_CASCADE: int = 2 function scatter_draw_casters(render3d_st: mut Render3dState, light_vp: floats) -> void { for i in 0 .. len(render3d_st.sc_layers) { let l = render3d_st.sc_layers[i] @@ -1067,7 +1068,10 @@ function scatter_draw_casters(render3d_st: mut Render3dState, light_vp: floats) if render3d_st.r3d_no_trees and l.imp != null and not l.card { continue } layer_update(render3d_st, l) if l.imp != null { - layer_draw_shadow(render3d_st, l, light_vp) + # the impostor casts only into the far cascades: every tree on the map was drawn as a card + # into the near two as well, whose receivers (within 60 m) are shaded by the near trees' + # own LOD2 meshes, cast just below + if render3d_st.sh_cascade >= SC_IMP_CAST_CASCADE { layer_draw_shadow(render3d_st, l, light_vp) } # Shadow LOD (the practice in every production engine: a caster uses a low mesh LOD, # the billboard only far away). The card alone is a side-view silhouette and a # crown of drooping needle cards is mostly slivers from the side, so the sun, which