Jump to content

PXD

From HedgeDocs
Revision as of 09:14, 8 April 2025 by AdelQue (talk | contribs)
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.