From edae60d58d2e816993f7de8c16a4f267bbbb1910 Mon Sep 17 00:00:00 2001 From: Hat Kid <6624576+Hat-Kid@users.noreply.github.com> Date: Sun, 21 Jul 2024 01:51:31 +0200 Subject: [PATCH] decompiler: support merc model replacements and adding custom actor models to vanilla fr3s (#3597) This adds support for replacing existing merc models in FR3 files with custom GLB model files. The replacements go in `custom_assets//merc_replacements`, similar to texture replacements. When a `.glb` file with a file name that matches any model present in an FR3 is detected (e.g. `eichar-lod0` for Jak), all merc model data is replaced with the given model. Additionally, models for custom actors can now also be added to vanilla FR3s. The models for this go in `custom_assets//models/` (e.g. `custom_assets/jak1/models/jungleb/test-actor-lod0.glb`) and will be added to the FR3 that has a matching name (exception: to add things to the common level file, the folder should be named `common` instead of `GAME`). For custom levels, these now go in `custom_assets//models/custom_levels` (previously `custom_assets//models`). Another small change: When level ripping is enabled, the resulting model files will now be stored in game name subfolders inside of `glb_out`. --- .gitignore | 5 + .../jak1/levels/test-zone/test-zone.jsonc | 2 +- .../models/{ => custom_levels}/test-actor.glb | Bin decompiler/CMakeLists.txt | 1 + decompiler/level_extractor/extract_level.cpp | 11 +- decompiler/level_extractor/extract_merc.cpp | 84 +++++++ .../level_extractor/merc_replacement.cpp | 211 ++++++++++++++++++ decompiler/level_extractor/merc_replacement.h | 32 +++ .../opengl_renderer/foreground/Merc2.cpp | 7 +- goal_src/jak1/game.gp | 4 +- goalc/build_level/common/build_level.cpp | 12 +- 11 files changed, 354 insertions(+), 15 deletions(-) rename custom_assets/jak1/models/{ => custom_levels}/test-actor.glb (100%) create mode 100644 decompiler/level_extractor/merc_replacement.cpp create mode 100644 decompiler/level_extractor/merc_replacement.h diff --git a/.gitignore b/.gitignore index 8356b23304..620924397f 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,11 @@ custom_assets/jak1/texture_replacements/* custom_assets/jak2/texture_replacements/* custom_assets/jak3/texture_replacements/* +# merc replacements +custom_assets/jak1/merc_replacements/* +custom_assets/jak2/merc_replacements/* +custom_assets/jak3/merc_replacements/* + # generated cmake files svnrev.h common/versions/revision.h diff --git a/custom_assets/jak1/levels/test-zone/test-zone.jsonc b/custom_assets/jak1/levels/test-zone/test-zone.jsonc index 928a3f286b..a5b22cc89b 100644 --- a/custom_assets/jak1/levels/test-zone/test-zone.jsonc +++ b/custom_assets/jak1/levels/test-zone/test-zone.jsonc @@ -62,7 +62,7 @@ // Note: You will still have to add them to your level's .gd file. // "art_groups": ["plat-ag"], - // If you have any custom models in the "custom_assets/jak1/models" folder that you want to use in your level, add them to this list. + // If you have any custom models in the "custom_assets/jak1/models/custom_levels" folder that you want to use in your level, add them to this list. // Note: Like with art groups, these should also be added to your level's .gd file. "custom_models": ["test-actor"], diff --git a/custom_assets/jak1/models/test-actor.glb b/custom_assets/jak1/models/custom_levels/test-actor.glb similarity index 100% rename from custom_assets/jak1/models/test-actor.glb rename to custom_assets/jak1/models/custom_levels/test-actor.glb diff --git a/decompiler/CMakeLists.txt b/decompiler/CMakeLists.txt index 7532391a82..7e6f1403d6 100644 --- a/decompiler/CMakeLists.txt +++ b/decompiler/CMakeLists.txt @@ -68,6 +68,7 @@ add_library( level_extractor/fr3_to_gltf.cpp level_extractor/MercData.cpp level_extractor/tfrag_tie_fixup.cpp + level_extractor/merc_replacement.cpp ObjectFile/LinkedObjectFile.cpp ObjectFile/LinkedObjectFileCreation.cpp diff --git a/decompiler/level_extractor/extract_level.cpp b/decompiler/level_extractor/extract_level.cpp index 42e341e65b..87471662e6 100644 --- a/decompiler/level_extractor/extract_level.cpp +++ b/decompiler/level_extractor/extract_level.cpp @@ -322,7 +322,8 @@ void extract_common(const ObjectFileDB& db, compressed.data(), compressed.size()); if (config.rip_levels) { - auto file_path = file_util::get_jak_project_dir() / "glb_out" / "common.glb"; + auto file_path = file_util::get_jak_project_dir() / "glb_out" / + game_version_names[config.game_version] / "common.glb"; file_util::create_dir_if_needed_for_file(file_path); save_level_foreground_as_gltf(tfrag_level, art_group_data, file_path); } @@ -360,15 +361,17 @@ void extract_from_level(const ObjectFileDB& db, if (config.rip_levels) { auto back_file_path = file_util::get_jak_project_dir() / "glb_out" / - fmt::format("{}_background.glb", level_data.level_name); + game_version_names[config.game_version] / + fmt::format("{}-background.glb", level_data.level_name); file_util::create_dir_if_needed_for_file(back_file_path); save_level_background_as_gltf(level_data, back_file_path); auto fore_file_path = file_util::get_jak_project_dir() / "glb_out" / - fmt::format("{}_foreground.glb", level_data.level_name); + game_version_names[config.game_version] / + fmt::format("{}-foreground.glb", level_data.level_name); file_util::create_dir_if_needed_for_file(fore_file_path); save_level_foreground_as_gltf(level_data, art_group_data, fore_file_path); } - file_util::write_text_file(entities_folder / fmt::format("{}_actors.json", level_data.level_name), + file_util::write_text_file(entities_folder / fmt::format("{}-actors.json", level_data.level_name), extract_actors_to_json(bsp_header.actors)); } diff --git a/decompiler/level_extractor/extract_merc.cpp b/decompiler/level_extractor/extract_merc.cpp index 5f86fb4f2e..2a97ef1782 100644 --- a/decompiler/level_extractor/extract_merc.cpp +++ b/decompiler/level_extractor/extract_merc.cpp @@ -1,5 +1,7 @@ #include "extract_merc.h" +#include "merc_replacement.h" + #include "common/log/log.h" #include "common/util/BitUtils.h" #include "common/util/FileUtil.h" @@ -1597,6 +1599,60 @@ void create_modifiable_vertex_data( } } +void replace_model(tfrag3::Level& lvl, tfrag3::MercModel& model, const fs::path& mdl_path) { + if (model.max_bones < 100) { + auto lvl_name = lvl.level_name == "" ? "common" : lvl.level_name; + lg::info("Replacing {} for {}: {} effects, {} max bones, {} max draws\n", model.name, lvl_name, + model.effects.size(), model.max_bones, model.max_draws); + + std::vector old_verts; + for (auto& e : model.effects) { + for (auto& d : e.all_draws) { + for (size_t i = 0; i < d.index_count; i++) { + auto idx = lvl.merc_data.indices.at(i + d.first_index); + if (idx != UINT32_MAX) { + old_verts.push_back(lvl.merc_data.vertices[idx]); + } + } + } + } + + auto swap_info = load_replacement_merc_model(model.name, lvl.merc_data.indices.size(), + lvl.merc_data.vertices.size(), lvl.textures.size(), + mdl_path.string(), old_verts, false); + model = swap_info.new_model; + size_t old_start = lvl.merc_data.vertices.size(); + for (auto& ind : swap_info.new_indices) { + ASSERT(ind >= old_start); + } + lvl.merc_data.indices.insert(lvl.merc_data.indices.end(), swap_info.new_indices.begin(), + swap_info.new_indices.end()); + lvl.merc_data.vertices.insert(lvl.merc_data.vertices.end(), swap_info.new_vertices.begin(), + swap_info.new_vertices.end()); + lvl.textures.insert(lvl.textures.end(), swap_info.new_textures.begin(), + swap_info.new_textures.end()); + } +} + +void add_custom_model_to_level(tfrag3::Level& lvl, + const std::string& name, + const fs::path& mdl_path) { + auto lvl_name = lvl.level_name == "" ? "common" : lvl.level_name; + lg::info("Adding custom model {} to {}", name, lvl_name); + auto merc_data = + load_replacement_merc_model(name, lvl.merc_data.indices.size(), lvl.merc_data.vertices.size(), + lvl.textures.size(), mdl_path.string(), {}, true); + for (auto& idx : merc_data.new_indices) { + lvl.merc_data.indices.push_back(idx); + } + 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()); +} + /*! * Top-level merc extraction */ @@ -1731,5 +1787,33 @@ void extract_merc(const ObjectFileData& ag_data, } } } + + // do model replacements if present + auto merc_replacement_folder = file_util::get_jak_project_dir() / "custom_assets" / + game_version_names[version] / "merc_replacements"; + if (file_util::file_exists(merc_replacement_folder.string())) { + auto merc_replacements = + file_util::find_files_in_dir(merc_replacement_folder, std::regex(".*\\.glb")); + for (auto& path : merc_replacements) { + auto name = path.stem().string(); + auto it = std::find_if(out.merc_data.models.begin(), out.merc_data.models.end(), + [&](const auto& m) { return m.name == name; }); + if (it != out.merc_data.models.end()) { + auto& model = *it; + replace_model(out, model, path); + } + } + } + + // add custom models if present + auto lvl_name = out.level_name == "" ? "common" : out.level_name; + auto models_folder = file_util::get_jak_project_dir() / "custom_assets" / + game_version_names[version] / "models" / lvl_name; + if (file_util::file_exists(models_folder.string())) { + auto custom_models = file_util::find_files_in_dir(models_folder, std::regex(".*\\.glb")); + for (auto& mdl : custom_models) { + add_custom_model_to_level(out, mdl.stem().string(), mdl); + } + } } } // namespace decompiler diff --git a/decompiler/level_extractor/merc_replacement.cpp b/decompiler/level_extractor/merc_replacement.cpp new file mode 100644 index 0000000000..e7f38846c9 --- /dev/null +++ b/decompiler/level_extractor/merc_replacement.cpp @@ -0,0 +1,211 @@ +#include "merc_replacement.h" + +using namespace gltf_util; + +namespace decompiler { +void extract(const std::string& name, + MercExtractData& out, + const tinygltf::Model& model, + const std::vector& all_nodes, + u32 index_offset, + u32 vertex_offset, + u32 tex_offset) { + ASSERT(out.new_vertices.empty()); + + std::map draw_by_material; + int mesh_count = 0; + int prim_count = 0; + + for (const auto& n : all_nodes) { + const auto& node = model.nodes[n.node_idx]; + if (node.extras.Has("set_invisible") && node.extras.Get("set_invisible").Get()) { + continue; + } + if (node.mesh >= 0) { + const auto& mesh = model.meshes[node.mesh]; + mesh_count++; + for (const auto& prim : mesh.primitives) { + prim_count++; + // extract index buffer + std::vector prim_indices = gltf_util::gltf_index_buffer( + model, prim.indices, out.new_vertices.size() + vertex_offset); + ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES, "Unsupported triangle mode"); + // extract vertices + auto verts = + gltf_util::gltf_vertices(model, prim.attributes, n.w_T_node, true, true, mesh.name); + out.new_vertices.insert(out.new_vertices.end(), verts.vtx.begin(), verts.vtx.end()); + out.new_colors.insert(out.new_colors.end(), verts.vtx_colors.begin(), + verts.vtx_colors.end()); + 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 + 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; + 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()); + } + } + } + + tfrag3::MercEffect e; + out.new_model.name = name; + out.new_model.max_bones = 120; + out.new_model.max_draws = 200; + for (const auto& [mat_idx, d_] : draw_by_material) { + e.all_draws.push_back(d_); + auto& draw = e.all_draws.back(); + draw.mode = make_default_draw_mode(); + + if (mat_idx == -1) { + lg::warn("Draw had a material index of -1, using default texture."); + draw.tree_tex_id = 0; + continue; + } + const auto& mat = model.materials[mat_idx]; + int tex_idx = mat.pbrMetallicRoughness.baseColorTexture.index; + if (tex_idx == -1) { + lg::warn("Material {} has no texture, using default texture.", mat.name); + draw.tree_tex_id = 0; + continue; + } + + const auto& tex = model.textures[tex_idx]; + ASSERT(tex.sampler >= 0); + ASSERT(tex.source >= 0); + draw.mode = draw_mode_from_sampler(model.samplers.at(tex.sampler)); + + const auto& img = model.images[tex.source]; + draw.tree_tex_id = tex_offset + texture_pool_add_texture(&out.tex_pool, img); + } + lg::info("total of {} unique materials", e.all_draws.size()); + e.has_mod_draw = false; + out.new_model.effects.push_back(e); + out.new_model.effects.push_back(e); + out.new_model.effects.push_back(e); + out.new_model.effects.push_back(e); + + lg::info("Merged {} meshes and {} prims into {} vertices", mesh_count, prim_count, + out.new_vertices.size()); +} + +const tfrag3::MercVertex& find_closest(const std::vector& old, + float x, + float y, + float z) { + float best_dist = 1e10; + int best_idx = 0; + + for (int i = 0; i < old.size(); i++) { + auto& v = old[i]; + float dx = v.pos[0] - x; + float dy = v.pos[1] - y; + float dz = v.pos[2] - z; + float dist = (dx * dx) + (dy * dy) + (dz * dz); + if (dist < best_dist) { + best_dist = dist; + best_idx = i; + } + } + + return old[best_idx]; +} + +void merc_convert_replacement(MercSwapData& out, + const MercExtractData& in, + const std::vector& old_verts) { + out.new_model = in.new_model; + out.new_indices = in.new_indices; + out.new_textures = in.tex_pool.textures_by_idx; + + // convert vertices + for (size_t i = 0; i < in.new_vertices.size(); i++) { + const auto& y = in.new_vertices[i]; + const auto& copy_from = find_closest(old_verts, y.x, y.y, y.z); + auto& x = out.new_vertices.emplace_back(); + x.pos[0] = y.x; + x.pos[1] = y.y; + x.pos[2] = y.z; + x.normal[0] = copy_from.normal[0]; + x.normal[1] = copy_from.normal[1]; + x.normal[2] = copy_from.normal[2]; + x.weights[0] = copy_from.weights[0]; + x.weights[1] = copy_from.weights[1]; + x.weights[2] = copy_from.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] = copy_from.mats[0]; + x.mats[1] = copy_from.mats[1]; + x.mats[2] = copy_from.mats[2]; + } +} + +void merc_convert_custom(MercSwapData& out, const MercExtractData& in) { + out.new_model = in.new_model; + out.new_indices = in.new_indices; + out.new_textures = in.tex_pool.textures_by_idx; + + // convert vertices + for (size_t i = 0; i < in.new_vertices.size(); i++) { + const auto& y = in.new_vertices[i]; + auto& x = out.new_vertices.emplace_back(); + x.pos[0] = y.x; + x.pos[1] = y.y; + x.pos[2] = y.z; + 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.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; + } +} + +MercSwapData load_replacement_merc_model(const std::string& name, + u32 current_idx_count, + u32 current_vtx_count, + u32 current_tex_count, + const std::string& path, + const std::vector& old_verts, + bool custom_mdl) { + MercSwapData result; + lg::info("Reading gltf mesh: {}", path); + tinygltf::TinyGLTF loader; + tinygltf::Model model; + std::string err, warn; + 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); + + MercExtractData extract_data; + extract(name, extract_data, model, all_nodes, current_idx_count, current_vtx_count, + current_tex_count); + if (custom_mdl) { + merc_convert_custom(result, extract_data); + } else { + merc_convert_replacement(result, extract_data, old_verts); + } + + return result; +} +} // namespace decompiler \ No newline at end of file diff --git a/decompiler/level_extractor/merc_replacement.h b/decompiler/level_extractor/merc_replacement.h new file mode 100644 index 0000000000..13b9e10ec9 --- /dev/null +++ b/decompiler/level_extractor/merc_replacement.h @@ -0,0 +1,32 @@ +#pragma once + +#include "common/log/log.h" +#include "common/util/gltf_util.h" + +namespace decompiler { +struct MercExtractData { + gltf_util::TexturePool tex_pool; + std::vector new_indices; + std::vector new_vertices; + std::vector> new_colors; + std::vector normals; + + tfrag3::MercModel new_model; +}; + +// Data produced by loading a replacement model +struct MercSwapData { + std::vector new_indices; + std::vector new_vertices; + std::vector new_textures; + tfrag3::MercModel new_model; +}; + +MercSwapData load_replacement_merc_model(const std::string& name, + u32 current_idx_count, + u32 current_vtx_count, + u32 current_tex_count, + const std::string& path, + const std::vector& old_verts, + bool custom_mdl); +} // namespace decompiler \ No newline at end of file diff --git a/game/graphics/opengl_renderer/foreground/Merc2.cpp b/game/graphics/opengl_renderer/foreground/Merc2.cpp index 7487f752d8..896c3203f6 100644 --- a/game/graphics/opengl_renderer/foreground/Merc2.cpp +++ b/game/graphics/opengl_renderer/foreground/Merc2.cpp @@ -535,11 +535,14 @@ void Merc2::handle_pc_model(const DmaTransfer& setup, auto* flags = (const PcMercFlags*)input_data; int num_effects = flags->effect_count; // mostly just a sanity check ASSERT(num_effects < kMaxEffect); + // hack for custom models to disable blerc/mod draws + bool is_custom_model = model->effects.at(0).all_draws.at(0).no_strip; u64 current_ignore_alpha_bits = flags->ignore_alpha_mask; // shader settings u64 current_effect_enable_bits = flags->enable_mask; // mask for game to disable an effect - bool model_uses_mod = flags->bitflags & 1; // if we should update vertices from game. + bool model_uses_mod = + flags->bitflags & 1 && !is_custom_model; // if we should update vertices from game. bool model_disables_fog = flags->bitflags & 2; - bool model_uses_pc_blerc = flags->bitflags & 4; + bool model_uses_pc_blerc = flags->bitflags & 4 && !is_custom_model; bool model_disables_envmap = flags->bitflags & 8; input_data += 32; diff --git a/goal_src/jak1/game.gp b/goal_src/jak1/game.gp index 1bb71aa7de..da6370fe95 100644 --- a/goal_src/jak1/game.gp +++ b/goal_src/jak1/game.gp @@ -225,7 +225,7 @@ :out '(,(string-append "$OUT/obj/" name ".go"))))) (defmacro build-actor (name &key (gen-mesh #f)) - (let* ((path (string-append "custom_assets/jak1/models/" name ".glb"))) + (let* ((path (string-append "custom_assets/jak1/models/custom_levels/" name ".glb"))) `(defstep :in '(,path ,(symbol->string gen-mesh)) :tool 'build-actor :out '(,(string-append "$OUT/obj/" name "-ag.go"))))) @@ -1659,7 +1659,7 @@ (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 +;; requires a .glb model file in custom_assets/jak1/models/custom_levels ;; to also generate a collide-mesh, add :gen-mesh #t (build-actor "test-actor" :gen-mesh #t) diff --git a/goalc/build_level/common/build_level.cpp b/goalc/build_level/common/build_level.cpp index edb22e7d37..ab94ebc546 100644 --- a/goalc/build_level/common/build_level.cpp +++ b/goalc/build_level/common/build_level.cpp @@ -45,12 +45,12 @@ std::vector find_art_groups( void add_model_to_level(GameVersion version, const std::string& name, tfrag3::Level& lvl) { lg::info("custom level: adding custom model {}", name); auto glb = name + ".glb"; - auto merc_data = load_merc_model(lvl.merc_data.indices.size(), lvl.merc_data.vertices.size(), - lvl.textures.size(), - fs::path(file_util::get_jak_project_dir() / "custom_assets" / - game_version_names[version] / "models" / glb) - .string(), - name + "-lod0"); + auto merc_data = load_merc_model( + lvl.merc_data.indices.size(), lvl.merc_data.vertices.size(), lvl.textures.size(), + fs::path(file_util::get_jak_project_dir() / "custom_assets" / game_version_names[version] / + "models" / "custom_levels" / glb) + .string(), + name + "-lod0"); for (auto& idx : merc_data.new_indices) { lvl.merc_data.indices.push_back(idx); }