From 641fb572e876750d8618434d9f7c1a531b43b366 Mon Sep 17 00:00:00 2001 From: Hat Kid <6624576+Hat-Kid@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:15:24 +0200 Subject: [PATCH] decompiler: misc gltf exporter fixes (#4371) This PR adds various fixes to the GLTF exporter: - Merc and TIE envmaps now properly extract - Changed joint offset in `convert_per_vertex_data` since we now export the `align` bone - The bone accessor for merc models now uses `TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE`, fixing a decompiler crash when attempting to reimport an extracted model into the game without first importing it into Blender and re-exporting - All model buffers now get merged into one large buffer before export, saving some space and increasing parsing speed of GLB files - Tfrags, TIEs and shrubs are now no longer stored in one giant mesh per tree and get grouped into categories: - Tfrags will be grouped by the tfrag tree name they come from (normal, trans, dirt, ice, etc.) and get separated by the texture their material uses (tfrags are not instanced, so this is the next best solution) - TIEs and shrubs get grouped by tree index and separated by their proto name (TIE wind will also be grouped separately) image image image image image --- common/util/gltf_util.cpp | 3 +- decompiler/level_extractor/extract_shrub.cpp | 9 +- decompiler/level_extractor/extract_tie.cpp | 13 +- decompiler/level_extractor/fr3_to_gltf.cpp | 495 +++++++++++++++---- 4 files changed, 422 insertions(+), 98 deletions(-) diff --git a/common/util/gltf_util.cpp b/common/util/gltf_util.cpp index 682fe224bf..12f8508e78 100644 --- a/common/util/gltf_util.cpp +++ b/common/util/gltf_util.cpp @@ -160,8 +160,7 @@ JointsAndWeights convert_per_vertex_data(const math::Vector4f& weights, if (src == discard_idx) { continue; } - // this +1 is to account for align not existing in the gltf. - ret.joints[dst] = joints[src] + 2; + ret.joints[dst] = joints[src] + 1; ret.weights[dst] = weights[src]; sum += ret.weights[dst]; dst++; diff --git a/decompiler/level_extractor/extract_shrub.cpp b/decompiler/level_extractor/extract_shrub.cpp index ec54718cf4..34591183be 100644 --- a/decompiler/level_extractor/extract_shrub.cpp +++ b/decompiler/level_extractor/extract_shrub.cpp @@ -511,8 +511,7 @@ void make_draws(tfrag3::Level& lev, if (existing_draws_in_tex != static_draws_by_tex.end()) { for (auto idx : existing_draws_in_tex->second) { auto& candidate_draw_out = tree_out.static_draws.at(idx); - if (candidate_draw_out.mode == mode && (!tree_out.has_per_proto_visibility_toggle || - candidate_draw_out.proto_idx == proto_idx)) { + if (candidate_draw_out.mode == mode && candidate_draw_out.proto_idx == proto_idx) { draw_to_add_to = &tree_out.static_draws[idx]; verts_to_add_to = &indices_regrouped_by_draw[idx]; } @@ -526,9 +525,9 @@ void make_draws(tfrag3::Level& lev, draw_to_add_to = &tree_out.static_draws.back(); draw_to_add_to->mode = mode; draw_to_add_to->tree_tex_id = idx_in_lev_data; - if (tree_out.has_per_proto_visibility_toggle) { - draw_to_add_to->proto_idx = proto_idx; - } + // jak 1 has no per-proto visibility toggle, but the gltf exporter still uses this to + // split the tree back up into one mesh per prototype + draw_to_add_to->proto_idx = proto_idx; verts_to_add_to = &indices_regrouped_by_draw.emplace_back(); } diff --git a/decompiler/level_extractor/extract_tie.cpp b/decompiler/level_extractor/extract_tie.cpp index 5d3d8d63e4..db7b71bd5c 100644 --- a/decompiler/level_extractor/extract_tie.cpp +++ b/decompiler/level_extractor/extract_tie.cpp @@ -2496,11 +2496,10 @@ void handle_draw_for_strip(tfrag3::TieTree& tree, ASSERT(inst.vis_id < UINT16_MAX); vgroup.vis_idx_in_pc_bvh = inst.vis_id; // associate with the instance for culling - // only bother with tie proto idx if we use it - if (tree.has_per_proto_visibility_toggle) { - ASSERT(proto_idx < UINT16_MAX); - vgroup.tie_proto_idx = proto_idx; - } + // jak 1 has no per-proto visibility toggle, but the gltf exporter still uses this to split the + // tree back up into one mesh per prototype + ASSERT(proto_idx < UINT16_MAX); + vgroup.tie_proto_idx = proto_idx; vgroup.num_inds = strip.verts.size() + 1; // one for the primitive restart! vgroup.num_tris = strip.verts.size() - 2; @@ -2560,9 +2559,7 @@ void add_vertices_and_static_draw(tfrag3::TieTree& tree, // loop over all prototypes for (size_t proto_idx = 0; proto_idx < protos.size(); proto_idx++) { const auto& proto = protos[proto_idx]; - if (tree.has_per_proto_visibility_toggle) { - tree.proto_names.push_back(proto.name); - } + tree.proto_names.push_back(proto.name); TieCategoryInfo info; switch (version) { diff --git a/decompiler/level_extractor/fr3_to_gltf.cpp b/decompiler/level_extractor/fr3_to_gltf.cpp index c75e3202fb..acf73dc5de 100644 --- a/decompiler/level_extractor/fr3_to_gltf.cpp +++ b/decompiler/level_extractor/fr3_to_gltf.cpp @@ -1,6 +1,8 @@ #include "fr3_to_gltf.h" #include +#include +#include #include "common/custom_data/Tfrag3Data.h" #include "common/math/Vector.h" @@ -92,33 +94,74 @@ void unstrip_tie_wind(std::vector& unstripped, } } +float merc_should_swap_tris(const std::vector& tris, + const std::vector& vertices) { + float sum = 0; + for (size_t i = 0; i + 2 < tris.size(); i += 3) { + const auto& a = vertices[tris[i]]; + const auto& b = vertices[tris[i + 1]]; + const auto& c = vertices[tris[i + 2]]; + const auto geometric = (b.pos_vec - a.pos_vec).cross(c.pos_vec - a.pos_vec); + const auto from_vertices = a.normal_vec + b.normal_vec + c.normal_vec; + const float scale = geometric.length() * from_vertices.length(); + if (scale > 0) { + sum += geometric.dot(from_vertices) / scale; + } + } + return sum; +} + /*! * Convert merc strips. Doesn't assume anything about strips. Output is [effect][draw] format (done * for each model) */ void unstrip_merc_draws(const std::vector& stripped_indices, const tfrag3::MercModel& model, + const std::vector& vertices, std::vector& unstripped, std::vector>& draw_to_start, std::vector>& draw_to_count) { + // triangles of the strip we're currently in, held back so we can flip the whole thing at once + std::vector strip; + + auto flush_strip = [&]() { + if (strip.empty()) { + return; + } + if (merc_should_swap_tris(strip, vertices) < 0) { + for (size_t i = 0; i + 2 < strip.size(); i += 3) { + std::swap(strip[i], strip[i + 1]); + } + } + unstripped.insert(unstripped.end(), strip.begin(), strip.end()); + strip.clear(); + }; + for (auto& effect : model.effects) { auto& effect_dts = draw_to_start.emplace_back(); auto& effect_dtc = draw_to_count.emplace_back(); for (auto& draw : effect.all_draws) { effect_dts.push_back(unstripped.size()); + // a triangle strip flips every other triangle, so un-flip them here. the toggle restarts + // with each strip. + bool toggle = false; 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) { + flush_strip(); + toggle = false; continue; } - unstripped.push_back(a); - unstripped.push_back(b); - unstripped.push_back(c); + strip.push_back(a); + strip.push_back(toggle ? b : c); + strip.push_back(toggle ? c : b); + toggle = !toggle; } + flush_strip(); effect_dtc.push_back(unstripped.size() - effect_dts.back()); } } @@ -483,11 +526,12 @@ int make_shrub_index_buffer_view(const std::vector& indices, int make_merc_index_buffer_view(const std::vector& indices, const tfrag3::MercModel& mmodel, + const std::vector& vertices, tinygltf::Model& model, std::vector>& draw_to_start, std::vector>& draw_to_count) { std::vector unstripped; - unstrip_merc_draws(indices, mmodel, unstripped, draw_to_start, draw_to_count); + unstrip_merc_draws(indices, mmodel, vertices, unstripped, draw_to_start, draw_to_count); // first create a buffer: int buffer_idx = (int)model.buffers.size(); @@ -536,11 +580,38 @@ int make_index_buffer_accessor(tinygltf::Model& model, u32 start, u32 count, int return accessor_idx; } +enum class TexImageKind { + RGBA, + ENVMAP_STRENGTH, +}; + +using TexImageMap = std::map, int>; + +struct EnvmapInfo { + int texture_idx = -1; + DrawMode mode; +}; + +bool blends_with_src_alpha(const DrawMode& mode) { + if (!mode.get_ab_enable()) { + return false; + } + switch (mode.get_alpha_blend()) { + case DrawMode::AlphaBlend::SRC_DST_SRC_DST: + case DrawMode::AlphaBlend::SRC_0_SRC_DST: + case DrawMode::AlphaBlend::ZERO_SRC_SRC_DST: + return true; + default: + return false; + } +} + 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); + TexImageMap& tex_image_map, + TexImageKind kind) { + const auto& existing = tex_image_map.find({tex_idx, kind}); if (existing != tex_image_map.end()) { return existing->second; } @@ -558,15 +629,49 @@ int add_image_for_tex(const tfrag3::Level& level, image.name = tex.debug_name; memcpy(image.image.data(), tex.data.data(), tex.data.size() * 4); - tex_image_map[tex_idx] = image_idx; + if (kind == TexImageKind::ENVMAP_STRENGTH) { + image.name = tex.debug_name + "-envmap-strength"; + for (size_t i = 0; i < tex.data.size(); i++) { + const u8 metallic = (u8)std::min(255, (u32)image.image[i * 4 + 3] * 2); + image.image[i * 4 + 0] = 255; + image.image[i * 4 + 1] = 0; + image.image[i * 4 + 2] = metallic; + image.image[i * 4 + 3] = 255; + } + } + + tex_image_map[{tex_idx, kind}] = image_idx; return image_idx; } +int add_gltf_texture(tinygltf::Model& model, + int image_idx, + const std::string& name, + const DrawMode& draw_mode) { + int texture_idx = (int)model.textures.size(); + auto& gltf_texture = model.textures.emplace_back(); + gltf_texture.name = name; + gltf_texture.source = image_idx; + gltf_texture.sampler = (int)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 = name; + return texture_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) { + TexImageMap& tex_image_map, + const DrawMode& draw_mode, + const EnvmapInfo* envmap = nullptr) { if (tex_idx < 0) { return 0; } @@ -579,29 +684,54 @@ int add_material_for_tex(const tfrag3::Level& level, // 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.pbrMetallicRoughness.baseColorTexture.index = add_gltf_texture( + model, add_image_for_tex(level, model, tex_idx, tex_image_map, TexImageKind::RGBA), + tex.debug_name, draw_mode); 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; + // metallic is reserved for the envmap strength below + mat.pbrMetallicRoughness.metallicFactor = 0.0; - gltf_texture.source = add_image_for_tex(level, model, tex_idx, tex_image_map); + if (envmap && envmap->texture_idx >= 0) { + mat.name = tex.debug_name + "-envmap"; + if (!blends_with_src_alpha(draw_mode)) { + mat.alphaMode = "OPAQUE"; + } + mat.pbrMetallicRoughness.metallicFactor = 1.0; + mat.pbrMetallicRoughness.roughnessFactor = 1.0; + mat.pbrMetallicRoughness.metallicRoughnessTexture.texCoord = 0; + mat.pbrMetallicRoughness.metallicRoughnessTexture.index = add_gltf_texture( + model, + add_image_for_tex(level, model, tex_idx, tex_image_map, TexImageKind::ENVMAP_STRENGTH), + tex.debug_name + "-envmap-strength", draw_mode); + + const auto& envmap_tex = level.textures.at(envmap->texture_idx); + tinygltf::Value::Object specular_color_texture; + specular_color_texture["index"] = tinygltf::Value(add_gltf_texture( + model, + add_image_for_tex(level, model, envmap->texture_idx, tex_image_map, TexImageKind::RGBA), + envmap_tex.debug_name, envmap->mode)); + tinygltf::Value::Object specular; + specular["specularColorTexture"] = tinygltf::Value(specular_color_texture); + mat.extensions["KHR_materials_specular"] = tinygltf::Value(specular); + + if (std::find(model.extensionsUsed.begin(), model.extensionsUsed.end(), + "KHR_materials_specular") == model.extensionsUsed.end()) { + model.extensionsUsed.push_back("KHR_materials_specular"); + } + } return mat_idx; } +std::string texture_name(const tfrag3::Level& level, s32 tree_tex_id) { + if (tree_tex_id < 0) { + return fmt::format("anim-slot-{}", -(tree_tex_id + 1)); + } + return level.textures.at(tree_tex_id).debug_name; +} + constexpr int kMaxColor = 1; /*! * Add the given tfrag data to a node under tfrag_root. @@ -609,7 +739,7 @@ constexpr int kMaxColor = 1; void add_tfrag(const tfrag3::Level& level, const tfrag3::TfragTree& tfrag_in, tinygltf::Model& model, - std::unordered_map& tex_image_map) { + TexImageMap& tex_image_map) { // copy and unpack in place tfrag3::TfragTree tfrag = tfrag_in; tfrag.unpack(); @@ -617,12 +747,10 @@ void add_tfrag(const tfrag3::Level& level, // 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(); + node.name = + fmt::format("{}-tfrag-{}", level.level_name, tfrag3::tfrag_tree_names[(int)tfrag.kind]); 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; @@ -634,23 +762,132 @@ void add_tfrag(const tfrag3::Level& level, 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]; + std::vector tex_order; + std::map> draws_by_tex; + for (size_t draw_idx = 0; draw_idx < tfrag.draws.size(); draw_idx++) { + const s32 tex = tfrag.draws[draw_idx].tree_tex_id; + if (draws_by_tex.find(tex) == draws_by_tex.end()) { + tex_order.push_back(tex); } - prim.mode = TINYGLTF_MODE_TRIANGLES; + draws_by_tex[tex].push_back(draw_idx); } + + for (s32 tex : tex_order) { + int c_node_idx = (int)model.nodes.size(); + auto& c_node = model.nodes.emplace_back(); + c_node.name = texture_name(level, tex); + model.nodes[node_idx].children.push_back(c_node_idx); + int mesh_idx = (int)model.meshes.size(); + model.meshes.emplace_back(); + c_node.mesh = mesh_idx; + + for (size_t draw_idx : draws_by_tex.at(tex)) { + const auto& draw = tfrag.draws[draw_idx]; + auto& prim = model.meshes[mesh_idx].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; + } + } +} + +std::unordered_map tie_envmap_by_run_start(const tfrag3::TieTree& tie) { + std::unordered_map result; + for (int cat = 0; cat < tfrag3::kNumTieCategories; cat++) { + if (!tfrag3::is_envmap_second_draw_category((tfrag3::TieCategory)cat)) { + continue; + } + for (u32 draw_idx = tie.category_draw_indices[cat]; + draw_idx < tie.category_draw_indices[cat + 1]; draw_idx++) { + const auto& draw = tie.static_draws.at(draw_idx); + for (const auto& run : draw.runs) { + auto& info = result[run.vertex0]; + info.texture_idx = draw.tree_tex_id; + info.mode = draw.mode; + } + } + } + return result; +} + +// a contiguous run of the gltf index buffer, in indices +struct IndexRange { + u32 start = 0; + u32 count = 0; +}; + +// the part of a tie draw that belongs to a single prototype +struct TieProtoDraw { + u16 proto_idx = 0; + u32 draw_idx = 0; + bool operator<(const TieProtoDraw& other) const { + return std::tie(proto_idx, draw_idx) < std::tie(other.proto_idx, other.draw_idx); + } +}; + +int make_tie_index_buffer_view_by_proto(const tfrag3::TieTree& tie, + const std::vector& draws_to_emit, + tinygltf::Model& model, + std::map& ranges_out) { + std::vector unstripped, old_to_new; + unstrip_tfrag_tie(tie.unpacked.indices, extract_positions(tie.unpacked.vertices), unstripped, + old_to_new); + + std::map> src_ranges; + for (u32 draw_idx : draws_to_emit) { + const auto& draw = tie.static_draws.at(draw_idx); + u32 old_start = draw.unpacked.idx_of_first_idx_in_full_buffer; + for (const auto& grp : draw.vis_groups) { + if (grp.num_tris) { + src_ranges[{grp.tie_proto_idx, draw_idx}].push_back( + {old_to_new.at(old_start), grp.num_tris * 3}); + } + old_start += grp.num_inds; + } + } + + std::vector grouped; + for (const auto& [key, ranges] : src_ranges) { + const u32 start = grouped.size(); + for (const auto& [src, count] : ranges) { + grouped.insert(grouped.end(), unstripped.begin() + src, unstripped.begin() + src + count); + } + ranges_out[key] = {start, (u32)grouped.size() - start}; + } + + int buffer_idx = (int)model.buffers.size(); + auto& buffer = model.buffers.emplace_back(); + buffer.data.resize(sizeof(u32) * grouped.size()); + memcpy(buffer.data.data(), grouped.data(), buffer.data.size()); + + 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; +} + +std::string proto_name(const std::vector& names, u16 proto_idx, const char* kind) { + if (proto_idx < names.size() && !names[proto_idx].empty()) { + return names[proto_idx]; + } + return fmt::format("{}-proto-{}", kind, proto_idx); } void add_tie(const tfrag3::Level& level, const tfrag3::TieTree& tie_in, + size_t tree_idx, tinygltf::Model& model, - std::unordered_map& tex_image_map) { + TexImageMap& tex_image_map) { // copy and unpack in place tfrag3::TieTree tie = tie_in; tie.unpack(); @@ -658,27 +895,68 @@ void add_tie(const tfrag3::Level& level, // 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(); + node.name = fmt::format("{}-tie-{}", level.level_name, tree_idx); 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); + const auto envmap_by_run_start = tie_envmap_by_run_start(tie); + + std::vector draws_to_emit; + std::map draw_material; + for (int cat = 0; cat < tfrag3::kNumTieCategories; cat++) { + const auto category = (tfrag3::TieCategory)cat; + if (tfrag3::is_envmap_second_draw_category(category)) { + continue; + } + for (u32 draw_idx = tie.category_draw_indices[cat]; + draw_idx < tie.category_draw_indices[cat + 1]; draw_idx++) { + const auto& draw = tie.static_draws[draw_idx]; + + EnvmapInfo envmap_info; + const EnvmapInfo* envmap = nullptr; + if (tfrag3::is_envmap_first_draw_category(category) && !draw.runs.empty()) { + const auto& first_run = draw.runs.front(); + const auto it = envmap_by_run_start.find(first_run.vertex0 + first_run.length); + if (it != envmap_by_run_start.end() && it->second.texture_idx >= 0 && + (size_t)it->second.texture_idx < level.textures.size()) { + envmap_info = it->second; + envmap = &envmap_info; + } + } + + draws_to_emit.push_back(draw_idx); + draw_material[draw_idx] = + add_material_for_tex(level, model, draw.tree_tex_id, tex_image_map, draw.mode, envmap); + } + } + + std::map ranges; + int index_buffer_view = make_tie_index_buffer_view_by_proto(tie, draws_to_emit, model, ranges); + + int mesh_idx = -1; + int prev_proto = -1; + for (const auto& [key, range] : ranges) { + if (key.proto_idx != prev_proto) { + prev_proto = key.proto_idx; + int c_node_idx = (int)model.nodes.size(); + auto& c_node = model.nodes.emplace_back(); + c_node.name = proto_name(tie.proto_names, key.proto_idx, "tie"); + model.nodes[node_idx].children.push_back(c_node_idx); + mesh_idx = (int)model.meshes.size(); + model.meshes.emplace_back(); + c_node.mesh = mesh_idx; + } + + auto& prim = model.meshes[mesh_idx].primitives.emplace_back(); + prim.material = draw_material.at(key.draw_idx); + prim.indices = make_index_buffer_accessor(model, range.start, range.count, index_buffer_view); prim.attributes["POSITION"] = position_buffer_accessor; prim.attributes["TEXCOORD_0"] = texture_buffer_accessor; for (int i = 0; i < kMaxColor; i++) { @@ -700,6 +978,7 @@ void add_tie(const tfrag3::Level& level, const auto& grp = wind_draw.instance_groups[grp_idx]; int c_node_idx = (int)model.nodes.size(); auto& c_node = model.nodes.emplace_back(); + c_node.name = fmt::format("wind-group-{}-{}", draw_idx, grp_idx); model.nodes[node_idx].children.push_back(c_node_idx); int c_mesh_idx = (int)model.meshes.size(); auto& c_mesh = model.meshes.emplace_back(); @@ -731,8 +1010,9 @@ void add_tie(const tfrag3::Level& level, void add_shrub(const tfrag3::Level& level, const tfrag3::ShrubTree& shrub_in, + size_t tree_idx, tinygltf::Model& model, - std::unordered_map& tex_image_map) { + TexImageMap& tex_image_map) { // copy and unpack in place tfrag3::ShrubTree shrub = shrub_in; shrub.unpack(); @@ -740,12 +1020,9 @@ void add_shrub(const tfrag3::Level& level, // 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(); + node.name = fmt::format("{}-shrub-{}", level.level_name, tree_idx); 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); @@ -757,19 +1034,34 @@ void add_shrub(const tfrag3::Level& level, colors[i] = make_color_buffer_accessor(shrub.unpacked.vertices, model, shrub, i); } - // for (auto& draw : shrub.static_draws) { + std::map> draws_by_proto; 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]; + draws_by_proto[shrub.static_draws[draw_idx].proto_idx].push_back(draw_idx); + } + + for (const auto& [proto_idx, draw_indices] : draws_by_proto) { + int c_node_idx = (int)model.nodes.size(); + auto& c_node = model.nodes.emplace_back(); + c_node.name = proto_name(shrub.proto_names, proto_idx, "shrub"); + model.nodes[node_idx].children.push_back(c_node_idx); + int mesh_idx = (int)model.meshes.size(); + model.meshes.emplace_back(); + c_node.mesh = mesh_idx; + + for (size_t draw_idx : draw_indices) { + auto& draw = shrub.static_draws[draw_idx]; + auto& prim = model.meshes[mesh_idx].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; } - prim.mode = TINYGLTF_MODE_TRIANGLES; } } @@ -816,8 +1108,9 @@ int make_normal_buffer_accessor(const std::vector& vertices, // and fill it u8* buffer_ptr = buffer.data.data(); for (const auto& vtx : vertices) { - auto tmp_vtx = vtx.normal_vec.normalized(); - memcpy(buffer_ptr, tmp_vtx.data(), 3 * sizeof(float)); + const float len = vtx.normal_vec.length(); + const auto normal = len > 0 ? vtx.normal_vec / len : math::Vector3f(0, 1, 0); + memcpy(buffer_ptr, normal.data(), 3 * sizeof(float)); buffer_ptr += 3 * sizeof(float); } @@ -846,18 +1139,18 @@ int make_bones_accessor(const std::vector& vertices, tinyglt // 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()); + buffer.data.resize(4 * vertices.size()); // and fill it u8* buffer_ptr = buffer.data.data(); for (const auto& vtx : vertices) { - s32 indices[4]; + u8 indices[4]; for (int i = 0; i < 3; i++) { indices[i] = vtx.mats[i] ? vtx.mats[i] - 1 : 0; } indices[3] = 0; - memcpy(buffer_ptr, indices, 4 * sizeof(s32)); - buffer_ptr += 4 * sizeof(s32); + memcpy(buffer_ptr, indices, 4); + buffer_ptr += 4; } // create a view of this buffer @@ -873,7 +1166,7 @@ int make_bones_accessor(const std::vector& vertices, tinyglt auto& accessor = model.accessors.emplace_back(); accessor.bufferView = buffer_view_idx; accessor.byteOffset = 0; - accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT; // blender doesn't support INT... + accessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; accessor.count = vertices.size(); accessor.type = TINYGLTF_TYPE_VEC4; return accessor_idx; @@ -1323,7 +1616,7 @@ void add_merc(const tfrag3::Level& level, const std::map& art_data, const tfrag3::MercModel& mmodel, tinygltf::Model& model, - std::unordered_map& tex_image_map, + TexImageMap& tex_image_map, const std::unordered_map& anim_slot_to_base_tex) { const auto& mverts = level.merc_data.vertices; @@ -1332,8 +1625,8 @@ void add_merc(const tfrag3::Level& level, 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, mmodel, model, - draw_to_start, draw_to_count); + int index_buffer_view = make_merc_index_buffer_view(level.merc_data.indices, mmodel, mverts, + model, draw_to_start, draw_to_count); int colors = make_color_buffer_accessor(mverts, model); auto joints_accessor = make_bones_accessor(mverts, model); @@ -1437,6 +1730,15 @@ void add_merc(const tfrag3::Level& level, for (size_t effect_idx = 0; effect_idx < mmodel.effects.size(); effect_idx++) { const auto& effect = mmodel.effects[effect_idx]; + + EnvmapInfo envmap_info; + const EnvmapInfo* envmap = nullptr; + if (effect.has_envmap && effect.envmap_texture < level.textures.size()) { + envmap_info.texture_idx = (int)effect.envmap_texture; + envmap_info.mode = effect.envmap_mode; + envmap = &envmap_info; + } + for (size_t draw_idx = 0; draw_idx < effect.all_draws.size(); draw_idx++) { const auto& draw = effect.all_draws[draw_idx]; auto& prim = mesh.primitives.emplace_back(); @@ -1446,7 +1748,7 @@ void add_merc(const tfrag3::Level& level, const auto it = anim_slot_to_base_tex.find(-(tex_id + 1)); tex_id = it != anim_slot_to_base_tex.end() ? it->second : draw.tree_tex_id; } - prim.material = add_material_for_tex(level, model, tex_id, tex_image_map, draw.mode); + prim.material = add_material_for_tex(level, model, tex_id, tex_image_map, draw.mode, envmap); prim.indices = make_index_buffer_accessor(model, draw_to_start[effect_idx][draw_idx], draw_to_count[effect_idx][draw_idx], index_buffer_view); @@ -1462,6 +1764,29 @@ void add_merc(const tfrag3::Level& level, add_blerc_targets(level, mmodel, mesh, model); } + +void consolidate_buffers(tinygltf::Model& model) { + if (model.buffers.size() <= 1) { + return; + } + + std::vector buffer_start(model.buffers.size()); + std::vector merged; + for (size_t i = 0; i < model.buffers.size(); i++) { + merged.resize((merged.size() + 3) & ~(size_t)3); + buffer_start[i] = merged.size(); + const auto& data = model.buffers[i].data; + merged.insert(merged.end(), data.begin(), data.end()); + } + + for (auto& view : model.bufferViews) { + view.byteOffset += buffer_start.at(view.buffer); + view.buffer = 0; + } + + model.buffers.clear(); + model.buffers.emplace_back().data = std::move(merged); +} } // namespace /*! @@ -1479,25 +1804,27 @@ void save_level_background_as_gltf(const tfrag3::Level& level, const fs::path& g // hack, add a default material. tinygltf::Material mat; mat.pbrMetallicRoughness.baseColorFactor = {1.0f, 0.9f, 0.9f, 1.0f}; + mat.pbrMetallicRoughness.metallicFactor = 0.0; mat.doubleSided = true; model.materials.push_back(mat); - std::unordered_map tex_image_map; + TexImageMap 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 (size_t i = 0; i < level.tie_trees.at(0).size(); i++) { + add_tie(level, level.tie_trees.at(0)[i], i, model, tex_image_map); } - for (const auto& shrub : level.shrub_trees) { - add_shrub(level, shrub, model, tex_image_map); + for (size_t i = 0; i < level.shrub_trees.size(); i++) { + add_shrub(level, level.shrub_trees[i], i, model, tex_image_map); } model.asset.generator = "opengoal"; + consolidate_buffers(model); tinygltf::TinyGLTF gltf; gltf.WriteGltfSceneToFile(&model, glb_file.string(), true, // embedImages @@ -1534,14 +1861,16 @@ void save_level_foreground_as_gltf( // hack, add a default material. tinygltf::Material mat; mat.pbrMetallicRoughness.baseColorFactor = {1.0f, 0.9f, 0.9f, 1.0f}; + mat.pbrMetallicRoughness.metallicFactor = 0.0; mat.doubleSided = true; model.materials.push_back(mat); - std::unordered_map tex_image_map; + TexImageMap tex_image_map; add_merc(level, art_data, mmodel, model, tex_image_map, anim_slot_to_base_tex); model.asset.generator = "opengoal"; + consolidate_buffers(model); auto glb_file = glb_path / fmt::format("{}.glb", mmodel.name); file_util::create_dir_if_needed_for_file(glb_file);