A dwarf with a sack of gold coins, a goblin with a bundle of logs and a ratman with a banded pack of stone walking along a dirt track at night toward a lit, walled stone hall

Workers that carry what they gather, on ten different backs

Rendering & Graphics8 min readUpdated
ClaudeBuilt the thing
Adam SturrockDecided what mattered

A worker walking home from the gold mine used to look exactly like a worker walking anywhere else. Now it carries a sack of gold on its back, a bundle of logs if it has been chopping, or a pack of quarried stone, and the load is gone the moment it is dropped off.

The request came straight after the gold mine started lighting up while workers are inside. That fixed the moment a worker disappears. Adam wanted the rest of the trip to read too: a visual cue that a worker is carrying something, and which resource it is. Ten ground workers across the factions meant one load had to sit convincingly on a broad dwarf, a small goblin, a hunched ratman, a skink with a tail and a slim elf.

Five workers walking away from the camera, each with a load on its back: a dwarf with a gold sack, a goblin with a bundle of logs, a ratman with a stone crate, a skink with a gold sack and a red-haired wood elf with logs
The real worker models on their real walk cycles, with each load placed by the game's own renderer. Switch to Before to see the same walk with nothing on their backs, change the load, and use Close up with Side to watch it lean with the body.

One load, drawn twice

The game has a 2D renderer and a 3D one, and both read the same rule for when a load shows: the worker is carrying something, it is gold, lumber or stone, and it is not hidden inside a mine. Oil is left out on purpose. Only tankers carry it, and a ship has no back.

In 2D the load is a small sprite over the upper body, pushed away from the way the worker is facing. Facing the camera or sideways, it draws behind the body, so you see the top of the sack over the shoulders. Facing away, it draws over the body and you see all of it.

The three 2D carry sprites side by side: a burlap sack overflowing with gold coins, a bundle of four logs tied with rope, and an iron-banded wooden crate heaped with grey stones

The 3D loads are models from Meshy, generated from those same reference images. The stone pack went through four versions of its reference image before one was approved, and that version has no shoulder straps. Straps would have to wrap ten different body shapes, and a strapless pack can simply sit flush against whatever back it is on.

A raw Meshy export of a prop is around 10,000 triangles with a 2048 pixel texture, for an object drawn a few dozen pixels tall. A small tool built on glTF-Transform and meshoptimizer simplifies each one to about 1,500 triangles, shrinks the texture to 512 pixels, and re-anchors the model so its base sits on the ground plane and the face that rests against a back sits at zero depth. The rest of the placement code assumes exactly that frame.

Why the load is not parented to a bone

The obvious way to attach a backpack is to make it a child of a spine bone. That breaks here, because a unit in the 3D renderer is not one mesh. It is a ladder of detail levels, the subject of the level of detail post, and only one is drawn at a time. A prop parented to one level's bone disappears the moment the ladder swaps to another.

So the load is a child of the unit's root, which already carries its position, fog visibility and everything else about the unit, and every frame it copies its placement from whichever detail level is active. On a skinned level that is the chest bone. On the coarsest static levels, which have no skeleton, it falls back to a fixed point at about half the model's height, behind the body.

The bone called Spine02 is the lower back

The first version anchored every load on the bone named Spine02. It is the obvious choice by name, and it was wrong on every worker. On a Meshy humanoid rig the chain runs from the hips to Spine02, then Spine01, then Spine, and the shoulders and neck hang off that last one. Spine02 is the small of the back.

Lined up in the lab, it was plain. Loads sat low, sank into the chunkier torsos, and stayed upright while the body under them bent, because they took the model's facing and nothing else. A load strapped to a back leans and twists with the chest.

The fix finds the chest by structure instead of by name: it is the bone the neck hangs off, which holds on any humanoid rig whatever its spine happens to be called. Bone axes are arbitrary between rigs, so the prop's orientation is not taken from the bone either. It is built from anatomy, in the bind pose, and stored as an offset from the chest bone's rotation. After that, every frame is one multiplication, and the load leans, twists and bobs exactly as the chest does.

import * as THREE from 'three';
 
// A torso frame from anatomy: up is lower spine to neck, right is shoulder to
// shoulder, and "out" (away from the back) is their cross product.
// Pass bind-pose positions, so every instance of a model gets the same offset.
export function torsoOffset(chestBindQuat, lowerSpine, neck, leftShoulder, rightShoulder) {
  const up = new THREE.Vector3().subVectors(neck, lowerSpine).normalize();
  const right = new THREE.Vector3().subVectors(rightShoulder, leftShoulder);
  const out = new THREE.Vector3().crossVectors(right, up).normalize();
  right.crossVectors(up, out).normalize();
  const frame = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(right, up, out));
  return chestBindQuat.clone().invert().multiply(frame);
}
 
// Each frame: the chest's live rotation times the offset is the torso's frame.
export function torsoFrame(chestBone, offset, target = new THREE.Quaternion()) {
  return chestBone.getWorldQuaternion(target).multiply(offset);
}

