diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index 7f37a0c8..3393abb1 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -175,6 +175,10 @@ function ac_visible(render3d_st: Render3dState, a: Actor, shadow: bool) -> bool if a.model == null { return false } if not a.visible and not (shadow and a.cast_hidden) { return false } if shadow and not a.casts { return false } + # an actor with no radius given is sized generously, since a building or the dock may be one + var fr = a.radius * a.scale + if fr <= 0.0 { fr = 20.0 } + if r3d_beyond_fog(render3d_st, a.x, a.y, a.z, fr) { return false } if a.cull != 0.0 { let dx = a.x - render3d_st.cam_pos[0]; let dz = a.z - render3d_st.cam_pos[2] let d2 = dx * dx + dz * dz diff --git a/packages/ludic.render3d/camera.ludic b/packages/ludic.render3d/camera.ludic index d1f1fe61..4fc4a74a 100644 --- a/packages/ludic.render3d/camera.ludic +++ b/packages/ludic.render3d/camera.ludic @@ -70,6 +70,7 @@ function cam_planes_update(render3d_st: mut Render3dState) -> void { # is a sphere (float bits) at least partly inside the side planes of the view? function cam_sphere_visible(render3d_st: Render3dState, x: float, y: float, z: float, r: float) -> bool { if render3d_st.cam_planes == null { return true } + if r3d_beyond_fog(render3d_st, x, y, z, r) { return false } let nr = -r for p in 0 .. 4 { let o = p * 4 @@ -78,6 +79,14 @@ function cam_sphere_visible(render3d_st: Render3dState, x: float, y: float, z: f } return true } +# is a sphere wholly past the fog wall? Nothing there is seen, so nothing there is drawn or animated +export function r3d_beyond_fog(render3d_st: Render3dState, x: float, y: float, z: float, r: float) -> bool { + let w = render3d_st.r3d_fog_wall + if w <= 0.0 { return false } + let dx = x - render3d_st.cam_pos[0]; let dy = y - render3d_st.cam_pos[1]; let dz = z - render3d_st.cam_pos[2] + let far = w + R3D_FOG_MARGIN + r + return dx * dx + dy * dy + dz * dz > far * far +} # fly: forward/strafe in metres, turn in radians function cam_move(render3d_st: mut Render3dState, fwd: float, strafe: float, up: float, dyaw: float, dpitch: float) -> void { render3d_st.cam_yaw = render3d_st.cam_yaw + dyaw diff --git a/packages/ludic.render3d/fog_casters.ludic b/packages/ludic.render3d/fog_casters.ludic new file mode 100644 index 00000000..9e7cd6fb --- /dev/null +++ b/packages/ludic.render3d/fog_casters.ludic @@ -0,0 +1,40 @@ +# fog_casters.ludic — under a fog wall, a layer's cards cast shadows only from where they can be seen. +# The impostor shadow list is every instance on the map; past the wall that is a vertex bill for +# nothing, so the casters inside the wall (and a shadow's throw past it) go into a list of their own. + +const FOG_CAST_THROW: float = 30.0 # the longest shadow a tree throws back toward the camera +const FOG_CAST_STEP: float = 4.0 # the camera moves this far before the list is rebuilt + +# the buffer and count the card shadows draw from: the whole list, or the wall's share of it +function fog_casters(render3d_st: mut Render3dState, l: Layer) -> int { + let w = render3d_st.r3d_fog_wall + if w <= 0.0 { return l.sh_buf } + let dx = render3d_st.cam_pos[0] - l.fog_x + let dz = render3d_st.cam_pos[2] - l.fog_z + if l.fog_wall == w and l.fog_src == l.n_sh and dx * dx + dz * dz < FOG_CAST_STEP * FOG_CAST_STEP { return l.fog_buf } + l.fog_wall = w; l.fog_src = l.n_sh + l.fog_x = render3d_st.cam_pos[0]; l.fog_z = render3d_st.cam_pos[2] + let far = w + R3D_FOG_MARGIN + FOG_CAST_THROW + FOG_CAST_STEP + let far2 = far * far + let src = l.inst + let dst = l.scratch + var n = 0 + let total = Math.min(l.n_sh, l.count) + for i in 0 .. total { + let o = i * INST_FLOATS + let ex = src[o] - l.fog_x + let ez = src[o + 2] - l.fog_z + if ex * ex + ez * ez > far2 { continue } + let q = n * INST_FLOATS + for k in 0 .. INST_FLOATS { dst[q + k] = src[o + k] } + n += 1 + } + l.fog_n = n + if n > 0 { gpu_buffer_upload(render3d_st, l.fog_buf, n * INST_FLOATS * 4, data_of(dst), GPU_DYNAMIC) } + return l.fog_buf +} + +function fog_casters_n(render3d_st: Render3dState, l: Layer) -> int { + if render3d_st.r3d_fog_wall <= 0.0 { return l.n_sh } + return l.fog_n +} diff --git a/packages/ludic.render3d/grass.ludic b/packages/ludic.render3d/grass.ludic index 25611827..c386917a 100644 --- a/packages/ludic.render3d/grass.ludic +++ b/packages/ludic.render3d/grass.ludic @@ -127,6 +127,13 @@ function grass_init(render3d_st: mut Render3dState) -> void { } # indices per 16 m cell that could exist at distance d (the count the shader computes) +# no blades past the fog wall: they would be drawn only to be fogged away +function grass_reach(render3d_st: Render3dState) -> float { + let w = render3d_st.r3d_fog_wall + if w > 0.0 and w < render3d_st.grass_radius { return w } + return render3d_st.grass_radius +} + function grass_count_at(render3d_st: Render3dState, d: float) -> int { let spacing = render3d_st.grass_s0 * (1.0 + d / render3d_st.grass_d0) let n = float(GRASS_CELL * GRASS_CELL) / (spacing * spacing) @@ -137,7 +144,7 @@ function grass_count_at(render3d_st: Render3dState, d: float) -> int { function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_max: float, blade: Mesh) -> void { let p = render3d_st.grass_prog let cells = size / GRASS_CELL - if d_min >= render3d_st.grass_radius { return } + if d_min >= grass_reach(render3d_st) { return } if render3d_st.grass_merge { render3d_st.grass_band_start[render3d_st.grass_band_n] = render3d_st.grass_n; render3d_st.grass_band_cells[render3d_st.grass_band_n] = cells render3d_st.grass_band_mesh[render3d_st.grass_band_n] = blade; render3d_st.grass_band_n += 1 @@ -228,11 +235,11 @@ function grass_bind(render3d_st: mut Render3dState, p: int) -> void { u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_tint"), render3d_st.sc_blade_tint) u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_blade_base"), render3d_st.sc_blade_base) u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_blade_tip"), render3d_st.sc_blade_tip) - u_f(render3d_st, gpu_uniform(render3d_st, p, "u_cull"), render3d_st.grass_radius) + u_f(render3d_st, gpu_uniform(render3d_st, p, "u_cull"), grass_reach(render3d_st)) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_model_h"), 0.0) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_s0"), render3d_st.grass_s0) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_d0"), render3d_st.grass_d0) - u_f(render3d_st, gpu_uniform(render3d_st, p, "u_radius"), render3d_st.grass_radius) + u_f(render3d_st, gpu_uniform(render3d_st, p, "u_radius"), grass_reach(render3d_st)) var ph = render3d_st.post_h if ph <= 0 { ph = gl_height() } u_f(render3d_st, gpu_uniform(render3d_st, p, "u_px"), 2.0 * Math.tan(render3d_st.cam_fov * 0.5) / float(ph)) @@ -268,7 +275,7 @@ function grass_bands(render3d_st: mut Render3dState) -> void { grass_tiles(render3d_st, 4, 0.0, 6.0, render3d_st.grass_mesh) grass_tiles(render3d_st, 4, 6.0, 16.0, render3d_st.grass_mesh3) grass_tiles(render3d_st, 8, 16.0, 40.0, render3d_st.grass_mesh2) - grass_tiles(render3d_st, 16, 40.0, render3d_st.grass_radius, render3d_st.grass_mesh2) + grass_tiles(render3d_st, 16, 40.0, grass_reach(render3d_st), render3d_st.grass_mesh2) } # the records gathered this frame: uploaded once, then each band GRASS_CHUNK records a draw diff --git a/packages/ludic.render3d/grass_gpu.ludic b/packages/ludic.render3d/grass_gpu.ludic index 6a693b14..add91d4c 100644 --- a/packages/ludic.render3d/grass_gpu.ludic +++ b/packages/ludic.render3d/grass_gpu.ludic @@ -66,7 +66,7 @@ function gg_blade(render3d_st: mut Render3dState, rows: int) -> Mesh { # one tile size over one distance band: its visible tiles into gg_tv (corner, indices per cell, cells) function gg_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_max: float) -> void { - if d_min >= render3d_st.grass_radius { return } + if d_min >= grass_reach(render3d_st) { return } let cells = size / GRASS_CELL let sz = float(size) let half = sz * 0.5 @@ -102,7 +102,7 @@ function gg_cull_frame(render3d_st: mut Render3dState) -> void { render3d_st.gg_max = 0 gg_tiles(render3d_st, 4, 0.0, 16.0) gg_tiles(render3d_st, 8, 16.0, 40.0) - gg_tiles(render3d_st, 16, 40.0, render3d_st.grass_radius) + gg_tiles(render3d_st, 16, 40.0, grass_reach(render3d_st)) # made once, with the device (gvk_startup_state) let rb = render3d_st.gg_rb rb[0] = GG_BANDS; rb[1] = 0; rb[2] = 0; rb[3] = 0 @@ -126,7 +126,7 @@ function gg_params(render3d_st: mut Render3dState) -> words { let pr = render3d_st.gg_pr for i in 0 .. 48 { pr[i] = 0 } if render3d_st.cam_planes != null { for i in 0 .. 16 { pr[i] = float_bits(render3d_st.cam_planes[i]) } } - pr[16] = float_bits(render3d_st.cam_pos[0]); pr[17] = float_bits(render3d_st.cam_pos[1]); pr[18] = float_bits(render3d_st.cam_pos[2]); pr[19] = float_bits(render3d_st.grass_radius) + pr[16] = float_bits(render3d_st.cam_pos[0]); pr[17] = float_bits(render3d_st.cam_pos[1]); pr[18] = float_bits(render3d_st.cam_pos[2]); pr[19] = float_bits(grass_reach(render3d_st)) var ph = render3d_st.post_h if ph <= 0 { ph = gl_height() } pr[20] = float_bits(render3d_st.grass_s0); pr[21] = float_bits(render3d_st.grass_d0); pr[22] = float_bits(2.0 * Math.tan(render3d_st.cam_fov * 0.5) / float(ph)); pr[23] = float_bits(render3d_st.ter_snow_line) diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index d3f721c0..4a9237bc 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -33,6 +33,7 @@ import "gltf_release.ludic" import "gltf_cached.ludic" import "skin.ludic" import "scatter.ludic" +import "fog_casters.ludic" import "actor.ludic" import "stream.ludic" import "grass.ludic" diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index 31b85308..9532fe44 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -54,7 +54,10 @@ const R3D_FOG_MARGIN: float = 8.0 export function r3d_fog_wall(render3d_st: mut Render3dState, dist: float) -> void { var d = dist if d < 0.0 { d = 0.0 } + if d == render3d_st.r3d_fog_wall { return } render3d_st.r3d_fog_wall = d + # every layer and stream re-gathers for the new reach + render3d_st.sc_view_gen += 1 cam_update(render3d_st) } # how far anything is drawn: the fog wall and a margin, or `far` (0: no limit of its own) without one diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index ce06ccf6..5bcfac85 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -32,6 +32,12 @@ property Layer { imp_buf: int = 0, sh_buf: int = 0, # every instance, for shadow casting (no cull, no LOD split) n_sh: int = 0, + fog_buf: int = 0, # the casters inside the fog wall (fog_casters.ludic) + fog_n: int = 0, + fog_src: int = -1, + fog_wall: float = 0.0, + fog_x: float = 0.0, + fog_z: float = 0.0, n_far: int = 0, scratch: floats, last_cam: floats, @@ -261,6 +267,7 @@ function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, folia l.buf = gpu_buffer_new(render3d_st) l.imp_buf = gpu_buffer_new(render3d_st) l.sh_buf = gpu_buffer_new(render3d_st) + l.fog_buf = gpu_buffer_new(render3d_st) l.rough = 1.0 for i in 0 .. len(model.prims) { scatter_attach(render3d_st, model.prims[i].mesh, l.buf) } push(render3d_st.sc_layers, l) @@ -983,8 +990,11 @@ function layer_draw_shadow(render3d_st: mut Render3dState, l: Layer, light_vp: f u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_face_dir"), render3d_st.sun_dir) u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_cam_pos"), render3d_st.cam_pos) gpu_cull(render3d_st, false) - scatter_attach(render3d_st, render3d_st.sc_card, l.sh_buf) - mesh_draw_instanced(render3d_st, render3d_st.sc_card, l.n_sh) + let buf = fog_casters(render3d_st, l) + let n = fog_casters_n(render3d_st, l) + if n == 0 { return } + scatter_attach(render3d_st, render3d_st.sc_card, buf) + mesh_draw_instanced(render3d_st, render3d_st.sc_card, n) } @@ -1196,6 +1206,7 @@ function scatter_clear_all(render3d_st: mut Render3dState) -> void { if l.buf != 0 { gpu_buffer_free(render3d_st, l.buf) } if l.imp_buf != 0 { gpu_buffer_free(render3d_st, l.imp_buf) } if l.sh_buf != 0 { gpu_buffer_free(render3d_st, l.sh_buf) } + if l.fog_buf != 0 { gpu_buffer_free(render3d_st, l.fog_buf) } if l.lod_buf != null { for k in 0 .. l.n_lods { gpu_buffer_free(render3d_st, l.lod_buf[k]) } } if l.inst != null { free(l.inst) } if l.scratch != null { free(l.scratch) } diff --git a/packages/ludic.render3d/stream.ludic b/packages/ludic.render3d/stream.ludic index 99f8e42f..b1ffc818 100644 --- a/packages/ludic.render3d/stream.ludic +++ b/packages/ludic.render3d/stream.ludic @@ -221,7 +221,9 @@ function stream_update(render3d_st: mut Render3dState, s: Stream, cam_x: float, render3d_st.stream_walks += 1 render3d_st.stream_walk_no += 1 let tw = gl_now_us() - let r = int(s.reach / s.size) + 1 + # past the fog wall nothing is generated, gathered or kept wanted + let reach = r3d_reach(render3d_st, s.reach) + let r = int(reach / s.size) + 1 # rings outward from the camera's cell: the nearest chunks are generated first var ring = 0 while ring <= r { @@ -236,7 +238,7 @@ function stream_update(render3d_st: mut Render3dState, s: Stream, cam_x: float, let dx = wx - cam_x; let dz = wz - cam_z let d = Math.sqrt(dx * dx + dz * dz) let band = stream_band(s, d) - if band < 4 and band >= s.min_band and d < s.reach + s.size { + if band < 4 and band >= s.min_band and d < reach + s.size { let key = stream_key(cx, cz, band) var c = stream_find(s, key) if c != null { c.used = render3d_st.stream_walk_no }