diff --git a/common/custom_data/TFrag3Data.cpp b/common/custom_data/TFrag3Data.cpp index 939eb39139..ef723d2059 100644 --- a/common/custom_data/TFrag3Data.cpp +++ b/common/custom_data/TFrag3Data.cpp @@ -579,6 +579,11 @@ void MercModelGroup::serialize(Serializer& ser) { ser.from_pod_vector(&vertices); } +void DebugVisData::serialize(Serializer& ser) { + ser.from_pod_vector(&bsp_cell_vertices); + ser.from_pod_vector(&bsp_cell_indices); +} + void Level::serialize(Serializer& ser) { ser.from_ptr(&version); if (ser.is_loading() && version != TFRAG3_VERSION) { @@ -642,6 +647,7 @@ void Level::serialize(Serializer& ser) { collision.serialize(ser); merc_data.serialize(ser); + debug_data.serialize(ser); ser.from_ptr(&version2); if (ser.is_loading() && version2 != TFRAG3_VERSION) { @@ -762,6 +768,11 @@ void Hfragment::memory_usage(tfrag3::MemoryUsageTracker* tracker) const { tracker->add(MemoryUsageCategory::HFRAG_CORNERS, corners.size() * sizeof(HfragmentCorner)); } +void DebugVisData::memory_usage(MemoryUsageTracker* tracker) const { + tracker->add(MemoryUsageCategory::DEBUG_VIS, bsp_cell_indices.size() * sizeof(u32)); + tracker->add(MemoryUsageCategory::DEBUG_VIS, bsp_cell_vertices.size() * sizeof(BspVisVertex)); +} + void Level::memory_usage(MemoryUsageTracker* tracker) const { for (const auto& texture : textures) { texture.memory_usage(tracker); @@ -785,6 +796,7 @@ void Level::memory_usage(MemoryUsageTracker* tracker) const { hfrag.memory_usage(tracker); collision.memory_usage(tracker); merc_data.memory_usage(tracker); + debug_data.memory_usage(tracker); } void print_memory_usage(const tfrag3::Level& lev, int uncompressed_data_size) { @@ -829,9 +841,8 @@ void print_memory_usage(const tfrag3::Level& lev, int uncompressed_data_size) { {"hfrag-verts", mem_use.data[tfrag3::MemoryUsageCategory::HFRAG_VERTS]}, {"hfrag-index", mem_use.data[tfrag3::MemoryUsageCategory::HFRAG_INDEX]}, {"hfrag-time-of-day", mem_use.data[tfrag3::MemoryUsageCategory::HFRAG_TIME_OF_DAY]}, - {"hfrag-corners", mem_use.data[tfrag3::MemoryUsageCategory::HFRAG_CORNERS]} - - }; + {"hfrag-corners", mem_use.data[tfrag3::MemoryUsageCategory::HFRAG_CORNERS]}, + {"debug-vis", mem_use.data[tfrag3::MemoryUsageCategory::DEBUG_VIS]}}; for (auto& known : known_categories) { total_accounted += known.second; } diff --git a/common/custom_data/Tfrag3Data.h b/common/custom_data/Tfrag3Data.h index 2dac7cf479..e2679d5747 100644 --- a/common/custom_data/Tfrag3Data.h +++ b/common/custom_data/Tfrag3Data.h @@ -18,7 +18,7 @@ namespace tfrag3 { // - if changing any large things (vertices, vis, bvh, colors, textures) update get_memory_usage // - if adding a new category to the memory usage, update extract_level to print it. -constexpr int TFRAG3_VERSION = 43; +constexpr int TFRAG3_VERSION = 44; enum MemoryUsageCategory { TEXTURE, @@ -67,6 +67,7 @@ enum MemoryUsageCategory { HFRAG_CORNERS, COLLISION, + DEBUG_VIS, NUM_CATEGORIES }; @@ -614,6 +615,19 @@ struct MercModelGroup { void memory_usage(MemoryUsageTracker* tracker) const; }; +struct BspVisVertex { + float x, y, z; + u16 bsp_cell; +}; +static_assert(sizeof(BspVisVertex) == 16); + +struct DebugVisData { + std::vector bsp_cell_vertices; + std::vector bsp_cell_indices; + void serialize(Serializer& ser); + void memory_usage(MemoryUsageTracker* tracker) const; +}; + // constexpr int TFRAG_GEOS = 3; @@ -630,6 +644,7 @@ struct Level { Hfragment hfrag; CollisionMesh collision; MercModelGroup merc_data; + DebugVisData debug_data; u16 version2 = TFRAG3_VERSION; void serialize(Serializer& ser); void memory_usage(MemoryUsageTracker* tracker) const; diff --git a/decompiler/CMakeLists.txt b/decompiler/CMakeLists.txt index 7e6f1403d6..835312c194 100644 --- a/decompiler/CMakeLists.txt +++ b/decompiler/CMakeLists.txt @@ -58,6 +58,7 @@ add_library( level_extractor/extract_actors.cpp level_extractor/extract_collide_frags.cpp level_extractor/extract_common.cpp + level_extractor/extract_debug_vis.cpp level_extractor/extract_hfrag.cpp level_extractor/extract_joint_group.cpp level_extractor/extract_level.cpp diff --git a/decompiler/level_extractor/BspHeader.cpp b/decompiler/level_extractor/BspHeader.cpp index f69c4eef99..6a4d052fa3 100644 --- a/decompiler/level_extractor/BspHeader.cpp +++ b/decompiler/level_extractor/BspHeader.cpp @@ -1995,6 +1995,92 @@ void AdgifShaderArray::read_from_file(TypedRef ref, const decompiler::Decompiler sizeof(AdGifData) * length); } +Jak1BspNodeRef bsp_node_ref_from_file(Ref node_array, + TypedRef parent, + bool is_front, + const decompiler::DecompilerTypeSystem& dts) { + const char* name = is_front ? "front" : "back"; + auto type = get_word_kind_for_field(parent, name, dts); + Jak1BspNodeRef ret; + if (type == decompiler::LinkedWord::PTR) { + ret.is_leaf = false; + Ref child = deref_label(get_field_ref(parent, name, dts)); + int byte_offset = child.byte_offset - node_array.byte_offset; + ASSERT((byte_offset & 31) == 0); + ret.index = byte_offset / 32; + } else if (type == decompiler::LinkedWord::PLAIN_DATA) { + ret.is_leaf = true; + uint32_t leaf_value = read_plain_data_field(parent, name, dts); + ASSERT((leaf_value >> 16) == 0x8000); + ret.index = leaf_value & 0xffff; + } else { + ASSERT_NOT_REACHED(); + } + return ret; +} + +Jak1BspNode bsp_node_from_file(Ref node_array, + Ref ref, + const decompiler::DecompilerTypeSystem& dts) { + Jak1BspNode result; + TypedRef r(ref, dts.ts.lookup_type("bsp-node")); + result.front = bsp_node_ref_from_file(node_array, r, true, dts); + result.back = bsp_node_ref_from_file(node_array, r, false, dts); + result.front_flags = read_plain_data_field(r, "front-flags", dts); + result.back_flags = read_plain_data_field(r, "back-flags", dts); + result.plane.read_from_file(get_field_ref(r, "plane", dts)); + result.discovered = true; + return result; +} + +void print_node(const Jak1BspNode& node) { + fmt::print("[{:.3f} {:.3f} {:.3f} {:.3f}] ({} {} {}) ({} {} {})\n", + node.plane.data[0], node.plane.data[1], node.plane.data[2], node.plane.data[3], + node.front.is_leaf, node.front.index, node.front_flags, + node.back.is_leaf, node.back.index, node.back_flags + ); +} + +std::vector bsp_nodes_from_file(Ref node_array, + const decompiler::DecompilerTypeSystem& dts) { + std::vector nodes; + std::vector to_explore = {0}; + + while (!to_explore.empty()) { + int node_idx = to_explore.back(); + to_explore.pop_back(); + + // expand node array + if (nodes.size() <= node_idx) { + nodes.resize(node_idx + 1); + } + + if (nodes.at(node_idx).discovered) { + continue; + } + + Ref node_ref = node_array; + node_ref.byte_offset += 32 * node_idx; + nodes.at(node_idx) = bsp_node_from_file(node_array, node_ref, dts); + auto& n = nodes.at(node_idx); + if (!n.front.is_leaf) { + to_explore.push_back(n.front.index); + } + if (!n.back.is_leaf) { + to_explore.push_back(n.back.index); + } + // print_node(nodes.at(node_idx)); + + } + + int known = 0; + for (auto& n : nodes) { + if (n.discovered) known++; + } + ASSERT((int)nodes.size() == known); + return nodes; +} + void BspHeader::read_from_file(const decompiler::LinkedObjectFile& file, const decompiler::DecompilerTypeSystem& dts, GameVersion version, @@ -2012,6 +2098,7 @@ void BspHeader::read_from_file(const decompiler::LinkedObjectFile& file, if (version == GameVersion::Jak1) { adgifs.read_from_file(get_and_check_ref_to_basic(ref, "adgifs", "adgif-shader-array", dts), dts); + jak1_bsp_nodes = bsp_nodes_from_file(deref_label(get_field_ref(ref, "nodes", dts)), dts); } texture_page_count = read_plain_data_field(ref, "texture-page-count", dts); diff --git a/decompiler/level_extractor/BspHeader.h b/decompiler/level_extractor/BspHeader.h index e0b47969ff..367e921290 100644 --- a/decompiler/level_extractor/BspHeader.h +++ b/decompiler/level_extractor/BspHeader.h @@ -862,6 +862,22 @@ struct FileInfo { std::string print(int indent = 0) const; }; +struct Jak1BspNodeRef { + bool is_leaf = false; + int index = 0; +}; + +struct Jak1BspNode { + Jak1BspNodeRef front; + Jak1BspNodeRef back; + + uint32_t front_flags = 0; + uint32_t back_flags = 0; + Vector plane; + + bool discovered = false; +}; + struct BspHeader { // (info file-info :offset 4) FileInfo file_info; @@ -904,6 +920,7 @@ struct BspHeader { DrawableInlineArrayActor actors; // (cameras (array entity-camera) :offset-assert 116) // (nodes (inline-array bsp-node) :offset-assert 120) + std::vector jak1_bsp_nodes; // // (level level :offset-assert 124) // (current-leaf-idx uint16 :offset-assert 128) diff --git a/decompiler/level_extractor/extract_debug_vis.cpp b/decompiler/level_extractor/extract_debug_vis.cpp new file mode 100644 index 0000000000..c8227ccb94 --- /dev/null +++ b/decompiler/level_extractor/extract_debug_vis.cpp @@ -0,0 +1,703 @@ +#include "extract_debug_vis.h" + +#include "common/log/log.h" +#include "common/math/Vector.h" + +namespace { + +using Point = math::Vector3d; +using Plane = math::Vector4d; // Plane(a, b, c, d) -> ax + by + cz = d + +Plane flip_plane_normal(const Plane& in) { + return in * -1; +} + +math::Vector3d plane_normal(const Plane& in) { + return in.xyz(); +} + +math::Vector4d convert_vector(const level_tools::Vector& in) { + return math::Vector4d(in.data[0], in.data[1], in.data[2], in.data[3]); +} + +double point_plane_check(const Point& pt, const Plane& plane) { + return pt.x() * plane.x() + pt.y() * plane.y() + pt.z() * plane.z() - plane.w(); +} + +/*! + * Find the largest leaf index. + */ +int max_leaf_idx(const std::vector& nodes) { + int ret = -1; + for (auto& node : nodes) { + if (node.back.is_leaf) + ret = std::max(ret, node.back.index); + if (node.front.is_leaf) + ret = std::max(ret, node.front.index); + } + return ret; +} + +/*! + * Description of the tree structure of BSP nodes. + */ +struct ParentData { + std::vector node_parents; // parent_node = node_parents[child_node] + std::vector leaf_parents; // parent_node = leaf_parents[leaf] + + int num_leaves() const { return leaf_parents.size(); } +}; + +/*! + * Find the tree structure of BSP nodes from the array, that the BSP nodes are a tree, and there + * are no unparented/unused slots in either the leaf or node arrays. Things would still work if + * there were unused slots, but it just wastes memory and makes no sense. + */ +ParentData find_parents(const std::vector& nodes) { + ParentData result; + result.node_parents.resize(nodes.size(), -1); + result.leaf_parents.resize(max_leaf_idx(nodes) + 1, -1); + + // start at the root: + std::vector to_explore = {0}; + while (!to_explore.empty()) { + int node_idx = to_explore.back(); + to_explore.pop_back(); + + auto& n = nodes.at(node_idx); + + // add parents, and assert it's the first time we've seen the child (since it's a tree) + // you could imagine mapping two volumes to the same leaf, but it looks like they don't + if (n.front.is_leaf) { + ASSERT(result.leaf_parents.at(n.front.index) == -1); + result.leaf_parents.at(n.front.index) = node_idx; + } else { + to_explore.push_back(n.front.index); + ASSERT(result.node_parents.at(n.front.index) == -1); + result.node_parents.at(n.front.index) = node_idx; + } + + if (n.back.is_leaf) { + ASSERT(result.leaf_parents.at(n.back.index) == -1); + result.leaf_parents.at(n.back.index) = node_idx; + } else { + to_explore.push_back(n.back.index); + ASSERT(result.node_parents.at(n.back.index) == -1); + result.node_parents.at(n.back.index) = node_idx; + } + } + + // check for unused slots in leaf/node arrays. + int unparented_leaves = 0; + int unparented_nodes = 0; + for (auto np : result.node_parents) { + if (np == -1) + unparented_nodes++; + } + for (auto lp : result.leaf_parents) { + if (lp == -1) + unparented_leaves++; + } + ASSERT(unparented_nodes == 1); // the root is unparented + ASSERT(unparented_leaves == 0); // every leaf should be the child of a + + return result; +} + +/*! + * For a given leaf, find the bounding planes. A point (x, y, z) is inside the leaf, if for all + * planes (a, b, c, d): + * + * ax + by + cz - d > 0 + * + */ +std::vector planes_for_leaf(int leaf_idx, + const ParentData& tree, + const std::vector& nodes) { + std::vector planes; + + int parent_idx = tree.leaf_parents.at(leaf_idx); + int child_idx = leaf_idx; + + // each iteration adds the plane from parent_idx, then goes up the tree. + while (parent_idx != -1) { + // printf(" p %d\n", parent_idx); + const auto& node = nodes.at(parent_idx); + if (node.back.index == child_idx) { + // if this is the "back" child of the parent, flip the sign of the plane - this child is + // if the unflipped plane check fails. + planes.push_back(flip_plane_normal(convert_vector(node.plane))); + } else if (node.front.index == child_idx) { + planes.push_back(convert_vector(node.plane)); + } else { + ASSERT_NOT_REACHED(); + } + + int new_parent = tree.node_parents.at(parent_idx); + child_idx = parent_idx; + parent_idx = new_parent; + } + + return planes; +} + +/*! + * Representation of a convex face as collection of points on a plane. The winding of the points + * should match the normal. (do we really care about this?) + */ +struct Face { + std::vector points; + Plane plane; + void verify() const; + double area() const; + Point avg_vertex_pos() const; + void flip_if_needed(); +}; + +void Face::flip_if_needed() { + ASSERT(points.size() > 1); + const Point c = avg_vertex_pos(); + const Point ab = points.at(1) - points.at(0); + const Point ac = c - points.at(0); + const math::Vector3d abac = ab.cross(ac); + const double winding_check = abac.dot(plane_normal(plane)); + + if (winding_check < 0) { + std::reverse(points.begin(), points.end()); + } +} +/*! + * Get the average of all vertices. Because the face is convex, this returns a point in the area. + */ +Point Face::avg_vertex_pos() const { + Point ret = Point::zero(); + for (auto& p : points) { + ret += p; + } + ret /= double(points.size()); + return ret; +} + +/*! + * Get the area of the face, also checking the winding order. + */ +double Face::area() const { + // let c be any point inside the face: + const Point c = avg_vertex_pos(); + + double total = 0; + + for (int ai = 0; ai < points.size(); ai++) { + const int bi = (ai + 1) % points.size(); + const Point ab = points.at(bi) - points.at(ai); + const Point ac = c - points.at(ai); + const math::Vector3d abac = ab.cross(ac); + const double winding_check = abac.dot(plane_normal(plane)); + if (winding_check < 0) { + lg::die("winding check failed"); + } + // printf("abac: %f\n", abac.length()); + total += abac.length(); + } + return total * 0.5; +} + +/*! + * Verify that a face is valid: all points lie on the plane and the winding order is correct. + */ +void Face::verify() const { + // verify that all points lie on the plane. + for (const auto& pt : points) { + const double dist_from_plane = point_plane_check(pt, plane); + if (std::abs(dist_from_plane) > 200) { + lg::die("Point not on plane error {} {}", dist_from_plane, plane.to_string_aligned()); + } + } + + // checks winding: + const double a = area(); + ASSERT(a > 0); + + // TODO: could check for tangled stuff here? +} + +/*! + * Representation of a volume as a collection of bounding faces. + */ +struct Volume { + std::vector faces; + void verify() const; + bool check_point_in_volume(const Point& pt) const; + double surface_area() const; + double volume() const; + Point some_internal_point() const; +}; + +/*! + * Is pt inside this volume? + */ +bool Volume::check_point_in_volume(const Point& pt) const { + for (auto& face : faces) { + if (point_plane_check(pt, face.plane) < 0) { + return false; + } + } + return true; +} + +/*! + * Get a point inside the volume. Average of face centers now. + */ +Point Volume::some_internal_point() const { + Point ret = Point::zero(); + for (auto& face : faces) { + ret += face.avg_vertex_pos(); + } + return ret /= double(faces.size()); +} + +/*! + * Verify all faces in the volume, then verify face orientation. + */ +void Volume::verify() const { + for (const auto& face : faces) { + face.verify(); + } + + if (!check_point_in_volume(some_internal_point())) { + lg::die("pt in volume verify failed"); + } +} + +/*! + * Compute the surface area. + */ +double Volume::surface_area() const { + double ret = 0; + for (const auto& face : faces) { + ret += face.area(); + } + return ret; +} + +/*! + * Find the volume enclosed. + */ +double Volume::volume() const { + double ret = 0; + Point d = some_internal_point(); + for (const auto& face : faces) { + Point c = face.avg_vertex_pos(); + for (size_t ai = 0; ai < face.points.size(); ai++) { + size_t bi = (ai + 1) % face.points.size(); + Point a = face.points.at(ai); + Point b = face.points.at(bi); + ret += std::abs((a - d).dot((b - d).cross(c - d))) / 6.; + } + } + return ret; +} + +enum class FacePlaneResult { ALL_IN, ALL_OUT, SPLIT }; + +/*! + * Check a face against a plane to determine if the face is all on one side, all on the other, or in + * the middle. + */ +FacePlaneResult check_face_plane(const Face& face, const Plane& plane) { + bool found_in = false; + bool found_out = false; + + for (auto& pt : face.points) { + if (point_plane_check(pt, plane) > 0) { + found_in = true; + } else { + found_out = true; + } + } + + if (found_in && found_out) { + return FacePlaneResult::SPLIT; + } else if (found_in && !found_out) { + return FacePlaneResult::ALL_IN; + } else if (found_out && !found_in) { + return FacePlaneResult::ALL_OUT; + } else { + ASSERT_NOT_REACHED(); + } +} + +struct ClipFaceResult { + Face clipped_face; + Point a, b; // the new points added to this face. +}; + +/*! + * Compute the intersection between a line segment and plane. + */ +std::optional plane_line_segment_isect(const Plane& plane, + const Point& p0, + const Point& p1, + double* u) { + // let p = p0 + u * (p1 - p0) + // if p is on the plane, then dot(p, n) = d + // dot(p0 + u * (p1 - p0), n) = d + // dot(p0, n) + u * dot(p1 - p0, n) = d + // u = (d - dot(p0, n)) / dot(p1 - p0, n) + const math::Vector3d n = plane.xyz(); + *u = (plane.w() - p0.dot(n)) / (p1 - p0).dot(n); + if (*u >= 0 && *u <= 1) { + Point ret = p0 + (p1 - p0) * *u; + // fmt::print("ret dist: {}, {} {}\n", point_plane_check(ret, plane), point_plane_check(p0, + // plane), + // point_plane_check(p1, plane)); + return ret; + } else { + return std::nullopt; + } +} + +/*! + * Clip a face, returning the new face, and the two vertices of the new face that intersect the + * clipping plane. + */ +ClipFaceResult clip_face(const Face& face, const Plane& plane) { + ClipFaceResult result; + result.clipped_face.plane = face.plane; + + // determine if each point is in the new face. + std::vector pt_in; + for (auto& pt : face.points) { + pt_in.push_back(point_plane_check(pt, plane) > 0); + } + + // loop around points on this face, including them in the new face only if they are inside the + // plane. When the permiter enters and exits the clipping plane, generate new vertices, and store + // these in the a/b outputs. + int saw_exit = 0; + int saw_enter = 0; + for (size_t i = 0; i < face.points.size(); i++) { + const auto p0 = face.points.at(i); + const auto p1 = face.points.at((i + 1) % face.points.size()); + if (pt_in[i]) { + // if the point is in, just add it. + result.clipped_face.points.push_back(p0); + // exit point - need to insert a new vertex here! + if (!pt_in[(i + 1) % face.points.size()]) { + saw_exit++; + double u; + auto new_pt = plane_line_segment_isect(plane, p0, p1, &u); + ASSERT(new_pt.has_value()); + // fmt::print("CLIP A: {} {} -> {} (u = {})\n", p0.to_string_aligned(), + // p1.to_string_aligned(), + // new_pt->to_string_aligned(), u); + result.clipped_face.points.push_back(*new_pt); + result.a = *new_pt; + } + } else { + if (pt_in[(i + 1) % face.points.size()]) { + // enter + saw_enter++; + double u; + auto new_pt = plane_line_segment_isect(plane, p0, p1, &u); + ASSERT(new_pt.has_value()); + // fmt::print("CLIP B: {} {} -> {} (u = {})\n", p0.to_string_aligned(), + // p1.to_string_aligned(), + // new_pt->to_string_aligned(), u); + result.clipped_face.points.push_back(*new_pt); + result.b = *new_pt; + } + } + } + + ASSERT(saw_enter == 1); + ASSERT(saw_exit == 1); + result.clipped_face.verify(); + return result; +} + +std::vector extract_face_ring(const std::vector& cfr) { + std::vector result; + ASSERT(cfr.size() > 1); + std::vector used(cfr.size(), false); + + // add the first one + int num_used = 1; + used[0] = true; + // result.push_back(cfr[0].a); + result.push_back(cfr[0].b); + + // for (auto& cf : cfr) { + // fmt::print("{} {}\n", cf.a.to_string_aligned(), cf.b.to_string_aligned()); + // } + + // gross N^2 loop to guess at the order of the edges in the new face beacuse I didn't track face + // connectivity... :( + // printf("efr: %d %ld\n", num_used, used.size()); + while (num_used < used.size()) { + const auto& tgt = result.back(); + double best_dist = std::numeric_limits::max(); + size_t best_idx = -1; + + for (size_t i = 0; i < cfr.size(); i++) { + // printf("checking %ld (%d)\n", i, int(used[i])); + if (used[i]) { + continue; + } + const double dist = (cfr[i].a - tgt).squared_length(); + if (dist < best_dist) { + best_dist = dist; + best_idx = i; + } + } + num_used++; + used.at(best_idx) = true; + // printf("PICKED %ld\n", best_idx); + // ASSERT(best_dist < 4096 * 4096 * 10); // hmm + result.push_back(cfr[best_idx].b); + } + + // 0 b = [-16777216.000 16777216.000 -2211840.000] + // 3 a = [-16777216.000 16777216.000 -2211840.000] -> [-16777216.000 -16777216.000 -2211840.000] + // 1 a = [-16777216.000 -16777216.000 -2211840.000] -> [16777216.000 -16777216.000 -2211840.000] + // 2 a = [16777216.000 -16777216.000 -2211840.000] -> 16777216.000 16777216.000 -2211840.000] + + // fmt::print("last in the result: {}\n", result.back().to_string_aligned()); + // fmt::print("cfr[0]a : {}\n", cfr[0].a.to_string_aligned()); + // fmt::print("diff : {}\n", (result.back() - cfr[0].a).to_string_aligned()); + // fmt::print("diff : {}\n", (result.back() - cfr[0].a).squared_length()); + + // ASSERT(4096 * 4096 * 10 > (result.back() - cfr[0].a).squared_length()); + return result; +} + +// TODO: paranoid clipping that checks a = b, b = a, area sums. + +Volume clip_volume(const Volume& vol, const Plane& plane) { + Volume new_volume; + std::vector split_faces; + + // categorize faces + for (const auto& face : vol.faces) { + // printf("running on face\n"); + switch (check_face_plane(face, plane)) { + case FacePlaneResult::ALL_IN: + new_volume.faces.push_back(face); + break; + case FacePlaneResult::SPLIT: + split_faces.push_back(clip_face(face, plane)); + break; + case FacePlaneResult::ALL_OUT: + break; + } + } + size_t all_in_count = new_volume.faces.size(); + // printf("face counts: %ld -> %ld in, %ld split\n", vol.faces.size(), all_in_count, + // split_faces.size()); + // ASSERT(!new_volume.faces.empty()); + + if (!split_faces.empty()) { + // add clipped faces + for (auto& cf : split_faces) { + new_volume.faces.push_back(cf.clipped_face); + } + + // build the new face + Face new_face; + new_face.plane = plane; + new_face.points = extract_face_ring(split_faces); + new_face.flip_if_needed(); + new_face.verify(); + new_volume.faces.push_back(new_face); + } else { + } + + return new_volume; +} + +Volume paranoid_clip_volume(const Volume& vol, const Plane& plane) { + // printf("CLIP STARTING!\n"); + + Volume clipped = clip_volume(vol, plane); + clipped.verify(); + Volume other = clip_volume(vol, flip_plane_normal(plane)); + other.verify(); + + // TODO check areas + const double a1 = vol.surface_area(); + const double a2 = clipped.surface_area() + other.surface_area(); + ASSERT(a2 >= a1); // todo: could be better. + const double v1 = vol.volume(); + const double v2 = clipped.volume() + other.volume(); + if (std::abs(v1 - v2) > 0.001 * v1) { + lg::die("Bad volumes: {} != {}\n", v1 / 1e6, v2 / 1e6); + } + + // printf("CLIP Completed!\n"); + return clipped; +} + +/*! + * Given a sphere (x, y, z, radius), build a Volume for the axis-aligned bounding box of this + * sphere. + */ +Volume make_aabb_for_sphere(const level_tools::Vector& sphere) { + Volume volume; + + Point origin(sphere.data[0], sphere.data[1], sphere.data[2]); + const double ox = origin.x(); + const double oy = origin.y(); + const double oz = origin.z(); + const double r = sphere.data[3]; + + Face top_face; + top_face.plane = Plane(0, -1, 0, -oy - r); + top_face.points.emplace_back(ox - r, oy + r, oz - r); + top_face.points.emplace_back(ox + r, oy + r, oz - r); + top_face.points.emplace_back(ox + r, oy + r, oz + r); + top_face.points.emplace_back(ox - r, oy + r, oz + r); + volume.faces.push_back(top_face); + + Face bot_face; + bot_face.plane = Plane(0, 1, 0, -oy - r); + bot_face.points.emplace_back(ox - r, oy - r, oz - r); + bot_face.points.emplace_back(ox - r, oy - r, oz + r); + bot_face.points.emplace_back(ox + r, oy - r, oz + r); + bot_face.points.emplace_back(ox + r, oy - r, oz - r); + volume.faces.push_back(bot_face); + + Face s1_face; + s1_face.plane = Plane(-1, 0, 0, -ox - r); + s1_face.points.emplace_back(ox + r, oy - r, oz - r); + s1_face.points.emplace_back(ox + r, oy - r, oz + r); + s1_face.points.emplace_back(ox + r, oy + r, oz + r); + s1_face.points.emplace_back(ox + r, oy + r, oz - r); + volume.faces.push_back(s1_face); + + Face s2_face; + s2_face.plane = Plane(1, 0, 0, -ox - r); + s2_face.points.emplace_back(ox - r, oy - r, oz - r); + s2_face.points.emplace_back(ox - r, oy + r, oz - r); + s2_face.points.emplace_back(ox - r, oy + r, oz + r); + s2_face.points.emplace_back(ox - r, oy - r, oz + r); + volume.faces.push_back(s2_face); + + Face front_face; + front_face.plane = Plane(0, 0, -1, -oz - r); + front_face.points.emplace_back(ox - r, oy - r, oz + r); + front_face.points.emplace_back(ox - r, oy + r, oz + r); + front_face.points.emplace_back(ox + r, oy + r, oz + r); + front_face.points.emplace_back(ox + r, oy - r, oz + r); + volume.faces.push_back(front_face); + + Face back_face; + back_face.plane = Plane(0, 0, 1, -oz - r); + back_face.points.emplace_back(ox - r, oy - r, oz - r); + back_face.points.emplace_back(ox + r, oy - r, oz - r); + back_face.points.emplace_back(ox + r, oy + r, oz - r); + back_face.points.emplace_back(ox - r, oy + r, oz - r); + volume.faces.push_back(back_face); + + volume.verify(); + + const double expected_vol = 8 * r * r * r; + const double expected_sa = 24 * r * r; + + const double vol = volume.volume(); + const double sa = volume.surface_area(); + + if (std::abs(vol - expected_vol) > expected_vol * 0.001) { + lg::die("volume bad: {} {}\n", vol, expected_vol); + } + + if (std::abs(sa - expected_sa) > expected_sa * 0.001) { + lg::die("sa bad: {} {}\n", vol, expected_vol); + } + + // lg::print("got {} {}, {} {}\n", vol, expected_vol, sa, expected_sa); + + return volume; +} + +tfrag3::BspVisVertex make_vtx(const Point& pt, int leaf) { + tfrag3::BspVisVertex v; + v.bsp_cell = leaf; + v.x = pt.x(); + v.y = pt.y(); + v.z = pt.z(); + return v; +} + +void generate_volume_verts(std::vector* verts, + std::vector* indices, + int leaf, + const Volume& volume) { + // TODO: we could reuse vertices between faces... + + for (const auto& face : volume.faces) { + // center + indices->push_back(verts->size()); + verts->push_back(make_vtx(face.avg_vertex_pos(), leaf)); + + // points + for (const auto& pt : face.points) { + indices->push_back(verts->size()); + verts->push_back(make_vtx(pt, leaf)); + } + + // next fan + indices->push_back(UINT32_MAX); + } +} + +} // namespace + +void extract_bsp_cells(const level_tools::BspHeader& bsp, tfrag3::Level* out) { + printf("bsp cells for %s %f %f %f %f\n", bsp.name.c_str(), bsp.bsphere.data[0], + bsp.bsphere.data[1], bsp.bsphere.data[2], bsp.bsphere.data[3]); + + // ugh - bsphere seems like it's not set. + level_tools::Vector derp; + derp.data[0] = 0; + derp.data[1] = 0; + derp.data[2] = 0; + derp.data[3] = 4096. * 4096. * 10.; + + auto parents = find_parents(bsp.jak1_bsp_nodes); + for (int leaf_idx = 0; leaf_idx < parents.num_leaves(); leaf_idx++) { + // printf("-----------------------------LEAF %d/%d\n", leaf_idx, parents.num_leaves()); + auto planes = planes_for_leaf(leaf_idx, parents, bsp.jak1_bsp_nodes); + std::reverse(planes.begin(), planes.end()); + Volume vol = make_aabb_for_sphere(derp); + + for (auto& clip : planes) { + vol = paranoid_clip_volume(vol, clip); + } + + bool skip = false; + for (auto& face : vol.faces) { + for (auto& pt : face.points) { + for (int i = 0; i < 3; i++) { + if (std::abs(pt[i]) > derp.data[3] * 0.8) { + skip = true; + } + } + } + } + + if (!skip) { + generate_volume_verts(&out->debug_data.bsp_cell_vertices, &out->debug_data.bsp_cell_indices, + leaf_idx, vol); + } + + + + // ASSERT_NOT_REACHED(); + } +} + +namespace decompiler { +void extract_debug_vis(const level_tools::BspHeader& bsp, tfrag3::Level* out) { + extract_bsp_cells(bsp, out); +} +} // namespace decompiler diff --git a/decompiler/level_extractor/extract_debug_vis.h b/decompiler/level_extractor/extract_debug_vis.h new file mode 100644 index 0000000000..54203b6ad3 --- /dev/null +++ b/decompiler/level_extractor/extract_debug_vis.h @@ -0,0 +1,10 @@ +#pragma once + +#include "common/custom_data/Tfrag3Data.h" + +#include "decompiler/level_extractor/BspHeader.h" + +namespace decompiler { + +void extract_debug_vis(const level_tools::BspHeader& bsp, tfrag3::Level* out); +} \ No newline at end of file diff --git a/decompiler/level_extractor/extract_level.cpp b/decompiler/level_extractor/extract_level.cpp index 4cb5d8ae18..c3cd52ed0f 100644 --- a/decompiler/level_extractor/extract_level.cpp +++ b/decompiler/level_extractor/extract_level.cpp @@ -12,6 +12,7 @@ #include "decompiler/level_extractor/BspHeader.h" #include "decompiler/level_extractor/extract_actors.h" #include "decompiler/level_extractor/extract_collide_frags.h" +#include "decompiler/level_extractor/extract_debug_vis.h" #include "decompiler/level_extractor/extract_hfrag.h" #include "decompiler/level_extractor/extract_joint_group.h" #include "decompiler/level_extractor/extract_merc.h" @@ -255,6 +256,10 @@ level_tools::BspHeader extract_bsp_from_level(const ObjectFileDB& db, if (bsp_header.hfrag) { extract_hfrag(bsp_header, tex_db, &level_data); } + + if (db.version() == GameVersion::Jak1) { // for now... + extract_debug_vis(bsp_header, &level_data); + } level_data.level_name = bsp_header.name; return bsp_header; @@ -404,7 +409,7 @@ void extract_all_levels(const ObjectFileDB& db, [&](int idx) { extract_from_level(db, tex_db, dgo_names[idx], config, output_path, entities_dir); }, - dgo_names.size()); + dgo_names.size(), 1); threads.join(); } diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index df32b480ad..a576cef213 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -39,6 +39,7 @@ set(RUNTIME_SOURCE graphics/opengl_renderer/background/TFragment.cpp graphics/opengl_renderer/background/Tie3.cpp graphics/opengl_renderer/BlitDisplays.cpp + graphics/opengl_renderer/BspRenderer.cpp graphics/opengl_renderer/BucketRenderer.cpp graphics/opengl_renderer/CollideMeshRenderer.cpp graphics/opengl_renderer/debug_gui.cpp diff --git a/game/graphics/opengl_renderer/BspRenderer.cpp b/game/graphics/opengl_renderer/BspRenderer.cpp new file mode 100644 index 0000000000..33f7cc2bf4 --- /dev/null +++ b/game/graphics/opengl_renderer/BspRenderer.cpp @@ -0,0 +1,139 @@ +#include "BspRenderer.h" + +#include "common/log/log.h" + +#include + +BspRenderer::BspRenderer(GameVersion version) { + glGenVertexArrays(1, &m_vao); +} + +void BspRenderer::render(SharedRenderState* render_state, ScopedProfilerNode& prof) { + // can't render + if (!render_state->has_pc_data) { + return; + } + + // check loaded levels + auto levels = render_state->loader->get_in_use_levels(); + if (levels.empty()) { + return; + } + + glBindVertexArray(m_vao); + + // see if we need to load meshes for any + for (const auto& level : levels) { + const auto& cached = m_level_cache.find(level->load_id); + if (cached == m_level_cache.end()) { + lg::info("BspRenderer loading for {}", level->level->level_name); + unload_cached_for_name(level->level->level_name); + load_level(level, &m_level_cache[level->load_id]); + } + render_level(render_state, prof, &m_level_cache.at(level->load_id)); + } +} + +void BspRenderer::load_level(LevelData* level, LevelCache* lc) { + glGenBuffers(1, &lc->vertex_buffer); + glGenBuffers(1, &lc->index_buffer); + glBindBuffer(GL_ARRAY_BUFFER, lc->vertex_buffer); + glBufferData(GL_ARRAY_BUFFER, + level->level->debug_data.bsp_cell_vertices.size() * sizeof(tfrag3::BspVisVertex), + level->level->debug_data.bsp_cell_vertices.data(), GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, lc->index_buffer); + glBufferData(GL_ARRAY_BUFFER, level->level->debug_data.bsp_cell_indices.size() * sizeof(u32), + level->level->debug_data.bsp_cell_indices.data(), GL_STATIC_DRAW); + lc->index_count = level->level->debug_data.bsp_cell_indices.size(); +} + +void BspRenderer::unload_cached_for_name(const std::string& name) { + for (auto it = m_level_cache.begin(); it != m_level_cache.end();) { + if (it->second.name == name) { + lg::info("BspRenderer first removing loaded {}", name); + unload_level(&it->second); + it = m_level_cache.erase(it); + } else { + ++it; + } + } +} + +void BspRenderer::unload_level(LevelCache* lc) { + glDeleteBuffers(1, &lc->index_buffer); + glDeleteBuffers(1, &lc->vertex_buffer); +} + +BspRenderer::~BspRenderer() { + glDeleteVertexArrays(1, &m_vao); +} + +void BspRenderer::draw_debug_window() { + ImGui::InputInt("min-leaf", &min_leaf); + ImGui::InputInt("max-leaf", &max_leaf); +} + +void BspRenderer::render_level(SharedRenderState* render_state, + ScopedProfilerNode& prof, + LevelCache* lc) { + auto shader = render_state->shaders[ShaderId::BSP].id(); + render_state->shaders[ShaderId::BSP].activate(); + glUniformMatrix4fv(glGetUniformLocation(shader, "camera"), 1, GL_FALSE, + render_state->camera_matrix[0].data()); + glUniform4f(glGetUniformLocation(shader, "hvdf_offset"), render_state->camera_hvdf_off[0], + render_state->camera_hvdf_off[1], render_state->camera_hvdf_off[2], + render_state->camera_hvdf_off[3]); + const auto& trans = render_state->camera_pos; + glUniform4f(glGetUniformLocation(shader, "camera_position"), trans[0], trans[1], trans[2], + trans[3]); + glUniform1f(glGetUniformLocation(shader, "fog_constant"), render_state->camera_fog.x()); + glUniform1f(glGetUniformLocation(shader, "fog_min"), render_state->camera_fog.y()); + glUniform1f(glGetUniformLocation(shader, "fog_max"), render_state->camera_fog.z()); + glUniform1i(glGetUniformLocation(shader, "min_leaf"), (GLint)min_leaf); + glUniform1i(glGetUniformLocation(shader, "max_leaf"), (GLint)max_leaf); + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_GEQUAL); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // ? + glDepthMask(GL_TRUE); + + glBindBuffer(GL_ARRAY_BUFFER, lc->vertex_buffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, lc->index_buffer); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + glEnableVertexAttribArray(2); + glEnableVertexAttribArray(3); + glVertexAttribPointer(0, // location 0 in the shader + 3, // 3 values per vert + GL_FLOAT, // floats + GL_FALSE, // normalized + sizeof(tfrag3::BspVisVertex), // stride + 0 // offset (0) + ); + glVertexAttribIPointer(1, // location 1 in the shader + 1, // 3 values per vert + GL_UNSIGNED_SHORT, // u16 + sizeof(tfrag3::BspVisVertex), // stride + (void*)offsetof(tfrag3::BspVisVertex, bsp_cell) // offset + ); + glEnable(GL_PRIMITIVE_RESTART); + glPrimitiveRestartIndex(UINT32_MAX); + glUniform1i(glGetUniformLocation(shader, "wireframe"), 0); + // glDrawElements(GL_TRIANGLE_FAN, lc->index_count, GL_UNSIGNED_INT, nullptr); + + if (true) { + glUniform1i(glGetUniformLocation(shader, "wireframe"), 1); + glDisable(GL_BLEND); + glDepthMask(GL_FALSE); + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + glLineWidth(3.0); + glDrawElements(GL_TRIANGLE_FAN, lc->index_count, GL_UNSIGNED_INT, nullptr); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glEnable(GL_BLEND); + glDepthMask(GL_TRUE); + } + + prof.add_draw_call(); + prof.add_tri(lc->index_count); // not exactly, but who cares. +} \ No newline at end of file diff --git a/game/graphics/opengl_renderer/BspRenderer.h b/game/graphics/opengl_renderer/BspRenderer.h new file mode 100644 index 0000000000..77e2144167 --- /dev/null +++ b/game/graphics/opengl_renderer/BspRenderer.h @@ -0,0 +1,33 @@ +#pragma once + +#include "common/versions/versions.h" + +#include "game/graphics/opengl_renderer/BucketRenderer.h" + +class BspRenderer { +public: + BspRenderer(GameVersion version); + void render(SharedRenderState* render_state, ScopedProfilerNode& prof); + void draw_debug_window(); + ~BspRenderer(); + +private: + int min_leaf = 0; + int max_leaf = UINT16_MAX; + struct LevelCache { + std::string name; + GLuint index_buffer = -1; + GLuint vertex_buffer = -1; + int index_count = 0; + }; + + void unload_cached_for_name(const std::string& name); + void load_level(LevelData* level, LevelCache* lc); + void unload_level(LevelCache* lc); + void render_level(SharedRenderState* render_state, ScopedProfilerNode& prof, LevelCache* lc); + + + std::map m_level_cache; + + GLuint m_vao; +}; diff --git a/game/graphics/opengl_renderer/OpenGLRenderer.cpp b/game/graphics/opengl_renderer/OpenGLRenderer.cpp index aeacfc1558..e868386228 100644 --- a/game/graphics/opengl_renderer/OpenGLRenderer.cpp +++ b/game/graphics/opengl_renderer/OpenGLRenderer.cpp @@ -74,6 +74,7 @@ OpenGLRenderer::OpenGLRenderer(std::shared_ptr texture_pool, GameVersion version) : m_render_state(texture_pool, loader, version), m_collide_renderer(version), + m_bsp_renderer(version), m_version(version) { // requires OpenGL 4.3 #ifndef __APPLE__ @@ -1104,6 +1105,7 @@ void OpenGLRenderer::draw_renderer_selection_window() { ImGui::Checkbox("Sky CPU", &m_render_state.use_sky_cpu); ImGui::Checkbox("Occlusion Cull", &m_render_state.use_occlusion_culling); ImGui::Checkbox("Blackout Loads", &m_enable_fast_blackout_loads); + m_bsp_renderer.draw_debug_window(); if (m_texture_animator && ImGui::TreeNode("Texture Animator")) { m_texture_animator->draw_debug_window(); @@ -1304,9 +1306,16 @@ void OpenGLRenderer::dispatch_buckets_jak1(DmaFollower dma, m_category_times[(int)m_bucket_categories[bucket_id]] += bucket_prof.get_elapsed_time(); // hack to draw the collision mesh in the middle the drawing - if (bucket_id == 31 - 1 && Gfx::g_global_settings.collision_enable) { - auto p = prof.make_scoped_child("collision-draw"); - m_collide_renderer.render(&m_render_state, p); + if (bucket_id == 31 - 1) { + if (Gfx::g_global_settings.collision_enable) { + auto p = prof.make_scoped_child("collision-draw"); + m_collide_renderer.render(&m_render_state, p); + } + + { + auto p = prof.make_scoped_child("bsp-draw"); + m_bsp_renderer.render(&m_render_state, p); + } } } diff --git a/game/graphics/opengl_renderer/OpenGLRenderer.h b/game/graphics/opengl_renderer/OpenGLRenderer.h index 9f0faad591..36cff88213 100644 --- a/game/graphics/opengl_renderer/OpenGLRenderer.h +++ b/game/graphics/opengl_renderer/OpenGLRenderer.h @@ -5,6 +5,7 @@ #include "common/dma/dma_chain_read.h" +#include "game/graphics/opengl_renderer/BspRenderer.h" #include "game/graphics/opengl_renderer/BucketRenderer.h" #include "game/graphics/opengl_renderer/CollideMeshRenderer.h" #include "game/graphics/opengl_renderer/Fbo.h" @@ -120,6 +121,7 @@ class OpenGLRenderer { std::array m_category_times; FullScreenDraw m_blackout_renderer; CollideMeshRenderer m_collide_renderer; + BspRenderer m_bsp_renderer; float m_last_pmode_alp = 1.; bool m_enable_fast_blackout_loads = true; diff --git a/game/graphics/opengl_renderer/Shader.cpp b/game/graphics/opengl_renderer/Shader.cpp index 9850d6b624..8fd497e410 100644 --- a/game/graphics/opengl_renderer/Shader.cpp +++ b/game/graphics/opengl_renderer/Shader.cpp @@ -131,6 +131,7 @@ ShaderLibrary::ShaderLibrary(GameVersion version) { at(ShaderId::HFRAG) = {"hfrag", version}; at(ShaderId::HFRAG_MONTAGE) = {"hfrag_montage", version}; at(ShaderId::PLAIN_TEXTURE) = {"plain_texture", version}; + at(ShaderId::BSP) = {"bsp", version}; for (auto& shader : m_shaders) { ASSERT_MSG(shader.okay(), "error compiling shader"); diff --git a/game/graphics/opengl_renderer/Shader.h b/game/graphics/opengl_renderer/Shader.h index 02769f9528..17af54ef75 100644 --- a/game/graphics/opengl_renderer/Shader.h +++ b/game/graphics/opengl_renderer/Shader.h @@ -64,6 +64,7 @@ enum class ShaderId { HFRAG = 37, HFRAG_MONTAGE = 38, PLAIN_TEXTURE = 39, + BSP = 40, MAX_SHADERS }; diff --git a/game/graphics/opengl_renderer/shaders/bsp.frag b/game/graphics/opengl_renderer/shaders/bsp.frag new file mode 100644 index 0000000000..5c9ea5b9b6 --- /dev/null +++ b/game/graphics/opengl_renderer/shaders/bsp.frag @@ -0,0 +1,11 @@ +#version 410 core + +out vec4 color; + +in vec4 fragment_color; + +void main() { + + if (fragment_color.a <= 0) discard; + color = fragment_color; +} diff --git a/game/graphics/opengl_renderer/shaders/bsp.vert b/game/graphics/opengl_renderer/shaders/bsp.vert new file mode 100644 index 0000000000..4135515d7f --- /dev/null +++ b/game/graphics/opengl_renderer/shaders/bsp.vert @@ -0,0 +1,61 @@ +#version 410 core + +layout (location = 0) in vec3 position_in; +layout (location = 1) in uint bsp_cell; +// layout (location = 2) in vec3 normal_in; +// layout (location = 3) in uint pat; + +uniform vec4 hvdf_offset; +uniform mat4 camera; +uniform vec4 camera_position; +uniform float fog_constant; +uniform float fog_min; +uniform float fog_max; +uniform int wireframe; +uniform int min_leaf; +uniform int max_leaf; + +out vec4 fragment_color; + +void main() { + // Step 3, the camera transform + vec4 transformed = -camera[3].xyzw; + transformed += -camera[0] * position_in.x; + transformed += -camera[1] * position_in.y; + transformed += -camera[2] * position_in.z; + + // compute Q + float Q = fog_constant / transformed[3]; + + // perspective divide! + transformed.xyz *= Q; + + // offset + transformed.xyz += hvdf_offset.xyz; + + // correct xy offset + transformed.xy -= (2048.); + + // correct z scale + transformed.z /= (8388608); + transformed.z -= 1; + + // correct xy scale + transformed.x /= (256); + transformed.y /= -(128); + + // hack + transformed.xyz *= transformed.w; + + gl_Position = transformed; + // scissoring area adjust + gl_Position.y *= SCISSOR_ADJUST * HEIGHT_SCALE; + + // + fragment_color = vec4(0.12, 0.12, 0.12, 0.5); + + if (bsp_cell < min_leaf || bsp_cell > max_leaf) { + fragment_color.a = 0; + } + +}