Meshes
A Mesh (class 43) holds the vertices of one or more submeshes. Each submesh is a run of indices into the shared vertices; a renderer’s material i draws submesh i (scenes). The vertex data is either laid out in channels over interleaved streams, or compressed into bit-packed arrays. Observed
Vertex channels
Section titled “Vertex channels”A mesh has up to 14 channels, always in this order of the m_Channels table: Vertex, Normal, Tangent, Color,
TexCoord0 to TexCoord7, BlendWeight, BlendIndices. Sourced · Unity scripting reference
Each channel names the stream it lives in, its offset in a vertex of that stream, a format and a
dimension. A stream’s stride is the sum of its channels’ sizes; the streams follow one another in the vertex data, each
starting on a 16-byte boundary. Observed Every one of SkateBIRD’s 7,491 stored meshes, 45 of them with more than one
stream, ends where its vertex data ends when laid out so. Proven · SkateBIRD · every stored mesh of the player
| Format id | Name | Bytes per component | Confidence |
|---|---|---|---|
| 0 | Float | 4 | Observed |
| 1 | Float16 | 2 | Observed |
| 2 | UNorm8 | 1 | Observed |
| 3 | SNorm8 | 1 | Sourced · Unity scripting reference |
| 4 | UNorm16 | 2 | Observed |
| 5 | SNorm16 | 2 | Observed |
| 6 | UInt8 | 1 | Observed |
| 7 | SInt8 | 1 | Sourced · Unity scripting reference |
| 8 | UInt16 | 2 | Observed |
| 9 | SInt16 | 2 | Sourced · Unity scripting reference |
| 10 | UInt32 | 4 | Observed |
| 11 | SInt32 | 4 | Sourced · Unity scripting reference |
The vertex data is inside the mesh, or streamed: m_StreamData then names a range of the .resS beside the file
(StreamingInfo). Observed · SkateBIRD, Sledding SkateBIRD’s playground
(level12) has 95 meshes, 85 of them readable by scripts (m_IsReadable) and 10 not; the unreadable ones keep their vertex
data in the stream file all the same. Observed · SkateBIRD
Some channels have more components than the data uses. SkateBIRD’s TexCoord0 often has four components (the mesh stores
u, v and two more); the first two are the texture coordinates. Observed · SkateBIRD A normal is three components;
the stored vertex can pad it to four. Observed
Compressed meshes
Section titled “Compressed meshes”A mesh can store its vertices in m_CompressedMesh instead: arrays of PackedBitVectors. A packed vector has n items of
bits bits each, little-endian bit order, and each item decodes to start + item * (range / (2^bits − 1)) in float. Sourced · UnityPy
| Content | Storage | Confidence |
|---|---|---|
| positions | packed floats, 3 per vertex | Observed · Sledding |
| texture coordinates | packed floats; m_UVInfo has a nibble per channel: bit 2 set means present, the low two bits are the dimension minus 1 |
Observed · Sledding |
| normals | x and y packed, z rebuilt from the unit length with a stored sign: z = sqrt(1 − x² − y²), or (x, y, 0) normalised when that is negative |
Observed · Sledding |
| tangents | x and y packed, z as for normals, and a sign for w |
Observed · Sledding |
| colours | packed floats, 4 per vertex | Observed · Sledding |
| indices | packed integers | Observed · Sledding |
Over the meshes of Sledding Game’s level1 read by an independent decoder, the stored channels and index buffers of
552 meshes agree bit for bit, and 74 compressed meshes agree to the quantisation of their packed vectors: the dequantised
values differ only by the f32 against f64 rounding. Observed · Sledding
That rounding is set by the size of the packed vector’s ends, max(|start|, |start + range|), not by the size of the
decoded value: a value near zero carries the absolute error of the vector’s largest end. A tolerance of four f32
epsilons relative to max(1, |value|) refuses 9,552 vertex components near zero; the same tolerance relative to
max(1, ends) accepts all of them. Observed · Sledding A rebuilt normal or tangent z = sqrt(1 − x² − y²)
amplifies that error near z = 0 (the square root of an error e can be as large as sqrt(e)); the largest
difference measured is 3.9 × 10⁻⁴. Observed · Sledding
A compressed mesh can also hold bone weights (m_Weights in the compressed mesh). The readers used here do not decode them.
No compressed mesh found has any: reading every Mesh object of the installs of Hollow Knight (1,007 files), Lethal Company
(43), ULTRAKILL (302) and Risk of Rain 2 (1,799) with an independent reader finds 33 compressed meshes (3 in Lethal Company,
30 in Risk of Rain 2, none in the other two), and every one has an empty m_Weights and m_BoneIndices and no bind pose.
Observed · the four installs Whether weights decode as the other packed vectors do stays open, since none of the 33 is skinned.
Indices and submeshes
Section titled “Indices and submeshes”Indices are 16-bit or 32-bit. A submesh is firstByte (a byte offset into the index buffer), indexCount, topology and
baseVertex; the base vertex is added to every index of the submesh. Observed A decoder that
ignores it reads the wrong vertices for any submesh whose base vertex is not 0. Inferred
| Topology | Id | Notes | Confidence |
|---|---|---|---|
| Triangles | 0 | Observed | |
| Quads | 2 | four indices per quad | Observed |
| Lines | 3 | Observed | |
| Line strip | 4 | one segment per consecutive pair | Observed |
| Points | 5 | Observed |
A submesh without indices draws nothing. A mesh none of whose submeshes has indices has no geometry. Observed
On the GPU
Section titled “On the GPU”Unity uploads a mesh one stream to a buffer: each vertex buffer a draw binds holds exactly one stream’s bytes (vertex count
times stride, without the padding before the next stream), and its index buffer holds the mesh’s whole m_IndexBuffer, every
submesh’s indices as stored. In a title-screen frame of SkateBIRD on Direct3D 11, 183 of the 209 buffers the frame uses hold
those bytes of a stored mesh: 76 first streams, 11 second and 10 third streams (the 11 meshes of more than one stream in the
frame, each stream in its own buffer, none bound with the streams together) and 86 index buffers. The rest match no stored
mesh. The same title screen on Direct3D 12 (-force-d3d12) uploads the same bytes: the same 183 of 209 buffers, by
stream and index buffer alike. Observed · SkateBIRD
Non-finite values
Section titled “Non-finite values”Meshes can store non-finite numbers. A non-finite position leaves a primitive impossible to place; a non-finite normal or
tangent has no direction; a non-finite texture coordinate or colour has no value. Materials carry the same trap: a shader
parameter such as SkateBIRD’s _CameraFadeParams can store infinity as a sentinel. Observed · SkateBIRD
Orientation
Section titled “Orientation”Triangles’ front faces are clockwise in Unity’s left-handed frame. Sourced · Unity scripting reference See the frame for what mirroring it into a right-handed frame does to winding, normals and tangents.
Geometry of the games
Section titled “Geometry of the games”SkateBIRD’s playable-level meshes are ProBuilder output and statically batched combined meshes; there are no LODs, no
terrain and no lightmaps, and the playable extent is 200 to 400 m with about 150,000 triangles per level.
Observed · SkateBIRD Sledding Game and PEAK use LODGroups (scenes).
Observed · Sledding, PEAK
Sledding Game’s level1 has 626 meshes, 74 of them compressed. All use triangle topology and 16-bit indices, every
texture coordinate channel is two-dimensional, and normals are stored as four half floats whose fourth is padding.
One mesh, fireplace curve, has no vertices and a single submesh of zero indices. Observed · Sledding