Format reference · .3do

Models & moving pieces

A model is more than a mesh. Follow its pieces, inspect its faces, and discover how a compact tree of geometry becomes a unit on screen.

Structure
Tree of named pieces
Coordinates
Signed 16.16 fixed point
Byte order
Little-endian
Root record
52 bytes · offset 0

A model you can take apart

A .3do file stores a model as a hierarchy of named pieces. Each piece owns its vertices, faces, and a translation from its parent. Pieces can form a chassis, turret, barrel, articulated limb, or even an invisible point where an effect begins.

Explore a 3DO

One model. Five connected pieces.

Original five-piece survey crawler: a low chassis with side tracks, amber turret, green-tipped barrel, rear mast and a muzzle locatorEXAMPLE.3DO / AUTHORING SPACE
An original survey crawler, decoded from example.3do. Orbit the view, select a piece, or switch to wireframe. Rotate the turret to see its descendants follow; explode the hierarchy to separate the pieces. Selection plate overlays the face designated by the root’s selection index, rather than simulating retail selection rendering.

Models live under objects3d/. A unit’s ObjectName selects its model. Its corpse reference selects a feature definition, whose object names the wreck model. Names such as <unitname>.3do and <unitname>_dead.3do are conventions, not lookup rules. See the FBI and TDF references.

Stored in the fileSupplied elsewhere
Named pieces and parent-relative translationsScript motion from COB; unit and projectile transforms from runtime state
Vertex positions and ordered face indexesRuntime normals, lighting, culling and rasterization
Palette-index fields and optional texture namesImage data in texture GAF entries; palette and lookup tables
A per-piece selection-primitive indexRuntime normalization, drawing, picking and bounds contracts

Follow the object tree

There is no separate file header. The root is an ordinary 52-byte object record at file offset 0. Each record points to its name, vertex array, primitive array, first child, and next sibling. All integers are little-endian; all stored offsets are absolute from the start of the file.

Object @ 0x0000  (52 bytes)
  name pointer ───────► NUL-terminated piece name
  vertices pointer ──► Vertex[n]       (12 bytes each)
  primitives pointer ► Primitive[m]   (32 bytes each)
                         indexes ────► u16[k], local to this piece
                         texture ────► GAF entry name, or 0
  child pointer ─────► first child Object
  sibling pointer ───► next Object with the same parent

Children of one parent form a linked list through their sibling pointers. Zero terminates a child or sibling link. The decoder also accepts a root sibling chain; the layout does not prohibit one. Names, arrays and other objects can appear elsewhere in the file. Follow their pointers instead of assuming an exporter’s packing order.

Piece anatomy

Local translations add along the chain

base
├── turret       (+0, +8, +0)
│   └── barrel   (+0, +3, −5)
│       └── flare (+0, +0, −15)
└── mast         (−8, +8, +8)
Exploded view of the original model, with lines linking base to turret and mast, turret to barrel, and barrel to flare
flare origin in bind pose(0, 8, 0) + (0, 3, −5) + (0, 0, −15)= (0, 11, −20)
The fixture’s turret and mast are siblings. The barrel belongs to the turret; flare belongs to the barrel. The drawing separates pieces for inspection. The arithmetic uses their original, unmodified bind-pose translations.

Without runtime rotations, summing parent translations places each piece’s origin in the file’s bind-pose coordinate system. The flare in this example is at (0, 11, −20). That is not a world position: it has not yet gone through runtime import, piece animation, unit orientation, or world placement.

A piece with one vertex and no primitives is useful. It has a position but no surface to draw. Such pieces commonly locate muzzle flashes, smoke, nano spray, thrust, or factory build pads. Select flare in the inspector to see one.

Byte layouts

The following offsets are relative to each record. i32 is signed; u32 and u16 are unsigned. Each pointer targets an absolute file offset. Vertex positions and piece translations use signed 16.16 fixed point.

Object record · 52 bytes

