From f2e7606f1b0aeb46c01b4545d575fae6a442cc72 Mon Sep 17 00:00:00 2001 From: water111 <48171810+water111@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:25:53 -0400 Subject: [PATCH] [build_actor] Add skeleton and animation support (#3638) This adds a feature to `build_actor` to support importing skeletons and animations from .glb files. Multiple animations are handled and will use the name in the GLB. The default `viewer` process will end up playing back the first animation. There are a few limitations: - You can only have around 100 bones. It is technically possibly to have slightly more, but certain animations may fail to compress when there are more than ~100 bones. - Currently, all animations have 60 keyframes per second. This is a higher quality than what is normally used. If animation size becomes problematic, we could make this customizable somehow. - There is no support for the `align` bone. --------- Co-authored-by: water111 --- common/math/Vector.h | 11 + common/util/gltf_util.cpp | 174 ++++++- common/util/gltf_util.h | 59 +++ .../level_extractor/merc_replacement.cpp | 4 +- goalc/CMakeLists.txt | 1 + goalc/build_actor/common/MercExtract.cpp | 41 +- goalc/build_actor/common/MercExtract.h | 2 +- .../common/animation_processing.cpp | 281 +++++++++++ .../build_actor/common/animation_processing.h | 69 +++ goalc/build_actor/jak1/build_actor.cpp | 445 +++++++++++++++--- goalc/build_actor/jak1/build_actor.h | 65 +-- goalc/build_actor/main.cpp | 2 +- goalc/build_level/common/build_level.cpp | 1 + 13 files changed, 1042 insertions(+), 113 deletions(-) create mode 100644 goalc/build_actor/common/animation_processing.cpp create mode 100644 goalc/build_actor/common/animation_processing.h diff --git a/common/math/Vector.h b/common/math/Vector.h index 6c8622722f..e185522163 100644 --- a/common/math/Vector.h +++ b/common/math/Vector.h @@ -369,6 +369,17 @@ struct Matrix { return result; } + Matrix transposed() const { + static_assert(Rows == Cols); + Matrix ret; + for (int i = 0; i < Rows; i++) { + for (int j = 0; j < Cols; j++) { + ret(i, j) = operator()(j, i); + } + } + return ret; + } + template Matrix operator*(const Matrix& y) const { Matrix result; diff --git a/common/util/gltf_util.cpp b/common/util/gltf_util.cpp index 984c0a6071..4ec71e4e70 100644 --- a/common/util/gltf_util.cpp +++ b/common/util/gltf_util.cpp @@ -44,6 +44,20 @@ std::vector> extract_color_from_vec4_float(const u8* data, return result; } +std::vector> extract_color_from_vec3_float(const u8* data, + u32 count, + u32 stride) { + std::vector> result; + result.reserve(count); + for (u32 i = 0; i < count; i++) { + math::Vector temp; + memcpy(&temp, data, sizeof(math::Vector)); + data += stride; + result.emplace_back(temp.x() * 255, temp.y() * 255, temp.z() * 255, 255); + } + return result; +} + /*! * Convert a GLTF color buffer (u16 format) to u8 colors. */ @@ -61,6 +75,16 @@ std::vector> extract_color_from_vec4_u16(const u8* data, return result; } +std::vector> extract_color_from_vec4_u8(const u8* data, u32 count, u32 stride) { + std::vector> result; + result.reserve(count); + for (u32 i = 0; i < count; i++) { + result.push_back(math::Vector(data[0], data[1], data[2], data[3])); + data += stride; + } + return result; +} + /*! * Convert a GLTF index buffer */ @@ -92,6 +116,74 @@ std::vector gltf_index_buffer(const tinygltf::Model& model, } } +std::vector extract_mat4(const tinygltf::Model& model, int accessor_idx) { + const auto& accessor = model.accessors[accessor_idx]; + const auto& buffer_view = model.bufferViews[accessor.bufferView]; + const auto& buffer = model.buffers[buffer_view.buffer]; + const u8* data_ptr = buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const auto stride = accessor.ByteStride(buffer_view); + const auto count = accessor.count; + + // ASSERT(buffer_view.target == TINYGLTF_TARGET_ARRAY_BUFFER); // ?? + ASSERT(accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT); + ASSERT(accessor.type == TINYGLTF_TYPE_MAT4); + + std::vector result(accessor.count); + for (size_t x = 0; x < count; x++) { + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + memcpy(&result[x](j, i), data_ptr + sizeof(float) * (i * 4 + j), sizeof(float)); + } + } + data_ptr += stride; + } + return result; +} + +JointsAndWeights convert_per_vertex_data(const math::Vector4f& weights, + const math::Vector& joints) { + int discard_idx = -1; + float discard_weight = 100; + for (int i = 0; i < 4; i++) { + if (weights[i] < discard_weight) { + discard_idx = i; + discard_weight = weights[i]; + } + } + + JointsAndWeights ret; + int dst = 0; + float sum = 0; + for (int src = 0; src < 4; src++) { + if (src == discard_idx) { + continue; + } + // this +1 is to account for align not existing in the gltf. + ret.joints[dst] = joints[src] + 2; + ret.weights[dst] = weights[src]; + sum += ret.weights[dst]; + dst++; + } + + ret.weights /= sum; + return ret; +} + +std::vector extract_and_flatten_joints_and_weights( + const tinygltf::Model& model, + const tinygltf::Primitive& prim) { + auto weights = + extract_vec(model, prim.attributes.at("WEIGHTS_0"), TINYGLTF_COMPONENT_TYPE_FLOAT); + auto joints = extract_vec(model, prim.attributes.at("JOINTS_0"), + TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE); + std::vector ret; + ASSERT(weights.size() == joints.size()); + for (size_t i = 0; i < weights.size(); i++) { + ret.push_back(convert_per_vertex_data(weights[i], joints[i])); + } + return ret; +} + /*! * Extract positions, colors, and normals from a mesh. */ @@ -148,19 +240,37 @@ ExtractedVertices gltf_vertices(const tinygltf::Model& model, buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset; const auto byte_stride = attrib_accessor.ByteStride(buffer_view); const auto count = attrib_accessor.count; - - ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC4, "COLOR_0 wasn't vec4"); std::vector> colors; - switch (attrib_accessor.componentType) { - case TINYGLTF_COMPONENT_TYPE_FLOAT: - colors = extract_color_from_vec4_float(data_ptr, count, byte_stride); + + switch (attrib_accessor.type) { + case TINYGLTF_TYPE_VEC4: + switch (attrib_accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_FLOAT: + colors = extract_color_from_vec4_float(data_ptr, count, byte_stride); + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + colors = extract_color_from_vec4_u16(data_ptr, count, byte_stride); + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + colors = extract_color_from_vec4_u8(data_ptr, count, byte_stride); + break; + default: + lg::die("Unknown type for COLOR_0: {}", attrib_accessor.componentType); + } break; - case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: - colors = extract_color_from_vec4_u16(data_ptr, count, byte_stride); + case TINYGLTF_TYPE_VEC3: + switch (attrib_accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_FLOAT: + colors = extract_color_from_vec3_float(data_ptr, count, byte_stride); + break; + default: + lg::die("unkonwn component type for vec3 color {}", attrib_accessor.componentType); + } break; default: - lg::die("Unknown component type for COLOR_0: {}", attrib_accessor.componentType); + lg::die("unknown attribute type for color {}", attrib_accessor.type); } + vtx_colors.insert(vtx_colors.end(), colors.begin(), colors.end()); } } @@ -348,6 +458,15 @@ math::Vector4f vector4f_from_gltf(const std::vector& in) { return math::Vector4f{in[0], in[1], in[2], in[3]}; } +math::Matrix4f matrix_from_trs(const math::Vector3f& trans, + const math::Vector4f& quat, + const math::Vector3f& scale) { + math::Matrix4f t = affine_translation(trans); + math::Matrix4f r = affine_rot_qxyzw(quat); + math::Matrix4f s = affine_scale(scale); + return t * r * s; +} + math::Matrix4f matrix_from_node(const tinygltf::Node& node) { if (!node.matrix.empty()) { math::Matrix4f result; @@ -464,4 +583,43 @@ DrawMode draw_mode_from_sampler(const tinygltf::Sampler& sampler) { return mode; } + +std::optional find_single_skin(const tinygltf::Model& model, + const std::vector& all_nodes) { + std::optional skin_index; + for (const auto& n : all_nodes) { + const auto& node = model.nodes.at(n.node_idx); + if (node.skin >= 0) { + if (skin_index && *skin_index != node.skin) { + lg::die("GLTF contains multiple skins, but only one skin per actor is supported."); + } + skin_index = node.skin; + } + } + return skin_index; +} + +std::vector extract_floats(const tinygltf::Model& model, int accessor_idx) { + const auto& accessor = model.accessors[accessor_idx]; + const auto& buffer_view = model.bufferViews[accessor.bufferView]; + const auto& buffer = model.buffers[buffer_view.buffer]; + const u8* data_ptr = buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const auto stride = accessor.ByteStride(buffer_view); + const auto count = accessor.count; + + // ASSERT(buffer_view.target == TINYGLTF_TARGET_ARRAY_BUFFER); // ?? + if (accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) { + lg::die("mismatched format, wanted {} but got {}", TINYGLTF_COMPONENT_TYPE_FLOAT, + accessor.componentType); + } + ASSERT(accessor.type == TINYGLTF_TYPE_SCALAR); + + std::vector result(accessor.count); + for (size_t x = 0; x < count; x++) { + memcpy(&result[x], data_ptr, sizeof(float)); + data_ptr += stride; + } + return result; +} + } // namespace gltf_util \ No newline at end of file diff --git a/common/util/gltf_util.h b/common/util/gltf_util.h index 71dcc47bfa..4428394bdb 100644 --- a/common/util/gltf_util.h +++ b/common/util/gltf_util.h @@ -1,11 +1,13 @@ #pragma once +#include #include #include #include #include "common/common_types.h" #include "common/custom_data/Tfrag3Data.h" +#include "common/log/log.h" #include "common/math/Vector.h" #include "third-party/tiny_gltf/tiny_gltf.h" @@ -28,10 +30,19 @@ std::vector index_list_to_u32(const u8* data, u32 num_verts, u32 offset, u3 return result; } +struct JointsAndWeights { + math::Vector joints = math::Vector::zero(); + math::Vector weights = math::Vector::zero(); +}; + std::vector extract_vec3f(const u8* data, u32 count, u32 stride); std::vector extract_vec2f(const u8* data, u32 count, u32 stride); std::vector> extract_color_from_vec4_u16(const u8* data, u32 count, u32 stride); std::vector gltf_index_buffer(const tinygltf::Model& model, int indices_idx, u32 index_offset); +std::vector extract_mat4(const tinygltf::Model& model, int accessor_idx); +std::vector extract_and_flatten_joints_and_weights( + const tinygltf::Model& model, + const tinygltf::Primitive& prim); struct ExtractedVertices { std::vector vtx; @@ -68,4 +79,52 @@ std::vector flatten_nodes_from_all_scenes(const tinygltf::Mod DrawMode draw_mode_from_sampler(const tinygltf::Sampler& sampler); +/*! + * Find the index of the skin for this model. Returns nullopt if there is no skin, the index of the + * skin if there is a single skin used, or fatal error if there are multiple skins. + */ +std::optional find_single_skin(const tinygltf::Model& model, + const std::vector& all_nodes); + +template +std::vector> extract_vec(const tinygltf::Model& model, + int accessor_idx, + int format) { + const auto& accessor = model.accessors[accessor_idx]; + const auto& buffer_view = model.bufferViews[accessor.bufferView]; + const auto& buffer = model.buffers[buffer_view.buffer]; + const u8* data_ptr = buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const auto stride = accessor.ByteStride(buffer_view); + const auto count = accessor.count; + + // ASSERT(buffer_view.target == TINYGLTF_TARGET_ARRAY_BUFFER); // ?? + if (accessor.componentType != format) { + lg::die("mismatched format, wanted {} but got {}", format, accessor.componentType); + } + ASSERT(accessor.componentType == format); + switch (n) { + case 3: + ASSERT(accessor.type == TINYGLTF_TYPE_VEC3); + break; + case 4: + ASSERT(accessor.type == TINYGLTF_TYPE_VEC4); + break; + default: + ASSERT_NOT_REACHED(); + } + + std::vector> result(accessor.count); + for (size_t x = 0; x < count; x++) { + for (int i = 0; i < n; i++) { + memcpy(&result[x][i], data_ptr + sizeof(T) * i, sizeof(T)); + } + data_ptr += stride; + } + return result; +} + +std::vector extract_floats(const tinygltf::Model& model, int accessor_idx); +math::Matrix4f matrix_from_trs(const math::Vector3f& trans, + const math::Vector4f& quat, + const math::Vector3f& scale); } // namespace gltf_util \ No newline at end of file diff --git a/decompiler/level_extractor/merc_replacement.cpp b/decompiler/level_extractor/merc_replacement.cpp index e7f38846c9..1cc4205faa 100644 --- a/decompiler/level_extractor/merc_replacement.cpp +++ b/decompiler/level_extractor/merc_replacement.cpp @@ -99,9 +99,9 @@ const tfrag3::MercVertex& find_closest(const std::vector& ol float y, float z) { float best_dist = 1e10; - int best_idx = 0; + size_t best_idx = 0; - for (int i = 0; i < old.size(); i++) { + for (size_t i = 0; i < old.size(); i++) { auto& v = old[i]; float dx = v.pos[0] - x; float dy = v.pos[1] - y; diff --git a/goalc/CMakeLists.txt b/goalc/CMakeLists.txt index 81964a43f5..3fab9307c5 100644 --- a/goalc/CMakeLists.txt +++ b/goalc/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(compiler emitter/Register.cpp debugger/disassemble.cpp build_level/common/build_level.cpp + build_actor/common/animation_processing.cpp build_actor/common/MercExtract.cpp build_level/jak1/build_level.cpp build_level/jak2/build_level.cpp diff --git a/goalc/build_actor/common/MercExtract.cpp b/goalc/build_actor/common/MercExtract.cpp index 1aceda7140..ad67298a2c 100644 --- a/goalc/build_actor/common/MercExtract.cpp +++ b/goalc/build_actor/common/MercExtract.cpp @@ -55,26 +55,29 @@ void extract(const std::string& name, out.normals.insert(out.normals.end(), verts.normals.begin(), verts.normals.end()); ASSERT(out.new_colors.size() == out.new_vertices.size()); - // TODO: just putting it all in one material + if (prim.attributes.count("JOINTS_0") && prim.attributes.count("WEIGHTS_0")) { + auto joints_and_weights = gltf_util::extract_and_flatten_joints_and_weights(model, prim); + ASSERT(joints_and_weights.size() == verts.vtx.size()); + out.joints_and_weights.insert(out.joints_and_weights.end(), joints_and_weights.begin(), + joints_and_weights.end()); + } else { + // add fake data for vertices without this data + gltf_util::JointsAndWeights dummy; + dummy.joints[0] = 3; + dummy.weights[0] = 1.f; + for (size_t i = 0; i < out.new_vertices.size(); i++) { + out.joints_and_weights.push_back(dummy); + } + } + + // real draw details will be filled out in the next loop. auto& draw = draw_by_material[prim.material]; draw.mode = gltf_util::make_default_draw_mode(); // todo rm draw.tree_tex_id = 0; // todo rm draw.num_triangles += prim_indices.size() / 3; draw.no_strip = true; - // if (draw.vis_groups.empty()) { - // auto& grp = draw.vis_groups.emplace_back(); - // grp.num_inds += prim_indices.size(); - // grp.num_tris += draw.num_triangles; - // grp.vis_idx_in_pc_bvh = UINT32_MAX; - // } else { - // auto& grp = draw.vis_groups.back(); - // grp.num_inds += prim_indices.size(); - // grp.num_tris += draw.num_triangles; - // grp.vis_idx_in_pc_bvh = UINT32_MAX; - // } draw.index_count = prim_indices.size(); draw.first_index = index_offset + out.new_indices.size(); - out.new_indices.insert(out.new_indices.end(), prim_indices.begin(), prim_indices.end()); } } @@ -136,18 +139,18 @@ void merc_convert(MercSwapData& out, const MercExtractData& in) { x.normal[0] = in.normals.at(i).x(); x.normal[1] = in.normals.at(i).y(); x.normal[2] = in.normals.at(i).z(); - x.weights[0] = 1.0f; - x.weights[1] = 0.0f; - x.weights[2] = 0.0f; + x.weights[0] = in.joints_and_weights.at(i).weights[0]; + x.weights[1] = in.joints_and_weights.at(i).weights[1]; + x.weights[2] = in.joints_and_weights.at(i).weights[2]; x.st[0] = y.s; x.st[1] = y.t; x.rgba[0] = in.new_colors[i][0]; x.rgba[1] = in.new_colors[i][1]; x.rgba[2] = in.new_colors[i][2]; x.rgba[3] = in.new_colors[i][3]; - x.mats[0] = 3; - x.mats[1] = 0; - x.mats[2] = 0; + x.mats[0] = in.joints_and_weights.at(i).joints[0]; + x.mats[1] = in.joints_and_weights.at(i).joints[1]; + x.mats[2] = in.joints_and_weights.at(i).joints[2]; } } diff --git a/goalc/build_actor/common/MercExtract.h b/goalc/build_actor/common/MercExtract.h index 7653b93d03..31adb08dd4 100644 --- a/goalc/build_actor/common/MercExtract.h +++ b/goalc/build_actor/common/MercExtract.h @@ -10,7 +10,7 @@ struct MercExtractData { std::vector new_vertices; std::vector> new_colors; std::vector normals; - + std::vector joints_and_weights; tfrag3::MercModel new_model; }; diff --git a/goalc/build_actor/common/animation_processing.cpp b/goalc/build_actor/common/animation_processing.cpp new file mode 100644 index 0000000000..337c60f287 --- /dev/null +++ b/goalc/build_actor/common/animation_processing.cpp @@ -0,0 +1,281 @@ +#include "animation_processing.h" + +#include "common/log/log.h" +#include "common/util/gltf_util.h" + +namespace anim { + +namespace { +int find_max_joint(const tinygltf::Animation& anim, const std::map& node_to_joint) { + int max_joint = 0; + for (const auto& channel : anim.channels) { + if (node_to_joint.find(channel.target_node) != node_to_joint.end()) { + max_joint = std::max(node_to_joint.at(channel.target_node), max_joint); + } else { + lg::error("animated node not in our skeleton!!"); + } + } + return max_joint; +} + +template +std::vector compute_keyframes(const std::vector& times, + const std::vector& values, + float framerate) { + std::vector ret; + ASSERT(!times.empty()); + ASSERT(times.size() == values.size()); + size_t i = 0; + float t = 0; + while (t < times.back()) { + // advance input keyframe + while ((i + 1 < times.size()) // can advance + && times.at(i + 1) < t // next keyframe is before sample point + ) { + i++; + } + + const float fraction = (t - times.at(i)) / (times.at(i + 1) - times.at(i)); + ret.push_back(values.at(i) * (1.f - fraction) + values.at(i + 1) * fraction); + // lg::info("{} + {:.3f}, {}", i, fraction, ret.back().to_string_aligned()); + t += 1.f / framerate; + } + return ret; +} + +template +std::vector> extract_keyframed_gltf_vecn( + const tinygltf::Model& model, + const tinygltf::AnimationSampler& sampler, + float framerate) { + std::vector times = gltf_util::extract_floats(model, sampler.input); + std::vector> values = + gltf_util::extract_vec(model, sampler.output, TINYGLTF_COMPONENT_TYPE_FLOAT); + ASSERT(times.size() == values.size()); + return compute_keyframes(times, values, framerate); +} +} // namespace + +UncompressedJointAnim extract_anim_from_gltf(const tinygltf::Model& model, + const tinygltf::Animation& anim, + const std::map& node_to_joint, + float framerate) { + UncompressedJointAnim out; + out.name = anim.name; + lg::info("Processing animation {}", anim.name); + const int max_joint = find_max_joint(anim, node_to_joint); + lg::info("Max joint is {}", max_joint); + out.joints.resize(max_joint + 1); + + for (const auto& channel : anim.channels) { + const int channel_node_idx = channel.target_node; + // const auto& channel_node = model.nodes.at(channel_node_idx); + const int channel_joint = node_to_joint.at(channel_node_idx); + // lg::info("channel for {} {} / {}", channel_joint, channel_node.name, channel.target_path); + const auto& sampler = anim.samplers.at(channel.sampler); + if (channel.target_path == "translation") { + out.joints.at(channel_joint).trans_frames = + extract_keyframed_gltf_vecn<3>(model, sampler, framerate); + } else if (channel.target_path == "rotation") { + out.joints.at(channel_joint).quat_frames = + extract_keyframed_gltf_vecn<4>(model, sampler, framerate); + } else if (channel.target_path == "scale") { + out.joints.at(channel_joint).scale_frames = + extract_keyframed_gltf_vecn<3>(model, sampler, framerate); + } else { + lg::die("unknown target_path {}", channel.target_path); + } + } + + size_t max_frames = 0; + for (auto& joint : out.joints) { + max_frames = std::max(max_frames, joint.quat_frames.size()); + max_frames = std::max(max_frames, joint.trans_frames.size()); + max_frames = std::max(max_frames, joint.scale_frames.size()); + } + lg::info("max frames is {}", max_frames); + + // make up data for missing joints (like align, for example) + for (auto& joint : out.joints) { + if (joint.quat_frames.size() < max_frames) { + lg::warn("joint with {} / {} quat frames!", joint.quat_frames.size(), max_frames); + if (joint.quat_frames.empty()) { + joint.quat_frames.emplace_back(0, 0, 0, 1); + } + while (joint.quat_frames.size() < max_frames) { + joint.quat_frames.push_back(joint.quat_frames.back()); + } + } + + if (joint.trans_frames.size() < max_frames) { + lg::warn("joint with {} / {} trans frames!", joint.trans_frames.size(), max_frames); + if (joint.trans_frames.empty()) { + joint.trans_frames.emplace_back(0, 0, 0); + } + while (joint.trans_frames.size() < max_frames) { + joint.trans_frames.push_back(joint.trans_frames.back()); + } + } + + if (joint.scale_frames.size() < max_frames) { + lg::warn("joint with {} / {} scale frames!", joint.scale_frames.size(), max_frames); + if (joint.scale_frames.empty()) { + joint.scale_frames.emplace_back(1, 1, 1); + } + while (joint.scale_frames.size() < max_frames) { + joint.scale_frames.push_back(joint.scale_frames.back()); + } + } + } + + out.framerate = framerate; + out.frames = max_frames; + return out; +} + +namespace { +template +bool is_constant(const std::vector>& in) { + if (in.empty()) { + return true; + } + auto first = in.at(0); + for (auto& x : in) { + if (x != first) { + return false; + } + } + return true; +} + +bool can_use_small_trans(const std::vector& trans_frames) { + constexpr float kMaxTrans = 32767.f * (4.f / 4096.f); + for (auto& trans : trans_frames) { + for (int i = 0; i < 3; i++) { + if (trans[i] > kMaxTrans || trans[i] < -kMaxTrans) { + return false; + } + } + } + return true; +} + +bool is_matrix_constant(const UncompressedSingleJointAnim& anim) { + return is_constant(anim.quat_frames) && is_constant(anim.scale_frames) && + is_constant(anim.trans_frames); +} + +void compress_frame_to_matrix(CompressedFrame* frame, + const math::Vector3f& trans, + const math::Vector4f& quat, + const math::Vector3f& scale) { + auto mat = gltf_util::matrix_from_trs(trans * 4096, quat, scale); + constexpr int n = 4 * 4 * sizeof(float) / sizeof(u64); + u64 data[n]; + memcpy(data, mat.data(), 4 * 4 * sizeof(float)); + frame->data64.insert(frame->data64.end(), data, data + n); +} + +void compress_trans(CompressedFrame* frame, const math::Vector3f& trans, bool big) { + if (big) { + // 64, 64, 32 + u64 data[1]; + memcpy(data, trans.data(), 2 * sizeof(float)); + frame->data64.push_back(data[0]); + u32 data_32[1]; + memcpy(data_32, &trans.z(), sizeof(float)); + frame->data32.push_back(data_32[0]); + } else { + constexpr float kTransScale = 4.f / 4096.f; + s16 data1[3]; + for (int i = 0; i < 3; i++) { + data1[i] = s16(trans[i] / kTransScale); + } + u32 data2[1]; + memcpy(data2, data1, 4); + frame->data32.push_back(data2[0]); + frame->data16.push_back(data1[2]); + } +} + +void compress_quat(CompressedFrame* frame, const math::Vector4f& quat) { + constexpr float kQuatScale = 0.000030517578125; + s16 data1[4]; + for (int i = 0; i < 4; i++) { + data1[i] = s16(quat[i] / kQuatScale); + } + u64 data2[1]; + memcpy(data2, data1, 8); + frame->data64.push_back(data2[0]); +} + +void compress_scale(CompressedFrame* frame, const math::Vector3f& scale) { + constexpr float kScaleScale = 0.000244140625; + s16 data1[3]; + for (int i = 0; i < 3; i++) { + data1[i] = s16(scale[i] / kScaleScale); + } + u32 data2[1]; + memcpy(data2, data1, 4); + frame->data32.push_back(data2[0]); + frame->data16.push_back(data1[2]); +} +} // namespace + +CompressedAnim compress_animation(const UncompressedJointAnim& in) { + ASSERT(in.joints.size() >= 2); // need two matrix joints. + CompressedAnim out; + out.name = in.name; + out.framerate = in.framerate; + out.frames.resize(in.frames); + for (int matrix = 0; matrix < 2; matrix++) { + const auto& joint_data = in.joints.at(matrix); + if (is_matrix_constant(joint_data)) { + out.matrix_animated[matrix] = false; + compress_frame_to_matrix(&out.fixed, joint_data.trans_frames[0], joint_data.quat_frames[0], + joint_data.scale_frames[0]); + } else { + out.matrix_animated[matrix] = true; + for (int i = 0; i < in.frames; i++) { + compress_frame_to_matrix(&out.frames[i], joint_data.trans_frames[i], + joint_data.quat_frames[i], joint_data.scale_frames[i]); + } + } + } + + for (size_t joint = 2; joint < in.joints.size(); joint++) { + const auto& joint_data = in.joints.at(joint); + auto& metadata = out.joint_metadata.emplace_back(); + + metadata.animated_trans = !is_constant(joint_data.trans_frames); + metadata.animated_quat = !is_constant(joint_data.quat_frames); + metadata.animated_scale = !is_constant(joint_data.scale_frames); + metadata.big_trans_mode = !can_use_small_trans(joint_data.trans_frames); + + if (metadata.animated_trans) { + for (int i = 0; i < in.frames; i++) { + compress_trans(&out.frames[i], joint_data.trans_frames[i], metadata.big_trans_mode); + } + } else { + compress_trans(&out.fixed, joint_data.trans_frames[0], metadata.big_trans_mode); + } + + if (metadata.animated_quat) { + for (int i = 0; i < in.frames; i++) { + compress_quat(&out.frames[i], joint_data.quat_frames[i]); + } + } else { + compress_quat(&out.fixed, joint_data.quat_frames[0]); + } + + if (metadata.animated_scale) { + for (int i = 0; i < in.frames; i++) { + compress_scale(&out.frames[i], joint_data.scale_frames[i]); + } + } else { + compress_scale(&out.fixed, joint_data.scale_frames[0]); + } + } + return out; +} +} // namespace anim \ No newline at end of file diff --git a/goalc/build_actor/common/animation_processing.h b/goalc/build_actor/common/animation_processing.h new file mode 100644 index 0000000000..9d70e1df4e --- /dev/null +++ b/goalc/build_actor/common/animation_processing.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include + +#include "common/common_types.h" +#include "common/math/Vector.h" + +#include "third-party/tiny_gltf/tiny_gltf.h" + +namespace anim { + +struct UncompressedSingleJointAnim { + std::vector trans_frames; + std::vector scale_frames; + std::vector quat_frames; +}; + +struct UncompressedJointAnim { + std::string name; + std::vector joints; + float framerate = 60; + int frames = 0; +}; + +struct CompressedMatrixMetadata { + bool is_animated[2]; +}; + +struct CompressedFrame { + std::vector data16; + std::vector data32; + std::vector data64; + + int size_bytes() const { return data16.size() * 2 + data32.size() * 4 + data64.size() * 8; } +}; + +struct CompressedJointMetadata { + bool animated_trans = false; + bool animated_quat = false; + bool animated_scale = false; + bool big_trans_mode = false; +}; + +struct CompressedAnim { + std::string name; + CompressedFrame fixed; + std::vector frames; + bool matrix_animated[2] = {false, false}; + std::vector joint_metadata; + float framerate = 60; +}; + +/*! + * Load animation data from GLTF file. + * @param model The GLTF model containing the animation + * @param anim The animation to convert + * @param node_to_joint Mapping from GLTF node index to the joint index + * @param framerate Number of key-frames per second. (this doesn't have to match frame rate, the + * game will interpolate between keyframes as needed.) + */ +UncompressedJointAnim extract_anim_from_gltf(const tinygltf::Model& model, + const tinygltf::Animation& anim, + const std::map& node_to_joint, + float framerate); + +CompressedAnim compress_animation(const UncompressedJointAnim& in); + +} // namespace anim \ No newline at end of file diff --git a/goalc/build_actor/jak1/build_actor.cpp b/goalc/build_actor/jak1/build_actor.cpp index f9b54ae60f..603f06505f 100644 --- a/goalc/build_actor/jak1/build_actor.cpp +++ b/goalc/build_actor/jak1/build_actor.cpp @@ -1,14 +1,143 @@ #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; namespace jak1 { +JointAnimCompressedHDR::JointAnimCompressedHDR(const anim::CompressedAnim& anim) { + matrix_bits = 0; + if (anim.matrix_animated[0]) { + matrix_bits |= 1; + } + if (anim.matrix_animated[1]) { + matrix_bits |= 2; + } + + for (auto& word : control_bits) { + word = 0; + } + + for (size_t i = 0; i < anim.joint_metadata.size(); i++) { + const int word_idx = i / 8; + if (word_idx >= 14) { + lg::error("too many joints!!"); + break; + } + + u32 val = 0; + const auto& metadata = anim.joint_metadata[i]; + if (metadata.animated_trans) { + val |= (0b1); + } + if (metadata.animated_quat) { + val |= (0b10); + } + if (metadata.animated_scale) { + val |= (0b100); + } + if (metadata.big_trans_mode) { + val |= (0b1000); + } + val = (val << (4 * (i % 8))); + control_bits[word_idx] |= val; + } + + num_joints = 2 + anim.joint_metadata.size(); +} + +JointAnimCompressedFixed::JointAnimCompressedFixed(const anim::CompressedAnim& anim) : hdr(anim) { + u8* dest = (u8*)data; + const u8* u64_src = (const u8*)anim.fixed.data64.data(); + const u8* u32_src = (const u8*)anim.fixed.data32.data(); + const u8* u16_src = (const u8*)anim.fixed.data16.data(); + + const int u64_size = anim.fixed.data64.size() * sizeof(u64); + const int u32_size = anim.fixed.data32.size() * sizeof(u32); + const int u16_size = anim.fixed.data16.size() * sizeof(u16); + + if (u64_size + u32_size + u16_size > 133 * 4 * 4) { + lg::die("fixed sizes are {} and {}", 133 * 4 * 4, u64_size + u32_size + u16_size); + } + ASSERT(u64_size + u32_size + u16_size <= 133 * 4 * 4); + + offset_64 = 0; + memcpy(dest, u64_src, u64_size); + dest += u64_size; + + offset_32 = offset_64 + u64_size; + memcpy(dest, u32_src, u32_size); + dest += u32_size; + + offset_16 = offset_32 + u32_size; + memcpy(dest, u16_src, u16_size); + reserved = 0; + + num_data_qw_used = (15 + u64_size + u32_size + u16_size) / 16; + ASSERT(num_data_qw_used <= 133); +} + +JointAnimCompressedFrame::JointAnimCompressedFrame(const anim::CompressedFrame& frame) { + reserved = 0; + u8* dest = (u8*)data; + const u8* u64_src = (const u8*)frame.data64.data(); + const u8* u32_src = (const u8*)frame.data32.data(); + const u8* u16_src = (const u8*)frame.data16.data(); + + const int u64_size = frame.data64.size() * sizeof(u64); + const int u32_size = frame.data32.size() * sizeof(u32); + const int u16_size = frame.data16.size() * sizeof(u16); + + if (u64_size + u32_size + u16_size > 133 * 4 * 4) { + lg::die("frame sizes are {} and {}", 133 * 4 * 4, u64_size + u32_size + u16_size); + } + + offset_64 = 0; + memcpy(dest, u64_src, u64_size); + dest += u64_size; + + offset_32 = offset_64 + u64_size; + memcpy(dest, u32_src, u32_size); + dest += u32_size; + + offset_16 = offset_32 + u32_size; + memcpy(dest, u16_src, u16_size); + reserved = 0; + + num_data_qw_used = (15 + u64_size + u32_size + u16_size) / 16; + ASSERT(num_data_qw_used <= 133); +} + +JointAnimCompressedControl::JointAnimCompressedControl(const anim::CompressedAnim& anim) + : fixed(anim) { + num_frames = anim.frames.size(); + for (auto& in_frame : anim.frames) { + frame.emplace_back(in_frame); + } + fixed_qwc = fixed.num_data_qw_used; + frame_qwc = frame.at(0).num_data_qw_used; +} + +ArtJointAnim::ArtJointAnim(const anim::CompressedAnim& anim, const std::vector& joints) + : frames(anim) { + this->name = anim.name; + length = joints.size(); + speed = 1.0f; + artist_base = 0.0f; + artist_step = 1.0f; + master_art_group_name = name; + master_art_group_index = 2; + for (auto& joint : joints) { + data.emplace_back(joint, anim.frames.size()); + } +} + std::map g_joint_map; size_t Joint::generate(DataObjectGenerator& gen) const { @@ -36,6 +165,7 @@ size_t JointAnimCompressed::generate(DataObjectGenerator& gen) const { size_t result = gen.current_offset_bytes(); gen.add_ref_to_string_in_pool(name); gen.add_word((length << 16) + number); + // data is always nullptr. // for (auto& word : data) { // gen.add_word(word); // } @@ -43,12 +173,19 @@ size_t JointAnimCompressed::generate(DataObjectGenerator& gen) const { } size_t JointAnimCompressedFrame::generate(DataObjectGenerator& gen) const { + gen.align(16); // might have been 8, but 16 doesn't hurt. + size_t result = gen.current_offset_bytes(); gen.add_word(offset_64); // 0 gen.add_word(offset_32); // 4 gen.add_word(offset_16); // 8 gen.add_word(reserved); // 12 - gen.align(4); + + for (u32 i = 0; i < num_data_qw_used; i++) { + for (int j = 0; j < 4; j++) { + gen.add_word_float(data[i][j]); + } + } return result; } @@ -59,11 +196,11 @@ size_t JointAnimCompressedHDR::generate(DataObjectGenerator& gen) const { } gen.add_word(num_joints); gen.add_word(matrix_bits); - gen.align(4); return result; } size_t JointAnimCompressedFixed::generate(DataObjectGenerator& gen) const { + gen.align(16); // might have been 8, but 16 doesn't hurt. size_t result = gen.current_offset_bytes(); hdr.generate(gen); // 0-64 (inline) gen.add_word(offset_64); // 64 @@ -71,12 +208,13 @@ size_t JointAnimCompressedFixed::generate(DataObjectGenerator& gen) const { gen.add_word(offset_16); // 72 gen.add_word(reserved); // 76 // default joint poses (taken from money-idle) - for (size_t i = 0; i < 8; i++) { + for (int i = 0; i < num_data_qw_used; i++) { gen.add_word_float(data[i].x()); gen.add_word_float(data[i].y()); gen.add_word_float(data[i].z()); gen.add_word_float(data[i].w()); } + // -- not sure what this is, part of the dummy data maybe? gen.add_word(0); gen.add_word(0x7fff0000); gen.add_word(0x2250000); @@ -90,16 +228,22 @@ size_t JointAnimCompressedFixed::generate(DataObjectGenerator& gen) const { } size_t JointAnimCompressedControl::generate(DataObjectGenerator& gen) const { + gen.align(16); size_t result = gen.current_offset_bytes(); gen.add_word(num_frames); // 0 gen.add_word(fixed_qwc); // 4 gen.add_word(frame_qwc); // 8 auto ja_fixed_slot = gen.add_word(0); - auto ja_frame_slot = gen.add_word(0); - gen.align(4); + std::vector ja_frame_slots; + for (u32 i = 0; i < num_frames; i++) { + ja_frame_slots.push_back(gen.add_word(0)); + } gen.link_word_to_byte(ja_fixed_slot, fixed.generate(gen)); - gen.link_word_to_byte(ja_frame_slot, frame[0].generate(gen)); + ASSERT(num_frames == frame.size()); + for (size_t i = 0; i < num_frames; i++) { + gen.link_word_to_byte(ja_frame_slots[i], frame[i].generate(gen)); + } return result; } @@ -161,6 +305,7 @@ size_t ArtJointGeo::generate_mesh(DataObjectGenerator& gen) const { gen.add_type_tag("collide-mesh"); // 8 (content-type) content_slots.reserve(cmeshes.size()); for (auto& data : cmeshes) { + (void)data; content_slots.push_back(gen.add_word(0)); // 12 (data) } gen.align(4); @@ -185,11 +330,11 @@ size_t ArtJointGeo::generate(DataObjectGenerator& gen) { gen.add_word(0); gen.add_word(0); std::vector joint_slots; - for (size_t i = 0; i < length; i++) { + for (int i = 0; i < length; i++) { joint_slots.push_back(gen.add_word(0)); } gen.align(4); - for (size_t i = 0; i < length; i++) { + for (int i = 0; i < length; i++) { auto joint = data.at(i).generate(gen); gen.link_word_to_byte(joint_slots.at(i), joint); g_joint_map[data.at(i).number] = joint; @@ -218,12 +363,12 @@ size_t ArtJointAnim::generate(DataObjectGenerator& gen) const { gen.add_symbol_link("#f"); // 40 (blerc) auto ctrl_slot = gen.add_word(0); std::vector frame_slots; - for (size_t i = 0; i < length; i++) { + for (int i = 0; i < length; i++) { frame_slots.push_back(gen.add_word(0)); } gen.align(4); gen.link_word_to_byte(ctrl_slot, frames.generate(gen)); - for (size_t i = 0; i < length; i++) { + for (int i = 0; i < length; i++) { gen.link_word_to_byte(frame_slots.at(i), data.at(i).generate(gen)); } return result; @@ -351,6 +496,41 @@ size_t gen_dummy_frag_ctrl(DataObjectGenerator& gen) { return result; } +size_t gen_dummy_frag_ctrl_for_uploads(DataObjectGenerator& gen, int n) { + size_t result = gen.current_offset_bytes(); + + std::vector packed_frag_ctrls; + + // this is still a bit of a hack - the dummy_frag_ctrl above has 8 fragments, so we need to + // provide fragment controls for each. The PC merc renderer will de-duplicate bone uploads over + // all effects and fragments, so we just need to have a single fragment that asks for all bones, + // and everything will work out. + for (int k = 0; k < 8; k++) { + packed_frag_ctrls.push_back(0); + packed_frag_ctrls.push_back(0); + packed_frag_ctrls.push_back(0); + if (k == 0) { // for the first frag, do all matrix uploads. + // note that these are bogus destination addresses, but nothing uses it on PC + packed_frag_ctrls.push_back(n); + for (int i = 0; i < n; i++) { + packed_frag_ctrls.push_back(i); + packed_frag_ctrls.push_back(i); + } + } else { + // remaining frags can have empty matrix upload lists. + packed_frag_ctrls.push_back(0); + } + } + + std::vector u32s((packed_frag_ctrls.size() + 3) / 4); + memcpy(u32s.data(), packed_frag_ctrls.data(), packed_frag_ctrls.size()); + for (auto x : u32s) { + gen.add_word(x); + } + + return result; +} + size_t gen_dummy_extra_info(DataObjectGenerator& gen) { size_t result = gen.current_offset_bytes(); gen.add_word(0x1); @@ -369,7 +549,7 @@ size_t generate_dummy_merc_ctrl(DataObjectGenerator& gen, const ArtGroup& ag) { gen.add_type_tag("merc-ctrl"); size_t result = gen.current_offset_bytes(); // excluding align and prejoint - auto joints = ((ArtJointGeo*)ag.elts.at(0))->length - 2; + auto joints = ((ArtJointGeo*)ag.elts.at(0).get())->length - 2; gen.add_word(0); // 4 gen.add_ref_to_string_in_pool(ag.name + "-lod0"); // 8 gen.add_word(0); // 12 @@ -406,7 +586,7 @@ size_t generate_dummy_merc_ctrl(DataObjectGenerator& gen, const ArtGroup& ag) { gen.add_word(0x100011b); // 136 auto extra_info_slot = gen.add_word(0); // 140 (extra-info) gen.link_word_to_byte(extra_info_slot, gen_dummy_extra_info(gen)); - gen.link_word_to_byte(frag_ctrl_slot, gen_dummy_frag_ctrl(gen)); + gen.link_word_to_byte(frag_ctrl_slot, gen_dummy_frag_ctrl_for_uploads(gen, joints + 3)); gen.link_word_to_byte(frag_geo_slot, gen_dummy_frag_geo(gen)); return result; } @@ -428,15 +608,16 @@ std::vector ArtGroup::save_object_file() const { gen.set_word(28 / 4, 0); if (!elts.empty()) { if (elts.at(0)) { - auto jgeo = (ArtJointGeo*)elts.at(0); + auto jgeo = (ArtJointGeo*)elts.at(0).get(); gen.link_word_to_byte(32 / 4, jgeo->generate(gen)); } if (!elts.at(1)) { gen.link_word_to_byte(36 / 4, generate_dummy_merc_ctrl(gen, *this)); } - if (elts.at(2)) { - auto ja = (ArtJointAnim*)elts.at(2); - gen.link_word_to_byte(40 / 4, ja->generate(gen)); + + for (size_t i = 2; i < elts.size(); i++) { + auto ja = (ArtJointAnim*)elts.at(i).get(); + gen.link_word_to_byte((32 + i * 4) / 4, ja->generate(gen)); } } @@ -446,7 +627,7 @@ std::vector ArtGroup::save_object_file() const { int ArtGroup::get_joint_idx(const std::string& name) { for (auto& elt : this->elts) { if (elt && typeid(*elt) == typeid(ArtJointGeo)) { - auto jgeo = (ArtJointGeo*)elt; + auto jgeo = (ArtJointGeo*)elt.get(); for (auto& joint : jgeo->data) { if (joint.name == name) { return joint.number; @@ -457,6 +638,156 @@ int ArtGroup::get_joint_idx(const std::string& name) { return -1; } +/*! + * 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; +} + +struct GltfJoint { + math::Matrix4f bind_pose_T_w; // inverse bind pose + std::string name; + int gltf_node_index = 0; + int parent = -1; + std::vector children; +}; + +/*! + * 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 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 node_to_joint; + std::map joint_to_node; + std::vector 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 convert_joints(const std::vector& gjoints) { + std::vector 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 process_anim(const tinygltf::Model& model, + const std::vector& gjoints) { + if (model.animations.empty()) { + lg::warn("no animations detected!"); // TODO: make up a dummy one + return {}; + } + + std::map node_to_joint; + for (size_t i = 0; i < gjoints.size(); i++) { + node_to_joint[gjoints[i].gltf_node_index] = i + kGltfToGameJointOffset; + } + + std::vector ret; + for (auto& anim : model.animations) { + lg::info("Processing animation {}", anim.name); + ret.push_back( + anim::compress_animation(anim::extract_anim_from_gltf(model, anim, node_to_joint, 60))); + } + return ret; +} + +/*! + * Build GOAL format data for an actor. This doesn't generate the data for the .FR3. + */ bool run_build_actor(const std::string& mdl_name, const std::string& ag_out, bool gen_collide_mesh) { @@ -467,51 +798,59 @@ bool run_build_actor(const std::string& mdl_name, ASSERT_MSG(false, "Model file not found: " + mdl_name); } + // Load GLTF file to check for a skeleton + tinygltf::Model model = load_gltf_model(file_util::get_jak_project_dir() / mdl_name); + auto all_nodes = flatten_nodes_from_all_scenes(model); + auto skin_idx = find_single_skin(model, all_nodes); + if (skin_idx) { + lg::info("GLTF file contained a skin, this actor will have a real skeleton"); + } + std::vector user_anims; + ArtGroup ag(ag_name); std::vector joints; - auto identity = math::Matrix4f::identity(); - joints.emplace_back("align", 0, -1, identity); - joints.emplace_back("prejoint", 1, -1, identity); - // matrix stolen from "egg" joint from "money" art group - auto main_pose = math::Matrix4f::zero(); - main_pose(0, 0) = 1.0f; - main_pose(0, 1) = -0.0f; - main_pose(0, 2) = 0.0f; - main_pose(0, 3) = -0.0f; - main_pose(1, 0) = -0.0f; - main_pose(1, 1) = 1.0f; - main_pose(1, 2) = -0.0f; - main_pose(1, 3) = 0.0f; - main_pose(2, 0) = 0.0f; - main_pose(2, 1) = -0.0f; - main_pose(2, 2) = 1.0f; - main_pose(2, 3) = -0.0f; - main_pose(3, 0) = -0.0f; - main_pose(3, 1) = -2194.1628418f; - main_pose(3, 2) = -0.0f; - main_pose(3, 3) = 1.0f; - Joint main("main", 2, 1, main_pose); - joints.emplace_back(main); + MercExtractData extract_data; + extract("test", extract_data, model, all_nodes, 0, 0, 0); + // MercSwapData out; + // merc_convert(out, extract_data); + // Set up joints: + if (skin_idx) { + // read skeleton data out of GLTF + auto skeleton_joints = extract_skeleton(model, *skin_idx); + // convert to game format + joints = convert_joints(skeleton_joints); + // get animation from user. + user_anims = process_anim(model, skeleton_joints); + + } else { + auto identity = math::Matrix4f::identity(); + joints.emplace_back("align", 0, -1, identity); + joints.emplace_back("prejoint", 1, -1, identity); + // matrix stolen from "egg" joint from "money" art group + auto main_pose = math::Matrix4f::identity(); + main_pose(3, 1) = -2194.1628418f; + Joint main("main", 2, 1, main_pose); + joints.emplace_back(main); + } + std::vector mesh; if (gen_collide_mesh) { - tinygltf::TinyGLTF loader; - tinygltf::Model model; - std::string err, warn; - std::string path = (file_util::get_jak_project_dir() / mdl_name).string(); - bool res = loader.LoadBinaryFromFile(&model, &err, &warn, path); - ASSERT_MSG(warn.empty(), warn.c_str()); - ASSERT_MSG(err.empty(), err.c_str()); - ASSERT_MSG(res, "Failed to load GLTF file!"); - auto all_nodes = flatten_nodes_from_all_scenes(model); mesh = gen_collide_mesh_from_model(model, all_nodes, 3); } - ArtJointGeo jgeo(ag.name, mesh, joints); - ArtJointAnim ja(ag.name, joints); - ag.elts.emplace_back(&jgeo); + std::shared_ptr jgeo = std::make_shared(ag.name, mesh, joints); + + ag.elts.emplace_back(jgeo); // dummy merc-ctrl ag.elts.emplace_back(nullptr); - ag.elts.emplace_back(&ja); + + if (!user_anims.empty()) { + for (auto& anim : user_anims) { + ag.elts.emplace_back(std::make_shared(anim, joints)); + } + } else { + ag.elts.emplace_back(std::make_shared(ag.name, joints)); + } ag.length = ag.elts.size(); diff --git a/goalc/build_actor/jak1/build_actor.h b/goalc/build_actor/jak1/build_actor.h index 9868cea3bc..3150d5fb99 100644 --- a/goalc/build_actor/jak1/build_actor.h +++ b/goalc/build_actor/jak1/build_actor.h @@ -2,11 +2,17 @@ #include "common/util/gltf_util.h" +#include "goalc/build_actor/common/animation_processing.h" #include "goalc/build_actor/common/art_types.h" #include "goalc/build_level/collide/common/collide_common.h" namespace jak1 { +// Note: there's some weirdness with the Joint types here - I believe that very early on in +// development, joint animations were stored separately per joint. However, all joint animations use +// the "compressed" format, which combines data for all joints into a single structure. +// By jak 2, they had cleaned this up, but for Jak 1, we have to deal with this weirdness. + struct Joint { std::string name; s32 number; @@ -24,35 +30,22 @@ struct Joint { }; // basic -struct JointAnim { +// This is one of those weird leftover types. +// There's one of these per-joint, per-animation, and all it's useful for is storing the +// length of the animation. The game only looks at the data for joint 0 and assumes the rest are +// the same. (and by jak 2, this is gone entirely!) +struct JointAnimCompressed { std::string name; s16 number; s16 length; - - explicit JointAnim(const Joint& joint) { - this->name = joint.name; - number = joint.number; - length = 1; - } -}; - -// basic -struct JointAnimCompressed : JointAnim { std::vector data; - explicit JointAnimCompressed(const Joint& joint) : JointAnim(joint) { - number = joint.number; - length = 1; - } - size_t generate(DataObjectGenerator& gen) const; -}; - -struct JointAnimFrame { - math::Matrix4f matrices[2]; - std::vector data; - + explicit JointAnimCompressed(const Joint& joint, s16 num_frames) + : name(joint.name), number(joint.number), length(num_frames) {} size_t generate(DataObjectGenerator& gen) const; }; +// Header for a compressed joint animation - this tells the decompressor how to read +// the data in the animation. struct JointAnimCompressedHDR { u32 control_bits[14]; u32 num_joints; @@ -65,6 +58,9 @@ struct JointAnimCompressedHDR { num_joints = 1; matrix_bits = 0; } + + explicit JointAnimCompressedHDR(const anim::CompressedAnim& anim); + size_t generate(DataObjectGenerator& gen) const; }; @@ -75,6 +71,7 @@ struct JointAnimCompressedFixed { u32 offset_16; u32 reserved; math::Vector4f data[133]; + int num_data_qw_used = 0; JointAnimCompressedFixed() { offset_64 = 0; @@ -89,7 +86,11 @@ struct JointAnimCompressedFixed { data[5] = math::Vector4f(0.0f, 1.0f, 0.0f, 0.0f); data[6] = math::Vector4f(0.0f, 0.0f, 1.0f, 0.0f); data[7] = math::Vector4f(0.0f, 0.0f, 0.0f, 1.0f); + num_data_qw_used = 8; } + + JointAnimCompressedFixed(const anim::CompressedAnim& anim); + size_t generate(DataObjectGenerator& gen) const; }; @@ -99,6 +100,7 @@ struct JointAnimCompressedFrame { u32 offset_16; u32 reserved; math::Vector4f data[133]; + u32 num_data_qw_used = 0; JointAnimCompressedFrame() { offset_64 = 0; @@ -107,6 +109,8 @@ struct JointAnimCompressedFrame { reserved = 0; } + JointAnimCompressedFrame(const anim::CompressedFrame& frame); + size_t generate(DataObjectGenerator& gen) const; }; @@ -114,17 +118,18 @@ struct JointAnimCompressedControl { u32 num_frames; u32 fixed_qwc; u32 frame_qwc; - JointAnimCompressedFixed fixed{}; - JointAnimCompressedFrame frame[1]; + JointAnimCompressedFixed fixed; + std::vector frame; JointAnimCompressedControl() { num_frames = 1; fixed_qwc = 0xf; frame_qwc = 1; - fixed = JointAnimCompressedFixed(); - frame[0] = JointAnimCompressedFrame(); + frame.emplace_back(); } + JointAnimCompressedControl(const anim::CompressedAnim& anim); + size_t generate(DataObjectGenerator& gen) const; }; @@ -188,17 +193,19 @@ struct ArtJointAnim : ArtElement { artist_step = 1.0f; master_art_group_name = name; master_art_group_index = 2; - frames = JointAnimCompressedControl(); for (auto& joint : joints) { - data.emplace_back(joint); + data.emplace_back(joint, 1); } } + + ArtJointAnim(const anim::CompressedAnim& anim, const std::vector& joints); + size_t generate(DataObjectGenerator& gen) const; }; struct ArtGroup : Art { FileInfo info; - std::vector elts; + std::vector> elts; std::map joint_map; explicit ArtGroup(const std::string& file_name) { diff --git a/goalc/build_actor/main.cpp b/goalc/build_actor/main.cpp index b2525fb82c..86ccc2a598 100644 --- a/goalc/build_actor/main.cpp +++ b/goalc/build_actor/main.cpp @@ -14,7 +14,7 @@ int main(int argc, char** argv) { // game version std::string game, mdl_name, output_file; - bool gen_collide_mesh; + bool gen_collide_mesh = false; fs::path project_path_override; // path diff --git a/goalc/build_level/common/build_level.cpp b/goalc/build_level/common/build_level.cpp index ab94ebc546..43684230f2 100644 --- a/goalc/build_level/common/build_level.cpp +++ b/goalc/build_level/common/build_level.cpp @@ -57,6 +57,7 @@ void add_model_to_level(GameVersion version, const std::string& name, tfrag3::Le for (auto& vert : merc_data.new_vertices) { lvl.merc_data.vertices.push_back(vert); } + lvl.merc_data.models.push_back(merc_data.new_model); lvl.textures.insert(lvl.textures.end(), merc_data.new_textures.begin(), merc_data.new_textures.end());