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

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
}