`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>
204 lines
7.7 KiB
Text
204 lines
7.7 KiB
Text
# ============================================================================
|
|
# actor.ludic — a model placed once and moved every frame (a character, a prop
|
|
# that animates): the counterpart of a scatter Layer for the handful of things
|
|
# that are not instanced. Skinned models pose through their Skin (skin.ludic);
|
|
# rigid ones take the same path with u_skinned = 0. The game draws them from
|
|
# scene_draw / scene_draw_casters through actor_draw / actor_draw_casters.
|
|
#
|
|
# Uniform locations are looked up once per program (glGetUniformLocation by
|
|
# name is slow on this driver, and a hundred actors over five cascades made it
|
|
# the frame's biggest CPU cost); the scene's lighting is bound once per program
|
|
# per pass, and actors outside the view or beyond their cull distance are skipped.
|
|
# ============================================================================
|
|
|
|
property Actor {
|
|
model: Model,
|
|
x: int = 0, # float bits, metres
|
|
y: int = 0,
|
|
z: int = 0,
|
|
yaw: int = 0, # radians; 0 faces -z like the camera
|
|
scale: int = 0,
|
|
tint: words,
|
|
rough: int = 0,
|
|
mat: words, # the model matrix
|
|
visible: bool = true,
|
|
casts: bool = true,
|
|
id: int = 0,
|
|
cutout: bool = false, # alpha-tested (a flame's cards)
|
|
emissive: int = 0, # float bits: self-lit strength
|
|
skin: Skin, # this instance's own pose (skin_clone); null: the model's
|
|
ptint: words, # 4 per primitive: on flag, r, g, b (a part's own colour)
|
|
hide: words, # 1 per primitive: skip it (a cap taken off)
|
|
radius: int = 0, # float bits: bounding radius for culling (0 = never culled)
|
|
cull: int = 0 # float bits: not drawn beyond this distance (0 = always)
|
|
}
|
|
# a program and its uniform locations
|
|
property AcProg {
|
|
prog: int = 0,
|
|
l_model: int = -1,
|
|
l_bones: int = -1,
|
|
l_skin: int = -1,
|
|
l_lvp: int = -1,
|
|
l_tint: int = -1,
|
|
l_rough: int = -1,
|
|
l_emis: int = -1,
|
|
l_view: int = -1,
|
|
l_proj: int = -1,
|
|
l_mh: int = -1,
|
|
scene_frame: int = -1 # the frame its scene uniforms were bound
|
|
}
|
|
|
|
var ac_lit: AcProg = null
|
|
var ac_lit_cut: AcProg = null
|
|
var ac_sh: AcProg = null
|
|
var ac_sh_cut: AcProg = null
|
|
var ac_actors: []Actor = null
|
|
var ac_next_id: int = 0
|
|
var ac_frame: int = 0
|
|
|
|
function ac_prog_new(vs: string, fs: string, defs: string) -> AcProg {
|
|
let a = new AcProg
|
|
a.prog = r3d_program(vs, fs, defs)
|
|
let p = a.prog
|
|
a.l_model = gl_uniform(p, "u_model"); a.l_skin = gl_uniform(p, "u_skinned"); a.l_lvp = gl_uniform(p, "u_light_vp")
|
|
a.l_bones = gl_uniform(p, "u_bones[0]"); if a.l_bones < 0 { a.l_bones = gl_uniform(p, "u_bones") }
|
|
a.l_tint = gl_uniform(p, "u_tint"); a.l_rough = gl_uniform(p, "u_rough_scale"); a.l_emis = gl_uniform(p, "u_emissive")
|
|
a.l_view = gl_uniform(p, "u_view"); a.l_proj = gl_uniform(p, "u_proj"); a.l_mh = gl_uniform(p, "u_model_h")
|
|
# the samplers never move: units 0, 1, 2
|
|
gl_use_program(p)
|
|
gl_uniform1i(gl_uniform(p, "u_diff"), 0); gl_uniform1i(gl_uniform(p, "u_nrm"), 1); gl_uniform1i(gl_uniform(p, "u_arm"), 2)
|
|
return a
|
|
}
|
|
function actor_init() -> void {
|
|
ac_lit = ac_prog_new("skin.vert", "model.frag", "")
|
|
ac_lit_cut = ac_prog_new("skin.vert", "model.frag", "#define ALPHA_TEST\n")
|
|
ac_sh = ac_prog_new("skin.vert", "shadow.frag", "#define SHADOW_PASS\n")
|
|
ac_sh_cut = ac_prog_new("skin.vert", "shadow.frag", "#define SHADOW_PASS\n#define ALPHA_TEST\n")
|
|
ac_actors = new []Actor
|
|
}
|
|
function actor_remove(a: Actor) -> void {
|
|
if ac_actors == null { return }
|
|
let keep = new []Actor
|
|
for i in 0 .. len(ac_actors) { if ac_actors[i].id != a.id { push(keep, ac_actors[i]) } }
|
|
ac_actors = keep
|
|
}
|
|
# colour one named part of the model (a material name from the file)
|
|
function actor_tint_part(a: Actor, name: string, r: int, g: int, b: int) -> void {
|
|
if a.model == null { return }
|
|
let n = len(a.model.prims)
|
|
if a.ptint == null { a.ptint = words(n * 4); for i in 0 .. n * 4 { a.ptint[i] = 0 } }
|
|
for i in 0 .. n { if a.model.prims[i].name == name { a.ptint[i * 4] = 1; a.ptint[i * 4 + 1] = r; a.ptint[i * 4 + 2] = g; a.ptint[i * 4 + 3] = b } }
|
|
}
|
|
function actor_hide_part(a: Actor, name: string, hidden: bool) -> void {
|
|
if a.model == null { return }
|
|
let n = len(a.model.prims)
|
|
if a.hide == null { a.hide = words(n); for i in 0 .. n { a.hide[i] = 0 } }
|
|
var v = 0
|
|
if hidden { v = 1 }
|
|
for i in 0 .. n { if a.model.prims[i].name == name { a.hide[i] = v } }
|
|
}
|
|
|
|
function actor_new(model: Model) -> Actor {
|
|
let a = new Actor
|
|
a.model = model
|
|
a.scale = F_ONE
|
|
a.tint = v3_new(F_ONE, F_ONE, F_ONE)
|
|
a.rough = F_ONE
|
|
a.mat = m4_new()
|
|
ac_next_id += 1; a.id = ac_next_id
|
|
if model != null { a.radius = f_add(f_max(model.radius, model.height), F_ONE) }
|
|
a.cull = fi(450)
|
|
if ac_actors == null { ac_actors = new []Actor }
|
|
push(ac_actors, a)
|
|
return a
|
|
}
|
|
|
|
function actor_place(a: Actor, x: int, y: int, z: int, yaw: int) -> void {
|
|
a.x = x; a.y = y; a.z = z; a.yaw = yaw
|
|
m4_trs(a.mat, x, y, z, yaw, a.scale)
|
|
}
|
|
|
|
# the scene's lighting for a lit program, once per frame
|
|
function ac_bind_scene(ap: AcProg) -> void {
|
|
if ap.scene_frame == ac_frame { return }
|
|
ap.scene_frame = ac_frame
|
|
let p = ap.prog
|
|
gl_use_program(p)
|
|
u_mat4(ap.l_view, cam_view)
|
|
u_mat4(ap.l_proj, cam_proj)
|
|
u_f(ap.l_mh, F_ZERO)
|
|
sky_bind_lighting(p)
|
|
shadow_bind(p)
|
|
fog_bind(p)
|
|
}
|
|
function ac_visible(a: Actor, shadow: bool) -> bool {
|
|
if not a.visible or a.model == null { return false }
|
|
if shadow and not a.casts { return false }
|
|
if a.cull != 0 {
|
|
let dx = f_sub(a.x, cam_pos[0]); let dz = f_sub(a.z, cam_pos[2])
|
|
let d2 = f_add(f_mul(dx, dx), f_mul(dz, dz))
|
|
var c = a.cull
|
|
if shadow { c = f_min(c, fi(300)) }
|
|
if f_gt(d2, f_mul(c, c)) { return false }
|
|
}
|
|
if not shadow and a.radius != 0 {
|
|
let r = f_mul(a.radius, a.scale)
|
|
if not cam_sphere_visible(a.x, f_add(a.y, r), a.z, f_mul(r, fl(1.5))) { return false }
|
|
}
|
|
return true
|
|
}
|
|
function actor_draw_one(a: Actor, ap: AcProg, shadow: bool) -> void {
|
|
let p = ap.prog
|
|
gl_use_program(p)
|
|
u_mat4(ap.l_model, a.mat)
|
|
var skinned = F_ZERO
|
|
if a.skin != null { skinned = F_ONE; gl_uniform_matrix4fv(ap.l_bones, a.skin.n_joints, 0, a.skin.bones) }
|
|
else if a.model.skin != null { skinned = F_ONE; gl_uniform_matrix4fv(ap.l_bones, a.model.skin.n_joints, 0, a.model.skin.bones) }
|
|
u_f(ap.l_skin, skinned)
|
|
if not shadow {
|
|
u_f(ap.l_emis, a.emissive)
|
|
u_f(ap.l_rough, a.rough)
|
|
}
|
|
gl_disable(GL_CULL_FACE)
|
|
let model = a.model
|
|
for i in 0 .. len(model.prims) {
|
|
let pr = model.prims[i]
|
|
if a.hide != null and a.hide[i] != 0 { continue }
|
|
if not shadow {
|
|
if a.ptint != null and a.ptint[i * 4] != 0 { u_f3(ap.l_tint, a.ptint[i * 4 + 1], a.ptint[i * 4 + 2], a.ptint[i * 4 + 3]) }
|
|
else { u_v3(ap.l_tint, a.tint) }
|
|
}
|
|
gl_active_texture(GL_TEXTURE0); gl_bind_texture(GL_TEXTURE_2D, pr.diff)
|
|
if not shadow {
|
|
gl_active_texture(GL_TEXTURE0 + 1); gl_bind_texture(GL_TEXTURE_2D, pr.nrm)
|
|
gl_active_texture(GL_TEXTURE0 + 2); gl_bind_texture(GL_TEXTURE_2D, pr.arm)
|
|
}
|
|
mesh_draw(pr.mesh)
|
|
}
|
|
}
|
|
|
|
function actor_draw() -> void {
|
|
if ac_actors == null { return }
|
|
ac_frame += 1
|
|
for i in 0 .. len(ac_actors) {
|
|
let a = ac_actors[i]
|
|
if not ac_visible(a, false) { continue }
|
|
var ap = ac_lit
|
|
if a.cutout { ap = ac_lit_cut }
|
|
ac_bind_scene(ap)
|
|
actor_draw_one(a, ap, false)
|
|
}
|
|
gl_enable(GL_CULL_FACE)
|
|
}
|
|
function actor_draw_casters(light_vp: words) -> void {
|
|
if ac_actors == null { return }
|
|
gl_use_program(ac_sh.prog); u_mat4(ac_sh.l_lvp, light_vp)
|
|
gl_use_program(ac_sh_cut.prog); u_mat4(ac_sh_cut.l_lvp, light_vp)
|
|
for i in 0 .. len(ac_actors) {
|
|
let a = ac_actors[i]
|
|
if not ac_visible(a, true) { continue }
|
|
var ap = ac_sh
|
|
if a.cutout { ap = ac_sh_cut }
|
|
actor_draw_one(a, ap, true)
|
|
}
|
|
}
|