PXD: Difference between revisions
Started PXD Documentation |
mNo edit summary |
||
(5 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
{{Notice|type=warn|content=This page is unfinished.}} | |||
== PXD Files == | == PXD Files == | ||
Hedgehog Engine 2 games from 2016 Olympics to Shadow Generations contain skeleton and skeletal animation files with the file extensions <code>.skl.pxd</code> and <code>.anm.pxd</code> respectively, stored in a [[BINA]] container. Skeletons contain pose data that usually result in a T-Pose, bone and parent indices, and bone names. Animations contain playback metadata such as frame rate and frame count, track count, and either compressed animation pose data using [https://github.com/nfrechette/acl ACL compression,] or raw uncompressed pose data that use bone and frame indices to specify keyframed transforms, with linear interpolation between keyframes. For both skeletons and animations, position and rotation data for each bone is an absolute transform relative to the parent position, whereas scales are inherited from their respective parents locally, and have no effect on their positions. | Hedgehog Engine 2 games from [[Mario & Sonic at the Rio 2016 Olympic Games|Rio 2016 Olympics]] to [[Shadow Generations]] contain skeleton and skeletal animation files with the file extensions <code>.skl.pxd</code> and <code>.anm.pxd</code> respectively, stored in a [[BINA]] container. Skeletons contain pose data that usually result in a T-Pose, bone and parent indices, and bone names. Animations contain playback metadata such as frame rate and frame count, track count, and either compressed animation pose data using [https://github.com/nfrechette/acl ACL compression,] or raw uncompressed pose data that use bone and frame indices to specify keyframed transforms, with linear interpolation between keyframes. For both skeletons and animations, position and rotation data for each bone is an absolute transform relative to the parent position, whereas scales are inherited from their respective parents locally, and have no effect on their positions. | ||
== <code>.anm.pxd</code> Structure == | == <code>.anm.pxd</code> Structure == | ||
Animation files are stored in a [[BINA]] container. Below is the contents stored within the <code>DATA</code> node beginning at file offset <code>0x40</code>: | Animation files are stored in a [[BINA]] container. Below is the contents stored within the <code>DATA</code> node beginning at file offset <code>0x40</code> identified as <code>PXAN</code>, possibly for "PXD Animation" or similar: | ||
=== Header | === PXAN Header === | ||
<syntaxhighlight lang="c++">struct Header { | <syntaxhighlight lang="c++">struct Header { | ||
char[4] magic; // 'NAXP' | |||
uint32_t version; // Always 0x200 | |||
uint8_t additive; // 0x01 if additive, else 0x00 | |||
uint8_t compressed; // 0x08 if ACL compressed, 0x00 if uncompressed | |||
char[6]; // Null alignment to 8 bytes | |||
uint64_t metadata_offset; // Offset to metadata, always 0x18 | |||
float duration; // Duration of the animation in seconds, calculated as ((frame_count - 1) / FPS) | |||
uint32_t frame_count; | |||
uint32_t track_count; // Bone count | |||
uint64_t skel_anim_offset; // Offset for character's skeletal animation, always 0x40 if compressed, 0x38 if uncompressed | |||
uint64_t root_anim_offset; // Offset for root motion animation, 0x00 if no root motion is present, aligned to 16 bytes | |||
if (compressed == 0x08) | |||
char[8]; // Null alignment to 0x10 bytes for ACL data. | |||
};</syntaxhighlight> | };</syntaxhighlight> | ||
=== ACL Data: === | === ACL Data: === | ||
All Hedgehog Engine 2 games that utilize the ACL library appear to use v2.0.0. [https://github.com/nfrechette/acl/tree/a54c5c2781be9f14b840a60f8dd8ec6c5065885d/docs See the ACL docs for more info.] If <code>compressed == 0x08</code>, skeletal animations and root motion animations are compressed. | All Hedgehog Engine 2 games that utilize the ACL library appear to use v2.0.0, first seen in [[Sonic Origins]], and unchanged in any implementation since then. [https://github.com/nfrechette/acl/tree/a54c5c2781be9f14b840a60f8dd8ec6c5065885d/docs See the ACL docs for more info.] If <code>compressed == 0x08</code>, skeletal animations and root motion animations are compressed. | ||
==== Compressed Data: ==== | ==== Compressed Data: ==== | ||
Below is a high level overview of the stored ACL chunk data:<syntaxhighlight lang="c++">uint32_t acl_chunk_size; | Below is a high level overview of the stored ACL chunk data:<syntaxhighlight lang="c++">struct ACLData { | ||
int32_t acl_hash; | uint32_t acl_chunk_size; | ||
int32_t acl_hash; | |||
uint32_t acl_tag; // Identifies ACL buffer type, always 0xAC11AC11 to mark compressed_tracks | |||
// https://github.com/nfrechette/acl/blob/976ff051048477f2281c7d3609fddf0b3cba2c2d/includes/acl/core/ | // https://github.com/nfrechette/acl/blob/976ff051048477f2281c7d3609fddf0b3cba2c2d/includes/acl/core/buffer_tag.h#L49 | ||
uint16_t acl_version; // ACL Version Enum Identifier. 0x07 indicates v2.0.0, the only version observed in any HE2 game. | |||
// https://github.com/nfrechette/acl/blob/976ff051048477f2281c7d3609fddf0b3cba2c2d/includes/acl/core/compressed_tracks_version.h#L71 | |||
// https://github.com/nfrechette/acl/blob/976ff051048477f2281c7d3609fddf0b3cba2c2d/includes/acl/core/ | |||
uint32_t track_count; | char acl_padding; // always 0x00 | ||
uint32_t sample_count; | uint8_t acl_track_type; // ACL Track Type Enum. 0x0C indicates qvvf, the only track type observed in any HE2 game. | ||
float32 sample_rate; | // https://github.com/nfrechette/acl/blob/976ff051048477f2281c7d3609fddf0b3cba2c2d/includes/acl/core/track_types.h#L68 | ||
char[acl_chunk_size - 0x1C] acl_data; // String of compressed ACL data</syntaxhighlight> | |||
uint32_t track_count; // Bone count if skeletal animation, 0x01 if root motion. Should match header track_count if skeletal. | |||
uint32_t sample_count; // Frame count, should match header frame_count. | |||
float32 sample_rate; // Playback FPS of animation, should equal header ((frame_count - 1) / duration) | |||
char[acl_chunk_size - 0x1C] acl_data; // String of compressed ACL data | |||
};</syntaxhighlight>At the end of an ACL chunk, if it is a skeletal animation chunk and root motion is present in the file, the chunk will be null padded to 16 bytes. If the ACL chunk is a root motion chunk or a skeletal animation with no associated root motion chunk in the file, the chunk will be null padded to 4 bytes. The last ACL chunk is immediately followed by the BINA offset table. | |||
==== Raw/Decompressed Track List: ==== | ==== Raw/Decompressed Track List: ==== | ||
''< | The index lookup for which transform set belongs to which bone can be found in the skeleton file [[PXD#.skl.pxd Structure|(see .skl.pxd Structure)]]. Each bone uses an [https://github.com/nfrechette/rtm RTM] [https://github.com/nfrechette/rtm/blob/7c9a61e32744ee9ff2978328d0e585635fd55615/includes/rtm/types.h#L393 qvvf] struct for transformation. Though the <code>W</code> element of each vector has no bearing on the final animation, the games usually store a value in there anyways and appears to have an effect on compression:<syntaxhighlight lang="c++">struct qvvf | ||
{ | |||
quatf rotation; // XYZW Quaternion | |||
vector4f translation; // XYZW Vector, W is undefined but appears to be bone length | |||
vector4f scale; // XYZW Vector, W is undefined and always 1.0 | |||
};</syntaxhighlight> | |||
=== Native Uncompressed Data: === | |||
{{Notice|type=note|content=This section could use a less confusing explanation.}} | |||
Alternatively to compressed ACL track data, the PXAN structure can store individual components (rotation, translation, scale) of any given track at any frame, and linearly interpolate between them independently. This system was first seen in the [[Mario & Sonic at the Rio 2016 Olympic Games|M&S 2016 Rio Olympic Games]] as the exclusive method of storing animations. ACL compression may have an advantage in reducing the file size of many of these animations but—though rare—uncompressed animations have been observed in subsequent games for certain animations, presumably to combat any potential jitter during close up cutscenes or similar situations. Though the actual end data contained in this method is simple, the structure can end up like a spider's nest at first glance: | |||
==== Track Offset Tables: ==== | |||
The first struct array found in the file is a track table for each bone. Within it, for each transform type (translation, rotation, scale), there is a keyframe count, offset pointing to a frame table, and offset pointing to an array of transforms. <syntaxhighlight lang="c++">struct TrackTableOffsets { | |||
uint64_t pos_key_count; // Total number of translation keyframes for this bone | |||
uint64_t pos_table_offset; // Offset for this bone's translation frame table | |||
uint64_t pos_values_offset; // Starting offset for this bone's array of translation values | |||
uint64_t rot_key_count; // Total number of rotation keyframes for this bone | |||
uint64_t rot_table_offset; // Offset for this bone's rotation frame table | |||
uint64_t rot_values_offset; // Starting offset for this bone's array of rotation values | |||
uint64_t scale_key_count; // Total number of scale keyframes for this bone | |||
uint64_t scale_table_offset; // Offset for this bone's scale frame table | |||
uint64_t scale_values_offset; // Starting offset for this bone's array of scale values | |||
}; | |||
TrackTableOffsets TrackTables[track_count]; // One for each bone, track_count from PXAN header</syntaxhighlight> | |||
==== Frame Tables and Transforms: ==== | |||
Each transform type will get a frame table as <code>uint16_t[key_count] keys;</code> where each value is the frame that the keyframe will be inserted to, and transform array <code>vector4f[key_count] transforms; // XYZW floats</code> that will correspond to the frame with the matching array index. Unlike in ACL decompressed tracks, native uncompressed tracks' <code>W</code> component for translation and scale is always <code>0.0</code>. Both of these element arrays will be null aligned to 16 bytes. | |||
==== Posing and Transform Inheritance: ==== | |||
Each bone's transformation is stored as an object space (AKA 'Pose Space' or 'Model Space') transform relative to its parent bone's final transformation. That means, regardless of what the reset pose or bone orientations are in the model's skeleton, the stored values of any given frame will completely overwrite any pose previously present. | |||
However, there are two quirks that are unlike most interchange formats for 3D animations: | |||
# Scale inheritance is a simple local space copy of the parent's local space scale. There is no shearing or skewing of any kind. | |||
#* <sub>In Blender, a bone's scale inheritance mode can be changed from `Full` to `Aligned` to achieve this effect</sub> | |||
# Calculating the position and rotation of any given bone is to be done independent of any scale values. | |||
When reading in-game, though scales from recursive parents are inherited and multiplied, translation is completely unaffected in any capacity, unintuitively. Therefore, before applying any object space transformation in a setting where parent scale is assumed to affect the child bone's location, each bone's final scale must be calculated and applied by multiplying its and each recursive parent's scales together. | |||
'''Trivia''': ''The inheritance and relationships behavior for bone transform values have been found to be identical to all previous HE games that used Havok, possibly indicating that Sonic Team's custom implementation of animation playback and posing is loosely based on Havok's <code>hka</code> functions.'' | |||
''< | |||
== <code>.skl.pxd</code> Structure == | == <code>.skl.pxd</code> Structure == | ||
Line 53: | Line 97: | ||
''<sub>TODO...</sub>'' | ''<sub>TODO...</sub>'' | ||
== Thanks == | |||
* [https://github.com/Turk645 Turk645:] Original uncompressed PXD blender importer scripts | |||
* [https://github.com/WistfulHopes WistfulHopes:] ACL tools, blender export scripts | |||
* [https://github.com/ik-01 ik-01]: Format and game code research, filling in gaps for unknown values | |||
* [https://github.com/AdelQue AdelQue:] Format research, math relations and bone inheritance behaviors | |||
[[Category:File Formats]] |
Latest revision as of 09:14, 8 April 2025
PXD Files
Hedgehog Engine 2 games from Rio 2016 Olympics to Shadow Generations contain skeleton and skeletal animation files with the file extensions .skl.pxd
and .anm.pxd
respectively, stored in a BINA container. Skeletons contain pose data that usually result in a T-Pose, bone and parent indices, and bone names. Animations contain playback metadata such as frame rate and frame count, track count, and either compressed animation pose data using ACL compression, or raw uncompressed pose data that use bone and frame indices to specify keyframed transforms, with linear interpolation between keyframes. For both skeletons and animations, position and rotation data for each bone is an absolute transform relative to the parent position, whereas scales are inherited from their respective parents locally, and have no effect on their positions.
.anm.pxd
Structure
Animation files are stored in a BINA container. Below is the contents stored within the DATA
node beginning at file offset 0x40
identified as PXAN
, possibly for "PXD Animation" or similar:
PXAN Header
struct Header {
char[4] magic; // 'NAXP'
uint32_t version; // Always 0x200
uint8_t additive; // 0x01 if additive, else 0x00
uint8_t compressed; // 0x08 if ACL compressed, 0x00 if uncompressed
char[6]; // Null alignment to 8 bytes
uint64_t metadata_offset; // Offset to metadata, always 0x18
float duration; // Duration of the animation in seconds, calculated as ((frame_count - 1) / FPS)
uint32_t frame_count;
uint32_t track_count; // Bone count
uint64_t skel_anim_offset; // Offset for character's skeletal animation, always 0x40 if compressed, 0x38 if uncompressed
uint64_t root_anim_offset; // Offset for root motion animation, 0x00 if no root motion is present, aligned to 16 bytes
if (compressed == 0x08)
char[8]; // Null alignment to 0x10 bytes for ACL data.
};
ACL Data:
All Hedgehog Engine 2 games that utilize the ACL library appear to use v2.0.0, first seen in Sonic Origins, and unchanged in any implementation since then. See the ACL docs for more info. If compressed == 0x08
, skeletal animations and root motion animations are compressed.
Compressed Data:
Below is a high level overview of the stored ACL chunk data:
struct ACLData {
uint32_t acl_chunk_size;
int32_t acl_hash;
uint32_t acl_tag; // Identifies ACL buffer type, always 0xAC11AC11 to mark compressed_tracks
// https://github.com/nfrechette/acl/blob/976ff051048477f2281c7d3609fddf0b3cba2c2d/includes/acl/core/buffer_tag.h#L49
uint16_t acl_version; // ACL Version Enum Identifier. 0x07 indicates v2.0.0, the only version observed in any HE2 game.
// https://github.com/nfrechette/acl/blob/976ff051048477f2281c7d3609fddf0b3cba2c2d/includes/acl/core/compressed_tracks_version.h#L71
char acl_padding; // always 0x00
uint8_t acl_track_type; // ACL Track Type Enum. 0x0C indicates qvvf, the only track type observed in any HE2 game.
// https://github.com/nfrechette/acl/blob/976ff051048477f2281c7d3609fddf0b3cba2c2d/includes/acl/core/track_types.h#L68
uint32_t track_count; // Bone count if skeletal animation, 0x01 if root motion. Should match header track_count if skeletal.
uint32_t sample_count; // Frame count, should match header frame_count.
float32 sample_rate; // Playback FPS of animation, should equal header ((frame_count - 1) / duration)
char[acl_chunk_size - 0x1C] acl_data; // String of compressed ACL data
};
At the end of an ACL chunk, if it is a skeletal animation chunk and root motion is present in the file, the chunk will be null padded to 16 bytes. If the ACL chunk is a root motion chunk or a skeletal animation with no associated root motion chunk in the file, the chunk will be null padded to 4 bytes. The last ACL chunk is immediately followed by the BINA offset table.
Raw/Decompressed Track List:
The index lookup for which transform set belongs to which bone can be found in the skeleton file (see .skl.pxd Structure). Each bone uses an RTM qvvf struct for transformation. Though the W
element of each vector has no bearing on the final animation, the games usually store a value in there anyways and appears to have an effect on compression:
struct qvvf
{
quatf rotation; // XYZW Quaternion
vector4f translation; // XYZW Vector, W is undefined but appears to be bone length
vector4f scale; // XYZW Vector, W is undefined and always 1.0
};
Native Uncompressed Data:
Alternatively to compressed ACL track data, the PXAN structure can store individual components (rotation, translation, scale) of any given track at any frame, and linearly interpolate between them independently. This system was first seen in the M&S 2016 Rio Olympic Games as the exclusive method of storing animations. ACL compression may have an advantage in reducing the file size of many of these animations but—though rare—uncompressed animations have been observed in subsequent games for certain animations, presumably to combat any potential jitter during close up cutscenes or similar situations. Though the actual end data contained in this method is simple, the structure can end up like a spider's nest at first glance:
Track Offset Tables:
The first struct array found in the file is a track table for each bone. Within it, for each transform type (translation, rotation, scale), there is a keyframe count, offset pointing to a frame table, and offset pointing to an array of transforms.
struct TrackTableOffsets {
uint64_t pos_key_count; // Total number of translation keyframes for this bone
uint64_t pos_table_offset; // Offset for this bone's translation frame table
uint64_t pos_values_offset; // Starting offset for this bone's array of translation values
uint64_t rot_key_count; // Total number of rotation keyframes for this bone
uint64_t rot_table_offset; // Offset for this bone's rotation frame table
uint64_t rot_values_offset; // Starting offset for this bone's array of rotation values
uint64_t scale_key_count; // Total number of scale keyframes for this bone
uint64_t scale_table_offset; // Offset for this bone's scale frame table
uint64_t scale_values_offset; // Starting offset for this bone's array of scale values
};
TrackTableOffsets TrackTables[track_count]; // One for each bone, track_count from PXAN header
Frame Tables and Transforms:
Each transform type will get a frame table as uint16_t[key_count] keys;
where each value is the frame that the keyframe will be inserted to, and transform array vector4f[key_count] transforms; // XYZW floats
that will correspond to the frame with the matching array index. Unlike in ACL decompressed tracks, native uncompressed tracks' W
component for translation and scale is always 0.0
. Both of these element arrays will be null aligned to 16 bytes.
Posing and Transform Inheritance:
Each bone's transformation is stored as an object space (AKA 'Pose Space' or 'Model Space') transform relative to its parent bone's final transformation. That means, regardless of what the reset pose or bone orientations are in the model's skeleton, the stored values of any given frame will completely overwrite any pose previously present.
However, there are two quirks that are unlike most interchange formats for 3D animations:
- Scale inheritance is a simple local space copy of the parent's local space scale. There is no shearing or skewing of any kind.
- In Blender, a bone's scale inheritance mode can be changed from `Full` to `Aligned` to achieve this effect
- Calculating the position and rotation of any given bone is to be done independent of any scale values.
When reading in-game, though scales from recursive parents are inherited and multiplied, translation is completely unaffected in any capacity, unintuitively. Therefore, before applying any object space transformation in a setting where parent scale is assumed to affect the child bone's location, each bone's final scale must be calculated and applied by multiplying its and each recursive parent's scales together.
Trivia: The inheritance and relationships behavior for bone transform values have been found to be identical to all previous HE games that used Havok, possibly indicating that Sonic Team's custom implementation of animation playback and posing is loosely based on Havok's hka
functions.
.skl.pxd
Structure
Skeleton files are stored in a BINA container. Below is the contents stored within the DATA
node beginning at file offset 0x40
:
TODO...
Thanks
- Turk645: Original uncompressed PXD blender importer scripts
- WistfulHopes: ACL tools, blender export scripts
- ik-01: Format and game code research, filling in gaps for unknown values
- AdelQue: Format research, math relations and bone inheritance behaviors