feat(gl): OpenGL 4.1 and the ludic.render3d renderer
`Gl.*` binds the whole OpenGL 4.1 core API — every entry point of the platform gl3.h with every GL_* constant, generated by `ludic-dev glgen` with per-call ABI thunks. Windowed builds get an NSOpenGLContext on the existing window at Retina resolution; headless builds render into an offscreen CGL context, so a program that uses Gl.* renders and screenshots identically under the test harness. It links gl.ll, the thunks and OpenGL.framework only when used; every other build stays byte-identical. packages/ludic.render3d is a physically based renderer written on that surface: HDRI image-based lighting, GPU-generated terrain with scanned PBR materials, CDLOD, cascaded shadows, glTF with skinning, instanced vegetation with impostors, procedural grass, water, SSAO, and an HDR pipeline with bloom, auto-exposure and ACES. It also carries this session's work on it: the terrain at half its cost (10.3 -> 5.4 ms of frame), the streaming hitch that got worse the longer you played, a resize that emptied the world, and the packaging that lets a game use the renderer from its own repository — `ludic assets`, the material manifest shipping with the package, and shader lookup falling back to the install root. See changes/ for each, with its numbers. The camping game that drove all of it has moved out to its own repository, Maroon Lake; examples/rendering/smooth.ludic stays as the renderer's example here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
470971bf70
commit
f25289db20
90 changed files with 35316 additions and 19853 deletions
99
packages/ludic.render3d/collide.ludic
Normal file
99
packages/ludic.render3d/collide.ludic
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# ============================================================================
|
||||
# collide.ludic — the static colliders of the world as circles on the ground
|
||||
# plane (a trunk, a boulder, a tent), sorted once into 16 m cells over the whole
|
||||
# terrain. A moving thing asks col_resolve for its position pushed out of every
|
||||
# circle it overlaps: three by three cells, a few dozen tests, no broad phase
|
||||
# needed. Float bits, metres.
|
||||
# ============================================================================
|
||||
|
||||
const COL_CELL: int = 16
|
||||
const COL_CAP: int = 120000
|
||||
|
||||
var col_x: words = null
|
||||
var col_z: words = null
|
||||
var col_r: words = null
|
||||
var col_n: int = 0
|
||||
var col_side: int = 0 # cells per side
|
||||
var col_start: words = null # per cell: first index into col_sorted (side*side + 1)
|
||||
var col_sorted: words = null
|
||||
var col_built: bool = false
|
||||
var col_out: words = null # the resolved position (x, z)
|
||||
|
||||
function col_add(x: int, z: int, r: int) -> void {
|
||||
if col_x == null { col_x = words(COL_CAP); col_z = words(COL_CAP); col_r = words(COL_CAP); col_out = words(2) }
|
||||
if col_n >= COL_CAP { return }
|
||||
col_x[col_n] = x; col_z[col_n] = z; col_r[col_n] = r
|
||||
col_n += 1
|
||||
col_built = false
|
||||
}
|
||||
function col_cell_of(v: int, origin: int) -> int {
|
||||
var c = f_to_int(f_floor(f_div(f_add(f_sub(v, origin), fi(TERRAIN_HALF)), fi(COL_CELL))))
|
||||
if c < 0 { c = 0 }
|
||||
if c > col_side - 1 { c = col_side - 1 }
|
||||
return c
|
||||
}
|
||||
function col_build() -> void {
|
||||
col_side = (TERRAIN_HALF * 2) / COL_CELL
|
||||
let ncell = col_side * col_side
|
||||
if col_start == null { col_start = words(ncell + 1); col_sorted = words(COL_CAP) }
|
||||
for i in 0 .. ncell + 1 { col_start[i] = 0 }
|
||||
for i in 0 .. col_n { col_start[col_cell_of(col_z[i], ter_oz) * col_side + col_cell_of(col_x[i], ter_ox) + 1] += 1 }
|
||||
for c in 0 .. ncell { col_start[c + 1] += col_start[c] }
|
||||
let fill = words(ncell)
|
||||
for c in 0 .. ncell { fill[c] = col_start[c] }
|
||||
for i in 0 .. col_n {
|
||||
let c = col_cell_of(col_z[i], ter_oz) * col_side + col_cell_of(col_x[i], ter_ox)
|
||||
col_sorted[fill[c]] = i
|
||||
fill[c] += 1
|
||||
}
|
||||
free(fill)
|
||||
col_built = true
|
||||
print(`colliders: {col_n}`)
|
||||
}
|
||||
|
||||
# push (px, pz) with radius pr out of every circle it overlaps; the result is in col_out
|
||||
function col_resolve(px: int, pz: int, pr: int) -> bool {
|
||||
col_out[0] = px; col_out[1] = pz
|
||||
if not col_built or col_n == 0 { return false }
|
||||
var x = px; var z = pz
|
||||
var moved = false
|
||||
let cx = col_cell_of(px, ter_ox); let cz = col_cell_of(pz, ter_oz)
|
||||
for pass in 0 .. 2 {
|
||||
for dz in 0 .. 3 {
|
||||
let zc = cz + dz - 1
|
||||
if zc < 0 or zc >= col_side { continue }
|
||||
for dx in 0 .. 3 {
|
||||
let xc = cx + dx - 1
|
||||
if xc < 0 or xc >= col_side { continue }
|
||||
let c = zc * col_side + xc
|
||||
for k in col_start[c] .. col_start[c + 1] {
|
||||
let i = col_sorted[k]
|
||||
let ex = f_sub(x, col_x[i]); let ez = f_sub(z, col_z[i])
|
||||
let d2 = f_add(f_mul(ex, ex), f_mul(ez, ez))
|
||||
let rr = f_add(col_r[i], pr)
|
||||
if f_ls(d2, f_mul(rr, rr)) {
|
||||
var d = f_sqrt(d2)
|
||||
var nx = ex; var nz = ez
|
||||
if f_ls(d, fl(0.001)) { d = fl(0.001); nx = F_ONE; nz = F_ZERO }
|
||||
let push = f_sub(rr, d)
|
||||
x = f_add(x, f_mul(f_div(nx, d), push))
|
||||
z = f_add(z, f_mul(f_div(nz, d), push))
|
||||
moved = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
col_out[0] = x; col_out[1] = z
|
||||
return moved
|
||||
}
|
||||
# is the segment from (x0,z0) to (x1,z1) clear of every circle (a camera line of sight)?
|
||||
function col_clear(x0: int, z0: int, x1: int, z1: int, r: int) -> bool {
|
||||
let steps = 6
|
||||
for s in 0 .. steps + 1 {
|
||||
let t = fr(s, steps)
|
||||
let x = f_lerp(x0, x1, t); let z = f_lerp(z0, z1, t)
|
||||
if col_resolve(x, z, r) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue