1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/assets/office/optimus.ts
T
karti 51979feea0 Shadows land on the building, and the sky layers stop repeating themselves
**Shadows were the right size and pointed at nothing.** Last round fixed the
missing `updateProjectionMatrix()`, so the frustum finally became the size
every caller asks for — but nothing aimed it, and `sun.target` sits at the
world origin. A pack's origin is the **north-west corner of its slab**, so
for lumbridge-hq the box was off-centre by half the building: 14.4 m of a
48 m plate, about a third of the floor, fell outside the frustum and
neither cast nor received. Invisible while three's broken ±5 default made
shadows useless everywhere; obvious the moment they started working.

`SceneKitOptions` takes a `shadowTarget` now, both callers pass one, and
the light's target is added to the scene — which is the part that actually
matters, because `LightShadow.updateMatrices` reads `target.matrixWorld`
and an unparented `Object3D` is never reached by the traversal that
updates it. The sun is also placed relative to the target rather than the
origin, so light-to-target is exactly `sunDistance` for every direction,
which is the invariant each caller's `shadowNear`/`shadowFar` were chosen
against.

**`flights.ts` could not be tested, and that is why it was untested.** It
used a TypeScript parameter property — the one piece of TS syntax that
*emits code* rather than annotating a type — so Node's type stripping
refused the whole module. The bundler never cared, so nobody found out
until the first `node --test` file tried to import it. The module carrying
the worst bug this project has shipped was, by construction, the one
module that could not have a test. It has eleven now, including one that
fails if the live-aircraft repeat-skip is removed.

**Robots are on the plan panel** — a turned marker with a bow for heading,
in the one hue left that is neither the people-blue nor the camera-amber.

Review findings cleared across the four new sky/robot modules: a real
24 mm void at the ankle and an 8 mm hole through each forearm, a
per-frame allocation in the robot heading picker, a per-frame sort in the
starlink ranking, `uTime` growing unbounded until the cloud breath
quantises, and `DAY_REFERENCE`'s derivation which did not reproduce.

`createStarlinkMeshLayer` now takes a **board** radius — the same unit its
sibling takes — instead of a dome radius with nothing in the types to tell
them apart. That is the exact confusion that has already caused one real
bug here. `DOME_RADIUS_FACTOR` has one owner and is imported, not copied:
the points and the meshes must be on the same dome or a satellite that
grows geometry also jumps.

Several comments were wrong rather than merely stale — a fabricated claim
about `Object3D.clone`, a fabricated attribution to `Plan`, an inverted
`DoubleSide` argument, a triangle ledger citing a function that no longer
exists, and a defensive-call rationale that contradicted the paragraph
above it. In a codebase where the comments are the design record, those
are defects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 02:50:25 -07:00