Scroll table horizontally to see all fields →

OffsetBytesTypeFieldMeaning
+0x004i32VersionSignature1 in surveyed retail files. Retail never reads this word; Nanolathe’s decoder requires 1.
+0x044i32NumberOfVertexesVertices owned by this piece. May be zero.
+0x084i32NumberOfPrimitivesPrimitives owned by this piece. May be zero.
+0x0C4i32OffsetToSelectionPrimitiveHistorical name: a primitive index, not a byte offset. −1 means none; 0 selects primitive 0 when present.
+0x104i32XFromParentSigned 16.16 translation from the parent’s origin.
+0x144i32YFromParentParent-relative translation; Y is up.
+0x184i32ZFromParentParent-relative translation; see coordinate conventions.
+0x1C4i32OffsetToObjectNamePointer to a NUL-terminated piece name.
+0x204u32OptionalAuxiliaryOffsetRelocated when nonzero. Zero in the surveyed corpus; target meaning unknown. Historically Always_0.
+0x244i32OffsetToVertexArrayPointer to NumberOfVertexes × 12 bytes.
+0x284i32OffsetToPrimitiveArrayPointer to NumberOfPrimitives × 32 bytes.
+0x2C4i32OffsetToSiblingObjectNext piece sharing this parent; zero ends the list.
+0x304i32OffsetToChildObjectFirst child; zero means no children.

Vertex record · 12 bytes

Scroll table horizontally to see all fields →

OffsetBytesTypeFieldMeaning
+0x004i32xSigned 16.16 local X position.
+0x044i32ySigned 16.16 local Y position.
+0x084i32zSigned 16.16 local Z position.

Decode a coordinate as signed_integer / 65536. The sign belongs to the whole 32-bit word; do not interpret the fractional half independently. For example:

Stored bytes, little-endianSigned integerModel units
00 00 01 0065536+1.0
00 80 06 00425984+6.5
00 80 FE FF−98304−1.5

Primitives share vertices within their own piece through index arrays. An index never names a vertex in another piece. A lossless decoder retains authored coordinates; the runtime import conversion is a later step.

Primitive record · 32 bytes

Scroll table horizontally to see all fields →

OffsetBytesTypeFieldMeaning
+0x004u32ColorIndexAuthored palette-index field. Some textured retail faces contain values above 255; preserve the full word.
+0x044i32NumberOfVertexIndexesNumber of u16 indexes in this face’s index array. Stored counts include lines, triangles, quads and larger polygons.
+0x084u32OptionalAuxiliaryOffsetRelocated when nonzero; meaning unknown. Historically Always_0.
+0x0C4i32OffsetToVertexIndexArrayPointer to this face’s local u16 vertex indexes.
+0x104i32OffsetToTextureNamePointer to a NUL-terminated GAF entry name, without an extension. Zero means no texture name.
+0x144i32Unknown_1Often nonzero. Historically attributed to editors; do not validate as zero.
+0x184i32Unknown_2Same caveat; preserve it.
+0x1C4i32IsColoredAuthored flag word. Bit 0 distinguishes flat from textured rendering on the established path; noncanonical full-word values occur.

Strings and packing

Object and texture names are NUL-terminated strings and can appear anywhere their pointers address. One observed exporter layout puts a texture-name pool immediately after the root, then indexes, vertices, primitives, and additional objects. That is an observation, not a format constraint. Records need not be aligned to four-byte boundaries; the authored fixture’s vertex array starts at 0x0109.

Faces and implied texture corners

A texture name refers to an entry in a GAF under textures/. The name identifies image data; the 3DO does not embed it. Read the GAF guide for named entries and frames, or the palette reference for indexed colors and lookup tables.

For a textured quad, the established default corner assignment is:

Position in the face’s index arrayTexture coordinate
First(0, 0)
Second(width − 1, 0)
Third(width − 1, height − 1)
Fourth(0, height − 1)
Interactive example

