# shared/models – cars and people from Grand Theft Frankfurt

Grand Theft Frankfurt (GTF, same owner) builds its vehicles and people in code with three.js; nothing existed as a
file. `shared/tools/export-gtf-models/` runs GTF's own builders in headless Chromium and writes them as binary glTF
(GLB) with three.js `GLTFExporter` (r185), so Godot, Unity and Unreal all use the same cars and characters.
Source: GTF commit `c7c81ef` (`src/entities/VehicleModels.ts`, `CharacterMesh.ts`, `CharacterAssets.ts`,
`Character.ts`). Every file passes the Khronos glTF validator with 0 errors (`node validate.mjs`): the cars with no
warnings (infos only: unused `TEXCOORD_0` on untextured primitives), the people with one warning each (tangents, see
below).

## Conventions (all files)

- Units metres, **+Y up, forward −Z, left −X** (right-handed, as glTF). Real sizes, no scale on any node.
- Node transforms are TRS (translation/rotation/scale), never matrices.
- Every primitive has `NORMAL` and `TEXCOORD_0`. Untextured parts carry box-projected UVs in metres (1 UV unit = 1 m)
  for detail/grime textures or lightmap generation.
- glTF importers: Godot 4 (built in), Unity (glTFast or UnityGLTF), Unreal 5 (Interchange glTF). Godot keeps −Z as
  forward, which is Godot's own forward; Unity's and Unreal's importers convert the axes – check the arrow once.

## Vehicles – `vehicles/<class>.glb`

| File | GTF name | Triangles (body + 4 wheels) | KiB | Size W × H × L (m, incl. mirrors) | Front-left hub | Driver seat (cushion top) |
|---|---|---|---|---|---|---|
| `compact.glb` | Kolibri | 5,700 (3,780 + 1,920) | 279 | 2.07 × 1.58 × 3.96 | −0.73, 0.31, −1.25 | −0.36, 0.53, 0.55 |
| `sedan.glb` | Taunus 500 | 5,876 (3,956 + 1,920) | 283 | 2.25 × 1.53 × 4.86 | −0.79, 0.34, −1.425 | −0.38, 0.55, 0.55 |
| `sports.glb` | Mainhattan GT | 5,840 (3,920 + 1,920) | 280 | 2.27 × 1.33 × 4.56 | −0.81, 0.35, −1.30 | −0.40, 0.53, 0.85 |
| `suv.glb` | Taunus Rover | 5,928 (3,960 + 1,968) | 291 | 2.43 × 1.93 × 4.96 | −0.83, 0.40, −1.475 | −0.40, 0.77, 0.35 |
| `van.glb` | Sprinter Lieferwagen | 5,602 (3,666 + 1,936) | 266 | 2.49 × 2.42 × 5.66 | −0.85, 0.38, −1.70 | −0.45, 0.93, −0.75 |
| `taxi.glb` | Frankfurt Taxi | 6,176 (4,256 + 1,920) | 319 | 2.25 × 1.61 × 4.86 | −0.79, 0.34, −1.425 | −0.38, 0.55, 0.55 |
| `police.glb` | Polizei Streifenwagen | 6,224 (4,304 + 1,920) | 326 | 2.28 × 1.67 × 4.91 | −0.80, 0.35, −1.45 | −0.38, 0.55, 0.525 |
| `bus.glb` | RMV Stadtbus | 6,914 (4,514 + 2,400) | 349 | 3.26 × 3.33 × 11.52 | −1.05, 0.50, −3.00 | −0.53, 1.04, −4.05 (estimated, no seat modelled) |
| `truck.glb` | Osthafen LKW | 5,130 (2,730 + 2,400) | 292 | 3.26 × 3.40 × 8.56 | −1.05, 0.50, −2.40 | −0.55, 1.17, −2.55 |

**Origin:** ground level (the tyres touch y = 0) under the centre of the body; the body runs from z = −L/2 (front
bumper) to +L/2, symmetric in x. GTF's own vehicle root is the physics body centre a few cm above the ground; the
export shifts everything so the origin is on the ground.

**Nodes:**

