diff --git a/CMakeLists.txt b/CMakeLists.txt index d1d964e9d..4a8c20ef9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,8 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") # Note: this is only _reserved_ memory, not necessarily _committed_ memory # TODO - test with add_link_options instead set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${LDFLAGS} -Xlinker /STACK:16000000") + else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ggdb -g -Wextra") endif() # additional c++ and linker flags for release mode for our projects @@ -78,6 +80,7 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") -Wcast-qual \ -Wdisabled-optimization \ -Wformat \ + -Wextra \ -Wmissing-include-dirs \ -Woverloaded-virtual \ -Wredundant-decls \ diff --git a/common/custom_data/TFrag3Data.cpp b/common/custom_data/TFrag3Data.cpp index 85acad65b..72f774793 100644 --- a/common/custom_data/TFrag3Data.cpp +++ b/common/custom_data/TFrag3Data.cpp @@ -277,7 +277,6 @@ void MercModel::serialize(Serializer& ser) { for (auto& effect : effects) { effect.serialize(ser); } - ser.from_ptr(&scale_xyz); ser.from_ptr(&max_draws); ser.from_ptr(&max_bones); } diff --git a/common/custom_data/Tfrag3Data.h b/common/custom_data/Tfrag3Data.h index a573f8fb7..930e0163c 100644 --- a/common/custom_data/Tfrag3Data.h +++ b/common/custom_data/Tfrag3Data.h @@ -53,7 +53,7 @@ enum MemoryUsageCategory { NUM_CATEGORIES }; -constexpr int TFRAG3_VERSION = 20; +constexpr int TFRAG3_VERSION = 21; // These vertices should be uploaded to the GPU at load time and don't change struct PreloadedVertex { @@ -376,7 +376,6 @@ struct MercEffect { struct MercModel { std::string name; std::vector effects; - float scale_xyz; u32 max_draws; u32 max_bones; void serialize(Serializer& ser); diff --git a/common/util/FileUtil.cpp b/common/util/FileUtil.cpp index 27e91751b..ffc8412ba 100644 --- a/common/util/FileUtil.cpp +++ b/common/util/FileUtil.cpp @@ -512,7 +512,7 @@ std::vector decompress_dgo(const std::vector& data_in) { return decompressed_data; } -FILE* open_file(const fs::path& path, std::string mode) { +FILE* open_file(const fs::path& path, const std::string& mode) { #ifdef _WIN32 return _wfopen(path.wstring().c_str(), std::wstring(mode.begin(), mode.end()).c_str()); #else @@ -520,7 +520,7 @@ FILE* open_file(const fs::path& path, std::string mode) { #endif } -std::vector find_files_recursively(const fs::path base_dir, const std::regex& pattern) { +std::vector find_files_recursively(const fs::path& base_dir, const std::regex& pattern) { std::vector files = {}; for (auto& p : fs::recursive_directory_iterator(base_dir)) { if (p.is_regular_file()) { diff --git a/common/util/FileUtil.h b/common/util/FileUtil.h index 83bd6c315..a830b4c18 100644 --- a/common/util/FileUtil.h +++ b/common/util/FileUtil.h @@ -55,6 +55,6 @@ void ISONameFromAnimationName(char* dst, const char* src); void assert_file_exists(const char* path, const char* error_message); bool dgo_header_is_compressed(const std::vector& data); std::vector decompress_dgo(const std::vector& data_in); -FILE* open_file(const fs::path& path, std::string mode); -std::vector find_files_recursively(const fs::path base_dir, const std::regex& pattern); +FILE* open_file(const fs::path& path, const std::string& mode); +std::vector find_files_recursively(const fs::path& base_dir, const std::regex& pattern); } // namespace file_util diff --git a/common/util/FontUtils.cpp b/common/util/FontUtils.cpp index 91e65724a..3f1bba422 100644 --- a/common/util/FontUtils.cpp +++ b/common/util/FontUtils.cpp @@ -164,7 +164,7 @@ std::string GameTextFontBank::convert_utf8_to_game(std::string str) const { std::string GameTextFontBank::convert_utf8_to_game_with_escape(const std::string& str) const { std::string newstr; - for (int i = 0; i < str.size(); ++i) { + for (size_t i = 0; i < str.size(); ++i) { auto c = str.at(i); if (c == '"') { newstr.push_back('"'); diff --git a/custom_levels/test-zone/test-zone.jsonc b/custom_levels/test-zone/test-zone.jsonc index 56b6a41d2..695543624 100644 --- a/custom_levels/test-zone/test-zone.jsonc +++ b/custom_levels/test-zone/test-zone.jsonc @@ -6,14 +6,20 @@ "iso_name": "TESTZONE", // The nickname, should be exactly 3 characters "nickname": "TSZ", // 3 char name, all uppercase - // Background mesh file. // Must have vertex colors. Use the blender cycles renderer, bake, diffuse, uncheck color, // and bake to vertex colors. For now, only the first vertex color group is used, so make sure you // only have 1. "gltf_file": "custom_levels/test-zone/test-zone2.glb", + + // automatically set wall vs. ground based on angle. Useful if you don't want to assign this yourself "automatic_wall_detection": true, "automatic_wall_angle": 45.0, + + // if your mesh has triangles with incorrect orientation, set this to make all collision mesh triangles double sided + // this makes collision 2x slower and bigger, so only use if really needed + "double_sided_collide": false, + "actors" : [ { "trans": [-21.6238, 20.0496, 17.1191], // translation diff --git a/decompiler/CMakeLists.txt b/decompiler/CMakeLists.txt index 8bfadabdc..db9b96959 100644 --- a/decompiler/CMakeLists.txt +++ b/decompiler/CMakeLists.txt @@ -60,7 +60,9 @@ add_library( level_extractor/extract_tfrag.cpp level_extractor/extract_tie.cpp level_extractor/extract_shrub.cpp + level_extractor/fr3_to_gltf.cpp level_extractor/MercData.cpp + level_extractor/tfrag_tie_fixup.cpp ObjectFile/LinkedObjectFile.cpp ObjectFile/LinkedObjectFileCreation.cpp @@ -86,6 +88,7 @@ target_link_libraries(decomp fmt stb_image xdelta3 + tiny_gltf ) add_executable(decompiler @@ -96,7 +99,8 @@ target_link_libraries(decompiler common lzokay fmt - stb_image) + stb_image + tiny_gltf) add_executable(extractor @@ -108,4 +112,5 @@ target_link_libraries(extractor lzokay fmt compiler - stb_image) + stb_image + tiny_gltf) diff --git a/decompiler/analysis/analyze_inspect_method.cpp b/decompiler/analysis/analyze_inspect_method.cpp index 02c527608..7df38ed84 100644 --- a/decompiler/analysis/analyze_inspect_method.cpp +++ b/decompiler/analysis/analyze_inspect_method.cpp @@ -238,12 +238,7 @@ FieldPrint get_field_print(const std::string& str) { return field_print; } -int get_start_idx_process(Function& function, - LinkedObjectFile& file, - TypeInspectorResult* result, - const std::string& parent_type, - const std::string& type_name, - Env& env) { +int get_start_idx_process(Function& function, const std::string& parent_type, Env& env) { if (function.basic_blocks.size() != 5) { fmt::print("[iim] inspect {} had {} basic blocks, expected 5\n", function.name(), function.basic_blocks.size()); @@ -944,8 +939,7 @@ std::string inspect_inspect_method(Function& inspect_method, inspect_method.ir2.env); if (idx < 0) { - idx = get_start_idx_process(inspect_method, file, &result, result.parent_type_name, type_name, - inspect_method.ir2.env); + idx = get_start_idx_process(inspect_method, result.parent_type_name, inspect_method.ir2.env); } StructureType* old_game_type = nullptr; if (previous_game_ts.fully_defined_type_exists(type_name)) { diff --git a/decompiler/config.cpp b/decompiler/config.cpp index 892c674bb..4f2673b30 100644 --- a/decompiler/config.cpp +++ b/decompiler/config.cpp @@ -69,7 +69,7 @@ Config read_config_file(const fs::path& path_to_config_file, const std::string& config.print_cfgs = cfg.at("print_cfgs").get(); config.generate_symbol_definition_map = cfg.at("generate_symbol_definition_map").get(); config.is_pal = cfg.at("is_pal").get(); - config.rip_levels = cfg.at("levels_convert_to_obj").get(); + config.rip_levels = cfg.at("rip_levels").get(); config.extract_collision = cfg.at("extract_collision").get(); config.generate_all_types = cfg.at("generate_all_types").get(); if (cfg.contains("old_all_types_file")) { diff --git a/decompiler/config/jak1_jp.jsonc b/decompiler/config/jak1_jp.jsonc index 041426990..c4c57d02d 100644 --- a/decompiler/config/jak1_jp.jsonc +++ b/decompiler/config/jak1_jp.jsonc @@ -96,8 +96,8 @@ // turn this on to extract level background graphics data "levels_extract": true, - // turn this on if you want extracted levels to be saved out as .obj files - "levels_convert_to_obj": false, + // turn this on if you want extracted levels to be saved out as .glb files + "rip_levels": false, // should we extract collision meshes? // these can be displayed in game, but makes the .fr3 files slightly larger "extract_collision": true, diff --git a/decompiler/config/jak1_ntsc_black_label.jsonc b/decompiler/config/jak1_ntsc_black_label.jsonc index 3cdaa84d6..893648cb4 100644 --- a/decompiler/config/jak1_ntsc_black_label.jsonc +++ b/decompiler/config/jak1_ntsc_black_label.jsonc @@ -97,7 +97,7 @@ // turn this on to extract level background graphics data "levels_extract": true, // turn this on if you want extracted levels to be saved out as .obj files - "levels_convert_to_obj": false, + "rip_levels": false, // should we extract collision meshes? // these can be displayed in game, but makes the .fr3 files slightly larger "extract_collision": true, diff --git a/decompiler/config/jak1_pal.jsonc b/decompiler/config/jak1_pal.jsonc index 041641914..a8aa031d7 100644 --- a/decompiler/config/jak1_pal.jsonc +++ b/decompiler/config/jak1_pal.jsonc @@ -97,7 +97,7 @@ // turn this on to extract level background graphics data "levels_extract": true, // turn this on if you want extracted levels to be saved out as .obj files - "levels_convert_to_obj": false, + "rip_levels": false, // should we extract collision meshes? // these can be displayed in game, but makes the .fr3 files slightly larger "extract_collision": true, diff --git a/decompiler/config/jak1_us2.jsonc b/decompiler/config/jak1_us2.jsonc index 9b3d2d609..edbaab4b9 100644 --- a/decompiler/config/jak1_us2.jsonc +++ b/decompiler/config/jak1_us2.jsonc @@ -97,7 +97,7 @@ // turn this on to extract level background graphics data "levels_extract": true, // turn this on if you want extracted levels to be saved out as .obj files - "levels_convert_to_obj": false, + "rip_levels": false, // should we extract collision meshes? // these can be displayed in game, but makes the .fr3 files slightly larger "extract_collision": true, diff --git a/decompiler/config/jak2_ntsc_v1.jsonc b/decompiler/config/jak2_ntsc_v1.jsonc index 6d5eeff75..d889a1540 100644 --- a/decompiler/config/jak2_ntsc_v1.jsonc +++ b/decompiler/config/jak2_ntsc_v1.jsonc @@ -101,7 +101,7 @@ // turn this on to extract level background graphics data "levels_extract": false, // turn this on if you want extracted levels to be saved out as .obj files - "levels_convert_to_obj": false, + "rip_levels": false, // should we extract collision meshes? // these can be displayed in game, but makes the .fr3 files slightly larger "extract_collision": true, diff --git a/decompiler/level_extractor/extract_level.cpp b/decompiler/level_extractor/extract_level.cpp index a17e47ad3..c7f390480 100644 --- a/decompiler/level_extractor/extract_level.cpp +++ b/decompiler/level_extractor/extract_level.cpp @@ -13,6 +13,7 @@ #include "decompiler/level_extractor/extract_shrub.h" #include "decompiler/level_extractor/extract_tfrag.h" #include "decompiler/level_extractor/extract_tie.h" +#include "decompiler/level_extractor/fr3_to_gltf.h" namespace decompiler { @@ -98,13 +99,12 @@ void extract_art_groups_from_level(const ObjectFileDB& db, const TextureDB& tex_db, const std::vector& tex_remap, const std::string& dgo_name, - tfrag3::Level& level_data, - bool dump_level) { + tfrag3::Level& level_data) { const auto& files = db.obj_files_by_dgo.at(dgo_name); for (const auto& file : files) { if (file.name.length() > 3 && !file.name.compare(file.name.length() - 3, 3, "-ag")) { const auto& ag_file = db.lookup_record(file); - extract_merc(ag_file, tex_db, db.dts, tex_remap, level_data, dump_level); + extract_merc(ag_file, tex_db, db.dts, tex_remap, level_data, false); } } } @@ -113,7 +113,6 @@ std::vector extract_bsp_from_level(const ObjectFileDB const TextureDB& tex_db, const std::string& dgo_name, const DecompileHacks& hacks, - bool dump_level, bool extract_collision, tfrag3::Level& level_data) { auto bsp_rec = get_bsp_file(db.obj_files_by_dgo.at(dgo_name)); @@ -165,18 +164,18 @@ std::vector extract_bsp_from_level(const ObjectFileDB } extract_tfrag(as_tfrag_tree, fmt::format("{}-{}", dgo_name, i++), bsp_header.texture_remap_table, tex_db, expected_missing_textures, level_data, - dump_level); + false); } else if (draw_tree->my_type() == "drawable-tree-instance-tie") { auto as_tie_tree = dynamic_cast(draw_tree.get()); ASSERT(as_tie_tree); extract_tie(as_tie_tree, fmt::format("{}-{}-tie", dgo_name, i++), - bsp_header.texture_remap_table, tex_db, level_data, dump_level); + bsp_header.texture_remap_table, tex_db, level_data, false); } else if (draw_tree->my_type() == "drawable-tree-instance-shrub") { auto as_shrub_tree = dynamic_cast(draw_tree.get()); ASSERT(as_shrub_tree); extract_shrub(as_shrub_tree, fmt::format("{}-{}-shrub", dgo_name, i++), - bsp_header.texture_remap_table, tex_db, {}, level_data, dump_level); + bsp_header.texture_remap_table, tex_db, {}, level_data, false); } else if (draw_tree->my_type() == "drawable-tree-collide-fragment" && extract_collision) { auto as_collide_frags = dynamic_cast(draw_tree.get()); @@ -184,7 +183,7 @@ std::vector extract_bsp_from_level(const ObjectFileDB ASSERT(!got_collide); got_collide = true; extract_collide_frags(as_collide_frags, all_ties, fmt::format("{}-{}-collide", dgo_name, i++), - level_data, dump_level); + level_data, false); } else { // fmt::print(" unsupported tree {}\n", draw_tree->my_type()); } @@ -218,7 +217,8 @@ void extract_common(const ObjectFileDB& db, tfrag3::Level tfrag_level; add_all_textures_from_level(tfrag_level, dgo_name, tex_db); - extract_art_groups_from_level(db, tex_db, {}, dgo_name, tfrag_level, dump_levels); + extract_art_groups_from_level(db, tex_db, {}, dgo_name, tfrag_level); + Serializer ser; tfrag_level.serialize(ser); auto compressed = @@ -231,6 +231,11 @@ void extract_common(const ObjectFileDB& db, file_util::write_binary_file( output_folder / fmt::format("{}.fr3", dgo_name.substr(0, dgo_name.length() - 4)), compressed.data(), compressed.size()); + + if (dump_levels) { + save_level_foreground_as_gltf(tfrag_level, + file_util::get_jak_project_dir() / "debug_out" / "common.glb"); + } } void extract_from_level(const ObjectFileDB& db, @@ -248,9 +253,9 @@ void extract_from_level(const ObjectFileDB& db, add_all_textures_from_level(level_data, dgo_name, tex_db); // the bsp header file data - auto tex_remap = extract_bsp_from_level(db, tex_db, dgo_name, hacks, dump_level, - extract_collision, level_data); - extract_art_groups_from_level(db, tex_db, tex_remap, dgo_name, level_data, dump_level); + auto tex_remap = + extract_bsp_from_level(db, tex_db, dgo_name, hacks, extract_collision, level_data); + extract_art_groups_from_level(db, tex_db, tex_remap, dgo_name, level_data); Serializer ser; level_data.serialize(ser); @@ -263,6 +268,15 @@ void extract_from_level(const ObjectFileDB& db, file_util::write_binary_file( output_folder / fmt::format("{}.fr3", dgo_name.substr(0, dgo_name.length() - 4)), compressed.data(), compressed.size()); + + if (dump_level) { + save_level_background_as_gltf(level_data, + file_util::get_jak_project_dir() / "debug_out" / + fmt::format("{}_background.glb", level_data.level_name)); + save_level_foreground_as_gltf(level_data, + file_util::get_jak_project_dir() / "debug_out" / + fmt::format("{}_foreground.glb", level_data.level_name)); + } } void extract_all_levels(const ObjectFileDB& db, diff --git a/decompiler/level_extractor/extract_merc.cpp b/decompiler/level_extractor/extract_merc.cpp index 543f86094..73c1873c5 100644 --- a/decompiler/level_extractor/extract_merc.cpp +++ b/decompiler/level_extractor/extract_merc.cpp @@ -827,11 +827,11 @@ u8 convert_mat(int in) { } } -tfrag3::MercVertex convert_vertex(const MercUnpackedVtx& vtx) { +tfrag3::MercVertex convert_vertex(const MercUnpackedVtx& vtx, float xyz_scale) { tfrag3::MercVertex out; - out.pos[0] = vtx.pos[0]; - out.pos[1] = vtx.pos[1]; - out.pos[2] = vtx.pos[2]; + out.pos[0] = vtx.pos[0] * xyz_scale; + out.pos[1] = vtx.pos[1] * xyz_scale; + out.pos[2] = vtx.pos[2] * xyz_scale; out.pad0 = 0; out.normal[0] = vtx.nrm[0]; out.normal[1] = vtx.nrm[1]; @@ -896,7 +896,6 @@ void extract_merc(const ObjectFileData& ag_data, auto& ctrl = ctrls[ci]; pc_ctrl.name = ctrl.name; - pc_ctrl.scale_xyz = ctrl.header.xyz_scale; pc_ctrl.max_draws = 0; pc_ctrl.max_bones = 0; @@ -906,7 +905,7 @@ void extract_merc(const ObjectFileData& ag_data, auto& effect = all_effects[ci][ei]; u32 first_vertex = out.merc_data.vertices.size(); for (auto& vtx : effect.vertices) { - auto cvtx = convert_vertex(vtx); + auto cvtx = convert_vertex(vtx, ctrl.header.xyz_scale); out.merc_data.vertices.push_back(cvtx); for (int i = 0; i < 3; i++) { pc_ctrl.max_bones = std::max(pc_ctrl.max_bones, (u32)cvtx.mats[i]); diff --git a/decompiler/level_extractor/extract_merc.h b/decompiler/level_extractor/extract_merc.h index 35e1a1c3b..c6947747a 100644 --- a/decompiler/level_extractor/extract_merc.h +++ b/decompiler/level_extractor/extract_merc.h @@ -14,4 +14,4 @@ void extract_merc(const ObjectFileData& ag_data, const std::vector& map, tfrag3::Level& out, bool dump_level); -} +} // namespace decompiler \ No newline at end of file diff --git a/decompiler/level_extractor/fr3_to_gltf.cpp b/decompiler/level_extractor/fr3_to_gltf.cpp new file mode 100644 index 000000000..51d35ea52 --- /dev/null +++ b/decompiler/level_extractor/fr3_to_gltf.cpp @@ -0,0 +1,777 @@ +#include "fr3_to_gltf.h" + +#include "common/custom_data/Tfrag3Data.h" +#include "common/math/Vector.h" + +#include "decompiler/level_extractor/tfrag_tie_fixup.h" + +#include "third-party/tiny_gltf/tiny_gltf.h" + +namespace { + +/*! + * Convert fr3 format indices (strip format, with UINT32_MAX as restart) to unstripped tris. + * Assumes that this is the tfrag/tie format of stripping. Will flip tris as needed so the faces + * in this fragment all point a consistent way. However, the entire frag may be flipped. + */ +void unstrip_tfrag_tie(const std::vector& stripped_indices, + const std::vector& positions, + std::vector& unstripped, + std::vector& old_to_new_start) { + fixup_and_unstrip_tfrag_tie(stripped_indices, positions, unstripped, old_to_new_start); +} + +/*! + * Convert shrub strips. This doesn't assume anything about the strips. + */ +void unstrip_shrub_draws(const std::vector& stripped_indices, + std::vector& unstripped, + std::vector& draw_to_start, + std::vector& draw_to_count, + const std::vector& draws) { + for (auto& draw : draws) { + draw_to_start.push_back(unstripped.size()); + + for (size_t i = 2; i < draw.num_indices; i++) { + int idx = i + draw.first_index_index; + u32 a = stripped_indices[idx]; + u32 b = stripped_indices[idx - 1]; + u32 c = stripped_indices[idx - 2]; + if (a == UINT32_MAX || b == UINT32_MAX || c == UINT32_MAX) { + continue; + } + unstripped.push_back(a); + unstripped.push_back(b); + unstripped.push_back(c); + } + draw_to_count.push_back(unstripped.size() - draw_to_start.back()); + } +} + +/*! + * Convert merc strips. Doesn't assume anything about strips. Output is [model][effect][draw] format + */ +void unstrip_merc_draws(const std::vector& stripped_indices, + const std::vector& models, + std::vector& unstripped, + std::vector>>& draw_to_start, + std::vector>>& draw_to_count) { + for (auto& model : models) { + auto& model_dts = draw_to_start.emplace_back(); + auto& model_dtc = draw_to_count.emplace_back(); + for (auto& effect : model.effects) { + auto& effect_dts = model_dts.emplace_back(); + auto& effect_dtc = model_dtc.emplace_back(); + for (auto& draw : effect.draws) { + effect_dts.push_back(unstripped.size()); + + for (size_t i = 2; i < draw.index_count; i++) { + int idx = i + draw.first_index; + u32 a = stripped_indices[idx]; + u32 b = stripped_indices[idx - 1]; + u32 c = stripped_indices[idx - 2]; + if (a == UINT32_MAX || b == UINT32_MAX || c == UINT32_MAX) { + continue; + } + unstripped.push_back(a); + unstripped.push_back(b); + unstripped.push_back(c); + } + effect_dtc.push_back(unstripped.size() - effect_dts.back()); + } + } + } +} + +/*! + * Get just the xyz positions from a preloaded vertex vector. + */ +std::vector extract_positions(const std::vector& vtx) { + std::vector result; + for (auto& v : vtx) { + auto& o = result.emplace_back(); + o[0] = v.x; + o[1] = v.y; + o[2] = v.z; + } + return result; +} + +/*! + * Set up a buffer for the positions of the given vertices. + * Return the index of the accessor. + */ +template +int make_position_buffer_accessor(const std::vector& vertices, tinygltf::Model& model) { + // first create a buffer: + int buffer_idx = (int)model.buffers.size(); + auto& buffer = model.buffers.emplace_back(); + buffer.data.resize(sizeof(float) * 3 * vertices.size()); + + // and fill it + u8* buffer_ptr = buffer.data.data(); + for (const auto& vtx : vertices) { + if constexpr (std::is_same::value) { + float xyz[3] = {vtx.pos[0] / 4096.f, vtx.pos[1] / 4096.f, vtx.pos[2] / 4096.f}; + memcpy(buffer_ptr, xyz, 3 * sizeof(float)); + buffer_ptr += 3 * sizeof(float); + } else { + float xyz[3] = {vtx.x / 4096.f, vtx.y / 4096.f, vtx.z / 4096.f}; + memcpy(buffer_ptr, xyz, 3 * sizeof(float)); + buffer_ptr += 3 * sizeof(float); + } + } + + // create a view of this buffer + int buffer_view_idx = (int)model.bufferViews.size(); + auto& buffer_view = model.bufferViews.emplace_back(); + buffer_view.buffer = buffer_idx; + buffer_view.byteOffset = 0; + buffer_view.byteLength = buffer.data.size(); + buffer_view.byteStride = 0; // tightly packed + buffer_view.target = TINYGLTF_TARGET_ARRAY_BUFFER; + + int accessor_idx = (int)model.accessors.size(); + auto& accessor = model.accessors.emplace_back(); + accessor.bufferView = buffer_view_idx; + accessor.byteOffset = 0; + accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + accessor.count = vertices.size(); + accessor.type = TINYGLTF_TYPE_VEC3; + + return accessor_idx; +} + +/*! + * Set up a buffer for the texture coordinates of the given vertices, multiplying by scale. + * Return the index of the accessor. + */ +template +int make_tex_buffer_accessor(const std::vector& vertices, tinygltf::Model& model, float scale) { + // first create a buffer: + int buffer_idx = (int)model.buffers.size(); + auto& buffer = model.buffers.emplace_back(); + buffer.data.resize(sizeof(float) * 2 * vertices.size()); + + // and fill it + u8* buffer_ptr = buffer.data.data(); + for (const auto& vtx : vertices) { + if constexpr (std::is_same::value) { + float st[2] = {vtx.st[0] * scale, vtx.st[1] * scale}; + memcpy(buffer_ptr, st, 2 * sizeof(float)); + buffer_ptr += 2 * sizeof(float); + } else { + float st[2] = {vtx.s * scale, vtx.t * scale}; + memcpy(buffer_ptr, st, 2 * sizeof(float)); + buffer_ptr += 2 * sizeof(float); + } + } + + // create a view of this buffer + int buffer_view_idx = (int)model.bufferViews.size(); + auto& buffer_view = model.bufferViews.emplace_back(); + buffer_view.buffer = buffer_idx; + buffer_view.byteOffset = 0; + buffer_view.byteLength = buffer.data.size(); + buffer_view.byteStride = 0; // tightly packed + buffer_view.target = TINYGLTF_TARGET_ARRAY_BUFFER; + + int accessor_idx = (int)model.accessors.size(); + auto& accessor = model.accessors.emplace_back(); + accessor.bufferView = buffer_view_idx; + accessor.byteOffset = 0; + accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + accessor.count = vertices.size(); + accessor.type = TINYGLTF_TYPE_VEC2; + + return accessor_idx; +} + +/*! + * Set up a buffer of vertex colors for the given time of day index, for tfrag. + * Uses the time of day texture to look up colors. + */ +int make_color_buffer_accessor(const std::vector& vertices, + tinygltf::Model& model, + const tfrag3::TfragTree& tfrag_tree, + int time_of_day) { + // first create a buffer: + int buffer_idx = (int)model.buffers.size(); + auto& buffer = model.buffers.emplace_back(); + buffer.data.resize(sizeof(float) * 4 * vertices.size()); + std::vector floats; + + for (size_t i = 0; i < vertices.size(); i++) { + auto& color = tfrag_tree.colors.at(vertices[i].color_index); + for (int j = 0; j < 3; j++) { + floats.push_back(((float)color.rgba[time_of_day][j]) / 255.f); + } + floats.push_back(1.f); + } + memcpy(buffer.data.data(), floats.data(), sizeof(float) * floats.size()); + + // create a view of this buffer + int buffer_view_idx = (int)model.bufferViews.size(); + auto& buffer_view = model.bufferViews.emplace_back(); + buffer_view.buffer = buffer_idx; + buffer_view.byteOffset = 0; + buffer_view.byteLength = buffer.data.size(); + buffer_view.byteStride = 0; // tightly packed + buffer_view.target = TINYGLTF_TARGET_ARRAY_BUFFER; + + int accessor_idx = (int)model.accessors.size(); + auto& accessor = model.accessors.emplace_back(); + accessor.bufferView = buffer_view_idx; + accessor.byteOffset = 0; + accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + accessor.count = vertices.size(); + accessor.type = TINYGLTF_TYPE_VEC4; + + return accessor_idx; +} + +/*! + * Set up a buffer of vertex colors for the given time of day index, for tie. + * Uses the time of day texture to look up colors. + */ +int make_color_buffer_accessor(const std::vector& vertices, + tinygltf::Model& model, + const tfrag3::TieTree& tie_tree, + int time_of_day) { + // first create a buffer: + int buffer_idx = (int)model.buffers.size(); + auto& buffer = model.buffers.emplace_back(); + buffer.data.resize(sizeof(float) * 4 * vertices.size()); + std::vector floats; + + for (size_t i = 0; i < vertices.size(); i++) { + auto& color = tie_tree.colors.at(vertices[i].color_index); + for (int j = 0; j < 3; j++) { + floats.push_back(((float)color.rgba[time_of_day][j]) / 255.f); + } + floats.push_back(1.f); + } + memcpy(buffer.data.data(), floats.data(), sizeof(float) * floats.size()); + + // create a view of this buffer + int buffer_view_idx = (int)model.bufferViews.size(); + auto& buffer_view = model.bufferViews.emplace_back(); + buffer_view.buffer = buffer_idx; + buffer_view.byteOffset = 0; + buffer_view.byteLength = buffer.data.size(); + buffer_view.byteStride = 0; // tightly packed + buffer_view.target = TINYGLTF_TARGET_ARRAY_BUFFER; + + int accessor_idx = (int)model.accessors.size(); + auto& accessor = model.accessors.emplace_back(); + accessor.bufferView = buffer_view_idx; + accessor.byteOffset = 0; + accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + accessor.count = vertices.size(); + accessor.type = TINYGLTF_TYPE_VEC4; + + return accessor_idx; +} + +int make_color_buffer_accessor(const std::vector& vertices, + tinygltf::Model& model) { + // first create a buffer: + int buffer_idx = (int)model.buffers.size(); + auto& buffer = model.buffers.emplace_back(); + buffer.data.resize(sizeof(float) * 4 * vertices.size()); + std::vector floats; + + for (size_t i = 0; i < vertices.size(); i++) { + for (int j = 0; j < 3; j++) { + floats.push_back(((float)vertices[i].rgba[j]) / 255.f); + } + floats.push_back(1.f); + } + memcpy(buffer.data.data(), floats.data(), sizeof(float) * floats.size()); + + // create a view of this buffer + int buffer_view_idx = (int)model.bufferViews.size(); + auto& buffer_view = model.bufferViews.emplace_back(); + buffer_view.buffer = buffer_idx; + buffer_view.byteOffset = 0; + buffer_view.byteLength = buffer.data.size(); + buffer_view.byteStride = 0; // tightly packed + buffer_view.target = TINYGLTF_TARGET_ARRAY_BUFFER; + + int accessor_idx = (int)model.accessors.size(); + auto& accessor = model.accessors.emplace_back(); + accessor.bufferView = buffer_view_idx; + accessor.byteOffset = 0; + accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + accessor.count = vertices.size(); + accessor.type = TINYGLTF_TYPE_VEC4; + + return accessor_idx; +} + +/*! + * Set up a buffer of vertex colors for the given time of day index, for shrub. + * Uses the time of day texture to look up colors. + */ +int make_color_buffer_accessor(const std::vector& vertices, + tinygltf::Model& model, + const tfrag3::ShrubTree& shrub_tree, + int time_of_day) { + // first create a buffer: + int buffer_idx = (int)model.buffers.size(); + auto& buffer = model.buffers.emplace_back(); + buffer.data.resize(sizeof(float) * 4 * vertices.size()); + std::vector floats; + + for (size_t i = 0; i < vertices.size(); i++) { + auto& color = shrub_tree.time_of_day_colors.at(vertices[i].color_index); + for (int j = 0; j < 3; j++) { + floats.push_back(((float)color.rgba[time_of_day][j]) / 255.f); + } + floats.push_back(1.f); + } + memcpy(buffer.data.data(), floats.data(), sizeof(float) * floats.size()); + + // create a view of this buffer + int buffer_view_idx = (int)model.bufferViews.size(); + auto& buffer_view = model.bufferViews.emplace_back(); + buffer_view.buffer = buffer_idx; + buffer_view.byteOffset = 0; + buffer_view.byteLength = buffer.data.size(); + buffer_view.byteStride = 0; // tightly packed + buffer_view.target = TINYGLTF_TARGET_ARRAY_BUFFER; + + int accessor_idx = (int)model.accessors.size(); + auto& accessor = model.accessors.emplace_back(); + accessor.bufferView = buffer_view_idx; + accessor.byteOffset = 0; + accessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT; + accessor.count = vertices.size(); + accessor.type = TINYGLTF_TYPE_VEC4; + + return accessor_idx; +} + +/*! + * Create a tinygltf buffer and buffer view for indices, and convert to gltf format. + * The map can be used to go from slots in the old index buffer to new. + */ +int make_tfrag_tie_index_buffer_view(const std::vector& indices, + const std::vector& positions, + tinygltf::Model& model, + std::vector& map_out) { + std::vector unstripped; + unstrip_tfrag_tie(indices, positions, unstripped, map_out); + + // first create a buffer: + int buffer_idx = (int)model.buffers.size(); + auto& buffer = model.buffers.emplace_back(); + buffer.data.resize(sizeof(u32) * unstripped.size()); + + // and fill it + memcpy(buffer.data.data(), unstripped.data(), buffer.data.size()); + + // create a view of this buffer + int buffer_view_idx = (int)model.bufferViews.size(); + auto& buffer_view = model.bufferViews.emplace_back(); + buffer_view.buffer = buffer_idx; + buffer_view.byteOffset = 0; + buffer_view.byteLength = buffer.data.size(); + buffer_view.byteStride = 0; // tightly packed + buffer_view.target = TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER; + return buffer_view_idx; +} + +/*! + * Create a tinygltf buffer and buffer view for indices, and convert to gltf format. + * The map can be used to go from slots in the old index buffer to new. + */ +int make_shrub_index_buffer_view(const std::vector& indices, + const std::vector& draws, + tinygltf::Model& model, + std::vector& draw_to_start, + std::vector& draw_to_count) { + std::vector unstripped; + unstrip_shrub_draws(indices, unstripped, draw_to_start, draw_to_count, draws); + + // first create a buffer: + int buffer_idx = (int)model.buffers.size(); + auto& buffer = model.buffers.emplace_back(); + buffer.data.resize(sizeof(u32) * unstripped.size()); + + // and fill it + memcpy(buffer.data.data(), unstripped.data(), buffer.data.size()); + + // create a view of this buffer + int buffer_view_idx = (int)model.bufferViews.size(); + auto& buffer_view = model.bufferViews.emplace_back(); + buffer_view.buffer = buffer_idx; + buffer_view.byteOffset = 0; + buffer_view.byteLength = buffer.data.size(); + buffer_view.byteStride = 0; // tightly packed + buffer_view.target = TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER; + return buffer_view_idx; +} + +int make_merc_index_buffer_view(const std::vector& indices, + const std::vector& models, + tinygltf::Model& model, + std::vector>>& draw_to_start, + std::vector>>& draw_to_count) { + std::vector unstripped; + unstrip_merc_draws(indices, models, unstripped, draw_to_start, draw_to_count); + + // first create a buffer: + int buffer_idx = (int)model.buffers.size(); + auto& buffer = model.buffers.emplace_back(); + buffer.data.resize(sizeof(u32) * unstripped.size()); + + // and fill it + memcpy(buffer.data.data(), unstripped.data(), buffer.data.size()); + + // create a view of this buffer + int buffer_view_idx = (int)model.bufferViews.size(); + auto& buffer_view = model.bufferViews.emplace_back(); + buffer_view.buffer = buffer_idx; + buffer_view.byteOffset = 0; + buffer_view.byteLength = buffer.data.size(); + buffer_view.byteStride = 0; // tightly packed + buffer_view.target = TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER; + return buffer_view_idx; +} + +int make_index_buffer_accessor(tinygltf::Model& model, + const tfrag3::StripDraw& draw, + const std::vector& idx_map, + int buffer_view_idx) { + int accessor_idx = (int)model.accessors.size(); + auto& accessor = model.accessors.emplace_back(); + accessor.bufferView = buffer_view_idx; + accessor.byteOffset = sizeof(u32) * idx_map.at(draw.unpacked.idx_of_first_idx_in_full_buffer); + accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT; + accessor.count = draw.num_triangles * 3; + accessor.type = TINYGLTF_TYPE_SCALAR; + + return accessor_idx; +} + +int make_index_buffer_accessor(tinygltf::Model& model, u32 start, u32 count, int buffer_view_idx) { + int accessor_idx = (int)model.accessors.size(); + auto& accessor = model.accessors.emplace_back(); + accessor.bufferView = buffer_view_idx; + + accessor.byteOffset = sizeof(u32) * start; + accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT; + accessor.count = count; + accessor.type = TINYGLTF_TYPE_SCALAR; + + return accessor_idx; +} + +int add_image_for_tex(const tfrag3::Level& level, + tinygltf::Model& model, + int tex_idx, + std::unordered_map& tex_image_map) { + const auto& existing = tex_image_map.find(tex_idx); + if (existing != tex_image_map.end()) { + return existing->second; + } + + auto& tex = level.textures.at(tex_idx); + int image_idx = (int)model.images.size(); + auto& image = model.images.emplace_back(); + image.pixel_type = TINYGLTF_TEXTURE_TYPE_UNSIGNED_BYTE; + image.width = tex.w; + image.height = tex.h; + image.image.resize(tex.data.size() * 4); + image.bits = 8; + image.component = 4; + image.mimeType = "image/png"; + image.name = tex.debug_name; + memcpy(image.image.data(), tex.data.data(), tex.data.size() * 4); + + tex_image_map[tex_idx] = image_idx; + return image_idx; +} + +int add_material_for_tex(const tfrag3::Level& level, + tinygltf::Model& model, + int tex_idx, + std::unordered_map& tex_image_map, + const DrawMode& draw_mode) { + int mat_idx = (int)model.materials.size(); + auto& mat = model.materials.emplace_back(); + auto& tex = level.textures.at(tex_idx); + + mat.doubleSided = true; + // the 2.0 here compensates for the ps2's weird blending where 0.5 behaves like 1.0 + mat.pbrMetallicRoughness.baseColorFactor = {2.0, 2.0, 2.0, 2.0}; + mat.pbrMetallicRoughness.baseColorTexture.texCoord = 0; // TEXCOORD_0, I think + mat.pbrMetallicRoughness.baseColorTexture.index = model.textures.size(); + mat.alphaMode = draw_mode.get_ab_enable() ? "BLEND" : "MASK"; + // the foreground and background renderers both use this cutoff + mat.alphaCutoff = (float)0x26 / 255.f; + auto& gltf_texture = model.textures.emplace_back(); + gltf_texture.name = tex.debug_name; + gltf_texture.sampler = model.samplers.size(); + auto& sampler = model.samplers.emplace_back(); + sampler.minFilter = draw_mode.get_filt_enable() ? TINYGLTF_TEXTURE_FILTER_LINEAR + : TINYGLTF_TEXTURE_FILTER_NEAREST; + sampler.magFilter = draw_mode.get_filt_enable() ? TINYGLTF_TEXTURE_FILTER_LINEAR + : TINYGLTF_TEXTURE_FILTER_NEAREST; + sampler.wrapS = draw_mode.get_clamp_s_enable() ? TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE + : TINYGLTF_TEXTURE_WRAP_REPEAT; + sampler.wrapT = draw_mode.get_clamp_t_enable() ? TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE + : TINYGLTF_TEXTURE_WRAP_REPEAT; + sampler.name = tex.debug_name; + + gltf_texture.source = add_image_for_tex(level, model, tex_idx, tex_image_map); + + return mat_idx; +} + +constexpr int kMaxColor = 1; +/*! + * Add the given tfrag data to a node under tfrag_root. + */ +void add_tfrag(const tfrag3::Level& level, + const tfrag3::TfragTree& tfrag_in, + tinygltf::Model& model, + std::unordered_map& tex_image_map) { + // copy and unpack in place + tfrag3::TfragTree tfrag = tfrag_in; + tfrag.unpack(); + + // we'll make a Node, Mesh, Primitive, then add the data to the primitive. + int node_idx = (int)model.nodes.size(); + auto& node = model.nodes.emplace_back(); + model.scenes.at(0).nodes.push_back(node_idx); + + int mesh_idx = (int)model.meshes.size(); + auto& mesh = model.meshes.emplace_back(); + node.mesh = mesh_idx; + + int position_buffer_accessor = make_position_buffer_accessor(tfrag.unpacked.vertices, model); + int texture_buffer_accessor = make_tex_buffer_accessor(tfrag.unpacked.vertices, model, 1.f); + std::vector index_map; + int index_buffer_view = make_tfrag_tie_index_buffer_view( + tfrag.unpacked.indices, extract_positions(tfrag.unpacked.vertices), model, index_map); + int colors[kMaxColor]; + + for (int i = 0; i < kMaxColor; i++) { + colors[i] = make_color_buffer_accessor(tfrag.unpacked.vertices, model, tfrag, i); + } + + for (auto& draw : tfrag.draws) { + auto& prim = mesh.primitives.emplace_back(); + prim.material = add_material_for_tex(level, model, draw.tree_tex_id, tex_image_map, draw.mode); + prim.indices = make_index_buffer_accessor(model, draw, index_map, index_buffer_view); + prim.attributes["POSITION"] = position_buffer_accessor; + prim.attributes["TEXCOORD_0"] = texture_buffer_accessor; + for (int i = 0; i < kMaxColor; i++) { + prim.attributes[fmt::format("COLOR_{}", i)] = colors[i]; + } + prim.mode = TINYGLTF_MODE_TRIANGLES; + } +} + +void add_tie(const tfrag3::Level& level, + const tfrag3::TieTree& tie_in, + tinygltf::Model& model, + std::unordered_map& tex_image_map) { + // copy and unpack in place + tfrag3::TieTree tie = tie_in; + tie.unpack(); + + // we'll make a Node, Mesh, Primitive, then add the data to the primitive. + int node_idx = (int)model.nodes.size(); + auto& node = model.nodes.emplace_back(); + model.scenes.at(0).nodes.push_back(node_idx); + + int mesh_idx = (int)model.meshes.size(); + auto& mesh = model.meshes.emplace_back(); + node.mesh = mesh_idx; + + int position_buffer_accessor = make_position_buffer_accessor(tie.unpacked.vertices, model); + int texture_buffer_accessor = make_tex_buffer_accessor(tie.unpacked.vertices, model, 1.f); + std::vector index_map; + int index_buffer_view = make_tfrag_tie_index_buffer_view( + tie.unpacked.indices, extract_positions(tie.unpacked.vertices), model, index_map); + int colors[kMaxColor]; + + for (int i = 0; i < kMaxColor; i++) { + colors[i] = make_color_buffer_accessor(tie.unpacked.vertices, model, tie, i); + } + + for (auto& draw : tie.static_draws) { + auto& prim = mesh.primitives.emplace_back(); + prim.material = add_material_for_tex(level, model, draw.tree_tex_id, tex_image_map, draw.mode); + prim.indices = make_index_buffer_accessor(model, draw, index_map, index_buffer_view); + prim.attributes["POSITION"] = position_buffer_accessor; + prim.attributes["TEXCOORD_0"] = texture_buffer_accessor; + for (int i = 0; i < kMaxColor; i++) { + prim.attributes[fmt::format("COLOR_{}", i)] = colors[i]; + } + prim.mode = TINYGLTF_MODE_TRIANGLES; + } +} + +void add_shrub(const tfrag3::Level& level, + const tfrag3::ShrubTree& shrub_in, + tinygltf::Model& model, + std::unordered_map& tex_image_map) { + // copy and unpack in place + tfrag3::ShrubTree shrub = shrub_in; + shrub.unpack(); + + // we'll make a Node, Mesh, Primitive, then add the data to the primitive. + int node_idx = (int)model.nodes.size(); + auto& node = model.nodes.emplace_back(); + model.scenes.at(0).nodes.push_back(node_idx); + + int mesh_idx = (int)model.meshes.size(); + auto& mesh = model.meshes.emplace_back(); + node.mesh = mesh_idx; + + int position_buffer_accessor = make_position_buffer_accessor(shrub.unpacked.vertices, model); + int texture_buffer_accessor = + make_tex_buffer_accessor(shrub.unpacked.vertices, model, 1.f / 4096.f); + std::vector draw_to_start, draw_to_count; + int index_buffer_view = make_shrub_index_buffer_view(shrub.indices, shrub.static_draws, model, + draw_to_start, draw_to_count); + int colors[kMaxColor]; + for (int i = 0; i < kMaxColor; i++) { + colors[i] = make_color_buffer_accessor(shrub.unpacked.vertices, model, shrub, i); + } + + // for (auto& draw : shrub.static_draws) { + for (size_t draw_idx = 0; draw_idx < shrub.static_draws.size(); draw_idx++) { + auto& draw = shrub.static_draws[draw_idx]; + auto& prim = mesh.primitives.emplace_back(); + prim.material = add_material_for_tex(level, model, draw.tree_tex_id, tex_image_map, draw.mode); + prim.indices = make_index_buffer_accessor(model, draw_to_start.at(draw_idx), + draw_to_count.at(draw_idx), index_buffer_view); + prim.attributes["POSITION"] = position_buffer_accessor; + prim.attributes["TEXCOORD_0"] = texture_buffer_accessor; + for (int i = 0; i < kMaxColor; i++) { + prim.attributes[fmt::format("COLOR_{}", i)] = colors[i]; + } + prim.mode = TINYGLTF_MODE_TRIANGLES; + } +} + +void add_merc(const tfrag3::Level& level, + tinygltf::Model& model, + std::unordered_map& tex_image_map) { + const auto& mverts = level.merc_data.vertices; + + // create position and uv buffers + int position_buffer_accessor = make_position_buffer_accessor(mverts, model); + int texture_buffer_accessor = make_tex_buffer_accessor(mverts, model, 1.f); + + std::vector>> draw_to_start, draw_to_count; + int index_buffer_view = make_merc_index_buffer_view( + level.merc_data.indices, level.merc_data.models, model, draw_to_start, draw_to_count); + int colors = make_color_buffer_accessor(mverts, model); + + for (size_t model_idx = 0; model_idx < level.merc_data.models.size(); model_idx++) { + const auto& mmodel = level.merc_data.models[model_idx]; + + int node_idx = (int)model.nodes.size(); + auto& node = model.nodes.emplace_back(); + model.scenes.at(0).nodes.push_back(node_idx); + node.name = mmodel.name; + int mesh_idx = (int)model.meshes.size(); + auto& mesh = model.meshes.emplace_back(); + mesh.name = node.name; + node.mesh = mesh_idx; + + 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.draws.size(); draw_idx++) { + const auto& draw = effect.draws[draw_idx]; + auto& prim = mesh.primitives.emplace_back(); + prim.material = + add_material_for_tex(level, model, draw.tree_tex_id, tex_image_map, draw.mode); + prim.indices = make_index_buffer_accessor( + model, draw_to_start[model_idx][effect_idx][draw_idx], + draw_to_count[model_idx][effect_idx][draw_idx], index_buffer_view); + prim.attributes["POSITION"] = position_buffer_accessor; + prim.attributes["TEXCOORD_0"] = texture_buffer_accessor; + prim.attributes["COLOR_0"] = colors; + prim.mode = TINYGLTF_MODE_TRIANGLES; + } + } + } +} +} // namespace + +/*! + * Export the background geometry (tie, tfrag, shrub) to a GLTF binary format (.glb) file. + */ +void save_level_background_as_gltf(const tfrag3::Level& level, const fs::path& glb_file) { + // the top level container for everything is the model. + tinygltf::Model model; + + // a "scene" is a traditional scene graph, made up of Nodes. + // sadly, attempting to nest stuff makes the blender importer unhappy, so we just dump + // everything into the top level. + auto& scene = model.scenes.emplace_back(); + + // hack, add a default material. + tinygltf::Material mat; + mat.pbrMetallicRoughness.baseColorFactor = {1.0f, 0.9f, 0.9f, 1.0f}; + mat.doubleSided = true; + model.materials.push_back(mat); + + std::unordered_map tex_image_map; + + // add all hi-lod tfrag trees + for (const auto& tfrag : level.tfrag_trees.at(0)) { + add_tfrag(level, tfrag, model, tex_image_map); + } + + for (const auto& tie : level.tie_trees.at(0)) { + add_tie(level, tie, model, tex_image_map); + } + + for (const auto& shrub : level.shrub_trees) { + add_shrub(level, shrub, model, tex_image_map); + } + + model.asset.generator = "opengoal"; + tinygltf::TinyGLTF gltf; + gltf.WriteGltfSceneToFile(&model, glb_file.string(), + true, // embedImages + true, // embedBuffers + true, // pretty print + true); // write binary +} + +void save_level_foreground_as_gltf(const tfrag3::Level& level, const fs::path& glb_file) { + // the top level container for everything is the model. + tinygltf::Model model; + + // a "scene" is a traditional scene graph, made up of Nodes. + // sadly, attempting to nest stuff makes the blender importer unhappy, so we just dump + // everything into the top level. + auto& scene = model.scenes.emplace_back(); + + // hack, add a default material. + tinygltf::Material mat; + mat.pbrMetallicRoughness.baseColorFactor = {1.0f, 0.9f, 0.9f, 1.0f}; + mat.doubleSided = true; + model.materials.push_back(mat); + + std::unordered_map tex_image_map; + + add_merc(level, model, tex_image_map); + + model.asset.generator = "opengoal"; + tinygltf::TinyGLTF gltf; + gltf.WriteGltfSceneToFile(&model, glb_file.string(), + true, // embedImages + true, // embedBuffers + true, // pretty print + true); // write binary +} \ No newline at end of file diff --git a/decompiler/level_extractor/fr3_to_gltf.h b/decompiler/level_extractor/fr3_to_gltf.h new file mode 100644 index 000000000..f2b52989f --- /dev/null +++ b/decompiler/level_extractor/fr3_to_gltf.h @@ -0,0 +1,10 @@ +#pragma once + +#include "common/custom_data/Tfrag3Data.h" +#include "common/util/FileUtil.h" + +/*! + * Export the background geometry (tie, tfrag, shrub) to a GLTF binary format (.glb) file. + */ +void save_level_background_as_gltf(const tfrag3::Level& level, const fs::path& glb_file); +void save_level_foreground_as_gltf(const tfrag3::Level& level, const fs::path& glb_file); \ No newline at end of file diff --git a/decompiler/level_extractor/tfrag_tie_fixup.cpp b/decompiler/level_extractor/tfrag_tie_fixup.cpp new file mode 100644 index 000000000..3e2f93c80 --- /dev/null +++ b/decompiler/level_extractor/tfrag_tie_fixup.cpp @@ -0,0 +1,456 @@ +#include "tfrag_tie_fixup.h" + +#include +#include +#include + +#include "common/math/Vector.h" +#include "common/util/Assert.h" + +// Approach: +// 1: un-strip vertices and make individual strips consistent. Group triangles by strip. +// 2: build a graph of neighboring groups +// 3: find connected components +// 4: for each connected component, order strips in bfs order +// 5: iterate through the strips. If flipping the strip will result in fewer inconsistencies with +// lowered-ordered neighbors, then flip it. +// 6: build final index buffer. + +// vertex indices for a single triangle +struct Tri { + u32 idx[3]; +}; + +// a list of triangles, assumed to have consistent orientation. +struct TriGroup { + std::vector tris; +}; + +/*! + * Step 1: convert strips to groups of un-stripped triangles, determine the old->new mapping. + * Note: for the old->new mapping, we don't have to give the right answer for triangles inside of a + * strip, or for degenerate strips. This mapping is just convenient for users of this data. + */ +void unstrip(const std::vector& stripped_indices, + std::vector& groups, + std::vector& old_to_new_start) { + // first triangle is the first triangle + old_to_new_start.push_back(0); + // doesn't matter, in the middle of a strip + old_to_new_start.push_back(0); + + // the tfrag output flips every other triangle, we'll need to unflip that. + bool toggle = false; + + // total number of indices created in the output. + size_t num_unstripped_idx = 0; + + // the current strip + TriGroup building_group; + + // loop over all groups of 3 indices... + for (size_t i = 2; i < stripped_indices.size(); i++) { + u32 a = stripped_indices[i]; + u32 b = stripped_indices[i - 1]; + u32 c = stripped_indices[i - 2]; + old_to_new_start.push_back(num_unstripped_idx); + if (a == UINT32_MAX || b == UINT32_MAX || c == UINT32_MAX) { + toggle = false; + if (!building_group.tris.empty()) { + groups.push_back(building_group); + building_group.tris.clear(); + } + continue; + } else { + num_unstripped_idx += 3; + auto& tri = building_group.tris.emplace_back(); + tri.idx[0] = a; + tri.idx[1] = toggle ? b : c; + tri.idx[2] = toggle ? c : b; + toggle = !toggle; + } + } + old_to_new_start.push_back(num_unstripped_idx); +} + +// A Node in the graph of groups. +// The self and and neighbors fields refers to group indices. +struct Node { + int self_idx = -1; + std::set neighbors; +}; + +u64 group_pair_to_neighbor_key(u64 a, u64 b) { + if (a < b) { + std::swap(a, b); + } + ASSERT(a < UINT32_MAX && b < UINT32_MAX); + return a | (b << 32); +} + +// For each pair of groups that are neighbors: +struct GroupPairInfo { + // which groups are neighbors + int idx[2] = {-1, -1}; + + // per-triangle-sharing-an-edge + std::vector matched_edge_twists; // true if twisted +}; + +/*! + * Arbitrary ordering of vector3's. + * We can use this to make sure that Edge(a, b) and Edge(b, a) are considered the same edge + * by always storing an edge as [min(a,b), max(a,b)]. + */ +bool greater_than(const math::Vector3f& a, const math::Vector3f& b) { + for (int dim = 0; dim < 3; dim++) { + if (a[dim] > b[dim]) { + return true; + } else if (a[dim] < b[dim]) { + return false; + } + } + // ASSERT(false); + return false; +} + +math::Vector3f round_vector(const math::Vector3f& in) { + math::Vector3f rounded; + for (int i = 0; i < 3; i++) { + s64 x = in[i]; + rounded[i] = x; + } + return rounded; +} +/*! + * Edge that can be hashed, and Edge(a, b) == Edge(b, a), using the trick described above. + */ +class HashableEdge { + public: + HashableEdge(const math::Vector3f& a, const math::Vector3f& b) { + if (greater_than(a, b)) { + m_greater_pt = a; + m_lesser_pt = b; + } else { + m_greater_pt = b; + m_lesser_pt = a; + } + } + + bool operator==(const HashableEdge& other) const { + return m_lesser_pt == other.m_lesser_pt && m_greater_pt == other.m_greater_pt; + } + + struct hash { + std::size_t operator()(const HashableEdge& in) const { + std::size_t result = 0; + for (int i = 0; i < 3; i++) { + result ^= std::hash()(in.m_lesser_pt[i]) ^ std::hash()(in.m_greater_pt[i]); + } + return result; + } + }; + + private: + math::Vector3f m_lesser_pt, m_greater_pt; +}; + +math::Vector3f triangle_normal(const Tri& tri, const std::vector& positions) { + const auto& a = positions.at(tri.idx[0]); + const auto& b = positions.at(tri.idx[1]); + const auto& c = positions.at(tri.idx[2]); + return (b - a).cross(c - a); +} + +float triangle_normal_dot(const Tri& a, + const Tri& b, + const std::vector& positions) { + return triangle_normal(a, positions).dot(triangle_normal(b, positions)); +} + +struct PerEdgeInfo { + int source_group = -1; + int tri_idx = -1; // in group + int edge_idx = -1; + bool change_order = false; +}; + +/*! + * Step 2: build a graph of connected groups. Groups are considered connected if they share an edge. + * Also collect information about pairs of connected groups. + * @param nodes: the node for each group. + * @param neighbor_info: info for each pair of connected group, keyed on group_pair_to_neighbor_key + * @param groups: input groups + * @param positions: vertex position input + */ +void build_graph(std::vector& nodes, + std::unordered_map& neighbor_info, + const std::vector& groups, + const std::vector& positions) { + nodes.reserve(groups.size()); + + // to avoid slow O(n^2) edge checks, we'll add all edges to a hash table, recording which + // groups they appear in. + std::unordered_map, HashableEdge::hash> edge_info_map; + + // first pass: set up nodes and build hash table + for (int group_idx = 0; group_idx < (int)groups.size(); group_idx++) { + const auto& group = groups[group_idx]; + // add the node + auto& node = nodes.emplace_back(); + node.self_idx = group_idx; + + for (int tri_idx = 0; tri_idx < (int)group.tris.size(); tri_idx++) { + const auto& tri = group.tris[tri_idx]; + for (int edge_idx = 0; edge_idx < 3; edge_idx++) { + u32 edge_indices[2] = { + tri.idx[(edge_idx + 0) % 3], + tri.idx[(edge_idx + 1) % 3], + }; + HashableEdge edge(round_vector(positions.at(edge_indices[0])), + round_vector(positions.at(edge_indices[1]))); + auto& info_list = edge_info_map[edge]; + bool found = false; + for (auto& x : info_list) { + if (x.source_group == group_idx) { + found = true; + break; + } + } + if (found) { + continue; + } + auto& info = edge_info_map[edge].emplace_back(); + info.source_group = group_idx; + info.tri_idx = tri_idx; + info.edge_idx = edge_idx; + info.change_order = + greater_than(positions.at(edge_indices[0]), positions.at(edge_indices[1])); + } + } + } + + // second pass: loop over shared edges + int shared_edge_count = 0; + for (const auto& [edge, infos] : edge_info_map) { + // skip any edge that only shows up once. + if (infos.size() < 2) { + continue; + } + shared_edge_count++; + + for (int i = 0; i < (int)infos.size(); i++) { + for (int j = 0; j < i; j++) { + const auto& info_a = infos.at(i); + const auto& info_b = infos.at(j); + int group_a = info_a.source_group; + int group_b = info_b.source_group; + if (info_a.source_group == info_b.source_group) { + fmt::print("duplicate edge in group!\n"); // ?? + continue; + } + + // link neighbors + nodes.at(group_a).neighbors.insert(group_b); + nodes.at(group_b).neighbors.insert(group_a); + + // make neighbor info (or append to existing) + auto neighbor_key = group_pair_to_neighbor_key(group_a, group_b); + auto& info = neighbor_info[neighbor_key]; + int a_idx = group_a > group_b ? 0 : 1; + int b_idx = 1 - a_idx; + info.idx[a_idx] = group_a; + info.idx[b_idx] = group_b; + info.matched_edge_twists.push_back( + info_a.change_order == info_b.change_order // should be opposite dirs. + // triangle_normal_dot(groups.at(group_a).tris.at(info_a.tri_idx), + // groups.at(group_b).tris.at(info_b.tri_idx), positions) + ); + } + } + } +} + +struct ConnectedComponent { + std::vector groups; +}; + +/*! + * Step 3: find connected components + */ +std::vector find_connected_components(const std::vector& nodes) { + std::vector result; + std::set added; + + // loop over each node + for (int node_idx = 0; node_idx < (int)nodes.size(); node_idx++) { + // skip nodes already part of a connected component. + if (added.find(node_idx) != added.end()) { + continue; + } + + // new node, create a new component and find everything connected. + auto& component = result.emplace_back(); + // added this first node + component.groups.push_back(node_idx); + added.insert(node_idx); + + // initialize the search with neighbors of the first node + std::vector to_visit; + for (auto x : nodes.at(node_idx).neighbors) { + to_visit.push_back(x); + } + + // find all connected! + while (!to_visit.empty()) { + int next_idx = to_visit.back(); + to_visit.pop_back(); + if (added.find(next_idx) != added.end()) { + // already seen it, skip! + continue; + } + // new node, add it + added.insert(next_idx); + component.groups.push_back(next_idx); + // also look at neighbors + for (auto x : nodes.at(next_idx).neighbors) { + if (added.find(x) == added.end()) { + to_visit.push_back(x); + } + } + } + } + return result; +} + +/*! + * Step 4: order components in bfs order + */ +ConnectedComponent bfs_order_connected_component(const ConnectedComponent& in, + const std::vector& graph) { + ConnectedComponent out; + + // add the first one + std::set added; + std::vector frontier = {in.groups.at(0)}; + + // go! + while (!frontier.empty()) { + std::stable_sort(frontier.begin(), frontier.end(), [&graph](int a, int b) { + return graph.at(a).neighbors.size() < graph.at(b).neighbors.size(); + }); + std::vector next_frontier; + + for (auto x : frontier) { + if (added.find(x) != added.end()) { + continue; + } + added.insert(x); + out.groups.push_back(x); + + auto& node = graph.at(x); + for (auto& neighbor : node.neighbors) { + if (added.find(neighbor) == added.end()) { + next_frontier.push_back(neighbor); + } + } + } + + frontier = std::move(next_frontier); + } + + return out; +} + +std::vector compute_flips(const std::vector& graph, + const std::vector& components, + const std::unordered_map& neighbor_info) { + std::vector flips; + flips.resize(graph.size(), false); + + for (const auto& component : components) { + std::set decided; + for (int node_idx : component.groups) { + const auto& group = graph.at(node_idx); + float flip_sum = 0; + for (int neighbor_idx : group.neighbors) { + if (decided.find(neighbor_idx) == decided.end()) { + continue; // skip, don't know this ones orientation + } + const auto& info = neighbor_info.at(group_pair_to_neighbor_key(node_idx, neighbor_idx)); + for (auto originally_twisted : info.matched_edge_twists) { + bool twisted = originally_twisted; + if (flips[neighbor_idx]) { + twisted = !twisted; + } + if (twisted) { + flip_sum--; + } else { + flip_sum++; + } + } + } + + if (flip_sum < 0) { + flips[node_idx] = true; + } + + decided.insert(node_idx); + } + } + + return flips; +} + +void apply_flips(const std::vector& flips, std::vector& groups) { + ASSERT(flips.size() == groups.size()); + for (size_t i = 0; i < groups.size(); i++) { + if (flips[i]) { + for (auto& tri : groups[i].tris) { + std::swap(tri.idx[0], tri.idx[1]); + } + } + } +} + +void make_final_indices(const std::vector& groups, std::vector& idx) { + for (auto& g : groups) { + for (auto& t : g.tris) { + for (auto i : t.idx) { + idx.push_back(i); + } + } + } +} + +void fixup_and_unstrip_tfrag_tie(const std::vector& stripped_indices, + const std::vector& positions, + std::vector& unstripped, + std::vector& old_to_new_start) { + // Part 1 + std::vector groups; + unstrip(stripped_indices, groups, old_to_new_start); + + // Part 2 + std::vector nodes; + std::unordered_map neighbor_info; + build_graph(nodes, neighbor_info, groups, positions); + + // Part 3 + auto connected_components = find_connected_components(nodes); + + // Part 4 + std::vector ordered_components; + for (auto& c : connected_components) { + ordered_components.push_back(bfs_order_connected_component(c, nodes)); + } + + // Part 5 + auto flips = compute_flips(nodes, ordered_components, neighbor_info); + + // Part 6 + apply_flips(flips, groups); + + // Part 7 + make_final_indices(groups, unstripped); +} \ No newline at end of file diff --git a/decompiler/level_extractor/tfrag_tie_fixup.h b/decompiler/level_extractor/tfrag_tie_fixup.h new file mode 100644 index 000000000..f986ad362 --- /dev/null +++ b/decompiler/level_extractor/tfrag_tie_fixup.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "common/common_types.h" +#include "common/math/Vector.h" + +/*! + * Fix-up tfrag/tie format mesh to a best-guess unstripped mesh with proper triangle orientation. + * The input is the tie/tfrag index list, using UINT32_MAX as primitive restart. + * The output is an unstripped index list (groups of three indices, one for each triangle) + * and a map from each element in the original index buffer to the new one. + * Ex: unstripped[old_to_new_start[i]] is the start of prim stripped_indices[i]. + * + * This depends on specific behavior of the original tfrag/tie renderers, and how + * extract_tie/extract_tfrag work (basically keeping the order of vertices exactly the same as in VU + * memory). + * + * The stripping logic of shrub/merc/generic models appears to be different, and this likely won't + * work. + */ +void fixup_and_unstrip_tfrag_tie(const std::vector& stripped_indices, + const std::vector& positions, + std::vector& unstripped, + std::vector& old_to_new_start); \ No newline at end of file diff --git a/game/graphics/opengl_renderer/Sprite3.cpp b/game/graphics/opengl_renderer/Sprite3.cpp index 7b65e4fbf..f7a17706d 100644 --- a/game/graphics/opengl_renderer/Sprite3.cpp +++ b/game/graphics/opengl_renderer/Sprite3.cpp @@ -415,7 +415,7 @@ void Sprite3::distort_dma(DmaFollower& dma, ScopedProfilerNode& /*prof*/) { } else { // VU address >= 512 is the actual vertex data ASSERT(dest >= 512); - ASSERT(sprite_idx + (qwc / 3) <= m_sprite_distorter_frame_data.capacity()); + ASSERT(sprite_idx + (qwc / 3) <= (int)m_sprite_distorter_frame_data.capacity()); unpack_to_no_stcycl(&m_sprite_distorter_frame_data.at(sprite_idx), distort_data, VifCode::Kind::UNPACK_V4_32, qwc * 16, dest, false, false); diff --git a/game/graphics/opengl_renderer/foreground/Merc2.cpp b/game/graphics/opengl_renderer/foreground/Merc2.cpp index 01807731f..6abceea36 100644 --- a/game/graphics/opengl_renderer/foreground/Merc2.cpp +++ b/game/graphics/opengl_renderer/foreground/Merc2.cpp @@ -384,7 +384,7 @@ void Merc2::handle_merc_chain(DmaFollower& dma, * Queue up some bones to be included in the bone buffer. * Returns the index of the first bone vector. */ -u32 Merc2::alloc_bones(int count, float scale) { +u32 Merc2::alloc_bones(int count) { u32 first_bone_vector = m_next_free_bone_vector; ASSERT(count * 8 + first_bone_vector <= MAX_SHADER_BONE_VECTORS); @@ -397,12 +397,10 @@ u32 Merc2::alloc_bones(int count, float scale) { auto* shader_mat = &m_shader_bone_vector_buffer[m_next_free_bone_vector]; int bv = 0; - // scale the transformation matrix (todo: can we move this to the extraction) // and copy to the large bone buffer. - for (int j = 0; j < 3; j++) { - shader_mat[bv++] = skel_mat.tmat[j] * scale; + for (int j = 0; j < 4; j++) { + shader_mat[bv++] = skel_mat.tmat[j]; } - shader_mat[bv++] = skel_mat.tmat[3]; for (int j = 0; j < 3; j++) { shader_mat[bv++] = skel_mat.nmat[j]; @@ -487,7 +485,7 @@ void Merc2::flush_pending_model(SharedRenderState* render_state, ScopedProfilerN return; } - u32 first_bone = alloc_bones(bone_count, model->scale_xyz); + u32 first_bone = alloc_bones(bone_count); // allocate lights u32 lights = alloc_lights(m_current_lights); diff --git a/game/graphics/opengl_renderer/foreground/Merc2.h b/game/graphics/opengl_renderer/foreground/Merc2.h index 2c8f83241..27f926543 100644 --- a/game/graphics/opengl_renderer/foreground/Merc2.h +++ b/game/graphics/opengl_renderer/foreground/Merc2.h @@ -54,7 +54,7 @@ class Merc2 : public BucketRenderer { void handle_matrix_dma(const DmaTransfer& dma); void flush_pending_model(SharedRenderState* render_state, ScopedProfilerNode& prof); - u32 alloc_bones(int count, float scale); + u32 alloc_bones(int count); std::optional m_current_model = std::nullopt; u16 m_current_effect_enable_bits = 0; diff --git a/game/graphics/pipelines/opengl.cpp b/game/graphics/pipelines/opengl.cpp index 3f65a5000..8602f83d8 100644 --- a/game/graphics/pipelines/opengl.cpp +++ b/game/graphics/pipelines/opengl.cpp @@ -164,7 +164,7 @@ static void gl_exit() { static std::shared_ptr gl_make_display(int width, int height, const char* title, - GfxSettings& settings, + GfxSettings& /*settings*/, GameVersion game_version, bool is_main) { GLFWwindow* window = glfwCreateWindow(width, height, title, NULL, NULL); @@ -317,7 +317,7 @@ void GLDisplay::on_window_size(GLFWwindow* /*window*/, int width, int height) { } } -void GLDisplay::on_iconify(GLFWwindow* window, int iconified) { +void GLDisplay::on_iconify(GLFWwindow* /*window*/, int iconified) { m_minimized = iconified == GLFW_TRUE; } diff --git a/game/graphics/pipelines/opengl.h b/game/graphics/pipelines/opengl.h index 4e9e798c3..ffe4a1b0b 100644 --- a/game/graphics/pipelines/opengl.h +++ b/game/graphics/pipelines/opengl.h @@ -23,21 +23,21 @@ class GLDisplay : public GfxDisplay { GLDisplay(GLFWwindow* window, bool is_main); virtual ~GLDisplay(); - void* get_window() const { return m_window; } - void get_position(int* x, int* y); - void get_size(int* w, int* h); - void get_scale(float* x, float* y); - void get_screen_size(int vmode_idx, s32* w, s32* h); - int get_screen_rate(int vmode_idx); - int get_screen_vmode_count(); - int get_monitor_count(); - void set_size(int w, int h); - void update_fullscreen(GfxDisplayMode mode, int screen); - void render(); - bool minimized(); + void* get_window() const override { return m_window; } + void get_position(int* x, int* y) override; + void get_size(int* w, int* h) override; + void get_scale(float* x, float* y) override; + void get_screen_size(int vmode_idx, s32* w, s32* h) override; + int get_screen_rate(int vmode_idx) override; + int get_screen_vmode_count() override; + int get_monitor_count() override; + void set_size(int w, int h) override; + void update_fullscreen(GfxDisplayMode mode, int screen) override; + void render() override; + bool minimized() override; bool fullscreen_pending() override; void fullscreen_flush() override; - void set_lock(bool lock); + void set_lock(bool lock) override; void on_key(GLFWwindow* window, int key, int scancode, int action, int mods); void on_window_pos(GLFWwindow* window, int xpos, int ypos); void on_window_size(GLFWwindow* window, int width, int height); diff --git a/game/kernel/jak2/kmachine.cpp b/game/kernel/jak2/kmachine.cpp index ab6f1c49c..f9ec84c60 100644 --- a/game/kernel/jak2/kmachine.cpp +++ b/game/kernel/jak2/kmachine.cpp @@ -532,7 +532,7 @@ void PutDisplayEnv(u32 /*ptr*/) { ASSERT(false); } -u32 sceGsSyncV(u32 mode) { +u32 sceGsSyncV(u32 /*mode*/) { // stub, jak2 probably works differently here ASSERT(false); return 0; diff --git a/game/kernel/jak2/kscheme.cpp b/game/kernel/jak2/kscheme.cpp index 803acab56..367fdf848 100644 --- a/game/kernel/jak2/kscheme.cpp +++ b/game/kernel/jak2/kscheme.cpp @@ -1739,22 +1739,22 @@ int InitHeapAndSymbol() { return 0; } -u64 load(u32 file_name_in, u32 heap_in) { +u64 load(u32 /*file_name_in*/, u32 /*heap_in*/) { ASSERT(false); return 0; } -u64 loadb(u32 file_name_in, u32 heap_in, u32 param3) { +u64 loadb(u32 /*file_name_in*/, u32 /*heap_in*/, u32 /*param3*/) { ASSERT(false); return 0; } -u64 loadc(const char* file_name, kheapinfo* heap, u32 flags) { +u64 loadc(const char* /*file_name*/, kheapinfo* /*heap*/, u32 /*flags*/) { ASSERT(false); return 0; } -u64 loado(u32 file_name_in, u32 heap_in) { +u64 loado(u32 /*file_name_in*/, u32 /*heap_in*/) { ASSERT(false); return 0; } @@ -1767,7 +1767,10 @@ u64 unload(u32 name) { return 0; } -s64 load_and_link(const char* filename, char* decode_name, kheapinfo* heap, u32 flags) { +s64 load_and_link(const char* /*filename*/, + char* /*decode_name*/, + kheapinfo* /*heap*/, + u32 /*flags*/) { ASSERT(false); return 0; } diff --git a/goalc/build_level/build_level.cpp b/goalc/build_level/build_level.cpp index d26722672..240974794 100644 --- a/goalc/build_level/build_level.cpp +++ b/goalc/build_level/build_level.cpp @@ -49,6 +49,7 @@ bool run_build_level(const std::string& input_file, mesh_extract_in.filename = file_util::get_file_path({level_json.at("gltf_file").get()}); mesh_extract_in.auto_wall_enable = level_json.value("automatic_wall_detection", true); + mesh_extract_in.double_sided_collide = level_json.at("double_sided_collide").get(); mesh_extract_in.auto_wall_angle = level_json.value("automatic_wall_angle", 30.0); mesh_extract_in.tex_pool = &tex_pool; gltf_mesh_extract::Output mesh_extract_out; diff --git a/goalc/build_level/collide_bvh.cpp b/goalc/build_level/collide_bvh.cpp index 049fb2c63..cd832a688 100644 --- a/goalc/build_level/collide_bvh.cpp +++ b/goalc/build_level/collide_bvh.cpp @@ -1,6 +1,7 @@ #include "collide_bvh.h" #include +#include #include "common/log/log.h" #include "common/util/Assert.h" @@ -16,18 +17,7 @@ namespace collide { namespace { -constexpr int MAX_FACES_IN_FRAG = 100; - -/*! - * The Collide node. - * Has either children collide node or children faces, but not both - * The size of child_nodes is either 0 or 8 at all times. - */ -struct CNode { - std::vector child_nodes; - std::vector faces; - math::Vector4f bsphere; -}; +constexpr int MAX_UNIQUE_VERTS_IN_FRAG = 128; struct BBox { math::Vector3f mins, maxs; @@ -38,6 +28,40 @@ struct BBox { } }; +/*! + * The Collide node. + * If it's a leaf, it has faces + * Otherwise it has 2, 4, or 8 children nodes. + */ +struct CNode { + std::vector child_nodes; + std::vector faces; + math::Vector4f bsphere; +}; + +struct VectorHash { + size_t operator()(const math::Vector3f& in) const { + return std::hash()(in.x()) ^ std::hash()(in.y()) ^ std::hash()(in.z()); + } +}; + +/*! + * Recursively get a set of unique vertices. + */ +void collect_vertices(const CNode& node, std::unordered_set& verts) { + for (auto& child : node.child_nodes) { + collect_vertices(child, verts); + } + for (auto& face : node.faces) { + verts.insert(face.v[0]); + verts.insert(face.v[1]); + verts.insert(face.v[2]); + } +} + +/*! + * Recursively get a list of vertices. + */ void collect_vertices(const CNode& node, std::vector& verts) { for (auto& child : node.child_nodes) { collect_vertices(child, verts); @@ -49,6 +73,33 @@ void collect_vertices(const CNode& node, std::vector& verts) { } } +/*! + * Get the axis-aligned bounding box of these vertices + */ +BBox compute_my_bbox(const std::vector& verts) { + ASSERT(!verts.empty()); + BBox result; + result.mins = verts.front(); + result.maxs = verts.front(); + for (auto& v : verts) { + result.mins.min_in_place(v); + result.maxs.min_in_place(v); + } + return result; +} + +/*! + * Get the axis-aligned bounding box of all vertices in this node and its children + */ +BBox compute_my_bbox(const CNode& node) { + std::vector verts; + collect_vertices(node, verts); + return compute_my_bbox(verts); +} + +/*! + * Find the vertex in verts that is most distant from pt. + */ size_t find_most_distant(math::Vector3f pt, const std::vector& verts) { float max_dist_squared = 0; size_t idx_of_best = 0; @@ -62,9 +113,10 @@ size_t find_most_distant(math::Vector3f pt, const std::vector& v return idx_of_best; } -void compute_my_bsphere_ritters(CNode& node) { - std::vector verts; - collect_vertices(node, verts); +/*! + * Compute a bounding sphere for a node and its children. + */ +void compute_my_bsphere_ritters(CNode& node, const std::vector& verts) { ASSERT(verts.size() > 0); auto px = verts[0]; auto py = verts[find_most_distant(px, verts)]; @@ -82,6 +134,15 @@ void compute_my_bsphere_ritters(CNode& node) { node.bsphere.w() = std::sqrt(max_squared); } +/*! + * Compute a bounding sphere for a node and its children. + */ +void compute_my_bsphere_ritters(CNode& node) { + std::vector verts; + collect_vertices(node, verts); + compute_my_bsphere_ritters(node, verts); +} + /*! * Split faces in two along a coordinate plane. * Will clear the input faces @@ -132,76 +193,85 @@ void split_node_once(CNode& node, CNode* out0, CNode* out1) { *out1 = temps[best_dim * 2 + 1]; } -/*! - * Split a node into 8 children and store these in the given node. - */ -void split_node_to_8_children(CNode& node) { +bool needs_split(const CNode& node) { + // quick reject. + if (node.faces.size() > 100) { + return true; + } + + if (node.bsphere.w() > (125.f * 4096.f)) { + return true; + } + ASSERT(node.child_nodes.empty()); - node.child_nodes.resize(8); - // level 0 + std::unordered_set unique_verts; + for (auto& f : node.faces) { + for (auto& v : f.v) { + unique_verts.insert(v); + } + } + + return unique_verts.size() >= 128; +} + +void split_recursive(CNode& to_split) { + ASSERT(to_split.child_nodes.empty()); + ASSERT(!to_split.faces.empty()); + CNode level0[2]; - split_node_once(node, &level0[0], &level0[1]); - // level 1 - CNode level1[4]; - split_node_once(level0[0], &level1[0], &level1[1]); - split_node_once(level0[1], &level1[2], &level1[3]); - // level 2 - split_node_once(level1[0], &node.child_nodes[0], &node.child_nodes[1]); - split_node_once(level1[1], &node.child_nodes[2], &node.child_nodes[3]); - split_node_once(level1[2], &node.child_nodes[4], &node.child_nodes[5]); - split_node_once(level1[3], &node.child_nodes[6], &node.child_nodes[7]); -} - -struct SplitResult { - size_t max_leaf_count = 0; - float max_bsphere_w = 0; -}; -/*! - * Split all leaf nodes. Returns the number of faces in the leaf with the most faces after - * splitting. - * This slightly unusual recursion pattern is to make sure we split everything to same depth, - * which we believe might be a requirement of the collision system. - */ -SplitResult split_all_leaves(CNode& node) { - SplitResult result; - if (node.child_nodes.empty()) { - // we're a leaf! - // split us: - split_node_to_8_children(node); - for (auto& child : node.child_nodes) { - result.max_leaf_count = std::max(result.max_leaf_count, child.faces.size()); - result.max_bsphere_w = std::max(result.max_bsphere_w, child.bsphere.w()); - } - return result; - } else { - // not a leaf, recurse - for (auto& child : node.child_nodes) { - auto cret = split_all_leaves(child); - result.max_bsphere_w = std::max(result.max_bsphere_w, cret.max_bsphere_w); - result.max_leaf_count = std::max(result.max_leaf_count, cret.max_leaf_count); - } - return result; - } -} - -/*! - * Main BVH construction function. Splits leaves until it is no longer needed. - */ -void split_as_needed(CNode& root) { - int initial_tri_count = root.faces.size(); - int num_leaves = 1; - bool need_to_split = true; - while (need_to_split) { - SplitResult worst = split_all_leaves(root); - num_leaves *= 8; - lg::info("after splitting, the worst leaf has {} tris, {} radius", worst.max_leaf_count, - worst.max_bsphere_w / 4096.f); - if (worst.max_leaf_count < MAX_FACES_IN_FRAG && worst.max_bsphere_w < (125.f * 4096.f)) { - need_to_split = false; + split_node_once(to_split, &level0[0], &level0[1]); + for (int i = 0; i < 2; i++) { + if (needs_split(level0[i])) { + CNode level1[2]; + split_node_once(level0[i], &level1[0], &level1[1]); + for (int j = 0; j < 2; j++) { + if (needs_split(level1[j])) { + CNode level2[2]; + split_node_once(level1[j], &level2[0], &level2[1]); + for (int k = 0; k < 2; k++) { + if (needs_split(level2[k])) { + to_split.child_nodes.push_back(std::move(level2[k])); + split_recursive(to_split.child_nodes.back()); + } else { + to_split.child_nodes.push_back(std::move(level2[k])); + } + } + } else { + to_split.child_nodes.push_back(std::move(level1[j])); + } + } + } else { + to_split.child_nodes.push_back(std::move(level0[i])); + } + } + + ASSERT(to_split.child_nodes.size() <= 8); + + bool has_leaves = false; + bool has_not_leaves = false; + for (auto& child : to_split.child_nodes) { + if (!child.faces.empty()) { + has_leaves = true; + } + if (!child.child_nodes.empty()) { + has_not_leaves = true; + } + } + + if (has_leaves && has_not_leaves) { + std::vector temp_children = std::move(to_split.child_nodes); + to_split.child_nodes = {}; + for (auto& c : temp_children) { + if (!c.faces.empty()) { + to_split.child_nodes.emplace_back(); + to_split.child_nodes.emplace_back(); + split_node_once(c, &to_split.child_nodes[to_split.child_nodes.size() - 1], + &to_split.child_nodes[to_split.child_nodes.size() - 2]); + } else { + to_split.child_nodes.push_back(std::move(c)); + } } } - lg::info("average triangles per leaf: {}", initial_tri_count / num_leaves); - lg::info("leaf count: {}", num_leaves); } /*! @@ -215,35 +285,34 @@ void bsphere_recursive(CNode& node) { } } -void drawable_layout_helper(CNode& node, int depth, CollideTree& tree_out, size_t my_idx_check) { - if (node.child_nodes.empty()) { - // we're a leaf! add us to the frags - auto& frag = tree_out.frags.frags.emplace_back(); - frag.bsphere = node.bsphere; - frag.faces = node.faces; +void drawable_layout_helper(const CNode& node_in, + CollideTree& tree_out, + DrawNode& parent_to_add_to) { + if (node_in.faces.empty()) { + ASSERT(!node_in.child_nodes.empty()); + auto& next = parent_to_add_to.draw_node_children.emplace_back(); + next.bsphere = node_in.bsphere; + for (auto& c : node_in.child_nodes) { + drawable_layout_helper(c, tree_out, next); + } + } else { - // not a leaf - if ((int)tree_out.node_arrays.size() <= depth) { - tree_out.node_arrays.resize(depth + 1); - } - ASSERT(my_idx_check == tree_out.node_arrays.at(depth).nodes.size()); - auto& draw_node = tree_out.node_arrays[depth].nodes.emplace_back(); - draw_node.bsphere = node.bsphere; - for (int i = 0; i < 8; i++) { - draw_node.children[i] = my_idx_check * 8 + i; - drawable_layout_helper(node.child_nodes.at(i), depth + 1, tree_out, draw_node.children[i]); - } + ASSERT(node_in.child_nodes.empty()); + size_t frag_idx = tree_out.frags.frags.size(); + auto& frag_out = tree_out.frags.frags.emplace_back(); + frag_out.faces = node_in.faces; + frag_out.bsphere = node_in.bsphere; + parent_to_add_to.frag_children.push_back((int)frag_idx); } } CollideTree build_collide_tree(CNode& root) { CollideTree tree; - drawable_layout_helper(root, 0, tree, 0); + drawable_layout_helper(root, tree, tree.fake_root_node); return tree; } void debug_stats(const CollideTree& tree) { - lg::info("Tree build: {} draw node layers", tree.node_arrays.size()); float sum_w = 0, max_w = 0; for (auto& frag : tree.frags.frags) { sum_w += frag.bsphere.w(); @@ -261,7 +330,7 @@ CollideTree construct_collide_bvh(const std::vector& tris) { lg::info("Building collide bvh from {} triangles", tris.size()); CNode root; root.faces = tris; - split_as_needed(root); + split_recursive(root); lg::info("BVH tree constructed in {:.2f} ms", bvh_timer.getMs()); // part 2: compute bspheres diff --git a/goalc/build_level/collide_bvh.h b/goalc/build_level/collide_bvh.h index c4abe6c0c..dca6b47e0 100644 --- a/goalc/build_level/collide_bvh.h +++ b/goalc/build_level/collide_bvh.h @@ -12,7 +12,8 @@ namespace collide { struct DrawNode { - s32 children[8] = {-1, -1, -1, -1, -1, -1, -1, -1}; + std::vector draw_node_children; + std::vector frag_children; math::Vector4f bsphere; }; @@ -30,7 +31,8 @@ struct DrawableInlineArrayCollideFrag { }; struct CollideTree { - std::vector node_arrays; + // std::vector node_arrays; + DrawNode fake_root_node; // the children of this are the ones that go in the top level. DrawableInlineArrayCollideFrag frags; }; diff --git a/goalc/build_level/collide_drawable.cpp b/goalc/build_level/collide_drawable.cpp index 788f5e058..75b6fb130 100644 --- a/goalc/build_level/collide_drawable.cpp +++ b/goalc/build_level/collide_drawable.cpp @@ -1,5 +1,7 @@ #include "collide_drawable.h" +#include + #include "common/util/Assert.h" #include "goalc/data_compiler/DataObjectGenerator.h" @@ -136,7 +138,7 @@ size_t generate_collide_fragment(DataObjectGenerator& gen, size_t generate_collide_fragment_array(DataObjectGenerator& gen, const std::vector& meshes, const std::vector& frag_mesh_locs, - std::vector& parent_ref_out) { + std::vector& loc_out) { gen.align_to_basic(); gen.add_type_tag("drawable-inline-array-collide-fragment"); // 0 size_t result = gen.current_offset_bytes(); @@ -161,22 +163,32 @@ size_t generate_collide_fragment_array(DataObjectGenerator& gen, for (int j = 0; j < 4; j++) { gen.add_word_float(mesh.bsphere[j]); } - if ((i % 8) == 0) { - parent_ref_out.push_back(me); - } + loc_out.push_back(me); } return result; } -size_t generate_collide_draw_node_array(DataObjectGenerator& gen, - const std::vector& nodes, - u32 flag, - const std::vector& children, - std::vector& parent_ref_out) { +int child_count(const collide::DrawNode* node) { + if (node->frag_children.empty()) { + return node->draw_node_children.size(); + } else { + return node->frag_children.size(); + } +} + +std::unordered_map add_draw_nodes( + DataObjectGenerator& gen, + const std::vector& nodes, + const std::vector& frag_locs, + size_t& array_out) { + std::unordered_map result; + std::unordered_map back_map; + gen.align_to_basic(); gen.add_type_tag("drawable-inline-array-node"); // 0 - size_t result = gen.current_offset_bytes(); + array_out = gen.current_offset_bytes(); + ASSERT(nodes.size() < UINT16_MAX); gen.add_word(nodes.size() << 16); // 4, 6 gen.add_word(0); // 8 gen.add_word(0); // 12 @@ -185,30 +197,72 @@ size_t generate_collide_draw_node_array(DataObjectGenerator& gen, gen.add_word(0); // 24 gen.add_word(0); // 28 - ASSERT(nodes.size() == children.size()); - for (size_t i = 0; i < nodes.size(); i++) { - auto& node = nodes[i]; + for (auto& node : nodes) { + bool is_draw_node = node->frag_children.empty(); + // should be 8 words here: gen.add_type_tag("draw-node"); // 1 size_t me = gen.current_offset_bytes(); u32 packed_flags = 0; - packed_flags |= (8 << 16); // TODO hard-coded size here - packed_flags |= (flag << 24); - gen.add_word(packed_flags); // 2 - gen.link_word_to_byte(gen.add_word(0), children[i]); // 3 - gen.add_word(0); // 4 - if ((i % 8) == 0) { - parent_ref_out.push_back(me); + packed_flags |= + ((is_draw_node ? node->draw_node_children.size() : node->frag_children.size()) << 16); + packed_flags |= ((is_draw_node ? 1 : 0) << 24); + gen.add_word(packed_flags); // 2 + if (is_draw_node) { + // gen.link_word_to_byte(gen.add_word(0), result.at(&node->draw_node_children.at(0))); // 3 + back_map[gen.add_word(0)] = &node->draw_node_children.at(0); + } else { + gen.link_word_to_byte(gen.add_word(0), frag_locs.at(node->frag_children.at(0))); } - gen.add_word_float(node.bsphere.x()); // 5 - gen.add_word_float(node.bsphere.y()); // 6 - gen.add_word_float(node.bsphere.z()); // 7 - gen.add_word_float(node.bsphere.w()); // 8 + + result[node] = me; + + gen.add_word(0); // 4 + + gen.add_word_float(node->bsphere.x()); // 5 + gen.add_word_float(node->bsphere.y()); // 6 + gen.add_word_float(node->bsphere.z()); // 7 + gen.add_word_float(node->bsphere.w()); // 8 + } + + for (const auto& [loc, node] : back_map) { + gen.link_word_to_byte(loc, result.at(node)); } return result; } +std::vector bfs_nodes(const collide::DrawNode& fake_root) { + std::vector out; + std::unordered_set added; + std::vector frontier; + for (auto& dnc : fake_root.draw_node_children) { + frontier.push_back(&dnc); + } + + while (!frontier.empty()) { + std::vector next_frontier; + + for (auto x : frontier) { + if (added.find(x) != added.end()) { + continue; + } + added.insert(x); + out.push_back(x); + + for (auto& child : x->draw_node_children) { + if (added.find(&child) == added.end()) { + next_frontier.push_back(&child); + } + } + } + + frontier = std::move(next_frontier); + } + + return out; +} + size_t DrawableTreeCollideFragment::add_to_object_file(DataObjectGenerator& gen) const { // generate pat array size_t pat_array_loc = generate_pat_array(gen, packed_frags.pats); @@ -226,38 +280,64 @@ size_t DrawableTreeCollideFragment::add_to_object_file(DataObjectGenerator& gen) packed_data_locs[i], pat_array_loc)); } - std::vector array_locs; - array_locs.resize(bvh.node_arrays.size() + 1); // plus one for the frags. - int array_slot = bvh.node_arrays.size(); - std::vector children_refs; - array_locs[array_slot--] = generate_collide_fragment_array(gen, packed_frags.packed_frag_data, - collide_frag_meshes, children_refs); - u32 flag = 0; - while (array_slot >= 0) { - ASSERT(children_refs.size() == bvh.node_arrays.at(array_slot).nodes.size()); - std::vector next_children; + generate_collide_fragment_array(gen, packed_frags.packed_frag_data, collide_frag_meshes, + children_refs); - array_locs[array_slot] = generate_collide_draw_node_array( - gen, bvh.node_arrays.at(array_slot).nodes, flag, children_refs, next_children); + auto order = bfs_nodes(bvh.fake_root_node); + size_t others_array; + auto located_nodes = add_draw_nodes(gen, order, children_refs, others_array); - children_refs = std::move(next_children); - array_slot--; - flag = 1; + size_t root_dian = -1; + { + gen.align_to_basic(); + gen.add_type_tag("drawable-inline-array-node"); // 0 + root_dian = gen.current_offset_bytes(); + gen.add_word(bvh.fake_root_node.draw_node_children.size() << 16); // 4, 6 + gen.add_word(0); // 8 + gen.add_word(0); // 12 + gen.add_word(0); // 16 + gen.add_word(0); // 20 + gen.add_word(0); // 24 + gen.add_word(0); // 28 + + for (auto& node : bvh.fake_root_node.draw_node_children) { + bool is_draw_node = node.frag_children.empty(); + + // should be 8 words here: + gen.add_type_tag("draw-node"); // 1 + + u32 packed_flags = 0; + packed_flags |= + ((is_draw_node ? node.draw_node_children.size() : node.frag_children.size()) << 16); + packed_flags |= ((is_draw_node ? 1 : 0) << 24); + gen.add_word(packed_flags); // 2 + if (is_draw_node) { + gen.link_word_to_byte(gen.add_word(0), + located_nodes.at(&node.draw_node_children.at(0))); // 3 + } else { + gen.link_word_to_byte(gen.add_word(0), children_refs.at(node.frag_children.at(0))); + } + + gen.add_word(0); // 4 + gen.add_word_float(node.bsphere.x()); // 5 + gen.add_word_float(node.bsphere.y()); // 6 + gen.add_word_float(node.bsphere.z()); // 7 + gen.add_word_float(node.bsphere.w()); // 8 + } } { gen.align_to_basic(); gen.add_type_tag("drawable-tree-collide-fragment"); size_t result = gen.current_offset_bytes(); - gen.add_word((array_locs.size() - 1) << 16); // todo the minus one here?? + gen.add_word(2 << 16); // todo the minus one here?? for (int i = 0; i < 6; i++) { gen.add_word(0); } - for (size_t i = 1; i < array_locs.size(); i++) { // todo the offset here? - gen.link_word_to_byte(gen.add_word(0), array_locs[i]); - } + gen.link_word_to_byte(gen.add_word(0), root_dian); + gen.link_word_to_byte(gen.add_word(0), others_array); return result; } diff --git a/goalc/build_level/collide_pack.cpp b/goalc/build_level/collide_pack.cpp index fe7c9d72c..511e695ab 100644 --- a/goalc/build_level/collide_pack.cpp +++ b/goalc/build_level/collide_pack.cpp @@ -200,6 +200,12 @@ CollideFragMeshDataArray pack_collide_frags(const std::vector 128) { + fmt::print("frag with too many vertices: {} had {} tris\n", frag_out.vertex_count, + frag_in.faces.size()); + lg::error("SHOULD CRASH\n"); + } + // the frag_out.packed_data.resize(sizeof(u16) * frag_out.vertex_count * 3); memcpy(frag_out.packed_data.data(), indexed.vertices_u16.vertex.data(), frag_out.packed_data.size()); @@ -228,12 +234,14 @@ CollideFragMeshDataArray pack_collide_frags(const std::vector extract_vec2f(const u8* data, u32 count, u32 stride) } /*! - * Convert a GLTF color buffer to u8 colors. + * Convert a GLTF color buffer (u16 format) to u8 colors. */ std::vector> extract_color_from_vec4_u16(const u8* data, u32 count, @@ -111,6 +111,9 @@ struct ExtractedVertices { std::vector normals; }; +/*! + * Extract positions, colors, and normals from a mesh. + */ ExtractedVertices gltf_vertices(const tinygltf::Model& model, const std::map& attributes, const math::Matrix4f& w_T_local, @@ -298,6 +301,8 @@ int texture_pool_add_texture(TexturePool* pool, const tinygltf::Image& tex) { if (existing != pool->textures_by_name.end()) { lg::info("Reusing image: {}", tex.name); return existing->second; + } else { + lg::info("adding new texture: {}, size {} kB", tex.name, tex.width * tex.height * 4 / 1024); } ASSERT(tex.bits == 8); @@ -313,9 +318,9 @@ int texture_pool_add_texture(TexturePool* pool, const tinygltf::Image& tex) { tt.debug_tpage_name = "custom-level"; tt.load_to_pool = false; tt.combo_id = 0; // doesn't matter, not a pool tex - tt.data.resize(tt.w * tt.h * 4); + tt.data.resize(tt.w * tt.h); ASSERT(tex.image.size() >= tt.data.size()); - memcpy(tt.data.data(), tex.image.data(), tt.data.size()); + memcpy(tt.data.data(), tex.image.data(), tt.data.size() * 4); return idx; } } // namespace @@ -671,7 +676,7 @@ struct PatResult { PatSurface pat; }; -PatResult custom_props_to_pat(const tinygltf::Value& val, const std::string& debug_name) { +PatResult custom_props_to_pat(const tinygltf::Value& val, const std::string& /*debug_name*/) { PatResult result; if (!val.IsObject() || !val.Has("set_collision") || !val.Get("set_collision").Get()) { // unset. @@ -806,6 +811,16 @@ void extract(const Input& in, fixed_faces.push_back(face); } } + + if (in.double_sided_collide) { + size_t os = fixed_faces.size(); + for (size_t i = 0; i < os; i++) { + auto f0 = fixed_faces.at(i); + std::swap(f0.v[0], f0.v[1]); + fixed_faces.push_back(f0); + } + } + out.faces = std::move(fixed_faces); if (in.auto_wall_enable) { diff --git a/goalc/build_level/gltf_mesh_extract.h b/goalc/build_level/gltf_mesh_extract.h index 2980a8873..1c5c62a89 100644 --- a/goalc/build_level/gltf_mesh_extract.h +++ b/goalc/build_level/gltf_mesh_extract.h @@ -15,6 +15,7 @@ struct Input { bool get_colors = true; bool auto_wall_enable = true; float auto_wall_angle = 30.f; + bool double_sided_collide = false; }; struct TfragOutput { diff --git a/test/offline/offline_test_main.cpp b/test/offline/offline_test_main.cpp index 4147d8a9f..20cdc3d20 100644 --- a/test/offline/offline_test_main.cpp +++ b/test/offline/offline_test_main.cpp @@ -423,7 +423,7 @@ int main(int argc, char* argv[]) { lg::info("Finding files..."); auto files = find_files(game_name, config->dgos); - if (max_files > 0 && max_files < files.size()) { + if (max_files > 0 && max_files < (int)files.size()) { files.erase(files.begin() + max_files, files.end()); } diff --git a/third-party/libco/CMakeLists.txt b/third-party/libco/CMakeLists.txt index 0224c4b76..1c8527b06 100644 --- a/third-party/libco/CMakeLists.txt +++ b/third-party/libco/CMakeLists.txt @@ -1,4 +1,4 @@ -set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD 11) set(LIBCO_SOURCES libco.c diff --git a/tools/memory_dump_tool/main.cpp b/tools/memory_dump_tool/main.cpp index 309e6455c..ad1ef6f9f 100644 --- a/tools/memory_dump_tool/main.cpp +++ b/tools/memory_dump_tool/main.cpp @@ -582,11 +582,6 @@ void inspect_basics(const Ram& ram, } } -static bool ends_with(const std::string& str, const std::string& suffix) { - return str.size() >= suffix.size() && - 0 == str.compare(str.size() - suffix.size(), suffix.size(), suffix); -} - void inspect_symbols(const Ram& ram, const std::unordered_map& types, const SymbolMap& symbols) {