Files
Hat Kid e6260e48ab decompiler: support animation export and support master art groups in build-actor tool (#4260)
Adds support for exporting animations for foreground models. It's not
perfect and doesn't handle the Jak 2/3 animations very well in some
cases (scale can often get messed up, especially for the LZO compressed
ones, I have no idea what is going on with the data in those art groups
sometimes, so that'll have to be revisited later...), but it does a
decent job on Jak 1.

Additionally, the `build-actor` tool has also been changed to support
setting the `master-art-group-name` and `master-art-group-index` fields
to allow for custom art groups to link their animations to a different
master art group, which lets you add custom animations to vanilla art
groups.
2026-05-04 17:19:41 +02:00

268 lines
10 KiB
C++

#include "extract_anim.h"
#include "common_formats.h"
#include "decompiler/ObjectFile/LinkedObjectFile.h"
#include "decompiler/util/goal_data_reader.h"
#include "third-party/lzokay/lzokay.hpp"
namespace decompiler {
static std::vector<u8> get_plain_data_bytes_up_to_label(const Ref& ref) {
const auto& words = ref.data->words_by_seg.at(ref.seg);
int start_word = ref.byte_offset / 4;
std::vector<u8> result;
for (int w = start_word; w < (int)words.size(); w++) {
if (words[w].kind() != LinkedWord::PLAIN_DATA)
break;
for (int b = 0; b < 4; b++)
result.push_back(words[w].get_byte(b));
}
return result;
}
static u32 read_u32(const u8* p) {
u32 v;
memcpy(&v, p, 4);
return v;
}
static void parse_fixed_from_buf(const u8* p,
level_tools::JointAnimCompressedFixed& fixed,
u32 fixed_qwc) {
memcpy(fixed.hdr.control_bits, p, sizeof(u32) * 14);
fixed.hdr.num_joints = read_u32(p + 56);
fixed.hdr.matrix_bits = read_u32(p + 60);
fixed.offset_64 = read_u32(p + 64);
fixed.offset_32 = read_u32(p + 68);
fixed.offset_16 = read_u32(p + 72);
fixed.reserved = read_u32(p + 76);
fixed.data64_size = fixed.offset_32 - fixed.offset_64;
fixed.data32_size = fixed.offset_16 - fixed.offset_32;
int data16 = (int)((fixed_qwc - 5) * 16) - (int)fixed.data64_size - (int)fixed.data32_size;
ASSERT(data16 >= 0);
fixed.data16_size = (u16)data16;
fixed.mat[0] = (fixed.hdr.matrix_bits & 1) == 0;
fixed.mat[1] = (fixed.hdr.matrix_bits & 2) == 0;
const u8* data = p + 80;
int d64 = (int)fixed.data64_size;
int d32 = (int)fixed.data32_size;
int d16 = (int)fixed.data16_size;
fixed.data64.resize((d64 + 7) / 8);
if (d64 > 0)
memcpy(fixed.data64.data(), data + fixed.offset_64, d64);
fixed.data32.resize((d32 + 3) / 4);
if (d32 > 0)
memcpy(fixed.data32.data(), data + fixed.offset_32, d32);
fixed.data16.resize((d16 + 1) / 2);
if (d16 > 0)
memcpy(fixed.data16.data(), data + fixed.offset_16, d16);
}
static void parse_frame_from_buf(const u8* p,
level_tools::JointAnimCompressedFrame& frame,
u32 frame_qwc) {
frame.offset_64 = read_u32(p + 0);
frame.offset_32 = read_u32(p + 4);
frame.offset_16 = read_u32(p + 8);
frame.reserved = read_u32(p + 12);
frame.data64_size = frame.offset_32 - frame.offset_64;
frame.data32_size = frame.offset_16 - frame.offset_32;
int data16 = (int)((frame_qwc - 1) * 16) - (int)frame.data64_size - (int)frame.data32_size;
ASSERT(data16 >= 0);
frame.data16_size = (u16)data16;
const u8* data = p + 16;
int fd64 = (int)frame.data64_size;
int fd32 = (int)frame.data32_size;
int fd16 = (int)frame.data16_size;
frame.data64.resize((fd64 + 7) / 8);
if (fd64 > 0)
memcpy(frame.data64.data(), data + frame.offset_64, fd64);
frame.data32.resize((fd32 + 3) / 4);
if (fd32 > 0)
memcpy(frame.data32.data(), data + frame.offset_32, fd32);
frame.data16.resize((fd16 + 1) / 2);
if (fd16 > 0)
memcpy(frame.data16.data(), data + frame.offset_16, fd16);
}
void extract_animations(const ObjectFileData& ag_data,
const DecompilerTypeSystem& dts,
GameVersion version,
std::map<std::string, level_tools::ArtData>& out) {
auto ja_locs = find_objects_with_type(ag_data.linked_data, "art-joint-anim");
if (ja_locs.empty()) {
lg::warn("extract_animations: art group {} has no anims, skipping", ag_data.name_in_dgo);
return;
}
// jak 2/3 split the first word into num-frames + flags
const bool has_flags = version != GameVersion::Jak1;
for (auto loc : ja_locs) {
TypedRef ref(Ref{&ag_data.linked_data, 0, loc * 4}, dts.ts.lookup_type("art-joint-anim"));
auto master_art_name = read_string_field(ref, "master-art-group-name", dts, false);
level_tools::ArtJointAnim ja;
ja.name = read_string_field(ref, "name", dts, false);
ja.speed = read_plain_data_field<float>(ref, "speed", dts);
ja.artist_base = read_plain_data_field<float>(ref, "artist-base", dts);
ja.artist_step = read_plain_data_field<float>(ref, "artist-step", dts);
Ref jacc = deref_label(get_field_ref(ref, "frames", dts));
int jacc_word_off = 0;
u32 first_word = deref_u32(jacc, jacc_word_off++);
ja.frames.num_frames = has_flags ? (first_word & 0xFFFF) : first_word;
ja.frames.fixed_qwc = deref_u32(jacc, jacc_word_off++);
ja.frames.frame_qwc = deref_u32(jacc, jacc_word_off++);
// jak 2/3 may lzo compress the animation, check the flag bit
const bool lzo_compressed = has_flags && ((first_word >> 16) & 1) != 0;
// lg::info("{}: extracting anim {} (compressed {})", ag_data.name_in_dgo, ja.name,
// lzo_compressed);
Ref fixed_ptr = jacc;
fixed_ptr.byte_offset += jacc_word_off * 4;
Ref fixed_ref = deref_label(fixed_ptr);
if (lzo_compressed) {
size_t decompressed_size =
((size_t)ja.frames.fixed_qwc + (size_t)ja.frames.num_frames * ja.frames.frame_qwc) * 16;
auto compressed = get_plain_data_bytes_up_to_label(fixed_ref);
ASSERT(!compressed.empty());
std::vector<u8> decompressed(decompressed_size);
size_t out_size = 0;
auto lzo_result = lzokay::decompress(compressed.data(), compressed.size(),
decompressed.data(), decompressed.size(), out_size);
ASSERT(lzo_result == lzokay::EResult::Success ||
lzo_result == lzokay::EResult::InputNotConsumed);
// if (out_size != decompressed_size) {
// lg::warn("lzo decomp size mismatch for '{}' in '{}': got {} bytes, expected {}", ja.name,
// ag_data.name_in_dgo, out_size, decompressed_size);
// }
parse_fixed_from_buf(decompressed.data(), ja.frames.fixed, ja.frames.fixed_qwc);
size_t frame_base = (size_t)ja.frames.fixed_qwc * 16;
for (int i = 0; i < (int)ja.frames.num_frames; i++) {
auto& frame = ja.frames.frame.emplace_back();
parse_frame_from_buf(
decompressed.data() + frame_base + (size_t)i * ja.frames.frame_qwc * 16, frame,
ja.frames.frame_qwc);
}
} else {
int fixed_word_off = 0;
// fixed hdr
memcpy_from_plain_data((u8*)ja.frames.fixed.hdr.control_bits, fixed_ref, 4 * 14);
fixed_word_off += 14;
ja.frames.fixed.hdr.num_joints = deref_u32(fixed_ref, fixed_word_off++);
ja.frames.fixed.hdr.matrix_bits = deref_u32(fixed_ref, fixed_word_off++);
ja.frames.fixed.offset_64 = deref_u32(fixed_ref, fixed_word_off++);
ja.frames.fixed.offset_32 = deref_u32(fixed_ref, fixed_word_off++);
ja.frames.fixed.offset_16 = deref_u32(fixed_ref, fixed_word_off++);
ja.frames.fixed.reserved = deref_u32(fixed_ref, fixed_word_off++);
ja.frames.fixed.data64_size = ja.frames.fixed.offset_32 - ja.frames.fixed.offset_64;
ja.frames.fixed.data32_size = ja.frames.fixed.offset_16 - ja.frames.fixed.offset_32;
{
int data16 = (int)((ja.frames.fixed_qwc - 5) * 16) - (int)ja.frames.fixed.data64_size -
(int)ja.frames.fixed.data32_size;
ASSERT(data16 >= 0);
ja.frames.fixed.data16_size = (u16)data16;
}
// matrix flags
ja.frames.fixed.mat[0] = (ja.frames.fixed.hdr.matrix_bits & 1) == 0;
ja.frames.fixed.mat[1] = (ja.frames.fixed.hdr.matrix_bits & 2) == 0;
fixed_ref.byte_offset += fixed_word_off * 4;
int d64_bytes = (int)ja.frames.fixed.data64_size;
int d32_bytes = (int)ja.frames.fixed.data32_size;
int d16_bytes = (int)ja.frames.fixed.data16_size;
ja.frames.fixed.data64.resize((d64_bytes + 7) / 8);
if (d64_bytes > 0) {
Ref d64 = fixed_ref;
d64.byte_offset += ja.frames.fixed.offset_64;
memcpy_from_plain_data((u8*)ja.frames.fixed.data64.data(), d64, d64_bytes);
}
ja.frames.fixed.data32.resize((d32_bytes + 3) / 4);
if (d32_bytes > 0) {
Ref d32 = fixed_ref;
d32.byte_offset += ja.frames.fixed.offset_32;
memcpy_from_plain_data((u8*)ja.frames.fixed.data32.data(), d32, d32_bytes);
}
ja.frames.fixed.data16.resize((d16_bytes + 1) / 2);
if (d16_bytes > 0) {
Ref d16 = fixed_ref;
d16.byte_offset += ja.frames.fixed.offset_16;
memcpy_from_plain_data((u8*)ja.frames.fixed.data16.data(), d16, d16_bytes);
}
Ref frames_ref = jacc;
frames_ref.byte_offset += 16;
for (int i = 0; i < (int)ja.frames.num_frames; i++) {
Ref frame_ref = deref_label(frames_ref);
int frame_off = 0;
auto& frame = ja.frames.frame.emplace_back();
frame.offset_64 = deref_u32(frame_ref, frame_off++);
frame.offset_32 = deref_u32(frame_ref, frame_off++);
frame.offset_16 = deref_u32(frame_ref, frame_off++);
frame.reserved = deref_u32(frame_ref, frame_off++);
frame.data64_size = frame.offset_32 - frame.offset_64;
frame.data32_size = frame.offset_16 - frame.offset_32;
{
int data16 = (int)((ja.frames.frame_qwc - 1) * 16) - (int)frame.data64_size -
(int)frame.data32_size;
ASSERT(data16 >= 0);
frame.data16_size = (u16)data16;
}
Ref frame_data = frame_ref;
frame_data.byte_offset += frame_off * 4;
int fd64_bytes = (int)frame.data64_size;
int fd32_bytes = (int)frame.data32_size;
int fd16_bytes = (int)frame.data16_size;
frame.data64.resize((fd64_bytes + 7) / 8);
if (fd64_bytes > 0) {
Ref fd64 = frame_data;
fd64.byte_offset += frame.offset_64;
memcpy_from_plain_data((u8*)frame.data64.data(), fd64, fd64_bytes);
}
frame.data32.resize((fd32_bytes + 3) / 4);
if (fd32_bytes > 0) {
Ref fd32 = frame_data;
fd32.byte_offset += frame.offset_32;
memcpy_from_plain_data((u8*)frame.data32.data(), fd32, fd32_bytes);
}
frame.data16.resize((fd16_bytes + 1) / 2);
if (fd16_bytes > 0) {
Ref fd16 = frame_data;
fd16.byte_offset += frame.offset_16;
memcpy_from_plain_data((u8*)frame.data16.data(), fd16, fd16_bytes);
}
frames_ref.byte_offset += 4;
}
}
// this should catch 99% of cases, but there could be mismatches between
// master art names and model names
out[master_art_name + "-lod0"].anims.push_back(ja);
// out[master_art_name + "-lod1"].anims.push_back(ja);
// out[master_art_name + "-lod2"].anims.push_back(ja);
}
}
} // namespace decompiler