Two details are easy to miss. Find the chest as the parent of the neck bone, not by guessing a spine name. And check which way "out" points against something that knows where the face is: Meshy rigs carry a bone just ahead of the head, and a mirrored rig with left and right swapped would otherwise put the load on the worker's chest. Deciding that from a live pose instead is fragile. One worker's idle animation bends almost double, and it had its load put inside its chest.

Five workers walking in profile, each load sitting flush on its back: a dwarf with a stone crate, a goblin with a gold sack, a ratman with logs, a skink with a stone crate and a peasant in a straw hat with logs

Measuring ten backs instead of guessing ten offsets

With the anchor right, the remaining question is how big each load should be and how far out from the chest it should sit. A dwarf's back is a barrel well behind its chest bone. A skink is thin but hunched. A ratman has a rounded hump. Six numbers per worker per load (size, out from the back, up, sideways, lean and spin) across ten workers and three loads is 180 numbers, and tuning them by eye in the carry lab was the first plan.

The lab now fits them instead. It takes each worker's real skinned mesh in the poses a load is mostly seen in, standing idle and at four points of the walk cycle, and measures the back in the same torso frame the load is placed in. Size comes from shoulder breadth, height from the space between the hips and the shoulders, and lean from the slope of the back across the load's height, so a hunched back gets a load that lies along it.

What counts as back took some deciding. Arms and legs are excluded, or a swinging arm in the walk cycle would shove every load a hand's width off the body. So is everything below the hips: Meshy rigs have no tail bones, so a ratman's or skink's tail is skinned to the hips and sticks straight out behind, and measured as body it pushed every load about a third of a body height off the back.

Loads that hovered, and loads that sank

The first auto-fit still looked wrong on the contact sheets, a grid of every worker wearing every load, from behind and in profile, idle and walking. Almost every load hovered slightly off its back. The fit had pushed each load out until it cleared the single furthest point of the back, so a flat pack touched a rounded back at one spot and stood proud everywhere else.

The second version matches the load's actual surface against the back's, patch by patch, and pushes each one in until the closest quarter of the contact sits just inside the body, by 1.2% of the worker's height. A cap stops any part going deeper than 6%, because without it a round sack disappeared into the slimmer backs. One more exception came from the elves: their long hair is skinned to the head bone, the fit ignored it, and loads sank into the hair. Anything skinned to the head now counts as back when it hangs below the neck.

All 30 worker and load fits were re-run and checked on the sheets from behind and in profile, standing and walking, before they shipped.

Two rows of the same five workers in profile, a dwarf miner, a goblin, a ratman, an armoured dwarf prospector and a skink. In the top row, from the first version, each gold sack hangs in the air behind the worker. In the bottom row each sack sits against its back

Small things that make a load belong to the unit

A load casts a shadow when the player has unit shadows on, since it is part of the unit. The three prop models load with the rest of a match's models, so the first worker to leave a mine never pops a load in a frame late. The gold sack also borrows the gold mine's glow: the same colour mask that lights the veins in the rock picks out the coins spilling from the sack, tuned so the burlap stays dull.

Fitting your own props to many characters

If one prop has to sit on many different bodies, do not tune an offset per character by eye. Measure instead. Put the prop in a frame built from the skeleton's anatomy, sample the skinned mesh in the two or three poses the prop is actually seen in, leave out limbs and anything below the hips, and fit the prop's surface against the body's rather than against a single point. Then photograph every combination from behind and in profile on one sheet, because a bad fit is invisible on the one character you happen to be looking at.

Every fit is stored as a fraction of the worker's height, so rescaling a worker keeps its load in place. A new worker added to the roster needs one run of the auto-fit and one look at its row on the sheet.

Questions

How do you show which resource a worker is carrying in an RTS?

Put the load on the worker. In Shards of Stone a worker walking home wears a coin sack for gold, a lashed bundle of logs for lumber and a banded pack of stone for stone, and the load disappears when it is dropped off. It reads at game-camera distance, where a small icon or a number over the unit would not.

How do you attach a prop to an animated character in three.js?

Do not parent it to the bone directly if the character swaps between detail levels, because the prop disappears with the mesh it was parented to. Parent it to the unit's root instead and copy the bone's world position and rotation into it every frame. Build the prop's orientation from the torso's anatomy in the bind pose, not from the bone's own axes, which differ between rigs.

Which bone should a backpack attach to on a Meshy rig?

The upper chest, which on a Meshy humanoid rig is the bone the neck and shoulders hang off. The chain runs from the hips through two lower spine bones to that chest bone, so the bone with the most obvious name is the lower back. Find the chest by structure, as the parent of the neck, and it works whatever the spine bones are called.

Why does a backpack float off or sink into a character's back?

Usually because it was placed against one point, the furthest-out point of the back. A round sack or a flat pack then touches there and hovers everywhere else. Measure the back's surface in the poses the character is mostly seen in, match the prop's own surface against it, and let a small share of the contact sit just inside the body, with a cap on how deep any part can go.

← All posts