```
<class>            root, no mesh; extras = GTF's spec (see below)
├── body           one mesh, one primitive per material slot (paint, glass, lights, plate, trim, ...)
├── wheel_fl       mesh node, origin = hub centre; front left  (x < 0, z < 0)
├── wheel_fr       front right
├── wheel_rl       rear left  (bus/truck: both tyres of the twin wheel in this one mesh)
└── wheel_rr       rear right
```

Wheels: spin about the node's local **+X** (the axle), steer the front wheels about local **+Y**. Compose as
`rotation = Ry(steer) · Rx(spin)` (steer in the parent, spin about the steered axle). Positive steer turns the
wheel to the left (front towards −X); rolling forward is a negative spin (`spin = −distance / wheelRadius`). The
hub sits at `y = wheelRadius` with no suspension compression; GTF lowers each wheel by its suspension travel.

**Material slots** (glTF material names; each is one primitive in `body` or a wheel):

| Slot | What | glTF values |
|---|---|---|
| `paint` | car body colour | metallic 0.6, roughness 0.35, `KHR_materials_clearcoat` 1.0 / roughness 0.1 |
| `glass` | all windows | #0b1118, roughness 0.06, alpha blend 0.82 |
| `headlight` | headlamp lenses | #4e535a metallic 0.9, roughness 0.15, emissive black (off) |
| `taillight` | rear lamp lenses | #6a0b0b, emissive #ff2010 × 0.15 (GTF's running light) |
| `indicator`, `white` | amber indicator and white reversing inserts (`white` is also the truck box) | plain PBR |
| `plate` | licence plates, texture "F-GW 2026" (512 × 128) | replace the texture for other plates |
| `chrome`, `steel` | grille surround, emblems, exhaust tips, mirror glass / hubs of steel wheels, brake calipers, truck tank and rear bar | metallic 1.0 / 0.9 |
| `plastic_trim`, `plastic_gloss` | matte black bumpers, cladding, arch lips / gloss black grille, pillars, seams | |
| `underbody`, `interior`, `seat` | underside and wheel wells, dashboard and floor, seats | |
| `rubber`, `rubber_sidewall` | tyre tread, tyre sidewall | roughness 0.9 / 0.78 |
| `rim`, `rim_dark`, `brake_caliper` | alloy rim, dark rim well, sports car's red calipers | |
| `livery_blue` (police), `livery_teal` (bus) | painted liveries | |
| `decal_text` (police "POLIZEI", truck "OSTHAFEN LOGISTIK") | text decals 512 × 128 | alpha mask 0.4 |
| `sign` (taxi roof sign, bus destination "36 Hauptbahnhof") | emissive textured signs | emissive = texture × 0.6 / 0.9 |
| `lightbar_left`, `lightbar_right` (police) | blue light bar halves | emissive #2040ff × 0.2, alpha blend 0.9 |

**Colouring the paint:** set the base colour of material `paint` (glTF `baseColorFactor`; Godot
`StandardMaterial3D.albedo_color` of that surface, Unity `material.color`/`_BaseColor`, Unreal the base colour of
the material instance in that slot). Give each car its own material instance. Exported colours: compact #2a6b8a,
sedan #c0c3c8, sports #8a1c1c, suv #3a3f4a, van #f2f2f0, truck cab #1f3a7a; taxi #efe0b0, police #dfe3e8 and bus
#f2f4f6 are fixed in GTF. GTF picks traffic colours from this palette (also in the root extras as `paintPalette`):
#0d0f14 #f2f2f0 #c0c3c8 #5b5f66 #8a1c1c #1f3a7a #2d5a2d #b8862b #4b2f6b #e8e0c8 #6b2a12 #2a6b8a #9c1f3b #3a3f4a #d94a1f #1a4f3a.

**Lights as GTF drives them** (emissive colour × strength): headlights #fff2d2 × 2.5 at night, 0 by day;
taillights #ff2010 × 0.15 by day, × 1.0 at night, + 2.5 while braking; police light bar #2040ff × 0.2 idle, × 6 on
the flashing half (left and right alternate). A crushed front or rear multiplies its lamps by 0.05 / 0.08.

