Skip to main content

MESHBIN

Mesh geometry, optionally with a skeleton. Produced by tools/obj-to-meshbin.py, consumed by MeshManager::LoadMesh.

Every field is little-endian. The reader memcpys field by field rather than casting the buffer to a struct, so there is no implicit padding anywhere: the wire layout is exactly the sum of the field widths below.

Fixed-point conventions​

Three different conventions appear in this file and they are not interchangeable.

DataConventionMeaning
Positions (vertices, AABB, bounding sphere, bone localPos)x128 integer128 units = one engine metre, matching ONE_METRE in world_defs.hh. The exporter multiplies by ONE_ENGINE_METRE = 128.
Bone localRotationFP12 (x4096)A psyqo::GTE::Short, i.e. FixedPoint<12, int16_t>.
UVsraw texel bytes0-255, no scaling.

Position values are copied straight into the .value of a psyqo::Vec3, whose nominal precision is 12 bits. The reader never rescales, so the x128 convention is what actually round-trips. Do not assume a position field behaves like a 12-bit fixed-point number just because its C++ type says so: FixedPoint::integer() on one of these divides by 4096, not 128, and will return 0 for anything under 32 metres.

OffsetFieldTypeBytesNotes
0x00magicchar[7]7MESHBIN, no terminator. Mismatch aborts the load.
0x07versionuint8_t1The exporter emits 3. See the version gating below.
0x08typeuint8_t1Parsed and discarded. MeshBin::type is never assigned and nothing in the engine reads it. The exporter always writes 1.
0x09vertexCountuint32_t4
0x0DindicesCountuint32_t4Number of face index groups. The exporter derives this from the same counter as facesCount, so in practice the two are always equal.
0x11facesCountuint32_t4Rejected if >= MAX_FACES_PER_MESH (1000).
0x15normalsCountuint32_t4
0x19uvCountuint32_t4
0x1DhasSkeletonuint8_t1Version 2 and later only.
0x1EnumBonesuint8_t1Version 2 and later only.

The two skeleton bytes are read only when version > 1. For a version 0 or 1 file they are absent from the stream entirely and every subsequent offset shifts down by two.

Body​

In wire order after the header.

FieldTypeSizePresent when
verticesint32_t x3 per vertex12 x vertexCountalways
vertexColoursuint8_t x3 per vertex3 x vertexCountalways
vertexIndicesint16_t x4 per entry8 x indicesCountalways
normalsint16_t x3 per normal6 x normalsCountalways
normalIndicesint16_t x4 per entry8 x indicesCountalways
uvsuint8_t x2 per uv2 x uvCountalways
uvIndicesint16_t x4 per entry8 x indicesCountalways
collisionBox.minint16_t x36always
collisionBox.maxint16_t x36always
boundingSphere.centreint16_t x36version 3+
boundingSphere.radiusint32_t4version 3+
bones21 bytes per bone21 x numBonesversion 2+ and hasSkeleton
boneForVertexuint8_t per vertex1 x vertexCountversion 2+ and hasSkeleton

Faces​

Every index group is four int16_t, whether the face is a quad or a triangle. A triangle sets the second slot to -1 and carries its three indices in slots 1, 3 and 4. The renderer selects a quad or a triangle primitive by testing that slot.

Vertex colours​

Three unsigned bytes. The exporter writes the OBJ vertex-colour extension values when present and (128, 128, 128) when not. It never emits a sentinel, so there is no way to express "this mesh has no colours"; an absent colour is mid grey.

Bones​

21 bytes each, not 24:

FieldTypeBytes
parentint8_t1
localPosint32_t x312
localRotationint16_t x4 (w, x, y, z)8

parent is -1 for a root bone. MAX_BONES is 15; see the caveats below.

Bounding sphere​

Version 3 added it. The reader adds 6 * 128 to whatever radius the file supplies, as slack. If the field is absent or zero, the reader falls back to deriving a radius from the AABB. In practice the fallback is unreachable because the exporter always writes an explicit sphere.

Caveats​

  • Skeletons are capped at MAX_BONES (15) bones. A file that declares more is rejected and the load aborts.

Changelog​

  • Version 3 (2026-04-21): added the bounding sphere.
  • Version 2 (2025-10-07): added hasSkeleton, numBones, the bone table and boneForVertex.
  • Version 1: initial format.