# ============================================================================ # 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 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: 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 } 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 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") render3d_st.ac_actors = new []Actor } # 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 } if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor } a.row = len(render3d_st.ac_actors) push(render3d_st.ac_actors, a) } # colour one named part of the model (a material name from the file) 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) } } } 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(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 } 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. 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=: 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 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 { if render3d_st.ac_lp0 == null { render3d_st.ac_lp0 = floats(3); render3d_st.ac_lpx = floats(3); render3d_st.ac_lpy = floats(3); render3d_st.ac_lpz = floats(3) } 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) } 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 { render3d_st.ac_actors = new []Actor } 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. 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.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. 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 } a.model = null; a.skin = null; a.ocol = null a.id = 0 if render3d_st.ac_spare == null { render3d_st.ac_spare = new []Actor } push(render3d_st.ac_spare, a) }