SKL Skeleton Format
A .skl file is the bone tree of a character: every bone with its rest pose and its parent. Meshes (.msh) name the bones they are skinned to, and animations (.ani) move those bones by name. The skeleton is what ties the two together.
This page follows the client code in io3DEngine (ioSkeletonManager::LoadFile, ioRcSkeleton, ioRcBiped, ioBiped, ioSkeleton) and the web tools’ reader.
The general conventions (little-endian, int32 length strings, 4-byte token) are in MSH Mesh Format.
Layout
Section titled “Layout”| Part | Type | Field |
|---|---|---|
| Header | char[4] | SKL\0 |
| int32 | version (1000 in the sources) |
|
| int32 | bone count | |
| Bones | per bone | see below |
| Tail | string | linked skeleton name (optional) |
One bone
Section titled “One bone”| Type | Size | Field |
|---|---|---|
| string | 4 + n | bone name |
| float3 | 12 | local position (relative to the parent) |
| float4 | 16 | local rotation (quaternion x, y, z, w) |
| float3 | 12 | inverse object position |
| float4 | 16 | inverse object rotation |
| float[16] | 64 | object matrix (4 × 4, row by row) |
| string | 4 + n | parent bone name (length 0 for the root) |
| int32 | 4 | child count |
| string × count | child bone names |
So a bone is 120 bytes of numbers plus its names.
The inverse position and rotation together are the “bone space” matrix. The client builds it as a rotation matrix from the quaternion and puts the position into the last row (_41, _42, _43). This is the inverse bind pose. For skinning, the client multiplies it with the bone’s current world transform every frame (ioBiped: bone space × current transform), so a vertex skinned to the bone follows the bone from its rest position.
The object matrix (the bone’s rest transform in model space) is only used when the skeleton has a linked skeleton (next section). A skeleton without a link reads it and never uses it.
Tail: linked skeleton
Section titled “Tail: linked skeleton”If bytes are left after the last bone, they hold one more string: the name of a linked skeleton. The client uses it when its length is above 0. A file that ends right after the last bone is valid.
With a link, a skeleton of other proportions can play animations made for the linked skeleton (ioRcSkeleton::SetLinkedSkeleton):
- The client loads the linked
.sklby that name. If it is missing, it logs “Not Exist” and the link is dropped. - Both skeletons must have the same bone count, otherwise it logs “Not Same Biped Cnt” and drops the link. Bones are paired by index (bone 0 with bone 0, and so on), not by name, so the bone order must match.
- For each pair the client computes an offset: inverse of the linked bone’s object matrix × this bone’s object matrix, and a length rate: this bone’s local position length ÷ the linked bone’s (
ioRcBiped::MakeOffsetTM). - At run time the offset is applied to every bone each frame (
ApplyObjectOffsetTM), and the bone space matrix becomes offset × bone space. Effect planes attached to such a skeleton get the same offset and rate (ioAnimateFX).
Rules to keep
Section titled “Rules to keep”- Bone names are the keys. A name such as
Bip01 Spinemust be spelled the same in the mesh, the animation and the skeleton. The client compares them case-sensitively (ioSkeleton::GetBipedlooks them up asioHashString, which compares withstrcmp; same in all three sources), soBip01 Spineandbip01 spineare different bones in game. The web tools lower-case names when they match, so a case mismatch that the tools accept can still fail in the client. - Parent and child lists must agree. The client reads both and does not check them against each other.
- The bone count is limited to 4 096 by the web tools’ reader. The loader in the client has no such check.
- Meshes store bone names, not skeleton indices, so the order of bones in the file is not what ties a mesh to the skeleton.
Differences between the sources
Section titled “Differences between the sources”| Part | 2014 | 2019 | 2021 |
|---|---|---|---|
| Layout and fields | same | same | same |
| Token check | compares the string SKL\0 |
compares a 4-byte constant kept in a protected data segment | same as 2019 |
The check in 2019 and 2021 was changed for anti-cheat reasons. The bytes it accepts are the same SKL\0, so a skeleton file is valid in all three clients. ioSkeleton.cpp is identical in the three sources.
| Tool | Use |
|---|---|
| Models | Reads .skl to draw bones and to play animations |
| Anim Studio | Uses the skeleton as the character; poses bones. It does not write a .skl |
| Heroes, Costumes | Pick the skeleton a hero or gear piece uses (for example the shared human skeleton) |
The web tools can read a skeleton, but no tool writes .skl files yet. To change a skeleton you need an outside editor.
Other facts from the code
Section titled “Other facts from the code”- The version is a local value in the loader: it is read and never used, here or anywhere else. Any number works.
- Every bone is handled the same way, whether it is part of the biped or a prop or effect bone: the client has no bone types, only names.
ioSkeleton.cpp,ioRcSkeleton.cpp,ioBiped.cppandioRcBiped.cppare byte-identical in the three sources, so skinning and linking behave the same in all three clients.
