Files
jak-project/goalc/build_level/common/gltf_mesh_extract.cpp
T
Hat Kid 710f3ac117 custom levels: etie and build actor support for jak2/3 (#3851)
Custom levels for Jak 2/3 now support envmapped TIE geometry. The TIE
extract was also changed to ignore materials that have the specular flag
set, but are missing a roughness texture.

Jak 2/3 now also support the `build-actor` tool.

The `build-custom-level` and `build-actor` macros now have a few new
options:

- Both now have a `force-run` option (`#f` by default) that, when set to
`#t`, will always run level/art group generation even if the output
files are up to date.
- `build-custom-level` has a `gen-fr3` option (`#t` by default) that,
when set to `#f`, will skip generating the FR3 file for the custom level
and only generate the GOAL level file to skip the potentially slow
process of finding and adding art groups and textures. Useful for when
you want to temporarily edit only the GOAL side of the level (such as
entity placement, etc.).
- `build-actor` has a `texture-bucket` option (default 0) which will
determine what DMA sink group the model will be placed in, which is
useful to determine the draw order of the model. Previously, this was
omitted, resulting in shadows not drawing over custom actors because the
actors were put in a bucket that is drawn after shadows (this behavior
can be restored with `:texture-bucket #f`).
2025-02-01 18:04:26 +01:00

629 lines
21 KiB
C++

/*!
* Mesh extraction for GLTF meshes.
*/
#include "gltf_mesh_extract.h"
#include <optional>
#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<tfrag3::PreloadedVertex> new_verts;
std::vector<u32> 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<TieFullVertex> 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<TieFullVertex> new_verts;
std::vector<u32> 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<NodeWithTransform>& all_nodes) {
std::vector<math::Vector<u8, 4>> all_vtx_colors;
ASSERT(out.tfrag_vertices.empty());
struct MaterialInfo {
tfrag3::StripDraw draw;
bool needs_tie = false;
};
std::map<int, MaterialInfo> 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<int>()) {
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<int>()) {
continue;
}
if (prim_needs_tie(model, prim)) {
continue;
}
prim_count++;
// extract index buffer
std::vector<u32> 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<tfrag3::PackedTieVertices::Vertex>* out,
const std::vector<tfrag3::PreloadedVertex>& vtx,
const std::vector<math::Vector3f>& 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<NodeWithTransform>& all_nodes) {
std::vector<math::Vector<u8, 4>> all_vtx_colors;
struct MaterialInfo {
tfrag3::StripDraw draw;
bool needs_tie = false;
};
std::map<int, MaterialInfo> 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<int>()) {
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<int>()) {
continue;
}
if (!prim_needs_tie(model, prim)) {
continue;
}
prim_count++;
// extract index buffer
std::vector<u32> 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<std::vector<jak1::CollideFace>> 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<jak1::CollideFace> 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<int>()) {
// unset.
result.set = false;
return result;
}
result.set = true;
if (val.Get("ignore").Get<int>()) {
result.ignore = true;
return result;
}
result.ignore = false;
int mat = val.Get("collide_material").Get<int>();
ASSERT(mat < (int)jak1::PatSurface::Material::MAX_MATERIAL);
result.pat.set_material(jak1::PatSurface::Material(mat));
int evt = val.Get("collide_event").Get<int>();
ASSERT(evt < (int)jak1::PatSurface::Event::MAX_EVENT);
result.pat.set_event(jak1::PatSurface::Event(evt));
if (val.Get("nolineofsight").Get<int>()) {
result.pat.set_nolineofsight(true);
}
if (val.Get("noedge").Get<int>()) {
result.pat.set_noedge(true);
}
if (val.Has("collide_mode")) {
int mode = val.Get("collide_mode").Get<int>();
ASSERT(mode < (int)jak1::PatSurface::Mode::MAX_MODE);
result.pat.set_mode(jak1::PatSurface::Mode(mode));
}
if (val.Get("nocamera").Get<int>()) {
result.pat.set_nocamera(true);
}
if (val.Get("noentity").Get<int>()) {
result.pat.set_noentity(true);
}
return result;
}
void extract(const Input& in,
CollideOutput& out,
const tinygltf::Model& model,
const std::vector<NodeWithTransform>& 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<u32> 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<jak1::CollideFace> 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