diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index e2c7884b..deb5a41e 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -236,9 +236,40 @@ function outline_clear() -> void { ac_oq_n = 0; ac_oq_closed = true } # highlighted tree left its rim on until something else was highlighted. function outline_frame() -> void { if ac_oq_closed { ac_oq_n = 0 } } +# R3D_ACTOR_CENSUS=: at that frame, the lit pass's actors grouped by model - skinned or rigid, +# how many share it, the primitives each draws - so instancing is aimed at what repeats +function actor_census() -> void { + let keys = new []Model + let cnt = new []int + let prims = new []int + let rigid = new []int + for i in 0 .. len(ac_actors) { + let a = ac_actors[i] + if not ac_visible(a, false) { continue } + var k = -1 + for j in 0 .. len(keys) { if keys[j] == a.model { k = j } } + if k < 0 { push(keys, a.model); push(cnt, 0); push(prims, 0); var r = 1; if a.model.skin != null { r = 0 }; push(rigid, r); k = len(keys) - 1 } + if a.skin != null { rigid[k] = 0 } + cnt[k] += 1 + var shown = 0 + for p in 0 .. len(a.model.prims) { if a.hide == null or a.hide[p] == 0 { shown += 1 } } + prims[k] += shown + } + var total = 0 + for j in 0 .. len(keys) { + var kind = "skinned" + if rigid[j] == 1 { kind = "rigid" } + print(`actor census: model {j} ({kind}, {len(keys[j].prims)} prims, {keys[j].tris} tris): {cnt[j]} actors, {prims[j]} draws`) + total += prims[j] + } + print(`actor census: {len(keys)} models, {total} lit draws`) +} +var ac_census_at: int = -2 function actor_draw() -> void { if ac_actors == null { return } ac_frame += 1 + if ac_census_at == -2 { ac_census_at = -1; if Os.has_env("R3D_ACTOR_CENSUS") { ac_census_at = Text.to_int(Os.env("R3D_ACTOR_CENSUS")) } } + if ac_census_at > 0 and ac_frame == ac_census_at { actor_census() } for i in 0 .. len(ac_actors) { let a = ac_actors[i] if not ac_visible(a, false) { continue } diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index 93bcb342..618d88f5 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -197,7 +197,7 @@ function r3d_frame(time: int) -> void { prof_cpu_mark("streaming") if not r3d_no_shadow { prof_begin("shadow"); shadow_pass(); prof_end() } prof_cpu_mark("shadow pass") - if water_on and not r3d_no_refl { prof_begin("water reflection"); water_reflection_pass(); prof_end() } + if water_on and not r3d_no_refl and water_reflect_visible() { prof_begin("water reflection"); water_reflection_pass(); prof_end() } prof_cpu_mark("reflection") post_begin_scene() # the near tree foliage lays its depth down before anything is shaded, so the terrain diff --git a/packages/ludic.render3d/water.ludic b/packages/ludic.render3d/water.ludic index bd4909bf..fbb6c657 100644 --- a/packages/ludic.render3d/water.ludic +++ b/packages/ludic.render3d/water.ludic @@ -96,6 +96,184 @@ function water_reflection_pass() -> void { gpu_fb_bind(0) } +# Is any of the mirrored water in view? The reflection pass is a second copy of the terrain, the +# vegetation and the actors - 170-260 draws - and it ran on every frame of a map with a lake, looking +# straight down at a meadow included, where no pixel samples it. The mirrored body's bounds are cut +# into rectangles (water_cells_build) that count where the ground lies below the water, and the pass +# runs when one of those rectangles meets the view frustum and its water is not all dry where it shows, +# outside the frame or behind the ground (wb_cell_occluded). Past 16 rectangles in view it runs without +# asking, so it errs on the side of running. R3D_REFL_ALWAYS=1 runs it on every frame, for comparing. +# Mac, the five shot viewpoints: looking down at the meadow (c) 225 reflection draws -> none; every view +# with the lake in it unchanged, byte-identical frames. +var wb_cells: words = null # x0, z0, x1, z1 of each rectangle with water showing +var wb_ncells: int = -1 # -1: not built for the current mirrored body +var wb_blocks: words = null # x0, z0, x1, z1, first cell, cells past the last - of each block +var wb_nblocks: int = 0 +var wb_refl_always: int = -1 +var wb_dbg: int = -1 +var wb_build_us: long = 0 # how long the last water_cells_build took (R3D_REFL_DBG prints it) +function water_reflect_visible() -> bool { + if wb_primary < 0 { return false } + if wb_refl_always < 0 { wb_refl_always = 0; if Os.has_env("R3D_REFL_ALWAYS") { wb_refl_always = 1 } } + if wb_refl_always == 1 or ter_heights == null { return true } + if wb_ncells < 0 { let t0 = gl_now_us(); water_cells_build(); wb_build_us = gl_now_us() - t0 } + if wb_dbg < 0 { wb_dbg = 0; if Os.has_env("R3D_REFL_DBG") { wb_dbg = 1 } } + if wb_dbg == 1 { + # R3D_REFL_DBG: once, what the test is made of and how much of it is in view + wb_dbg = 2 + var seen = 0 + var hidden = 0 + for i in 0 .. wb_ncells { if wb_cell_visible(i) { seen += 1; if wb_cell_occluded(i) { hidden += 1 } } } + print(`water reflection test: {wb_ncells} wet rectangles in {wb_nblocks} blocks (built in {wb_build_us} us), {seen} in view, {hidden} of them behind the ground`) + } + # A rectangle in the frustum can still be behind the ground or dry where it shows: standing on a shore + # above the lake and looking down at your feet puts water inside the view and none of it on screen. + # The first 16 in view are checked against the height field; past that the pass simply runs. + var checked = 0 + for b in 0 .. wb_nblocks { + let o = b * 6 + if not wb_rect_visible(wb_blocks[o], wb_blocks[o + 1], wb_blocks[o + 2], wb_blocks[o + 3]) { continue } + for i in wb_blocks[o + 4] .. wb_blocks[o + 5] { + if not wb_cell_visible(i) { continue } + if checked >= 16 { return true } + checked += 1 + if not wb_cell_occluded(i) { return true } + } + } + return false +} +# A flat rectangle at the water's level against the view's side planes: hidden when all four corners +# lie outside one plane. (A bounding sphere was the first version: a 156 m cell's sphere reached into +# the view from a lake the camera had its back to, and the pass never skipped.) +function wb_rect_visible(x0: int, z0: int, x1: int, z1: int) -> bool { + if cam_planes == null { return true } + for p in 0 .. 4 { + let q = p * 4 + let a = cam_planes[q]; let c = cam_planes[q + 2] + let by = f_add(f_mul(cam_planes[q + 1], water_level), cam_planes[q + 3]) + let ax0 = f_mul(a, x0); let ax1 = f_mul(a, x1); let cz0 = f_mul(c, z0); let cz1 = f_mul(c, z1) + if f_ls(f_add(f_add(ax0, cz0), by), F_ZERO) and f_ls(f_add(f_add(ax1, cz0), by), F_ZERO) and f_ls(f_add(f_add(ax0, cz1), by), F_ZERO) and f_ls(f_add(f_add(ax1, cz1), by), F_ZERO) { return false } + } + return true +} +function wb_cell_visible(i: int) -> bool { + let o = i * 4 + return wb_rect_visible(wb_cells[o], wb_cells[o + 1], wb_cells[o + 2], wb_cells[o + 3]) +} +# Is the water at (x, z) out of sight - dry ground there, outside the frame, or behind the ground? The +# sight line is sampled at seven points short of both ends, with half a metre (and a little more with +# distance) of margin, so a far ridge the drawn terrain rounds off never hides real water. A camera at +# or under the surface hides nothing behind the ground. +function wb_point_hidden(x: int, z: int) -> bool { + if f_gt(terrain_height(x, z), f_add(water_level, fl(0.05))) { return true } + if not cam_sphere_visible(x, water_level, z, F_HALF) { return true } + let ey = cam_pos[1] + if not f_gt(ey, water_level) { return false } + let dx = f_sub(x, cam_pos[0]); let dz = f_sub(z, cam_pos[2]) + let margin = f_add(F_HALF, f_mul(f_sqrt(f_add(f_mul(dx, dx), f_mul(dz, dz))), fl(0.004))) + for k in 1 .. 8 { + let t = fr(k, 8) + let sy = f_add(ey, f_mul(f_sub(water_level, ey), t)) + if f_gt(terrain_height(f_add(cam_pos[0], f_mul(dx, t)), f_add(cam_pos[2], f_mul(dz, t))), f_add(sy, margin)) { return true } + } + return false +} +# every point the wet test sampled (the centre and four near the corners) is out of sight +function wb_cell_occluded(i: int) -> bool { + let o = i * 4 + let x0 = wb_cells[o]; let z0 = wb_cells[o + 1]; let x1 = wb_cells[o + 2]; let z1 = wb_cells[o + 3] + let cx = f_mul(f_add(x0, x1), F_HALF); let cz = f_mul(f_add(z0, z1), F_HALF) + let ox = f_mul(f_sub(x1, x0), fl(0.45)); let oz = f_mul(f_sub(z1, z0), fl(0.45)) + if not wb_point_hidden(cx, cz) { return false } + if not wb_point_hidden(f_sub(cx, ox), f_sub(cz, oz)) { return false } + if not wb_point_hidden(f_add(cx, ox), f_sub(cz, oz)) { return false } + if not wb_point_hidden(f_sub(cx, ox), f_add(cz, oz)) { return false } + return wb_point_hidden(f_add(cx, ox), f_add(cz, oz)) +} +function wb_cell_add(x0: int, z0: int, x1: int, z1: int) -> void { + let o = wb_ncells * 4 + wb_cells[o] = x0; wb_cells[o + 1] = z0; wb_cells[o + 2] = x1; wb_cells[o + 3] = z1 + wb_ncells += 1 +} +function wb_block_add(x0: int, z0: int, x1: int, z1: int, first: int) -> void { + if wb_ncells <= first { return } + let o = wb_nblocks * 6 + wb_blocks[o] = x0; wb_blocks[o + 1] = z0; wb_blocks[o + 2] = x1; wb_blocks[o + 3] = z1 + wb_blocks[o + 4] = first; wb_blocks[o + 5] = wb_ncells + wb_nblocks += 1 +} +function wb_wet(x: int, z: int, off: int) -> bool { + if f_ls(terrain_height(x, z), water_level) { return true } + if off == 0 { return false } + if f_ls(terrain_height(f_sub(x, off), f_sub(z, off)), water_level) { return true } + if f_ls(terrain_height(f_add(x, off), f_sub(z, off)), water_level) { return true } + if f_ls(terrain_height(f_sub(x, off), f_add(z, off)), water_level) { return true } + return f_ls(terrain_height(f_add(x, off), f_add(z, off)), water_level) +} +# Over the height map, 32 m rectangles (at most 512 a side) tested at five points and kept in blocks of +# 8 x 8, so a block out of view skips its rectangles in one test - looking away from the water was a +# walk of every rectangle, every frame. The body beyond the map (a sea runs far past it) is 64 x 64 +# coarse rectangles tested at their centre, in one last block, which only ever matter kilometres away. +const WB_BLOCK: int = 8 +function water_cells_build() -> void { + let bx0 = f_sub(water_cx, water_ex); let bx1 = f_add(water_cx, water_ex) + let bz0 = f_sub(water_cz, water_ez); let bz1 = f_add(water_cz, water_ez) + let th = fi(TERRAIN_HALF) + let tx0 = f_max(bx0, f_sub(ter_ox, th)); let tx1 = f_min(bx1, f_add(ter_ox, th)) + let tz0 = f_max(bz0, f_sub(ter_oz, th)); let tz1 = f_min(bz1, f_add(ter_oz, th)) + let inside = f_gt(tx1, tx0) and f_gt(tz1, tz0) + var cell = fi(32) + var nx = 0 + var nz = 0 + if inside { + let span = f_max(f_sub(tx1, tx0), f_sub(tz1, tz0)) + if f_gt(f_div(span, cell), fi(512)) { cell = f_div(span, fi(512)) } + nx = f_to_int(f_div(f_sub(tx1, tx0), cell)) + 1 + nz = f_to_int(f_div(f_sub(tz1, tz0), cell)) + 1 + } + if wb_cells != null { free(wb_cells) } + if wb_blocks != null { free(wb_blocks) } + wb_cells = words((nx * nz + 64 * 64) * 4) + let nbx = (nx + WB_BLOCK - 1) / WB_BLOCK + let nbz = (nz + WB_BLOCK - 1) / WB_BLOCK + wb_blocks = words((nbx * nbz + 1) * 6) + wb_ncells = 0 + wb_nblocks = 0 + let off = f_mul(cell, fl(0.45)) + let half = f_mul(cell, F_HALF) + for bz in 0 .. nbz { + for bx in 0 .. nbx { + let first = wb_ncells + var iz = bz * WB_BLOCK + while iz < (bz + 1) * WB_BLOCK and iz < nz { + let z0 = f_add(tz0, f_mul(fi(iz), cell)) + var ix = bx * WB_BLOCK + while ix < (bx + 1) * WB_BLOCK and ix < nx { + let x0 = f_add(tx0, f_mul(fi(ix), cell)) + if wb_wet(f_add(x0, half), f_add(z0, half), off) { wb_cell_add(x0, z0, f_add(x0, cell), f_add(z0, cell)) } + ix += 1 + } + iz += 1 + } + let x0 = f_add(tx0, f_mul(fi(bx * WB_BLOCK), cell)); let z0 = f_add(tz0, f_mul(fi(bz * WB_BLOCK), cell)) + wb_block_add(x0, z0, f_add(x0, f_mul(fi(WB_BLOCK), cell)), f_add(z0, f_mul(fi(WB_BLOCK), cell)), first) + } + } + let first = wb_ncells + let cw = f_div(f_sub(bx1, bx0), fi(64)) + let ch = f_div(f_sub(bz1, bz0), fi(64)) + for iz in 0 .. 64 { + let z0 = f_add(bz0, f_mul(fi(iz), ch)); let z1 = f_add(z0, ch) + for ix in 0 .. 64 { + let x0 = f_add(bx0, f_mul(fi(ix), cw)); let x1 = f_add(x0, cw) + # inside the height map's rectangle: the fine cells above cover it + if inside and not f_ls(x0, tx0) and not f_gt(x1, tx1) and not f_ls(z0, tz0) and not f_gt(z1, tz1) { continue } + if wb_wet(f_add(x0, f_mul(cw, F_HALF)), f_add(z0, f_mul(ch, F_HALF)), 0) { wb_cell_add(x0, z0, x1, z1) } + } + } + wb_block_add(bx0, bz0, bx1, bz1, first) +} + # the program and the plane are the process's, built once; the bodies are the map's function water_setup() -> void { if water_prog != 0 { return } @@ -107,6 +285,7 @@ function water_setup() -> void { function water_bodies_clear() -> void { wb_n = 0 wb_primary = -1 + wb_ncells = -1 water_on = false } # Add a plane at `level` over (cx, cz) +- (ex, ez); true `reflect` makes it the mirrored one @@ -122,6 +301,7 @@ function water_body_add(level: int, cx: int, cz: int, ex: int, ez: int, reflect: water_on = true if reflect and wb_primary < 0 { wb_primary = i + wb_ncells = -1 water_level = level; water_cx = cx; water_cz = cz; water_ex = ex; water_ez = ez } return i