**Root extras** (glTF `extras` on the `<class>` node): `length`, `width`, `height` (body, without mirrors),
`wheelBase`, `track`, `wheelRadius`, `suspensionRest`, `steerMaxRad`, `massKg`, `maxSpeedKmh`, `seats`, `gtfName`,
`paint`, `paintPalette`, `driverSeat` (top centre of the driver's seat cushion: put the seated character's pelvis
just above it), `lights`, `gtfCommit`.

## People – `people/<archetype>.glb`

GTF's skinned pedestrians: lofted body with hands (five fingers), a painted face with eyeballs, ears and nose, hair
or hat shells, and clothing cells carrying GTF's photographed weaves (jersey, wool, melton, ripstop, twill).

| File | Sex | Top / shirt | Bottom | Shoes | Hair, hat | Triangles | Vertices | KiB | Height (m) |
|---|---|---|---|---|---|---|---|---|---|
| `casual_male.glb` | male | t-shirt #7a1f2b | jeans #2a3a5a | sneakers | short | 3,334 | 2,124 | 1,386 | 1.80 |
| `casual_female.glb` | female | t-shirt #e8e6e0 | jeans #1f2a44 | sneakers | ponytail | 3,398 | 2,169 | 1,401 | 1.79 |
| `hoodie_male.glb` | male | hoodie #3a3a4a | jeans #1a1a2a | sneakers | buzz, beanie | 3,278 | 2,105 | 1,371 | 1.81 |
| `suit_male.glb` | male | suit #1c2233, shirt #f4f4f4, tie | slacks #1c2233 | leather | short | 3,414 | 2,196 | 1,361 | 1.80 |
| `suit_female.glb` | female | suit #23262d, shirt #dfe8f2, tie | slacks #23262d | leather | bun | 3,478 | 2,241 | 1,372 | 1.79 |
| `coat_male.glb` | male | overcoat #3a3a3a over shirt | slacks #3c3c44 | leather | short, grey | 3,510 | 2,252 | 1,411 | 1.80 |
| `coat_female.glb` | female | camel overcoat #b08a5a | slacks #2a2a2a | leather | long | 3,630 | 2,330 | 1,396 | 1.79 |
| `sportswear_male.glb` | male | track jacket #1f3f8a | track pants #1a1a1a | sneakers | short, cap | 3,226 | 2,083 | 1,393 | 1.81 |
| `sportswear_female.glb` | female | track jacket #b02020 | track pants #2a2a2a | sneakers | ponytail | 3,398 | 2,169 | 1,391 | 1.79 |
| `shirt_female.glb` | female | shirt #6a8ab0 | jeans #4a5a78 | leather | bob | 3,502 | 2,241 | 1,377 | 1.79 |
| `player.glb` | male | hoodie #5a1f1f (GTF's protagonist, `Game.ts`) | jeans #1f2a44 | sneakers | buzz | 3,326 | 2,114 | 1,379 | 1.79 |
| `police.glb` | male | blue t-shirt #1f3f7a (GTF's officer, `Police.ts`) | jeans #14264a | sneakers | short, cap #1a2a5a | 3,226 | 2,083 | 1,384 | 1.81 |

**Origin and facing:** the origin is on the ground between the feet; the character faces **−Z** (GTF's people face
+Z; the export turns geometry, bones and every key 180° about Y so cars and people share one convention). Its left
hand is on −X. Rest pose: standing straight, arms hanging down (not a T-pose).

**Mesh and materials:** one skinned mesh node `body` with two primitives: material `body` (1024² base colour atlas
with the outfit colours baked in, 1024² normal map at scale 0.7, roughness) and material `face` (512 × 576 painted
face with the eyeball strip on top, roughness). Up to 2 bone influences per vertex. No vertex colours: GTF tints a
shared atlas per vertex; the export bakes those colours into each file's own atlas, so every importer shows them.
The validator's one warning per file (`MESH_PRIMITIVE_GENERATED_TANGENT_SPACE`) means the engine computes MikkTSpace
tangents for the normal map on import, which is what glTF prescribes when a file has none.

**Skeleton** (18 joints, all rest rotations identity):

