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

178 lines
6.1 KiB
C++

#include "build_actor.h"
#include "common/log/log.h"
#include "common/math/geometry.h"
#include "goalc/build_actor/common/MercExtract.h"
#include "goalc/build_actor/common/animation_processing.h"
#include "third-party/tiny_gltf/tiny_gltf.h"
using namespace gltf_util;
std::map<int, size_t> g_joint_map;
size_t Joint::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("joint");
size_t result = gen.current_offset_bytes();
gen.add_ref_to_string_in_pool(name);
gen.add_word(number);
if (parent == -1) {
gen.add_symbol_link("#f");
} else {
gen.link_word_to_byte(gen.add_word(0), g_joint_map[parent]);
}
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
gen.add_word_float(bind_pose(i, j));
}
}
return result;
}
/*!
* Load tinygltf::Model from a .glb file (binary format), fatal error if it fails.
*/
tinygltf::Model load_gltf_model(const fs::path& path) {
tinygltf::TinyGLTF loader;
tinygltf::Model model;
std::string err, warn;
bool res = loader.LoadBinaryFromFile(&model, &err, &warn, path.string());
ASSERT_MSG(warn.empty(), warn.c_str());
ASSERT_MSG(err.empty(), err.c_str());
ASSERT_MSG(res, "Failed to load GLTF file!");
return model;
}
/*!
* Extract the "skeleton" structure from a GLTF model's skin. This requires that the skin's joints
* are topologically sorted (parents always have lower index than children).
*/
std::vector<GltfJoint> extract_skeleton(const tinygltf::Model& model, int skin_idx) {
const auto& skin = model.skins.at(skin_idx);
lg::info("skin name is {}", skin.name);
lg::info("skeleton root is {}", skin.skeleton);
auto inverse_bind_matrices = extract_mat4(model, skin.inverseBindMatrices);
ASSERT(inverse_bind_matrices.size() == skin.joints.size());
std::map<int, int> node_to_joint;
std::map<int, int> joint_to_node;
std::vector<GltfJoint> joints;
for (size_t i = 0; i < skin.joints.size(); i++) {
auto joint_node_idx = skin.joints[i];
const auto& joint_node = model.nodes.at(joint_node_idx);
// auto ibm = inverse_bind_matrices[i];
// lg::info(" joint {}", joint_node_idx);
// lg::info(" {}", joint_node.name);
// lg::info("\n{}", ibm.to_string_aligned());
node_to_joint[joint_node_idx] = i;
joint_to_node[i] = joint_node_idx;
auto& gjoint = joints.emplace_back();
gjoint.bind_pose_T_w = inverse_bind_matrices[i];
gjoint.name = joint_node.name;
gjoint.gltf_node_index = joint_node_idx;
}
for (size_t i = 0; i < skin.joints.size(); i++) {
auto joint_node_idx = skin.joints[i];
const auto& joint_node = model.nodes.at(joint_node_idx);
// set up children
for (int child_node_idx : joint_node.children) {
int child_joint_idx = node_to_joint.at(child_node_idx);
joints.at(i).children.push_back(child_joint_idx);
auto& child = joints.at(child_joint_idx);
ASSERT(child.parent == -1);
child.parent = i;
ASSERT(child_joint_idx > (int)i);
}
}
ASSERT(joints.at(0).parent == -1);
// for (auto& joint : joints) {
// if (joint.parent == -1) {
// lg::warn("parentless {}", joint.name);
// } else {
// lg::info("joint {}, child of {}", joint.name, joints.at(joint.parent).name);
// }
// }
lg::info("total of {} joints", joints.size());
return joints;
}
/*!
* Convert from GLTF joint format to game joint format.
* @param joint_index the index of the joint, in the GLTF file.
* @param prefix_joint_count number of joints to be inserted before GLTF joints in the game
* @param parent_of_gltf the parent game joint of all GLTF joints.
*/
Joint convert_joint(const GltfJoint& joint,
int joint_index,
int prefix_joint_count,
int parent_of_gltf) {
// node matrix is p_T_myself
// p_T_myself = parent_bind_pose_T_w * w_T_bind_pose
int parent;
if (joint.parent == -1) {
parent = parent_of_gltf;
} else {
parent = joint.parent + prefix_joint_count;
}
math::Matrix4f fixed_matrix = joint.bind_pose_T_w;
for (int i = 0; i < 3; i++) {
fixed_matrix(i, 3) *= 4096;
}
return Joint(joint.name, joint_index + prefix_joint_count, parent, fixed_matrix.transposed());
}
constexpr int kGltfToGameJointOffset = 1;
/*!
* Convert GTLF joint list to game joint list.
* Currently, this inserts a single "align" joint and places the root joint of the GLTF as the
* prejoint. However, we might want to change this, to allow GLTF files to specify "align" at some
* point.
*/
std::vector<Joint> convert_joints(const std::vector<GltfJoint>& gjoints) {
std::vector<Joint> joints;
joints.emplace_back("align", 0, -1, math::Matrix4f::identity());
ASSERT(kGltfToGameJointOffset == joints.size());
for (int gjoint_idx = 0; gjoint_idx < int(gjoints.size()); gjoint_idx++) {
// using -1 as the parent index since gltf's shouldn't be child of align.
joints.push_back(convert_joint(gjoints[gjoint_idx], gjoint_idx, kGltfToGameJointOffset, -1));
}
return joints;
}
std::vector<anim::CompressedAnim> process_anim(const tinygltf::Model& model,
const std::vector<GltfJoint>& gjoints,
const std::string& master_art_group,
const std::map<std::string, int>& master_ag_map,
float framerate) {
if (model.animations.empty()) {
lg::warn("no animations detected!"); // TODO: make up a dummy one
return {};
}
std::map<int, int> node_to_joint;
for (size_t i = 0; i < gjoints.size(); i++) {
node_to_joint[gjoints[i].gltf_node_index] = i + kGltfToGameJointOffset;
}
std::vector<anim::CompressedAnim> ret;
for (auto& anim : model.animations) {
lg::info("Processing animation {}", anim.name);
int master_ag_idx = -1;
if (!master_ag_map.empty() && master_ag_map.find(anim.name) != master_ag_map.end()) {
master_ag_idx = master_ag_map.at(anim.name);
}
ret.push_back(anim::compress_animation(anim::extract_anim_from_gltf(
model, anim, node_to_joint, master_art_group, master_ag_idx, framerate)));
}
return ret;
}