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>
264 lines
10 KiB
Text
264 lines
10 KiB
Text
# ============================================================================
|
|
# render.ludic — the frame. Shadow cascades, the HDR scene pass (terrain, the
|
|
# scene's objects, the sky), bloom, and the tonemapped composite to the screen.
|
|
# The scene (what the game places in the world) hooks in through scene_draw /
|
|
# scene_draw_casters, which the demo defines.
|
|
# ============================================================================
|
|
|
|
var r3d_sky_prog: int = 0
|
|
var r3d_fog_density: int = 0
|
|
var r3d_fog_scale: int = 0x3F800000 # float bits: a setting's multiplier over the density the day sets
|
|
var r3d_fog_falloff: int = 0
|
|
var r3d_fog_base: int = 0 # the height fog is measured from (float bits); 0 = y = 0
|
|
var r3d_time: int = 0
|
|
var r3d_ready: bool = false
|
|
# set before r3d_init to build the landscape from a real height map
|
|
var r3d_dem_path: string = null
|
|
var r3d_dem_min: int = 0
|
|
var r3d_dem_max: int = 0
|
|
var r3d_dem_base: int = 0
|
|
var r3d_dem_ox: int = 0
|
|
var r3d_dem_oz: int = 0
|
|
var r3d_ortho_path: string = null
|
|
var r3d_debug: bool = false
|
|
var r3d_debug_shadow: bool = false
|
|
var r3d_debug_max: bool = false
|
|
|
|
var r3d_cloud_shadow: int = 0x3F000000 # 0.5
|
|
|
|
var r3d_clip_y: int = 0xCF000000 # -2^31: no clipping
|
|
# R3D_NOPREPASS=1: light the foliage the old way, every card behind the front one included
|
|
var r3d_prepass_env: int = -1
|
|
function r3d_prepass_off() -> bool {
|
|
if r3d_prepass_env < 0 { r3d_prepass_env = 0; if Os.has_env("R3D_NOPREPASS") { r3d_prepass_env = 1 } }
|
|
return r3d_prepass_env == 1
|
|
}
|
|
function fog_bind(prog: int) -> void {
|
|
u_f(gpu_uniform(prog, "u_clip_y"), r3d_clip_y)
|
|
u_f(gpu_uniform(prog, "u_spec_scale"), F_ONE)
|
|
u_f(gpu_uniform(prog, "u_fog_density"), r3d_fog_density)
|
|
u_f(gpu_uniform(prog, "u_fog_height_falloff"), r3d_fog_falloff)
|
|
u_f(gpu_uniform(prog, "u_fog_base"), r3d_fog_base)
|
|
var cs = r3d_cloud_shadow
|
|
if Os.has_env("R3D_NOCLOUD") { cs = F_ZERO }
|
|
u_f(gpu_uniform(prog, "u_cloud_shadow"), cs)
|
|
u_f(gpu_uniform(prog, "u_time"), r3d_time)
|
|
}
|
|
|
|
# profiling switches (environment): R3D_NOSHADOW R3D_NOGI R3D_MSAA=n R3D_NOBLADES R3D_NOCARDS R3D_NOTREES R3D_NEAR=m
|
|
var r3d_test_frame: int = 0
|
|
var r3d_test_resize: int = 0
|
|
var r3d_no_shadow: bool = false
|
|
var r3d_no_trees: bool = false
|
|
var r3d_no_refl: bool = false
|
|
function r3d_env_flags() -> void {
|
|
r3d_no_shadow = Os.has_env("R3D_NOSHADOW")
|
|
r3d_no_trees = Os.has_env("R3D_NOTREES")
|
|
r3d_no_refl = Os.has_env("R3D_NOREFL")
|
|
if Os.has_env("R3D_DEBUG") { r3d_debug = true }
|
|
if Os.has_env("R3D_DBGSHADOW") { r3d_debug_shadow = true }
|
|
if Os.has_env("R3D_NOGI") { post_gi_strength = F_ZERO; post_ao_strength = F_ZERO; post_no_gi = true }
|
|
if Os.has_env("R3D_MSAA") { post_ms_samples = Text.to_int(Os.env("R3D_MSAA")) }
|
|
sc_skip_blade = Os.has_env("R3D_NOBLADES")
|
|
sc_skip_card = Os.has_env("R3D_NOCARDS")
|
|
sc_dbg_lod = Os.has_env("R3D_LODDBG")
|
|
if Os.has_env("R3D_ANISO") {
|
|
let a = Text.to_int(Os.env("R3D_ANISO"))
|
|
tex_anisotropy = 1.0
|
|
if a >= 2 { tex_anisotropy = 2.0 }
|
|
if a >= 4 { tex_anisotropy = 4.0 }
|
|
if a >= 8 { tex_anisotropy = 8.0 }
|
|
if a >= 16 { tex_anisotropy = 16.0 }
|
|
}
|
|
}
|
|
# Start the renderer in one call: the window, then every load step in order. A game that shows
|
|
# a loading screen calls r3d_open, draws its screen, and runs r3d_load_step itself between frames.
|
|
function r3d_init(w: int, h: int, title: string) -> bool {
|
|
if not r3d_open(w, h, title) { return false }
|
|
for i in 0 .. r3d_load_count() { if not r3d_load_step(i) { return false } }
|
|
return true
|
|
}
|
|
|
|
# the window and the graphics backend; nothing is baked yet, but a frame can be presented
|
|
function r3d_open(w: int, h: int, title: string) -> bool {
|
|
r3d_env_flags()
|
|
gpu_select()
|
|
if not gpu_open(w, h, title) { print("r3d: no OpenGL context"); return false }
|
|
if Os.has_env("R3D_NOVSYNC") { gpu_vsync(0) }
|
|
var renderer: string = gpu_renderer_name()
|
|
print(`r3d: {gl_w}x{gl_h} on {renderer}`)
|
|
prof_init()
|
|
cam_init(fr(gl_w, gl_h))
|
|
return true
|
|
}
|
|
|
|
# The load, as steps a loading screen can show between: 0 the sky and its image-based light,
|
|
# 1 - 4 the terrain (the height field and normals, its sun shadow, its materials, its patches and
|
|
# programs), 5 the shadow maps and the screen targets, 6 the scattered cover and the actors, 7 the
|
|
# grass and the sky pass. The order is r3d_init's.
|
|
function r3d_load_count() -> int { return 4 + TERRAIN_INIT_STEPS }
|
|
function r3d_load_step(i: int) -> bool {
|
|
let t = i - 1
|
|
if i == 0 {
|
|
if not sky_load(r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr") { return false }
|
|
daylight_init()
|
|
} else if t >= 0 and t < TERRAIN_INIT_STEPS {
|
|
if t == 0 {
|
|
if r3d_dem_path != null { terrain_use_dem(r3d_dem_path, r3d_dem_min, r3d_dem_max, r3d_dem_base, r3d_dem_ox, r3d_dem_oz) }
|
|
if r3d_ortho_path != null { terrain_use_ortho(r3d_ortho_path) }
|
|
}
|
|
terrain_init_step(t)
|
|
} else if i == 1 + TERRAIN_INIT_STEPS {
|
|
shadow_init()
|
|
post_init(gl_w, gl_h)
|
|
} else if i == 2 + TERRAIN_INIT_STEPS {
|
|
scatter_init()
|
|
actor_init()
|
|
} else if i == 3 + TERRAIN_INIT_STEPS {
|
|
grass_init()
|
|
r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "")
|
|
r3d_fog_density = fl(0.00014)
|
|
r3d_fog_falloff = fl(0.002)
|
|
r3d_ready = true
|
|
gpu_check("r3d init")
|
|
}
|
|
return true
|
|
}
|
|
|
|
function r3d_draw_sky() -> void {
|
|
gpu_depth_func(GL_LEQUAL)
|
|
gpu_depth_write(false)
|
|
gpu_cull(false)
|
|
let p = r3d_sky_prog
|
|
gpu_use_program(p)
|
|
r3d_bind_2d(p, "u_sky", 0, sky_tex)
|
|
sky_bind_rot(p)
|
|
sky_bind_lighting(p)
|
|
u_mat4(gpu_uniform(p, "u_inv_vp"), cam_inv_vp)
|
|
u_f(gpu_uniform(p, "u_sky_gain"), fl(0.95))
|
|
u_f(gpu_uniform(p, "u_sky_sat"), fl(1.35))
|
|
u_f(gpu_uniform(p, "u_time"), r3d_time)
|
|
mesh_draw(sky_fullscreen)
|
|
gpu_depth_write(true)
|
|
gpu_depth_func(GL_LESS)
|
|
}
|
|
|
|
# The end of the game's frame, whichever backend draws it: a screenshot first if one is wanted,
|
|
# then the present. A game calls these rather than Gl.swap / Gl.screenshot.
|
|
function r3d_screenshot(path: string) -> bool { return gpu_screenshot(path) }
|
|
function r3d_present() -> void { gpu_present() }
|
|
|
|
# the drawable changed size: the camera's aspect and every screen-sized target follow
|
|
function r3d_resize() -> void {
|
|
cam_aspect = fr(gl_w, gl_h)
|
|
cam_update()
|
|
post_free()
|
|
post_init(gl_w, gl_h)
|
|
if water_refl != null { target_free(water_refl); water_refl = null }
|
|
print(`r3d: resized to {gl_w}x{gl_h}`)
|
|
}
|
|
function r3d_frame(time: int) -> void {
|
|
gpu_glcheck_after("the time between frames")
|
|
outline_frame()
|
|
if not r3d_ready { return }
|
|
if gpu_resize_check() { r3d_resize() }
|
|
# R3D_RESIZE_AT=<frame>: rebuild every screen-sized buffer mid-run, as a window resize
|
|
# or a fullscreen change does. Headless has no window to resize, and this path is where
|
|
# a stale attachment or a texture freed twice shows up.
|
|
r3d_test_frame += 1
|
|
if r3d_test_resize == 0 and Os.has_env("R3D_RESIZE_AT") { r3d_test_resize = Text.to_int(Os.env("R3D_RESIZE_AT")) }
|
|
# R3D_RESIZE_AT=<n>: from frame n on, rebuild every screen-sized buffer every few
|
|
# frames at a different size, as dragging a window edge or entering fullscreen does.
|
|
if r3d_test_resize > 0 and r3d_test_frame >= r3d_test_resize and r3d_test_frame % 4 == 0 {
|
|
let step = (r3d_test_frame / 4) % 4
|
|
var w = 1920; var h = 1080
|
|
if step == 1 { w = 1440; h = 810 }
|
|
if step == 2 { w = 2560; h = 1440 }
|
|
if step == 3 { w = 1281; h = 721 }
|
|
gl_w = w; gl_h = h
|
|
r3d_resize()
|
|
}
|
|
r3d_time = time
|
|
# the frame's counters close here, before any of its own work: the window each of them
|
|
# covers is exactly one frame, from this point to the same point next time
|
|
prof_gen_frame()
|
|
prof_mark_start()
|
|
cam_begin_frame(post_frame, gl_w, gl_h)
|
|
# the height-field shadow rebakes as the light moves in steps (daylight), or with the sky yaw when there is no clock
|
|
if (not day_on and ter_shadow_yaw != sky_yaw) or ter_shadow_gen != day_gen {
|
|
ter_shadow_gen = day_gen
|
|
let t_bk = gl_now_us()
|
|
terrain_bake_shadow()
|
|
prof_bake_add(gl_now_us() - t_bk)
|
|
}
|
|
scatter_begin_frame()
|
|
prof_cpu_mark("shadow rebake")
|
|
stream_update_all()
|
|
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 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
|
|
# under the stands and the cards behind the front ones are rejected before lighting.
|
|
# It has to come straight after post_begin_scene: terrain_sun_prepare binds its own
|
|
# target, and depth drawn after it lands in the sun buffer, not the scene's.
|
|
if not r3d_prepass_off() {
|
|
prof_begin("foliage prepass")
|
|
gpu_color_write(false)
|
|
scatter_draw_depth()
|
|
gpu_color_write(true)
|
|
prof_end()
|
|
sc_prepass = true
|
|
}
|
|
prof_cpu_mark("foliage prepass")
|
|
prof_begin("terrain sun")
|
|
terrain_sun_prepare()
|
|
prof_end()
|
|
prof_begin("terrain")
|
|
terrain_draw()
|
|
prof_end()
|
|
prof_cpu_mark("terrain")
|
|
prof_begin("scene (vegetation)")
|
|
scene_draw()
|
|
prof_end()
|
|
sc_prepass = false
|
|
prof_cpu_mark("vegetation")
|
|
prof_begin("grass")
|
|
grass_draw()
|
|
prof_end()
|
|
prof_cpu_mark("grass")
|
|
prof_begin("sky")
|
|
r3d_draw_sky()
|
|
prof_end()
|
|
prof_begin("resolve MSAA")
|
|
post_resolve()
|
|
prof_end()
|
|
# transparent water over the resolved frame: it tests against the frame's own
|
|
# depth and reads a copy of it for the depth tint and soft shores
|
|
if water_on {
|
|
prof_begin("water surface")
|
|
post_capture_scene()
|
|
target_bind(post_hdr)
|
|
gpu_depth_test(true)
|
|
gpu_depth_func(GL_LESS)
|
|
water_draw(post_depth_copy.depth)
|
|
prof_end()
|
|
}
|
|
post_color = post_hdr.color
|
|
if not post_no_gi { prof_begin("SSAO/GI"); post_ssao_pass(); prof_end() }
|
|
if r3d_debug_max { tex_max(post_hdr.color, post_hdr.w, post_hdr.h, "hdr") }
|
|
prof_begin("bloom")
|
|
post_bloom_pass()
|
|
prof_end()
|
|
prof_begin("tonemap+exposure")
|
|
post_tonemap(post_color)
|
|
prof_end()
|
|
prof_cpu_mark("post")
|
|
prof_begin("prev-colour copy")
|
|
post_capture_prev()
|
|
prof_end()
|
|
prof_collect()
|
|
gpu_check("frame")
|
|
}
|