1034 lines
48 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* A Tesla Optimus, procedurally, as a rig you can pose — plus the merged static
* version for a pack that just wants one standing in a corner.
*
* This is the first asset in the library that is **not** a single merged mesh,
* and the reason is the only reason that would justify it: something has to walk
* it around the office (`interiors/robots.ts`), and a walk cycle needs limbs
* that move independently. `parts.ts` says it plainly — anything that has to
* move on its own belongs in its own object rather than in a bin — so the figure
* is eleven small bins, one per articulating joint, each merged internally and
* hung off the joint it belongs to. Eighteen meshes per robot rather than two.
* That price is stated again, in draw calls, at the bottom of this comment and
* in `robots.ts`, because it is the whole cost of the feature.
*
* ### The figure
*
* Gen 3 from reference knowledge, and the proportions are what carry it, because
* ten metres is the distance this is normally seen from and at ten metres a
* silhouette is all there is. What has to be right, in order of how much it
* matters:
*
* 1. **1.73 m and slim.** Optimus is human-height and noticeably narrower than a
* human — a 0.35 m shoulder span on a 1.73 m frame. Build it at human width
* and it reads as a person in a costume.
* 2. **The waist.** The single most identifying line: a wide pelvis, a wide
* chest, and a genuinely thin dark column between them. It is 0.15 m across
* where the pelvis is 0.28 and the chest is 0.34. Widen it and the whole
* thing turns into a mannequin.
* 3. **The visor.** A smooth black panel filling the front of a small pale head,
* with no features on it at all. Two eyes, a mouth line, a "friendly" curve —
* any of them and it stops being Optimus. It has to *follow the head*: a flat
* plate on a rounded shell overhangs at its corners, and a face with the
* corners of its visor hanging in the air beside it is the single most
* conspicuous thing this figure can get wrong. See `emitHead`.
* 4. **Pale shells over a dark frame.** Every limb is a light shell that stops
* short of the joint, with dark structure showing in the gap. That gap is
* what makes it read as a machine rather than as a white plastic doll, and it
* costs nothing but a few millimetres of geometry. The ankle, knee, hip,
* wrist and elbow all have one; the shoulder has a dark drum showing past the
* edge of its cap instead, for a draw-call reason written out at the drum.
* Whatever else changes, do not let two pale parts meet each other — and do
* not let them merely *touch*, either. Every join in this file overlaps by a
* few millimetres, because parts that share a face z-fight along it and parts
* that stop at the same plane by arithmetic eventually stop a millimetre
* short instead. Both of those have happened here.
* 5. The knee actuator, the shoulder caps, the five-fingered hands. Detail, not
* silhouette. Present because they are cheap, not because they are load-
* bearing.
*
* ### Two materials, and why not three
*
* `paper` for the shells and `screenBezel` for the dark frame and the visor.
* Both are borrowed — `materials.ts` has a closed role list with no robot in it,
* and inventing a role is not on offer — so the borrow is chosen to survive a
* self-hoster recolouring the palette. `paper` is the library's palest
* untextured neutral, which is exactly what an Optimus shell is; `screenBezel`
* is its near-black, and the visor genuinely *is* a bezel, so a self-hoster who
* darkens their screen surrounds darkens the robot's face, which is the right
* coupling rather than a coincidental one.
*
* `metalTrim` was tried as a third material for the joint barrels and dropped.
* The office rig carries no environment map, so a `metalness: 0.85` role has
* nothing to reflect and renders as a dull dark grey — indistinguishable from
* `screenBezel` at ten metres — while costing another mesh in nine of the eleven
* groups. Two materials, eighteen meshes.
*
* ### The indexed/non-indexed rule bites here harder than anywhere else
*
* `common.ts` warns that every part under one material must be all-indexed or
* all-non-indexed or `mergeGeometries` silently drops the material. With
* eighteen bins there are eighteen chances to get it wrong and the symptom is an
* invisible shin, so the split is made structural rather than remembered:
*
* - **`shell` is drawn with `roundedBox()` and nothing else.** `ExtrudeGeometry`,
* never indexed. Every pale part of the robot is a rounded box, which is also
* what Optimus actually looks like — flat-sided shells with softened edges,
* not tubes.
* - **`frame` is drawn with `box()` and `cylinder()` and nothing else.** Both
* indexed. This is why the visor is five flat boxes fitted round the head's
* corner rather than one moulded shell: it is drawn in the dark material, so
* it may not be an extrusion, so the curve has to be faceted by hand.
*
* Add a part and put it in the material whose primitive class it already
* belongs to. If you cannot, you want the *other* material, and it will usually
* turn out to look better there anyway.
*
* ### The joint frame
*
* The figure faces **Z at yaw 0**, same as every other asset (`common.ts`), and
* the origin is on the floor between the feet. Consequences worth writing down,
* because getting a sign wrong here produces a robot that walks backwards
* through its own knees:
*
* - **+X is the robot's right, X its left.** Stand behind it looking the way it
* faces — which is looking down Z, the three.js default view direction — and
* its right hand is on your right, which is +X. So `hipR` is at `x > 0`.
* - **Every joint group's local axes are the root's axes.** No rotation is baked
* into a joint; a rig with every rotation at zero is a figure standing to
* attention. `OPTIMUS_REST` is applied on top of that, by `buildOptimus`, and
* is what "standing still" means to the walk cycle.
* - **`rotation.x` is the sagittal hinge and positive swings the limb forward.**
* A limb hangs along Y; `Rx(θ)` sends (0,1,0) to (0,cos θ,sin θ), and Z
* is the front. So `hipL.rotation.x = +0.4` is a leg reaching forward and
* `shoulderR.rotation.x = +0.4` is a hand swung forward.
* - **A knee bends the other way, so `knee.rotation.x` must stay ≤ 0.** The shin
* folds backwards, toward +Z. Positive values hyperextend it, and because
* nothing clamps them, a sign slip in an animation shows up as a robot with
* its knees on backwards rather than as an error. This is the asymmetry to
* remember: hip, shoulder and elbow all bend positive; the knee is the one
* that bends negative.
* - **An elbow bends positive.** The forearm folds forward, toward the chest,
* which is Z. It shares its sign with the shoulder and not with the knee.
* - **`rotation.y` is the transverse twist and positive turns left**
* (counter-clockwise from above — the house `Yaw` sense, unconverted). Used on
* `pelvis` and `torso` for the counter-rotation of a walk.
* - **`rotation.z` is the frontal lean and positive leans left.** `Rz(θ)` sends
* up (0,1,0) toward X. Used on `torso` for sway and on `shoulder` for the
* outward splay that keeps the hands off the hips.
* - **`pelvis.position.y` is the body's height above the floor** and starts at
* `OPTIMUS.hipY`. Everything else hangs off it, feet included, so lowering the
* pelvis lowers the whole robot — which is what a walk's vertical oscillation
* wants and is why the bob is applied there and nowhere else.
*
* ### What it costs
*
* Eighteen meshes and about 7,100 triangles per figure. The mesh count is the
* one that matters and it is fixed by the eleven bins and the two materials;
* the triangles are almost all `roundedBox`, which is 236 of them whatever size
* it is drawn at and however tight its bevel. That flat rate is worth knowing
* before adding shell detail: a 14 mm finger and a 340 mm chest cost the same,
* which is why the fingers were a quarter of the figure until somebody counted
* (see `DIGITS`). `buildOptimus` is
* meant to be called **once**; `cloneOptimus` gives you another figure sharing
* every geometry and both materials, which is how four robots cost four times
* the draw calls but one times the memory. Seventy-two draw calls for a crowd of
* four is real money against an office that draws in about thirty, and it is the
* number to look at first if a floor starts dropping frames.
*
* The `tera:robot.optimus` asset is the other end of that trade: it flattens the
* same rig into one mesh per material, so a pack that wants a robot standing
* still gets two draw calls and no articulation. `furnishings.ts` instances per
* mesh, so twenty static robots in a pack still cost those two.
*/
import * as THREE from "three";
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
import { defineAsset, type AssetContext } from "../kit.ts";
import type { SurfaceMaterial } from "../materials.ts";
import { MeshBin, type PartBin } from "../parts.ts";
/**
* Every dimension the figure and the walk cycle both have to agree on, in
* metres above the sole, in the rest pose.
*
* Exported because `robots.ts` needs `hipY` to bob the pelvis around and would
* otherwise carry a second copy of a number that must not drift. Everything
* else — shell widths, bevel radii, how far a finger sticks out — is local to
* the emitters below and deliberately not part of the contract.
*/
export const OPTIMUS = {
/** Sole to crown. Gen 3 is quoted at about 1.73 m. */
height: 1.73,
ankleY: 0.085,
kneeY: 0.5,
hipY: 0.92,
/** Top of the pelvis shell; the waist column starts here and the torso pivots here. */
waistY: 1.05,
/** Bottom of the chest shell. The 0.10 m between this and `waistY` is the slim bit. */
chestY: 1.15,
elbowY: 1.12,
wristY: 0.865,
shoulderY: 1.4,
/** Top of the chest shell, where the neck column starts. */
neckY: 1.43,
/** The head's pivot: the top of the neck, so a nod hinges where a neck does. */
headY: 1.505,
/** Half the distance between the two hip pivots. A narrow stance; Optimus has one. */
hipHalf: 0.085,
/** Half the distance between the two shoulder pivots. */
shoulderHalf: 0.175,
} as const;
/**
* What "standing still" is, in radians, applied by `buildOptimus` and used as
* the base every animated angle is added to.
*
* These are not zero because a figure with every joint at zero stands with its
* arms welded to its hips and its legs locked straight, which looks like a
* mannequin rather than like a machine that is idling. A few degrees of elbow
* and a few of outward arm splay is the whole difference.
*
* `shoulderZ` is a magnitude: it is applied as `+` on the right shoulder and ``
* on the left, because positive `rotation.z` swings a hanging arm toward +X and
* +X is the robot's right — so the same sign splays one arm out and tucks the
* other one in.
*
* `hipX` and `kneeX` cancel, and that is not a coincidence: nothing here moves
* the ankle, so the foot's pitch is the *sum* of the two, and any sum but zero
* stands the robot on its heels or its toes. The first draft had a 0.01 hip and
* a 0.05 knee and buried six millimetres of toe in the carpet. Change one of
* these and change the other. The residue is that a bent leg is fractionally
* shorter than a straight one, so the soles float about a millimetre — which is
* a millimetre, and cheaper than a third joint to correct it.
*/
export const OPTIMUS_REST = {
hipX: 0.04,
/** Negative, and exactly `hipX`. A knee only bends one way; see above. */
kneeX: -0.04,
shoulderX: 0.03,
shoulderZ: 0.075,
elbowX: 0.16,
} as const;
/**
* The joints a walk cycle drives, resolved once so an animation never has to
* search the scene graph per frame.
*
* `pelvis` carries the legs and the body's height; `torso` is a child of
* `pelvis` and carries everything above the waist, which is what lets the
* shoulders counter-rotate against the hips without the legs coming with them.
*/
export interface OptimusJoints {
/** Body height and hip twist. Pivots at `OPTIMUS.hipY`; the legs hang off it. */
pelvis: THREE.Group;
/** Sway, lean and shoulder counter-twist. Pivots at the waist. */
torso: THREE.Group;
head: THREE.Group;
hipL: THREE.Group;
hipR: THREE.Group;
kneeL: THREE.Group;
kneeR: THREE.Group;
shoulderL: THREE.Group;
shoulderR: THREE.Group;
elbowL: THREE.Group;
elbowR: THREE.Group;
}
export interface OptimusRig {
/** Origin on the floor between the feet, facing Z at `rotation.y === 0`. */
root: THREE.Group;
joints: OptimusJoints;
}
/** The two materials the whole figure is drawn in. See the header for the borrow. */
interface Skin {
shell: SurfaceMaterial;
frame: SurfaceMaterial;
}
/** A point in whichever joint frame the emitter is drawing into. */
interface Anchor {
x: number;
y: number;
z: number;
}
const JOINT_NAMES = [
"pelvis",
"torso",
"head",
"hipL",
"hipR",
"kneeL",
"kneeR",
"shoulderL",
"shoulderR",
"elbowL",
"elbowR",
] as const;
// ---- Emitters -------------------------------------------------------------
//
// Each of these draws one body part into a bin, in that bin's own frame, with
// `a` naming the joint centre the part hangs from. They are written this way so
// that the rig and the flattened asset are the *same* geometry rather than two
// bodies that will drift apart the first time somebody widens a shin.
/**
* A dark joint barrel lying across the body, centred on `a`.
*
* A unit cylinder stands along +Y with its base at the placement point, so a
* roll of +π/2 sends its length along X — which means the placement has to sit
* half a length to the *right* of where the barrel should end up. Getting that
* offset wrong puts every joint on the robot half a barrel off-centre, which is
* subtle enough to survive a first look and obvious enough to ruin the second.
*/
function barrel(
bin: MeshBin,
P: PartBin,
material: SurfaceMaterial,
a: Anchor,
diameter: number,
length: number,
): void {
bin.add(P.cylinder(12), material, {
x: a.x + length / 2,
y: a.y,
z: a.z,
size: [diameter, length, diameter],
roll: Math.PI / 2,
});
}
/** The pelvis block, and the hip axle the legs turn on. Drawn in the pelvis frame. */
function emitPelvis(bin: MeshBin, P: PartBin, s: Skin): void {
// Straddles the hip line: down to 0.855 m and up to the waist at 1.05 m. The
// width is what the slim waist above it is measured against.
bin.add(P.roundedBox(0.07), s.shell, { y: -0.065, size: [0.28, 0.195, 0.19] });
// The axle has to be longer than the pelvis is wide or none of it is visible,
// and for a long time none of it was: at `2·hipHalf + 0.05` it stopped at
// ±0.11 inside a shell that reaches ±0.14, and the comment on this line
// claimed it "runs right through and out both sides" on the strength of
// nobody having measured it. `+ 0.14` puts it at ±0.155: 15 mm of dark disc
// clear of the pelvis and 2.5 mm clear of the outer face of each thigh, which
// is the gap rule 4 in the header is about and the one the hip did not have.
// Shorten it again and the hip goes back to one unbroken pale mass from the
// waist to the knee, which is what a mannequin looks like.
barrel(bin, P, s.frame, { x: 0, y: 0, z: 0 }, 0.115, 2 * OPTIMUS.hipHalf + 0.14);
}
/**
* Waist, chest, shoulder caps and neck. Drawn in the torso frame, whose origin
* is the waist at `OPTIMUS.waistY`.
*
* The shoulder caps live here rather than on the arms on purpose, and it is not
* only a draw-call saving: on the real machine the cap is bodywork bolted to the
* torso and the arm swings inside it. Putting the cap on the arm makes the whole
* shoulder rotate when an arm swings, which reads as a shrug.
*/
function emitTorso(bin: MeshBin, P: PartBin, s: Skin): void {
const waist = OPTIMUS.chestY - OPTIMUS.waistY;
const chest = OPTIMUS.neckY - OPTIMUS.chestY;
// The waist. 0.15 m across between a 0.28 m pelvis and a 0.34 m chest, and it
// is drawn in the dark material so the gap reads as structure rather than as
// a robot that skipped lunch.
bin.add(P.box(), s.frame, { y: -0.02, size: [0.15, waist + 0.04, 0.135] });
// Chest: a shell, then a plate a few millimetres proud of it. The plate is
// what catches the light and gives the chest an edge at ten metres; without it
// the torso is one flat pale slab.
bin.add(P.roundedBox(0.06), s.shell, {
y: waist,
size: [0.34, chest, 0.205],
});
bin.add(P.roundedBox(0.055), s.shell, {
y: waist + 0.045,
z: -0.098,
size: [0.245, chest - 0.09, 0.022],
});
// The neck column, dark, running from the top of the chest into the head.
bin.add(P.cylinder(10), s.frame, {
y: OPTIMUS.neckY - OPTIMUS.waistY,
size: [0.072, OPTIMUS.headY - OPTIMUS.neckY + 0.012, 0.072],
});
const shoulderY = OPTIMUS.shoulderY - OPTIMUS.waistY;
for (const side of [-1, 1]) {
// The cap. Rounded hard, because a square shoulder is the other thing that
// makes a humanoid read as a costume.
bin.add(P.roundedBox(0.09), s.shell, {
x: side * OPTIMUS.shoulderHalf,
y: shoulderY - 0.078,
size: [0.13, 0.156, 0.15],
});
// The drum the arm turns in. 0.135 across and 0.17 long against a cap that
// is 0.13 wide, 0.156 tall and 0.15 deep, and every one of those numbers is
// chosen against the cap's: the drum is buried inside the cap in Y and Z
// and clears it only in X, so what shows is 20 mm of dark disc past the
// outboard face of the cap and, inboard, a nub of the same 20 mm in the
// 37 mm between the top of the chest and the top of the drum. The nub is
// wanted — it is the only thing between the neck and the shoulder — but it
// is the reason the drum's diameter is not free: take it past 0.16 and it
// breaks out under the cap as well, and the shoulder turns into a dark
// wheel with a pale plate stuck on it.
//
// That disc is the *only* dark structure at the shoulder, and it is here
// rather than on the arm for a reason worth stating, because the obvious
// fix looks better and costs more than it is worth. Rule 4 in the header
// wants a pale shell stopping short of the joint with dark showing in the
// gap; the knee and the elbow both do it. Doing it at the shoulder means a
// dark part in the *shoulder* bin, which today draws in `paper` alone — so
// it would go from one mesh to two, twice, on every figure: 20 meshes a
// robot and 80 draw calls for a crowd of four instead of 72. Eight draw
// calls for a 24 mm band of dark under a cap that already reads as a
// separate piece is not the trade. A fatter drum is free.
barrel(bin, P, s.frame, { x: side * OPTIMUS.shoulderHalf, y: shoulderY, z: 0 }, 0.135, 0.17);
}
}
/**
* The head shell's front-right corner, as fractions of its half-width and
* half-depth, walking round from the middle of the face toward the temple.
*
* These are **measured off the built shell**, by casting rays at it, not
* derived. They have to be, because the surface they describe is the offset
* outline of a quadratic-Bezier corner that `ExtrudeGeometry` then bevels, and
* writing that arithmetic out here would be a worse lie than a measurement:
* it would look authoritative and it would be a re-implementation of three.js
* internals. The visor is fitted to *these* numbers, so `x: 1` really is the
* widest point of the head and `z: 1` really is the flat front of the face.
*
* **They are only valid for `roundedBox(0.08)`.** The radius is what sets where
* the flat front stops — at 0.08 it stops 53% of the way out, and the corner
* from there to the temple is a four-segment polyline, of which the visor uses
* the first two. Change the head's bevel and these are wrong, and the symptom
* is a visor whose outer corners hang in the air beside the head rather than
* an error. Re-measure it: cast a ray down +Z at the shell at a series of x,
* at a y inside the shell's straight middle band, and read off where it hits.
* (An earlier visor was three hand-placed boxes and its outer pieces stood
* 19 mm proud of a 165 mm-wide head, which is what that failure looks like.)
*
* Scaling with the head rather than in metres so that the two survive being
* resized together; they do **not** survive being reproportioned, because a
* corner is not similar to itself under a non-uniform scale.
*/
const FACE_FLAT = { x: 0.5312, z: 1.0 } as const;
const FACE_MID = { x: 0.7146, z: 0.9738 } as const;
const FACE_TEMPLE = { x: 0.8766, z: 0.8766 } as const;
/** Head shell, metres. Named because the visor is fitted to all three. */
const HEAD_SIZE = { width: 0.165, height: 0.215, depth: 0.185 } as const;
/**
* How far the visor's outer face stands off the shell it is fitted to, and how
* thick the panel is.
*
* 1.5 mm is deliberately below anything that can resolve — at ten metres it is
* a seventh of a pixel — and it is not there to be seen. It is there so the
* sign can never go the other way. A visor flush with the shell z-fights it
* along the whole seam; a visor a millimetre *inside* it disappears in patches
* as the figure turns, which reads as a face flickering on and off and is by
* some distance the ugliest thing this asset has ever done.
*/
const VISOR_PROUD = 0.0015;
const VISOR_THICK = 0.016;
/**
* One panel of the visor, laid flat on the chord between two points of the
* head's front corner and pushed out until its outer face clears the shell.
*
* `from` and `to` are `FACE_*` fractions on the robot's right; `side` mirrors
* the whole thing for the left. Two things in here are easy to get wrong and
* neither of them shows up as an error:
*
* - **The chord is the surface, not an approximation of it.** The shell's
* corner really is a polyline in this band — it is an extruded polygon, not
* a curve — so a panel laid on one of its edges is parallel to the shell for
* that edge's whole length and stands off it by exactly `VISOR_PROUD`. This
* is the entire reason the corner is stored as measured points instead of as
* a radius: fit an arc to it instead and the panel touches at two places and
* sinks in between, which is the flickering failure `VISOR_PROUD` describes.
* - **The box is pushed *in* by half its thickness, not out.** A box's front is
* its Z face, so its centre has to sit `VISOR_THICK / 2` behind where the
* front is wanted. Add instead of subtract and the panel floats 16 mm off the
* head — which is very close to what the version before this one did.
*
* `seam` lengthens the panel at its inner end only. The inner end runs under
* the piece before it, which is what stops a hairline of shell showing through
* the joint between two panels at a grazing angle; the outer end is left exact,
* because past it the shell falls away and any overhang there is a ledge on the
* one edge of the visor anybody can see.
*/
function emitVisorPanel(
bin: MeshBin,
P: PartBin,
s: Skin,
side: number,
from: { x: number; z: number },
to: { x: number; z: number },
visorY: number,
visorHeight: number,
seam: number,
): void {
const halfX = HEAD_SIZE.width / 2;
const halfZ = HEAD_SIZE.depth / 2;
const x0 = from.x * halfX;
const z0 = -from.z * halfZ;
const x1 = to.x * halfX;
const z1 = -to.z * halfZ;
const dx = x1 - x0;
const dz = z1 - z0;
const span = Math.hypot(dx, dz);
// Outward normal of the chord: rotate its direction a quarter turn so it
// points away from the head. On the robot's right that is front-and-right.
const nx = dz / span;
const nz = -dx / span;
// Midpoint, dragged back along the chord by half the seam so the panel grows
// inward only, then offset along the normal to put its front face proud.
const offset = VISOR_PROUD - VISOR_THICK / 2;
const cx = (x0 + x1) / 2 - (dx / span) * (seam / 2) + nx * offset;
const cz = (z0 + z1) / 2 - (dz / span) * (seam / 2) + nz * offset;
bin.add(P.box(), s.frame, {
x: side * cx,
y: visorY,
z: cz,
size: [span + seam, visorHeight, VISOR_THICK],
// A box's front is Z, which a yaw of θ sends to (sin θ, cos θ); solving
// that for the normal is the whole of this line. Mirrored for the left,
// because a mirrored normal is a negated yaw.
yaw: side * Math.atan2(-nx, -nz),
});
}
/**
* The head: a small pale shell and a black visor. Drawn in the head frame, whose
* origin is the top of the neck.
*
* The visor is five plain `box`es because the dark material is the indexed one
* — see the primitive-class rule in the header, which is why the one part of
* this figure most deserving of a smooth curve is a faceted approximation of
* one. A flat plate across the middle of the face, then two panels a side
* following the shell's corner out to the temple, each sitting `VISOR_PROUD`
* off the surface it is laid on. Five boxes is 60 triangles; the head shell
* beside them is 236.
*
* **The visor tracks the shell, it does not overhang it.** That is the property
* to preserve if anything here is touched, and the reason the corner is stored
* as measured points at all. The version before this one was three hand-placed
* boxes with a hand-picked yaw: its flat middle plate stood 3.7 mm proud at the
* corners where it left the flat of the face, and its two side pieces stood
* **19 mm** proud of a head 165 mm wide, which is a face wearing goggles a size
* too big. The comment beside them said 1.5 mm, and had said so since before
* the shell's bevel was retuned from 0.120.22 to 0.0550.09 and moved every
* number it was describing. Measurements that are not re-taken are guesses with
* a decimal point on them.
*
* At five to fifteen metres a robot's head is fifteen to forty-five pixels
* tall. Nothing about the panel's *surface* survives that, and the only thing
* that does is its outline against the pale shell. So the outline is the thing
* that is built accurately and the rest is not built at all.
*
* It wraps as far as `FACE_TEMPLE` and stops, leaving about 10 mm of pale shell
* either side in a front view. Wrapping further gets a black head; stopping at
* the flat front gets a rectangle painted on a box.
*/
function emitHead(bin: MeshBin, P: PartBin, s: Skin): void {
const base = OPTIMUS.height - OPTIMUS.headY - HEAD_SIZE.height;
bin.add(P.roundedBox(0.08), s.shell, {
y: base,
size: [HEAD_SIZE.width, HEAD_SIZE.height, HEAD_SIZE.depth],
});
// Kept inside the shell's straight middle band — `roundedBox(0.08)` rounds
// the top and bottom 17 mm of a 215 mm head away in Y, and `FACE_*` describes
// the cross-section between those. Push the visor into either and it starts
// sinking into a surface that is no longer where the measurements say.
const visorY = base + 0.078;
const visorHeight = 0.082;
const seam = 0.005;
// The flat middle of the face, stopping exactly where the shell stops being
// flat so the first wrap panel meets it edge to edge rather than crossing it.
bin.add(P.box(), s.frame, {
y: visorY,
z: -HEAD_SIZE.depth / 2 - VISOR_PROUD + VISOR_THICK / 2,
size: [FACE_FLAT.x * HEAD_SIZE.width, visorHeight, VISOR_THICK],
});
for (const side of [-1, 1]) {
emitVisorPanel(bin, P, s, side, FACE_FLAT, FACE_MID, visorY, visorHeight, seam);
emitVisorPanel(bin, P, s, side, FACE_MID, FACE_TEMPLE, visorY, visorHeight, seam);
}
}
/** A thigh. Drawn in the hip frame; the shell stops short at both ends. */
function emitThigh(bin: MeshBin, P: PartBin, s: Skin): void {
const drop = OPTIMUS.hipY - OPTIMUS.kneeY;
// Two stacked shells rather than one, for the taper. A parallel-sided thigh is
// the difference between "slim humanoid" and "stilts".
bin.add(P.roundedBox(0.07), s.shell, { y: -drop * 0.52, size: [0.135, drop * 0.46, 0.165] });
bin.add(P.roundedBox(0.07), s.shell, { y: -drop + 0.045, size: [0.112, drop * 0.5, 0.14] });
}
/**
* Shin, ankle and foot, plus the knee actuator. Drawn in the knee frame.
*
* The actuator is the one piece of detail on the legs that is worth its
* geometry: a dark barrel across the front of the knee is the single most
* recognisable thing about an Optimus leg, and it is one cylinder.
*
* ### The ankle is built from the foot upward, and that is not a style choice
*
* Every part below the knee used to be placed off `drop`, and the ankle block
* came out at 0.0900.120 m above the floor while the foot shell ended at
* 0.066 — a 24 mm band of nothing, with the whole leg's weight visibly resting
* on air, and `OPTIMUS.ankleY` at 0.085 sitting in the middle of the void it is
* supposed to name. `robots.ts` measures the entire gait pendulum to that
* landmark, so the one height on this figure that had to have geometry at it
* was the one that had none.
*
* So the ankle is anchored to the top of the foot, which is a surface, rather
* than to a landmark, which is a number. It reaches 7 mm down into the foot
* shell and 13 mm up into the shin shell — overlapping at both ends on purpose,
* because two parts that merely touch share a face and z-fight along it, and a
* flickering seam is worse than a joint that is a few millimetres thicker than
* it needs to be. What is left visible is 39 mm of dark between a pale foot and
* a pale shin, which is rule 4 in the header and is what an ankle is for.
*/
function emitShin(bin: MeshBin, P: PartBin, s: Skin): void {
const drop = OPTIMUS.kneeY - OPTIMUS.ankleY;
barrel(bin, P, s.frame, { x: 0, y: 0, z: -0.012 }, 0.118, 0.125);
bin.add(P.roundedBox(0.065), s.shell, { y: -drop + 0.02, size: [0.1, drop - 0.05, 0.118] });
// The foot. The sole is a separate dark slab so the robot has something to
// stand on that is not the same colour as its shins — a monochrome foot
// dissolves into a pale floor.
const sole = -OPTIMUS.kneeY;
const footTop = sole + 0.066;
bin.add(P.box(), s.frame, { y: sole, z: -0.035, size: [0.098, 0.014, 0.25] });
bin.add(P.roundedBox(0.06), s.shell, {
y: sole + 0.014,
z: -0.035,
size: [0.106, 0.052, 0.243],
});
// The ankle, dark, filling everything between the two of them. Narrower than
// both in X and Z, so the gap reads as a joint rather than as a change of
// colour partway up a column — but only just. It is 12 mm narrower than the
// shin and 18 mm narrower than the foot, and both of those were 26 and 32 at
// one point, which left a 16 mm slot each side of the ankle that you could
// see the floor through. Narrow enough to read as a waisted joint, wide
// enough not to be a window: the two constraints point opposite ways and
// this is where they meet.
bin.add(P.box(), s.frame, { y: footTop - 0.007, size: [0.088, 0.059, 0.096] });
}
/**
* An upper arm. Drawn in the shoulder frame; the cap and the joint drum are
* both on the torso, for the reasons given at each of them.
*
* One shell, and it reaches 17 mm *into* the elbow barrel below it rather than
* stopping level with it. At `-drop + 0.048` the shell ended at 1.168 m and the
* barrel reached 1.167 — a one-millimetre ring of daylight all the way round
* the elbow, which does not read as a gap at any distance but does catch the
* background for a frame at a time as the arm swings, and chasing that kind of
* flicker back to a millimetre of arithmetic costs an afternoon.
*/
function emitUpperArm(bin: MeshBin, P: PartBin, s: Skin): void {
const drop = OPTIMUS.shoulderY - OPTIMUS.elbowY;
bin.add(P.roundedBox(0.07), s.shell, { y: -drop + 0.03, size: [0.088, drop - 0.08, 0.098] });
}
/**
* The digits of one hand, medial first. `medial` is the offset **toward the
* body**, not an X coordinate, so this table is the same for both hands and the
* one sign flip that makes a left hand a left hand happens where it is placed
* — the same discipline the thumb has always had, extended to the fingers so
* that the two cannot disagree about which way is inward.
*
* ### Three, not four, and why the count is the wrong question
*
* The previous version drew four identical 14 mm slabs in a row with 3 mm
* between them, on the argument that what a hand needs at ten metres is five of
* something, separate. Half of that is right and the expensive half is not. At
* ten metres this figure is about 170 pixels tall, so a 14 mm finger is 1.4 of
* them and a 3 mm gap is a third of one: **the separations were never once
* resolved on screen.** What is resolved is the shape the tips make, and four
* equal slabs make a straight line across the bottom of the hand — a comb, not
* a hand. So the gaps are not what the geometry is for; the *stagger* is.
*
* Three digits of different lengths give the curved tip line a hand has, and
* cost three of these rather than four. That matters more than it looks:
* `roundedBox` is 236 triangles whatever size it is drawn at, so the four
* fingers alone were 944 triangles a hand and the two hands together were a
* quarter of the entire figure — a quarter of the budget spent below the
* resolution limit. Dropping one digit a hand is 6% off the whole robot.
*
* `curl` is a rotation about +X applied at the *tip*, because a placement
* rotates a part about its own base and these are placed tip-down. The base is
* therefore solved for: it is the knuckle, less the rotated length. Getting
* that backwards pivots each finger about its knuckle-end and fans the hand out
* like a card trick.
*/
const DIGITS = [
{ medial: 0.022, width: 0.016, depth: 0.024, length: 0.068, curl: 0.26 },
{ medial: 0.002, width: 0.018, depth: 0.025, length: 0.076, curl: 0.3 },
{ medial: -0.021, width: 0.024, depth: 0.024, length: 0.064, curl: 0.34 },
];
/**
* Forearm, wrist and hand. Drawn in the elbow frame.
*
* The digits are pale rather than dark, with only the wrist block in the frame
* material — see `DIGITS` for what a hand is actually made of at this distance.
*
* This is the one emitter that has to know which side it is on, because a thumb
* is the only part of the figure that is not left-right symmetric. It goes
* medial — toward the body — which is where a relaxed arm puts it, and which
* means `side` flips its sign. Drawing both thumbs at a fixed `+x`, as the first
* version did, gives a robot with two right hands and a bounding box 27 mm wider
* on one side than the other.
*
* **Nothing here butts up against anything.** The wrist block straddles
* `OPTIMUS.wristY` and overlaps the forearm shell by 6 mm; the palm reaches
* 6 mm up into the wrist block; the digits reach 6 mm up into the palm. All
* three of those were exact abutments and two of them were exactly coplanar,
* which is a z-fight rather than a join — and the third was not an abutment at
* all but an 8 mm hole between the bottom of the forearm shell at 0.885 m and
* the top of the wrist block at 0.877, straight through the arm. Overlap costs
* nothing: these are merged into one buffer and the interior faces are never
* seen from outside.
*/
function emitForearm(bin: MeshBin, P: PartBin, s: Skin, side: number): void {
const drop = OPTIMUS.elbowY - OPTIMUS.wristY;
barrel(bin, P, s.frame, { x: 0, y: 0, z: 0 }, 0.094, 0.088);
bin.add(P.roundedBox(0.07), s.shell, { y: -drop + 0.02, size: [0.08, drop - 0.055, 0.088] });
// Wider than the palm below it and narrower than the forearm above, in that
// order. At 0.062 it was narrower than both, which put a 6 mm slot of
// daylight down each side of the wrist between two parts that are meant to
// be joined; a joint that reads as a taper cannot do that.
bin.add(P.box(), s.frame, { y: -drop - 0.016, size: [0.072, 0.042, 0.07] });
const palmTop = -drop - 0.01;
bin.add(P.roundedBox(0.075), s.shell, { y: palmTop - 0.082, size: [0.068, 0.082, 0.032] });
const knuckle = palmTop - 0.076;
for (const digit of DIGITS) {
bin.add(P.roundedBox(0.08), s.shell, {
x: -side * digit.medial,
y: knuckle - digit.length * Math.cos(digit.curl),
z: -digit.length * Math.sin(digit.curl),
size: [digit.width, digit.length, digit.depth],
pitch: digit.curl,
});
}
// The thumb, also placed tip-first, and the only part of the figure that
// needs two rotations: it runs up and *outward* from a tip set 12 mm medial
// of the palm's edge, and forward of the palm's face, so the pad ends up
// opposing the fingers rather than lying alongside them. The old one was a
// single roll about its own tip, which swung the knuckle 23 mm clear of the
// hand and left the thumb attached to the palm at one corner.
bin.add(P.roundedBox(0.08), s.shell, {
x: -side * 0.046,
y: palmTop - 0.078,
z: -0.03,
size: [0.019, 0.058, 0.026],
pitch: 0.33,
roll: -side * 0.28,
});
}
// ---- The rig --------------------------------------------------------------
/** A named, empty joint at a position in its parent's frame. */
function joint(parent: THREE.Object3D, name: string, x: number, y: number): THREE.Group {
const group = new THREE.Group();
group.name = name;
group.position.set(x, y, 0);
parent.add(group);
return group;
}
/** Merge a joint's parts and hang them off it, if it has any. */
function attach(target: THREE.Group, bin: MeshBin): void {
if (bin.size === 0) return;
target.add(bin.build(`${target.name}.mesh`));
}
/**
* Build one posable Optimus.
*
* Call this **once** and `cloneOptimus` for every figure after the first: a
* clone shares every geometry and both materials, so a crowd costs draw calls
* and nothing else. Whoever built the original disposes it with
* `disposeOptimus`, which frees the geometry the clones are all pointing at —
* so dispose last, and dispose exactly once.
*
* The materials come from `ctx.materials` and belong to the registry. Nothing
* here disposes them.
*/
export function buildOptimus(ctx: AssetContext): OptimusRig {
const P = ctx.parts;
const skin: Skin = {
shell: ctx.materials.get("paper"),
frame: ctx.materials.get("screenBezel"),
};
const root = new THREE.Group();
root.name = "optimus";
// Pelvis, and the legs hanging off it. The legs are children of the pelvis so
// that dropping the pelvis drops the whole robot — see the header note on
// `pelvis.position.y`.
const pelvis = joint(root, "pelvis", 0, OPTIMUS.hipY);
const pelvisBin = new MeshBin();
emitPelvis(pelvisBin, P, skin);
attach(pelvis, pelvisBin);
const torso = joint(pelvis, "torso", 0, OPTIMUS.waistY - OPTIMUS.hipY);
const torsoBin = new MeshBin();
emitTorso(torsoBin, P, skin);
attach(torso, torsoBin);
const head = joint(torso, "head", 0, OPTIMUS.headY - OPTIMUS.waistY);
const headBin = new MeshBin();
emitHead(headBin, P, skin);
attach(head, headBin);
// `side` is 1 for the robot's left and +1 for its right, which is the sign of
// X: see the joint-frame note. The two limbs are mirror images in position
// only — the shells themselves are symmetric, so there is no mirrored
// geometry and no wound-backwards triangles to worry about.
for (const side of [-1, 1]) {
const suffix = side < 0 ? "L" : "R";
const hip = joint(pelvis, `hip${suffix}`, side * OPTIMUS.hipHalf, 0);
const hipBin = new MeshBin();
emitThigh(hipBin, P, skin);
attach(hip, hipBin);
const knee = joint(hip, `knee${suffix}`, 0, OPTIMUS.kneeY - OPTIMUS.hipY);
const kneeBin = new MeshBin();
emitShin(kneeBin, P, skin);
attach(knee, kneeBin);
const shoulder = joint(
torso,
`shoulder${suffix}`,
side * OPTIMUS.shoulderHalf,
OPTIMUS.shoulderY - OPTIMUS.waistY,
);
const shoulderBin = new MeshBin();
emitUpperArm(shoulderBin, P, skin);
attach(shoulder, shoulderBin);
const elbow = joint(shoulder, `elbow${suffix}`, 0, OPTIMUS.elbowY - OPTIMUS.shoulderY);
const elbowBin = new MeshBin();
emitForearm(elbowBin, P, skin, side);
attach(elbow, elbowBin);
}
const rig: OptimusRig = { root, joints: optimusJoints(root) };
restOptimus(rig.joints);
return rig;
}
/**
* Resolve the joints of a rig root by name.
*
* This used to be exported, on the stated grounds that `Object3D.clone(true)`
* "hands back plain `Object3D`s" and so a caller holding a clone would need to
* re-resolve it. **That is not true of three.js and never was.** `clone` is
* `new this.constructor().copy(this, recursive)` (`three/src/core/Object3D.js`),
* so cloning a `Group` gives a `Group`, and a cloned rig is structurally
* identical to the original down to the class of every node. Nothing outside
* this file ever called it — `robots.ts` takes its joints from the `OptimusRig`
* that `cloneOptimus` hands back — so the export was justified by a wrong claim
* and used by nobody, and it is now private.
*
* The function itself is still needed, for the reason the false one was
* standing in front of: `clone` copies the *tree*, and `OptimusJoints` is a
* flat record of references **into** the original tree. Those references are
* not part of the tree and are not cloned with it, so a clone's joints have to
* be found again, and the names are the only thing that survives the copy to
* find them by.
*
* Throws rather than returning null: a root with no `kneeL` in it is not a rig,
* and the caller has nothing useful to do about that at runtime.
*/
function optimusJoints(root: THREE.Object3D): OptimusJoints {
const found = {} as Record<(typeof JOINT_NAMES)[number], THREE.Group>;
for (const name of JOINT_NAMES) {
const object = root.getObjectByName(name);
if (!object) throw new Error(`optimus: rig has no joint named "${name}"`);
found[name] = object as THREE.Group;
}
return found;
}
/**
* Put every joint back to the idle stance. Called by `buildOptimus`, and by an
* animation that wants a figure to stop moving without writing the eleven
* assignments out itself.
*/
export function restOptimus(j: OptimusJoints): void {
j.pelvis.position.y = OPTIMUS.hipY;
j.pelvis.rotation.set(0, 0, 0);
j.torso.rotation.set(0, 0, 0);
j.head.rotation.set(0, 0, 0);
j.hipL.rotation.set(OPTIMUS_REST.hipX, 0, 0);
j.hipR.rotation.set(OPTIMUS_REST.hipX, 0, 0);
j.kneeL.rotation.set(OPTIMUS_REST.kneeX, 0, 0);
j.kneeR.rotation.set(OPTIMUS_REST.kneeX, 0, 0);
// The splay is mirrored: positive `rotation.z` swings a hanging arm toward
// +X, so the right arm needs `+` and the left arm `` to both move outward.
j.shoulderL.rotation.set(OPTIMUS_REST.shoulderX, 0, -OPTIMUS_REST.shoulderZ);
j.shoulderR.rotation.set(OPTIMUS_REST.shoulderX, 0, OPTIMUS_REST.shoulderZ);
j.elbowL.rotation.set(OPTIMUS_REST.elbowX, 0, 0);
j.elbowR.rotation.set(OPTIMUS_REST.elbowX, 0, 0);
}
/**
* Another figure sharing the first one's geometry and materials.
*
* `Object3D.clone(true)` copies the hierarchy, the names and the transforms and
* *references* geometry and material, which is exactly the sharing wanted here —
* so do not dispose a clone. Dispose the original, once, with `disposeOptimus`.
*/
export function cloneOptimus(rig: OptimusRig): OptimusRig {
const root = rig.root.clone(true);
return { root, joints: optimusJoints(root) };
}
/**
* Free a rig's geometry.
*
* Materials are the registry's and are left alone, exactly as every asset in
* this directory leaves them alone. Call this on the rig `buildOptimus`
* returned, never on a clone, and only once everything cloned from it is out of
* the scene.
*/
export function disposeOptimus(rig: OptimusRig): void {
rig.root.traverse((child) => {
const mesh = child as THREE.Mesh;
if (mesh.isMesh) mesh.geometry.dispose();
});
}
// ---- The static asset -----------------------------------------------------
/**
* Collapse a posed rig into one mesh per material.
*
* The rig's own geometries are baked at their world transforms and disposed, so
* what comes back owns everything it points at and can go through `MeshBin`'s
* usual life cycle. This is safe only because of the primitive-class rule in the
* header: `paper` is all extrusions and `screenBezel` is all indexed
* primitives, so neither merge can hit the mixed-index refusal that would drop a
* material and leave a robot with no shells on it.
*/
function flatten(rig: OptimusRig, name: string): THREE.Group {
rig.root.updateMatrixWorld(true);
const byMaterial = new Map<THREE.Material, THREE.BufferGeometry[]>();
rig.root.traverse((child) => {
const mesh = child as THREE.Mesh;
if (!mesh.isMesh || Array.isArray(mesh.material)) return;
const baked = mesh.geometry.clone().applyMatrix4(mesh.matrixWorld);
const list = byMaterial.get(mesh.material);
if (list) list.push(baked);
else byMaterial.set(mesh.material, [baked]);
});
disposeOptimus(rig);
const group = new THREE.Group();
group.name = name;
for (const [material, list] of byMaterial) {
const merged = list.length === 1 ? list[0] : mergeGeometries(list, false);
if (list.length > 1) for (const geometry of list) geometry.dispose();
if (!merged) continue;
const mesh = new THREE.Mesh(merged, material);
mesh.name = `${name}:${material.name || "material"}`;
mesh.castShadow = true;
mesh.receiveShadow = true;
group.add(mesh);
}
return group;
}
type OptimusParams = {
/** Sole to crown, metres. The whole figure scales; 1.73 is the real one. */
height: number;
/**
* The stance to bake in. `"rest"` stands to attention, `"stride"` freezes it
* mid-step so a pack can put one in a corridor without it looking parked.
*/
pose: "rest" | "stride";
};
/**
* One Optimus, standing still, merged.
*
* Two draw calls, no articulation, and `furnishings.ts` instances it per mesh —
* so a pack with twenty of these still pays those two. Anything that has to walk
* wants `buildOptimus` instead, and pays eighteen.
*
* There is no `colorKey` and no tintable part. A robot that comes in a team
* colour is a mascot; this is a machine, and the two things it can be are the
* colour of its shells and the colour of its frame, both of which belong to the
* palette rather than to one instance.
*/
export const robotOptimus = defineAsset<OptimusParams>({
id: "tera:robot.optimus",
label: "Optimus humanoid robot",
defaults: { height: OPTIMUS.height, pose: "rest" },
footprint(p) {
const scale = p.height / OPTIMUS.height;
// Measured off the built figure rather than guessed, and measured off the
// *baked vertices* rather than off `Box3.setFromObject`, which inflates the
// box of anything rotated and had this at 0.534 for a while: 0.520 m across
// and 0.279 m from toe to heel, rounded up. The feet are the deepest part,
// not the chest, which is what you would reach for. The width is a near tie
// between the shoulder drums at 0.520 and the hands at 0.516 — the arms of
// a standing humanoid are as wide as its shoulders, which is the other
// thing you would get wrong — so widening either one moves this number.
return { width: 0.55 * scale, depth: 0.3 * scale, height: p.height, clearance: 0.35 };
},
build(p, ctx) {
const rig = buildOptimus(ctx);
if (p.pose === "stride") {
// A single frame of the walk cycle, written out rather than shared with
// `robots.ts`: this is a fixed pose for a static prop, that is a function
// of distance travelled, and coupling them would mean a pack's decorative
// robot changing shape whenever somebody retunes a gait.
rig.joints.hipL.rotation.x = 0.36;
rig.joints.hipR.rotation.x = -0.3;
rig.joints.kneeL.rotation.x = -0.12;
rig.joints.kneeR.rotation.x = -0.42;
rig.joints.shoulderL.rotation.x = -0.26;
rig.joints.shoulderR.rotation.x = 0.28;
rig.joints.elbowL.rotation.x = 0.2;
rig.joints.elbowR.rotation.x = 0.34;
rig.joints.torso.rotation.y = -0.07;
rig.joints.pelvis.rotation.y = 0.04;
// Dropped by the amount the straddle costs, and no further: the pose is
// hand-written, so nothing checks that the feet reach the floor except
// measuring the result. At 0.022 the leading toe was 2 mm under it.
rig.joints.pelvis.position.y = OPTIMUS.hipY - 0.019;
}
const group = flatten(rig, "robot.optimus");
if (p.height !== OPTIMUS.height) group.scale.setScalar(p.height / OPTIMUS.height);
return group;
},
});