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