/*! * Mesh extraction for GLTF meshes. */ #include "gltf_mesh_extract.h" #include #include "color_quantization.h" #include "common/log/log.h" #include "common/math/geometry.h" #include "common/util/Timer.h" #include "common/util/gltf_util.h" #include "common/util/image_resize.h" using namespace gltf_util; constexpr int kColorTreeDepth = 13; namespace gltf_mesh_extract { void dedup_tfrag_vertices(TfragOutput& data) { Timer timer; size_t original_size = data.tfrag_vertices.size(); std::vector new_verts; std::vector old_to_new; gltf_util::dedup_vertices(data.tfrag_vertices, new_verts, old_to_new); data.tfrag_vertices = std::move(new_verts); // TODO: properly split vertices between trees... for (auto drawlist : {&data.normal_strip_draws, &data.trans_strip_draws}) { for (auto& draw : *drawlist) { ASSERT(draw.runs.empty()); // not supported yet for (auto& idx : draw.plain_indices) { idx = old_to_new.at(idx); } } } lg::info("Deduplication took {:.2f} ms, {} -> {} ({:.2f} %)", timer.getMs(), original_size, data.tfrag_vertices.size(), 100.f * data.tfrag_vertices.size() / original_size); } void dedup_tie_vertices(TieOutput& data) { Timer timer; size_t original_size = data.vertices.size(); std::vector old_verts; old_verts.reserve(data.vertices.size()); for (size_t i = 0; i < data.vertices.size(); i++) { auto& x = old_verts.emplace_back(); x.color_index = data.color_indices[i]; x.vertex = data.vertices[i]; } std::vector new_verts; std::vector old_to_new; gltf_util::dedup_vertices(old_verts, new_verts, old_to_new); data.vertices.clear(); data.color_indices.clear(); data.vertices.reserve(new_verts.size()); data.color_indices.reserve(new_verts.size()); for (auto& x : new_verts) { data.vertices.push_back(x.vertex); data.color_indices.push_back(x.color_index); } // TODO: properly split vertices between trees... for (auto drawlist : {&data.base_draws, &data.envmap_draws}) { for (auto& draw : *drawlist) { ASSERT(draw.runs.empty()); // not supported yet for (auto& idx : draw.plain_indices) { idx = old_to_new.at(idx); } } } lg::info("Deduplication took {:.2f} ms, {} -> {} ({:.2f} %)", timer.getMs(), original_size, data.vertices.size(), 100.f * data.vertices.size() / original_size); } bool prim_needs_tie(const tinygltf::Model& model, const tinygltf::Primitive& prim) { if (prim.material >= 0) { auto mat = model.materials.at(prim.material); return envmap_is_valid(mat); } return false; } void extract(const Input& in, TfragOutput& out, const tinygltf::Model& model, const std::vector& all_nodes) { std::vector> all_vtx_colors; ASSERT(out.tfrag_vertices.empty()); struct MaterialInfo { tfrag3::StripDraw draw; bool needs_tie = false; }; std::map info_by_material; int mesh_count = 0; int prim_count = 0; for (const auto& n : all_nodes) { const auto& node = model.nodes[n.node_idx]; if (node.extras.Has("set_invisible") && node.extras.Get("set_invisible").Get()) { continue; } if (node.mesh >= 0) { const auto& mesh = model.meshes[node.mesh]; mesh_count++; for (const auto& prim : mesh.primitives) { if (prim.material >= 0 && model.materials[prim.material].extras.Has("set_invisible") && model.materials[prim.material].extras.Get("set_invisible").Get()) { continue; } if (prim_needs_tie(model, prim)) { continue; } prim_count++; // extract index buffer std::vector prim_indices = gltf_index_buffer(model, prim.indices, out.tfrag_vertices.size()); ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES, "Unsupported triangle mode"); // extract vertices auto verts = gltf_vertices(model, prim.attributes, n.w_T_node, true, false, mesh.name); out.tfrag_vertices.insert(out.tfrag_vertices.end(), verts.vtx.begin(), verts.vtx.end()); all_vtx_colors.insert(all_vtx_colors.end(), verts.vtx_colors.begin(), verts.vtx_colors.end()); ASSERT(all_vtx_colors.size() == out.tfrag_vertices.size()); auto& info = info_by_material[prim.material]; info.draw.mode = make_default_draw_mode(); // todo rm info.draw.tree_tex_id = texture_pool_debug_checker(in.tex_pool); // todo rm info.draw.num_triangles += prim_indices.size() / 3; if (info.draw.vis_groups.empty()) { auto& grp = info.draw.vis_groups.emplace_back(); grp.num_inds += prim_indices.size(); grp.num_tris += info.draw.num_triangles; grp.vis_idx_in_pc_bvh = UINT16_MAX; } else { auto& grp = info.draw.vis_groups.back(); grp.num_inds += prim_indices.size(); grp.num_tris += info.draw.num_triangles; grp.vis_idx_in_pc_bvh = UINT16_MAX; } info.draw.plain_indices.insert(info.draw.plain_indices.end(), prim_indices.begin(), prim_indices.end()); } } } for (const auto& [mat_idx, d_] : info_by_material) { // out.strip_draws.push_back(d_); // auto& draw = out.strip_draws.back(); tfrag3::StripDraw draw = d_.draw; draw.mode = make_default_draw_mode(); if (mat_idx == -1) { lg::warn("Draw had a material index of -1, using default texture."); draw.tree_tex_id = texture_pool_debug_checker(in.tex_pool); out.normal_strip_draws.push_back(draw); continue; } const auto& mat = model.materials[mat_idx]; setup_alpha_from_material(mat, &draw.mode); int tex_idx = mat.pbrMetallicRoughness.baseColorTexture.index; if (tex_idx == -1) { lg::warn("Material {} has no texture, using default texture.", mat.name); draw.tree_tex_id = texture_pool_debug_checker(in.tex_pool); if (draw.mode.get_ab_enable()) { out.trans_strip_draws.push_back(draw); } else { out.normal_strip_draws.push_back(draw); } continue; } const auto& tex = model.textures[tex_idx]; ASSERT(tex.sampler >= 0); ASSERT(tex.source >= 0); setup_draw_mode_from_sampler(model.samplers.at(tex.sampler), &draw.mode); const auto& img = model.images[tex.source]; draw.tree_tex_id = texture_pool_add_texture(in.tex_pool, img); if (draw.mode.get_ab_enable()) { out.trans_strip_draws.push_back(draw); } else { out.normal_strip_draws.push_back(draw); } } lg::info("total of {} normal, {} transparent unique materials", out.normal_strip_draws.size(), out.trans_strip_draws.size()); lg::info("Merged {} meshes and {} prims into {} vertices", mesh_count, prim_count, out.tfrag_vertices.size()); Timer quantize_timer; auto quantized = quantize_colors_kd_tree(all_vtx_colors, kColorTreeDepth); for (size_t i = 0; i < out.tfrag_vertices.size(); i++) { out.tfrag_vertices[i].color_index = quantized.vtx_to_color[i]; } out.color_palette = std::move(quantized.final_colors); lg::info("Color palette generation took {:.2f} ms", quantize_timer.getMs()); dedup_tfrag_vertices(out); } s8 normal_to_s8(float in) { s32 in_s32 = in * 127.f; ASSERT(in_s32 <= INT8_MAX); ASSERT(in_s32 >= INT8_MIN); return in_s32; } void add_to_packed_verts(std::vector* out, const std::vector& vtx, const std::vector& normals) { ASSERT(vtx.size() == normals.size()); for (size_t i = 0; i < normals.size(); i++) { auto& x = out->emplace_back(); // currently not supported. x.r = 255; x.g = 255; x.b = 255; x.a = 255; x.x = vtx[i].x; x.y = vtx[i].y; x.z = vtx[i].z; x.s = vtx[i].s; x.t = vtx[i].t; x.nx = normal_to_s8(normals[i].x()); x.ny = normal_to_s8(normals[i].y()); x.nz = normal_to_s8(normals[i].z()); } } void extract(const Input& in, TieOutput& out, const tinygltf::Model& model, const std::vector& all_nodes) { std::vector> all_vtx_colors; struct MaterialInfo { tfrag3::StripDraw draw; bool needs_tie = false; }; std::map info_by_material; int mesh_count = 0; int prim_count = 0; for (const auto& n : all_nodes) { const auto& node = model.nodes[n.node_idx]; if (node.extras.Has("set_invisible") && node.extras.Get("set_invisible").Get()) { continue; } if (node.mesh >= 0) { const auto& mesh = model.meshes[node.mesh]; mesh_count++; for (const auto& prim : mesh.primitives) { if (prim.material >= 0 && model.materials[prim.material].extras.Has("set_invisible") && model.materials[prim.material].extras.Get("set_invisible").Get()) { continue; } if (!prim_needs_tie(model, prim)) { continue; } prim_count++; // extract index buffer std::vector prim_indices = gltf_index_buffer(model, prim.indices, out.vertices.size()); ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES, "Unsupported triangle mode"); // extract vertices auto verts = gltf_vertices(model, prim.attributes, n.w_T_node, true, true, mesh.name); add_to_packed_verts(&out.vertices, verts.vtx, verts.normals); all_vtx_colors.insert(all_vtx_colors.end(), verts.vtx_colors.begin(), verts.vtx_colors.end()); ASSERT(all_vtx_colors.size() == out.vertices.size()); auto& info = info_by_material[prim.material]; info.draw.mode = make_default_draw_mode(); // todo rm info.draw.tree_tex_id = texture_pool_debug_checker(in.tex_pool); // todo rm info.draw.num_triangles += prim_indices.size() / 3; if (info.draw.vis_groups.empty()) { auto& grp = info.draw.vis_groups.emplace_back(); grp.num_inds += prim_indices.size(); grp.num_tris += info.draw.num_triangles; grp.vis_idx_in_pc_bvh = UINT16_MAX; } else { auto& grp = info.draw.vis_groups.back(); grp.num_inds += prim_indices.size(); grp.num_tris += info.draw.num_triangles; grp.vis_idx_in_pc_bvh = UINT16_MAX; } info.draw.plain_indices.insert(info.draw.plain_indices.end(), prim_indices.begin(), prim_indices.end()); } } } for (const auto& [mat_idx, d_] : info_by_material) { // out.strip_draws.push_back(d_); // auto& draw = out.strip_draws.back(); tfrag3::StripDraw draw = d_.draw; draw.mode = make_default_draw_mode(); if (mat_idx == -1) { lg::warn("Draw had a material index of -1, using default texture."); draw.tree_tex_id = texture_pool_debug_checker(in.tex_pool); out.base_draws.push_back(draw); continue; } const auto& mat = model.materials[mat_idx]; setup_alpha_from_material(mat, &draw.mode); int base_tex_idx = mat.pbrMetallicRoughness.baseColorTexture.index; if (base_tex_idx == -1) { lg::warn("Material {} has no texture, using default texture.", mat.name); draw.tree_tex_id = texture_pool_debug_checker(in.tex_pool); out.base_draws.push_back(draw); continue; } int roughness_tex_idx = mat.pbrMetallicRoughness.metallicRoughnessTexture.index; ASSERT(roughness_tex_idx >= 0); const auto& base_tex = model.textures[base_tex_idx]; ASSERT(base_tex.sampler >= 0); ASSERT(base_tex.source >= 0); setup_draw_mode_from_sampler(model.samplers.at(base_tex.sampler), &draw.mode); const auto& roughness_tex = model.textures.at(roughness_tex_idx); ASSERT(roughness_tex.sampler >= 0); ASSERT(roughness_tex.source >= 0); // draw.tree_tex_id = texture_pool_add_texture(in.tex_pool, model.images[base_tex.source]); draw.tree_tex_id = texture_pool_add_envmap_control_texture( in.tex_pool, model, base_tex.source, roughness_tex.source, !draw.mode.get_clamp_s_enable(), !draw.mode.get_clamp_t_enable()); out.base_draws.push_back(draw); // now, setup envmap draw: auto envmap_settings = envmap_settings_from_gltf(mat); const auto& envmap_tex = model.textures[envmap_settings.texture_idx]; ASSERT(envmap_tex.sampler >= 0); ASSERT(envmap_tex.source >= 0); draw.mode = make_default_draw_mode(); setup_draw_mode_from_sampler(model.samplers.at(envmap_tex.sampler), &draw.mode); draw.tree_tex_id = texture_pool_add_texture(in.tex_pool, model.images[envmap_tex.source]); draw.mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_0_DST_DST); draw.mode.enable_ab(); out.envmap_draws.push_back(draw); } lg::info("total of {} normal TIE draws, {} envmap", out.base_draws.size(), out.envmap_draws.size()); lg::info("Merged {} meshes and {} prims into {} vertices", mesh_count, prim_count, out.vertices.size()); Timer quantize_timer; auto quantized = quantize_colors_kd_tree(all_vtx_colors, kColorTreeDepth); for (size_t i = 0; i < out.vertices.size(); i++) { out.color_indices.push_back(quantized.vtx_to_color[i]); } out.color_palette = std::move(quantized.final_colors); lg::info("Color palette generation took {:.2f} ms", quantize_timer.getMs()); dedup_tie_vertices(out); } std::optional> subdivide_face_if_needed(jak1::CollideFace face_in) { math::Vector3f v_min = face_in.v[0]; v_min.min_in_place(face_in.v[1]); v_min.min_in_place(face_in.v[2]); v_min -= 16.f; bool needs_subdiv = false; for (auto& vert : face_in.v) { if ((vert - v_min).squared_length() > 154.f * 154.f * 4096.f * 4096.f) { needs_subdiv = true; break; } } if (needs_subdiv) { math::Vector3f a = (face_in.v[0] + face_in.v[1]) * 0.5f; math::Vector3f b = (face_in.v[1] + face_in.v[2]) * 0.5f; math::Vector3f c = (face_in.v[2] + face_in.v[0]) * 0.5f; math::Vector3f v0 = face_in.v[0]; math::Vector3f v1 = face_in.v[1]; math::Vector3f v2 = face_in.v[2]; jak1::CollideFace fs[4]; fs[0].v[0] = v0; fs[0].v[1] = a; fs[0].v[2] = c; fs[0].bsphere = math::bsphere_of_triangle(face_in.v); fs[1].v[0] = a; fs[1].v[1] = v1; fs[1].v[2] = b; fs[1].bsphere = math::bsphere_of_triangle(fs[1].v); fs[1].pat = face_in.pat; fs[2].v[0] = a; fs[2].v[1] = b; fs[2].v[2] = c; fs[2].bsphere = math::bsphere_of_triangle(fs[2].v); fs[2].pat = face_in.pat; fs[3].v[0] = b; fs[3].v[1] = v2; fs[3].v[2] = c; fs[3].bsphere = math::bsphere_of_triangle(fs[3].v); fs[3].pat = face_in.pat; std::vector result; for (auto f : fs) { auto next_faces = subdivide_face_if_needed(f); if (next_faces) { result.insert(result.end(), next_faces->begin(), next_faces->end()); } else { result.push_back(f); } } return result; } else { return std::nullopt; } } 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. result.set = false; return result; } result.set = true; if (val.Get("ignore").Get()) { result.ignore = true; return result; } result.ignore = false; int mat = val.Get("collide_material").Get(); ASSERT(mat < (int)jak1::PatSurface::Material::MAX_MATERIAL); result.pat.set_material(jak1::PatSurface::Material(mat)); int evt = val.Get("collide_event").Get(); ASSERT(evt < (int)jak1::PatSurface::Event::MAX_EVENT); result.pat.set_event(jak1::PatSurface::Event(evt)); if (val.Get("nolineofsight").Get()) { result.pat.set_nolineofsight(true); } if (val.Get("noedge").Get()) { result.pat.set_noedge(true); } if (val.Has("collide_mode")) { int mode = val.Get("collide_mode").Get(); ASSERT(mode < (int)jak1::PatSurface::Mode::MAX_MODE); result.pat.set_mode(jak1::PatSurface::Mode(mode)); } if (val.Get("nocamera").Get()) { result.pat.set_nocamera(true); } if (val.Get("noentity").Get()) { result.pat.set_noentity(true); } return result; } void extract(const Input& in, CollideOutput& out, const tinygltf::Model& model, const std::vector& all_nodes) { [[maybe_unused]] int mesh_count = 0; [[maybe_unused]] int prim_count = 0; int suspicious_faces = 0; for (const auto& n : all_nodes) { const auto& node = model.nodes[n.node_idx]; PatResult mesh_default_collide = custom_props_to_pat(node.extras, node.name); if (node.mesh >= 0) { const auto& mesh = model.meshes[node.mesh]; mesh_count++; for (const auto& prim : mesh.primitives) { // get material const auto& mat_idx = prim.material; PatResult pat = mesh_default_collide; if (mat_idx != -1) { const auto& mat = model.materials[mat_idx]; auto mat_pat = custom_props_to_pat(mat.extras, mat.name); if (mat_pat.set) { pat = mat_pat; } } if (pat.set && pat.ignore) { continue; // skip, no collide here } prim_count++; // extract index buffer std::vector prim_indices = gltf_index_buffer(model, prim.indices, 0); ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES, "Unsupported triangle mode"); // extract vertices auto verts = gltf_vertices(model, prim.attributes, n.w_T_node, false, true, mesh.name); for (size_t iidx = 0; iidx < prim_indices.size(); iidx += 3) { jak1::CollideFace face; // get the positions for (int j = 0; j < 3; j++) { auto& vtx = verts.vtx.at(prim_indices.at(iidx + j)); face.v[j].x() = vtx.x; face.v[j].y() = vtx.y; face.v[j].z() = vtx.z; } // now face normal math::Vector3f face_normal = (face.v[2] - face.v[0]).cross(face.v[1] - face.v[0]).normalized(); float dots[3]; for (int j = 0; j < 3; j++) { dots[j] = face_normal.dot(verts.normals.at(prim_indices.at(iidx + j)).normalized()); } if (dots[0] > 1e-3 && dots[1] > 1e-3 && dots[2] > 1e-3) { suspicious_faces++; auto temp = face.v[2]; face.v[2] = face.v[1]; face.v[1] = temp; } face.bsphere = math::bsphere_of_triangle(face.v); face.bsphere.w() += 1e-1 * 5; for (int j = 0; j < 3; j++) { float output_dist = face.bsphere.w() - (face.bsphere.xyz() - face.v[j]).length(); if (output_dist < 0) { lg::print("{}\n", output_dist); lg::print("BAD:\n{}\n{}\n{}\n", face.v[0].to_string_aligned(), face.v[1].to_string_aligned(), face.v[2].to_string_aligned()); lg::print("bsphere: {}\n", face.bsphere.to_string_aligned()); } } face.pat = pat.pat; out.faces.push_back(face); } } } } std::vector fixed_faces; int fix_count = 0; for (auto& face : out.faces) { auto try_fix = subdivide_face_if_needed(face); if (try_fix) { fix_count++; fixed_faces.insert(fixed_faces.end(), try_fix->begin(), try_fix->end()); } else { 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) { lg::info("automatically detecting walls with angle {}", in.auto_wall_angle); int wall_count = 0; float wall_cos = std::cos(in.auto_wall_angle * 2.f * 3.14159 / 360.f); for (auto& face : out.faces) { math::Vector3f face_normal = (face.v[1] - face.v[0]).cross(face.v[2] - face.v[0]).normalized(); if (face_normal[1] < wall_cos) { face.pat.set_mode(jak1::PatSurface::Mode::WALL); wall_count++; } } lg::info("automatic wall: {}/{} converted to walls", wall_count, out.faces.size()); } lg::info("{} out of {} faces appeared to have wrong orientation and were flipped", suspicious_faces, out.faces.size()); lg::info("{} faces were too big and were subdivided", fix_count); // lg::info("Collision extract{} {}", mesh_count, prim_count); } void extract(const Input& in, Output& out) { lg::info("Reading gltf mesh: {}", in.filename); Timer read_timer; tinygltf::TinyGLTF loader; tinygltf::Model model; std::string err, warn; bool res = loader.LoadBinaryFromFile(&model, &err, &warn, in.filename); ASSERT_MSG(warn.empty(), warn.c_str()); ASSERT_MSG(err.empty(), err.c_str()); ASSERT_MSG(res, "Failed to load GLTF file!"); auto all_nodes = flatten_nodes_from_all_scenes(model); extract(in, out.tfrag, model, all_nodes); extract(in, out.collide, model, all_nodes); extract(in, out.tie, model, all_nodes); lg::info("GLTF total took {:.2f} ms", read_timer.getMs()); } } // namespace gltf_mesh_extract