ludic/packages/ludic.render3d/collide.ludic
Orkuncakilkaya f25289db20
Some checks failed
ci / build-and-test (push) Waiting to run
commit-lint / conventional-commits (push) Waiting to run
bootstrap / cfree-fixpoint (push) Has been cancelled
docs / build-and-deploy (push) Successful in 34s
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>
2026-09-10 03:31:12 +03:00

99 lines
3.7 KiB
Text

# ============================================================================
# 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
}