The index order turns the texture

Texture entry · 160 × 160
Original four-corner texture: A at top-left, B top-right, C bottom-right, D bottom-left; arrow points up

Each quad corner receives a texture corner.

Face · viewed straight on
Default vertex order maps A, B, C, D around the face clockwise from top-leftv0v1v2v3

Index order [0, 1, 2, 3] → A, B, C, D.

A cyclic reordering preserves winding but changes which geometric vertex receives (0, 0). This diagram shows corner assignment; it does not simulate culling, shading or scanline rounding.

The geometric corners stay fixed while the index array cycles. Order [1, 2, 3, 0] gives geometric vertex 1 the texture’s A corner, rotating the image a quarter-turn. No UV fields were edited: none exist. This separate authored diagram uses the corner rule from 03 §2.4.1; the downloadable crawler uses flat-color primitives only.

Runtime dispatch is a separate contract

The bounded rendering research establishes the following branch behavior. These are runtime rules, not additional records in the file:

ConditionEstablished runtime behavior
IsColored bit 0 setFlat polygon filler using an explicit vertex count.
Bit 0 clear and vertex count not 4No textured face is drawn on this path.
Bit 0 clear and vertex count 4Textured quad mapper; texture resolution may use loader-owned state.
Texture name cannot be resolvedLoader changes the primitive to flat color index 209.
Shaded span writer selectedSamples PALETTE.SHD using the computed/interpolated row and source index.
Unshaded span writer selectedUses the source palette index directly.

The loader owns resolve-at-draw-time and team flags. The owner’s color index can select a complete LOGOS.GAF frame; see GAF team textures. Do not infer runtime precedence from a texture name alone, or from the complete integer value of IsColored.

The model stores no normals. Normal construction, renderer selection, SHD rows, dont-shade, affine sampling and culling belong to the behavioral reference. The inspector above uses neutral face contrast for readability; it does not preview those retail shading operations. Stored polygon counts are also not triangulation advice: retail’s established path does not first fan-triangulate every face.

The selection primitive

The selection field designates a face in its own piece. Retail normalizes this field on every piece, not just the root. Its drawing and picking consumers live in the behavioral specification.

In the original crawler, root selection value 0 designates the first face: a flat X/Z quadrilateral at Y = 0. Toggle Selection plate in the inspector to overlay it. The overlay is a visualization of the reference; it does not claim that the engine renders that face with a translucent green material.

Root selection valueModels in the 761-model surveyMeaning
Positive index361The indexed primitive
0300Primitive 0
−1100No designated selection primitive

A separate census cross-referenced 278 winning unit definitions to 278 unique models, excluding wrecks, projectiles and map features. All had at least one flat root X/Z quad. Among them, 167 selected primitive 0, 109 selected a positive index, and two (armmstor, cormine1) stored −1 despite still containing a flat primitive-zero plate.

Coordinates: authored, imported, world

A 3DO’s authored coordinates are Y-up. Retail then persistently negates both X and Z, for vertices and parent translations: a half-turn about Y. After composition, a model-space offset (x, y, z) maps to world offset (x, y, −z). These are two separate operations.

Coordinate spaces

Two conversions, two different jobs

01 / Stored in 3DOAuthored locator(2, 1, −30)
02 / Import half-turnNegate X and Z(−2, 1, +30)
03 / World offsetMap (x, y, −z)(−2, 1, −30)

Example with no additional piece rotation or parent translation. The first conversion is persistent model-space import. The last is the composed model-to-world offset mapping. A lossless decoder preserves step 01.

The source locator (2, 1, −30) becomes (-2, 1, +30) in imported model space, and (-2, 1, −30) as a world offset in this unrotated example. A source-Z-only reflection is not the recovered import operation. 03 §2.4 and R-RAST-01 §8.

