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); }