```
root (at the feet, origin)          only animated by the death clips (the fall pitches about the feet)
└── hips (0, 0.95, 0)
    ├── torso ── chest ─┬─ neck ── head
    │                   ├─ armL ── foreL ── handL
    │                   └─ armR ── foreR ── handR
    ├── thighL ── shinL ── footL
    └── thighR ── shinR ── footR
```

`L`/`R` are the character's own left/right. Unity Humanoid mapping: hips → Hips, torso → Spine, chest → Chest,
neck → Neck, head → Head, armL/foreL/handL → Left Upper Arm/Lower Arm/Hand, thighL/shinL/footL → Left Upper
Leg/Lower Leg/Foot (same for R); no shoulders, toes or finger bones (fingers are rigid on the hand bone); use
"Enforce T-Pose" when configuring the avatar. Weapon socket (GTF's grip point): on `handR` at
(−0.012, −0.118, −0.015) with rotation quaternion (x −0.7071, y 0, z 0, w 0.7071); a weapon modelled with the
barrel along −Z and the grip down −Y attaches there without further rotation.

**Clips** (all in place, no root motion; every clip keys all 18 bone rotations plus the `hips` position):

| Clip | Length (s) | Loop | What |
|---|---|---|---|
| `idle` | 10.0 | yes | standing: weight shift, breathing, head turns |
| `walk` | 0.873 | yes | one stride cycle; reference speed 1.4 m/s – GTF plays it at rate speed / 1.4 |
| `run` | 0.571 | yes | one stride cycle; reference speed 7.2 m/s – GTF plays it at rate speed / 7.2 |
| `aim` | 3.696 | yes | two-handed pistol aim, standing, level |
| `aim_walk` | 3.491 | yes | aiming while walking (four stride cycles, 1.4 m/s) |
| `punch` | 0.35 | no | right jab from the guard and back (60 fps) |
| `jump` | 0.70 | no | take-off tuck into the airborne pose; hold the last key while in the air |
| `sit` | 3.696 | yes | seated, hands forward at steering-wheel height |
| `drive` | 3.696 | yes | identical to `sit`: GTF drives with its sit pose |
| `death` | 0.80 | no | falls on its back (GTF: 65 % of deaths); hold the last key |
| `death_forward` | 0.80 | no | falls on its face (GTF: 35 %) |
| `swim` | 2.992 | yes | breaststroke, chest deep (GTF also uses it as the flight pose) |
| `pedal` | 1.0 | yes | on a city bike, one crank turn |
| `scoot` | 3.696 | yes | standing on an e-scooter |
| `skate` | 1.2 | yes | on a skateboard, one push stroke |
| `ride` | 3.696 | yes | on a motorbike or moped |

GTF computes its poses every frame from time; the export samples them at 30 fps (punch 60). Loops whose parts have
no common period (the idle sway, breathing under a walk) are closed by blending into the pose one period earlier, so
the last key equals the first. GTF also tilts the aim with the camera pitch (arms and head) and cross-fades states
over 0.15 s – both are left to the engine.

**A driver in a car:** play `drive` with the character's root at the car's `driverSeat` extra + (0, −0.45, +0.10)
in car space, with no extra rotation (both face −Z); checked in the sedan with the paint and glass hidden.

The extras of the `body` node hold the outfit, `height`, `weaponSocket` and the clip list with lengths and
reference speeds. Archetypes are defined in `tools/export-gtf-models/src/people.ts` (`ARCHETYPES`, GTF's own
colour palettes and styles); more are one line each.

## Regenerate

```
cd shared/tools/export-gtf-models && npm install
GTF_DIR=/path/to/Grand-Theft-Frankfurt node export.mjs all     # or vehicles | people
node validate.mjs                                              # Khronos validator, exit 1 on any error
node preview.mjs /tmp/cars.png --vehicles                      # renders the GLBs back with three.js
node preview.mjs /tmp/walk.png --people=walk                    # every archetype in one clip
GTF_DIR=... node preview.mjs /tmp/cmp.png cells.json            # cells with "gtf"/"gtfPerson" render GTF's own object
```

`GTF_DIR` needs only GTF's `src/` and `public/textures/`; Playwright (global install is fine) runs the page.
