From affeb9dadb6b715d35edc26882525d6c73391742 Mon Sep 17 00:00:00 2001 From: water Date: Thu, 19 Oct 2023 12:53:10 -0400 Subject: [PATCH] wip collision builder for jak 2 --- common/math/Vector.h | 22 + goalc/CMakeLists.txt | 4 + .../collide/common/collide_common.h | 1 - goalc/build_level/collide/jak2/collide.cpp | 896 ++++++++++++++++++ goalc/build_level/collide/jak2/collide.h | 118 +++ goalc/build_level/main.cpp | 69 ++ 6 files changed, 1109 insertions(+), 1 deletion(-) create mode 100644 goalc/build_level/collide/jak2/collide.cpp create mode 100644 goalc/build_level/collide/jak2/collide.h create mode 100644 goalc/build_level/main.cpp diff --git a/common/math/Vector.h b/common/math/Vector.h index 095b3cb32f..f9bf8d0aba 100644 --- a/common/math/Vector.h +++ b/common/math/Vector.h @@ -18,6 +18,12 @@ class Vector { return result; } + static Vector unit(int idx) { + Vector result = Vector::zero(); + result[idx] = T(1); + return result; + } + template constexpr Vector(Args... args) : m_data{T(args)...} { static_assert(sizeof...(args) == Size, "Incorrect number of args"); @@ -226,6 +232,22 @@ class Vector { } } + Vector min(const Vector& other) const { + Vector result; + for (int i = 0; i < Size; i++) { + result[i] = std::min(m_data[i], other[i]); + } + return result; + } + + Vector max(const Vector& other) const { + Vector result; + for (int i = 0; i < Size; i++) { + result[i] = std::max(m_data[i], other[i]); + } + return result; + } + std::string to_string_aligned() const { std::string result = "["; for (auto x : m_data) { diff --git a/goalc/CMakeLists.txt b/goalc/CMakeLists.txt index 9d1a52e3af..f2ae00b095 100644 --- a/goalc/CMakeLists.txt +++ b/goalc/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(compiler build_level/collide/jak1/collide_bvh.cpp build_level/collide/jak1/collide_drawable.cpp build_level/collide/jak1/collide_pack.cpp + build_level/collide/jak2/collide.cpp build_level/common/color_quantization.cpp build_level/common/Entity.cpp build_level/jak1/Entity.cpp @@ -72,6 +73,9 @@ endif () add_executable(goalc main.cpp) add_executable(goalc-simple simple_main.cpp) +add_executable(build_level build_level/main.cpp) + target_link_libraries(goalc common Zydis compiler) target_link_libraries(goalc-simple common Zydis compiler) +target_link_libraries(build_level common Zydis compiler) diff --git a/goalc/build_level/collide/common/collide_common.h b/goalc/build_level/collide/common/collide_common.h index e756dce450..2ca484f174 100644 --- a/goalc/build_level/collide/common/collide_common.h +++ b/goalc/build_level/collide/common/collide_common.h @@ -308,7 +308,6 @@ struct PatSurface { }; struct CollideFace { - math::Vector4f bsphere; math::Vector3f v[3]; PatSurface pat; }; diff --git a/goalc/build_level/collide/jak2/collide.cpp b/goalc/build_level/collide/jak2/collide.cpp new file mode 100644 index 0000000000..eae939b241 --- /dev/null +++ b/goalc/build_level/collide/jak2/collide.cpp @@ -0,0 +1,896 @@ +#include "collide.h" + +#include +#include +#include +#include + +#include "common/util/Assert.h" + +/*! + * An axis-aligned bounding box + */ +struct BoundingBox { + math::Vector3f min = math::Vector3f::zero(); + math::Vector3f max = math::Vector3f::zero(); +}; + +/*! + * See if "axis" is a separating axis for a bounding-box to triangle intersection test. + * The bounding box is centered at the origin. + * Return true if the axis is a separating axis. + */ +bool separating_axis_test(const math::Vector3f& bbox_half_side_length, + const math::Vector3f& axis, + const math::Vector3f& a, + const math::Vector3f& b, + const math::Vector3f& c) { + // project triangle to axis + const float pa = axis.dot(a); + const float pb = axis.dot(b); + const float pc = axis.dot(c); + + // project box to axis. + const float pbox_plus = std::abs(axis[0] * bbox_half_side_length[0]) + + std::abs(axis[1] * bbox_half_side_length[1]) + + std::abs(axis[2] * bbox_half_side_length[2]); + const float pbox_minus = -pbox_plus; + + const float ptri_max = std::max(std::max(pa, pb), pc); + const float ptri_min = std::min(std::min(pa, pb), pc); + + if (ptri_max < pbox_minus) { + return true; + } + + if (ptri_min > pbox_plus) { + return true; + } + + // there must be overlap. + return false; +} + +/*! + * Check to see if a triangle intersects an axis-aligned box. + */ +bool triangle_bounding_box(const BoundingBox& bbox_w, + const math::Vector3f& a_w, + const math::Vector3f& b_w, + const math::Vector3f& c_w) { + // first, translate everything so the center of the bounding box is at the origin + const math::Vector3f box_center = (bbox_w.max + bbox_w.min) / 2.f; + + const math::Vector3f half_side_length = bbox_w.max - box_center; + const math::Vector3f a = a_w - box_center; + const math::Vector3f b = b_w - box_center; + const math::Vector3f c = c_w - box_center; + + // the separating axis says that if two convex shapes don't intersect, you can project them onto a + // separating axis (line) and their projections don't overlap. This axis is either a face normal, + // or a cross-product of edges from each shape. + + // To check intersection, we'll check each possible separating axis - if any are valid, then the + // shapes don't intersect. + + // First, check the face normals of the box. This check is special-cased for speed - most + // calls to this function will not have intersection, one of these will be a valid separating + // axis. + + // find the elementwise min/max of triangle vertices + const math::Vector3f tri_min = a.min(b.min(c)); + const math::Vector3f tri_max = a.max(b.max(c)); + + // check face normals of the box + for (int axis = 0; axis < 3; axis++) { + if (tri_max[axis] < -half_side_length[axis]) { + return false; + } + if (tri_min[axis] > half_side_length[axis]) { + return false; + } + } + + // check the face normal of the tri + const math::Vector3f tri_normal = (b - a).cross(c - a); + if (separating_axis_test(half_side_length, tri_normal, a, b, c)) { + return false; + } + + // all three edges of the triangle + const math::Vector3f tri_edges[3] = { + a - b, + a - c, + c - b, + }; + + // check each triangle edge + for (auto tri_edge : tri_edges) { + // against each box edge + for (int box_axis = 0; box_axis < 3; box_axis++) { + const math::Vector3f axis = math::Vector3f::unit(box_axis).cross(tri_edge); + if (separating_axis_test(half_side_length, axis, a, b, c)) { + return false; + } + } + } + + // all possible separating axes failed, there is intersection. + return true; +} + +bool bounding_box_bounding_box(const BoundingBox& a, const BoundingBox& b) { + for (int i = 0; i < 3; i++) { + if (a.min[i] > b.max[i]) { + return false; + } + if (a.max[i] < b.min[i]) { + return false; + } + } + return true; +} + +/*! + * Convert jak1-format PatSurface to Jak 2. + */ +jak2::PatSurface jak2_pat(jak1::PatSurface jak1) { + jak2::PatSurface result; + + switch (jak1.get_mode()) { + case jak1::PatSurface::Mode::GROUND: + result.set_mode(jak2::PatSurface::Mode::GROUND); + break; + case jak1::PatSurface::Mode::WALL: + result.set_mode(jak2::PatSurface::Mode::WALL); + break; + case jak1::PatSurface::Mode::OBSTACLE: + result.set_mode(jak2::PatSurface::Mode::OBSTACLE); + break; + default: + ASSERT_NOT_REACHED(); + } + + switch (jak1.get_material()) { + case jak1::PatSurface::Material::STONE: + result.set_material(jak2::PatSurface::Material::STONE); + break; + case jak1::PatSurface::Material::ICE: + result.set_material(jak2::PatSurface::Material::ICE); + break; + case jak1::PatSurface::Material::QUICKSAND: + result.set_material(jak2::PatSurface::Material::QUICKSAND); + break; + case jak1::PatSurface::Material::WATERBOTTOM: + result.set_material(jak2::PatSurface::Material::WATERBOTTOM); + break; + case jak1::PatSurface::Material::TAR: + result.set_material(jak2::PatSurface::Material::TAR); + break; + case jak1::PatSurface::Material::SAND: + result.set_material(jak2::PatSurface::Material::SAND); + break; + case jak1::PatSurface::Material::WOOD: + result.set_material(jak2::PatSurface::Material::WOOD); + break; + case jak1::PatSurface::Material::GRASS: + result.set_material(jak2::PatSurface::Material::GRASS); + break; + case jak1::PatSurface::Material::PCMETAL: + result.set_material(jak2::PatSurface::Material::PCMETAL); + break; + case jak1::PatSurface::Material::SNOW: + result.set_material(jak2::PatSurface::Material::SNOW); + break; + case jak1::PatSurface::Material::DEEPSNOW: + result.set_material(jak2::PatSurface::Material::DEEPSNOW); + break; + case jak1::PatSurface::Material::HOTCOALS: + result.set_material(jak2::PatSurface::Material::HOTCOALS); + break; + case jak1::PatSurface::Material::LAVA: + result.set_material(jak2::PatSurface::Material::LAVA); + break; + case jak1::PatSurface::Material::CRWOOD: + result.set_material(jak2::PatSurface::Material::CRWOOD); + break; + case jak1::PatSurface::Material::GRAVEL: + result.set_material(jak2::PatSurface::Material::GRAVEL); + break; + case jak1::PatSurface::Material::DIRT: + result.set_material(jak2::PatSurface::Material::DIRT); + break; + case jak1::PatSurface::Material::METAL: + result.set_material(jak2::PatSurface::Material::METAL); + break; + case jak1::PatSurface::Material::STRAW: + result.set_material(jak2::PatSurface::Material::STRAW); + break; + case jak1::PatSurface::Material::TUBE: + result.set_material(jak2::PatSurface::Material::TUBE); + break; + case jak1::PatSurface::Material::SWAMP: + result.set_material(jak2::PatSurface::Material::SWAMP); + break; + case jak1::PatSurface::Material::STOPPROJ: + result.set_material(jak2::PatSurface::Material::STOPPROJ); + break; + case jak1::PatSurface::Material::ROTATE: + result.set_material(jak2::PatSurface::Material::ROTATE); + break; + case jak1::PatSurface::Material::NEUTRAL: + result.set_material(jak2::PatSurface::Material::NEUTRAL); + break; + default: + ASSERT_NOT_REACHED(); + } + + switch (jak1.get_event()) { + case jak1::PatSurface::Event::NONE: + result.set_event(jak2::PatSurface::Event::NONE); + break; + case jak1::PatSurface::Event::DEADLY: + result.set_event(jak2::PatSurface::Event::DEADLY); + break; + case jak1::PatSurface::Event::ENDLESSFALL: + result.set_event(jak2::PatSurface::Event::ENDLESSFALL); + break; + case jak1::PatSurface::Event::BURN: + result.set_event(jak2::PatSurface::Event::BURN); + break; + case jak1::PatSurface::Event::DEADLYUP: + result.set_event(jak2::PatSurface::Event::DEADLYUP); + break; + case jak1::PatSurface::Event::BURNUP: + result.set_event(jak2::PatSurface::Event::BURNUP); + break; + case jak1::PatSurface::Event::MELT: + result.set_event(jak2::PatSurface::Event::MELT); + break; + default: + ASSERT_NOT_REACHED(); + } + + result.set_noentity(jak1.get_noentity()); + result.set_nocamera(jak1.get_nocamera()); + + result.set_noedge(jak1.get_noedge()); + result.set_nolineofsight(jak1.get_nolineofsight()); + + return result; +} + +/*! + * Construct a collide hash from a jak1 format mesh by converting to jak 2. + */ +CollideHash construct_collide_hash(const std::vector& tris) { + std::vector jak2_tris; + jak2_tris.reserve(tris.size()); + + for (const auto& tri : tris) { + auto& new_tri = jak2_tris.emplace_back(); + for (int i = 0; i < 3; i++) { + new_tri.v[i] = tri.v[i]; + new_tri.pat = jak2_pat(tri.pat); + } + } + + return construct_collide_hash(jak2_tris); +} + +/*! + * Utility to build a bounding box. + * If no points are added, the box is set to 0. + */ +struct BBoxBuilder { + bool added_one = false; + BoundingBox box; + + // modify box to include this point. + void add_pt(const math::Vector3f& pt) { + if (added_one) { + box.min.min_in_place(pt); + box.max.max_in_place(pt); + } else { + box.min = pt; + box.max = pt; + } + added_one = true; + } + + // modify box to include this tri. + void add_tri(const jak2::CollideFace& tri) { + for (const auto& v : tri.v) { + add_pt(v); + } + } + + void add_box(const BoundingBox& box) { + add_pt(box.min); + add_pt(box.max); + } +}; + +/*! + * Given two bounding boxes, compute the volume of their intersection. + */ +float overlap_volume(const BoundingBox& a, const BoundingBox& b) { + BoundingBox intersection; + for (int i = 0; i < 3; i++) { + intersection.min[i] = std::max(a.min[i], b.min[i]); + intersection.max[i] = std::min(a.max[i], b.max[i]); + } + const math::Vector3f size = intersection.max - intersection.min; + float ret = 1.f; + for (int i = 0; i < 3; i++) { + if (size[i] <= 0) { + return 0; + } + ret *= size[i]; + } + return ret; +} + +/*! + * A portion of a mesh, used in the fragment_mesh function. + */ +struct Frag { + std::vector tri_indices; +}; + +/*! + * Statistics about a Frag, used for a few steps below. + */ +struct FragStats { + BoundingBox bbox; + math::Vector3f average_vertex_position; + math::Vector3f median_vertex_position; +}; + +/*! + * Find bounding box and average position for the triangles selected by indices. + */ +FragStats compute_frag_stats(const std::vector& tris, + const std::vector& indices) { + ASSERT(!tris.empty()); + ASSERT(!indices.empty()); + + const float inv_vert_count = 1.f / (indices.size() * 3); + + FragStats ret; + BBoxBuilder bbox; + ret.average_vertex_position.set_zero(); + + for (auto idx : indices) { + for (const auto& vtx : tris[idx].v) { + bbox.add_pt(vtx); + ret.average_vertex_position += vtx * inv_vert_count; + } + } + + for (int i = 0; i < 3; i++) { + std::vector vx; + vx.reserve(tris.size() * 3); + for (const auto& tri : tris) { + for (const auto& vtx : tri.v) { + vx.push_back(vtx[i]); + } + } + std::sort(vx.begin(), vx.end()); + ret.median_vertex_position[i] = vx[vx.size() / 2]; + } + + ret.bbox = bbox.box; + return ret; +} + +struct VectorHash { + size_t operator()(const math::Vector3f& in) const { + return std::hash()(in.x()) ^ std::hash()(in.y()) ^ std::hash()(in.z()); + } +}; + +struct CVertexHash { + size_t operator()(const math::Vector& in) const { + return std::hash()(in.x()) ^ std::hash()(in.y()) ^ std::hash()(in.z()); + } +}; + +/*! + * How many unique vertices are there in this frag? + * (currently using float equality, however, a smarter version could look at quantized vertices) + */ +int unique_vertex_count(const Frag& frag, const std::vector& tris) { + std::unordered_set vmap; + for (auto i : frag.tri_indices) { + for (const auto& v : tris[i].v) { + vmap.insert(v); + } + } + return (int)vmap.size(); +} + +/*! + * Is this a frag that we can use in the game? + */ +bool frag_is_valid_for_packing(const Frag& frag, + const FragStats& stats, + const std::vector& tris) { + if (frag.tri_indices.size() >= UINT8_MAX) { + // the fragment has too many triangles. I think this can actually be UINT8_MAX and we + // just put 0 as the size. However, this is confusing so let's just make the max 1 less + // for now. + return false; + } + + // there is a limit to the size of a fragment: + // the -4096 removes 1 meter from the end, just to make sure that order-of-operations rounding + // differences doesn't move a vertex outside the grid + const float kMaxFragSize = UINT16_MAX * 16 - 4096; + for (int i = 0; i < 3; i++) { + if (stats.bbox.max[i] - stats.bbox.min[i] >= kMaxFragSize) { + return false; + } + } + + // there is a limit to the number of unique vertices + if (unique_vertex_count(frag, tris) >= UINT8_MAX) { + return false; + } + + return true; +} + +/*! + * A way to split the fragment along a plane + */ +struct FragSplit { + // a plane that intersects the specified axis at the value. (and is normal to this axis) + int axis = 0; + float value = 0; +}; + +/*! + * Info about a split + */ +struct SplitStats { + // how many tris on each side + int tri_count[2] = {0, 0}; + + // the bounding box of those tris. only valid if nonzero tris. + BoundingBox bboxes[2]; + + float overlap_volume = 0; + float imbalance = 0; + bool had_zero = false; +}; + +SplitStats compute_split_stats(const Frag& frag, + const std::vector& tris, + const FragSplit& split) { + SplitStats stats; + BBoxBuilder bbox[2]; + + for (auto i : frag.tri_indices) { + const auto& tri = tris[i]; + const math::Vector3f average_pt = (tri.v[0] + tri.v[1] + tri.v[2]) / 3.f; + const int out_bin = (average_pt[split.axis] > split.value) ? 1 : 0; + bbox[out_bin].add_tri(tri); + stats.tri_count[out_bin]++; + } + stats.bboxes[0] = bbox[0].box; + stats.bboxes[1] = bbox[1].box; + + if (stats.tri_count[0] && stats.tri_count[1]) { + stats.overlap_volume = overlap_volume(stats.bboxes[0], stats.bboxes[1]); + float max_count = std::max(stats.tri_count[1], stats.tri_count[0]); + float min_count = std::min(stats.tri_count[1], stats.tri_count[0]); + stats.imbalance = max_count / min_count; + stats.had_zero = false; + } else { + stats.overlap_volume = 0; + stats.imbalance = 0; + stats.had_zero = true; + } + return stats; +} + +int idx_of_max(float a, float b, float c) { + if (a > b) { + if (a > c) { + return 0; + } else { + // a > b, c > a. + return 2; + } + } else { + if (b > c) { + return 1; + } else { + return 2; + } + } +} + +FragSplit pick_best_frag_split(const Frag& frag, + const FragStats& stats, + const std::vector& tris) { + // this is the tricky part. + + // I think the most important thing about splitting is that we should try to minimize overlapping + // fragments in the final mesh. Overlapping fragments means that we'll need more space for + // buckets, and the engine will need to check more fragments. + + // Based on what I learned with Jak 1, we also want to avoid: + // - fragments with bad (large) aspect ratio. Although the Jak 2 code is likely _much_ better at + // this case because it uses a box instead of a sphere, I think that we'll struggle to split + // up these fragments at the later levels. + + math::Vector3f box_size = stats.bbox.max - stats.bbox.min; + float min_box_size = box_size[0]; + float max_box_size = box_size[0]; + int max_idx = 0; + for (int i = 0; i < 3; i++) { + if (box_size[i] > max_box_size) { + max_idx = i; + max_box_size = box_size[i]; + } + min_box_size = std::min(box_size[i], min_box_size); + } + + const float aspect = max_box_size / min_box_size; + + FragSplit splits[3]; + SplitStats split_stats[3]; + + for (int i = 0; i < 3; i++) { + splits[i].axis = i; + splits[i].value = stats.median_vertex_position[i]; + split_stats[i] = compute_split_stats(frag, tris, splits[i]); + } + + if (aspect > 25) { + if (split_stats[max_idx].imbalance < 10) { + printf( + "pick best frag split splitting a frag of size %d due to bad aspect (%f), with imbalance " + "%f\n", + (int)frag.tri_indices.size(), aspect, split_stats[max_idx].imbalance); + return splits[max_idx]; + } else { + printf( + "weird: there's a bad aspect frag (%f, %f), but splitting along the worst axis causes " + "imbalance %f.\n", + max_box_size / 4096.f, min_box_size / 4096.f, split_stats[max_idx].imbalance); + } + } + + float scores[3]; + for (int i = 0; i < 3; i++) { + if (split_stats[i].had_zero) { + scores[i] = -std::numeric_limits::max(); + } else { + scores[i] = -split_stats[i].overlap_volume; + } + } + + return splits[idx_of_max(scores[0], scores[1], scores[2])]; +} + +Frag add_all_to_frag(const std::vector& tris) { + ASSERT(!tris.empty()); + + Frag ret; + ret.tri_indices.reserve(tris.size()); + for (size_t i = 0; i < tris.size(); i++) { + ret.tri_indices.push_back(i); + } + return ret; +} + +void split_frag(const Frag& in, + const FragSplit& split, + const std::vector& tris, + Frag* out_a, + Frag* out_b) { + for (auto i : in.tri_indices) { + const auto& tri = tris[i]; + const math::Vector3f average_pt = (tri.v[0] + tri.v[1] + tri.v[2]) / 3.f; + if (average_pt[split.axis] > split.value) { + out_a->tri_indices.push_back(i); + } else { + out_b->tri_indices.push_back(i); + } + } +} + +std::vector fragment_mesh(const std::vector& tris) { + struct FragAndStats { + Frag f; + FragStats s; + }; + + auto initial_frag = add_all_to_frag(tris); + auto initial_stats = compute_frag_stats(tris, initial_frag.tri_indices); + if (frag_is_valid_for_packing(initial_frag, initial_stats, tris)) { + printf("initial is good!\n"); + printf("%s\n%s\n\n", initial_stats.bbox.min.to_string_aligned().c_str(), + initial_stats.bbox.max.to_string_aligned().c_str()); + return {initial_frag}; + } + + // split up all "too big" frags until they are good. + std::vector too_big_frags = {{initial_frag, initial_stats}}; + std::vector good_frags; + + while (!too_big_frags.empty()) { + auto& back = too_big_frags.back(); + + // split it! + FragAndStats ab[2]; + auto split = pick_best_frag_split(back.f, back.s, tris); + split_frag(back.f, split, tris, &ab[0].f, &ab[1].f); + + too_big_frags.pop_back(); // invalidate back. + + // check if split frags are good or not. + for (auto& fs : ab) { + if (frag_is_valid_for_packing(fs.f, fs.s, tris)) { + good_frags.push_back(std::move(fs.f)); + } else { + too_big_frags.push_back(std::move(fs)); + } + } + } + return good_frags; +} + +CollideHash build_grid_for_main_hash(std::vector&& frags) { + CollideHash result; + BBoxBuilder bbox; + for (const auto& frag : frags) { + bbox.add_pt(frag.bbox_min_corner); + bbox.add_pt(frag.bbox_max_corner); + } + + const math::Vector3f box_size = bbox.box.max - bbox.box.min; + + // grid the box. It _looks_ like the village1 level just picks dims that get you closest to 10000 + // for the cell size. + constexpr float kTargetCellSize = 10000; + + const int grid_dimension[3] = {(int)(box_size[0] / kTargetCellSize), + (int)(box_size[1] / kTargetCellSize), + (int)(box_size[2] / kTargetCellSize)}; + const math::Vector3f grid_cell_size(box_size[0] / grid_dimension[0], + box_size[1] / grid_dimension[1], + box_size[2] / grid_dimension[2]); + + std::vector> frags_in_cells; + + // debug + std::vector debug_found_flags(frags.size(), false); + int debug_intersect_count = 0; + + // yzx order to match game + for (int yi = 0; yi < grid_dimension[1]; yi++) { + for (int zi = 0; zi < grid_dimension[2]; zi++) { + for (int xi = 0; xi < grid_dimension[0]; xi++) { + auto& cell_list = frags_in_cells.emplace_back(); + + BoundingBox cell; + cell.min = + math::Vector3f(xi * grid_cell_size[0], yi * grid_cell_size[1], zi * grid_cell_size[2]) + + bbox.box.min; + cell.max = cell.min + grid_cell_size; + + for (size_t fi = 0; fi < frags.size(); fi++) { + const auto& frag = frags[fi]; + if (bounding_box_bounding_box(cell, {frag.bbox_min_corner, frag.bbox_max_corner})) { + debug_found_flags[fi] = true; + debug_intersect_count++; + cell_list.push_back(fi); + } + } + + std::sort(cell_list.begin(), cell_list.end()); + }; + } + } + + // TODO: could dedup buckets here. + int unique_found = 0; + for (auto x : debug_found_flags) { + if (x) { + unique_found++; + } + } + + printf("frag find counts: %d %d %d\n", unique_found, (int)debug_found_flags.size(), + debug_intersect_count); + ASSERT(debug_intersect_count < INT16_MAX); // not really sure what to do if this happens... + if (unique_found != (int)debug_found_flags.size()) { + printf(" --- !!! %d frags disappeared\n", (int)debug_found_flags.size() - unique_found); + } + + for (auto& list : frags_in_cells) { + auto& bucket = result.buckets.emplace_back(); + bucket.index = result.index_array.size(); + bucket.count = list.size(); + for (auto x : list) { + result.index_array.push_back(x); + } + } + + result.grid_step = grid_cell_size; + result.axis_scale = + math::Vector3f(1.f / grid_cell_size[0], 1.f / grid_cell_size[1], 1.f / grid_cell_size[2]); + result.bbox_min_corner = bbox.box.min; + result.bbox_min_corner_i = (bbox.box.min / 16.f).cast(); + result.bbox_max_corner_i = (bbox.box.max / 16.f).cast(); + result.qwc_id_bits = (frags.size() + 127) / 128; + result.fragments = std::move(frags); + + for (int i = 0; i < 3; i++) { + result.dimension_array[i] = grid_dimension[i]; + } + return result; +} + +/*! + * Build a CollideFragment by "hashing" a list of triangles + */ +CollideFragment build_grid_for_frag(const std::vector& tris, const Frag& frag) { + CollideFragment result; + + // find the bounding box + BBoxBuilder bbox; + for (auto i : frag.tri_indices) { + bbox.add_tri(tris[i]); + } + + // build vertex, poly, pat tables: + std::vector> vertices; + std::vector polys; + std::vector pats; + + std::unordered_map, size_t, CVertexHash> vertex_to_vertex_array_index; + std::unordered_map pat_to_pat_array_index; + + for (auto ti : frag.tri_indices) { + const auto& input_tri = tris[ti]; + auto& poly = polys.emplace_back(); + + // add pat: + auto pat_it = pat_to_pat_array_index.find(input_tri.pat.val); + if (pat_it == pat_to_pat_array_index.end()) { + pat_to_pat_array_index[input_tri.pat.val] = pats.size(); + ASSERT(pats.size() < UINT8_MAX); + poly.pat_index = pats.size(); + pats.push_back(input_tri.pat); + } else { + poly.pat_index = pat_it->second; + } + + // add vertices + for (int i = 0; i < 3; i++) { + const math::Vector3f vert_f = (input_tri.v[i] - bbox.box.min) / 16.f; + for (int j = 0; j < 3; j++) { + ASSERT(vert_f[j] >= 0 && vert_f[j] < UINT16_MAX); + } + const auto vert_i = vert_f.cast(); + const auto& it = vertex_to_vertex_array_index.find(vert_i); + if (it == vertex_to_vertex_array_index.end()) { + vertex_to_vertex_array_index[vert_i] = vertex_to_vertex_array_index.size(); + ASSERT(vertex_to_vertex_array_index.size() < UINT8_MAX); + poly.vertex_index[i] = vertex_to_vertex_array_index.size(); + vertices.push_back(vert_i); + } else { + poly.vertex_index[i] = it->second; + } + } + } + + // grid the box. We can have only 256 cells, so we take a 1x1 grid and split it in half 8 times. + // TODO: there are probably smarter ways to do this. + math::Vector3f grid_cell_size = bbox.box.max - bbox.box.min; + int grid_dimension[3] = {1, 1, 1}; + for (int i = 0; i < 8; i++) { + int split_axis = idx_of_max(grid_cell_size[0], grid_cell_size[1], grid_cell_size[2]); + grid_dimension[split_axis] *= 2; + grid_cell_size[split_axis] /= 2; + } + ASSERT(grid_dimension[0] * grid_dimension[1] * grid_dimension[2] == 256); + + // per-cell, a list of polys that intersect it. + std::vector> polys_in_cells; + + // debug + std::vector debug_found_flags(frag.tri_indices.size(), false); + int debug_intersect_count = 0; + + // yzx order to match game + for (int yi = 0; yi < grid_dimension[1]; yi++) { + for (int zi = 0; zi < grid_dimension[2]; zi++) { + for (int xi = 0; xi < grid_dimension[0]; xi++) { + auto& cell_list = polys_in_cells.emplace_back(); + + BoundingBox cell; + cell.min = + math::Vector3f(xi * grid_cell_size[0], yi * grid_cell_size[1], zi * grid_cell_size[2]) + + bbox.box.min; + cell.max = cell.min + grid_cell_size; + + for (size_t ti = 0; ti < frag.tri_indices.size(); ti++) { + const auto& tri = tris[frag.tri_indices[ti]]; + if (triangle_bounding_box(cell, tri.v[0], tri.v[1], tri.v[2])) { + debug_found_flags[ti] = true; + debug_intersect_count++; + cell_list.push_back(ti); + } + } + + std::sort(cell_list.begin(), cell_list.end()); + }; + } + } + + // TODO: could dedup buckets here. + int unique_found = 0; + for (auto x : debug_found_flags) { + if (x) { + unique_found++; + } + } + + printf("find counts: %d %d %d\n", unique_found, (int)debug_found_flags.size(), + debug_intersect_count); + ASSERT(debug_intersect_count < INT16_MAX); // not really sure what to do if this happens... + if (unique_found != (int)debug_found_flags.size()) { + printf(" --- !!! %d triangles disappeared\n", (int)debug_found_flags.size() - unique_found); + } + + result.pat_array = std::move(pats); + for (auto& list : polys_in_cells) { + auto& bucket = result.buckets.emplace_back(); + bucket.index = result.index_array.size(); + bucket.count = list.size(); + for (auto x : list) { + result.index_array.push_back(x); + } + } + result.poly_array = std::move(polys); + for (auto x : vertices) { + auto& v = result.vert_array.emplace_back(); + v.position[0] = x.x(); + v.position[1] = x.y(); + v.position[2] = x.z(); + } + + result.grid_step = grid_cell_size; + result.axis_scale = + math::Vector3f(1.f / grid_cell_size[0], 1.f / grid_cell_size[1], 1.f / grid_cell_size[2]); + result.bbox_min_corner = bbox.box.min; + result.bbox_max_corner = bbox.box.max; + result.bbox_min_corner_i = (bbox.box.min / 16.f).cast(); + result.bbox_max_corner_i = (bbox.box.max / 16.f).cast(); + + for (int i = 0; i < 3; i++) { + result.dimension_array[i] = grid_dimension[i]; + } + return result; +} + +CollideHash construct_collide_hash(const std::vector& tris) { + CollideHash collide_hash; + + std::vector frags = fragment_mesh(tris); + std::vector hashed_frags; + for (auto& frag : frags) { + hashed_frags.push_back(build_grid_for_frag(tris, frag)); + } + + // hash tris in frags + // hash frags + // ?? + return build_grid_for_main_hash(std::move(hashed_frags)); +} \ No newline at end of file diff --git a/goalc/build_level/collide/jak2/collide.h b/goalc/build_level/collide/jak2/collide.h new file mode 100644 index 0000000000..5cb5e8ab5f --- /dev/null +++ b/goalc/build_level/collide/jak2/collide.h @@ -0,0 +1,118 @@ +#pragma once + +#include + +#include "common/common_types.h" +#include "common/math/Vector.h" + +#include "goalc/build_level/collide/common/collide_common.h" + +// High-level collision system idea: +// Each level has a single collide-hash object storing all collision data. +// The mesh is divided into "fragments". Each fragment is made up of triangles. +// There's a two-level lookup: if you want to find all triangles in a box, you must first find all +// the fragments that intersect the box, then find all the triangles in those fragments that +// intersect the box. +// Each fragment has a bounding box. All triangles inside that fragment fit inside the bounding box. + +/*! + * Vertex in the collide mesh. This is stored as an offset from the bottom corner of the bounding + * box. This is scaled by 16. (a "1" stored here means a distance of 16.f, or 16/4096 of in-game + * meter.) + */ +struct CollideFragmentVertex { + u16 position[3]; +}; + +/*! + * Polygon in the collide mesh. This is a reference to three vertices in the vertex array, and a + * "pat" (polygon attributes?) in the pat array. + */ +struct CollideFragmentPoly { + u8 vertex_index[3]; + u8 pat_index; +}; + +/*! + * The Collide Fragment is divided into a 3D grid. Each cell in the grid has a "bucket" which + * collects a list of all polygons that intersect the cell. The bucket stores a reference to values + * in the index list, which are polygon indices. + */ +struct CollideBucket { + s16 index; + s16 count; +}; + +struct CollideFragment { + std::vector pat_array; + + // per-cell references to the index list + std::vector buckets; + + // references to polygons + std::vector index_array; + + // references to vertices/pats + std::vector poly_array; + + std::vector vert_array; + + // others + + // the x/y/z sizes of a grid cell + math::Vector3f grid_step; + + // inverse of grid step + math::Vector3f axis_scale; + + // the corners of our bounding box + math::Vector3f bbox_min_corner; + math::Vector3f bbox_max_corner; + math::Vector bbox_min_corner_i; + math::Vector bbox_max_corner_i; + + // the number of cells in the grid along the x/y/z axis + u32 dimension_array[3] = {0, 0, 0}; +}; + +/* + ((num-ids uint16 :offset 4) +(id-count uint16 :offset 6) +(num-buckets uint32 :offset 8) +(qwc-id-bits uint32 :offset 12) +(grid-step vector :inline :offset 16) +(bbox bounding-box :inline :offset-assert 32) +(bbox4w bounding-box4w :inline :offset-assert 64) +(axis-scale vector :inline :offset 48) +(avg-extents vector :inline :offset 64) +(bucket-array uint32 :offset 44) +(item-array (inline-array collide-hash-item) :offset 60 :score 1) +(dimension-array uint32 3 :offset 76) ;; ? +(num-items uint32 :offset 92) + */ + +struct CollideHash { + // if you have a bit for each ID in the item list, how many quadwords (128-byte word) is it? + u32 qwc_id_bits = 0; + + // this is similar to the use in CollideHashFragment, but this points to entries in the . + std::vector buckets; + + // buckets point to this array, which points to the fragments below + std::vector index_array; + + // the actual fragments + std::vector fragments; + + // all these have the same meaning as in CollideFragment and define the grid. + math::Vector3f grid_step; + math::Vector3f axis_scale; + math::Vector3f bbox_min_corner; + math::Vector bbox_min_corner_i; + math::Vector bbox_max_corner_i; + u32 dimension_array[3] = {0, 0, 0}; +}; + +CollideHash construct_collide_hash(const std::vector& tris); +CollideHash construct_collide_hash(const std::vector& tris); + diff --git a/goalc/build_level/main.cpp b/goalc/build_level/main.cpp new file mode 100644 index 0000000000..d26d648808 --- /dev/null +++ b/goalc/build_level/main.cpp @@ -0,0 +1,69 @@ +#include "common/log/log.h" +#include "common/util/Assert.h" +#include "common/util/FileUtil.h" +#include "common/versions/versions.h" + +#include "goalc/build_level/jak1/build_level.h" +#include "goalc/build_level/jak2/build_level.h" + +#include "third-party/CLI11.hpp" + +// debug tool to run only build_level. +int main(int argc, char** argv) { + // logging + lg::set_stdout_level(lg::level::info); + lg::set_flush_level(lg::level::info); + lg::initialize(); + + // game version + std::string game, input_json, output_file; + fs::path project_path_override; + + // path + if (!file_util::setup_project_path(std::nullopt)) { + return 1; + } + + lg::info("Build Level Tool", versions::GOAL_VERSION_MAJOR, versions::GOAL_VERSION_MINOR); + + CLI::App app{"OpenGOAL Compiler / REPL"}; + app.add_option("input-json", input_json, + "Input JSON file (for example, custom_levels/jak2/test-zone/test-zone.jsonc)") + ->required(); + app.add_option("output-file", output_file, + "Output .go file, (for example out/jak2/obj/test-zone.go)") + ->required(); + app.add_option("-g,--game", game, "Game version (jak1 or jak2)")->required(); + app.add_option("--proj-path", project_path_override, + "Specify the location of the 'data/' folder"); + app.validate_positionals(); + CLI11_PARSE(app, argc, argv); + + GameVersion game_version = game_name_to_version(game); + + if (!project_path_override.empty()) { + if (!fs::exists(project_path_override)) { + lg::error("Error: project path override '{}' does not exist", project_path_override.string()); + return 1; + } + if (!file_util::setup_project_path(project_path_override)) { + lg::error("Could not setup project path!"); + return 1; + } + } else if (!file_util::setup_project_path(std::nullopt)) { + return 1; + } + + switch (game_version) { + case GameVersion::Jak1: + jak1::run_build_level(input_json, output_file, "jak1/"); + break; + case GameVersion::Jak2: + jak2::run_build_level(input_json, output_file, "jak2/"); + break; + default: + ASSERT_NOT_REACHED_MSG("unsupported game version"); + } + + return 0; +}