Supported inference: a common authoring convention puts the nose toward −Z. A recorded locator census found negative local Z for flare* translations 135 times, versus positive Z 13 times. That supports an asset convention, not a mandatory forward axis. Historical documentation also described +Z-forward; preserve the distinction between convention and the established runtime transform.

The 16.16 interpretation matches the surveyed data, but the research identifies no vendor statement of that scale. BOS/COB linear script values use another scale: 1 BOS unit = 2.5 model units. See the COB reference.

A file you can inspect

Download example.3do · 3,750 bytes and its decoded model and offset manifest. The manifest includes all five records, the local geometry, raw primitive fields represented by the fixture, and the original display palette. Nothing in the download depends on retail data.

PieceObject offsetParentVertices / primitives
base0x000036 / 25
turret0x0034base16 / 12
barrel0x0068turret16 / 12
flare0x009Cbarrel1 / 0
mast0x00D0base16 / 12

This fixture puts five object records first, followed by each piece’s names and arrays. The root’s first 52 bytes are the root object itself:

01 00 00 00  24 00 00 00  19 00 00 00  00 00 00 00
00 00 00 00  00 00 00 00  00 00 00 00  04 01 00 00
00 00 00 00  09 01 00 00  B9 02 00 00  00 00 00 00
34 00 00 00

Read those words as version 1, 36 vertices, 25 primitives, selection index 0, zero translation, name at 0x0104, zero auxiliary offset, vertices at 0x0109, primitives at 0x02B9, no root sibling, and first child at 0x0034.

The first vertex at 0x0109 is:

00 00 EF FF  00 00 00 00  00 00 EA FF

That is (-1114112, 0, -1441792) as signed integers, or (−17, 0, −22) model units. The selection face’s index array is [0, 1, 2, 3], so this is one corner of its flat plate. The turret’s sibling pointer reaches the mast; its child pointer reaches the barrel. The barrel’s child reaches the one-vertex flare.

The website’s scripts/3do-example.py writes the fixture, independently follows its pointers and index arrays, and generates the images and manifest from the decoded result. --check validates the committed outputs, including fixed-point quantization. The page does not use the publication-omitted retail byte dumps.

Loading, ordering and measured bounds

Retail reads the file whole. A missing or zero-length model is fatal, with the path shown, whether a unit, weapon, or feature names it. The loader relocates names and auxiliary offsets when nonzero, array offsets unconditionally, and child and sibling links recursively. It also relocates the primitive offsets. These operations are unbounded, and the relocator has no visited set.

Primitive compilation is runtime behavior. The loader normalizes selection and creates an ordered view using its established ordering and arithmetic. A lossless parser should retain authored primitive order and the original selection field; a runtime compiler can create a separate ordered representation. Do not bake runtime sorting into the stored-format interpretation.

The model-top walk is not an ordinary bounding box

Established: the definition’s upper Y bound is measured by a recursive sibling-chain walk. Each invocation starts top = 0. It considers every vertex.y + piece.translation.y. For children, it recursively computes a fresh child maximum, adds the parent piece’s own Y translation, and compares again. Additions wrap to signed 32 bits; comparisons are signed.

Zero is a floor at each recursive level. A small constructed example shows why that matters:

StepExample with a root translated to Y = +10
GeometryRoot has no vertices. Its child has zero translation and one vertex at local Y = −5.
Highest accumulated vertex+10 + (−5) = +5
Child walkmax(0, −5) = 0
Root walkmax(0, +10 + 0) = +10

The measured top is 10, although the only vertex reaches 5. The walk reads no script orientation and has no minimum-vertex-count gate. This example illustrates the specified arithmetic; it is separate from the downloadable model.

Host policy and remaining unknowns

Checked decoding and encoding

