A 3DM file is a 32-byte text header followed by a sequence of chunks. Every chunk starts with a four-byte typecode, so a reader can skip any chunk it does not understand. The format is defined by McNeel’s openNURBS toolkit, whose source is public; the names below are its names. All numbers are little-endian on every platform.
File header
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 24 | char[24] | signature | 3D Geometry File Format , with one space at the end |
| 24 | 8 | char[8] | archive version | the archive version as a whole number in ASCII digits, right-justified with spaces, at least 1 |
00000000 33 44 20 47 65 6F 6D 65 74 72 79 20 46 69 6C 65
00000010 20 46 6F 72 6D 61 74 20 20 20 20 20 20 20 37 30
70openNURBS has a temporary rule from the early days: a version field that ends in
X is read as version 2.
Archive versions
The archive version says which rules the rest of the file follows. Each major Rhino release has its own; openNURBS writes only 2, 3, 4, 50, 60, 70 and later multiples of ten, turning a requested 5 into 50.
| Version | Written by | Chunk value |
|---|---|---|
| 1 | Rhino 1 | 4 bytes; no tables, 16-bit CRCs on legacy chunks |
| 2 | Rhino 2 | 4 bytes |
| 3 | Rhino 3 | 4 bytes |
| 4 | Rhino 4 | 4 bytes |
| 50 | Rhino 5 | 8 bytes |
| 60 | Rhino 6 | 8 bytes |
| 70 | Rhino 7 | 8 bytes |
| 80 | Rhino 8 | 8 bytes |
openNURBS decides the size of a chunk value from the archive version alone: four bytes below 50, eight bytes from 50. A comment in its source notes that early version 5 files had four-byte values too.
A second number, the version of openNURBS that wrote the file, is kept in the
properties table (TCODE_PROPERTIES_OPENNURBS_VERSION). openNURBS 1 to 5
wrote it as a date, YYYYMMDDn, such as 200910190. From
version 6 it is a packed number holding the major and minor version, the year and day
of the year, and a build branch; openNURBS tells the two forms apart with its function
ON_VersionNumberIsYearMonthDateFormat.
Chunks
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 4 | u32 | typecode | what the chunk holds, with flag bits (see Typecodes) |
| 4 | 4 or 8 | i32 or i64 | value | for a short chunk, the data itself; for a long chunk, the number of bytes that follow |
| 8 or 12 | value | bytes | body | long chunks only: the data, often other chunks, and a CRC at the end when the typecode asks for one |
Short and long chunks
When the typecode has its top bit set (TCODE_SHORT,
0x80000000), the chunk is short: the value is the whole of its data,
such as an index, a colour or an on/off setting, and no body follows. Otherwise the
chunk is long and the value is the length of the body, counted from just after the
value field to the end of the chunk, including any CRC. A long chunk’s body can hold
further chunks, so the file is a tree.
CRCs
In archive versions 2 and later, a long chunk whose typecode has the
TCODE_CRC bit (0x8000) ends with a four-byte CRC-32 of the
body before it, computed by openNURBS’s ON_CRC32. Chunks without that
bit have no CRC. The chunk header itself has no CRC; openNURBS notes that the length
is written twice instead.
In version 1 files a two-byte CRC-16 (ON_CRC16) ends the legacy
geometry chunks (typecodes with TCODE_LEGACY_GEOMETRY),
TCODE_SUMMARY, and the old object chunk 0x0002FFFD (a
16-byte UUID and its CRC).
Order of a file
From version 2, the chunks after the header are grouped into sections and tables. openNURBS reads and writes them in this order and refuses any other:
- Start section: one
TCODE_COMMENTBLOCKchunk (0x00000001), which always begins at offset 32. Its body is text about the file and the program that wrote it (openNURBS adds the runtime, its own version and its build date) ended by a^Z(1A) and a zero byte. Files written before February 2000 have no^Z. - Properties table: revision history, notes, preview image, the application that wrote the file, the openNURBS version, and the file name it was saved as.
- Settings table: units and tolerances, mesh settings, annotation settings, named construction planes and views, the current layer, material, colour and other defaults, render settings and the plug-in list.
- Bitmap table.
- Texture mapping table.
- Material table.
- Linetype table.
- Layer table.
- Group table.
- Text style (font) table.
- Dimension style table.
- Light table.
- Hatch pattern table.
- Instance definition table: block definitions.
- Object table: the geometry and annotation.
- History record table: construction history.
- User tables, written by plug-ins, any number.
- End mark: one
TCODE_ENDOFFILEchunk.
A table is one long chunk whose typecode names it (TCODE_LAYER_TABLE and
so on), holding one chunk per record and ending with a TCODE_ENDOFTABLE
chunk (0xFFFFFFFF), which is short and has the value 0. The settings
and properties tables use one record chunk per setting. Version 1 files, from Rhino 1,
predate the tables: they hold the legacy chunks listed on the
Typecodes page, which openNURBS reads
by scanning the file.
End mark
The last chunk is TCODE_ENDOFFILE (0x00007FFF), a long chunk
without a CRC. Its body is the length of the whole file, end mark included, in four
bytes when chunk values are four bytes and in eight when they are eight. A reader can
compare it with the real size to tell whether the file was cut short. It was added in
February 2000, so older version 1 files may lack it, and openNURBS accepts a version 1
file without one.
00000000 FF 7F 00 00 08 00 00 00 00 00 00 00 40 E2 01 00
00000010 00 00 00 00
Object records
Each object in the object table is a TCODE_OBJECT_RECORD chunk
(0x20008070) holding these chunks in order:
| Typecode | Name | Holds |
|---|---|---|
| 0x82000071 | TCODE_OBJECT_RECORD_TYPE | required, short: the object type (below), so a reader can skip kinds it does not want |
| 0x00027FFA | TCODE_OPENNURBS_CLASS | required: the object itself, as a class chunk |
| 0x02008072 | TCODE_OBJECT_RECORD_ATTRIBUTES | optional: the object’s attributes (ON_3dmObjectAttributes: layer, colour, name, visibility and the like), then a CRC |
| 0x02000073 | TCODE_OBJECT_RECORD_ATTRIBUTES_USERDATA | optional: user data attached to the attributes, as user data chunks |
| 0x02008074 | TCODE_OBJECT_RECORD_HISTORY | optional: a two-byte version, then TCODE_OBJECT_RECORD_HISTORY_HEADER (0x02008075) and TCODE_OBJECT_RECORD_HISTORY_DATA (0x02008076), each a two-byte version, content and CRC |
| 0x8200007F | TCODE_OBJECT_RECORD_END | required, short: ends the record |
Light records (TCODE_LIGHT_RECORD) are built the same way, with
TCODE_LIGHT_RECORD_ATTRIBUTES, …_ATTRIBUTES_USERDATA and
TCODE_LIGHT_RECORD_END.
Object types
The value of TCODE_OBJECT_RECORD_TYPE is one of openNURBS’s
ON::object_type values. They are bits, so the filter values can be
combined; a record holds exactly one.
| Value | Name | Meaning |
|---|---|---|
| 0x00000000 | unknown_object_type | not known |
| 0x00000001 | point_object | a point |
| 0x00000002 | pointset_object | a point cloud or point set |
| 0x00000004 | curve_object | a curve: line, arc, polyline, NURBS and so on |
| 0x00000008 | surface_object | a surface |
| 0x00000010 | brep_object | a boundary representation: trimmed surface or polysurface |
| 0x00000020 | mesh_object | a polygon mesh |
| 0x00000040 | layer_object | a layer |
| 0x00000080 | material_object | a material |
| 0x00000100 | light_object | a light |
| 0x00000200 | annotation_object | text, a dimension or a leader |
| 0x00000400 | userdata_object | user data |
| 0x00000800 | instance_definition | a block definition |
| 0x00001000 | instance_reference | a block instance |
| 0x00002000 | text_dot | a text dot |
| 0x00004000 | grip_object | a grip, the editing handle of another object |
| 0x00008000 | detail_object | a layout detail view |
| 0x00010000 | hatch_object | a hatch |
| 0x00020000 | morph_control_object | a morph control (cage editing) |
| 0x00040000 | subd_object | a subdivision surface |
| 0x00080000 | loop_object | a brep loop |
| 0x00100000 | brepvertex_filter | selection filter: brep vertices |
| 0x00200000 | polysrf_filter | selection filter: polysurfaces |
| 0x00400000 | edge_filter | selection filter: edges |
| 0x00800000 | polyedge_filter | selection filter: chains of edges |
| 0x01000000 | meshvertex_filter | selection filter: mesh vertices |
| 0x02000000 | meshedge_filter | selection filter: mesh edges |
| 0x04000000 | meshface_filter | selection filter: mesh faces |
| 0x07000000 | meshcomponent_reference | a reference to mesh components (the three above together) |
| 0x08000000 | cage_object | a cage |
| 0x10000000 | phantom_object | a phantom object |
| 0x20000000 | clipplane_object | a clipping plane |
| 0x40000000 | extrusion_object | an extrusion |
| 0xFFFFFFFF | any_object | filter: every type |
Class chunks
Every openNURBS object written into a file (geometry, and the records of most tables)
is wrapped in a TCODE_OPENNURBS_CLASS chunk, which names the object’s
class by a UUID and holds the data that class writes:
| Typecode | Name | Holds |
|---|---|---|
| 0x0002FFFB | TCODE_OPENNURBS_CLASS_UUID | the 16-byte class id (ON_ClassId) that says which class follows, then a CRC |
| 0x0002FFFC | TCODE_OPENNURBS_CLASS_DATA | the object’s data, as written by that class’s Write function, then a CRC |
| 0x00027FFD | TCODE_OPENNURBS_CLASS_USERDATA | zero or more user data chunks (below) |
| 0x80027FFF | TCODE_OPENNURBS_CLASS_END | short, value 0: ends the class chunk |
What the data of each class holds (a NURBS curve’s degree, knots and control points, a
mesh’s vertices and faces, a layer’s name and colour) is defined by that class’s
Write and Read functions in openNURBS, each with its own
version numbers. There are many such classes and this site does not list their
layouts; the openNURBS source is the reference for them.
User data
Plug-ins can attach their own data to any object. Each
TCODE_OPENNURBS_CLASS_USERDATA chunk starts with a two-byte chunk
version (2.1 in current files), then holds:
| Part | Size | Description |
|---|---|---|
TCODE_OPENNURBS_CLASS_USERDATA_HEADER (0x0002FFF9) | chunk | the user data header, below |
| class id | 16 | UUID of the ON_UserData class that wrote it |
| user data id | 16 | m_userdata_uuid, which identifies this piece of user data |
| copy count | 4 | m_userdata_copycount: 0 when the data is not copied along with its object, otherwise a count of the copies made |
| transform | 128 | m_userdata_xform, a 4×4 matrix of doubles |
| application id | 16 | m_application_uuid, the plug-in, in version 2.1 chunks only |
TCODE_ANONYMOUS_CHUNK (0x40008000) | chunk | the plug-in’s own data, which only that plug-in can read |
User tables
Each user table is a TCODE_USER_TABLE chunk (0x10000017) with
two chunks inside. TCODE_USER_TABLE_UUID (0x20008080) holds the
UUID of the plug-in that wrote it; in files of version 50 or later written by openNURBS
200910190 or newer it also holds a TCODE_USER_TABLE_RECORD_HEADER
(0x20008082). TCODE_USER_RECORD (0x20000081)
holds the plug-in’s data. A reader without the plug-in skips both.