Assets

3D models in Blender

A real model built from a script, checked against the physics, and hooked into the scene.

A model of a vehicle, a machine, a prop, a building, or a cockpit seen from inside is a Python script run headless in Blender that writes a .glb. The script, its kit of building blocks and its own viewer sit beside the glb, so the model can always be built again.

Before building

  1. Settle the design before any script: what it is to feel like (a film, a real machine), its shape, how much of it is seen and from where, its style, what it shows (screens, lamps, damage) and how dark or bright it is. Write the answers down with the job.
  2. Write down what the player sees it from and what must be true from there, as in the table below. Each item becomes a line a viewer prints.
  3. Download photos of the real thing and take its proportions off them against a known size: a person, a tyre, the roof height the physics gives. List what makes it itself (for a 1970s pickup: the crease down the side, cut wheel arches, a flat hood, an egg-crate grille) and build every one.
  4. Screenshot the thing as it is in dev.html?tab from the angles of step 2.
  5. Read the module page of what draws it: what the model replaces, what is still drawn round it, and which parts are found by node name. box3dvehicle still draws the wheels, coil springs, dampers, arms and tailpipe.
  6. Work out the hard numbers. For a vehicle: the ground under the chassis (hub at rest is mount − spring.length + spring.sag, ground is hub minus wheel radius), each wheel at full bump (it rises travel − sag), and a steered wheel at full lock, whose inner corners swing in by about w/2·cos(a) + r·sin(a). For an interior: the eye, the game camera's vertical field of view, aspect and near plane, the lowest pitch the player can look, and the room to the hull. A body the wheel sweeps through is narrowed or cut, and a hood that rises into the view is lowered.
ThingSeen fromWhat must be true
A vehicleThe chase camera and beside itIts size against the physics shapes; every wheel clear of the body at rest, at full lock each way, at full bump and at bump with lock; room for the springs, arms and tailpipes the plugin draws.
An interiorOne eyeRoom for the head; nothing but glass above the line the world shows over; every screen seen whole, all four corners, turned to the eye and big enough to read; every screen's picture the right way round.
A prop or buildingPlay distanceIts outline from the camera's height and distance, its size, and where it meets the ground.

For every one, no faces lie on faces.

Where the files go

games/<game>/assets/<model>/
  kit.py        the building blocks
  <model>.py    the build; writes <model>.glb beside it
  look.py       the model's own viewer, for what view.py and eye.py do not check
  <model>.glb

Build with /Applications/Blender.app/Contents/MacOS/Blender -b --python <model>.py.

The build script

  1. Build in the game's frame, in metres with +y up; a vehicle has +x forward and +z right. A vertex built at [x, y, z] is at [x, y, z] in the glb.
  2. Read every number that belongs to the scene off the scene file: paint colours from look.paint, the exhaust from look.exhaust, axle positions, the beam's dip, an interior's eye. Place every part of an interior from the eye.
  3. Make one part per node. Parts the plugin finds by name are their own nodes: beam.L and beam.R empties (+x along the beam, turned down by lights.beam.dip), tail.*, reverse.*, amber.* and screen.* meshes, and steer, the steering wheel without its column, at its hub, +x down the column into the dash, +y up its face and +z right, which box3dvehicle turns about its x with the steering.
  4. Give a screen UVs running 0 to 1 over its face, u to the viewer's right and v up. The plugin lays its picture over them.
  5. Give glass an inside: a hollow cab with a floor, seat, dash and wheel. Leave room for what the plugin draws. Take out a part that does nothing the player sees rather than tuning it.
  6. Put a Weighted Normal modifier (FACE_AREA, keep sharp) on every part, and drop faces under 1e-10 m² after triangulating. Build a curved panel with a thickness, such as a hood, from its two surfaces and the rim joining them.
  7. Nothing lies on anything: two same-facing faces within 4 mm z-fight. Trim, seams and panes stand 5 to 6 mm off the skin, and a part that sits on another starts a few millimetres inside it.
  8. Rebuild each part in one fixed order before writing it, every face from its least corner and faces sorted, so the build is the same bytes every time.

Hooking it in

  1. Add an entry to the loader catalogue, world.loader.assets.<model>, with type: model, a url under /games/<game>/assets/ and preload: true, in every scene that shows it.
  2. Point the thing's look at it: for box3dvehicle, look.model: { asset }. Remove the look blocks the model replaces, such as bumpers, grille, fenders, spare or a cabin drawn from boxes.

Checking it

Three viewers measure the exported glb. Each line prints its number beside PASS, and a FAIL names the part and where, such as VIEW blocked by hood at yaw=-36° pitch=-7.9°. A threshold comes from the design and is written in the viewer with its reason; it is never moved to turn a FAIL into a PASS.

/Applications/Blender.app/Contents/MacOS/Blender -b --python .claude/skills/making-3d-assets-in-blender/view.py -- &lt;model.glb&gt; &lt;outdir&gt; [--look x,y,z,r ...]
/Applications/Blender.app/Contents/MacOS/Blender -b --python .claude/skills/making-3d-assets-in-blender/eye.py -- &lt;model.glb&gt; &lt;outdir&gt; --eye x,y,z [--fov 60] [--line -8] [--screens screen.]
ViewerLines
view.py, any glbNODE, BBOX, COPLANAR … pairs=0, and renders: turntables high and at eye height beside a 1.8 m figure, a plan, close-ups, and --look points.
eye.py, an interiorEYE room for the head, VIEW what stands above the line, SCREEN each screen's middle and four corners seen first, UV, COPLANAR, and renders from the eye with a test card on every screen.
look.py, a vehicleSIZE within 0.35 m of the physics shapes, CLEAR each wheel in every state, ROOM for coils, arms and tailpipes, TYRE each coil against its tyre, and a plan with ghost wheels.
  1. Build twice and cmp the two glbs: they must be identical. Read every render at full size; every test card's arrows point up and right.
  2. Put the game's picture beside the reference from the same angle, list every difference, fix them, and run every viewer line again, not only the one the fix was for.
  3. Open dev.html?tab and screenshot the angles of step 2. Something wrong in the game that the viewers passed is a missing check: add the line, and see it fail on that build before the fix.
  4. In the game, three.js's loader takes the dots out of node names (screen.left becomes screenleft, the original kept on userData.name), every copy of a model shares its materials and geometry, and the model is not there on the first frames.
  5. It is done when every viewer line passes, the game's picture looks like the reference, and nothing the player sees is wrong.