AreaNanolathe’s policy or limitation
VersionLoadThreeDO requires version 1 even though retail ignores it.
GraphRejects cycles and shared nodes; walks sibling lists iteratively and bounds child depth.
GeometryApplies aggregate object, geometry and polygon-index budgets.
PreservationKeeps original bytes, authored primitive order and raw fields.
Auxiliary offsetsStores both raw words under the legacy AlwaysZero name without resolving targets.
EncodingCopies auxiliary words while rebuilding other offsets; cannot safely relocate nonzero auxiliary targets.
Textured face countsEncoder restricts textured output to quads; decoder does not impose that restriction.

Retail has no corresponding aggregate budgets and does not check selection or vertex indexes before the ordering pass. A zero-index face reaching comparison in that pass can cause signed division by zero. Checked decoders may reject those malformed inputs instead of reproducing unsafe access. These are implementation acceptance boundaries, not file-format maxima.

Winding, flags and stock distributions

The recorded signed-volume census of 608 base models found 2,029 pieces with outward right-handed winding and 90 with inward winding. Signed volume establishes orientation for closed pieces; it does not by itself label an open piece defective. Runtime culling must come from the behavioral contract.

Survey populationObservationWhat it does not establish
608 base models, 50,443 primitivesAll 43,845 texture-named faces have four indexes; all 6,598 unnamed faces have IsColored bit 0 set. Bit 1 is never authored.Universal validation rules or the meaning of every noncanonical flag word
761 models including expansionsAbout 94% of primitives have four indexes, 4% have three; 315 have two, none have one, and about 950 have 5–16.A triangulation requirement
Same 761-model corpusMaximum hierarchy depth 10; 391 models have only one piece.Format or decoder maxima
Same 761-model corpusAuxiliary words zero; 868 textured faces carry out-of-range color fields.Nonzero auxiliary targets being universally unused

These censuses describe their specified reference populations. They are evidence about shipped assets, not constraints on all authored files.

Common names include base, turret, barrel1, flare1, pelvis, torso, lfoot, rfoot, nano1, pad, and door1. They connect authored model pieces with script queries, motion, and effects. Ship wake* and aircraft thrust locators often have a single vertex and no primitives; wrecks conventionally use names such as ground, wreck, or gp.

The format does not enforce those names. SMOKEPIECE is an authored helper convention. The engine’s SweetSpot query seeds piece index zero, which need not be named base. A parser should preserve names rather than infer fixed behavior from them.

Sources and evidence

This guide adapts the 3DO research at commit 1ef7b91, with a complete plain-text snapshot. It retains the source’s distinction between established byte layouts, bounded asset observations, supported inferences, unknowns, and Nanolathe policy. The texture-corner and span-writer explanations also use the owning behavioral reference at that same revision.

Owning referenceEvidence used here
02 · Content, VFS, formats and data loading“Model archive (3DO)” and R-MALF-01 §8: records, relocation, selection normalization, malformed input, ordering. R-CAT-01 §7: model-top arithmetic.
03 · World, visibility, rendering, audio and video§2.4 and §2.4.1: model import, selection consumers, face dispatch, corner mapping and shading. R-RAST-01 §1–§5, §8: rasterization and composed world offsets. R-RND-02A: renderer selection.
04 · Units, orders, scripts and movement§5.3: piece queries; R-MOV-01 §8b: the submergence consumer of the upper bound.
07 · Interface, input, camera and front endR-REV-01 §7: model-top walk from selection-hover consumers.
Nanolathe decoder and encoderHost implementation; the owning research describes decoder and encoder policy separately from retail behavior.
Unofficial .3do note v0.9.1Dan Melchione, revised by Dark Rain: historical structures and examples. Its selection-offset interpretation is corrected by the later evidence.
3DO, DXF, and LWOHistorical modeling conventions, hierarchy and ground-plate descriptions; forward-axis and flag lore need the qualifications above.

The source research also cites the inspected 3DOBuilder writers for their zero auxiliary fields and a 761-model base/expansion survey. Those findings do not recover the meaning of arbitrary auxiliary targets.

Continue with GAF images and animations, COB unit scripts, or FBI unit definitions.