`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>
95 lines
5 KiB
Text
95 lines
5 KiB
Text
# ============================================================================
|
|
# quat.ludic — unit quaternions (x, y, z, w) as 4-word float-bit buffers, for
|
|
# skeletal poses. Same conventions as fmath.ludic: float bits in words, column-
|
|
# major matrices, o may alias its inputs unless stated.
|
|
# ============================================================================
|
|
|
|
function q_new() -> words { let q = words(4); q_identity(q); return q }
|
|
function q_identity(q: words) -> void { q[0] = F_ZERO; q[1] = F_ZERO; q[2] = F_ZERO; q[3] = F_ONE }
|
|
function q_set(q: words, x: int, y: int, z: int, w: int) -> void { q[0] = x; q[1] = y; q[2] = z; q[3] = w }
|
|
function q_copy(o: words, a: words) -> void { o[0] = a[0]; o[1] = a[1]; o[2] = a[2]; o[3] = a[3] }
|
|
# the idx-th quaternion of a packed buffer
|
|
function q_load(o: words, src: words, idx: int) -> void { for i in 0 .. 4 { o[i] = src[idx * 4 + i] } }
|
|
function q_store(dst: words, idx: int, a: words) -> void { for i in 0 .. 4 { dst[idx * 4 + i] = a[i] } }
|
|
|
|
# a rotation of `angle` radians about the unit axis (ax, ay, az)
|
|
function q_axis_angle(o: words, ax: int, ay: int, az: int, angle: int) -> void {
|
|
let h = f_mul(angle, F_HALF)
|
|
let s = f_sin(h)
|
|
o[0] = f_mul(ax, s); o[1] = f_mul(ay, s); o[2] = f_mul(az, s); o[3] = f_cos(h)
|
|
}
|
|
# o = a * b (apply b first, then a); o may alias a or b
|
|
function q_mul(o: words, a: words, b: words) -> void {
|
|
let ax = a[0]; let ay = a[1]; let az = a[2]; let aw = a[3]
|
|
let bx = b[0]; let by = b[1]; let bz = b[2]; let bw = b[3]
|
|
let x = f_sub(f_add(f_add(f_mul(aw, bx), f_mul(ax, bw)), f_mul(ay, bz)), f_mul(az, by))
|
|
let y = f_add(f_add(f_sub(f_mul(aw, by), f_mul(ax, bz)), f_mul(ay, bw)), f_mul(az, bx))
|
|
let z = f_add(f_sub(f_add(f_mul(aw, bz), f_mul(ax, by)), f_mul(ay, bx)), f_mul(az, bw))
|
|
let w = f_sub(f_sub(f_sub(f_mul(aw, bw), f_mul(ax, bx)), f_mul(ay, by)), f_mul(az, bz))
|
|
o[0] = x; o[1] = y; o[2] = z; o[3] = w
|
|
}
|
|
function q_conj(o: words, a: words) -> void { o[0] = f_neg(a[0]); o[1] = f_neg(a[1]); o[2] = f_neg(a[2]); o[3] = a[3] }
|
|
function q_normalize(q: words) -> void {
|
|
let l = f_sqrt(f_add(f_add(f_mul(q[0], q[0]), f_mul(q[1], q[1])), f_add(f_mul(q[2], q[2]), f_mul(q[3], q[3]))))
|
|
if l == 0 { q_identity(q); return }
|
|
let inv = f_div(F_ONE, l)
|
|
for i in 0 .. 4 { q[i] = f_mul(q[i], inv) }
|
|
}
|
|
# normalised linear blend from a to b (shortest arc), fine for the small steps a pose takes
|
|
function q_nlerp(o: words, a: words, b: words, t: int) -> void {
|
|
var d = f_add(f_add(f_mul(a[0], b[0]), f_mul(a[1], b[1])), f_add(f_mul(a[2], b[2]), f_mul(a[3], b[3])))
|
|
var sg = F_ONE
|
|
if f_ls(d, F_ZERO) { sg = f_neg(F_ONE) }
|
|
for i in 0 .. 4 { o[i] = f_lerp(a[i], f_mul(b[i], sg), t) }
|
|
q_normalize(o)
|
|
}
|
|
# rotate the vector v by q: o = q v q*
|
|
function q_rotate(o: words, q: words, v: words) -> void {
|
|
let qx = q[0]; let qy = q[1]; let qz = q[2]; let qw = q[3]
|
|
# t = 2 * cross(q.xyz, v)
|
|
let tx = f_mul(F_TWO, f_sub(f_mul(qy, v[2]), f_mul(qz, v[1])))
|
|
let ty = f_mul(F_TWO, f_sub(f_mul(qz, v[0]), f_mul(qx, v[2])))
|
|
let tz = f_mul(F_TWO, f_sub(f_mul(qx, v[1]), f_mul(qy, v[0])))
|
|
# o = v + w t + cross(q.xyz, t)
|
|
let x = f_add(f_add(v[0], f_mul(qw, tx)), f_sub(f_mul(qy, tz), f_mul(qz, ty)))
|
|
let y = f_add(f_add(v[1], f_mul(qw, ty)), f_sub(f_mul(qz, tx), f_mul(qx, tz)))
|
|
let z = f_add(f_add(v[2], f_mul(qw, tz)), f_sub(f_mul(qx, ty), f_mul(qy, tx)))
|
|
o[0] = x; o[1] = y; o[2] = z
|
|
}
|
|
# pitch about X, yaw about Y, roll about Z, composed as yaw * pitch * roll
|
|
var q_scratch: words = null
|
|
function q_euler(o: words, pitch: int, yaw: int, roll: int) -> void {
|
|
if q_scratch == null { q_scratch = words(16) }
|
|
let qx = q_scratch; let qy = mem_off(q_scratch, 16); let qz = mem_off(q_scratch, 32); let t = mem_off(q_scratch, 48)
|
|
q_axis_angle(qx, F_ONE, F_ZERO, F_ZERO, pitch)
|
|
q_axis_angle(qy, F_ZERO, F_ONE, F_ZERO, yaw)
|
|
q_axis_angle(qz, F_ZERO, F_ZERO, F_ONE, roll)
|
|
q_mul(t, qy, qx)
|
|
q_mul(o, t, qz)
|
|
}
|
|
# the rotation matrix of q (column-major, translation cleared)
|
|
function q_to_m4(m: words, q: words) -> void {
|
|
let x = q[0]; let y = q[1]; let z = q[2]; let w = q[3]
|
|
let xx = f_mul(x, x); let yy = f_mul(y, y); let zz = f_mul(z, z)
|
|
let xy = f_mul(x, y); let xz = f_mul(x, z); let yz = f_mul(y, z)
|
|
let wx = f_mul(w, x); let wy = f_mul(w, y); let wz = f_mul(w, z)
|
|
m[0] = f_sub(F_ONE, f_mul(F_TWO, f_add(yy, zz)))
|
|
m[1] = f_mul(F_TWO, f_add(xy, wz))
|
|
m[2] = f_mul(F_TWO, f_sub(xz, wy))
|
|
m[3] = F_ZERO
|
|
m[4] = f_mul(F_TWO, f_sub(xy, wz))
|
|
m[5] = f_sub(F_ONE, f_mul(F_TWO, f_add(xx, zz)))
|
|
m[6] = f_mul(F_TWO, f_add(yz, wx))
|
|
m[7] = F_ZERO
|
|
m[8] = f_mul(F_TWO, f_add(xz, wy))
|
|
m[9] = f_mul(F_TWO, f_sub(yz, wx))
|
|
m[10] = f_sub(F_ONE, f_mul(F_TWO, f_add(xx, yy)))
|
|
m[11] = F_ZERO
|
|
m[12] = F_ZERO; m[13] = F_ZERO; m[14] = F_ZERO; m[15] = F_ONE
|
|
}
|
|
# m = translate(t) * rotate(q) * scale(s)
|
|
function m4_trs_q(m: words, tx: int, ty: int, tz: int, q: words, sx: int, sy: int, sz: int) -> void {
|
|
q_to_m4(m, q)
|
|
for r in 0 .. 3 { m[r] = f_mul(m[r], sx); m[4 + r] = f_mul(m[4 + r], sy); m[8 + r] = f_mul(m[8 + r], sz) }
|
|
m[12] = tx; m[13] = ty; m[14] = tz
|
|
}
|