render3d: the water reflection runs only when mirrored water can be on screen; an actor census
The reflection pass is a second copy of the terrain, the vegetation and the actors, and it ran on every frame of a map with water - looking straight down at a meadow included. The mirrored body is cut into rectangles where the ground lies below it (32 m over the height map in 8 x 8 blocks, the sea beyond it coarse), and the pass runs when one meets the view frustum and its water is not all dry where it shows, outside the frame or behind the ground. Three wrong turns on the way, each kept in a comment: bounding spheres (a 156 m cell reached into the view from behind the camera), sight lines that never asked whether the point was in the frame, and a walk of every rectangle every frame (0.1 s per 400 frames on the PC until the blocks). Built once in 6.6 ms. Mac, the five shot viewpoints: view c (the meadow) 225 reflection draws -> none; a, b, d, e unchanged; OpenGL frames byte-identical; self-tests 59/59 on OpenGL and Vulkan; MoltenVK validation adds nothing. PC camp (lake in view): 1882 draws either way, 3.8 s for 400 frames either way, three runs each, self-tests 59/59, validation 0. R3D_REFL_ALWAYS=1 runs the pass every frame; R3D_REFL_DBG=1 prints the test once. R3D_ACTOR_CENSUS=<frame> prints the lit pass's actors grouped by model: town is 17 models and 69 draws, and only four rigid models repeat (17 actors) - too little for instancing to be worth a shader variant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
44ecd55a86
commit
ef745a141d
3 changed files with 212 additions and 1 deletions
|
|
@ -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=<frame>: 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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue