ludic/packages/ludic.render3d/camera.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

101 lines
3.9 KiB
Text

# ============================================================================
# camera.ludic — a first-person fly camera (yaw / pitch, metres) and its view /
# projection matrices. Float bits throughout; see fmath.ludic.
# ============================================================================
var cam_pos: words = null # x, y, z
var cam_yaw: int = 0 # radians, 0 = looking down -z
var cam_pitch: int = 0
var cam_fov: int = 0 # vertical, radians
var cam_near: int = 0
var cam_far: int = 0
var cam_aspect: int = 0
var cam_view: words = null
var cam_proj: words = null
var cam_vp: words = null
var cam_inv_vp: words = null
var cam_inv_proj: words = null
var cam_vp_clean: words = null # view-projection, kept for depth reconstruction
var cam_inv_vp_clean: words = null
var cam_fwd: words = null
var cam_right: words = null
# the view frustum's four side planes (a, b, c, d), float bits: left, right, bottom, top;
# from the clean (unjittered) view-projection, column-major m[col * 4 + row]
var cam_planes: words = null
function cam_init(aspect: int) -> void {
cam_pos = v3_new(F_ZERO, fi(2), F_ZERO)
cam_view = m4_new(); cam_proj = m4_new(); cam_vp = m4_new(); cam_inv_vp = m4_new(); cam_inv_proj = m4_new()
cam_vp_clean = m4_new(); cam_inv_vp_clean = m4_new()
cam_fwd = v3_new(F_ZERO, F_ZERO, f_neg1())
cam_right = v3_new(F_ONE, F_ZERO, F_ZERO)
cam_fov = f_rad(fi(42))
cam_near = fl(0.3)
cam_far = fi(14000)
cam_aspect = aspect
cam_update()
}
function cam_begin_frame(n: int, w: int, h: int) -> void {
cam_update()
}
function cam_set(x: int, y: int, z: int, yaw_deg: int, pitch_deg: int) -> void {
v3_set(cam_pos, x, y, z)
cam_yaw = f_rad(yaw_deg)
cam_pitch = f_rad(pitch_deg)
cam_update()
}
function cam_update() -> void {
let cy = f_cos(cam_yaw); let sy = f_sin(cam_yaw)
let cp = f_cos(cam_pitch); let sp = f_sin(cam_pitch)
v3_set(cam_fwd, f_neg(f_mul(sy, cp)), sp, f_neg(f_mul(cy, cp)))
v3_set(cam_right, cy, F_ZERO, f_neg(sy))
let at = words(3)
v3_add(at, cam_pos, cam_fwd)
let up = v3_new(F_ZERO, F_ONE, F_ZERO)
m4_look_at(cam_view, cam_pos, at, up)
m4_perspective(cam_proj, cam_fov, cam_aspect, cam_near, cam_far)
m4_mul(cam_vp_clean, cam_proj, cam_view)
m4_inverse(cam_inv_vp_clean, cam_vp_clean)
m4_mul(cam_vp, cam_proj, cam_view)
m4_inverse(cam_inv_vp, cam_vp)
m4_inverse(cam_inv_proj, cam_proj)
free(at); free(up)
cam_planes_update()
}
function cam_planes_update() -> void {
if cam_planes == null { cam_planes = words(16) }
let m = cam_vp_clean
for p in 0 .. 4 {
var r = 0
if p >= 2 { r = 1 }
var sg = F_ONE
if p == 1 or p == 3 { sg = f_neg(F_ONE) }
var a = f_add(m[3], f_mul(sg, m[r]))
var b = f_add(m[7], f_mul(sg, m[4 + r]))
var c = f_add(m[11], f_mul(sg, m[8 + r]))
var d = f_add(m[15], f_mul(sg, m[12 + r]))
let inv = f_div(F_ONE, f_sqrt(f_add(f_add(f_mul(a, a), f_mul(b, b)), f_mul(c, c))))
cam_planes[p * 4] = f_mul(a, inv); cam_planes[p * 4 + 1] = f_mul(b, inv)
cam_planes[p * 4 + 2] = f_mul(c, inv); cam_planes[p * 4 + 3] = f_mul(d, inv)
}
}
# is a sphere (float bits) at least partly inside the side planes of the view?
function cam_sphere_visible(x: int, y: int, z: int, r: int) -> bool {
if cam_planes == null { return true }
let nr = f_neg(r)
for p in 0 .. 4 {
let o = p * 4
let dist = f_add(f_add(f_add(f_mul(cam_planes[o], x), f_mul(cam_planes[o + 1], y)), f_mul(cam_planes[o + 2], z)), cam_planes[o + 3])
if f_ls(dist, nr) { return false }
}
return true
}
# fly: forward/strafe in metres, turn in radians
function cam_move(fwd: int, strafe: int, up: int, dyaw: int, dpitch: int) -> void {
cam_yaw = f_add(cam_yaw, dyaw)
cam_pitch = f_clamp(f_add(cam_pitch, dpitch), f_neg(fl(1.5)), fl(1.5))
v3_madd(cam_pos, cam_pos, cam_fwd, fwd)
v3_madd(cam_pos, cam_pos, cam_right, strafe)
cam_pos[1] = f_add(cam_pos[1], up)
cam_update()
}