diff --git a/decompiler/CMakeLists.txt b/decompiler/CMakeLists.txt index 7e6f1403d6..e5aac4f6a3 100644 --- a/decompiler/CMakeLists.txt +++ b/decompiler/CMakeLists.txt @@ -60,6 +60,7 @@ add_library( level_extractor/extract_common.cpp level_extractor/extract_hfrag.cpp level_extractor/extract_joint_group.cpp + level_extractor/extract_anim.cpp level_extractor/extract_level.cpp level_extractor/extract_merc.cpp level_extractor/extract_tfrag.cpp @@ -100,7 +101,7 @@ target_link_libraries(decomp stb_image xdelta3 tiny_gltf - ) +) add_executable(decompiler main.cpp) diff --git a/decompiler/level_extractor/common_formats.h b/decompiler/level_extractor/common_formats.h index 5546c00252..914ef09e61 100644 --- a/decompiler/level_extractor/common_formats.h +++ b/decompiler/level_extractor/common_formats.h @@ -16,6 +16,136 @@ struct Joint { math::Matrix4f bind_pose_T_w; }; +struct UncompressedSingleJointAnim { + std::vector trans_frames; + std::vector scale_frames; + std::vector quat_frames; +}; + +struct UncompressedJointAnim { + std::string name; + std::vector joints; + float framerate = 30; + 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; +}; + +struct JointAnimCompressedHDR { + u32 control_bits[14]; + u32 num_joints; + u32 matrix_bits; + + JointAnimCompressedHDR() { + for (auto& bit : control_bits) { + bit = 0; + } + num_joints = 1; + matrix_bits = 0; + } +}; + +struct JointAnimCompressedFixed { + JointAnimCompressedHDR hdr; + u32 offset_64; + u32 offset_32; + u32 offset_16; + u32 reserved; + math::Vector4f data[133]; + int num_data_qw_used = 0; + bool mat[2] = {false, false}; + math::Matrix4f mats[2] = {math::Matrix4f::zero(), math::Matrix4f::zero()}; + u64 data64_size; + u32 data32_size; + u16 data16_size; + std::vector data64; + std::vector data32; + std::vector data16; + + JointAnimCompressedFixed() { + offset_64 = 0; + offset_32 = 0; + offset_16 = 0; + reserved = 0; + data[0] = math::Vector4f(1.0f, 0.0f, 0.0f, 0.0f); + data[1] = math::Vector4f(0.0f, 1.0f, 0.0f, 0.0f); + data[2] = math::Vector4f(0.0f, 0.0f, 1.0f, 0.0f); + data[3] = math::Vector4f(0.0f, 0.0f, 0.0f, 1.0f); + data[4] = math::Vector4f(1.0f, 0.0f, 0.0f, 0.0f); + 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; + } +}; + +struct JointAnimCompressedFrame { + u32 offset_64; + u32 offset_32; + u32 offset_16; + u32 reserved; + // math::Vector4f data[133]; + u32 num_data_qw_used = 0; + bool mat[2] = {false, false}; + math::Matrix4f mats[2] = {math::Matrix4f::zero(), math::Matrix4f::zero()}; + u64 data64_size; + u32 data32_size; + u16 data16_size; + std::vector data64; + std::vector data32; + std::vector data16; + + JointAnimCompressedFrame() { + offset_64 = 0; + offset_32 = 0; + offset_16 = 0; + reserved = 0; + } +}; + +struct JointAnimCompressedControl { + u32 num_frames; + u32 fixed_qwc; + u32 frame_qwc; + JointAnimCompressedFixed fixed; + std::vector frame; +}; + +struct ArtJointAnim { + std::string name; + float speed; + float artist_base; + float artist_step; + s16 length; + JointAnimCompressedControl frames; +}; + /*! * Data extracted from art groups that is not needed for .FR3, but is potentially needed for other * stuff (skeleton export). @@ -24,6 +154,7 @@ struct ArtData { std::string art_group_name; std::string art_name; std::vector joint_group; + std::vector anims; }; } // namespace level_tools \ No newline at end of file diff --git a/decompiler/level_extractor/extract_anim.cpp b/decompiler/level_extractor/extract_anim.cpp new file mode 100644 index 0000000000..da4c0621ed --- /dev/null +++ b/decompiler/level_extractor/extract_anim.cpp @@ -0,0 +1,267 @@ +#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 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 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& 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(ref, "speed", dts); + ja.artist_base = read_plain_data_field(ref, "artist-base", dts); + ja.artist_step = read_plain_data_field(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 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 diff --git a/decompiler/level_extractor/extract_anim.h b/decompiler/level_extractor/extract_anim.h new file mode 100644 index 0000000000..5678313dd2 --- /dev/null +++ b/decompiler/level_extractor/extract_anim.h @@ -0,0 +1,13 @@ +#pragma once +#include "common/custom_data/Tfrag3Data.h" + +#include "decompiler/ObjectFile/ObjectFileDB.h" +#include "decompiler/level_extractor/common_formats.h" +#include "goalc/build_actor/common/build_actor.h" + +namespace decompiler { +void extract_animations(const ObjectFileData& ag_data, + const DecompilerTypeSystem& dts, + GameVersion version, + std::map& out); +} // namespace decompiler \ No newline at end of file diff --git a/decompiler/level_extractor/extract_level.cpp b/decompiler/level_extractor/extract_level.cpp index 7471920c88..8af1ab0ee4 100644 --- a/decompiler/level_extractor/extract_level.cpp +++ b/decompiler/level_extractor/extract_level.cpp @@ -3,6 +3,8 @@ #include #include +#include "extract_anim.h" + #include "common/log/log.h" #include "common/util/FileUtil.h" #include "common/util/SimpleThreadGroup.h" @@ -129,6 +131,7 @@ void extract_art_groups_from_level(const ObjectFileDB& db, extract_merc(ag_file, tex_db, db.dts, tex_remap, level_data, false, db.version(), swapped_info); extract_joint_group(ag_file, db.dts, db.version(), art_group_data); + extract_animations(ag_file, db.dts, db.version(), art_group_data); } } } diff --git a/decompiler/level_extractor/fr3_to_gltf.cpp b/decompiler/level_extractor/fr3_to_gltf.cpp index 325dfb31a4..7277d396db 100644 --- a/decompiler/level_extractor/fr3_to_gltf.cpp +++ b/decompiler/level_extractor/fr3_to_gltf.cpp @@ -1,5 +1,6 @@ #include "fr3_to_gltf.h" +#include #include #include "common/custom_data/Tfrag3Data.h" @@ -879,6 +880,276 @@ int make_inv_matrix_bind_poses(const std::vector& joints, return accessor_idx; } +level_tools::UncompressedJointAnim decompress_anim(const level_tools::ArtJointAnim& art_anim) { + constexpr float kQuatScale = 0.000030517578125f; + constexpr float kScaleScale = 0.000244140625f; + constexpr float kTransScale = 4.f / 4096.f; + + auto read_f32 = [](const u8*& ptr) -> float { + float v; + memcpy(&v, ptr, 4); + ptr += 4; + return v; + }; + auto read_s16 = [](const u8*& ptr) -> float { + s16 v; + memcpy(&v, ptr, 2); + ptr += 2; + return v; + }; + + const auto& ctrl = art_anim.frames; + const auto& fixed = ctrl.fixed; + const auto& hdr = fixed.hdr; + int num_joints = (int)hdr.num_joints; + int total_frames = (int)ctrl.num_frames; + + level_tools::UncompressedJointAnim out; + out.name = art_anim.name; + out.framerate = art_anim.speed > 0.f ? art_anim.speed * 60.f : 30.f; + out.frames = total_frames; + out.joints.resize(2 + num_joints); + + auto d64 = (const u8*)fixed.data64.data(); + auto d32 = (const u8*)fixed.data32.data(); + auto d16 = (const u8*)fixed.data16.data(); + + if (fixed.mat[0]) + d64 += 64; + if (fixed.mat[1]) + d64 += 64; + + for (int tqi = 0; tqi < num_joints; tqi++) { + int ctrl_idx = tqi / 8; + int ctrl_shift = 4 * (tqi % 8); + int c = 0xf & (hdr.control_bits[ctrl_idx] >> ctrl_shift); + auto& joint = out.joints[2 + tqi]; + + if (!(c & 0b0001)) { + math::Vector3f t; + if (c & 0b1000) { + t.x() = read_f32(d64) / 4096.f; + t.y() = read_f32(d64) / 4096.f; + t.z() = read_f32(d32) / 4096.f; + } else { + t.x() = read_s16(d32) * kTransScale; + t.y() = read_s16(d32) * kTransScale; + t.z() = read_s16(d16) * kTransScale; + } + joint.trans_frames.push_back(t); + } + + if (!(c & 0b0010)) { + math::Vector4f q; + q.x() = read_s16(d64) * kQuatScale; + q.y() = read_s16(d64) * kQuatScale; + q.z() = read_s16(d64) * kQuatScale; + q.w() = read_s16(d64) * kQuatScale; + joint.quat_frames.push_back(q); + } + + if (!(c & 0b0100)) { + math::Vector3f s; + s.x() = read_s16(d32) * kScaleScale; + s.y() = read_s16(d32) * kScaleScale; + s.z() = read_s16(d16) * kScaleScale; + joint.scale_frames.push_back(s); + } + } + + for (int fi = 0; fi < total_frames; fi++) { + const auto& frame = ctrl.frame[fi]; + const u8* data64 = (const u8*)frame.data64.data(); + const u8* data32 = (const u8*)frame.data32.data(); + const u8* data16 = (const u8*)frame.data16.data(); + + if (!fixed.mat[0]) + data64 += sizeof(math::Matrix4f); + if (!fixed.mat[1]) + data64 += sizeof(math::Matrix4f); + + for (int tqi = 0; tqi < num_joints; tqi++) { + int ctrl_idx = tqi / 8; + int ctrl_shift = 4 * (tqi % 8); + int c = 0xf & (hdr.control_bits[ctrl_idx] >> ctrl_shift); + auto& joint = out.joints[2 + tqi]; + + if (c & 0b0001) { + math::Vector3f t; + if (c & 0b1000) { + t.x() = read_f32(data64) / 4096.f; + t.y() = read_f32(data64) / 4096.f; + t.z() = read_f32(data32) / 4096.f; + } else { + t.x() = read_s16(data32) * kTransScale; + t.y() = read_s16(data32) * kTransScale; + t.z() = read_s16(data16) * kTransScale; + } + joint.trans_frames.push_back(t); + } + + if (c & 0b0010) { + math::Vector4f q; + q.x() = read_s16(data64) * kQuatScale; + q.y() = read_s16(data64) * kQuatScale; + q.z() = read_s16(data64) * kQuatScale; + q.w() = read_s16(data64) * kQuatScale; + joint.quat_frames.push_back(q); + } + + if (c & 0b0100) { + math::Vector3f s; + s.x() = read_s16(data32) * kScaleScale; + s.y() = read_s16(data32) * kScaleScale; + s.z() = read_s16(data16) * kScaleScale; + joint.scale_frames.push_back(s); + } + } + } + + for (int ji = 2; ji < (int)out.joints.size(); ji++) { + auto& joint = out.joints[ji]; + while ((int)joint.trans_frames.size() < total_frames) { + if (joint.trans_frames.empty()) + joint.trans_frames.emplace_back(0.f, 0.f, 0.f); + else + joint.trans_frames.push_back(joint.trans_frames[0]); + } + while ((int)joint.quat_frames.size() < total_frames) { + if (joint.quat_frames.empty()) + joint.quat_frames.emplace_back(0.f, 0.f, 0.f, 1.f); + else + joint.quat_frames.push_back(joint.quat_frames[0]); + } + while ((int)joint.scale_frames.size() < total_frames) { + if (joint.scale_frames.empty()) + joint.scale_frames.emplace_back(1.f, 1.f, 1.f); + else + joint.scale_frames.push_back(joint.scale_frames[0]); + } + } + + return out; +} + +int make_anim_float_accessor(const std::vector& values, tinygltf::Model& model) { + int buf_idx = (int)model.buffers.size(); + auto& buf = model.buffers.emplace_back(); + buf.data.resize(values.size() * sizeof(float)); + memcpy(buf.data.data(), values.data(), buf.data.size()); + + int bv_idx = (int)model.bufferViews.size(); + auto& bv = model.bufferViews.emplace_back(); + bv.buffer = buf_idx; + bv.byteOffset = 0; + bv.byteLength = buf.data.size(); + + int acc_idx = (int)model.accessors.size(); + auto& acc = model.accessors.emplace_back(); + acc.bufferView = bv_idx; + acc.byteOffset = 0; + acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + acc.count = (int)values.size(); + acc.type = TINYGLTF_TYPE_SCALAR; + if (!values.empty()) { + float mn = values[0], mx = values[0]; + for (float v : values) { + mn = std::min(mn, v); + mx = std::max(mx, v); + } + acc.minValues = {(double)mn}; + acc.maxValues = {(double)mx}; + } + return acc_idx; +} + +int make_anim_vec3_accessor(const std::vector& values, tinygltf::Model& model) { + static_assert(sizeof(math::Vector3f) == 3 * sizeof(float)); + int buf_idx = (int)model.buffers.size(); + auto& buf = model.buffers.emplace_back(); + buf.data.resize(values.size() * sizeof(math::Vector3f)); + memcpy(buf.data.data(), values.data(), buf.data.size()); + + int bv_idx = (int)model.bufferViews.size(); + auto& bv = model.bufferViews.emplace_back(); + bv.buffer = buf_idx; + bv.byteOffset = 0; + bv.byteLength = buf.data.size(); + + int acc_idx = (int)model.accessors.size(); + auto& acc = model.accessors.emplace_back(); + acc.bufferView = bv_idx; + acc.byteOffset = 0; + acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + acc.count = (int)values.size(); + acc.type = TINYGLTF_TYPE_VEC3; + return acc_idx; +} + +int make_anim_vec4_accessor(const std::vector& values, tinygltf::Model& model) { + static_assert(sizeof(math::Vector4f) == 4 * sizeof(float)); + int buf_idx = (int)model.buffers.size(); + auto& buf = model.buffers.emplace_back(); + buf.data.resize(values.size() * sizeof(math::Vector4f)); + memcpy(buf.data.data(), values.data(), buf.data.size()); + + int bv_idx = (int)model.bufferViews.size(); + auto& bv = model.bufferViews.emplace_back(); + bv.buffer = buf_idx; + bv.byteOffset = 0; + bv.byteLength = buf.data.size(); + + int acc_idx = (int)model.accessors.size(); + auto& acc = model.accessors.emplace_back(); + acc.bufferView = bv_idx; + acc.byteOffset = 0; + acc.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + acc.count = (int)values.size(); + acc.type = TINYGLTF_TYPE_VEC4; + return acc_idx; +} + +void add_animation_to_gltf(const level_tools::UncompressedJointAnim& anim, + const tinygltf::Skin& skin, + tinygltf::Model& model) { + if (anim.frames == 0 || anim.joints.size() <= 2) + return; + + auto& gltf_anim = model.animations.emplace_back(); + gltf_anim.name = anim.name; + + std::vector times(anim.frames); + for (int i = 0; i < anim.frames; i++) + times[i] = i / anim.framerate; + int time_acc = make_anim_float_accessor(times, model); + + int n_anim_joints = (int)anim.joints.size(); + int n_skin_joints = (int)skin.joints.size(); + for (int ji = 2; ji < n_anim_joints && ji < n_skin_joints; ji++) { + const auto& joint = anim.joints[ji]; + int target_node = skin.joints[ji]; + + auto add_channel = [&](int val_acc, const std::string& path) { + int si = (int)gltf_anim.samplers.size(); + auto& sampler = gltf_anim.samplers.emplace_back(); + sampler.input = time_acc; + sampler.output = val_acc; + sampler.interpolation = "LINEAR"; + auto& channel = gltf_anim.channels.emplace_back(); + channel.sampler = si; + channel.target_node = target_node; + channel.target_path = path; + }; + + if ((int)joint.trans_frames.size() == anim.frames) + add_channel(make_anim_vec3_accessor(joint.trans_frames, model), "translation"); + if ((int)joint.quat_frames.size() == anim.frames) + add_channel(make_anim_vec4_accessor(joint.quat_frames, model), "rotation"); + if ((int)joint.scale_frames.size() == anim.frames) + add_channel(make_anim_vec3_accessor(joint.scale_frames, model), "scale"); + } +} + void add_merc(const tfrag3::Level& level, const std::map& art_data, const tfrag3::MercModel& mmodel, @@ -956,6 +1227,15 @@ void add_merc(const tfrag3::Level& level, skin.inverseBindMatrices = make_inv_matrix_bind_poses(game_bones, model); } + if (art != art_data.end() && !art->second.anims.empty() && node.skin >= 0 && + node.skin < model.skins.size()) { + const auto& skin = model.skins[node.skin]; + for (const auto& ja : art->second.anims) { + auto uncompressed = decompress_anim(ja); + add_animation_to_gltf(uncompressed, skin, model); + } + } + for (size_t effect_idx = 0; effect_idx < mmodel.effects.size(); effect_idx++) { const auto& effect = mmodel.effects[effect_idx]; for (size_t draw_idx = 0; draw_idx < effect.all_draws.size(); draw_idx++) { diff --git a/goal_src/jak1/game.gp b/goal_src/jak1/game.gp index f74ab13e24..61100d9195 100644 --- a/goal_src/jak1/game.gp +++ b/goal_src/jak1/game.gp @@ -225,9 +225,9 @@ :tool 'build-level :out '(,(string-append "$OUT/obj/" name ".go"))))) -(defmacro build-actor (name &key (gen-mesh #f) &key (force-run #f) &key (texture-bucket 0)) +(defmacro build-actor (name &key (gen-mesh #f) &key (force-run #f) &key (texture-bucket 0) &key (framerate 60.0) &key (master-art-group #f) &key (master-ag-map ()) &key (joint-channel 6)) (let* ((path (string-append "custom_assets/jak1/models/custom_levels/" name ".glb"))) - `(defstep :in '(,path ,(symbol->string gen-mesh) ,(symbol->string force-run) ,(if (integer? texture-bucket) (int->string texture-bucket) (symbol->string texture-bucket))) + `(defstep :in '(,path ,gen-mesh ,force-run ,texture-bucket ,framerate ,master-art-group ,master-ag-map ,joint-channel) :tool 'build-actor :out '(,(string-append "$OUT/obj/" name "-ag.go"))))) @@ -1653,13 +1653,30 @@ ;; Set up the build system to build the level geometry ;; this path is relative to the custom_assets/jak1/levels/ folder ;; it should point to the .jsonc file that specifies the level. +;; options: +;; - force-run: when #t, always forces a rebuild of the level instead of checking the "last modified" timestamp. +;; - gen-fr3: when #f, does not generate the .fr3 file for the level. useful for temporarily skipping the slow fr3 build process when +;; there's many custom levels that include extra art groups. (build-custom-level "test-zone") ;; the DGO file (custom-level-cgo "TSZ.DGO" "test-zone/testzone.gd") ;; generate the art group for a custom actor. ;; requires a .glb model file in custom_assets/jak1/models/custom_levels -;; to also generate a collide-mesh, add :gen-mesh #t +;; options: +;; - gen-mesh: when #t, generates a collision mesh for this actor. +;; - force-run: when #t, always forces a rebuild of the actor instead of checking the "last modified" timestamp. +;; - texture-bucket: sets the "texture-level" of the actor, which determines the level bucket to draw the actor in, affecting +;; the draw order. default is 0, which sets texture-level 0. if set to #f, no "texture-level" lump will be added to the +;; art group, defaulting to level 1. for example, actors that are in texture-level 1 will be drawn after shadows, meaning that +;; no shadows will be cast on them. the default value of 0 avoids this. +;; - framerate: set the framerate for custom animations, if available. defaults to 60. +;; - master-art-group: the name of the art group to link custom animations to. this can be used to +;; give existing art groups custom animations if there are enough empty slots left over. +;; - master-ag-map: a list of pairs of (anim-name master-art-group-idx), linking the animation with the given name +;; to the given index in the master art group. +;; - joint-channel: how many joint channels the actor should have. defaults to 6. +;; more complicated actors like jak that make a lot of use of animation blending can have 24+ channels. (build-actor "test-actor" :gen-mesh #t) ;;;;;;;;;;;;;;;;;;;;; diff --git a/goal_src/jak2/game.gp b/goal_src/jak2/game.gp index 7af246a2fb..ee90868352 100644 --- a/goal_src/jak2/game.gp +++ b/goal_src/jak2/game.gp @@ -299,6 +299,10 @@ ;; Set up the build system to build the level geometry ;; this path is relative to the custom_assets/jak2/levels folder ;; it should point to the .jsonc file that specifies the level. +;; options: +;; - force-run: when #t, always forces a rebuild of the level instead of checking the "last modified" timestamp. +;; - gen-fr3: when #f, does not generate the .fr3 file for the level. useful for temporarily skipping the slow fr3 build process when +;; there's many custom levels that include extra art groups. (build-custom-level "test-zone") ;; the DGO file (goal-src "levels/test-zone/test-zone-obs.gc" "process-focusable") @@ -306,7 +310,19 @@ ;; generate the art group for a custom actor. ;; requires a .glb model file in custom_assets/jak1/models/custom_levels -;; to also generate a collide-mesh, add :gen-mesh #t +;; options: +;; - gen-mesh: when #t, generates a collision mesh for this actor. +;; - force-run: when #t, always forces a rebuild of the actor instead of checking the "last modified" timestamp. +;; - texture-bucket: sets the "texture-level" of the actor, which determines the level bucket to draw the actor in, affecting +;; the draw order. default is 0, which sets texture-level 0. if set to #f, no "texture-level" lump will be added to the +;; art group, defaulting to level 1. +;; - framerate: set the framerate for custom animations, if available. defaults to 60. +;; - master-art-group: the name of the art group to link custom animations to. this can be used to +;; give existing art groups custom animations if there are enough empty slots left over. +;; - master-ag-map: a list of pairs of (anim-name master-art-group-idx), linking the animation with the given name +;; to the given index in the master art group. +;; - joint-channel: how many joint channels the actor should have. defaults to 6. +;; more complicated actors like jak that make a lot of use of animation blending can have 24+ channels. (build-actor "test-actor" :force-run #t :gen-mesh #t) ;;;;;;;;;;;;;;;;;;;;; diff --git a/goal_src/jak2/lib/project-lib.gp b/goal_src/jak2/lib/project-lib.gp index cd90d23fd9..36e2a81115 100644 --- a/goal_src/jak2/lib/project-lib.gp +++ b/goal_src/jak2/lib/project-lib.gp @@ -141,9 +141,9 @@ :tool 'build-level2 :out '(,(string-append "$OUT/obj/" name ".go"))))) -(defmacro build-actor (name &key (gen-mesh #f) &key (force-run #f) &key (texture-bucket 0)) +(defmacro build-actor (name &key (gen-mesh #f) &key (force-run #f) &key (texture-bucket 0) &key (framerate 60.0) &key (master-art-group #f) &key (master-ag-map ()) &key (joint-channel 6)) (let* ((path (string-append "custom_assets/jak2/models/custom_levels/" name ".glb"))) - `(defstep :in '(,path ,(symbol->string gen-mesh) ,(symbol->string force-run) ,(if (integer? texture-bucket) (int->string texture-bucket) (symbol->string texture-bucket))) + `(defstep :in '(,path ,gen-mesh ,force-run ,texture-bucket ,framerate ,master-art-group ,master-ag-map ,joint-channel) :tool 'build-actor2 :out '(,(string-append "$OUT/obj/" name "-ag.go"))))) diff --git a/goal_src/jak3/game.gp b/goal_src/jak3/game.gp index 23ac6c734c..abc118c0c6 100644 --- a/goal_src/jak3/game.gp +++ b/goal_src/jak3/game.gp @@ -402,6 +402,9 @@ ;; Set up the build system to build the level geometry ;; this path is relative to the custom_levels/jak3 folder ;; it should point to the .jsonc file that specifies the level. +;; - force-run: when #t, always forces a rebuild of the level instead of checking the "last modified" timestamp. +;; - gen-fr3: when #f, does not generate the .fr3 file for the level. useful for temporarily skipping the slow fr3 build process when +;; there's many custom levels that include extra art groups. (build-custom-level "test-zone") ;; the DGO file (goal-src "levels/test-zone/test-zone-obs.gc" "process-focusable") @@ -409,7 +412,19 @@ ;; generate the art group for a custom actor. ;; requires a .glb model file in custom_assets/jak3/models/custom_levels -;; to also generate a collide-mesh, add :gen-mesh #t +;; options: +;; - gen-mesh: when #t, generates a collision mesh for this actor. +;; - force-run: when #t, always forces a rebuild of the actor instead of checking the "last modified" timestamp. +;; - texture-bucket: sets the "texture-level" of the actor, which determines the level bucket to draw the actor in, affecting +;; the draw order. default is 0, which sets texture-level 0. if set to #f, no "texture-level" lump will be added to the +;; art group, defaulting to level 1. +;; - framerate: set the framerate for custom animations, if available. defaults to 60. +;; - master-art-group: the name of the art group to link custom animations to. this can be used to +;; give existing art groups custom animations if there are enough empty slots left over. +;; - master-ag-map: a list of pairs of (anim-name master-art-group-idx), linking the animation with the given name +;; to the given index in the master art group. +;; - joint-channel: how many joint channels the actor should have. defaults to 6. +;; more complicated actors like jak that make a lot of use of animation blending can have 24+ channels. (build-actor "test-actor" :gen-mesh #t) ;;;;;;;;;;;;;;;;;;;;; diff --git a/goal_src/jak3/lib/project-lib.gp b/goal_src/jak3/lib/project-lib.gp index 6776f22bed..2f883ec1d4 100644 --- a/goal_src/jak3/lib/project-lib.gp +++ b/goal_src/jak3/lib/project-lib.gp @@ -256,8 +256,8 @@ :tool 'build-level3 :out '(,(string-append "$OUT/obj/" name ".go"))))) -(defmacro build-actor (name &key (gen-mesh #f) &key (force-run #f) &key (texture-bucket 0)) +(defmacro build-actor (name &key (gen-mesh #f) &key (force-run #f) &key (texture-bucket 0) &key (framerate 60.0) &key (master-art-group #f) &key (master-ag-map ()) &key (joint-channel 6)) (let* ((path (string-append "custom_assets/jak3/models/custom_levels/" name ".glb"))) - `(defstep :in '(,path ,(symbol->string gen-mesh) ,(symbol->string force-run) ,(if (integer? texture-bucket) (int->string texture-bucket) (symbol->string texture-bucket))) + `(defstep :in '(,path ,gen-mesh ,force-run ,texture-bucket ,framerate ,master-art-group ,master-ag-map ,joint-channel) :tool 'build-actor3 :out '(,(string-append "$OUT/obj/" name "-ag.go"))))) \ No newline at end of file diff --git a/goalc/build_actor/common/animation_processing.cpp b/goalc/build_actor/common/animation_processing.cpp index 1153c101c9..bbcb04694e 100644 --- a/goalc/build_actor/common/animation_processing.cpp +++ b/goalc/build_actor/common/animation_processing.cpp @@ -68,6 +68,8 @@ std::vector> extract_keyframed_gltf_vecn( UncompressedJointAnim extract_anim_from_gltf(const tinygltf::Model& model, const tinygltf::Animation& anim, const std::map& node_to_joint, + const std::string& master_art_group, + int master_art_group_index, float framerate) { UncompressedJointAnim out; out.name = anim.name; @@ -75,6 +77,8 @@ UncompressedJointAnim extract_anim_from_gltf(const tinygltf::Model& model, const int max_joint = find_max_joint(anim, node_to_joint); lg::info("Max joint is {}", max_joint); out.joints.resize(max_joint + 1); + out.master_art_group_name = master_art_group; + out.master_art_group_index = master_art_group_index; for (const auto& channel : anim.channels) { const int channel_node_idx = channel.target_node; @@ -238,6 +242,8 @@ CompressedAnim compress_animation(const UncompressedJointAnim& in) { out.name = in.name; out.framerate = in.framerate; out.frames.resize(in.frames); + out.master_art_group_name = in.master_art_group_name; + out.master_art_group_index = in.master_art_group_index; for (int matrix = 0; matrix < 2; matrix++) { const auto& joint_data = in.joints.at(matrix); if (is_matrix_constant(joint_data)) { diff --git a/goalc/build_actor/common/animation_processing.h b/goalc/build_actor/common/animation_processing.h index 9d70e1df4e..6145861687 100644 --- a/goalc/build_actor/common/animation_processing.h +++ b/goalc/build_actor/common/animation_processing.h @@ -18,6 +18,8 @@ struct UncompressedSingleJointAnim { struct UncompressedJointAnim { std::string name; + std::string master_art_group_name; + int master_art_group_index = -1; std::vector joints; float framerate = 60; int frames = 0; @@ -44,6 +46,8 @@ struct CompressedJointMetadata { struct CompressedAnim { std::string name; + std::string master_art_group_name; + int master_art_group_index = -1; CompressedFrame fixed; std::vector frames; bool matrix_animated[2] = {false, false}; @@ -56,12 +60,17 @@ struct CompressedAnim { * @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 master_art_group The master art group to link this animation to, if set. + * @param master_art_group_index The index of the slot in the master art group to link this + * animation to. * @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, + const std::string& master_art_group, + int master_art_group_index, float framerate); CompressedAnim compress_animation(const UncompressedJointAnim& in); diff --git a/goalc/build_actor/common/build_actor.cpp b/goalc/build_actor/common/build_actor.cpp index 8aea4cba4b..26f4e18e2c 100644 --- a/goalc/build_actor/common/build_actor.cpp +++ b/goalc/build_actor/common/build_actor.cpp @@ -150,7 +150,10 @@ std::vector convert_joints(const std::vector& gjoints) { } std::vector process_anim(const tinygltf::Model& model, - const std::vector& gjoints) { + const std::vector& gjoints, + const std::string& master_art_group, + const std::map& master_ag_map, + float framerate) { if (model.animations.empty()) { lg::warn("no animations detected!"); // TODO: make up a dummy one return {}; @@ -164,8 +167,12 @@ std::vector process_anim(const tinygltf::Model& model, 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))); + 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; } \ No newline at end of file diff --git a/goalc/build_actor/common/build_actor.h b/goalc/build_actor/common/build_actor.h index 40f993098b..c8b5fc0dc4 100644 --- a/goalc/build_actor/common/build_actor.h +++ b/goalc/build_actor/common/build_actor.h @@ -31,6 +31,9 @@ struct BuildActorParams { s32 joint_channel = -1; math::Vector4f trans_offset = {0.f, 0.f, 0.f, 1.f}; std::vector lod_dist{}; + std::string master_art_group; + std::map master_ag_map; // ja name -> master ag slot + float framerate = 60; }; struct GltfJoint { @@ -45,4 +48,7 @@ tinygltf::Model load_gltf_model(const fs::path& path); std::vector extract_skeleton(const tinygltf::Model& model, int skin_idx); std::vector convert_joints(const std::vector& gjoints); std::vector process_anim(const tinygltf::Model& model, - const std::vector& gjoints); \ No newline at end of file + const std::vector& gjoints, + const std::string& master_art_group, + const std::map& master_ag_map, + float framerate); \ 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 2462e28bcc..54af30cca7 100644 --- a/goalc/build_actor/jak1/build_actor.cpp +++ b/goalc/build_actor/jak1/build_actor.cpp @@ -131,8 +131,16 @@ ArtJointAnim::ArtJointAnim(const anim::CompressedAnim& anim, const std::vector lod_dist{}; -}; +struct BuildActorParams1 : BuildActorParams {}; struct ArtJointGeo : ArtElement { std::vector data; @@ -161,7 +154,7 @@ struct ArtJointGeo : ArtElement { explicit ArtJointGeo(const std::string& name, std::vector cmeshes, std::vector& joints, - const BuildActorParams& params) { + const BuildActorParams1& params) { this->name = name + "-lod0"; length = joints.size(); for (auto& joint : joints) { @@ -230,5 +223,5 @@ struct ArtGroup : Art { bool run_build_actor(const std::string& input_model, const std::string& output_file, - const BuildActorParams& params); + const BuildActorParams1& params); } // namespace jak1 diff --git a/goalc/build_actor/jak2/build_actor.cpp b/goalc/build_actor/jak2/build_actor.cpp index 90fc7fb65a..484bb6e191 100644 --- a/goalc/build_actor/jak2/build_actor.cpp +++ b/goalc/build_actor/jak2/build_actor.cpp @@ -131,8 +131,16 @@ ArtJointAnim::ArtJointAnim(const anim::CompressedAnim& anim, const std::vector lod_dist{}; -}; +struct BuildActorParams2 : BuildActorParams {}; struct ArtJointGeo : ArtElement { std::vector data; @@ -143,7 +136,7 @@ struct ArtJointGeo : ArtElement { explicit ArtJointGeo(const std::string& name, std::vector cmeshes, std::vector& joints, - const BuildActorParams& params) { + const BuildActorParams2& params) { this->name = name + "-lod0"; length = joints.size(); for (auto& joint : joints) { @@ -211,5 +204,5 @@ struct ArtGroup : Art { bool run_build_actor(const std::string& input_model, const std::string& output_file, - const BuildActorParams& params); + const BuildActorParams2& params); } // namespace jak2 diff --git a/goalc/build_actor/jak3/build_actor.cpp b/goalc/build_actor/jak3/build_actor.cpp index ac0396d666..d7b0891a70 100644 --- a/goalc/build_actor/jak3/build_actor.cpp +++ b/goalc/build_actor/jak3/build_actor.cpp @@ -131,8 +131,16 @@ ArtJointAnim::ArtJointAnim(const anim::CompressedAnim& anim, const std::vector lod_dist{}; -}; +struct BuildActorParams3 : BuildActorParams {}; struct ArtJointGeo : ArtElement3 { std::vector data; @@ -149,7 +142,7 @@ struct ArtJointGeo : ArtElement3 { explicit ArtJointGeo(const std::string& name, std::vector cmeshes, std::vector& joints, - const BuildActorParams& params) { + const BuildActorParams3& params) { this->name = name + "-lod0"; master_art_group_name = name; master_art_group_index = 0; @@ -214,5 +207,5 @@ struct ArtGroup : Art { bool run_build_actor(const std::string& input_model, const std::string& output_file, - const BuildActorParams& params); + const BuildActorParams3& params); } // namespace jak3 diff --git a/goalc/build_actor/main.cpp b/goalc/build_actor/main.cpp index 842675268c..d6bc09f750 100644 --- a/goalc/build_actor/main.cpp +++ b/goalc/build_actor/main.cpp @@ -64,7 +64,7 @@ int main(int argc, char** argv) { switch (game_version) { case GameVersion::Jak1: { - jak1::BuildActorParams params; + jak1::BuildActorParams1 params; params.gen_collide_mesh = gen_collide_mesh; params.texture_bucket = texture_bucket; params.texture_level = texture_level; @@ -72,7 +72,7 @@ int main(int argc, char** argv) { jak1::run_build_actor(mdl_name, output_file, params); } break; case GameVersion::Jak2: { - jak2::BuildActorParams params; + jak2::BuildActorParams2 params; params.gen_collide_mesh = gen_collide_mesh; params.texture_bucket = texture_bucket; params.texture_level = texture_level; @@ -80,7 +80,7 @@ int main(int argc, char** argv) { jak2::run_build_actor(mdl_name, output_file, params); } break; case GameVersion::Jak3: { - jak3::BuildActorParams params; + jak3::BuildActorParams3 params; params.gen_collide_mesh = gen_collide_mesh; params.texture_bucket = texture_bucket; params.texture_level = texture_level; diff --git a/goalc/make/MakeSystem.cpp b/goalc/make/MakeSystem.cpp index 152cfdc722..551bc45307 100644 --- a/goalc/make/MakeSystem.cpp +++ b/goalc/make/MakeSystem.cpp @@ -160,7 +160,11 @@ goos::Object MakeSystem::handle_defstep(const goos::Object& form, if (in.is_pair()) { step->input.clear(); goos::for_each_in_list(in, [&](const goos::Object& o) { - step->input.push_back(m_path_map.apply_remaps(o.as_string()->data)); + if (o.type == goos::ObjectType::STRING) { + step->input.push_back(m_path_map.apply_remaps(o.as_string()->data)); + } else { + step->input.push_back(m_path_map.apply_remaps(o.print())); + } }); } else { step->input = {m_path_map.apply_remaps(in.as_string()->data)}; diff --git a/goalc/make/Tools.cpp b/goalc/make/Tools.cpp index 8ba8c29e17..268ff08f9f 100644 --- a/goalc/make/Tools.cpp +++ b/goalc/make/Tools.cpp @@ -313,89 +313,147 @@ bool BuildLevel3Tool::run(const ToolInput& task, const PathMap& path_map) { BuildActorTool::BuildActorTool() : Tool("build-actor") {} bool BuildActorTool::needs_run(const ToolInput& task, const PathMap& path_map) { - if (task.input.size() > 4) { + if (task.input.size() > 8) { throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); } auto rerun = task.input.at(2) == "#t"; - std::vector deps{task.input.at(0)}; + std::vector deps{task.input.at(0)}; return rerun || Tool::needs_run({deps, deps, task.output, task.arg}, path_map); } bool BuildActorTool::run(const ToolInput& task, const PathMap& path_map) { (void)path_map; - if (task.input.size() > 4) { + if (task.input.size() > 8) { throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); } - jak1::BuildActorParams params; + jak1::BuildActorParams1 params; params.gen_collide_mesh = task.input.at(1) == "#t"; if (task.input.at(3) == "#f") { params.texture_bucket = -1; } else { try { - params.texture_bucket = static_cast(std::stoi(task.input.at(3))); + params.texture_bucket = static_cast(std::stoi(task.input.at(3))); } catch (std::invalid_argument&) { throw std::runtime_error("[build-actor] texture-bucket must be #f or a valid integer."); } } + params.framerate = std::stof(task.input.at(4)); + if (task.input.at(5) != "#f") { + params.master_art_group = task.input.at(5); + } + auto master_ag_list = m_reader.read_from_string(task.input.at(6)); + // e.g. ((jakb-board-stance 180) (jakb-board-airwalk 181)) + if (!master_ag_list.as_pair()->cdr.is_empty_list()) { + std::map master_ag_map; + goos::for_each_in_list(master_ag_list.as_pair()->cdr.as_pair()->car, + [&](const goos::Object& o) { + auto map = o.as_pair(); + auto ja = std::string(map->car.as_symbol().name_ptr); + auto idx = map->cdr.as_pair()->car.as_int(); + master_ag_map.insert({ja, idx}); + }); + params.master_ag_map = master_ag_map; + } + if (task.input.at(7) != "6") { + params.joint_channel = std::stoi(task.input.at(7)); + } return jak1::run_build_actor(task.input.at(0), task.output.at(0), params); } BuildActor2Tool::BuildActor2Tool() : Tool("build-actor2") {} bool BuildActor2Tool::needs_run(const ToolInput& task, const PathMap& path_map) { - if (task.input.size() > 4) { + if (task.input.size() > 8) { throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); } auto rerun = task.input.at(2) == "#t"; - std::vector deps{task.input.at(0)}; + std::vector deps{task.input.at(0)}; return rerun || Tool::needs_run({deps, deps, task.output, task.arg}, path_map); } bool BuildActor2Tool::run(const ToolInput& task, const PathMap& path_map) { (void)path_map; - if (task.input.size() > 4) { + if (task.input.size() > 8) { throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); } - jak2::BuildActorParams params; + jak2::BuildActorParams2 params; params.gen_collide_mesh = task.input.at(1) == "#t"; if (task.input.at(3) == "#f") { params.texture_bucket = -1; } else { try { - params.texture_bucket = static_cast(std::stoi(task.input.at(3))); + params.texture_bucket = static_cast(std::stoi(task.input.at(3))); } catch (std::invalid_argument&) { throw std::runtime_error("[build-actor2] texture-bucket must be #f or a valid integer."); } } + params.framerate = std::stof(task.input.at(4)); + if (task.input.at(5) != "#f") { + params.master_art_group = task.input.at(5); + } + auto master_ag_list = m_reader.read_from_string(task.input.at(6)); + if (!master_ag_list.as_pair()->cdr.is_empty_list()) { + std::map master_ag_map; + goos::for_each_in_list(master_ag_list.as_pair()->cdr.as_pair()->car, + [&](const goos::Object& o) { + auto map = o.as_pair(); + auto ja = std::string(map->car.as_symbol().name_ptr); + auto idx = map->cdr.as_pair()->car.as_int(); + master_ag_map.insert({ja, idx}); + }); + params.master_ag_map = master_ag_map; + } + if (task.input.at(7) != "6") { + params.joint_channel = std::stoi(task.input.at(7)); + } return jak2::run_build_actor(task.input.at(0), task.output.at(0), params); } BuildActor3Tool::BuildActor3Tool() : Tool("build-actor3") {} bool BuildActor3Tool::needs_run(const ToolInput& task, const PathMap& path_map) { - if (task.input.size() > 4) { + if (task.input.size() > 8) { throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); } auto rerun = task.input.at(2) == "#t"; - std::vector deps{task.input.at(0)}; + std::vector deps{task.input.at(0)}; return rerun || Tool::needs_run({deps, deps, task.output, task.arg}, path_map); } bool BuildActor3Tool::run(const ToolInput& task, const PathMap& path_map) { (void)path_map; - if (task.input.size() > 4) { + if (task.input.size() > 8) { throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); } - jak3::BuildActorParams params; + jak3::BuildActorParams3 params; params.gen_collide_mesh = task.input.at(1) == "#t"; if (task.input.at(3) == "#f") { params.texture_bucket = -1; } else { try { - params.texture_bucket = static_cast(std::stoi(task.input.at(3))); + params.texture_bucket = static_cast(std::stoi(task.input.at(3))); } catch (std::invalid_argument&) { throw std::runtime_error("[build-actor3] texture-bucket must be #f or a valid integer."); } } + params.framerate = std::stof(task.input.at(4)); + if (task.input.at(5) != "#f") { + params.master_art_group = task.input.at(5); + } + auto master_ag_list = m_reader.read_from_string(task.input.at(6)); + if (!master_ag_list.as_pair()->cdr.is_empty_list()) { + std::map master_ag_map; + goos::for_each_in_list(master_ag_list.as_pair()->cdr.as_pair()->car, + [&](const goos::Object& o) { + auto map = o.as_pair(); + auto ja = std::string(map->car.as_symbol().name_ptr); + auto idx = map->cdr.as_pair()->car.as_int(); + master_ag_map.insert({ja, idx}); + }); + params.master_ag_map = master_ag_map; + } + if (task.input.at(7) != "6") { + params.joint_channel = std::stoi(task.input.at(7)); + } return jak3::run_build_actor(task.input.at(0), task.output.at(0), params); } \ No newline at end of file diff --git a/goalc/make/Tools.h b/goalc/make/Tools.h index 152007be8a..39a2d0aea0 100644 --- a/goalc/make/Tools.h +++ b/goalc/make/Tools.h @@ -98,6 +98,9 @@ class BuildActorTool : public Tool { BuildActorTool(); bool run(const ToolInput& task, const PathMap& path_map) override; bool needs_run(const ToolInput& task, const PathMap& path_map) override; + + private: + goos::Reader m_reader; }; class BuildActor2Tool : public Tool { @@ -105,6 +108,9 @@ class BuildActor2Tool : public Tool { BuildActor2Tool(); bool run(const ToolInput& task, const PathMap& path_map) override; bool needs_run(const ToolInput& task, const PathMap& path_map) override; + + private: + goos::Reader m_reader; }; class BuildActor3Tool : public Tool { @@ -112,4 +118,7 @@ class BuildActor3Tool : public Tool { BuildActor3Tool(); bool run(const ToolInput& task, const PathMap& path_map) override; bool needs_run(const ToolInput& task, const PathMap& path_map) override; + + private: + goos::Reader m_reader; }; \ No newline at end of file