Jump to content

PXD: Difference between revisions

From HedgeDocs
AdelQue (talk | contribs)
Started PXD Documentation
 
AdelQue (talk | contribs)
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'
char[4] magic; // 'NAXP'
    uint32_t version;   // Always 0x200
uint32_t version; // Always 0x200
    uint8_t additive;     // 0x01 if additive, else 0x00
uint8_t additive; // 0x01 if additive, else 0x00
    uint8_t compressed;   // 0x08 if ACL compressed, 0x00 if uncompressed
uint8_t compressed; // 0x08 if ACL compressed, 0x00 if uncompressed
    char[6];           // 0x00 align to 8 bytes
char[6]; // Null alignment to 8 bytes
    uint64_t metadata_offset;   // Offset to metadata, always 0x18
uint64_t metadata_offset; // Offset to metadata, always 0x18
    float32 duration;         // Duration of the animation in seconds, calculated as ((frame_count-1) / FPS)
float duration; // Duration of the animation in seconds, calculated as ((frame_count - 1) / FPS)
    uint32_t frame_count;   // Frame count
uint32_t frame_count;
    uint32_t track_count;   // Bone 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 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
uint64_t root_anim_offset; // Offset for root motion animation, 0x00 if no root motion is present, aligned to 16 bytes
    if (compressed)
if (compressed == 0x08)
        char[8];   // Likely alignment to 16 bytes for ACL data. Always 0x00 if compressed
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; // Total size of chunk
Below is a high level overview of the stored ACL chunk data:<syntaxhighlight lang="c++">struct ACLData {
int32_t acl_hash;   // Hash
uint32_t acl_chunk_size;
uint32_t acl_tag;  // Identifies ACL buffer type, always 0xAC11AC11 to mark compressed_tracks
int32_t acl_hash;
// 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 Hedgehog Engine 2 game.
uint32_t acl_tag; // Identifies ACL buffer type, always 0xAC11AC11 to mark compressed_tracks
// 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/buffer_tag.h#L49


char acl_padding;   // always 0x00
uint16_t acl_version; // ACL Version Enum Identifier. 0x07 indicates v2.0.0, the only version observed in any HE2 game.
uint8_t acl_track_type;    // ACL Track Type Enum. 0x0C indicates qvvf, the only track type observed in any Hedgehog Engine 2 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/track_types.h#L68


uint32_t track_count;   // Bone count if skeletal animation, 0x01 if root motion. Should match header track_count if skeletal.  
char acl_padding; // always 0x00
uint32_t sample_count;   // Frame count, should match header frame_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;   // Playback FPS of animation, should equal header ((frame_count - 1) / duration)
// 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: ====
''<sub>TODO...</sub>''
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.


=== Uncompressed Animation Data: ===
'''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.''
''<sub>TODO...</sub>''


== <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

Warning
This page is unfinished.


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:

Note
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 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:

  1. 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
  2. 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
Cookies help us deliver our services. By using our services, you agree to our use of cookies.