CANM Format - KCreator/Earth-Defence-Force-Documentation GitHub Wiki
All offsets are relative to the beginning of their table entries.
Since the header is always at the beginning of the file, the offsets there are also absolute file offsets.
File format largely derived from EDF5, EDF4.1 might have unknown differences.
Header: 32 (0x20) bytes
| Offset | Size | Type | Description |
|---|---|---|---|
| 0x00 | 4 | char[4] | Magic, always CANM |
| 0x04 | 4 | uint32 | Version: 0x200 for EDF5, 0x300 for EDF6 |
| 0x08 | 4 | uint32 | Animation-data count |
| 0x0C | 4 | uint32 | Animation-data table offset |
| 0x10 | 4 | uint32 | Animation-channel count |
| 0x14 | 4 | uint32 | Animation-channel table offset |
| 0x18 | 4 | uint32 | Bone-name count |
| 0x1C | 4 | uint32 | Bone-name table offset |
Animation data: 28 (0x1C) bytes
| Offset | Size | Type | Description |
|---|---|---|---|
| 0x00 | 4 | uint32 | Loop flag (1 = loop; observed values are 0 and 1) |
| 0x04 | 4 | int32 | Animation-name UTF-16LE string offset |
| 0x08 | 4 | float32 | Clip lifetime/completion time in engine time units; normally (sample count - 1) * sample interval |
| 0x0C | 4 | float32 | Sample interval in engine time units (not a playback-speed multiplier); normally duration / (sample count - 1), or the full duration when there is only one sample |
| 0x10 | 4 | uint32 | Playback sample count; the sampler trusts this as its available sample range |
| 0x14 | 4 | uint32 | Bone-data count |
| 0x18 | 4 | int32 | Bone-data table offset |
Playback timing and count semantics:
The examined EDF6 code uses the three fields independently; it does not derive one from the others. Given the current clip time t, interval dt at +0x0C, and declared sample count N at +0x10, the sampler first calculates:
samplePosition = t / dt
sampleIndex = floor(samplePosition)
blend = fractionalPart(samplePosition)
The integer part selects the current sample and blend interpolates toward the next sample. Looping wraps the sample selection over N - 1; non-looping playback clamps it to the final sample pair. Consequently, +0x0C controls the rate at which stored samples are traversed: a larger value makes the animation visibly slower, and a smaller value makes it faster. It is therefore better described as sample interval than as “speed.”
+0x08 is the clip lifetime used by higher-level playback/completion logic; it does not rescale the above sample calculation. Thus it is possible for the pose samples to finish before the clip lifetime or still be progressing when the clip ends. With a loop flag set, a clip whose lifetime exceeds (N - 1) * dt can replay the sample sequence until its +0x08 lifetime expires. Without looping, the pose remains clamped at the last sample once the sample sequence has been exhausted, subject to whatever the calling controller does when the clip lifetime ends.
Normal files make the two timelines coincide:
duration (+0x08) = (sample count (+0x10) - 1) * sample interval (+0x0C)
All examined EDF5 and EDF6 files satisfy this relationship. It is a serialization convention for coherent playback, not a loader validation rule.
+0x10 is a trusted playback bound, not a checked description of the bytes actually present in every channel. Reducing it causes the sampler to stop/clamp or wrap at the earlier declared bound even if channel keyframe data contains more samples. Increasing it permits indices beyond the real channel data; the examined EDF6 sampler does not reject that mismatch before using the index to read sample data. The resulting reads can decode adjacent data as transforms, producing distorted poses rather than necessarily crashing. This should be avoided at all costs.
Bone data: 8 (0x08) bytes
| Offset | Size | Type | Description |
|---|---|---|---|
| 0x00 | 2 | uint16 | Bone-name-table index |
| 0x02 | 2 | uint16 | Position/translation animation-channel index (-1 for none) |
| 0x04 | 2 | uint16 | Rotation animation-channel index (-1 for none) (Radians are encoded) |
| 0x06 | 2 | uint16 | Scale animation-channel index (-1 for none) |
Note that the -1 for none is easier for us to process. In reality this is "unsigned" to 0xFFFF is the "reserved none channel"
The runtime bone-mapping cache used by the examined EDF6 code stores the matched bone index in one byte, so practical skeletons appear limited to 255 mapped bones even though the file field is 16 bits.
EDF5 animation channel: 32 (0x20) bytes
| Offset | Size | Type | Description |
|---|---|---|---|
| 0x00 | 2 | uint16 | Has-keyframes flag (1 = animated, 0 = static) |
| 0x02 | 2 | uint16 | Channel keyframe/sample count |
| 0x04 | 4 | float32 | Base X |
| 0x08 | 4 | float32 | Base Y |
| 0x0C | 4 | float32 | Base Z |
| 0x10 | 4 | float32 | Quantization multiplier X |
| 0x14 | 4 | float32 | Quantization multiplier Y |
| 0x18 | 4 | float32 | Quantization multiplier Z |
| 0x1C | 4 | int32 | Compressed keyframe-data offset (0 for a static channel) |
EDF5 compressed keyframe data: 6 (0x06) bytes per sample
| Offset | Size | Type | Description |
|---|---|---|---|
| 0x00 | 2 | uint16 | Quantized X |
| 0x02 | 2 | uint16 | Quantized Y |
| 0x04 | 2 | uint16 | Quantized Z |
After all EDF5 keyframe blocks, zero padding aligns the following section to a 4-byte absolute file offset.
EDF6 animation channel: 48 (0x30) bytes
| Offset | Size | Type | Description |
|---|---|---|---|
| 0x00 | 4 | float32 | Base X |
| 0x04 | 4 | float32 | Base Y |
| 0x08 | 4 | float32 | Base Z |
| 0x0C | 4 | float32 | Base W |
| 0x10 | 4 | float32 | Quantization multiplier X |
| 0x14 | 4 | float32 | Quantization multiplier Y |
| 0x18 | 4 | float32 | Quantization multiplier Z |
| 0x1C | 4 | float32 | Quantization multiplier W |
| 0x20 | 4 | int32 | Keyframe-data offset (0 for a static channel) |
| 0x24 | 4 | int32 | Channel encoding type; see below |
| 0x28 | 4 | int32 | Channel keyframe/sample count |
| 0x2C | 4 | int32 | Reserved; written as zero |
Known EDF6 channel encoding types:
| Type | Meaning | Keyframe encoding |
|---|---|---|
| 0 | Static vector | No keyframe block; XYZ come from the base values |
| 1 | Animated vector | Quantized XYZ records, 6 bytes per sample |
| 2 | Static quaternion | No keyframe block; XYZW come from the base values |
| 3 | Animated quaternion | Absolute float32 XYZW records, 16 bytes per sample |
EDF6 compressed vector keyframe data: 6 (0x06) bytes per sample
| Offset | Size | Type | Description |
|---|---|---|---|
| 0x00 | 2 | uint16 | Quantized X |
| 0x02 | 2 | uint16 | Quantized Y |
| 0x04 | 2 | uint16 | Quantized Z |
EDF6 quaternion keyframe data: 16 (0x10) bytes per sample
| Offset | Size | Type | Description |
|---|---|---|---|
| 0x00 | 4 | float32 | Quaternion X |
| 0x04 | 4 | float32 | Quaternion Y |
| 0x08 | 4 | float32 | Quaternion Z |
| 0x0C | 4 | float32 | Quaternion W |
Bone-name table: 4 (0x04) bytes
| Offset | Size | Type | Description |
|---|---|---|---|
| 0x00 | 4 | int32 | Bone-name UTF-16LE string offset |
Each string offset is relative to its own bone-name-table entry.
Strings: Variable length
| Offset | Size | Type | Description |
|---|---|---|---|
| 0x00 | Variable | UTF-16LE[] | Null-terminated bone and animation names referenced by the tables above |
Note that at the end of the file, all the strings are appended. With the exception of "Scene_Root" as the first entry, all these strings are sorted in alphabetical order. Bones and Animation names, all on a pile. It seems however that the EDF6 loader code does not care for this ordering, and it is purely cosmetic.
Quantized-channel decoding:
For EDF5 channels and EDF6 type-1 vector channels, each component is reconstructed independently:
X = BaseX + Keyframe[i].X * MultiplierX
Y = BaseY + Keyframe[i].Y * MultiplierY
Z = BaseZ + Keyframe[i].Z * MultiplierZ
At times between stored samples, the EDF6 runtime linearly interpolates the two quantized values before applying the multiplier and base. This is mathematically equivalent, aside from floating-point rounding, to interpolating the two decoded values.
EDF5 rotations use the same formula and produce Euler angles in radians. EDF6 animated rotations instead store absolute XYZW quaternions and do not use the base-plus-multiplier formula.
To quantize a vector channel:
Min = minimum value of all samples in this component
Max = maximum value of all samples in this component
Difference = Max - Min
Base = Min
Multiplier = Difference / 65535
Quantized[i] = (Sample[i] - Min) * 65535 / Difference
Transform composition:
For EDF5 Euler rotations, the known importer constructs rotation as:
Rotation = RotZ @ RotY @ RotX
For both versions, the local CANM transform is composed as:
LocalTransform = Translation @ Rotation @ Scale
GlobalTransform = ParentGlobalTransform @ LocalTransform
Additive animations:
No additive flag, blend weight, base-animation index, or base-animation name exists in the CANM header or animation-data record.
The examined game code looks up controller/state entries named additive_action, and game configuration also contains names such as RandomAdditiveAnimation. This places additive selection and blending in CAS/controller or higher-level character logic rather than in the CANM record itself. Names ending in _add or _poseAim_add are therefore conventions, not format metadata.
As a result, without full CAS context understanding, the additive animations cannot be properly displayed or edited.
Credits:
AUK - A lot of the ground work
stafern - Making the first model viewer that had working Matrices to base further work on
Smileynator - Formatting and verification
ktaro - additional research and animation tooling