mirror of
https://github.com/open-goal/jak-project
synced 2026-06-23 17:35:19 -04:00
0b799821ed
This adds the blerc data from merc models to the GLB export as shape keys, also including the data from animations. Blender is unfortunately a bit weird about this and only really lets you change what animated weights are used via the NLA track editor by selecting both the corresponding armature animation and then the one for the shape keys. Additionally, due to the way the Blender GLB import works when you have animated weights on a model + animations as actions for an armature, any models that have blerc data get an extra useless empty, but this seems to be harmless. This PR also fixes materials that use texture animations so they now have their base texture in the export, rather than using the default material. Materials now also get named after their base texture for easier recognition.
308 lines
12 KiB
C++
308 lines
12 KiB
C++
#include "extract_anim.h"
|
|
|
|
#include "common_formats.h"
|
|
|
|
#include "decompiler/ObjectFile/LinkedObjectFile.h"
|
|
#include "decompiler/ObjectFile/LinkedWord.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;
|
|
}
|
|
}
|
|
// extract blerc data if present
|
|
const char* blerc_field = version == GameVersion::Jak1 ? "blerc-data" : "blend-shape-anim";
|
|
if (get_word_kind_for_field(ref, blerc_field, dts) == LinkedWord::PTR) {
|
|
Ref blerc_data_ref = deref_label(get_field_ref(ref, blerc_field, dts));
|
|
ja.blend_shape_data = get_plain_data_bytes_up_to_label(blerc_data_ref);
|
|
}
|
|
|
|
// some master art groups and model names do not match, this remaps the model name based on the
|
|
// animation name prefix for these rare exceptions
|
|
PerGameVersion<std::vector<std::string>> mdl_name_remap{
|
|
{},
|
|
{
|
|
"collectables-bomb-blast",
|
|
"collectables-health",
|
|
"collectables-gem",
|
|
"collectables-generic-blast",
|
|
"collectables-generic-ripples",
|
|
"collectables-skill",
|
|
},
|
|
{
|
|
"collectables-bomb-blast",
|
|
"collectables-health",
|
|
"collectables-gem",
|
|
"collectables-generic-blast",
|
|
"collectables-generic-ripples",
|
|
"collectables-skill",
|
|
"collectables-warp-time",
|
|
},
|
|
{},
|
|
};
|
|
auto lst = mdl_name_remap[version];
|
|
auto remap = std::ranges::find_if(lst.begin(), lst.end(), [&](const std::string& prefix) {
|
|
return ja.name.find(prefix) != std::string::npos;
|
|
});
|
|
if (remap != lst.end()) {
|
|
const auto& mdl_prefix = *remap;
|
|
out[mdl_prefix + "-lod0"].anims.push_back(ja);
|
|
// out[mdl_name + "-lod1"].anims.push_back(ja);
|
|
// out[mdl_name + "-lod2"].anims.push_back(ja);
|
|
} else {
|
|
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
|