ludic/packages/ludic.render3d/terrain.ludic
Orkuncakilkaya a12f1b1201 render3d: the terrain as quantized tiles (LTT2) that every reader answers from, baked or cut
Heights as u16 over each 64 m tile's own minimum and step (a tile spanning 200 m steps 3 mm), normals
octahedral 8 + 8, the photograph RGB8, and the coarse level (2048: heights f32, normals, photograph)
- 30 + 32 + 48 + 16 + 8 + 12 MB, about 146 MB a map where the float tiles were 192 plus nothing coarse.
The writer puts the whole copy back to the quantized values as it goes, so the build's own queries,
the physics, the placements' bake and every machine read the same numbers; a tile read decodes them
into the pools the queries and the page pool already use.

terrain_tiles_bake(path, key, version, inputs_hash) writes a bake's file (ludic.base's LBAK header,
the tiles as its payload) from a made map; terrain_from_baked(path, key, version, half, ox, oz) opens
one at boot in place of terrain_use_dem / terrain_use_ortho, and terrain_init then generates nothing
(and bakes the sun's shadow from the coarse level unless terrain_shadow_from_bytes gave it). Without a
bake the cut writes the same format to <dir>/<key>.tiles and reads it back. The GPU's coarse level is
made from the file's coarse sections (the blit from the whole maps is gone).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:23:53 +03:00

870 lines
52 KiB
Text

# ============================================================================
# terrain.ludic — the landscape: a height map generated on the GPU (R32F),
# read back for placement queries, drawn as a lifted grid with four scanned
# PBR materials blended by slope, altitude and the track mask.
# ============================================================================
const TERRAIN_RES: int = 4096 # height-map texels per side (2 m over 8 km)
const TERRAIN_SHADOW_RES: int = 2048 # the baked height-field shadow / cloud mask
# CDLOD: the map is a quadtree of 32x32-cell patches; the leaf patch is 32 m (1 m cells)
const CD_G: int = 32 # cells per patch side
const CD_LEVELS: int = 9 # 32 m leaves .. 8192 m root
const CD_LEAVES: int = 256 # leaf patches per side (8192 / 32)
# The far tier compiled on its own (FAR_ONLY). A patch that lies entirely beyond the
# near/far split is drawn with it: same pixels, a shader small enough to run wide.
# 0 dry .. 1 soaked. The game sets it from the weather and lets it dry out.
function terrain_set_carpet(render3d_st: mut Render3dState, tex: int) -> void { render3d_st.ter_carpet = tex }
# Carve a lake bed below `level` inside the ellipse (cx, cz) ± (ex, ez); call before r3d_init.
function terrain_lake(render3d_st: mut Render3dState, level: float, cx: float, cz: float, ex: float, ez: float) -> void {
render3d_st.ter_lake_level = level; render3d_st.ter_lake_cx = cx; render3d_st.ter_lake_cz = cz; render3d_st.ter_lake_ex = ex; render3d_st.ter_lake_ez = ez
}
# Whether the generator carves that bed. A height map that already carries a shaped bed
# turns it off and keeps the lake for everything else (its line, its outline, its shore):
# the carve drops the bed metres within the survey's last 0.9 m of shore, which reads as a
# cut edge on a lake whose own bank was shaped to come down gently.
function terrain_lake_carve(render3d_st: mut Render3dState, on: bool) -> void { render3d_st.ter_lake_carve = on }
# The SEA's level, for the coast, the strand and the shoreline shading, when it is not the
# carved lake's. They were one number, which holds only while the lake is at sea level: a
# lake eighty metres up the valley drowned the whole coast toward its own surface. Unset,
# the sea is the lake exactly as before, so a map with one water line needs no call.
function terrain_sea(render3d_st: mut Render3dState, level: float) -> void { render3d_st.ter_sea_level = level; render3d_st.ter_sea_set = true }
# the level the generator scales the coast toward (it always read the lake's, set or not)
function ter_sea_gen(render3d_st: Render3dState) -> float { if render3d_st.ter_sea_set { return render3d_st.ter_sea_level }; return render3d_st.ter_lake_level }
# An island: land out to `r` from (cx, cz), then the terrain scaled down into the water
# over `fall` metres and on down to a shelf. Scaling rather than blending to a fixed bed is
# what makes the coastline come out of the terrain that is already there — low ground turns
# into beach and shallows, high ground into cliff. `r = 0` leaves the survey alone.
const TER_ISLE_NONE: int = 0
const TER_ISLE_RADIAL: int = 1
const TER_ISLE_COAST: int = 2
function terrain_island(render3d_st: mut Render3dState, cx: float, cz: float, r: float, fall: float) -> void {
render3d_st.ter_isle_cx = cx; render3d_st.ter_isle_cz = cz; render3d_st.ter_isle_r = r; render3d_st.ter_isle_fall = fall
render3d_st.ter_isle_mode = TER_ISLE_RADIAL
if r == 0.0 { render3d_st.ter_isle_mode = TER_ISLE_NONE }
}
# The other way to make an island, and the one a real survey usually wants: put the sea
# around the survey's OWN EDGE rather than cutting a circle out of the middle of it.
# Everything the data covers stays land; the outer `margin` metres are under water and the
# `fall` metres inside that are scaled down into it, exactly as terrain_island scales.
#
# The difference matters more than it sounds. Measuring a radius from a point in the middle
# of an 8 km mountain survey drowns most of the survey to make an island of the rest —
# you paid for the data and then threw two thirds of it away. Measuring inward from the
# boundary keeps all of it and puts the coast where the data runs out, which is also where
# a surveyor would tell you it runs out.
#
# The band is wobbled by low-frequency noise so the coastline is headlands and bays rather
# than the square the data arrived in. `margin = 0` leaves the survey alone.
function terrain_coast(render3d_st: mut Render3dState, cx: float, cz: float, margin: float, fall: float) -> void {
render3d_st.ter_isle_cx = cx; render3d_st.ter_isle_cz = cz; render3d_st.ter_isle_r = margin; render3d_st.ter_isle_fall = fall
render3d_st.ter_isle_mode = TER_ISLE_COAST
if margin == 0.0 { render3d_st.ter_isle_mode = TER_ISLE_NONE }
}
# Use a real place: a 16-bit PNG height map plus its elevation range (metres). The
# elevation `base` becomes y = 0; `ox`/`oz` put the map's centre in the world.
# the photograph's colour at world (x, z): packed 0xRRGGBB (0 outside the map)
function terrain_ortho(render3d_st: mut Render3dState, x: float, z: float) -> int {
if render3d_st.ter_ortho_px == null and render3d_st.tt_file == null { return 0 }
if render3d_st.ter_o_scale == 0.0 { render3d_st.ter_o_scale = float(render3d_st.ter_ortho_w) / float(render3d_st.TERRAIN_HALF * 2) }
let scale = render3d_st.ter_o_scale
var ix = int(Math.floor((x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale))
var iz = int(Math.floor((z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) * scale))
if ix < 0 { ix = 0 }; if iz < 0 { iz = 0 }
if ix > render3d_st.ter_ortho_w - 1 { ix = render3d_st.ter_ortho_w - 1 }; if iz > render3d_st.ter_ortho_w - 1 { iz = render3d_st.ter_ortho_w - 1 }
let o = (iz * render3d_st.ter_ortho_w + ix) * render3d_st.ter_ortho_c
if render3d_st.ter_ortho_px == null { return ter_o(render3d_st, ix, iz) }
return (render3d_st.ter_ortho_px[o] << 16) | (render3d_st.ter_ortho_px[o + 1] << 8) | render3d_st.ter_ortho_px[o + 2]
}
# The three classifiers below all read the same pixel. A caller that wants more than
# one should fetch the colour once with terrain_ortho() and use the *_of forms — the
# cover generator tests all three per candidate, so this is three fetches saved out of
# every four in the hottest loop in the program.
function ortho_green_of(c: int) -> float {
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
var v = g - max(r, b)
if v < 0 { v = 0 }
return Math.min(float(v) / 22.0, 1.0)
}
function ortho_scree_of(c: int) -> float {
if c == 0 { return 0.0 }
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
let mx = max(r, max(g, b))
if g - max(r, b) > 2 or mx < 60 { return 0.0 }
return 1.0
}
function ortho_forest_of(c: int) -> float {
if c == 0 { return 0.0 }
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
let mx = max(r, max(g, b))
if g - max(r, b) < 3 { return 0.0 }
if mx <= 80 { return 1.0 }
if mx <= 105 { return 0.5 }
return 0.0
}
# how green the ground is in the photograph (0..1 float bits): meadow / forest vs rock, scree, water
function terrain_ortho_green(render3d_st: mut Render3dState, x: float, z: float) -> float {
let c = terrain_ortho(render3d_st, x, z)
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
var v = g - max(r, b) # green excess
if v < 0 { v = 0 }
return Math.min(float(v) / 22.0, 1.0)
}
# how grey and mid-bright (scree / pebbles / bare rock) the photograph is there (0..1)
# Bare ground only where most of a 50 m neighbourhood is bare: a single 10 m trail pixel
# must not place a boulder or bar a tree.
function terrain_ortho_scree(render3d_st: mut Render3dState, x: float, z: float) -> float {
var votes = 0
for j in 0 .. 5 { for i in 0 .. 5 { if ortho_scree_of(terrain_ortho(render3d_st, x + float((i - 2) * 10), z + float((j - 2) * 10))) != 0.0 { votes += 1 } } }
if votes >= 15 { return 1.0 }
return 0.0
}
function terrain_ortho_scree_pixel(render3d_st: mut Render3dState, x: float, z: float) -> float {
let c = terrain_ortho(render3d_st, x, z)
if c == 0 { return 0.0 }
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
let mx = max(r, max(g, b))
# bare ground: not green-dominant (grey scree, the maroon rock, the moraine's tan gravel), lit enough not to be water
if g - max(r, b) > 2 or mx < 60 { return 0.0 }
return 1.0
}
# dense conifer forest in the photograph: green-dominant and dark (the meadows are brighter)
function terrain_ortho_forest(render3d_st: mut Render3dState, x: float, z: float) -> float {
let c = terrain_ortho(render3d_st, x, z)
if c == 0 { return 0.0 }
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
let mx = max(r, max(g, b))
if g - max(r, b) < 3 { return 0.0 }
if mx <= 80 { return 1.0 }
if mx <= 105 { return 0.5 }
return 0.0
}
# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM)
function terrain_use_ortho(render3d_st: mut Render3dState, path: string) -> void {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TERRAIN
terrain_use_ortho__t(render3d_st, path)
render3d_st.gvk_tag = was
}
function terrain_use_ortho__t(render3d_st: mut Render3dState, path: string) -> void {
let px = png_decode(render3d_st, path)
if px == null { return }
# a map set up over another lets go of the last one's photograph first
if render3d_st.ter_ortho_px != null { free(render3d_st.ter_ortho_px) }
if render3d_st.ter_ortho_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_ortho_tex) }
render3d_st.ter_ortho_px = px
render3d_st.ter_ortho_w = render3d_st.tex_w
render3d_st.ter_ortho_c = render3d_st.tex_channels
render3d_st.ter_ortho_tex = tex_upload(render3d_st, px, true, true)
gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_ortho_tex)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
# a photograph that comes after the height field: now both are here, they can go to tiles
if render3d_st.cd_range != null { terrain_tiles_cut(render3d_st) }
}
# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM)
function terrain_use_dem(render3d_st: mut Render3dState, path: string, emin: float, emax: float, base: float, ox: float, oz: float) -> void {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TERRAIN
terrain_use_dem__t(render3d_st, path, emin, emax, base, ox, oz)
render3d_st.gvk_tag = was
}
function terrain_use_dem__t(render3d_st: mut Render3dState, path: string, emin: float, emax: float, base: float, ox: float, oz: float) -> void {
if render3d_st.ter_dem_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_dem_tex) }
render3d_st.ter_dem_tex = tex_load(render3d_st, path, false)
render3d_st.ter_dem_min = emin; render3d_st.ter_dem_max = emax; render3d_st.ter_dem_base = base
render3d_st.ter_ox = ox; render3d_st.ter_oz = oz
gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_dem_tex)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
}
# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM)
function terrain_generate(render3d_st: mut Render3dState) -> void {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TERRAIN
terrain_generate__t(render3d_st)
render3d_st.gvk_tag = was
}
function terrain_generate__t(render3d_st: mut Render3dState) -> void {
var defs = ""
if render3d_st.ter_dem_tex != 0 { defs = "#define DEM\n" }
if render3d_st.ter_smooth { defs = "#define SMOOTH\n" }
let p = r3d_program(render3d_st, "fullscreen.vert", "heightgen.frag", defs)
# a map generated over another: its 256 MB height texture goes first
if render3d_st.ter_height_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_height_tex); render3d_st.ter_height_tex = 0 }
render3d_st.ter_height_tex = tex_target(render3d_st, TERRAIN_RES, TERRAIN_RES, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR)
let fbo = gpu_fb_new(render3d_st)
gpu_fb_bind(render3d_st, fbo)
gpu_fb_color(render3d_st, 0, render3d_st.ter_height_tex)
gpu_viewport(render3d_st, 0, 0, TERRAIN_RES, TERRAIN_RES)
gpu_depth_test(render3d_st, false)
gpu_use_program(render3d_st, p)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF))
if render3d_st.ter_dem_tex != 0 {
r3d_bind_2d(render3d_st, p, "u_dem", 0, render3d_st.ter_dem_tex)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_dem_min"), render3d_st.ter_dem_min)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_dem_max"), render3d_st.ter_dem_max)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_dem_base"), render3d_st.ter_dem_base)
u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_origin"), render3d_st.ter_ox, render3d_st.ter_oz)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_lake"), render3d_st.ter_lake_cx, render3d_st.ter_lake_cz, render3d_st.ter_lake_ex, render3d_st.ter_lake_ez)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_lake_level"), render3d_st.ter_lake_level)
var carve = 1.0
if not render3d_st.ter_lake_carve { carve = 0.0 }
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_lake_carve"), carve)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_sea_level"), ter_sea_gen(render3d_st))
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_isle"), render3d_st.ter_isle_cx, render3d_st.ter_isle_cz, render3d_st.ter_isle_r, render3d_st.ter_isle_fall)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_isle_mode"), float(render3d_st.ter_isle_mode))
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_dem_blur"), float(render3d_st.ter_dem_blur))
}
mesh_draw(render3d_st, render3d_st.sky_fullscreen)
# second pass: R = height, GBA = the smooth surface normal, baked once (ternormal.frag)
# the height stays in its own R32F (physics and every placement read it at full precision); the
# baked normal goes beside it as x and z in RG16F, y rebuilt where it is read - 128 MB where one
# RGBA32F carrying both was 256 (plan 23 of maroon-lake)
let raw = render3d_st.ter_height_tex
if render3d_st.ter_normal_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_normal_tex) }
render3d_st.ter_normal_tex = tex_target(render3d_st, TERRAIN_RES, TERRAIN_RES, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR)
gpu_fb_color(render3d_st, 0, render3d_st.ter_normal_tex)
let pn = r3d_program(render3d_st, "fullscreen.vert", "ternormal.frag", "")
gpu_use_program(render3d_st, pn)
r3d_bind_2d(render3d_st, pn, "u_src", 0, raw)
u_f(render3d_st, gpu_uniform(render3d_st, pn, "u_half"), float(render3d_st.TERRAIN_HALF))
mesh_draw(render3d_st, render3d_st.sky_fullscreen)
gpu_program_free(render3d_st, pn)
# read the heights back for placement, into the array a previous map had: always the same size,
# and made anew on every build it was 64 MB lost per world swap
if render3d_st.ter_heights == null { render3d_st.ter_heights = floats(TERRAIN_RES * TERRAIN_RES) }
gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_height_tex)
gpu_pixel_store(render3d_st, GL_PACK_ALIGNMENT, 4)
gpu_tex_read(render3d_st, GPU_TEX2D, GL_RED, GL_FLOAT, data_of(render3d_st.ter_heights))
gpu_fb_bind(render3d_st, 0)
gpu_fb_free(render3d_st, fbo)
gpu_program_free(render3d_st, p)
# the survey is read only here: kept, it was 44 MB (R16 with mips) for the life of the map. A
# world swap loads its own again (terrain_reload), and nothing else generates
if render3d_st.ter_dem_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_dem_tex); render3d_st.ter_dem_tex = 0 }
gpu_check(render3d_st, "terrain generate")
}
# The height field for shaders that place things on the ground (model.vert's u_ground)
function terrain_bind_height(render3d_st: mut Render3dState, p: int) -> void {
tp_bind(render3d_st, p)
r3d_bind_2d(render3d_st, p, "u_ts_height", 5, render3d_st.ter_height_tex)
r3d_bind_2d(render3d_st, p, "u_ter_normal", 6, render3d_st.ter_normal_tex)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_ts_half"), float(render3d_st.TERRAIN_HALF))
u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_ts_origin"), render3d_st.ter_ox, render3d_st.ter_oz)
}
# Bake the height-field sun shadow (see tershadow.frag). Cheap enough to redo whenever
# the sun moves; r3d_frame calls it again when sky_set_yaw has changed the yaw.
function terrain_bake_shadow(render3d_st: mut Render3dState) -> void {
if render3d_st.sun_dir == null { return }
# the lowest lit height needs 32 bits (a receiver 3000 m up compares against it to a quarter metre);
# the occluder's distance and the cloud mask do not, and go beside it in RG16F - 32 MB where one
# RGBA32F carrying all three (and an unused fourth) was 64
if render3d_st.ter_shadow_tex == 0 { render3d_st.ter_shadow_tex = tex_target(render3d_st, TERRAIN_SHADOW_RES, TERRAIN_SHADOW_RES, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR) }
if render3d_st.ter_shadow_aux == 0 { render3d_st.ter_shadow_aux = tex_target(render3d_st, TERRAIN_SHADOW_RES, TERRAIN_SHADOW_RES, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR) }
if render3d_st.ter_shadow_prog == 0 { render3d_st.ter_shadow_prog = r3d_program(render3d_st, "fullscreen.vert", "tershadow.frag", "#define NOISE_ONLY\n") }
if render3d_st.ter_shadow_prog_aux == 0 { render3d_st.ter_shadow_prog_aux = r3d_program(render3d_st, "fullscreen.vert", "tershadow.frag", "#define NOISE_ONLY\n#define TS_AUX\n") }
# two passes, one target each: a pipeline takes one colour format for all its targets, and the two
# are R32F and RG16F (drawn together, the second's writes went nowhere and nothing was lit)
terrain_bake_pass(render3d_st, render3d_st.ter_shadow_prog, render3d_st.ter_shadow_tex)
terrain_bake_pass(render3d_st, render3d_st.ter_shadow_prog_aux, render3d_st.ter_shadow_aux)
render3d_st.ter_shadow_yaw = render3d_st.sky_yaw
gpu_check(render3d_st, "terrain shadow bake")
}
# one of the bake's two targets drawn: the march is the same, the define picks what it writes
function terrain_bake_pass(render3d_st: mut Render3dState, p: int, target: int) -> void {
let fbo = gpu_fb_new(render3d_st)
gpu_fb_bind(render3d_st, fbo)
gpu_fb_color(render3d_st, 0, target)
gpu_viewport(render3d_st, 0, 0, TERRAIN_SHADOW_RES, TERRAIN_SHADOW_RES)
gpu_depth_test(render3d_st, false)
gpu_blend(render3d_st, false)
gpu_use_program(render3d_st, p)
tp_bind(render3d_st, p)
r3d_bind_2d(render3d_st, p, "u_height", 0, render3d_st.ter_height_tex)
r3d_bind_2d(render3d_st, p, "u_ter_normal", 7, render3d_st.ter_normal_tex)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF))
u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_sun"), render3d_st.sun_dir)
mesh_draw(render3d_st, render3d_st.sky_fullscreen)
gpu_fb_bind(render3d_st, 0)
gpu_fb_free(render3d_st, fbo)
}
# height at world (x, z) — float bits, bilinear over the CPU copy
# The height and photograph scales are constants, but they were being recomputed —
# a fixed-point divide — on every call, and the cover generator calls terrain_height
# five times per candidate (once directly, four more inside slope_at) across hundreds
# of thousands of candidates per chunk. Hoisted, they cost nothing.
function terrain_height(render3d_st: mut Render3dState, x: float, z: float) -> float {
if not ter_present(render3d_st) { return 0.0 } # a plate (r3d_plate_mode): flat at y = 0
if render3d_st.ter_h_scale == 0.0 { render3d_st.ter_h_scale = float(TERRAIN_RES) / float(render3d_st.TERRAIN_HALF * 2) }
let scale = render3d_st.ter_h_scale
let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale
let fz = (z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) * scale
var ix = int(Math.floor(fx)); var iz = int(Math.floor(fz))
if ix < 0 { ix = 0 }; if iz < 0 { iz = 0 }
if ix > TERRAIN_RES - 2 { ix = TERRAIN_RES - 2 }; if iz > TERRAIN_RES - 2 { iz = TERRAIN_RES - 2 }
let tx = Math.clamp(fx - float(ix), 0.0, 1.0)
let tz = Math.clamp(fz - float(iz), 0.0, 1.0)
let h00 = ter_h(render3d_st, ix, iz)
let h10 = ter_h(render3d_st, ix + 1, iz)
let h01 = ter_h(render3d_st, ix, iz + 1)
let h11 = ter_h(render3d_st, ix + 1, iz + 1)
return Math.lerp(Math.lerp(h00, h10, tx), Math.lerp(h01, h11, tx), tz)
}
# the same for Q16.16 callers
function terrain_height_fx(render3d_st: mut Render3dState, x: fixed, z: fixed) -> fixed { return fixed(terrain_height(render3d_st, float(x), float(z))) }
# The height the terrain is DRAWN at: the cubic B-spline of the texels (heightSmooth in
# terrain.vert), not the bilinear read above. The two differ by up to half a metre on
# rough ground, which is the difference between a character standing on the meadow
# and one buried to the knee in it. Sixteen taps; for things that move, not for the
# thousands of placement queries a chunk makes.
function terrain_height_smooth(render3d_st: mut Render3dState, x: float, z: float) -> float {
if not ter_present(render3d_st) { return 0.0 }
if render3d_st.ter_h_scale == 0.0 { render3d_st.ter_h_scale = float(TERRAIN_RES) / float(render3d_st.TERRAIN_HALF * 2) }
let scale = render3d_st.ter_h_scale
let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale - 0.5
let fz = (z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) * scale - 0.5
let ix = int(Math.floor(fx)); let iz = int(Math.floor(fz))
let tx = Math.clamp(fx - float(ix), 0.0, 1.0)
let tz = Math.clamp(fz - float(iz), 0.0, 1.0)
# the four cubic B-spline weights of a fraction, over texels i-1 .. i+2
for a in 0 .. 2 {
var t = tx
if a == 1 { t = tz }
let t2 = t * t; let t3 = t2 * t
let one_t = 1.0 - t
let w0 = one_t * one_t * one_t / 6.0
let w1 = (4.0 - 6.0 * t2 + 3.0 * t3) / 6.0
let w3 = t3 / 6.0
let w2 = 1.0 - w0 - w1 - w3
render3d_st.ter_bw[a * 4] = w0; render3d_st.ter_bw[a * 4 + 1] = w1; render3d_st.ter_bw[a * 4 + 2] = w2; render3d_st.ter_bw[a * 4 + 3] = w3
}
var h = 0.0
for j in 0 .. 4 {
var rz = iz - 1 + j
if rz < 0 { rz = 0 }; if rz > TERRAIN_RES - 1 { rz = TERRAIN_RES - 1 }
var row = 0.0
for i in 0 .. 4 {
var rx = ix - 1 + i
if rx < 0 { rx = 0 }; if rx > TERRAIN_RES - 1 { rx = TERRAIN_RES - 1 }
row = row + render3d_st.ter_bw[i] * ter_h(render3d_st, rx, rz)
}
h = h + render3d_st.ter_bw[4 + j] * row
}
return h
}
# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM)
function terrain_load_textures(render3d_st: mut Render3dState) -> void {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TERRAIN
terrain_load_textures__t(render3d_st)
render3d_st.gvk_tag = was
}
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
function terrain_load_textures__t(render3d_st: mut Render3dState) -> void {
render3d_st.ter_tex = words(15)
let a = render3d_st.r3d_assets + "/textures/"
render3d_st.ter_tex[0] = tex_load(render3d_st, a + "aerial_grass_rock_diff_2k.png", true)
render3d_st.ter_tex[1] = tex_load(render3d_st, a + "aerial_grass_rock_nor_gl_2k.png", false)
render3d_st.ter_tex[2] = tex_load(render3d_st, a + "aerial_grass_rock_arm_2k.png", false)
render3d_st.ter_tex[3] = tex_load(render3d_st, a + "grass_path_2_diff_2k.png", true)
render3d_st.ter_tex[4] = tex_load(render3d_st, a + "grass_path_2_nor_gl_2k.png", false)
render3d_st.ter_tex[5] = tex_load(render3d_st, a + "grass_path_2_arm_2k.png", false)
render3d_st.ter_tex[6] = tex_load(render3d_st, a + "gray_rocks_diff_2k.png", true)
render3d_st.ter_tex[7] = tex_load(render3d_st, a + "gray_rocks_nor_gl_2k.png", false)
render3d_st.ter_tex[8] = tex_load(render3d_st, a + "gray_rocks_arm_2k.png", false)
render3d_st.ter_tex[9] = tex_load(render3d_st, a + "snow_02_diff_2k.png", true)
render3d_st.ter_tex[10] = tex_load(render3d_st, a + "snow_02_nor_gl_2k.png", false)
render3d_st.ter_tex[11] = tex_load(render3d_st, a + "snow_02_arm_2k.png", false)
render3d_st.ter_tex[12] = tex_load(render3d_st, a + "aerial_grass_rock_disp_2k.png", false)
render3d_st.ter_tex[13] = tex_load(render3d_st, a + "cliff_side_diff_2k.png", true)
render3d_st.ter_tex[14] = tex_load(render3d_st, a + "cliff_side_nor_gl_2k.png", false)
if r3d_env_has(render3d_st, "R3D_TEXDBG") { for i in 0 .. 15 { print(`ter_tex[{string(i)}] = {string(render3d_st.ter_tex[i])}`) } }
}
# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM)
function terrain_init(render3d_st: mut Render3dState) -> void {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TERRAIN
terrain_init__t(render3d_st)
render3d_st.gvk_tag = was
}
function terrain_init__t(render3d_st: mut Render3dState) -> void {
tp_dummies(render3d_st)
for i in 0 .. TERRAIN_INIT_STEPS { terrain_init_step(render3d_st, i) }
}
# The terrain's start-up as steps a loading screen can show between, in terrain_init's order:
# 0 the height field and its normals (and the read-back for placement), 1 the sun's height-field
# shadow, 2 the ground's materials, 3 the patch tree and the terrain programs.
const TERRAIN_INIT_STEPS: int = 4
function terrain_init_step(render3d_st: mut Render3dState, i: int) -> void {
if i == 0 { if not render3d_st.tt_baked { terrain_generate(render3d_st) }; return }
# a baked terrain whose shadow came from its own bake (terrain_shadow_from_bytes) keeps it
if i == 1 { if not (render3d_st.tt_baked and render3d_st.ter_shadow_tex != 0) { terrain_bake_shadow(render3d_st) }; return }
if i == 2 { terrain_load_textures(render3d_st); return }
terrain_init_finish(render3d_st)
}
# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM)
function terrain_init_finish(render3d_st: mut Render3dState) -> void {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TERRAIN
terrain_init_finish__t(render3d_st)
render3d_st.gvk_tag = was
}
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
function terrain_init_finish__t(render3d_st: mut Render3dState) -> void {
cdlod_init(render3d_st)
render3d_st.ter_wire = r3d_env_has(render3d_st, "R3D_WIRE")
render3d_st.ter_force_far = r3d_env_has(render3d_st, "R3D_TFARONLY")
render3d_st.ter_no_split = r3d_env_has(render3d_st, "R3D_NOSPLIT")
render3d_st.ter_force_near = r3d_env_has(render3d_st, "R3D_TNEARONLY")
render3d_st.ter_skip = r3d_env_has(render3d_st, "R3D_NOTERRAIN")
var defs = ""
if render3d_st.r3d_debug_shadow { defs = "#define DEBUG_SHADOW\n" }
if r3d_env_has(render3d_st, "R3D_DEBUG_MAT") { defs = "#define DEBUG_MAT\n" }
if r3d_env_has(render3d_st, "R3D_DEBUG_WIND") { defs = "#define DEBUG_WIND\n" }
if r3d_env_has(render3d_st, "R3D_DEBUG_NRM") { defs = "#define DEBUG_NRM\n" }
if r3d_env_has(render3d_st, "R3D_DEBUG_ALB") { defs = "#define DEBUG_ALB\n" }
# Elimination profiling. Measure these by FRAME TIME (prof_ft_report), not by the
# per-pass GPU timers: this driver's timer queries attribute a pass's fragment work
# almost arbitrarily, and will happily report a pass at a tenth of its cost.
# R3D_TFAST=1 the ground reads no sun visibility =2 every noise field at its mean
# =3 no scanned material taps =4 no survey photograph
# =5 the detailed tier at every distance =20 no photograph grain
# R3D_NOTERRAIN skips the ground entirely (what it costs); R3D_TNEARONLY / R3D_TFARONLY
# draw every patch with one tier's program (what each tier costs over a whole frame);
# R3D_NOSPLIT goes back to the single program that holds both tiers.
if r3d_env_has(render3d_st, "R3D_TFAST") { defs = defs + "#define TFAST_" + r3d_env(render3d_st, "R3D_TFAST") + "\n" }
# Vulkan reads the cascades inside the ground's own shader and draws no separate sun pass
# (terrain.frag, SUN_INLINE): one rasterisation of the patches instead of two, in the frame and in
# the reflection. R3D_SUN_PASS=1 keeps the pass there too, for comparing.
render3d_st.ter_sun_inline = render3d_st.gpu_kind == GPU_VK and not r3d_env_has(render3d_st, "R3D_SUN_PASS")
if render3d_st.ter_sun_inline { defs = defs + "#define SUN_INLINE\n" }
render3d_st.ter_prog = r3d_program(render3d_st, "terrain.vert", "terrain.frag", defs)
render3d_st.ter_prog_far = r3d_program(render3d_st, "terrain.vert", "terrain.frag", defs + "#define FAR_ONLY\n")
render3d_st.ter_prog_near = r3d_program(render3d_st, "terrain.vert", "terrain.frag", defs + "#define NEAR_ONLY\n")
render3d_st.ter_sun_prog = r3d_program(render3d_st, "terrain.vert", "tersun.frag", "")
render3d_st.ter_far_split = 200.0
if r3d_env_has(render3d_st, "R3D_TFAR") { render3d_st.ter_far_split = float(Text.to_int(r3d_env(render3d_st, "R3D_TFAR"))) }
render3d_st.ter_far_band = 60.0
if r3d_env_has(render3d_st, "R3D_TBAND") { render3d_st.ter_far_band = float(Text.to_int(r3d_env(render3d_st, "R3D_TBAND"))) }
render3d_st.ter_snow_line = 880.0
gpu_check(render3d_st, "terrain init")
}
# draw into the current cascade with the given light view-projection
# The terrain no longer casts into the shadow map: it shadows itself by marching its
# own height field in terrain.frag, which cannot produce the self-shadow grid a depth
# map does, and it saves drawing the whole grid five times a frame.
function terrain_draw_shadow(light_vp: floats) -> void {
}
# Every per-frame uniform of one terrain program. Both tiers are bound up front so
# selection can switch between them per patch without re-binding anything but the node.
function terrain_bind_prog(render3d_st: mut Render3dState, p: int) -> void {
gpu_use_program(render3d_st, p)
tp_bind(render3d_st, p)
r3d_bind_2d(render3d_st, p, "u_height", 0, render3d_st.ter_height_tex)
r3d_bind_2d(render3d_st, p, "u_ter_normal", 7, render3d_st.ter_normal_tex)
r3d_bind_2d(render3d_st, p, "u_grass_d", 1, render3d_st.ter_tex[0]); r3d_bind_2d(render3d_st, p, "u_grass_n", 2, render3d_st.ter_tex[1]); r3d_bind_2d(render3d_st, p, "u_grass_a", 3, render3d_st.ter_tex[2])
# The cliff maps went with the dead cliff sample. Binding textures for uniforms the
# shader no longer declares leaves those units pointing at nothing, which the driver
# reports as an unloadable sampler and resolves as a zero texture.
# A scene with no photograph still has to bind something valid here: sampler unit
# pointed at texture 0 is an incomplete texture, which the driver reports as
# unloadable and which poisons sampling for the rest of the unit's stage.
var orthotex = render3d_st.ter_ortho_tex
if orthotex == 0 { orthotex = render3d_st.ter_tex[0] }
r3d_bind_2d(render3d_st, p, "u_ortho", 4, orthotex)
var oon = 0.0
if render3d_st.ter_ortho_tex != 0 { oon = 1.0 }
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_ortho_on"), oon)
r3d_bind_2d(render3d_st, p, "u_rock_d", 7, render3d_st.ter_tex[6]); r3d_bind_2d(render3d_st, p, "u_rock_n", 8, render3d_st.ter_tex[7]); r3d_bind_2d(render3d_st, p, "u_rock_a", 9, render3d_st.ter_tex[8])
r3d_bind_2d(render3d_st, p, "u_snow_d", 10, render3d_st.ter_tex[9])
if render3d_st.ter_carpet != 0 { r3d_bind_2d(render3d_st, p, "u_carpet", 11, render3d_st.ter_carpet); u_f(render3d_st, gpu_uniform(render3d_st, p, "u_carpet_on"), 1.0) }
else { r3d_bind_2d(render3d_st, p, "u_carpet", 11, render3d_st.ter_tex[0]); u_f(render3d_st, gpu_uniform(render3d_st, p, "u_carpet_on"), 0.0) }
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF))
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_texel"), 1.0 / float(TERRAIN_RES))
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_snow_line"), render3d_st.ter_snow_line)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_wet"), render3d_st.ter_wet)
var lake = -100000.0
if render3d_st.ter_lake_ex != 0.0 { lake = render3d_st.ter_lake_level }
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_lake_level"), lake)
# the shoreline, forest and scree gates read the sea; unset it is what they always read
var sea = lake
if render3d_st.ter_sea_set { sea = render3d_st.ter_sea_level }
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_sea_level"), sea)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_lake"), render3d_st.ter_lake_cx, render3d_st.ter_lake_cz, render3d_st.ter_lake_ex, render3d_st.ter_lake_ez)
u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_view"), render3d_st.cam_view)
u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_proj"), render3d_st.cam_proj)
u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_origin"), render3d_st.ter_ox, render3d_st.ter_oz)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_far_split"), render3d_st.ter_far_split)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_far_band"), render3d_st.ter_far_band)
sky_bind_lighting(render3d_st, p)
shadow_bind(render3d_st, p)
fog_bind(render3d_st, p)
# GROUND IS NOT A MIRROR, and this has to come AFTER fog_bind, which hands every
# program u_spec_scale = 1. That is right for water and for a varnished prop and wrong
# for a hillside: the image-based specular lays a broad reflection of a bright sky over
# every square metre of rock and meadow and washes them toward the sky's own colour.
# It is why the range named for the colour of its rock rendered pale lilac rather than
# maroon - the maroon was under a sheet of reflected sky. Foliage already gets 0.05.
#
# Set before fog_bind it measured as EXACTLY zero pixels changed, which is the same
# shape of mistake as setting r3d_fog_scale before gfx_apply: the value was right and
# something downstream put it back.
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_spec_scale"), 0.22)
u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_cam_pos"), render3d_st.cam_pos)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_grid"), float(CD_G))
# The ground reads its sun visibility out of the buffer tersun.frag filled, and has no
# use for the cascade array shadow_bind just put on this unit; leaving both bound under
# one unit is undefined ground, so the array comes off first.
# (With the read inline, the array shadow_bind put there is exactly what the ground wants.)
if not render3d_st.ter_sun_inline {
gpu_tex_unit(render3d_st, 15)
gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, 0)
r3d_bind_2d(render3d_st, p, "u_sunshadow", 15, render3d_st.ter_sun_tex)
}
}
# The visibility buffer for the size being drawn into. The reflection is rendered at its
# own (smaller) size, so it keeps its own.
#
# Neither owns a depth buffer. The pass BORROWS the depth the frame is about to be drawn
# with, so its rasterisation doubles as a depth prepass — and a borrowed texture must
# never be written into the Target, because a Target deletes whatever its `depth` names
# when it is freed. Storing it there deleted the frame's own depth buffer on the first
# resize (post_init had already made the replacement, and GL hands the freed name straight
# back, so the new one was deleted instead of the old). The scene framebuffer lost its
# depth attachment, the sky's fullscreen quad had nothing left to fail against, and it
# painted over the whole valley — with "gl error 1286" every frame from the passes whose
# attachment now named a deleted texture.
function terrain_sun_target(render3d_st: mut Render3dState, w: int, h: int) -> Target {
var t = render3d_st.ter_sun_tgt
if render3d_st.ter_reflect { t = render3d_st.ter_sun_refl }
if t == null or t.w != w or t.h != h {
target_free(render3d_st, t) # owns its colour, and nothing else
t = target_new(render3d_st, w, h, GL_R8, GL_RED, GL_UNSIGNED_BYTE, false, GL_NEAREST)
if render3d_st.ter_reflect { render3d_st.ter_sun_refl = t } else { render3d_st.ter_sun_tgt = t }
}
return t
}
# Rasterise the patches once with the small shader that reads the cascades (tersun.frag).
function terrain_sun_pass(render3d_st: mut Render3dState, w: int, h: int, depth: int) -> Target {
let t = terrain_sun_target(render3d_st, w, h)
# Attach the frame's depth afresh every pass. It is a different texture every time the
# screen-sized buffers are rebuilt — a resize, a fullscreen change — and an attachment
# naming a texture that has been deleted leaves this framebuffer incomplete, which is
# an error per draw and a pass that silently does nothing. One call a pass is cheaper
# than any scheme for noticing.
target_bind(render3d_st, t)
gpu_fb_depth(render3d_st, depth)
if not render3d_st.ter_sun_checked {
render3d_st.ter_sun_checked = true
let st = gpu_fb_status(render3d_st)
if st != GL_FRAMEBUFFER_COMPLETE { terrain_say_sun_fbo(st) }
}
gpu_depth_test(render3d_st, true)
gpu_depth_func(render3d_st, GL_LESS)
gpu_depth_write(render3d_st, true)
# the depth is the frame's own and was cleared with it; only the visibility is cleared
gpu_clear_color(render3d_st, 1.0, 1.0, 1.0, 1.0) # unshadowed where nothing is drawn
gpu_clear(render3d_st, GL_COLOR_BUFFER_BIT)
let p = render3d_st.ter_sun_prog
gpu_use_program(render3d_st, p)
tp_bind(render3d_st, p)
r3d_bind_2d(render3d_st, p, "u_height", 0, render3d_st.ter_height_tex)
r3d_bind_2d(render3d_st, p, "u_ter_normal", 7, render3d_st.ter_normal_tex)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF))
u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_view"), render3d_st.cam_view)
u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_proj"), render3d_st.cam_proj)
u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_origin"), render3d_st.ter_ox, render3d_st.ter_oz)
u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_cam_pos"), render3d_st.cam_pos)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_grid"), float(CD_G))
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_far_split"), render3d_st.ter_far_split)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_far_band"), render3d_st.ter_far_band)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_clip_y"), render3d_st.r3d_clip_y)
shadow_bind(render3d_st, p)
sky_bind_lighting(render3d_st, p)
render3d_st.ter_sun_pass = true
gpu_mesh_bind(render3d_st, render3d_st.cd_mesh)
cdlod_select(render3d_st, CD_LEVELS - 1, 0, 0)
render3d_st.ter_sun_pass = false
if r3d_env_has(render3d_st, "R3D_DUMP_SUN") and not render3d_st.ter_sun_dumped and not render3d_st.ter_reflect { render3d_st.ter_sun_dumped = true; tex_dump(render3d_st, t.color, w, h, "build/dbg_sun.ppm") }
return t
}
function terrain_sun_prepare(render3d_st: mut Render3dState) -> void {
if render3d_st.ter_sun_inline { return }
render3d_st.ter_sun_tex = terrain_sun_pass(render3d_st, render3d_st.post_w, render3d_st.post_h, render3d_st.post_hdr.depth).color
render3d_st.ter_sun_done = true
}
function terrain_draw(render3d_st: mut Render3dState) -> void {
if render3d_st.ter_skip { return }
# The sun visibility first, into its own buffer; the shading pass looks it up per pixel.
# The pass binds its own framebuffer, so the caller's target is restored afterwards —
# the reflection's, or the scene's, without disturbing what is already drawn in it.
var vw = render3d_st.post_w
var vh = render3d_st.post_h
if render3d_st.ter_reflect { vw = render3d_st.water_refl.w; vh = render3d_st.water_refl.h }
if not render3d_st.ter_sun_done and not render3d_st.ter_sun_inline {
var dep = render3d_st.post_hdr.depth
if render3d_st.ter_reflect { dep = render3d_st.water_refl.depth }
render3d_st.ter_sun_tex = terrain_sun_pass(render3d_st, vw, vh, dep).color
}
render3d_st.ter_sun_done = false
if render3d_st.ter_reflect { target_bind(render3d_st, render3d_st.water_refl) }
else {
target_bind(render3d_st, render3d_st.post_hdr)
if render3d_st.post_ms_fbo != 0 { gpu_fb_bind(render3d_st, render3d_st.post_ms_fbo) }
}
gpu_depth_test(render3d_st, true)
# the prepass already laid this geometry's depth down: only the frontmost fragment of
# each pixel has anything to shade, and it meets that depth exactly
gpu_depth_func(render3d_st, GL_LEQUAL)
gpu_depth_write(render3d_st, true)
terrain_bind_prog(render3d_st, render3d_st.ter_prog)
terrain_bind_prog(render3d_st, render3d_st.ter_prog_far)
terrain_bind_prog(render3d_st, render3d_st.ter_prog_near)
render3d_st.ter_prog_cur = 0
render3d_st.cd_draws = 0
render3d_st.cd_far_draws = 0
render3d_st.cd_near_draws = 0
gpu_mesh_bind(render3d_st, render3d_st.cd_mesh)
if render3d_st.ter_wire { gpu_wireframe(render3d_st, true) }
cdlod_select(render3d_st, CD_LEVELS - 1, 0, 0)
if render3d_st.ter_wire { gpu_wireframe(render3d_st, false) }
gpu_depth_func(render3d_st, GL_LESS)
if render3d_st.r3d_debug and not render3d_st.ter_printed { render3d_st.ter_printed = true; terrain_say_patches(render3d_st) }
}
# ---- CDLOD --------------------------------------------------------------------------
# One 32x32 patch mesh (a_xz in 0..1) drawn once per selected quadtree node; the vertex
# shader places, scales and morphs it. Levels are drawn out to cd_range[k] = 48 * 2^k m,
# so cells are 1 m within 48 m, 2 m to 96 m, 4 m to 192 m ... 256 m at the root.
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
function cdlod_init(render3d_st: mut Render3dState) -> void {
let m = gpu_mesh_new(render3d_st)
let n = CD_G + 1
let v = gl_floats(n * n * 2)
var k = 0
for j in 0 .. n { for i in 0 .. n { gl_put_bits(v, k, float_bits(float(i) / float(CD_G))); gl_put_bits(v, k + 1, float_bits(float(j) / float(CD_G))); k += 2 } }
gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(n * n * 2), GPU_STATIC)
gpu_mesh_attr(render3d_st, m, 0, 2, GPU_F32, 8, 0, false)
free(v)
let ni = CD_G * CD_G * 6
let idx = words(ni)
k = 0
for j in 0 .. CD_G {
for i in 0 .. CD_G {
let a = j * n + i
idx[k] = a; idx[k + 1] = a + n; idx[k + 2] = a + 1
idx[k + 3] = a + 1; idx[k + 4] = a + n; idx[k + 5] = a + n + 1
k += 6
}
}
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.cd_mesh = m
render3d_st.cd_range = floats(CD_LEVELS)
var r = 48.0
if r3d_env_has(render3d_st, "R3D_CD_R0") { r = float(Text.to_int(r3d_env(render3d_st, "R3D_CD_R0"))) }
for l in 0 .. CD_LEVELS { render3d_st.cd_range[l] = r; r = r * 2.0 }
cdlod_bounds(render3d_st)
terrain_tiles_cut(render3d_st)
}
# min/max height per patch at every level, from the CPU copy of the height field
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
function cdlod_bounds(render3d_st: mut Render3dState) -> void {
render3d_st.cd_min = new []floats; render3d_st.cd_max = new []floats
let t = TERRAIN_RES / CD_LEAVES # texels per leaf patch side
var n = CD_LEAVES
var lo = floats(n * n); var hi = floats(n * n)
for j in 0 .. n {
for i in 0 .. n {
var mn = 100000.0; var mx = -100000.0
for y in 0 .. t + 1 {
let ty = min(j * t + y, TERRAIN_RES - 1)
for x in 0 .. t + 1 {
let tx = min(i * t + x, TERRAIN_RES - 1)
let h = ter_h(render3d_st, tx, ty)
mn = Math.min(mn, h); mx = Math.max(mx, h)
}
}
lo[j * n + i] = mn; hi[j * n + i] = mx
}
}
push(render3d_st.cd_min, lo); push(render3d_st.cd_max, hi)
while n > 1 {
let m = n / 2
let plo = floats(m * m); let phi = floats(m * m)
for j in 0 .. m {
for i in 0 .. m {
let a = (2 * j) * n + 2 * i
plo[j * m + i] = Math.min(Math.min(lo[a], lo[a + 1]), Math.min(lo[a + n], lo[a + n + 1]))
phi[j * m + i] = Math.max(Math.max(hi[a], hi[a + 1]), Math.max(hi[a + n], hi[a + n + 1]))
}
}
push(render3d_st.cd_min, plo); push(render3d_st.cd_max, phi)
lo = plo; hi = phi; n = m
}
}
# A patch's side in metres at a level. The quadtree is CD_LEAVES leaves a side over the whole
# map, so a leaf is (2 * TERRAIN_HALF) / CD_LEAVES - 32 m only on the 8192 m map the numbers
# were chosen for. Placing patches at a fixed 32 m put every bound in the wrong place on any
# other TERRAIN_HALF.
function cd_size(render3d_st: Render3dState, level: int) -> float { return float(32 << level) * (float(render3d_st.TERRAIN_HALF * 2) / 8192.0) }
# does the patch's box come within r of the camera?
function cd_within(render3d_st: Render3dState, x0: float, z0: float, size: float, ymin: float, ymax: float, r: float) -> bool {
let dx = Math.max(Math.max(x0 - render3d_st.cam_pos[0], render3d_st.cam_pos[0] - (x0 + size)), 0.0)
let dz = Math.max(Math.max(z0 - render3d_st.cam_pos[2], render3d_st.cam_pos[2] - (z0 + size)), 0.0)
let dy = Math.max(Math.max(ymin - render3d_st.cam_pos[1], render3d_st.cam_pos[1] - ymax), 0.0)
return dx * dx + dz * dz + dy * dy < r * r
}
# is the patch's box entirely inside r of the camera? (its farthest corner is within r)
function cd_inside(render3d_st: Render3dState, x0: float, z0: float, size: float, ymin: float, ymax: float, r: float) -> bool {
let x1 = x0 + size
let z1 = z0 + size
let dx = Math.max(Math.abs(render3d_st.cam_pos[0] - x0), Math.abs(render3d_st.cam_pos[0] - x1))
let dz = Math.max(Math.abs(render3d_st.cam_pos[2] - z0), Math.abs(render3d_st.cam_pos[2] - z1))
let dy = Math.max(Math.abs(render3d_st.cam_pos[1] - ymin), Math.abs(render3d_st.cam_pos[1] - ymax))
return dx * dx + dz * dz + dy * dy < r * r
}
function cdlod_draw(render3d_st: mut Render3dState, level: int, ix: int, iz: int) -> void {
let size = cd_size(render3d_st, level)
let x0 = render3d_st.ter_ox - float(render3d_st.TERRAIN_HALF) + float(ix) * size
let z0 = render3d_st.ter_oz - float(render3d_st.TERRAIN_HALF) + float(iz) * size
# Which tier can run inside this patch. A patch that never comes within the split takes
# the cheap tier at every pixel; one that lies wholly inside it takes the detailed tier
# at every pixel. Only a patch that straddles the band needs the program that holds both
# and cross-fades between them — and there are few of those, one ring of them.
if render3d_st.ter_sun_pass {
let t = ter_scratch(render3d_st)
t[0] = x0; t[1] = z0; t[2] = size
u_v3(render3d_st, gpu_uniform(render3d_st, render3d_st.ter_sun_prog, "u_node"), t)
var st0 = 0.0
if level > 0 { st0 = render3d_st.cd_range[level - 1] }
u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.ter_sun_prog, "u_morph"), Math.lerp(st0, render3d_st.cd_range[level], 0.7), render3d_st.cd_range[level])
gpu_draw_bound_elements(render3d_st, render3d_st.cd_mesh)
return
}
let n = CD_LEAVES >> level
let ymin = render3d_st.cd_min[level][iz * n + ix]
let ymax = render3d_st.cd_max[level][iz * n + ix]
var p = render3d_st.ter_prog
if not cd_within(render3d_st, x0, z0, size, ymin, ymax, render3d_st.ter_far_split + render3d_st.ter_far_band) { p = render3d_st.ter_prog_far }
else if cd_inside(render3d_st, x0, z0, size, ymin, ymax, render3d_st.ter_far_split - render3d_st.ter_far_band) { p = render3d_st.ter_prog_near }
if render3d_st.ter_force_far { p = render3d_st.ter_prog_far }
if render3d_st.ter_no_split { p = render3d_st.ter_prog }
if render3d_st.ter_force_near { p = render3d_st.ter_prog_near }
if p == render3d_st.ter_prog_far { render3d_st.cd_far_draws += 1 }
if p == render3d_st.ter_prog_near { render3d_st.cd_near_draws += 1 }
if p != render3d_st.ter_prog_cur { gpu_use_program(render3d_st, p); render3d_st.ter_prog_cur = p }
let t = ter_scratch(render3d_st)
t[0] = x0; t[1] = z0; t[2] = size
u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_node"), t)
var start = 0.0
if level > 0 { start = render3d_st.cd_range[level - 1] }
start = Math.lerp(start, render3d_st.cd_range[level], 0.7)
u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_morph"), start, render3d_st.cd_range[level])
gpu_draw_bound_elements(render3d_st, render3d_st.cd_mesh)
render3d_st.cd_draws += 1
}
# Strugar's selection: a node is drawn at its own level unless it is close enough to need
# its children, in which case each child either selects itself or is drawn at this level.
function cdlod_select(render3d_st: mut Render3dState, level: int, ix: int, iz: int) -> bool {
let n = CD_LEAVES >> level
let size = cd_size(render3d_st, level)
let x0 = render3d_st.ter_ox - float(render3d_st.TERRAIN_HALF) + float(ix) * size
let z0 = render3d_st.ter_oz - float(render3d_st.TERRAIN_HALF) + float(iz) * size
let ymin = render3d_st.cd_min[level][iz * n + ix]
let ymax = render3d_st.cd_max[level][iz * n + ix]
if not cd_within(render3d_st, x0, z0, size, ymin, ymax, render3d_st.cd_range[level]) { return false }
let half = size * 0.5
let cy = (ymin + ymax) * 0.5
let rad = Math.sqrt(half * half * 2.0 + (ymax - cy) * (ymax - cy) * 1.0)
if not cam_sphere_visible(render3d_st, x0 + half, cy, z0 + half, rad + 2.0) { return true }
if level == 0 { cdlod_draw(render3d_st, 0, ix, iz); return true }
if not cd_within(render3d_st, x0, z0, size, ymin, ymax, render3d_st.cd_range[level - 1]) { cdlod_draw(render3d_st, level, ix, iz); return true }
for c in 0 .. 4 {
let cx = ix * 2 + (c & 1); let cz = iz * 2 + (c >> 1)
if not cdlod_select(render3d_st, level - 1, cx, cz) { cdlod_draw(render3d_st, level - 1, cx, cz) }
}
return true
}
# ---- another map at run time ------------------------------------------------------------
# What belongs to ONE map - the generated height field and its CPU copy, the survey, the
# photograph, the patch bounds and the scales cached from TERRAIN_HALF - is released here,
# so a different survey can be generated in its place without restarting the process. What
# belongs to the process stays: the patch mesh, the material textures, the programs, the
# frame-sized targets and the shadow texture, whose size does not depend on the map.
function terrain_unload(render3d_st: mut Render3dState) -> void {
if render3d_st.ter_height_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_height_tex); render3d_st.ter_height_tex = 0 }
if render3d_st.ter_normal_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_normal_tex); render3d_st.ter_normal_tex = 0 }
if render3d_st.ter_dem_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_dem_tex); render3d_st.ter_dem_tex = 0 }
if render3d_st.ter_ortho_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_ortho_tex); render3d_st.ter_ortho_tex = 0 }
tp_pool_free(render3d_st)
if render3d_st.ter_heights != null { free(render3d_st.ter_heights); render3d_st.ter_heights = null }
tt_close(render3d_st)
if render3d_st.ter_ortho_px != null { free(render3d_st.ter_ortho_px); render3d_st.ter_ortho_px = null }
render3d_st.ter_ortho_w = 0
if render3d_st.cd_min != null {
for i in 0 .. len(render3d_st.cd_min) { free(render3d_st.cd_min[i]); free(render3d_st.cd_max[i]) }
render3d_st.cd_min = null; render3d_st.cd_max = null
}
# derived from TERRAIN_HALF and cached on first use: stale ones would keep the old size
render3d_st.ter_h_scale = 0.0; render3d_st.ter_o_scale = 0.0
render3d_st.ter_carpet = 0
render3d_st.ter_shadow_gen = -1; render3d_st.ter_shadow_yaw = 1000000000.0
}
# Generate another map in place. Call terrain_lake / terrain_coast / terrain_island for it
# first - the generator reads them - then this. `half` is the new TERRAIN_HALF in metres.
# An empty `dem` generates the analytic ground (with ter_smooth) and an empty `ortho` drapes
# no photograph. The shadow is rebaked and the patch bounds rebuilt before it returns.
function terrain_reload(render3d_st: mut Render3dState, dem: string, emin: float, emax: float, base: float, ox: float, oz: float, ortho: string, half: int) -> void {
terrain_unload(render3d_st)
render3d_st.TERRAIN_HALF = half
render3d_st.ter_ox = ox; render3d_st.ter_oz = oz
if len(dem) > 0 { terrain_use_dem(render3d_st, dem, emin, emax, base, ox, oz) }
if len(ortho) > 0 { terrain_use_ortho(render3d_st, ortho) }
terrain_generate(render3d_st)
terrain_bake_shadow(render3d_st)
cdlod_bounds(render3d_st)
terrain_tiles_cut(render3d_st)
gpu_check(render3d_st, "terrain reload")
}
function ter_scratch(render3d_st: mut Render3dState) -> floats {
return render3d_st.ter_scr
}
# messages, each built in a function of its own so the path that says it holds no allocation
@alloc_ok("a message, built only when it is said: a failure, a warning or a debug switch")
function terrain_say_sun_fbo(st: int) -> void { print(`r3d: sun-visibility framebuffer incomplete {st}`) }
@alloc_ok("a message, built only when it is said: a failure, a warning or a debug switch")
function terrain_say_patches(render3d_st: Render3dState) -> void { print(`cdlod patches drawn: {render3d_st.cd_draws} (far {render3d_st.cd_far_draws}, near {render3d_st.cd_near_draws}, band {render3d_st.cd_draws - render3d_st.cd_far_draws - render3d_st.cd_near_draws})`) }