cam_sphere_visible refuses a sphere wholly past the wall (terrain patches, scatter cells, stream
chunks, grass tiles, water), actors past it are skipped for draws, casters and outlines, the grass
reach and the streams' generate-and-gather reach end at it, and the card shadows cast only from
the wall's share of a layer (fog_casters.ludic, rebuilt every 4 m). r3d_beyond_fog is exported for
the game to skip animating what will not be drawn. Render-only: no query of the ground or the world
changes, and off is the old frame (0 pixels over 8 against 160ce96, twice per side).
Draws per frame at the overlook: 2757 off, 1104 at 175 m, 484 at 30 m.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
582 lines
29 KiB
Text
582 lines
29 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: float = 0.0, # float bits, metres
|
|
y: float = 0.0,
|
|
z: float = 0.0,
|
|
yaw: float = 0.0, # radians; 0 faces -z like the camera
|
|
scale: float = 0.0,
|
|
tint: floats,
|
|
rough: float = 0.0,
|
|
mat: floats, # the model matrix
|
|
visible: bool = true,
|
|
# Drawn and CASTING are not the same question. An actor hidden because the camera is inside
|
|
# its head still stands in the sun, and a body that stops casting the moment you look out of
|
|
# its own eyes loses its shadow - which in a game played in a sunlit basin is the one thing
|
|
# that tells you where you are standing. Set this on anything hidden for a reason the sun
|
|
# does not share; everything else keeps stopping its shadow when it is hidden.
|
|
cast_hidden: bool = false,
|
|
casts: bool = true,
|
|
id: int = 0,
|
|
row: int = -1, # where it stands in ac_actors, so it leaves in O(1); -1 off the stage
|
|
cutout: bool = false, # alpha-tested (a flame's cards)
|
|
emissive: float = 0.0, # float bits: self-lit strength
|
|
@owns(SkinClone) skin: Skin, # this instance's own pose (skin_clone); null: the model's
|
|
skin_owned: bool = false, # made by actor_own_skin: actor_release frees it
|
|
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: float = 0.0, # float bits: bounding radius for culling (0 = never culled)
|
|
cull: float = 0.0, # float bits: not drawn beyond this distance (0 = always)
|
|
outline: float = 0.0, # float bits: metres of rim drawn around it (0 = none)
|
|
ocol: floats # the rim's colour (null = white)
|
|
}
|
|
const ACTOR_POOL: int = 512
|
|
# 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_out: int = -1,
|
|
l_ocol: int = -1,
|
|
l_mh: int = -1,
|
|
scene_frame: int = -1 # the frame its scene uniforms were bound
|
|
}
|
|
|
|
|
|
function ac_prog_new(render3d_st: mut Render3dState, vs: string, fs: string, defs: string) -> AcProg {
|
|
let a = new AcProg
|
|
a.prog = r3d_program(render3d_st, vs, fs, defs)
|
|
let p = a.prog
|
|
a.l_model = gpu_uniform(render3d_st, p, "u_model"); a.l_skin = gpu_uniform(render3d_st, p, "u_skinned"); a.l_lvp = gpu_uniform(render3d_st, p, "u_light_vp")
|
|
a.l_bones = gpu_uniform(render3d_st, p, "u_bones[0]"); if a.l_bones < 0 { a.l_bones = gpu_uniform(render3d_st, p, "u_bones") }
|
|
a.l_tint = gpu_uniform(render3d_st, p, "u_tint"); a.l_rough = gpu_uniform(render3d_st, p, "u_rough_scale"); a.l_emis = gpu_uniform(render3d_st, p, "u_emissive")
|
|
a.l_view = gpu_uniform(render3d_st, p, "u_view"); a.l_proj = gpu_uniform(render3d_st, p, "u_proj"); a.l_mh = gpu_uniform(render3d_st, p, "u_model_h")
|
|
a.l_out = gpu_uniform(render3d_st, p, "u_outline"); a.l_ocol = gpu_uniform(render3d_st, p, "u_outline_col")
|
|
# the samplers never move: units 0, 1, 2
|
|
gpu_use_program(render3d_st, p)
|
|
u_i(render3d_st, gpu_uniform(render3d_st, p, "u_diff"), 0); u_i(render3d_st, gpu_uniform(render3d_st, p, "u_nrm"), 1); u_i(render3d_st, gpu_uniform(render3d_st, p, "u_arm"), 2)
|
|
return a
|
|
}
|
|
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
|
|
function actor_init(render3d_st: mut Render3dState) -> void {
|
|
# the records play takes, made now: an actor first placed in play (a fish, a sign, a bed) made its
|
|
# record, matrix and tint then (actor_release gives them back to this pool)
|
|
if render3d_st.ac_spare == null { render3d_st.ac_spare = new []Actor }
|
|
let sp = render3d_st.ac_spare
|
|
if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor }
|
|
actor_room(render3d_st.ac_actors, 4 * ACTOR_POOL)
|
|
actor_room(sp, 4 * ACTOR_POOL)
|
|
for i in 0 .. ACTOR_POOL {
|
|
let a = new Actor
|
|
a.tint = v3_new(1.0, 1.0, 1.0)
|
|
a.mat = m4_new()
|
|
push(sp, a)
|
|
}
|
|
render3d_st.ac_lit = ac_prog_new(render3d_st, "skin.vert", "model.frag", "")
|
|
render3d_st.ac_lit_cut = ac_prog_new(render3d_st, "skin.vert", "model.frag", "#define ALPHA_TEST\n")
|
|
render3d_st.ac_sh = ac_prog_new(render3d_st, "skin.vert", "shadow.frag", "#define SHADOW_PASS\n")
|
|
render3d_st.ac_sh_cut = ac_prog_new(render3d_st, "skin.vert", "shadow.frag", "#define SHADOW_PASS\n#define ALPHA_TEST\n")
|
|
render3d_st.ac_out = ac_prog_new(render3d_st, "skin.vert", "outline.frag", "#define OUTLINE\n")
|
|
render3d_st.ac_out_cut = ac_prog_new(render3d_st, "skin.vert", "outline.frag", "#define OUTLINE\n#define ALPHA_TEST\n")
|
|
let stage = render3d_st.ac_actors # made above with its room; a new valley starts empty
|
|
List.clear(stage)
|
|
}
|
|
# off the stage: the last actor takes its row, so the list stays dense and nothing is rebuilt
|
|
# (it was a new list of every other actor per removal, never freed)
|
|
function actor_remove(render3d_st: mut Render3dState, a: Actor) -> void {
|
|
let l = render3d_st.ac_actors
|
|
if l == null or a == null { return }
|
|
let r = a.row
|
|
if r < 0 or r >= len(l) or l[r].id != a.id { return }
|
|
let last = List.pop(l)
|
|
if last.id != a.id {
|
|
l[r] = last
|
|
last.row = r
|
|
}
|
|
a.row = -1
|
|
}
|
|
|
|
# back on the stage (an actor kept across a world swap)
|
|
function actor_keep(render3d_st: mut Render3dState, a: Actor) -> void {
|
|
if a == null { return }
|
|
a.row = len(render3d_st.ac_actors)
|
|
@alloc_ok("within the stage's room made at start-up (@max 2048)")
|
|
push(render3d_st.ac_actors, a)
|
|
}
|
|
# colour one named part of the model (a material name from the file)
|
|
@alloc_ok("an actor's part tint or hidden-part table, made once per actor and given back by actor_release")
|
|
function actor_tint_part(a: Actor, name: string, r: float, g: float, b: float) -> 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] = float_bits(r); a.ptint[i * 4 + 2] = float_bits(g); a.ptint[i * 4 + 3] = float_bits(b) } }
|
|
}
|
|
@alloc_ok("an actor's part tint or hidden-part table, made once per actor and given back by actor_release")
|
|
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 } }
|
|
}
|
|
|
|
@creates(Actor)
|
|
function actor_new(render3d_st: mut Render3dState, model: Model) -> Actor {
|
|
let a = actor_fresh(render3d_st)
|
|
a.model = model
|
|
a.scale = 1.0
|
|
a.rough = 1.0
|
|
render3d_st.ac_next_id += 1; a.id = render3d_st.ac_next_id
|
|
if model != null { a.radius = Math.max(model.radius, model.height) + 1.0 }
|
|
a.cull = 450.0
|
|
actor_keep(render3d_st, a)
|
|
return a
|
|
}
|
|
|
|
function actor_place(a: Actor, x: float, y: float, z: float, yaw: float) -> 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(render3d_st: mut Render3dState, ap: AcProg) -> void {
|
|
if ap.scene_frame == render3d_st.ac_frame { return }
|
|
ap.scene_frame = render3d_st.ac_frame
|
|
let p = ap.prog
|
|
gpu_use_program(render3d_st, p)
|
|
u_mat4(render3d_st, ap.l_view, render3d_st.cam_view)
|
|
u_mat4(render3d_st, ap.l_proj, render3d_st.cam_proj)
|
|
u_f(render3d_st, ap.l_mh, 0.0)
|
|
sky_bind_lighting(render3d_st, p)
|
|
shadow_bind(render3d_st, p)
|
|
fog_bind(render3d_st, p)
|
|
}
|
|
function ac_visible(render3d_st: Render3dState, a: Actor, shadow: bool) -> bool {
|
|
if a.model == null { return false }
|
|
if not a.visible and not (shadow and a.cast_hidden) { return false }
|
|
if shadow and not a.casts { return false }
|
|
# an actor with no radius given is sized generously, since a building or the dock may be one
|
|
var fr = a.radius * a.scale
|
|
if fr <= 0.0 { fr = 20.0 }
|
|
if r3d_beyond_fog(render3d_st, a.x, a.y, a.z, fr) { return false }
|
|
if a.cull != 0.0 {
|
|
let dx = a.x - render3d_st.cam_pos[0]; let dz = a.z - render3d_st.cam_pos[2]
|
|
let d2 = dx * dx + dz * dz
|
|
var c = a.cull
|
|
if shadow { c = Math.min(c, 300.0) }
|
|
if d2 > c * c { return false }
|
|
}
|
|
if not shadow and a.radius != 0.0 {
|
|
let r = a.radius * a.scale
|
|
var cy = a.y + r
|
|
# In the water reflection what reaches the picture is the actor's mirror image, 2L - y. The pass
|
|
# keeps the main camera's frustum planes, so the image is what has to be tested against them: the
|
|
# actor itself was, and the town's people - far above the lake, their images far below the frame -
|
|
# all drew again into the reflection.
|
|
if render3d_st.ter_reflect { cy = 2.0 * render3d_st.water_level - cy }
|
|
if not cam_sphere_visible(render3d_st, a.x, cy, a.z, r * 1.5) { return false }
|
|
}
|
|
return true
|
|
}
|
|
function actor_draw_one(render3d_st: mut Render3dState, a: Actor, ap: AcProg, shadow: bool) -> void {
|
|
let p = ap.prog
|
|
gpu_use_program(render3d_st, p)
|
|
u_mat4(render3d_st, ap.l_model, a.mat)
|
|
var skinned = 0.0
|
|
if a.skin != null { skinned = 1.0; u_mat4n(render3d_st, ap.l_bones, a.skin.n_joints, a.skin.bones) }
|
|
else if a.model.skin != null { skinned = 1.0; u_mat4n(render3d_st, ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
|
|
u_f(render3d_st, ap.l_skin, skinned)
|
|
if not shadow {
|
|
u_f(render3d_st, ap.l_emis, a.emissive)
|
|
u_f(render3d_st, ap.l_rough, a.rough)
|
|
}
|
|
gpu_cull(render3d_st, false)
|
|
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(render3d_st, ap.l_tint, float_from_bits(a.ptint[i * 4 + 1]), float_from_bits(a.ptint[i * 4 + 2]), float_from_bits(a.ptint[i * 4 + 3])) }
|
|
else { u_v3(render3d_st, ap.l_tint, a.tint) }
|
|
}
|
|
gpu_tex_unit(render3d_st, 0); gpu_tex_bind(render3d_st, GPU_TEX2D, pr.diff)
|
|
if not shadow {
|
|
gpu_tex_unit(render3d_st, 1); gpu_tex_bind(render3d_st, GPU_TEX2D, pr.nrm)
|
|
gpu_tex_unit(render3d_st, 2); gpu_tex_bind(render3d_st, GPU_TEX2D, pr.arm)
|
|
}
|
|
mesh_draw(render3d_st, pr.mesh)
|
|
}
|
|
}
|
|
|
|
# ---- outlining something that is not an actor ---------------------------------------------
|
|
# An Actor gets its rim from its own `outline` field. Everything else in a scene - a
|
|
# scatter instance, a tile, anything drawn by a system that owns its own transforms - had
|
|
# no way to ask for one, because the outline pass walked the actor list and nothing else.
|
|
# A tree is the case that matters: instanced scatter has no actor to hull, so a game that
|
|
# highlights whatever the crosshair is on could highlight every object in the world except
|
|
# the twenty thousand most common ones.
|
|
#
|
|
# So: a queue. `outline_model` says "put a rim round this model at this transform" from
|
|
# anywhere in the frame, and the outline pass flushes it alongside the actors' - which is
|
|
# what gets the depth test right, since the rim has to be drawn after the scene it is
|
|
# tested against and the caller does not control pass order.
|
|
#
|
|
# The rim is the model's own geometry swollen along its normals. A layer whose vertex
|
|
# shader moves its instances - wind, or standing them on the drawn terrain - will differ
|
|
# from the rim by however much that shader moved them, so pass the transform the shader
|
|
# would have arrived at (for a grounded layer, the terrain height at that instance).
|
|
property OutlineReq {
|
|
model: Model,
|
|
mat: floats,
|
|
width: float = 0.0,
|
|
r: float = 0.0,
|
|
g: float = 0.0,
|
|
b: float = 0.0
|
|
}
|
|
# A frame is drawn by more than one pass - a shadow map, a water reflection, the scene -
|
|
# and actor_draw runs in each of them. An Actor survives that because its rim is a field
|
|
# on the actor; a queue does not, if the first pass empties it. So a flush CLOSES the
|
|
# batch rather than clearing it, and the next add opens a new one: every pass in a frame
|
|
# sees the same requests, and the caller needs no frame hook.
|
|
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
|
|
function outline_model(render3d_st: mut Render3dState, m: Model, mat: floats, width: float, r: float, g: float, b: float) -> void {
|
|
if m == null or mat == null or width == 0.0 { return }
|
|
if render3d_st.ac_oq_closed { render3d_st.ac_oq_n = 0; render3d_st.ac_oq_closed = false }
|
|
if render3d_st.ac_oq == null { render3d_st.ac_oq = new []OutlineReq }
|
|
var q: OutlineReq = null
|
|
if render3d_st.ac_oq_n < len(render3d_st.ac_oq) { q = render3d_st.ac_oq[render3d_st.ac_oq_n] } else { q = new OutlineReq; q.mat = m4_new(); push(render3d_st.ac_oq, q) }
|
|
q.model = m; q.width = width; q.r = r; q.g = g; q.b = b
|
|
m4_copy(q.mat, mat)
|
|
render3d_st.ac_oq_n += 1
|
|
}
|
|
function outline_clear(render3d_st: mut Render3dState) -> void { render3d_st.ac_oq_n = 0; render3d_st.ac_oq_closed = true }
|
|
# Called at the top of every frame. A batch the last frame closed and nobody has reopened
|
|
# since is stale: the caller stopped asking. Without this it was drawn for ever - flush
|
|
# closes a batch but only the NEXT outline_model call empties it, so looking away from a
|
|
# highlighted tree left its rim on until something else was highlighted.
|
|
function outline_frame(render3d_st: mut Render3dState) -> void { if render3d_st.ac_oq_closed { render3d_st.ac_oq_n = 0 } }
|
|
|
|
# R3D_ACTOR_CENSUS=<frame>: at that frame, the lit pass's actors grouped by model - skinned or rigid,
|
|
# how many share it, the primitives each draws - so instancing is aimed at what repeats
|
|
@alloc_ok("a debug census and dumps, only under their switches")
|
|
function actor_census(render3d_st: Render3dState) -> void {
|
|
let keys = new []Model
|
|
let cnt = new []int
|
|
let prims = new []int
|
|
let rigid = new []int
|
|
for i in 0 .. len(render3d_st.ac_actors) {
|
|
let a = render3d_st.ac_actors[i]
|
|
if not ac_visible(render3d_st, a, false) { continue }
|
|
var k = -1
|
|
for j in 0 .. len(keys) { if keys[j] == a.model { k = j } }
|
|
if k < 0 { push(keys, a.model); push(cnt, 0); push(prims, 0); var r = 1; if a.model.skin != null { r = 0 }; push(rigid, r); k = len(keys) - 1 }
|
|
if a.skin != null { rigid[k] = 0 }
|
|
cnt[k] += 1
|
|
var shown = 0
|
|
for p in 0 .. len(a.model.prims) { if a.hide == null or a.hide[p] == 0 { shown += 1 } }
|
|
prims[k] += shown
|
|
}
|
|
var total = 0
|
|
for j in 0 .. len(keys) {
|
|
var kind = "skinned"
|
|
if rigid[j] == 1 { kind = "rigid" }
|
|
print(`actor census: model {j} ({kind}, {len(keys[j].prims)} prims, {keys[j].tris} tris): {cnt[j]} actors, {prims[j]} draws`)
|
|
total += prims[j]
|
|
}
|
|
print(`actor census: {len(keys)} models, {total} lit draws`)
|
|
}
|
|
function actor_draw(render3d_st: mut Render3dState) -> void {
|
|
if render3d_st.ac_actors == null { return }
|
|
render3d_st.ac_frame += 1
|
|
if render3d_st.ac_census_at == -2 { render3d_st.ac_census_at = -1; if r3d_env_has(render3d_st, "R3D_ACTOR_CENSUS") { render3d_st.ac_census_at = Text.to_int(r3d_env(render3d_st, "R3D_ACTOR_CENSUS")) } }
|
|
if render3d_st.ac_census_at > 0 and render3d_st.ac_frame == render3d_st.ac_census_at { actor_census(render3d_st) }
|
|
for i in 0 .. len(render3d_st.ac_actors) {
|
|
let a = render3d_st.ac_actors[i]
|
|
if not ac_visible(render3d_st, a, false) { continue }
|
|
var ap = render3d_st.ac_lit
|
|
if a.cutout { ap = render3d_st.ac_lit_cut }
|
|
ac_bind_scene(render3d_st, ap)
|
|
actor_draw_one(render3d_st, a, ap, false)
|
|
}
|
|
actor_draw_outlines(render3d_st)
|
|
gpu_cull(render3d_st, true)
|
|
}
|
|
# The rim around a highlighted actor: the model again, its vertices pushed out along their
|
|
# normals (skin.vert, OUTLINE) and its front faces culled, so only the far side of the
|
|
# swollen shell survives — a silhouette exactly `outline` metres wide. Depth-tested
|
|
# against the scene, so anything in front of the actor hides its rim too.
|
|
function actor_draw_outlines(render3d_st: mut Render3dState) -> void {
|
|
var any = render3d_st.ac_oq_n > 0
|
|
for i in 0 .. len(render3d_st.ac_actors) { if render3d_st.ac_actors[i].outline != 0.0 and ac_visible(render3d_st, render3d_st.ac_actors[i], false) { any = true; break } }
|
|
if not any { return }
|
|
gpu_cull(render3d_st, true)
|
|
gpu_cull_face(render3d_st, GL_FRONT)
|
|
for i in 0 .. len(render3d_st.ac_actors) {
|
|
let a = render3d_st.ac_actors[i]
|
|
if a.outline == 0.0 or not ac_visible(render3d_st, a, false) { continue }
|
|
var ap = render3d_st.ac_out
|
|
if a.cutout { ap = render3d_st.ac_out_cut }
|
|
gpu_use_program(render3d_st, ap.prog)
|
|
u_mat4(render3d_st, ap.l_view, render3d_st.cam_view); u_mat4(render3d_st, ap.l_proj, render3d_st.cam_proj)
|
|
u_f(render3d_st, ap.l_out, a.outline * a.scale)
|
|
if a.ocol != null { u_v3(render3d_st, ap.l_ocol, a.ocol) } else { u_f3(render3d_st, ap.l_ocol, 1.0, 1.0, 1.0) }
|
|
actor_draw_outline_one(render3d_st, a, ap)
|
|
}
|
|
# and whatever asked for a rim without being an actor
|
|
for i in 0 .. render3d_st.ac_oq_n {
|
|
let q = render3d_st.ac_oq[i]
|
|
let ap = render3d_st.ac_out
|
|
gpu_use_program(render3d_st, ap.prog)
|
|
u_mat4(render3d_st, ap.l_view, render3d_st.cam_view); u_mat4(render3d_st, ap.l_proj, render3d_st.cam_proj)
|
|
u_f(render3d_st, ap.l_out, q.width)
|
|
u_f3(render3d_st, ap.l_ocol, q.r, q.g, q.b)
|
|
u_mat4(render3d_st, ap.l_model, q.mat)
|
|
u_f(render3d_st, ap.l_skin, 0.0)
|
|
for k in 0 .. len(q.model.prims) { mesh_draw(render3d_st, q.model.prims[k].mesh) }
|
|
}
|
|
render3d_st.ac_oq_closed = true
|
|
gpu_cull_face(render3d_st, GL_BACK)
|
|
}
|
|
function actor_draw_outline_one(render3d_st: mut Render3dState, a: Actor, ap: AcProg) -> void {
|
|
u_mat4(render3d_st, ap.l_model, a.mat)
|
|
var skinned = 0.0
|
|
if a.skin != null { skinned = 1.0; u_mat4n(render3d_st, ap.l_bones, a.skin.n_joints, a.skin.bones) }
|
|
else if a.model.skin != null { skinned = 1.0; u_mat4n(render3d_st, ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
|
|
u_f(render3d_st, ap.l_skin, skinned)
|
|
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 a.cutout { gpu_tex_unit(render3d_st, 0); gpu_tex_bind(render3d_st, GPU_TEX2D, pr.diff) }
|
|
mesh_draw(render3d_st, pr.mesh)
|
|
}
|
|
}
|
|
# Is a sphere inside this cascade's light box? The projection is orthographic, so a point's
|
|
# clip x and y are the matrix applied to it, and the sphere's reach along each is bounded by the
|
|
# three axis offsets. An actor outside the box casts nothing into that layer: skipping it changes no
|
|
# depth, only the draw. (Each actor used to draw into every cascade out to 300 m - in town that was
|
|
# about 1500 skinned shadow draws a frame against 69 in the lit pass.)
|
|
function ac_in_light(render3d_st: mut Render3dState, vp: floats, x: float, y: float, z: float, r: float) -> bool {
|
|
m4_xform_point(render3d_st.ac_lp0, vp, x, y, z)
|
|
m4_xform_point(render3d_st.ac_lpx, vp, x + r, y, z)
|
|
m4_xform_point(render3d_st.ac_lpy, vp, x, y + r, z)
|
|
m4_xform_point(render3d_st.ac_lpz, vp, x, y, z + r)
|
|
for k in 0 .. 2 {
|
|
let c = render3d_st.ac_lp0[k]
|
|
let e = ac_abs(render3d_st.ac_lpx[k] - c) + ac_abs(render3d_st.ac_lpy[k] - c) + ac_abs(render3d_st.ac_lpz[k] - c)
|
|
if ac_abs(c) - e > 1.0 { return false }
|
|
}
|
|
return true
|
|
}
|
|
function ac_abs(v: float) -> float { return Math.max(v, -v) }
|
|
|
|
function actor_draw_casters(render3d_st: mut Render3dState, light_vp: floats) -> void {
|
|
if render3d_st.ac_actors == null { return }
|
|
gpu_use_program(render3d_st, render3d_st.ac_sh.prog); u_mat4(render3d_st, render3d_st.ac_sh.l_lvp, light_vp)
|
|
gpu_use_program(render3d_st, render3d_st.ac_sh_cut.prog); u_mat4(render3d_st, render3d_st.ac_sh_cut.l_lvp, light_vp)
|
|
for i in 0 .. len(render3d_st.ac_actors) {
|
|
let a = render3d_st.ac_actors[i]
|
|
if not ac_visible(render3d_st, a, true) { continue }
|
|
if a.radius != 0.0 {
|
|
# the radius is the horizontal extent: a person is far taller than wide, so the sphere is
|
|
# centred at half the model's height and reaches the larger of the two, with a margin for
|
|
# the pose (a first version centred it at the radius and dropped a head at a cascade's edge)
|
|
let hh = a.model.height * 0.5 * a.scale
|
|
let r = Math.max(a.radius * a.scale, hh)
|
|
if not ac_in_light(render3d_st, light_vp, a.x, a.y + hh, a.z, r * 1.5) { continue }
|
|
}
|
|
# Inside the light box is not the same as able to shade anything this cascade covers: every actor
|
|
# within 300 m sits inside the far cascades' boxes, whose receivers start 250 and 1100 m out - at
|
|
# the camp 160 actors cast 300 draws into each of those, against 41 into the nearest.
|
|
let dxa = a.x - render3d_st.cam_pos[0]; let dza = a.z - render3d_st.cam_pos[2]
|
|
let da = Math.sqrt(dxa * dxa + dza * dza)
|
|
let ra = a.radius * a.scale
|
|
if not cast_band_reaches(render3d_st, Math.max(da - ra, 0.0), da + ra, a.model.height * a.scale) { continue }
|
|
var ap = render3d_st.ac_sh
|
|
if a.cutout { ap = render3d_st.ac_sh_cut }
|
|
if render3d_st.ac_census_at > 0 and render3d_st.ac_frame == render3d_st.ac_census_at { ac_caster_count(render3d_st, a) }
|
|
if ac_proxy_figure(render3d_st, a) { actor_draw_proxy(render3d_st, a) } else { actor_draw_one(render3d_st, a, ap, true) }
|
|
}
|
|
if render3d_st.ac_census_at > 0 and render3d_st.ac_frame == render3d_st.ac_census_at {
|
|
print(`actor census, cascade {render3d_st.sh_cascade}: {render3d_st.ac_cc_actors} actors cast {render3d_st.ac_cc_draws} draws ({render3d_st.ac_cc_skinned} of them skinned, {render3d_st.ac_cc_cut} cutout)`)
|
|
render3d_st.ac_cc_actors = 0; render3d_st.ac_cc_draws = 0; render3d_st.ac_cc_skinned = 0; render3d_st.ac_cc_cut = 0
|
|
}
|
|
prof_cpu_mark(render3d_st, "shadow actors")
|
|
}
|
|
function ac_caster_count(render3d_st: mut Render3dState, a: Actor) -> void {
|
|
var shown = 0
|
|
for p in 0 .. len(a.model.prims) { if a.hide == null or a.hide[p] == 0 { shown += 1 } }
|
|
render3d_st.ac_cc_actors += 1
|
|
render3d_st.ac_cc_draws += shown
|
|
if a.skin != null or a.model.skin != null { render3d_st.ac_cc_skinned += shown }
|
|
if a.cutout { render3d_st.ac_cc_cut += shown }
|
|
}
|
|
|
|
# every actor off the stage at once, for a world being replaced. Actors own no GL objects;
|
|
# their models belong to whoever loaded them.
|
|
function actor_clear_all(render3d_st: mut Render3dState) -> void {
|
|
if render3d_st.ac_actors == null { return }
|
|
for i in 0 .. len(render3d_st.ac_actors) { render3d_st.ac_actors[i].row = -1 }
|
|
List.clear(render3d_st.ac_actors)
|
|
outline_clear(render3d_st)
|
|
}
|
|
|
|
# A record as `new Actor` makes it, with its matrix and tint: a released one when there is one.
|
|
@alloc_ok("the actor pool ran dry: one more record, which actor_release keeps in the pool after")
|
|
function actor_fresh(render3d_st: mut Render3dState) -> Actor {
|
|
let sp = render3d_st.ac_spare
|
|
if sp == null or len(sp) == 0 {
|
|
let a = new Actor
|
|
a.tint = v3_new(1.0, 1.0, 1.0)
|
|
a.mat = m4_new()
|
|
return a
|
|
}
|
|
let a = List.pop(sp)
|
|
a.x = 0.0; a.y = 0.0; a.z = 0.0; a.yaw = 0.0; a.scale = 0.0; a.rough = 0.0
|
|
a.tint[0] = 1.0; a.tint[1] = 1.0; a.tint[2] = 1.0
|
|
m4_identity(a.mat)
|
|
a.visible = true; a.cast_hidden = false; a.casts = true; a.row = -1
|
|
a.cutout = false; a.emissive = 0.0; a.radius = 0.0; a.cull = 0.0; a.outline = 0.0
|
|
a.skin = null; a.skin_owned = false; a.ocol = null
|
|
return a
|
|
}
|
|
|
|
# Off the stage for good, and its record kept for the next actor_new: whoever released it never
|
|
# touches it again. What it was lent stays its lender's (a skin clone, a rim colour); what it made
|
|
# for itself (a part's tint, a part hidden) goes.
|
|
@releases(Actor)
|
|
function actor_release(render3d_st: mut Render3dState, a: Actor) -> void {
|
|
if a == null or a.id == 0 { return } # released already
|
|
actor_remove(render3d_st, a)
|
|
if a.ptint != null { free(a.ptint); a.ptint = null }
|
|
if a.hide != null { free(a.hide); a.hide = null }
|
|
if a.skin_owned and a.skin != null { skin_clone_free(a.skin) }
|
|
a.skin_owned = false
|
|
a.model = null; a.skin = null; a.ocol = null
|
|
a.id = 0
|
|
if render3d_st.ac_spare == null { return } # made in actor_init, with its room
|
|
@alloc_ok("within the spares' room made at start-up (@max 2048)")
|
|
push(render3d_st.ac_spare, a)
|
|
}
|
|
|
|
# The actor poses on its own copy of its model's skeleton (a person, an animal), which it keeps until
|
|
# actor_release gives it back with the record: a clone made by the caller and dropped with the actor
|
|
# was one kept skeleton per figure ever placed.
|
|
function actor_own_skin(a: Actor) -> Skin {
|
|
if a == null or a.model == null or a.model.skin == null { return null }
|
|
if a.skin_owned and a.skin != null { skin_clone_free(a.skin) }
|
|
a.skin = skin_clone(a.model.skin)
|
|
a.skin_owned = true
|
|
return a.skin
|
|
}
|
|
|
|
# room for n more actors in a list, made at start-up: filled and emptied, it keeps what it grew to
|
|
function actor_room(l: []Actor, n: int) -> void {
|
|
let have = len(l)
|
|
for i in 0 .. n { push(l, null) }
|
|
while len(l) > have { List.pop(l) }
|
|
}
|
|
|
|
# ---- a figure's shadow past 60 m: one capsule ---------------------------------------------------
|
|
# A dressed figure is up to 86 skinned pieces, and the far cascades (2 and out: receivers from 60 m)
|
|
# drew each piece of every walker in them - most of a forest frame's shadow pass. Nobody can tell a
|
|
# garment's shadow from the body's there, so a figure casts one capsule its own height instead.
|
|
# Animals (a piece or two) and anything rigid keep their own shape.
|
|
const AC_PROXY_CASCADE: int = 2
|
|
const AC_PROXY_SIDES: int = 8
|
|
const AC_PROXY_R: float = 0.13 # the capsule's radius, as a fraction of the figure's height
|
|
function ac_proxy_figure(render3d_st: Render3dState, a: Actor) -> bool {
|
|
if render3d_st.sh_cascade < AC_PROXY_CASCADE { return false }
|
|
if a.skin == null and a.model.skin == null { return false }
|
|
return len(a.model.prims) > 8
|
|
}
|
|
function actor_draw_proxy(render3d_st: mut Render3dState, a: Actor) -> void {
|
|
let m = ac_proxy_mesh(render3d_st)
|
|
let ap = render3d_st.ac_sh
|
|
gpu_use_program(render3d_st, ap.prog)
|
|
let h = a.model.height
|
|
m4_scaling(render3d_st.ac_proxy_s, h * AC_PROXY_R, h, h * AC_PROXY_R)
|
|
m4_mul(render3d_st.ac_proxy_m, a.mat, render3d_st.ac_proxy_s)
|
|
u_mat4(render3d_st, ap.l_model, render3d_st.ac_proxy_m)
|
|
u_f(render3d_st, ap.l_skin, 0.0)
|
|
gpu_cull(render3d_st, false)
|
|
mesh_draw(render3d_st, m)
|
|
}
|
|
# a unit capsule (radius 1, height 1, feet at the origin): a ring of AC_PROXY_SIDES at each end of the
|
|
# body and a cone to a point below and above them
|
|
@alloc_ok("made once: the capsule every far figure casts, and its two matrices")
|
|
function ac_proxy_mesh(render3d_st: mut Render3dState) -> Mesh {
|
|
if render3d_st.ac_proxy != null { return render3d_st.ac_proxy }
|
|
render3d_st.ac_proxy_s = m4_new()
|
|
render3d_st.ac_proxy_m = m4_new()
|
|
let n = AC_PROXY_SIDES
|
|
let nv = 2 + 2 * n
|
|
let pos = gl_floats(nv * 3)
|
|
let nrm = gl_floats(nv * 3)
|
|
let uv = gl_floats(nv * 2)
|
|
ac_proxy_vert(pos, nrm, 0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0)
|
|
ac_proxy_vert(pos, nrm, 1, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0)
|
|
for i in 0 .. n {
|
|
let t = float(i) * 6.2831853 / float(n)
|
|
let cx = Math.cos(t)
|
|
let sz = Math.sin(t)
|
|
ac_proxy_vert(pos, nrm, 2 + i, cx, AC_PROXY_R, sz, cx, 0.0, sz)
|
|
ac_proxy_vert(pos, nrm, 2 + n + i, cx, 1.0 - AC_PROXY_R, sz, cx, 0.0, sz)
|
|
}
|
|
for i in 0 .. nv * 2 { gl_put_bits(uv, i, float_bits(0.0)) }
|
|
let m = gpu_mesh_new(render3d_st)
|
|
gpu_mesh_vertices(render3d_st, m, pos, gl_bytes_of(nv * 3), GPU_STATIC)
|
|
gpu_mesh_attr(render3d_st, m, 0, 3, GPU_F32, 0, 0, false)
|
|
gpu_mesh_vertices(render3d_st, m, nrm, gl_bytes_of(nv * 3), GPU_STATIC)
|
|
gpu_mesh_attr(render3d_st, m, 1, 3, GPU_F32, 0, 0, false)
|
|
gpu_mesh_vertices(render3d_st, m, uv, gl_bytes_of(nv * 2), GPU_STATIC)
|
|
gpu_mesh_attr(render3d_st, m, 2, 2, GPU_F32, 0, 0, false)
|
|
free(pos)
|
|
free(nrm)
|
|
free(uv)
|
|
let ni = n * 12
|
|
let idx = words(ni)
|
|
var k = 0
|
|
for i in 0 .. n {
|
|
let j = (i + 1) % n
|
|
let b0 = 2 + i
|
|
let b1 = 2 + j
|
|
let t0 = 2 + n + i
|
|
let t1 = 2 + n + j
|
|
idx[k] = 0; idx[k + 1] = b0; idx[k + 2] = b1
|
|
idx[k + 3] = b0; idx[k + 4] = t0; idx[k + 5] = t1
|
|
idx[k + 6] = b0; idx[k + 7] = t1; idx[k + 8] = b1
|
|
idx[k + 9] = 1; idx[k + 10] = t1; idx[k + 11] = t0
|
|
k += 12
|
|
}
|
|
gpu_mesh_indices(render3d_st, m, data_of(idx), ni * 4, 4)
|
|
free(idx)
|
|
m.count = ni
|
|
gpu_mesh_done(render3d_st, m)
|
|
render3d_st.ac_proxy = m
|
|
return m
|
|
}
|
|
function ac_proxy_vert(pos: pointer, nrm: pointer, v: int, x: float, y: float, z: float, nx: float, ny: float, nz: float) -> void {
|
|
gl_put_bits(pos, v * 3, float_bits(x)); gl_put_bits(pos, v * 3 + 1, float_bits(y)); gl_put_bits(pos, v * 3 + 2, float_bits(z))
|
|
gl_put_bits(nrm, v * 3, float_bits(nx)); gl_put_bits(nrm, v * 3 + 1, float_bits(ny)); gl_put_bits(nrm, v * 3 + 2, float_bits(nz))
|
|
}
|