diff --git a/.github/scripts/releases/extract_build_linux.sh b/.github/scripts/releases/extract_build_linux.sh index 8fca5cde22..8bd4be176d 100755 --- a/.github/scripts/releases/extract_build_linux.sh +++ b/.github/scripts/releases/extract_build_linux.sh @@ -28,3 +28,4 @@ cp -r $SOURCE/decompiler/config $DEST/data/decompiler/ cp -r $SOURCE/goal_src $DEST/data cp -r $SOURCE/game/assets $DEST/data/game/ cp -r $SOURCE/game/graphics/opengl_renderer/shaders $DEST/data/game/graphics/opengl_renderer +cp -r $SOURCE/custom_levels $DEST/data diff --git a/.github/scripts/releases/extract_build_windows.sh b/.github/scripts/releases/extract_build_windows.sh index 8982346d80..7829505725 100755 --- a/.github/scripts/releases/extract_build_windows.sh +++ b/.github/scripts/releases/extract_build_windows.sh @@ -24,3 +24,4 @@ cp -r $SOURCE/decompiler/config $DEST/data/decompiler/ cp -r $SOURCE/goal_src $DEST/data cp -r $SOURCE/game/assets $DEST/data/game/ cp -r $SOURCE/game/graphics/opengl_renderer/shaders $DEST/data/game/graphics/opengl_renderer +cp -r $SOURCE/custom_levels $DEST/data \ No newline at end of file diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index f813120fc1..6c9656c9fd 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -4,6 +4,7 @@ add_library(common cross_sockets/XSocket.cpp cross_sockets/XSocketServer.cpp cross_sockets/XSocketClient.cpp + custom_data/pack_helpers.cpp custom_data/TFrag3Data.cpp deserialization/subtitles/subtitles.cpp dma/dma.cpp diff --git a/common/custom_data/TFrag3Data.cpp b/common/custom_data/TFrag3Data.cpp index 70c527eb9a..bd36741f69 100644 --- a/common/custom_data/TFrag3Data.cpp +++ b/common/custom_data/TFrag3Data.cpp @@ -1,3 +1,5 @@ +#include +#include #include "Tfrag3Data.h" #include "common/util/Assert.h" @@ -21,6 +23,7 @@ void StripDraw::serialize(Serializer& ser) { ser.from_ptr(&mode); ser.from_ptr(&tree_tex_id); ser.from_pod_vector(&runs); + ser.from_pod_vector(&plain_indices); ser.from_pod_vector(&vis_groups); ser.from_ptr(&num_triangles); } @@ -64,6 +67,7 @@ void TfragTree::serialize(Serializer& ser) { ser.from_pod_vector(&packed_vertices.cluster_origins); ser.from_pod_vector(&colors); bvh.serialize(ser); + ser.from_ptr(&use_strips); } void TieTree::unpack() { @@ -78,7 +82,7 @@ void TieTree::unpack() { vtx.x = proto_vtx.x; vtx.y = proto_vtx.y; vtx.z = proto_vtx.z; - vtx.q = 1.f; + vtx.q_unused = 1.f; vtx.s = proto_vtx.s; vtx.t = proto_vtx.t; i++; @@ -93,7 +97,7 @@ void TieTree::unpack() { vtx.x = temp.x(); vtx.y = temp.y(); vtx.z = temp.z(); - vtx.q = 1.f; + vtx.q_unused = 1.f; vtx.s = proto_vtx.s; vtx.t = proto_vtx.t; i++; @@ -103,6 +107,7 @@ void TieTree::unpack() { for (auto& draw : static_draws) { draw.unpacked.idx_of_first_idx_in_full_buffer = unpacked.indices.size(); + ASSERT(draw.plain_indices.empty()); for (auto& run : draw.runs) { for (u32 ri = 0; ri < run.length; ri++) { unpacked.indices.push_back(run.vertex0 + ri); @@ -152,7 +157,7 @@ void TfragTree::unpack() { o.z = cz + in.zoff * rescale; o.s = in.s / (1024.f); o.t = in.t / (1024.f); - o.q = 1.f; + o.q_unused = 1.f; o.color_index = in.color_index; } @@ -162,8 +167,12 @@ void TfragTree::unpack() { for (u32 ri = 0; ri < run.length; ri++) { unpacked.indices.push_back(run.vertex0 + ri); } - unpacked.indices.push_back(UINT32_MAX); + if (use_strips) { + unpacked.indices.push_back(UINT32_MAX); + } } + unpacked.indices.insert(unpacked.indices.end(), draw.plain_indices.begin(), + draw.plain_indices.end()); } } @@ -359,6 +368,7 @@ std::array Level::get_memory_usage() c for (const auto& tfrag_tree : tfrag_tree_geoms) { for (const auto& draw : tfrag_tree.draws) { result[TFRAG_INDEX] += draw.runs.size() * sizeof(StripDraw::VertexRun); + result[TFRAG_INDEX] += draw.plain_indices.size() * sizeof(u32); result[TFRAG_VIS] += draw.vis_groups.size() * sizeof(StripDraw::VisGroup); } result[TFRAG_VERTS] += @@ -417,4 +427,54 @@ std::array Level::get_memory_usage() c return result; } +void print_memory_usage(const tfrag3::Level& lev, int uncompressed_data_size) { + int total_accounted = 0; + auto memory_use_by_category = lev.get_memory_usage(); + + std::vector> known_categories = { + {"texture", memory_use_by_category[tfrag3::MemoryUsageCategory::TEXTURE]}, + {"tie-deinst-vis", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_DEINST_VIS]}, + {"tie-deinst-idx", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_DEINST_INDEX]}, + {"tie-inst-vis", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_INST_VIS]}, + {"tie-inst-idx", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_INST_INDEX]}, + {"tie-bvh", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_BVH]}, + {"tie-verts", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_VERTS]}, + {"tie-colors", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_TIME_OF_DAY]}, + {"tie-wind-inst-info", + memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_WIND_INSTANCE_INFO]}, + {"tie-cidx", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_CIDX]}, + {"tie-mats", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_MATRICES]}, + {"tie-grps", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_GRPS]}, + {"tfrag-vis", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_VIS]}, + {"tfrag-idx", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_INDEX]}, + {"tfrag-vert", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_VERTS]}, + {"tfrag-colors", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_TIME_OF_DAY]}, + {"tfrag-cluster", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_CLUSTER]}, + {"tfrag-bvh", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_BVH]}, + {"shrub-colors", memory_use_by_category[tfrag3::MemoryUsageCategory::SHRUB_TIME_OF_DAY]}, + {"shrub-vert", memory_use_by_category[tfrag3::MemoryUsageCategory::SHRUB_VERT]}, + {"shrub-ind", memory_use_by_category[tfrag3::MemoryUsageCategory::SHRUB_IND]}, + {"collision", memory_use_by_category[tfrag3::MemoryUsageCategory::COLLISION]}, + {"merc-vert", memory_use_by_category[tfrag3::MemoryUsageCategory::MERC_VERT]}, + {"merc-idx", memory_use_by_category[tfrag3::MemoryUsageCategory::MERC_INDEX]}}; + for (auto& known : known_categories) { + total_accounted += known.second; + } + + known_categories.push_back({"unknown", uncompressed_data_size - total_accounted}); + + std::sort(known_categories.begin(), known_categories.end(), + [](const auto& a, const auto& b) { return a.second > b.second; }); + + for (const auto& x : known_categories) { + fmt::print("{:30s} : {:6d} kB {:3.1f}%\n", x.first, x.second / 1024, + 100.f * (float)x.second / uncompressed_data_size); + } +} + +std::size_t PreloadedVertex::hash::operator()(const PreloadedVertex& v) const { + return std::hash()(v.x) ^ std::hash()(v.y) ^ std::hash()(v.z) ^ + std::hash()(v.s) ^ std::hash()(v.t) ^ std::hash()(v.color_index); +} + } // namespace tfrag3 diff --git a/common/custom_data/Tfrag3Data.h b/common/custom_data/Tfrag3Data.h index 2354eeac2c..c730190bb4 100644 --- a/common/custom_data/Tfrag3Data.h +++ b/common/custom_data/Tfrag3Data.h @@ -53,17 +53,26 @@ enum MemoryUsageCategory { NUM_CATEGORIES }; -constexpr int TFRAG3_VERSION = 19; +constexpr int TFRAG3_VERSION = 20; // These vertices should be uploaded to the GPU at load time and don't change struct PreloadedVertex { // the vertex position float x, y, z; // texture coordinates - float s, t, q; + float s, t, q_unused; // color table index u16 color_index; u16 pad[3]; + + struct hash { + std::size_t operator()(const PreloadedVertex& x) const; + }; + + bool operator==(const PreloadedVertex& other) const { + return x == other.x && y == other.y && z == other.z && s == other.s && t == other.t && + color_index == other.color_index; + } }; static_assert(sizeof(PreloadedVertex) == 32, "PreloadedVertex size"); @@ -144,12 +153,14 @@ struct StripDraw { u32 idx_of_first_idx_in_full_buffer = 0; } unpacked; + // indices can be specified as lists of runs and plain indices. + // the runs are still drawn with indexed opengl calls, it just uses less space in the file. struct VertexRun { u32 vertex0; u16 length; }; - std::vector runs; + std::vector plain_indices; // to do culling, the above vertex stream is grouped. // by following the visgroups and checking the visibility, you can leave out invisible vertices. @@ -260,6 +271,7 @@ struct TfragTree { PackedTfragVertices packed_vertices; std::vector colors; // vertex colors (pre-interpolation) BVH bvh; // the bvh for frustum culling + bool use_strips = true; struct { std::vector vertices; // mesh vertices @@ -397,4 +409,6 @@ struct Level { std::array get_memory_usage() const; }; +void print_memory_usage(const tfrag3::Level& lev, int uncompressed_data_size); + } // namespace tfrag3 diff --git a/common/custom_data/pack_helpers.cpp b/common/custom_data/pack_helpers.cpp new file mode 100644 index 0000000000..0dd17d700b --- /dev/null +++ b/common/custom_data/pack_helpers.cpp @@ -0,0 +1,71 @@ +#include "pack_helpers.h" +#include + +constexpr float kClusterSize = 4096 * 40; // 100 in-game meters +constexpr float kMasterOffset = 12000 * 4096; + +std::pair position_to_cluster_and_offset(float in) { + in += kMasterOffset; + if (in < 0) { + fmt::print("negative: {}\n", in); + } + ASSERT(in >= 0); + int cluster_cell = (in / kClusterSize); + float leftover = in - (cluster_cell * kClusterSize); + u16 offset = (leftover / kClusterSize) * float(UINT16_MAX); + + float recovered = ((float)cluster_cell + ((float)offset / UINT16_MAX)) * kClusterSize; + float diff = std::fabs(recovered - in); + ASSERT(diff < 7); + ASSERT(cluster_cell >= 0); + ASSERT(cluster_cell < UINT16_MAX); + return {cluster_cell, offset}; +} + +void pack_tfrag_vertices(tfrag3::PackedTfragVertices* result, + const std::vector& vertices) { + u32 next_cluster_idx = 0; + std::map clusters; + + for (auto& vtx : vertices) { + auto x = position_to_cluster_and_offset(vtx.x); + auto y = position_to_cluster_and_offset(vtx.y); + auto z = position_to_cluster_and_offset(vtx.z); + u64 cluster_id = 0; + cluster_id |= x.first; + cluster_id |= (y.first << 16); + cluster_id |= (z.first << 32); + + auto cluster_it = clusters.find(cluster_id); + u32 my_cluster_idx = 0; + if (cluster_it == clusters.end()) { + // first in cluster + clusters[cluster_id] = next_cluster_idx; + my_cluster_idx = next_cluster_idx; + next_cluster_idx++; + } else { + my_cluster_idx = cluster_it->second; + } + + tfrag3::PackedTfragVertices::Vertex out_vtx; + out_vtx.xoff = x.second; + out_vtx.yoff = y.second; + out_vtx.zoff = z.second; + out_vtx.cluster_idx = my_cluster_idx; + // TODO check these + out_vtx.s = vtx.s * 1024; + out_vtx.t = vtx.t * 1024; + out_vtx.color_index = vtx.color_index; + result->vertices.push_back(out_vtx); + } + + result->cluster_origins.resize(next_cluster_idx); + for (auto& cluster : clusters) { + auto& res = result->cluster_origins[cluster.second]; + res.x() = (u16)cluster.first; + res.y() = (u16)(cluster.first >> 16); + res.z() = (u16)(cluster.first >> 32); + } + + ASSERT(next_cluster_idx < UINT16_MAX); +} \ No newline at end of file diff --git a/common/custom_data/pack_helpers.h b/common/custom_data/pack_helpers.h new file mode 100644 index 0000000000..c477a22c40 --- /dev/null +++ b/common/custom_data/pack_helpers.h @@ -0,0 +1,6 @@ +#pragma once + +#include "common/custom_data/Tfrag3Data.h" + +void pack_tfrag_vertices(tfrag3::PackedTfragVertices* result, + const std::vector& vertices); \ No newline at end of file diff --git a/common/math/Vector.h b/common/math/Vector.h index bb0ba31a1c..9351b123ea 100644 --- a/common/math/Vector.h +++ b/common/math/Vector.h @@ -89,6 +89,14 @@ class Vector { return result; } + Vector operator+(const T& other) const { + Vector result; + for (int i = 0; i < Size; i++) { + result[i] = m_data[i] + other; + } + return result; + } + Vector& operator+=(const Vector& other) { for (int i = 0; i < Size; i++) { m_data[i] += other[i]; @@ -103,6 +111,13 @@ class Vector { return *this; } + Vector& operator-=(const T& other) { + for (int i = 0; i < Size; i++) { + m_data[i] -= other; + } + return *this; + } + Vector elementwise_multiply(const Vector& other) const { Vector result; for (int i = 0; i < Size; i++) { @@ -180,6 +195,18 @@ class Vector { void normalize(const T& norm = T(1)) { *this = normalized(norm); } + void max_in_place(const Vector& other) { + for (int i = 0; i < Size; i++) { + m_data[i] = std::max(m_data[i], other[i]); + } + } + + void min_in_place(const Vector& other) { + for (int i = 0; i < Size; i++) { + m_data[i] = std::min(m_data[i], other[i]); + } + } + std::string to_string_aligned() const { std::string result = "["; for (auto x : m_data) { @@ -229,6 +256,8 @@ class Vector { } } + void set_zero() { fill(0); } + private: T m_data[Size]; }; @@ -246,8 +275,24 @@ struct Matrix { return result; } - // const T& operator()(int r, int c) const { return m_data[c + r * Cols]; } - // T& operator()(int r, int c) { return m_data[r + c * Rows]; } + static Matrix identity() { + Matrix result; + for (int c = 0; c < Cols; c++) { + for (int r = 0; r < Rows; r++) { + result(r, c) = r == c ? T(1) : T(0); + } + } + return result; + } + + void set_zero() { + for (auto& x : m_data) { + x = 0; + } + } + + T& operator()(int r, int c) { return m_data[r + c * Rows]; } + const T& operator()(int r, int c) const { return m_data[r + c * Rows]; } Vector col(int c) const { Vector result; @@ -274,6 +319,31 @@ struct Matrix { return result; } + template + Matrix operator*(const Matrix& y) const { + Matrix result; + result.set_zero(); + for (int rx = 0; rx < Rows; rx++) { + for (int cx = 0; cx < Cols; cx++) { + for (int yi = 0; yi < OtherCols; yi++) { + result(rx, yi) += operator()(rx, cx) * y(cx, yi); + } + } + } + return result; + } + + Vector operator*(const Vector& y) const { + Vector result; + result.set_zero(); + for (int rx = 0; rx < Rows; rx++) { + for (int cx = 0; cx < Cols; cx++) { + result[rx] += operator()(rx, cx) * y[cx]; + } + } + return result; + } + private: T m_data[Rows * Cols]; }; diff --git a/common/math/geometry.cpp b/common/math/geometry.cpp index e69de29bb2..59aad2c4c0 100644 --- a/common/math/geometry.cpp +++ b/common/math/geometry.cpp @@ -0,0 +1,40 @@ +#include "geometry.h" + +namespace math { + +Vector4f bsphere_of_triangle(const Vector3f* v) { + Vector4f bsphere; + auto& p1 = v[0]; + auto& p2 = v[1]; + auto& p3 = v[2]; + float A = (p1 - p2).length(); + float B = (p2 - p3).length(); + float C = (p3 - p1).length(); + + const Vector3f *a = &p3, *b = &p1, *c = &p2; + if (B < C) + std::swap(B, C), std::swap(b, c); + if (A < B) + std::swap(A, B), std::swap(a, b); + + float r; + math::Vector3f origin; + if ((B * B) + (C * C) <= (A * A)) { + r = A / 2.f; + origin = (*b + *c) / 2.f; + } else { + float cos_a = (B * B + C * C - A * A) / (B * C * 2); + r = A / (sqrt(1 - cos_a * cos_a) * 2.f); + Vector3f alpha = *a - *c, beta = *b - *c; + origin = (beta * alpha.dot(alpha) - alpha * beta.dot(beta)).cross(alpha.cross(beta)) / + (alpha.cross(beta).dot(alpha.cross(beta)) * 2.f) + + *c; + } + bsphere.x() = origin.x(); + bsphere.y() = origin.y(); + bsphere.z() = origin.z(); + bsphere.w() = r; + return bsphere; +} + +} // namespace math \ No newline at end of file diff --git a/common/math/geometry.h b/common/math/geometry.h index 4c71592bf7..a61988f444 100644 --- a/common/math/geometry.h +++ b/common/math/geometry.h @@ -44,4 +44,10 @@ RaySphereResult ray_sphere_intersect(const Vector3& ray_origin, result.u[1] = minus_b - sqrt_val; return result; } + +math::Vector4f bsphere_of_triangle(const Vector3f* vertices); + +inline bool point_in_bsphere(const Vector4f& sphere, const Vector3f& pt) { + return (sphere.xyz() - pt).squared_length() <= (sphere.w() * sphere.w()); +} } // namespace math \ No newline at end of file diff --git a/custom_levels/README.md b/custom_levels/README.md new file mode 100644 index 0000000000..0a6a9aff04 --- /dev/null +++ b/custom_levels/README.md @@ -0,0 +1,36 @@ +# Custom Levels +Disclaimer: custom levels are still in development and are missing most features. + + +The first three steps are already done for "test zone", so this can be used as a starting point. + +# 1: File Setup +To create a custom level, copy the layout of `custom_levels/test-zone`. See `test-zone.jsonc` for information on how to name things. The `.gd` file also contains the level name. + +# 2: Modify the engine +Modify `goal_src/engine/level/level-info.gc` to add level info for each custom level. There is level info for `test-zone` at the bottom that can be used as an example. + +# 3: Modify the build system +Modify `goal_src/game.gp` and add a custom level target: +```lisp +(build-custom-level "test-zone") +;; the DGO file +(custom-level-cgo "TESTZONE.DGO" "test-zone/testzone.gd") +``` + +# 4: Export the GLTF file from blender. +For now, all meshes are displayed and treated as ground collision. This causes buggy collision because walls shouldn't use "floor" mode. + +Blender will create a `.glb` file, which must have the name specified in the `.jsonc` file and should be located in `custom_level/your_level` + +# 5: Rebuild the game +Any time the `.glb` file is changed, you must rebuild the game. Launch the compiler (`goalc`) and run `(mi)` to rebuild everything. It's recommended to leave the compiler open - it will remember files that haven't changed and skip rebuilding them. + +# 6: Go to the custom level +Start the game in debug mode `gk`. + +In the compiler window, run `(lt)` to connect to the game. You must run this again every time you restart the game. If this doesn't work, there could be a firewall issue and you must allow goalc/gk to use the network. They don't make any outside connections. + +In the compiler window, run a command like `(bg-custom 'test-zone-vis)` to load and start at a custom level. + + diff --git a/custom_levels/test-zone/test-zone.jsonc b/custom_levels/test-zone/test-zone.jsonc new file mode 100644 index 0000000000..73c66edf00 --- /dev/null +++ b/custom_levels/test-zone/test-zone.jsonc @@ -0,0 +1,15 @@ +{ + // The "in-game" name of the level. Should be lower case, with dashes (GOAL symbol name) + // the name of this file, and the folder this file is in must have the same name. + "long_name": "test-zone", + // The file name, should be upper case and 8 characters or less. + "iso_name": "TESTZONE", + // The nickname, should be exactly 3 characters + "nickname": "TSZ", // 3 char name, all uppercase + + // Background mesh file. + // Must have vertex colors. Use the blender cycles renderer, bake, diffuse, uncheck color, + // and bake to vertex colors. For now, only the first vertex color group is used, so make sure you + // only have 1. + "gltf_file": "custom_levels/test-zone/test-zone2.glb" +} \ No newline at end of file diff --git a/custom_levels/test-zone/test-zone2.glb b/custom_levels/test-zone/test-zone2.glb new file mode 100644 index 0000000000..2df7f4d595 Binary files /dev/null and b/custom_levels/test-zone/test-zone2.glb differ diff --git a/custom_levels/test-zone/testzone.gd b/custom_levels/test-zone/testzone.gd new file mode 100644 index 0000000000..0ecd61c2ae --- /dev/null +++ b/custom_levels/test-zone/testzone.gd @@ -0,0 +1,8 @@ +;; DGO definition file for Awful Village level +;; We use the convention of having a longer DGO name for levels without precomputed visibility. + +;; the actual file name still needs to be 8.3 +("TSZ.DGO" + ("static-screen.o" "static-screen") + ("test-zone.go" "test-zone") + ) \ No newline at end of file diff --git a/decompiler/level_extractor/extract_level.cpp b/decompiler/level_extractor/extract_level.cpp index f63931a31c..ec1a12d34a 100644 --- a/decompiler/level_extractor/extract_level.cpp +++ b/decompiler/level_extractor/extract_level.cpp @@ -53,51 +53,6 @@ bool is_valid_bsp(const decompiler::LinkedObjectFile& file) { return true; } -void print_memory_usage(const tfrag3::Level& lev, int uncompressed_data_size) { - int total_accounted = 0; - auto memory_use_by_category = lev.get_memory_usage(); - - std::vector> known_categories = { - {"texture", memory_use_by_category[tfrag3::MemoryUsageCategory::TEXTURE]}, - {"tie-deinst-vis", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_DEINST_VIS]}, - {"tie-deinst-idx", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_DEINST_INDEX]}, - {"tie-inst-vis", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_INST_VIS]}, - {"tie-inst-idx", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_INST_INDEX]}, - {"tie-bvh", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_BVH]}, - {"tie-verts", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_VERTS]}, - {"tie-colors", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_TIME_OF_DAY]}, - {"tie-wind-inst-info", - memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_WIND_INSTANCE_INFO]}, - {"tie-cidx", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_CIDX]}, - {"tie-mats", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_MATRICES]}, - {"tie-grps", memory_use_by_category[tfrag3::MemoryUsageCategory::TIE_GRPS]}, - {"tfrag-vis", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_VIS]}, - {"tfrag-idx", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_INDEX]}, - {"tfrag-vert", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_VERTS]}, - {"tfrag-colors", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_TIME_OF_DAY]}, - {"tfrag-cluster", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_CLUSTER]}, - {"tfrag-bvh", memory_use_by_category[tfrag3::MemoryUsageCategory::TFRAG_BVH]}, - {"shrub-colors", memory_use_by_category[tfrag3::MemoryUsageCategory::SHRUB_TIME_OF_DAY]}, - {"shrub-vert", memory_use_by_category[tfrag3::MemoryUsageCategory::SHRUB_VERT]}, - {"shrub-ind", memory_use_by_category[tfrag3::MemoryUsageCategory::SHRUB_IND]}, - {"collision", memory_use_by_category[tfrag3::MemoryUsageCategory::COLLISION]}, - {"merc-vert", memory_use_by_category[tfrag3::MemoryUsageCategory::MERC_VERT]}, - {"merc-idx", memory_use_by_category[tfrag3::MemoryUsageCategory::MERC_INDEX]}}; - for (auto& known : known_categories) { - total_accounted += known.second; - } - - known_categories.push_back({"unknown", uncompressed_data_size - total_accounted}); - - std::sort(known_categories.begin(), known_categories.end(), - [](const auto& a, const auto& b) { return a.second > b.second; }); - - for (const auto& x : known_categories) { - fmt::print("{:30s} : {:6d} kB {:3.1f}%\n", x.first, x.second / 1024, - 100.f * (float)x.second / uncompressed_data_size); - } -} - void add_all_textures_from_level(tfrag3::Level& lev, const std::string& level_name, const TextureDB& tex_db) { diff --git a/decompiler/level_extractor/extract_tfrag.cpp b/decompiler/level_extractor/extract_tfrag.cpp index 2af87aa9dc..82a3045d8a 100644 --- a/decompiler/level_extractor/extract_tfrag.cpp +++ b/decompiler/level_extractor/extract_tfrag.cpp @@ -5,6 +5,7 @@ #include "common/util/FileUtil.h" #include "common/dma/gs.h" #include "common/util/Assert.h" +#include "common/custom_data/pack_helpers.h" namespace decompiler { namespace { @@ -2056,9 +2057,9 @@ void make_tfrag3_data(std::map>& draws, vtx.z = vert.pre_cam_trans_pos.z(); vtx.s = vert.stq.x(); vtx.t = vert.stq.y(); - vtx.q = vert.stq.z(); + vtx.q_unused = vert.stq.z(); // if this is true, we can remove a divide in the shader - ASSERT(vtx.q == 1.f); + ASSERT(vtx.q_unused == 1.f); vtx.color_index = vert.rgba / 4; // ASSERT((vert.rgba >> 2) < 1024); spider cave has 2048? ASSERT((vert.rgba & 3) == 0); @@ -2137,85 +2138,6 @@ void merge_groups(std::vector& grps) { } // namespace -constexpr float kClusterSize = 4096 * 40; // 100 in-game meters -constexpr float kMasterOffset = 12000 * 4096; - -std::pair position_to_cluster_and_offset(float in) { - in += kMasterOffset; - if (in < 0) { - fmt::print("negative: {}\n", in); - } - ASSERT(in >= 0); - int cluster_cell = (in / kClusterSize); - float leftover = in - (cluster_cell * kClusterSize); - u16 offset = (leftover / kClusterSize) * float(UINT16_MAX); - - float recovered = ((float)cluster_cell + ((float)offset / UINT16_MAX)) * kClusterSize; - float diff = std::fabs(recovered - in); - ASSERT(diff < 7); - ASSERT(cluster_cell >= 0); - ASSERT(cluster_cell < UINT16_MAX); - return {cluster_cell, offset}; -} - -void pack_vertices(tfrag3::PackedTfragVertices* result, - const std::vector& vertices) { - u32 next_cluster_idx = 0; - std::map clusters; - - for (auto& vtx : vertices) { - auto x = position_to_cluster_and_offset(vtx.x); - auto y = position_to_cluster_and_offset(vtx.y); - auto z = position_to_cluster_and_offset(vtx.z); - u64 cluster_id = 0; - cluster_id |= x.first; - cluster_id |= (y.first << 16); - cluster_id |= (z.first << 32); - - auto cluster_it = clusters.find(cluster_id); - u32 my_cluster_idx = 0; - if (cluster_it == clusters.end()) { - // first in cluster - clusters[cluster_id] = next_cluster_idx; - my_cluster_idx = next_cluster_idx; - next_cluster_idx++; - } else { - my_cluster_idx = cluster_it->second; - } - - tfrag3::PackedTfragVertices::Vertex out_vtx; - out_vtx.xoff = x.second; - out_vtx.yoff = y.second; - out_vtx.zoff = z.second; - out_vtx.cluster_idx = my_cluster_idx; - // TODO check these - out_vtx.s = vtx.s * 1024; - out_vtx.t = vtx.t * 1024; - out_vtx.color_index = vtx.color_index; - result->vertices.push_back(out_vtx); - } - - result->cluster_origins.resize(next_cluster_idx); - for (auto& cluster : clusters) { - auto& res = result->cluster_origins[cluster.second]; - res.x() = (u16)cluster.first; - res.y() = (u16)(cluster.first >> 16); - res.z() = (u16)(cluster.first >> 32); - } - - /* - std::unordered_set - a; - for (auto& v : result->vertices) { - a.insert(v); - } - fmt::print("SIZE: {} vs {} {}\n", a.size(), result->vertices.size(), - (float)a.size() / result->vertices.size()); - */ - - ASSERT(next_cluster_idx < UINT16_MAX); -} - void extract_tfrag(const level_tools::DrawableTreeTfrag* tree, const std::string& debug_name, const std::vector& map, @@ -2282,7 +2204,7 @@ void extract_tfrag(const level_tools::DrawableTreeTfrag* tree, std::vector vertices; emulate_tfrags(geom, as_tfrag_array->tfragments, debug_name, map, out, this_tree, vertices, tex_db, expected_missing_textures, dump_level); - pack_vertices(&this_tree.packed_vertices, vertices); + pack_tfrag_vertices(&this_tree.packed_vertices, vertices); extract_time_of_day(tree, this_tree); for (auto& draw : this_tree.draws) { diff --git a/game/graphics/opengl_renderer/background/Tfrag3.cpp b/game/graphics/opengl_renderer/background/Tfrag3.cpp index 1b6846e0a7..a1ee89a64a 100644 --- a/game/graphics/opengl_renderer/background/Tfrag3.cpp +++ b/game/graphics/opengl_renderer/background/Tfrag3.cpp @@ -78,6 +78,7 @@ void Tfrag3::update_load(const std::vector& tree_kind tree_cache.vis = &tree.bvh; tree_cache.index_data = tree.unpacked.indices.data(); tree_cache.tod_cache = swizzle_time_of_day(tree.colors); + tree_cache.draw_mode = tree.use_strips ? GL_TRIANGLE_STRIP : GL_TRIANGLES; vis_temp_len = std::max(vis_temp_len, tree.bvh.vis_nodes.size()); glBindBuffer(GL_ARRAY_BUFFER, tree_cache.vertex_buffer); // glBufferData(GL_ARRAY_BUFFER, verts * sizeof(tfrag3::PreloadedVertex), @@ -240,11 +241,11 @@ void Tfrag3::render_tree(int geom, prof.add_draw_call(); if (render_state->no_multidraw) { - glDrawElements(GL_TRIANGLE_STRIP, singledraw_indices.second, GL_UNSIGNED_INT, + glDrawElements(tree.draw_mode, singledraw_indices.second, GL_UNSIGNED_INT, (void*)(singledraw_indices.first * sizeof(u32))); } else { - glMultiDrawElements(GL_TRIANGLE_STRIP, - &m_cache.multidraw_count_buffer[multidraw_indices.first], GL_UNSIGNED_INT, + glMultiDrawElements(tree.draw_mode, &m_cache.multidraw_count_buffer[multidraw_indices.first], + GL_UNSIGNED_INT, &m_cache.multidraw_index_offset_buffer[multidraw_indices.first], multidraw_indices.second); } @@ -260,11 +261,11 @@ void Tfrag3::render_tree(int geom, double_draw.aref_second); glDepthMask(GL_FALSE); if (render_state->no_multidraw) { - glDrawElements(GL_TRIANGLE_STRIP, singledraw_indices.second, GL_UNSIGNED_INT, + glDrawElements(tree.draw_mode, singledraw_indices.second, GL_UNSIGNED_INT, (void*)(singledraw_indices.first * sizeof(u32))); } else { glMultiDrawElements( - GL_TRIANGLE_STRIP, &m_cache.multidraw_count_buffer[multidraw_indices.first], + tree.draw_mode, &m_cache.multidraw_count_buffer[multidraw_indices.first], GL_UNSIGNED_INT, &m_cache.multidraw_index_offset_buffer[multidraw_indices.first], multidraw_indices.second); } diff --git a/game/graphics/opengl_renderer/background/Tfrag3.h b/game/graphics/opengl_renderer/background/Tfrag3.h index 24762a087f..295263ee1f 100644 --- a/game/graphics/opengl_renderer/background/Tfrag3.h +++ b/game/graphics/opengl_renderer/background/Tfrag3.h @@ -64,6 +64,7 @@ class Tfrag3 { const tfrag3::BVH* vis = nullptr; const u32* index_data = nullptr; SwizzledTimeOfDay tod_cache; + u64 draw_mode = 0; void reset_stats() { rendered_this_frame = false; diff --git a/game/graphics/opengl_renderer/loader/LoaderStages.cpp b/game/graphics/opengl_renderer/loader/LoaderStages.cpp index e75e76c2a2..618ae46611 100644 --- a/game/graphics/opengl_renderer/loader/LoaderStages.cpp +++ b/game/graphics/opengl_renderer/loader/LoaderStages.cpp @@ -83,6 +83,7 @@ class TfragLoadStage : public LoaderStage { GLuint& tree_out = data.lev_data->tfrag_vertex_data[geo].emplace_back(); glGenBuffers(1, &tree_out); glBindBuffer(GL_ARRAY_BUFFER, tree_out); + glBufferData(GL_ARRAY_BUFFER, in_tree.unpacked.vertices.size() * sizeof(tfrag3::PreloadedVertex), nullptr, GL_STATIC_DRAW); @@ -97,34 +98,38 @@ class TfragLoadStage : public LoaderStage { u32 unique_buffers = 0; while (true) { - const auto& tree = data.lev_data->level->tfrag_trees[m_next_geo][m_next_tree]; - u32 end_vert_in_tree = tree.unpacked.vertices.size(); - // the number of vertices we'd need to finish the tree right now - size_t num_verts_left_in_tree = end_vert_in_tree - m_next_vert; - size_t start_vert_for_chunk; - size_t end_vert_for_chunk; - bool complete_tree; - if (num_verts_left_in_tree > CHUNK_SIZE) { - complete_tree = false; - // should only do partial - start_vert_for_chunk = m_next_vert; - end_vert_for_chunk = start_vert_for_chunk + CHUNK_SIZE; - m_next_vert += CHUNK_SIZE; - } else { - // should do all! - start_vert_for_chunk = m_next_vert; - end_vert_for_chunk = end_vert_in_tree; + if (data.lev_data->level->tfrag_trees[m_next_geo].empty()) { complete_tree = true; - } + } else { + const auto& tree = data.lev_data->level->tfrag_trees[m_next_geo][m_next_tree]; + u32 end_vert_in_tree = tree.unpacked.vertices.size(); + // the number of vertices we'd need to finish the tree right now + size_t num_verts_left_in_tree = end_vert_in_tree - m_next_vert; + size_t start_vert_for_chunk; + size_t end_vert_for_chunk; - glBindBuffer(GL_ARRAY_BUFFER, data.lev_data->tfrag_vertex_data[m_next_geo][m_next_tree]); - u32 upload_size = - (end_vert_for_chunk - start_vert_for_chunk) * sizeof(tfrag3::PreloadedVertex); - glBufferSubData(GL_ARRAY_BUFFER, start_vert_for_chunk * sizeof(tfrag3::PreloadedVertex), - upload_size, tree.unpacked.vertices.data() + start_vert_for_chunk); - uploaded_bytes += upload_size; + if (num_verts_left_in_tree > CHUNK_SIZE) { + complete_tree = false; + // should only do partial + start_vert_for_chunk = m_next_vert; + end_vert_for_chunk = start_vert_for_chunk + CHUNK_SIZE; + m_next_vert += CHUNK_SIZE; + } else { + // should do all! + start_vert_for_chunk = m_next_vert; + end_vert_for_chunk = end_vert_in_tree; + complete_tree = true; + } + + glBindBuffer(GL_ARRAY_BUFFER, data.lev_data->tfrag_vertex_data[m_next_geo][m_next_tree]); + u32 upload_size = + (end_vert_for_chunk - start_vert_for_chunk) * sizeof(tfrag3::PreloadedVertex); + glBufferSubData(GL_ARRAY_BUFFER, start_vert_for_chunk * sizeof(tfrag3::PreloadedVertex), + upload_size, tree.unpacked.vertices.data() + start_vert_for_chunk); + uploaded_bytes += upload_size; + } if (complete_tree) { unique_buffers++; diff --git a/goal_src/engine/collide/collide-cache.gc b/goal_src/engine/collide/collide-cache.gc index 031c25d23a..800af98612 100644 --- a/goal_src/engine/collide/collide-cache.gc +++ b/goal_src/engine/collide/collide-cache.gc @@ -384,6 +384,8 @@ (set! (-> v1-5 mesh) (-> obj mesh)) (set! (-> v1-5 inst) #f) ) + ; (add-debug-sphere #t (bucket-id debug) (-> obj bsphere) (-> obj bsphere w) (new 'static 'rgba :g #x80 :a #x80)) + ; (format 0 "~f~%" (-> obj bsphere w)) (+! (-> arg1 num-items) 1) ) (&+! obj 32) diff --git a/goal_src/engine/collide/collide-frag.gc b/goal_src/engine/collide/collide-frag.gc index a9703339dc..7246ac2ab7 100644 --- a/goal_src/engine/collide/collide-frag.gc +++ b/goal_src/engine/collide/collide-frag.gc @@ -83,6 +83,19 @@ (defmethod draw collide-fragment ((obj collide-fragment) (arg0 collide-fragment) (arg1 display-frame)) ;; if we wanted to draw collide-fragment's we'd do it here. + ; (when (< (-> obj bsphere w) (meters 22.)) + ; (format 0 "sp: ~m : ~D~%" (-> obj bsphere w) (-> obj mesh poly-count)) + ; (let ((mins (vector-copy! (new-stack-vector0) (-> obj bsphere))) + ; (maxs (vector-copy! (new-stack-vector0) (-> obj bsphere)))) + ; (dotimes (i 3) + ; (-! (-> mins data i) (-> obj bsphere w)) + ; (+! (-> maxs data i) (-> obj bsphere w)) + ; ) + ; (add-debug-box #t (bucket-id debug) mins maxs (new 'static 'rgba :r #x80 :a #x80) + ; ) + ; ;(add-debug-sphere #t (bucket-id debug) (-> obj bsphere) (-> obj bsphere w) (new 'static 'rgba :r #x80 :a #x80)) + ; ) + ;; (add-debug-point #t (bucket-id debug) (-> obj bsphere)) (none) ) diff --git a/goal_src/engine/gfx/texture.gc b/goal_src/engine/gfx/texture.gc index acab5a8189..83b85241d6 100644 --- a/goal_src/engine/gfx/texture.gc +++ b/goal_src/engine/gfx/texture.gc @@ -1371,7 +1371,12 @@ (dotimes (page-idx 9) (set! (-> level texture-page page-idx) #f) ) - + (#when PC_PORT + (when (zero? id-array) + (format #t "ERROR: texture id array is 0, skipping texture login!~%") + (return #f) + ) + ) (if (>= max-page-kind 0) ;; tfrag. ;; login the texture. If the texture isn't there, it will try to load it ;; and allocate with the given allocation function. diff --git a/goal_src/engine/level/level-info.gc b/goal_src/engine/level/level-info.gc index c8761e7778..5864764e72 100644 --- a/goal_src/engine/level/level-info.gc +++ b/goal_src/engine/level/level-info.gc @@ -1972,3 +1972,44 @@ +;;;;;;;;; CUSTOM LEVELS +(define test-zone (new 'static 'level-load-info + :index 26 + :name 'test-zone + :visname 'test-zone-vis ;; name + -vis + :nickname 'tsz ;; nickname + :packages '() + :sound-banks '() + :music-bank #f + :ambient-sounds '() + :mood '*default-mood* + :mood-func 'update-mood-default + :ocean #f + :sky #t + :continues '((new 'static 'continue-point + :name "test-zone-start" + :level 'test-zone + :trans (new 'static 'vector :x 0.0 :y (meters 10.) :z (meters 10.) :w 1.0) + :quat (new 'static 'quaternion :w 1.0) + :camera-trans (new 'static 'vector :x 0.0 :y 4096.0 :z 0.0 :w 1.0) + :camera-rot (new 'static 'array float 9 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0) + :load-commands '() + :vis-nick 'none + :lev0 'test-zone + :disp0 'display + :lev1 'village1 + :disp1 'display + )) + :tasks '() + :priority 100 + :load-commands '() + :alt-load-commands '() + :bsp-mask #xffffffffffffffff + :bsphere (new 'static 'sphere :w 167772160000.0) + :bottom-height (meters -20) + :run-packages '() + :wait-for-load #t + ) + ) + +(cons! *level-load-list* 'test-zone) \ No newline at end of file diff --git a/goal_src/engine/level/level.gc b/goal_src/engine/level/level.gc index 7b3c549f74..371e0fcf8a 100644 --- a/goal_src/engine/level/level.gc +++ b/goal_src/engine/level/level.gc @@ -93,13 +93,13 @@ "Draw a level!" ;; do the draw (draw arg0 arg0 arg3) - + (if (nonzero? *display-strip-lines*) (debug-draw arg0 arg0 arg3) ) (none) ) - + (defmethod print level ((obj level)) "print a level." @@ -115,7 +115,7 @@ (defmethod relocate bsp-header ((obj bsp-header) (dest-heap kheap) (name (pointer uint8))) "Handle a bsp file load." - + ;; we expect that we'll have a loading-level set when we link/login a bsp-header (let ((s5-0 (-> *level* loading-level))) (if s5-0 @@ -175,7 +175,7 @@ (defmethod vis-clear level ((obj level)) "Clear the visibility info for when the level is loading." - + ;; clear vis-infos, so we can't try to look up a vis string. (countdown (v1-0 8) (nop!) ;; the usual. @@ -194,11 +194,11 @@ (defmethod vis-load level ((obj level)) "Start the initial load of a VIS file to the IOP VIS buffer. After this is done, we can use ramdisk-load to load chunks." - - ;; check to see if we have a buffer for loaded vis data. + + ;; check to see if we have a buffer for loaded vis data. (when (zero? (-> obj vis-info (-> obj vis-self-index) ramdisk)) ;; nope, we have no vis data buffer, we need to set it up. - + ;; first, we should see if the other level has loaded vis. if so, kill it. (let ((vis (-> obj other vis-info (-> obj other vis-self-index)))) (when (and vis (nonzero? (-> vis ramdisk))) @@ -207,7 +207,7 @@ 0 ) ) - + ;; set up a ramdisk rpc (fill command, actually load the file from DVD to IOP buffer) (let ((visname (make-file-name (file-kind vis) (the-as string (-> obj nickname)) 0 #f)) (cmd (the-as ramdisk-rpc-fill (add-element *ramdisk-rpc*))) @@ -222,7 +222,7 @@ (set! (-> obj vis-info (-> obj vis-self-index) ramdisk) s5-0) ) ) - + ;; return the ramdisk ID. (-> obj vis-info (-> obj vis-self-index) ramdisk) ) @@ -278,7 +278,7 @@ ) ) ) - + ;; check for up to 6 neighbor level vis info. The last one is always left as null. (dotimes (s5-1 6) (let* ((s3-0 (+ s5-1 1)) @@ -425,7 +425,7 @@ (('alive) (when (and *dproc* (= want-status 'active)) ;; only if we want to do alive -> active - + ;; will set the level to be drawn. (remove-by-param1 *background-draw-engine* (-> obj bsp)) (add-connection *background-draw-engine* *dproc* (the (function object object object object object) add-bsp-drawable) (-> obj bsp) obj #f) @@ -539,10 +539,10 @@ ;; set the level heap. level code logins called from linker may allocate here (set! loading-level (-> obj heap)) - + ;; relocate method of the bsp will look for this (set! (-> *level* loading-level) obj) - + ;; clear out old stuff (set! (-> *level* log-in-level-bsp) #f) (set! (-> obj nickname) #f) @@ -551,12 +551,12 @@ (set! (-> obj ambient) #f) (set! (-> obj linking) #f) (vis-clear obj) - + (set! (-> obj status) 'loading) - + ;; incoming textures should use the level allocator (set! (-> *texture-pool* allocate-func) texture-page-level-allocate) - + ;; build name (if (= (-> obj load-name) (-> obj info visname)) (format (clear *temp-string*) "~S" (-> obj info nickname)) @@ -564,19 +564,19 @@ ) (set! (-> *temp-string* data 8) (the-as uint 0)) (format *temp-string* ".DGO") - + ;; reset temporary allocations on level heap (set! (-> obj heap top) (-> obj heap top-base)) - + ;; allocate DGO loading buffers (let ((s4-0 (kmalloc (-> obj heap) (* 2 1024 1024) (kmalloc-flags align-64 top) "dgo-level-buf-2")) (s5-2 (kmalloc (-> obj heap) (* 2 1024 1024) (kmalloc-flags align-64 top) "dgo-level-buf-2")) ) (load-dbg " DGO buffers at #x~X #x~X~%" s4-0 s5-2) - + ;; we expect to load code first, remember where the heap is now. (set! (-> obj code-memory-start) (-> obj heap current)) - + (format 0 "-----------> begin load ~A [~S]~%" (-> obj load-name) *temp-string*) ;; kick off the load! (dgo-load-begin *temp-string* s5-2 s4-0 (the pointer (align64 (-> obj heap current)))) @@ -586,10 +586,10 @@ (defmethod login-begin level ((obj level)) "Start the login. This is spread over multiple frames." - + ;; done with load, reset the texture page allocator (set! (-> *texture-pool* allocate-func) texture-page-default-allocate) - + (cond ((-> obj bsp) (set! (-> *level* log-in-level-bsp) (-> obj bsp)) @@ -605,7 +605,7 @@ ) ) ) - + ;; set the login state machine at the beginning. (set! (-> *login-state* state) -1) (set! (-> *login-state* pos) (the-as uint 0)) @@ -633,16 +633,16 @@ (sv-16 prototype-bucket-tie) (sv-32 int) ) - + ;; there is some logic for not doing the whole login all at once... ;; for now, we will somewhat ignore that. - - + + (let ((level-drawable-trees (-> loaded-level bsp drawable-trees))) ;;(.mfc0 initial-timer Count) (label cfg-1) ;;(.mfc0 current-timer Count) - + ;; this would quit the login function after some amount of time elapsed. #| (let ((elapsed-timer (- current-timer initial-timer))) @@ -652,16 +652,16 @@ ) ) |# - + (let ((current-login-pos (the-as int (-> level-login-state pos)))) - + ;; Login state -1. ;; in this state, we log in drawables/art-groups that are in referenced in the bsp directly ;; the current-login-pos in the index of the drawable/art to login. - + (when (= (-> level-login-state state) -1) ;;(load-dbg "login state -1~%") - + ;; login some drawables. (when (< current-login-pos (-> level-drawable-trees length)) (let ((current-drawable (-> level-drawable-trees trees (the-as uint current-login-pos)))) @@ -699,7 +699,7 @@ (+! (-> level-login-state pos) 1) (goto cfg-1) ) - + ;; this makes the art groups go at the end. (let ((v1-39 (- (the-as uint current-login-pos) (the-as uint (-> level-drawable-trees length))))) (when (< (the-as int v1-39) (-> loaded-level art-group art-group-array length)) @@ -713,14 +713,14 @@ (goto cfg-1) ) ) - + ;; if we got here, we're done with state -1! (set! (-> level-login-state pos) (the-as uint 0)) (set! (-> level-login-state state) 0) (goto cfg-1) ) - - + + ;; login state 0. ;; we log in children of the drawables from state -1. (when (< (-> level-login-state state) (the-as int (-> level-login-state elts))) @@ -755,7 +755,7 @@ (when (< current-login-pos (-> s1-2 length)) (set! sv-16 (-> s1-2 array-data (the-as uint current-login-pos))) (set! sv-32 0) - + (#when PC_PORT ;; if a TIE uses environment mapping, disable the fade out so it always renderers with ;; the generic renderer. In the port, we just make envmapped things always envmap. @@ -763,7 +763,7 @@ (*! (-> sv-16 envmap-fade-far) 10000.) ) ) - + (while (< sv-32 4) (let ((a0-28 (-> sv-16 geometry sv-32))) ;;(load-dbg " login geom: ~A~%" a0-28) @@ -810,8 +810,8 @@ ) (goto cfg-1) ) - - + + (when (= (-> level-login-state state) (-> level-login-state elts)) (let ((v1-115 (-> loaded-level bsp))) (cond @@ -837,8 +837,7 @@ ) ) ) - - + ;; done! (set! (-> loaded-level nickname) (-> loaded-level bsp nickname)) (if (nonzero? (-> loaded-level bsp nodes)) @@ -857,7 +856,7 @@ (set! (-> *subdivide-settings* close 3) f0-0) (set! (-> *subdivide-settings* far 3) f1-0) ) - + (load-dbg "init-vis~%") (init-vis loaded-level) (load-dbg "package load~%") @@ -905,31 +904,31 @@ (case (-> obj status) (('active 'alive) (format 0 "----------- kill ~A (status ~A)~%" obj (-> obj status)) - + ;; copy data from the level to the game-info storage. This will remember permanent level stuff, like ;; what you collected/completed. (copy-perms-from-level! *game-info* obj) (send-event *camera* 'level-deactivate (-> obj name)) (send-event *target* 'level-deactivate (-> obj name)) - + ;; remove this BSP from the engine. This will stop us from being drawn. (remove-by-param1 *background-draw-engine* (-> obj bsp)) - + ;; track down all the entities and kill them (deactivate-entities (-> obj bsp)) - + ;; kill any remaining particles not associated with a part-tracker (kill-all-particles-in-level obj) - + ;; clean up our level (set! (-> obj inside-sphere?) #f) (set! (-> obj inside-boxes?) #f) (set! (-> obj meta-inside?) #f) (set! (-> obj force-inside?) #f) - + ;; we're still loaded. (set! (-> obj status) 'loaded) - + (set! (-> obj all-visible?) 'loading) ;; clear vis buffers (dotimes (v1-19 128) @@ -955,10 +954,10 @@ (defmethod unload! level ((obj level)) "Unloads the level. This does not free the heap. The level will be made inactive and ready to be loaded some other time." - + (deactivate obj) (when (!= (-> obj status) 'inactive) - + ;; if we linked art group, unlink it. (when (or (= (-> obj status) 'loaded) (= (-> obj status) 'alive) @@ -973,7 +972,7 @@ ) ) ) - + ;; turn some things off (set! (-> obj bsp) #f) (set! (-> obj entity) #f) @@ -981,7 +980,7 @@ (set! (-> obj status) 'inactive) (set! (-> obj art-group string-array length) 0) (set! (-> obj art-group art-group-array length) 0) - + ;; unload texture pages (countdown (s5-1 (-> obj loaded-texture-page-count)) (dotimes (v1-27 32) @@ -993,10 +992,10 @@ ) (set! (-> obj loaded-texture-page-count) 0) (unlink-textures-in-heap! *texture-page-dir* (-> obj heap)) - + ;; unload particle groups that were defined in the level data (unlink-part-group-by-heap (-> obj heap)) - + ;; if there are any in-progress art loads for this level, kill them. (dotimes (s5-2 2) (let ((v1-41 (-> *art-control* buffer s5-2 pending-load-file))) @@ -1019,7 +1018,7 @@ (a0-29 (car s5-3)) ) (while (not (null? s5-3)) - (case (rtype-of a0-29) + (case (rtype-of a0-29) ((symbol) (unload (symbol->string (the-as symbol a0-29))) ) @@ -1033,7 +1032,7 @@ ) (vis-clear obj) - + ;; reset the level heap! (let ((v1-64 (-> obj heap))) (set! (-> v1-64 current) (-> v1-64 base)) @@ -1057,14 +1056,14 @@ ;; note : pc port added option to show every actor regardless (with-pc (if (-> *pc-settings* force-actors?) (return #t))) - + ;; check the vis bits! - (let* (;; lwu v1, 388(a0) + (let* (;; lwu v1, 388(a0) (vis-data (-> obj vis-bits)) ;; sra a0, a1, 3 (byte-idx (sar arg0 3)) ;; daddu v1, a0, v1 - ;; lb v1, 0(v1) + ;; lb v1, 0(v1) (vis-byte (-> (the (pointer int8) vis-data) byte-idx)) ;; andi a0, a1, 7 (bit-idx (logand arg0 #b111)) @@ -1117,7 +1116,7 @@ (defmethod debug-print-splitbox level ((obj level) (arg0 vector) (arg1 string)) "Print the current splitbox, if we're in one." (cond - ((or (not (-> obj bsp)) (zero? (-> obj bsp boxes)) (zero? (-> obj bsp split-box-indices))) + ((or (not (-> obj bsp)) (zero? (-> obj bsp boxes)) (zero? (-> obj bsp split-box-indices))) ;; do nothing! ) (else @@ -1431,6 +1430,11 @@ ) (defun bg ((level-name symbol)) + "Begin game in a given level. + The level name can be the full name (village3), the nickname (vi3), or visname (village3-vis) + If the visname is used (and its a recognized level in level-info), it will use vis mode. + Otherwise, it will use the non-vis DGO name (like VILLAGE3.DGO) which will usually fail. + " (set! *cheat-mode* (if *debug-segment* 'debug #f @@ -1496,6 +1500,62 @@ 0 ) +(defun bg-custom ((level-name symbol)) + "Modified version of bg for the PC Port custom levels." + + ;; lookup info + (format 0 "(bg-custom ~A)%" level-name) + (let ((lev-info (lookup-level-info level-name))) + (when (= lev-info default-level) + (format 0 "Unable to (bg-custom ~A), the level was not found in *level-load-list*~%" level-name) + (return #f) + ) + + ;; kill jak (rip) + (format 0 "doing stop~%") + (stop 'play) + + ;; enable visiblity. the custom level won't use it, but we want it on so other levels can be loaded. + (set! (-> *level* vis?) #t) + + ;; disable border and play mode to prevent loading levels + (set! (-> *level* border?) #f) + (set! (-> *setting-control* default border-mode) #f) + (set! (-> *level* play?) #f) + + (format 0 "doing level load~%") + ;; allocate level. This may start the loading process, but won't finish it. + (let ((lev (level-get-for-use *level* level-name 'active))) + (when (not lev) + (format 0 "Unable to load level, could not level-get-for-use~%") + (return #f) + ) + (format 0 "about to start load loop, game will freeze and hopefully come back soon~%") + + ;; spin in a loop and load it. This will cause the game to freeze during the load, + ;; but this is good enough for now. + (while (or (= (-> lev status) 'loading) + (= (-> lev status) 'loading-bt) + (= (-> lev status) 'login) + ) + (load-continue lev) + ) + + (when (not (-> lev info continues)) + (format 0 "level info has no continues, can't load it.~%") + ) + + (let ((cont (car (-> lev info continues)))) + (start 'play (the continue-point cont)) + ) + + (vis-load lev) + (set! (-> lev all-visible?) #f) + (set! (-> lev force-all-visible?) #t) + ) + ) + ) + (defun play ((use-vis symbol) (init-game symbol)) "The entry point to the actual game! This allocates the level heaps, loads some data, sets some default parameters and sets the startup level." @@ -1817,11 +1877,11 @@ ) ) ) - + ;; load vis info. ;; The load-state's vis-nick is the level we want vis data for. ;; Note that we won't load vis until we are inside the level's boxes. - + ;; this will be the level that is currently being used. (let ((s5-3 #f)) (dotimes (v1-121 (-> *level* length)) @@ -1833,7 +1893,7 @@ ) ) ) - + ;; if we have the wrong vis (when (and (!= s5-3 (-> obj vis-nick)) (-> *level* vis?)) ;; and we want a vis @@ -1859,23 +1919,23 @@ ;; method 16 level-group (debug text stuff) (defmethod level-update level-group ((obj level-group)) - + ;; this does nothing... (camera-pos) (new 'static 'boxed-array :type symbol :length 0 :allocated-length 2) - + ;; compute the settings for this frame (update *setting-control*) - + ;; run the art loading system (update *art-control* #t) (clear-rec *art-control*) - + ;; run level loading! (dotimes (s5-0 2) (load-continue (-> obj level s5-0)) ) - + ;; compute inside for each level (dotimes (s5-1 (-> obj length)) (let ((s4-0 (-> obj level s5-1))) @@ -1889,10 +1949,9 @@ ) ) ) - + ;; update load state machine (the level-border one) (update! *load-state*) - ;; checkpoint assignment (dotimes (s5-2 (-> obj length)) (let ((s4-1 (-> obj level s5-2))) @@ -1927,7 +1986,7 @@ ) ) ) - + ;; determine vis info idx for each level (dotimes (v1-67 (-> obj length)) (let ((a0-26 (-> obj level v1-67))) @@ -1950,7 +2009,7 @@ ) ) ) - + ;; display level vis info (when *display-level-border* (dotimes (s5-3 (-> obj length)) @@ -1978,7 +2037,7 @@ ) ) ) - + ;; if we have vis for level A, but we aren't "in" it, display an error and ;; force us out of the other level. Ideally the boxes and the load boundary system ;; will be consistent and there is no way to set a vis to a level that we aren't in. @@ -2002,7 +2061,6 @@ ) ) ) - ;; if we are outside of the boxes, we consider ourselves "outside of bsp" ;; if we are outside of both levels boxes, then we don't really know what to do ;; for vis, and we can display the classic "outside of bsp" error. @@ -2041,7 +2099,7 @@ ) ) ) - + ;; now, handle setting bit 31 (maybe single vis mode?) (cond ;; special display self mode. @@ -2074,7 +2132,6 @@ ) ) ) - (when (or *display-level-border* *display-texture-download* *display-split-box-info*) (when *display-level-border* (format *stdcon* " want: ~A ~A/~A ~A ~A/~A~%" @@ -2144,13 +2201,13 @@ ) ) ) - + ;; tell PC port about our levels - (__pc-set-levels + (__pc-set-levels (if (= (-> obj level0 status) 'inactive) "none" (symbol->string (-> obj level0 nickname))) (if (= (-> obj level1 status) 'inactive) "none" (symbol->string (-> obj level1 nickname))) ) - + 0 ) diff --git a/goal_src/engine/target/target-death.gc b/goal_src/engine/target/target-death.gc index 110ce8b206..8e1621fae4 100644 --- a/goal_src/engine/target/target-death.gc +++ b/goal_src/engine/target/target-death.gc @@ -180,9 +180,11 @@ ) ) (let ((s5-2 (level-get *level* (-> arg0 level)))) - (when s5-2 + + ;; vis info check added for PC, don't bother waiting for vis if the level doesn't have it. + (when (and s5-2 (-> s5-2 vis-info 0)) (while (and (= (-> s5-2 all-visible?) 'loading) (-> *level* vis?)) - (suspend) + (suspend) ) ) ) diff --git a/goal_src/game.gp b/goal_src/game.gp index 40b7e3b588..26e536dff2 100644 --- a/goal_src/game.gp +++ b/goal_src/game.gp @@ -103,6 +103,17 @@ ) ) +(defun custom-level-cgo (output-name desc-file-name) + "Add a CGO with the given output name (in out/iso) and input name (in custom_levels/)" + (let ((out-name (string-append "out/iso/" output-name))) + (defstep :in (string-append "custom_levels/" desc-file-name) + :tool 'dgo + :out `(,out-name) + ) + (set! *all-cgos* (cons out-name *all-cgos*)) + ) + ) + (defun cgo (output-name desc-file-name) "Add a CGO with the given output name (in out/iso) and input name (in goal_src/dgos)" (let ((out-name (string-append "out/iso/" output-name))) @@ -147,6 +158,12 @@ ) ) +(defmacro build-custom-level (name) + (let* ((path (string-append "custom_levels/" name "/" name ".jsonc"))) + `(defstep :in ,path + :tool 'build-level + :out '(,(string-append "out/obj/" name ".go"))))) + (defun get-iso-data-path () (if *use-iso-data-path* (string-append *iso-data* "/") @@ -1553,6 +1570,17 @@ "ndi-volumes-ag" "title-vis") +;;;;;;;;;;;;;;;;;;;;;;;;; +;; Example Custom Level +;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Set up the build system to build the level geometry +;; this path is relative to the custom_levels/ folder +;; it should point to the .jsonc file that specifies the level. +(build-custom-level "test-zone") +;; the DGO file +(custom-level-cgo "TESTZONE.DGO" "test-zone/testzone.gd") + ;;;;;;;;;;;;;;;;;;;;; ;; Game Engine Code ;;;;;;;;;;;;;;;;;;;;; diff --git a/goalc/CMakeLists.txt b/goalc/CMakeLists.txt index 24b84263bb..40aa875e4b 100644 --- a/goalc/CMakeLists.txt +++ b/goalc/CMakeLists.txt @@ -5,6 +5,16 @@ add_library(compiler emitter/ObjectGenerator.cpp emitter/Register.cpp debugger/disassemble.cpp + build_level/build_level.cpp + build_level/collide_bvh.cpp + build_level/collide_drawable.cpp + build_level/collide_pack.cpp + build_level/color_quantization.cpp + build_level/FileInfo.cpp + build_level/gltf_mesh_extract.cpp + build_level/LevelFile.cpp + build_level/ResLump.cpp + build_level/Tfrag.cpp compiler/Compiler.cpp compiler/Env.cpp compiler/Val.cpp @@ -44,7 +54,7 @@ add_library(compiler regalloc/Allocator_v2.cpp ) -target_link_libraries(compiler common Zydis) +target_link_libraries(compiler common Zydis tiny_gltf) if (WIN32) target_link_libraries(compiler mman) diff --git a/goalc/build_level/FileInfo.cpp b/goalc/build_level/FileInfo.cpp new file mode 100644 index 0000000000..1c5367143e --- /dev/null +++ b/goalc/build_level/FileInfo.cpp @@ -0,0 +1,30 @@ +#include "FileInfo.h" +#include "goalc/data_compiler/DataObjectGenerator.h" +#include "common/versions.h" + +size_t FileInfo::add_to_object_file(DataObjectGenerator& gen) const { + gen.align_to_basic(); + gen.add_type_tag("file-info"); + size_t offset = gen.current_offset_bytes(); + gen.add_type_tag(file_type); + gen.add_ref_to_string_in_pool(file_name); + gen.add_word(major_version); + gen.add_word(minor_version); + gen.add_ref_to_string_in_pool(maya_file_name); + gen.add_ref_to_string_in_pool(tool_debug); + gen.add_ref_to_string_in_pool(tool_debug); + + return offset; +} + +FileInfo make_file_info_for_level(const std::string& file_name) { + FileInfo info; + info.file_type = "bsp-header"; + info.file_name = file_name; + info.major_version = versions::jak1::LEVEL_FILE_VERSION; + info.minor_version = 0; + info.maya_file_name = "Unknown"; + info.tool_debug = "Created by OpenGOAL buildlevel"; + info.mdb_file_name = "Unknown"; + return info; +} \ No newline at end of file diff --git a/goalc/build_level/FileInfo.h b/goalc/build_level/FileInfo.h new file mode 100644 index 0000000000..7932018ca1 --- /dev/null +++ b/goalc/build_level/FileInfo.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "common/common_types.h" + +class DataObjectGenerator; + +struct FileInfo { + // (file-type symbol :offset-assert 4) + std::string file_type; + // (file-name basic :offset-assert 8) + std::string file_name; + // (major-version uint32 :offset-assert 12) + u32 major_version = 0; + // (minor-version uint32 :offset-assert 16) + u32 minor_version = 0; + // (maya-file-name basic :offset-assert 20) + std::string maya_file_name; + // (tool-debug basic :offset-assert 24) + std::string tool_debug; + // (mdb-file-name basic :offset-assert 28) + std::string mdb_file_name; + + size_t add_to_object_file(DataObjectGenerator& gen) const; +}; + +FileInfo make_file_info_for_level(const std::string& file_name); \ No newline at end of file diff --git a/goalc/build_level/LevelFile.cpp b/goalc/build_level/LevelFile.cpp new file mode 100644 index 0000000000..b5902eb988 --- /dev/null +++ b/goalc/build_level/LevelFile.cpp @@ -0,0 +1,105 @@ +#include "LevelFile.h" +#include "goalc/data_compiler/DataObjectGenerator.h" + +size_t DrawableTreeArray::add_to_object_file(DataObjectGenerator& gen) const { + /* + (deftype drawable-tree-array (drawable-group) + ((trees drawable-tree 1 :offset 32 :score 100)) + :flag-assert #x1200000024 + ) + (deftype drawable-group (drawable) + ((length int16 :offset 6) + (data drawable 1 :offset-assert 32) + ) + (:methods + (new (symbol type int) _type_) + ) + :flag-assert #x1200000024 + ) + */ + gen.align_to_basic(); + gen.add_type_tag("drawable-tree-array"); + size_t result = gen.current_offset_bytes(); + int num_trees = 0; + num_trees += tfrags.size(); + num_trees += collides.size(); + gen.add_word(num_trees << 16); + gen.add_word(0); + gen.add_word(0); + + gen.add_word(0); + gen.add_word(0); + gen.add_word(0); + gen.add_word(0); + + // todo add trees... + + if (num_trees == 0) { + gen.add_word(0); // the one at the end. + } else { + int tree_word = (int)gen.current_offset_bytes() / 4; + for (int i = 0; i < num_trees; i++) { + gen.add_word(0); + } + + for (auto& tfrag : tfrags) { + // gen.set_word(tree_word++, tfrag.add_to_object_file(gen)); + gen.link_word_to_byte(tree_word++, tfrag.add_to_object_file(gen)); + } + + for (auto& collide : collides) { + gen.link_word_to_byte(tree_word++, collide.add_to_object_file(gen)); + } + } + + return result; +} + +std::vector LevelFile::save_object_file() const { + DataObjectGenerator gen; + gen.add_type_tag("bsp-header"); + + // add blank space for the bsp-header + while (gen.words() < 100) { + gen.add_word(0); + } + + //(info file-info :offset 4) + auto file_info_slot = info.add_to_object_file(gen); + gen.link_word_to_byte(1, file_info_slot); + + //(bsphere vector :inline :offset-assert 16) + //(all-visible-list (pointer uint16) :offset-assert 32) + //(visible-list-length int32 :offset-assert 36) + //(drawable-trees drawable-tree-array :offset-assert 40) + gen.link_word_to_byte(40 / 4, drawable_trees.add_to_object_file(gen)); + //(pat pointer :offset-assert 44) + //(pat-length int32 :offset-assert 48) + //(texture-remap-table (pointer uint64) :offset-assert 52) + //(texture-remap-table-len int32 :offset-assert 56) + //(texture-ids (pointer texture-id) :offset-assert 60) + //(texture-page-count int32 :offset-assert 64) + //(unk-zero-0 basic :offset-assert 68) + //(name symbol :offset-assert 72) + gen.link_word_to_symbol(name, 72 / 4); + //(nickname symbol :offset-assert 76) + gen.link_word_to_symbol(nickname, 76 / 4); + //(vis-info level-vis-info 8 :offset-assert 80) + //(actors drawable-inline-array-actor :offset-assert 112) + //(cameras (array entity-camera) :offset-assert 116) + //(nodes (inline-array bsp-node) :offset-assert 120) + //(level level :offset-assert 124) + //(current-leaf-idx uint16 :offset-assert 128) + //(unk-data-2 uint16 9 :offset-assert 130) + //(boxes box8s-array :offset-assert 148) + //(current-bsp-back-flags uint32 :offset-assert 152) + //(ambients drawable-inline-array-ambient :offset-assert 156) + //(unk-data-4 float :offset-assert 160) + //(unk-data-5 float :offset-assert 164) + //(adgifs adgif-shader-array :offset-assert 168) + //(actor-birth-order (pointer uint32) :offset-assert 172) + //(split-box-indices (pointer uint16) :offset-assert 176) + //(unk-data-8 uint32 55 :offset-assert 180) + + return gen.generate_v2(); +} \ No newline at end of file diff --git a/goalc/build_level/LevelFile.h b/goalc/build_level/LevelFile.h new file mode 100644 index 0000000000..29f53dd4c0 --- /dev/null +++ b/goalc/build_level/LevelFile.h @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include + +#include "common/common_types.h" +#include "goalc/build_level/FileInfo.h" +#include "goalc/build_level/Tfrag.h" +#include "goalc/build_level/collide_pack.h" +#include "goalc/build_level/collide_common.h" +#include "goalc/build_level/collide_bvh.h" +#include "goalc/build_level/collide_drawable.h" + +struct VisibilityString { + std::vector bytes; +}; + +struct DrawableTreeInstanceTie {}; + +struct DrawableTreeActor {}; + +struct DrawableTreeAmbient {}; + +struct DrawableTreeInstanceShrub {}; + +struct DrawableTreeArray { + std::vector tfrags; + std::vector ties; + std::vector actors; // unused? + std::vector collides; + std::vector ambients; + std::vector shrubs; + size_t add_to_object_file(DataObjectGenerator& gen) const; +}; + +struct TextureRemap {}; + +struct TextureId {}; + +struct VisInfo {}; + +struct DrawableActor {}; + +struct DrawableInlineArrayActor {}; + +struct EntityCamera {}; + +struct BspNode {}; + +struct Box8s {}; + +struct DrawableAmbient {}; + +struct DrawableInlineArrayAmbient {}; + +struct AdgifShaderArray {}; + +// This is a place to collect all the data that should go into the bsp-header file. +struct LevelFile { + // (info file-info :offset 4) + FileInfo info; + + // (all-visible-list (pointer uint16) :offset-assert 32) + // (visible-list-length int32 :offset-assert 36) + VisibilityString all_visibile_list; + + // (drawable-trees drawable-tree-array :offset-assert 40) + DrawableTreeArray drawable_trees; + + // (pat pointer :offset-assert 44) + // (pat-length int32 :offset-assert 48) + std::vector pat; + + // (texture-remap-table (pointer uint64) :offset-assert 52) + // (texture-remap-table-len int32 :offset-assert 56) + std::vector texture_remap_table; + + // (texture-ids (pointer texture-id) :offset-assert 60) + // (texture-page-count int32 :offset-assert 64) + std::vector texture_ids; + + // (unk-zero-0 basic :offset-assert 68) + // "misc", seems like it can be zero and is unused. + + // (name symbol :offset-assert 72) + std::string name; // full name + + // (nickname symbol :offset-assert 76) + std::string nickname; // 3 char name + + // (vis-info level-vis-info 8 :offset-assert 80) ;; note: 0 when + std::array vis_infos; + + // (actors drawable-inline-array-actor :offset-assert 112) + DrawableInlineArrayActor actors; + + // (cameras (array entity-camera) :offset-assert 116) + std::vector cameras; + + // (nodes (inline-array bsp-node) :offset-assert 120) + std::vector nodes; + + // (level level :offset-assert 124) + // zero + + // (current-leaf-idx uint16 :offset-assert 128) + // zero + + // (unk-data-2 uint16 9 :offset-assert 130) + // looks like padding plus 4 floats? unused + + // (boxes box8s-array :offset-assert 148) + std::vector boxes; + + // (current-bsp-back-flags uint32 :offset-assert 152) + // zero + + // (ambients drawable-inline-array-ambient :offset-assert 156) + DrawableInlineArrayAmbient ambients; + + // (unk-data-4 float :offset-assert 160) + float close_subdiv = 0; + + // (unk-data-5 float :offset-assert 164) + float far_subdiv = 0; + + // (adgifs adgif-shader-array :offset-assert 168) + AdgifShaderArray adgifs; + + // (actor-birth-order (pointer uint32) :offset-assert 172) + std::vector actor_birth_order; + + // (split-box-indices (pointer uint16) :offset-assert 176) + std::vector split_box_indices; + + // (unk-data-8 uint32 55 :offset-assert 180) + + std::vector save_object_file() const; +}; \ No newline at end of file diff --git a/goalc/build_level/ResLump.cpp b/goalc/build_level/ResLump.cpp new file mode 100644 index 0000000000..f70a09ee2b --- /dev/null +++ b/goalc/build_level/ResLump.cpp @@ -0,0 +1,4 @@ +#include "ResLump.h" + +#include "third-party/fmt/core.h" +#include "goalc/data_compiler/DataObjectGenerator.h" diff --git a/goalc/build_level/ResLump.h b/goalc/build_level/ResLump.h new file mode 100644 index 0000000000..19b6ee6453 --- /dev/null +++ b/goalc/build_level/ResLump.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include +#include + +#include "common/common_types.h" diff --git a/goalc/build_level/TexturePool.h b/goalc/build_level/TexturePool.h new file mode 100644 index 0000000000..2cef160246 --- /dev/null +++ b/goalc/build_level/TexturePool.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +#include "common/custom_data/Tfrag3Data.h" + +struct TexturePool { + std::unordered_map textures_by_name; + std::vector textures_by_idx; +}; \ No newline at end of file diff --git a/goalc/build_level/Tfrag.cpp b/goalc/build_level/Tfrag.cpp new file mode 100644 index 0000000000..449cdbb22f --- /dev/null +++ b/goalc/build_level/Tfrag.cpp @@ -0,0 +1,99 @@ +#include + +#include "Tfrag.h" +#include "common/custom_data/pack_helpers.h" +#include "goalc/data_compiler/DataObjectGenerator.h" +#include "goalc/build_level/gltf_mesh_extract.h" + +void tfrag_from_gltf(const gltf_mesh_extract::TfragOutput& mesh_extract_out, + DrawableTreeTfrag& out, + tfrag3::TfragTree& out_pc) { + out_pc.kind = tfrag3::TFragmentTreeKind::NORMAL; // todo more types? + out_pc.draws = std::move(mesh_extract_out.strip_draws); + fmt::print("have {} draws\n", out_pc.draws.size()); + pack_tfrag_vertices(&out_pc.packed_vertices, mesh_extract_out.vertices); + fmt::print("have {} vertices\n", out_pc.packed_vertices.vertices.size()); + + for (auto& col : mesh_extract_out.color_palette) { + tfrag3::TimeOfDayColor todc; + for (auto& rgba : todc.rgba) { + rgba = col; + } + out_pc.colors.push_back(todc); + } + out_pc.use_strips = false; +} + +/* + +(deftype drawable-group (drawable) + ((length int16 :offset 6) + (data drawable 1 :offset-assert 32) + ) + (:methods + (new (symbol type int) _type_) + ) + :flag-assert #x1200000024 + ) + + (deftype drawable-tree (drawable-group) + () + :flag-assert #x1200000024 + ) + +(deftype drawable-inline-array (drawable) + ((length int16 :offset 6) ;; this is kinda weird. + ) + :method-count-assert 18 + :size-assert #x20 + :flag-assert #x1200000020 + ) + +(deftype drawable-inline-array-tfrag (drawable-inline-array) + ((data tfragment 1 :inline :offset-assert 32) + (pad uint32)) + :method-count-assert 18 + :size-assert #x64 + :flag-assert #x1200000064 + ) + +(deftype drawable-tree-tfrag (drawable-tree) + ((time-of-day-pal time-of-day-palette :offset 12) + (arrays drawable-inline-array 1 :offset 32 :score 100) ;; either drawable-inline-array-node +or drawable-inline-array-tfrag + ) + :method-count-assert #x12 + :size-assert #x24 + :flag-assert #x1200000024 + ) + + + */ + +size_t add_empty_dia(const std::string& name, DataObjectGenerator& gen, int total_size) { + gen.align_to_basic(); + gen.add_type_tag(name); + size_t result = gen.current_offset_bytes(); + total_size -= 4; + while (total_size > 0) { + gen.add_word(0); + total_size -= 4; + } + + return result; +} + +size_t DrawableTreeTfrag::add_to_object_file(DataObjectGenerator& gen) const { + gen.align_to_basic(); + gen.add_type_tag("drawable-tree-tfrag"); + size_t result = gen.current_offset_bytes(); + gen.add_word(1 << 16); + for (int i = 0; i < 6; i++) { + gen.add_word(0); + } + size_t slot = gen.add_word(0); + ASSERT(slot * 4 - result == 28); + gen.link_word_to_byte(slot, add_empty_dia("drawable-inline-array-tfrag", gen, 0x64)); + + return result; +} \ No newline at end of file diff --git a/goalc/build_level/Tfrag.h b/goalc/build_level/Tfrag.h new file mode 100644 index 0000000000..f9c137a473 --- /dev/null +++ b/goalc/build_level/Tfrag.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "common/custom_data/Tfrag3Data.h" +#include "goalc/build_level/TexturePool.h" +#include "goalc/build_level/gltf_mesh_extract.h" + +class DataObjectGenerator; + +struct DrawableTreeTfrag { + size_t add_to_object_file(DataObjectGenerator& gen) const; +}; + +void tfrag_from_gltf(const gltf_mesh_extract::TfragOutput& mesh_extract_out, + DrawableTreeTfrag& out, + tfrag3::TfragTree& out_pc); \ No newline at end of file diff --git a/goalc/build_level/build_level.cpp b/goalc/build_level/build_level.cpp new file mode 100644 index 0000000000..9edb3586ef --- /dev/null +++ b/goalc/build_level/build_level.cpp @@ -0,0 +1,102 @@ +#include "third-party/fmt/core.h" +#include "common/util/json_util.h" +#include "common/util/FileUtil.h" +#include "common/log/log.h" +#include "goalc/build_level/LevelFile.h" +#include "goalc/build_level/FileInfo.h" +#include "goalc/build_level/Tfrag.h" +#include "goalc/build_level/gltf_mesh_extract.h" +#include "goalc/build_level/collide_bvh.h" +#include "goalc/build_level/collide_pack.h" + +#include "common/custom_data/Tfrag3Data.h" +#include "common/util/compress.h" + +void save_pc_data(const std::string& nickname, tfrag3::Level& data) { + Serializer ser; + data.serialize(ser); + auto compressed = + compression::compress_zstd(ser.get_save_result().first, ser.get_save_result().second); + fmt::print("stats for {}\n", data.level_name); + print_memory_usage(data, ser.get_save_result().second); + fmt::print("compressed: {} -> {} ({:.2f}%)\n", ser.get_save_result().second, compressed.size(), + 100.f * compressed.size() / ser.get_save_result().second); + file_util::write_binary_file(file_util::get_file_path({fmt::format("assets/{}.fr3", nickname)}), + compressed.data(), compressed.size()); +} + +std::vector get_build_level_deps(const std::string& input_file) { + auto level_json = parse_commented_json( + file_util::read_text_file(file_util::get_file_path({input_file})), input_file); + return {level_json.at("gltf_file").get()}; +} + +bool run_build_level(const std::string& input_file, const std::string& output_file) { + auto level_json = parse_commented_json( + file_util::read_text_file(file_util::get_file_path({input_file})), input_file); + LevelFile file; // GOAL level file + tfrag3::Level pc_level; // PC level file + TexturePool tex_pool; // pc level texture pool + + // process input mesh from blender + gltf_mesh_extract::Input mesh_extract_in; + mesh_extract_in.filename = + file_util::get_file_path({level_json.at("gltf_file").get()}); + mesh_extract_in.tex_pool = &tex_pool; + gltf_mesh_extract::Output mesh_extract_out; + gltf_mesh_extract::extract(mesh_extract_in, mesh_extract_out); + + // add stuff to the GOAL level structure + file.info = make_file_info_for_level(std::filesystem::path(input_file).filename().string()); + // all vis + // drawable trees + // pat + // texture remap + // texture ids + // unk zero + // name + file.name = level_json.at("long_name").get(); + // nick + file.nickname = level_json.at("nickname").get(); + // vis infos + // actors + // cameras + // nodes + // boxes + // ambients + // subdivs + // adgifs + // actor birht + // split box + + // add stuff to the PC level structure + pc_level.level_name = file.name; + + // TFRAG + auto& tfrag_drawable_tree = file.drawable_trees.tfrags.emplace_back(); + tfrag_from_gltf(mesh_extract_out.tfrag, tfrag_drawable_tree, + pc_level.tfrag_trees[0].emplace_back()); + pc_level.textures = std::move(tex_pool.textures_by_idx); + + // COLLIDE + if (mesh_extract_out.collide.faces.empty()) { + lg::error("No collision geometry was found"); + } else { + auto& collide_drawable_tree = file.drawable_trees.collides.emplace_back(); + collide_drawable_tree.bvh = collide::construct_collide_bvh(mesh_extract_out.collide.faces); + collide_drawable_tree.packed_frags = pack_collide_frags(collide_drawable_tree.bvh.frags.frags); + } + + // Save the GOAL level + auto result = file.save_object_file(); + fmt::print("Level bsp file size {} bytes\n", result.size()); + auto save_path = file_util::get_file_path({output_file}); + file_util::create_dir_if_needed_for_file(save_path); + fmt::print("Saving to {}\n", save_path); + file_util::write_binary_file(save_path, result.data(), result.size()); + + // Save the PC level + save_pc_data(file.nickname, pc_level); + + return true; +} diff --git a/goalc/build_level/build_level.h b/goalc/build_level/build_level.h new file mode 100644 index 0000000000..afe685daac --- /dev/null +++ b/goalc/build_level/build_level.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +bool run_build_level(const std::string& input_file, const std::string& output_file); +std::vector get_build_level_deps(const std::string& input_file); \ No newline at end of file diff --git a/goalc/build_level/collide_bvh.cpp b/goalc/build_level/collide_bvh.cpp new file mode 100644 index 0000000000..8d410b2a3c --- /dev/null +++ b/goalc/build_level/collide_bvh.cpp @@ -0,0 +1,284 @@ +#include + +#include "collide_bvh.h" +#include "common/util/Assert.h" +#include "common/log/log.h" +#include "common/util/Timer.h" + +// Collision BVH algorithm +// We start with all the points in a single node, then recursively split nodes in 8 until no nodes +// have too many faces. +// The splitting is done by doing median cuts along the x, y, or z axis. + +// The bspheres are built at the end. + +namespace collide { + +namespace { + +constexpr int MAX_FACES_IN_FRAG = 100; + +/*! + * The Collide node. + * Has either children collide node or children faces, but not both + * The size of child_nodes is either 0 or 8 at all times. + */ +struct CNode { + std::vector child_nodes; + std::vector faces; + math::Vector4f bsphere; +}; + +struct BBox { + math::Vector3f mins, maxs; + std::string sz_to_string() const { + return fmt::format("({})", ((maxs - mins) / 4096.f).to_string_aligned()); + } +}; + +/*! + * Make the bounding box hold this node and all its children. + */ +void add_to_bbox_recursive(const CNode& node, BBox& bbox) { + if (node.faces.empty()) { + ASSERT(node.child_nodes.size() == 8); + for (auto& child : node.child_nodes) { + add_to_bbox_recursive(child, bbox); + } + } else { + for (auto& face : node.faces) { + for (auto& vert : face.v) { + bbox.mins.min_in_place(vert); + bbox.maxs.max_in_place(vert); + } + } + } +} + +BBox bbox_of_node(const CNode& node) { + BBox bbox; + bbox.mins.fill(std::numeric_limits::max()); + bbox.maxs.fill(-std::numeric_limits::max()); + add_to_bbox_recursive(node, bbox); + return bbox; +} + +/*! + * Make the bsphere hold this node and all its children. + */ +void update_bsphere_recursive(const CNode& node, const math::Vector3f& origin, float& r_squared) { + if (node.faces.empty()) { + ASSERT(node.child_nodes.size() == 8); + for (auto& child : node.child_nodes) { + update_bsphere_recursive(child, origin, r_squared); + } + } else { + for (auto& face : node.faces) { + for (auto& vert : face.v) { + r_squared = std::max(r_squared, (vert - origin).squared_length()); + } + } + } +} + +/*! + * Compute the bsphere of a single node. + */ +void compute_my_bsphere(CNode& node) { + // first compute bbox. + BBox bbox = bbox_of_node(node); + float r = 0; + math::Vector3f origin = (bbox.maxs + bbox.mins) * 0.5; + update_bsphere_recursive(node, origin, r); + node.bsphere.x() = origin.x(); + node.bsphere.y() = origin.y(); + node.bsphere.z() = origin.z(); + node.bsphere.w() = std::sqrt(r); +} + +/*! + * Split faces in two along a coordinate plane. + * Will clear the input faces + */ +void split_along_dim(std::vector& faces, + int dim, + std::vector* out0, + std::vector* out1) { + std::sort(faces.begin(), faces.end(), [=](const CollideFace& a, const CollideFace& b) { + return a.bsphere[dim] < b.bsphere[dim]; + }); + size_t split_idx = faces.size() / 2; + out0->insert(out0->end(), faces.begin(), faces.begin() + split_idx); + out1->insert(out1->end(), faces.begin() + split_idx, faces.end()); +} + +/*! + * Split a node into two nodes. The outputs should be uninitialized nodes + */ +void split_node_once(CNode& node, CNode* out0, CNode* out1) { + CNode temps[6]; + // split_along_dim(node.faces, pick_dim_for_split(node.faces), &out0->faces, &out1->faces); + split_along_dim(node.faces, 0, &temps[0].faces, &temps[1].faces); + split_along_dim(node.faces, 1, &temps[2].faces, &temps[3].faces); + split_along_dim(node.faces, 2, &temps[4].faces, &temps[5].faces); + node.faces.clear(); + for (auto& t : temps) { + compute_my_bsphere(t); + } + + float max_bspheres[3] = {0, 0, 0}; + for (int i = 0; i < 3; i++) { + max_bspheres[i] = std::max(temps[i * 2].bsphere.w(), temps[i * 2 + 1].bsphere.w()); + } + + int best_dim = 0; + float best_w = max_bspheres[0]; + for (int i = 0; i < 3; i++) { + if (max_bspheres[i] < best_w) { + best_dim = i; + best_w = max_bspheres[i]; + } + } + + *out0 = temps[best_dim * 2]; + *out1 = temps[best_dim * 2 + 1]; +} + +/*! + * Split a node into 8 children and store these in the given node. + */ +void split_node_to_8_children(CNode& node) { + ASSERT(node.child_nodes.empty()); + node.child_nodes.resize(8); + // level 0 + CNode level0[2]; + split_node_once(node, &level0[0], &level0[1]); + // level 1 + CNode level1[4]; + split_node_once(level0[0], &level1[0], &level1[1]); + split_node_once(level0[1], &level1[2], &level1[3]); + // level 2 + split_node_once(level1[0], &node.child_nodes[0], &node.child_nodes[1]); + split_node_once(level1[1], &node.child_nodes[2], &node.child_nodes[3]); + split_node_once(level1[2], &node.child_nodes[4], &node.child_nodes[5]); + split_node_once(level1[3], &node.child_nodes[6], &node.child_nodes[7]); +} + +/*! + * Split all leaf nodes. Returns the number of faces in the leaf with the most faces after + * splitting. + * This slightly unusual recursion pattern is to make sure we split everything to same depth, + * which we believe might be a requirement of the collision system. + */ +size_t split_all_leaves(CNode& node) { + size_t worst_leaf_face_count = 0; + if (node.child_nodes.empty()) { + // we're a leaf! + // split us: + split_node_to_8_children(node); + for (auto& child : node.child_nodes) { + worst_leaf_face_count = std::max(worst_leaf_face_count, child.faces.size()); + } + return worst_leaf_face_count; + } else { + // not a leaf, recurse + for (auto& child : node.child_nodes) { + worst_leaf_face_count = std::max(worst_leaf_face_count, split_all_leaves(child)); + } + return worst_leaf_face_count; + } +} + +/*! + * Main BVH construction function. Splits leaves until it is no longer needed. + */ +void split_as_needed(CNode& root) { + int initial_tri_count = root.faces.size(); + int num_leaves = 1; + bool need_to_split = true; + while (need_to_split) { + int faces_in_worst = split_all_leaves(root); + num_leaves *= 8; + lg::info("after splitting, the worst leaf has {} tris", faces_in_worst); + if (faces_in_worst < MAX_FACES_IN_FRAG) { + need_to_split = false; + } + } + lg::info("average triangles per leaf: {}", initial_tri_count / num_leaves); + lg::info("leaf count: {}", num_leaves); +} + +/*! + * Recursively compute bspheres of all children + * (note that we don't do bspheres of bspheres... I think this is better?) + */ +void bsphere_recursive(CNode& node) { + compute_my_bsphere(node); + for (auto& child : node.child_nodes) { + bsphere_recursive(child); + } +} + +void drawable_layout_helper(CNode& node, int depth, CollideTree& tree_out, size_t my_idx_check) { + if (node.child_nodes.empty()) { + // we're a leaf! add us to the frags + auto& frag = tree_out.frags.frags.emplace_back(); + frag.bsphere = node.bsphere; + frag.faces = node.faces; + } else { + // not a leaf + if ((int)tree_out.node_arrays.size() <= depth) { + tree_out.node_arrays.resize(depth + 1); + } + ASSERT(my_idx_check == tree_out.node_arrays.at(depth).nodes.size()); + auto& draw_node = tree_out.node_arrays[depth].nodes.emplace_back(); + draw_node.bsphere = node.bsphere; + for (int i = 0; i < 8; i++) { + draw_node.children[i] = my_idx_check * 8 + i; + drawable_layout_helper(node.child_nodes.at(i), depth + 1, tree_out, draw_node.children[i]); + } + } +} + +CollideTree build_collide_tree(CNode& root) { + CollideTree tree; + drawable_layout_helper(root, 0, tree, 0); + return tree; +} + +void debug_stats(const CollideTree& tree) { + lg::info("Tree build: {} draw node layers", tree.node_arrays.size()); + float sum_w = 0, max_w = 0; + for (auto& frag : tree.frags.frags) { + sum_w += frag.bsphere.w(); + max_w = std::max(frag.bsphere.w(), max_w); + } + lg::info("Max bsphere radius: {:.2f}m, average {:.2f} (aiming for around 20-30m avg)", + max_w / 4096, sum_w / (4096 * tree.frags.frags.size())); +} + +} // namespace + +CollideTree construct_collide_bvh(const std::vector& tris) { + // part 1: build the tree + Timer bvh_timer; + lg::info("Building collide bvh from {} triangles", tris.size()); + CNode root; + root.faces = tris; + split_as_needed(root); + lg::info("BVH tree constructed in {:.2f} ms", bvh_timer.getMs()); + + // part 2: compute bspheres + bvh_timer.start(); + bsphere_recursive(root); + lg::info("Found bspheres in {:.2f} ms", bvh_timer.getMs()); + + // part 3: layout tree + bvh_timer.start(); + auto tree = build_collide_tree(root); + debug_stats(tree); + lg::info("Tree layout done in {:.2f} ms", bvh_timer.getMs()); + return tree; +} + +} // namespace collide \ No newline at end of file diff --git a/goalc/build_level/collide_bvh.h b/goalc/build_level/collide_bvh.h new file mode 100644 index 0000000000..11b4f4bdaa --- /dev/null +++ b/goalc/build_level/collide_bvh.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include "goalc/build_level/collide_common.h" + +// requirements: +// max depth of 3 (maybe?) +// max face per frag = 90 +// max vert per frag = 110 +// branching factor of 8 everywhere. +namespace collide { + +struct DrawNode { + s32 children[8] = {-1, -1, -1, -1, -1, -1, -1, -1}; + math::Vector4f bsphere; +}; + +struct CollideFrag { + math::Vector4f bsphere; + std::vector faces; +}; + +struct DrawableInlineArrayNode { + std::vector nodes; +}; + +struct DrawableInlineArrayCollideFrag { + std::vector frags; +}; + +struct CollideTree { + std::vector node_arrays; + DrawableInlineArrayCollideFrag frags; +}; + +CollideTree construct_collide_bvh(const std::vector& tris); +} // namespace collide diff --git a/goalc/build_level/collide_common.h b/goalc/build_level/collide_common.h new file mode 100644 index 0000000000..3ef5146167 --- /dev/null +++ b/goalc/build_level/collide_common.h @@ -0,0 +1,110 @@ +#pragma once +#include "common/common_types.h" +#include "common/math/Vector.h" + +struct PatSurface { + enum class Mode { GROUND = 0, WALL = 1, OBSTACLE = 2 }; + enum class Material { + STONE = 0, + ICE = 1, + QUICKSAND = 2, + WATERBOTTOM = 3, + TAR = 4, + SAND = 5, + WOOD = 6, + GRASS = 7, + PCMETAL = 8, + SNOW = 9, + DEEPSNOW = 10, + HOTCOALS = 11, + LAVA = 12, + CRWOOD = 13, + GRAVEL = 14, + DIRT = 15, + METAL = 16, + STRAW = 17, + TUBE = 18, + SWAMP = 19, + STOPPROJ = 20, + ROTATE = 21, + NEUTRAL = 22, + }; + + enum class Event { + NONE = 0, + DEADLY = 1, + ENDLESSFALL = 2, + BURN = 3, + DEADLYUP = 4, + BURNUP = 5, + MELT = 6, + }; + + void set_noentity(bool x) { + if (x) { + val |= (1 << 0); + } else { + val &= ~(1 << 0); + } + } + bool get_noentity() const { return val & (1 << 0); } + + void set_nocamera(bool x) { + if (x) { + val |= (1 << 1); + } else { + val &= ~(1 << 1); + } + } + bool get_nocamera() const { return val & (1 << 1); } + + void set_noedge(bool x) { + if (x) { + val |= (1 << 2); + } else { + val &= ~(1 << 2); + } + } + bool get_noedge() const { return val & (1 << 2); } + + void set_mode(Mode mode) { + val &= ~(0b111 << 3); + val |= ((u32)mode << 3); + } + Mode get_mode() const { return (Mode)(0b111 & (val >> 3)); } + + void set_material(Material mat) { + val &= ~(0b111111 << 6); + val |= ((u32)mat << 6); + } + Material get_material() const { return (Material)(0b111111 & (val >> 6)); } + + void set_nolineofsight(bool x) { + if (x) { + val |= (1 << 12); + } else { + val &= ~(1 << 12); + } + } + bool get_nolineofsight() const { return val & (1 << 12); } + + void set_event(Event ev) { + val &= ~(0b111111 << 14); + val |= ((u32)ev << 6); + } + Event get_event() const { return (Event)(0b111111 & (val >> 14)); } + + bool operator==(const PatSurface& other) const { return val == other.val; } + // bits 13, [15-31] are unused, or have unknown purpose. + u32 val = 0; +}; + +struct CollideVertex { + float x, y, z; +}; + +struct CollideFace { + math::Vector4f bsphere; + math::Vector3f v[3]; + PatSurface pat; +}; diff --git a/goalc/build_level/collide_drawable.cpp b/goalc/build_level/collide_drawable.cpp new file mode 100644 index 0000000000..4198eadfbd --- /dev/null +++ b/goalc/build_level/collide_drawable.cpp @@ -0,0 +1,268 @@ +#include "collide_drawable.h" +#include "goalc/data_compiler/DataObjectGenerator.h" +#include "common/util/Assert.h" + +/* +(deftype drawable (basic) + ((id int16 :offset-assert 4) + (bsphere vector :inline :offset-assert 16) + ) + +(deftype drawable-group (drawable) + ((length int16 :offset 6) + (data drawable 1 :offset-assert 32) + ) + +(deftype drawable-tree (drawable-group) + () + +(deftype drawable-inline-array (drawable) + ((length int16 :offset 6) ;; this is kinda weird. + ) + +(deftype drawable-tree-collide-fragment (drawable-tree) + ((data-override drawable-inline-array 1 :offset 32)) ;; should be 1 there + +(deftype drawable-inline-array-collide-fragment (drawable-inline-array) + ((data collide-fragment 1 :inline :offset-assert 32) + + (deftype collide-fragment (drawable) + ((mesh collide-frag-mesh :offset 8) + ) + :method-count-assert 18 + :size-assert #x20 + +(deftype collide-frag-mesh (basic) + ((packed-data uint32 :offset-assert 4) + (pat-array uint32 :offset-assert 8) + (strip-data-len uint16 :offset-assert 12) + (poly-count uint16 :offset-assert 14) + (base-trans vector :inline :offset-assert 16) + ;; these go in the w of the vector above. + (vertex-count uint8 :offset 28) + (vertex-data-qwc uint8 :offset 29) + (total-qwc uint8 :offset 30) + (unused uint8 :offset 31) + ) + :method-count-assert 9 + :size-assert #x20 + +(deftype draw-node (drawable) + ((child-count uint8 :offset 6) + (flags uint8 :offset 7) + (child drawable :offset 8) + (distance float :offset 12) + ) + :size-assert #x20 + + (deftype drawable-inline-array-node (drawable-inline-array) + ((data draw-node 1 :inline) + (pad uint32) + ) + :method-count-assert 18 + :size-assert #x44 + */ + +size_t generate_pat_array(DataObjectGenerator& gen, const std::vector& pats) { + gen.align_to_basic(); + size_t result = gen.current_offset_bytes(); + for (auto& pat : pats) { + gen.add_word(pat.val); + } + return result; +} + +size_t generate_packed_collide_data(DataObjectGenerator& gen, const std::vector& data) { + gen.align_to_basic(); + size_t result = gen.current_offset_bytes(); + ASSERT((data.size() % 4) == 0); + for (size_t i = 0; i < data.size(); i += 4) { + u32 word; + memcpy(&word, data.data() + i, 4); + gen.add_word(word); + } + return result; +} + +size_t generate_collide_frag_mesh(DataObjectGenerator& gen, + const CollideFragMeshData& mesh, + size_t packed_data_loc, + size_t pat_array_loc) { + gen.align_to_basic(); + gen.add_type_tag("collide-frag-mesh"); // 0 + size_t result = gen.current_offset_bytes(); + gen.link_word_to_byte(gen.add_word(0), packed_data_loc); // 4 + gen.link_word_to_byte(gen.add_word(0), pat_array_loc); // 8 + gen.add_word(mesh.strip_data_len | (mesh.poly_count << 16)); // 12 + gen.add_word(mesh.base_trans_xyz_s32.x()); // 16 + gen.add_word(mesh.base_trans_xyz_s32.y()); // 20 + gen.add_word(mesh.base_trans_xyz_s32.z()); // 24 + u32 packed = 0; + packed |= mesh.vertex_count; + packed |= ((u32)mesh.vertex_data_qwc) << 8; + packed |= ((u32)mesh.total_qwc) << 16; + gen.add_word(packed); // 28 + return result; +} + +size_t generate_collide_fragment(DataObjectGenerator& gen, + const CollideFragMeshData& mesh, + size_t frag_mesh_loc) { + /* + .type collide-fragment + .word 0x10000 + .word L705 + .word 0x0 + .word 0x46bf480a + .word 0x43dc730b + .word 0xb71ed4fe + .word 0x46c42e44 + */ + gen.align_to_basic(); + gen.add_type_tag("collide-fragment"); + size_t result = gen.current_offset_bytes(); + gen.add_word(0x10000); // ??? + gen.link_word_to_byte(gen.add_word(0), frag_mesh_loc); + gen.add_word(0); + for (int i = 0; i < 4; i++) { + gen.add_word_float(mesh.bsphere[i]); + } + + return result; +} + +size_t generate_collide_fragment_array(DataObjectGenerator& gen, + const std::vector& meshes, + const std::vector& frag_mesh_locs, + std::vector& parent_ref_out) { + gen.align_to_basic(); + gen.add_type_tag("drawable-inline-array-collide-fragment"); // 0 + size_t result = gen.current_offset_bytes(); + ASSERT(meshes.size() < UINT16_MAX); + gen.add_word(meshes.size() << 16); // 4, 6 + gen.add_word(0); // 8 + gen.add_word(0); // 12 + gen.add_word(0); // 16 + gen.add_word(0); // 20 + gen.add_word(0); // 24 + gen.add_word(0); // 28 + + fmt::print("have: {}\n", meshes.size()); + + ASSERT(meshes.size() == frag_mesh_locs.size()); + for (size_t i = 0; i < meshes.size(); i++) { + auto& mesh = meshes[i]; + // should be 8 words here: + gen.add_type_tag("collide-fragment"); // 1 + size_t me = gen.current_offset_bytes(); + gen.add_word(0x10000); // ??? + gen.link_word_to_byte(gen.add_word(0), frag_mesh_locs[i]); + gen.add_word(0); + for (int j = 0; j < 4; j++) { + gen.add_word_float(mesh.bsphere[j]); + } + if ((i % 8) == 0) { + parent_ref_out.push_back(me); + } + } + + return result; +} + +size_t generate_collide_draw_node_array(DataObjectGenerator& gen, + const std::vector& nodes, + u32 flag, + const std::vector& children, + std::vector& parent_ref_out) { + gen.align_to_basic(); + gen.add_type_tag("drawable-inline-array-node"); // 0 + size_t result = gen.current_offset_bytes(); + gen.add_word(nodes.size() << 16); // 4, 6 + gen.add_word(0); // 8 + gen.add_word(0); // 12 + gen.add_word(0); // 16 + gen.add_word(0); // 20 + gen.add_word(0); // 24 + gen.add_word(0); // 28 + + ASSERT(nodes.size() == children.size()); + for (size_t i = 0; i < nodes.size(); i++) { + auto& node = nodes[i]; + // should be 8 words here: + gen.add_type_tag("draw-node"); // 1 + size_t me = gen.current_offset_bytes(); + u32 packed_flags = 0; + packed_flags |= (8 << 16); // TODO hard-coded size here + packed_flags |= (flag << 24); + gen.add_word(packed_flags); // 2 + gen.link_word_to_byte(gen.add_word(0), children[i]); // 3 + gen.add_word(0); // 4 + if ((i % 8) == 0) { + parent_ref_out.push_back(me); + } + gen.add_word_float(node.bsphere.x()); // 5 + gen.add_word_float(node.bsphere.y()); // 6 + gen.add_word_float(node.bsphere.z()); // 7 + gen.add_word_float(node.bsphere.w()); // 8 + } + + return result; +} + +size_t DrawableTreeCollideFragment::add_to_object_file(DataObjectGenerator& gen) const { + for (auto& lev : bvh.node_arrays) { + fmt::print("lev: {}\n", lev.nodes.size()); + } + // generate pat array + size_t pat_array_loc = generate_pat_array(gen, packed_frags.pats); + + // generated packed data + std::vector packed_data_locs; + for (auto& mesh : packed_frags.packed_frag_data) { + packed_data_locs.push_back(generate_packed_collide_data(gen, mesh.packed_data)); + } + + // generate collide frag meshes + std::vector collide_frag_meshes; + for (size_t i = 0; i < packed_data_locs.size(); i++) { + collide_frag_meshes.push_back(generate_collide_frag_mesh(gen, packed_frags.packed_frag_data[i], + packed_data_locs[i], pat_array_loc)); + } + + std::vector array_locs; + array_locs.resize(bvh.node_arrays.size() + 1); // plus one for the frags. + int array_slot = bvh.node_arrays.size(); + + std::vector children_refs; + array_locs[array_slot--] = generate_collide_fragment_array(gen, packed_frags.packed_frag_data, + collide_frag_meshes, children_refs); + u32 flag = 0; + while (array_slot >= 0) { + fmt::print("sizes: {} {}\n", children_refs.size(), bvh.node_arrays.at(array_slot).nodes.size()); + ASSERT(children_refs.size() == bvh.node_arrays.at(array_slot).nodes.size()); + std::vector next_children; + + array_locs[array_slot] = generate_collide_draw_node_array( + gen, bvh.node_arrays.at(array_slot).nodes, flag, children_refs, next_children); + + children_refs = std::move(next_children); + array_slot--; + flag = 1; + } + + { + gen.align_to_basic(); + gen.add_type_tag("drawable-tree-collide-fragment"); + size_t result = gen.current_offset_bytes(); + gen.add_word((array_locs.size() - 1) << 16); // todo the minus one here?? + for (int i = 0; i < 6; i++) { + gen.add_word(0); + } + + for (size_t i = 1; i < array_locs.size(); i++) { // todo the offset here? + gen.link_word_to_byte(gen.add_word(0), array_locs[i]); + } + + return result; + } +} diff --git a/goalc/build_level/collide_drawable.h b/goalc/build_level/collide_drawable.h new file mode 100644 index 0000000000..5804dbb5c1 --- /dev/null +++ b/goalc/build_level/collide_drawable.h @@ -0,0 +1,12 @@ +#pragma once + +#include "goalc/build_level/collide_bvh.h" +#include "goalc/build_level/collide_pack.h" + +class DataObjectGenerator; + +struct DrawableTreeCollideFragment { + CollideFragMeshDataArray packed_frags; + collide::CollideTree bvh; + size_t add_to_object_file(DataObjectGenerator& gen) const; +}; diff --git a/goalc/build_level/collide_pack.cpp b/goalc/build_level/collide_pack.cpp new file mode 100644 index 0000000000..595ad6c89c --- /dev/null +++ b/goalc/build_level/collide_pack.cpp @@ -0,0 +1,263 @@ +#include + +#include "collide_pack.h" +#include "common/util/Assert.h" +#include "common/log/log.h" +#include "common/util/Timer.h" + +struct PackedU16Verts { + math::Vector base; + std::vector> vertex; +}; + +/*! + * Assert that the given float can be converted to a packed u16, and return that u16. + */ +u16 magic_float_to_u16(float in) { + u16 u16s[2]; + memcpy(u16s, &in, 4); + ASSERT_MSG(u16s[1] == 0x4d00, + "Unable to pack collision vertex data to u16's. Try to make smaller triangles and " + "avoid skinny triangles. (or there is a bug in the packer)"); + return u16s[0]; +} + +namespace { +float u32_to_float(u32 in) { + float out; + memcpy(&out, &in, 4); + return out; +} +} // namespace + +/*! + * Pack vertices to base + u16. + * The format is quite strange to allow for fast unpacking. + */ +PackedU16Verts pack_verts_to_u16(const std::vector& input) { + PackedU16Verts result; + ASSERT(!input.empty()); + + // this "magic" offset is a large float where a ulp is 16.f, or 1/256th of a meter. + // this means that float -> int can be done as a single addition. + // (or, in some cases, we can avoid float->int entirely) + math::Vector3f magic_offset; + magic_offset.fill(u32_to_float(0x4d000000)); + + // we'll be treating everything as an offset from this minimum vertex: + math::Vector3f min_vtx = input[0]; + for (auto& vtx : input) { + min_vtx.min_in_place(vtx); + } + // give us a tiny bit of extra room to avoid rounding problems + min_vtx -= 16.f; + + // to round down to nearest integer + result.base = min_vtx.cast(); + auto base = result.base.cast(); + + // compute offset relative to base. + for (auto& vtx : input) { + // add the "magic offset" to make this a 0x4dXXXXXX float. + // subtract the base to make this a 0x4d00XXXX float. + auto vertex_magic = vtx + magic_offset - base; + // and if we did it right, we should be able to pack to u16's here + result.vertex.emplace_back(magic_float_to_u16(vertex_magic[0]), + magic_float_to_u16(vertex_magic[1]), + magic_float_to_u16(vertex_magic[2])); + } + + // verify + /* + for (size_t i = 0; i < input.size(); i++) { + math::Vector3f v; + v[0] = u32_to_float(0x4d000000 + result.vertex[i][0]); + v[1] = u32_to_float(0x4d000000 + result.vertex[i][1]); + v[2] = u32_to_float(0x4d000000 + result.vertex[i][2]); + float base_offset = u32_to_float(0x4d000000); + math::Vector3f vf13_combo_offset(base_offset, base_offset, base_offset); + math::Vector3f vf14_base_trans_float(result.base[0], result.base[1], result.base[2]); + vf13_combo_offset -= vf14_base_trans_float; + v -= vf13_combo_offset; + fmt::print("error {}\n", (v - input[i]).to_string_aligned());; + } + */ + + return result; +} + +struct PatSurfaceHash { + size_t operator()(const PatSurface& in) const { return std::hash()(in.val); } +}; + +/*! + * pat -> pat index mapping. + * There's a pat "palette" with up 255 unique pats. + */ +struct PatMap { + std::unordered_map map; + std::vector pats; + + u32 add_pat(PatSurface pat) { + const auto& lookup = map.find(pat); + if (lookup == map.end()) { + u32 new_idx = pats.size(); + if (new_idx > UINT8_MAX) { + lg::die("Too many pats. Use fewer. Or improve the pat code to use multiple pat arrays."); + } + map[pat] = new_idx; + pats.push_back(pat); + return new_idx; + } else { + return lookup->second; + } + } +}; + +/*! + * A face, represented as indices + */ +struct IndexFace { + math::Vector vertex_indices; // per vertex, winding order matters here + u8 pat_idx; // pat for the whole face +}; + +/*! + * All the faces in a frag + */ +struct IndexedFaces { + std::vector faces; // index in to the two vertex array below: + std::vector vertices_float; + PackedU16Verts vertices_u16; +}; + +struct Vector3fHash { + size_t operator()(const math::Vector3f& in) const { + return std::hash()(in.x()) ^ std::hash()(in.y()) ^ std::hash()(in.z()); + } +}; + +/*! + * Deduplicate vertices, converted to indexed, add to pat palette, pack to u16s. + */ +IndexedFaces dedup_frag_mesh(const collide::CollideFrag& frag, PatMap* pat_map) { + IndexedFaces result; + std::unordered_map vertex_map; + // avoid confusion with 0 in strip table. (todo, can probably remove) + result.vertices_float.push_back(math::Vector3f::zero()); + + for (auto& face_in : frag.faces) { + auto& face_out = result.faces.emplace_back(); + // pat: + face_out.pat_idx = pat_map->add_pat(face_in.pat); + // vertices + for (int i = 0; i < 3; i++) { + const auto& lookup = vertex_map.find(face_in.v[i]); + if (lookup == vertex_map.end()) { + u32 idx = result.vertices_float.size(); + result.vertices_float.push_back(face_in.v[i]); + face_out.vertex_indices[i] = idx; + vertex_map[face_in.v[i]] = idx; + } else { + face_out.vertex_indices[i] = lookup->second; + } + } + } + // fmt::print("{} -> {}\n", frag.faces.size() * 3, result.vertices_float.size()); + result.vertices_u16 = pack_verts_to_u16(result.vertices_float); + return result; +} + +/*! + * make strip table that doesn't do any stripping. It will be quite long, which might cause problem + */ +std::vector make_dumb_strip_table(const IndexedFaces& faces) { + std::vector out; + ASSERT_MSG( + faces.vertices_float.size() < UINT8_MAX, + "somehow have UINT8_MAX deduped vertices in a single fragment, likely a bug somewhere."); + for (auto& face : faces.faces) { + out.push_back(face.vertex_indices[0]); + out.push_back(face.vertex_indices[1]); + out.push_back(face.vertex_indices[2]); + out.push_back(0); + } + out.push_back(-1); + + return out; +} + +CollideFragMeshDataArray pack_collide_frags(const std::vector& frag_data) { + Timer pack_timer; + CollideFragMeshDataArray result; + PatMap pat_map; + + size_t total_pack_bytes = 0; + lg::info("Packing {} fragments", frag_data.size()); + + for (auto& frag_in : frag_data) { + auto& frag_out = result.packed_frag_data.emplace_back(); + auto indexed = dedup_frag_mesh(frag_in, &pat_map); + // first part of packed_data is the u16 vertex data: + frag_out.vertex_count = indexed.vertices_u16.vertex.size(); + frag_out.packed_data.resize(sizeof(u16) * frag_out.vertex_count * 3); + memcpy(frag_out.packed_data.data(), indexed.vertices_u16.vertex.data(), + frag_out.packed_data.size()); + // align to 16-bytes + while (frag_out.packed_data.size() & 0xf) { + frag_out.packed_data.push_back(0); + } + // remember where + frag_out.vertex_data_qwc = frag_out.packed_data.size() / 16; + + // up next, the strip table + auto strip = make_dumb_strip_table(indexed); + frag_out.packed_data.insert(frag_out.packed_data.end(), strip.begin(), strip.end()); + frag_out.strip_data_len = strip.size(); + ASSERT(frag_out.strip_data_len < UINT16_MAX); // probably in big trouble in here. + + // pat table + for (auto& face : indexed.faces) { + frag_out.packed_data.push_back(face.pat_idx); + } + + // align to 16-bytes so total_qwc works. + while (frag_out.packed_data.size() & 0xf) { + frag_out.packed_data.push_back(0); + } + // gonna guess here: + frag_out.poly_count = indexed.faces.size(); + frag_out.total_qwc = frag_out.packed_data.size() / 16; + frag_out.base_trans_xyz_s32 = indexed.vertices_u16.base; + frag_out.bsphere = frag_in.bsphere; + total_pack_bytes += frag_out.packed_data.size(); + } + + result.pats = pat_map.pats; + lg::info("Total packed data size: {} kB, took {:.2f} ms", total_pack_bytes / 1024, + pack_timer.getMs()); + return result; +} + +/* +(deftype collide-frag-mesh (basic) + ((packed-data uint32 :offset-assert 4) <- ptr + (pat-array uint32 :offset-assert 8) <- ptr + (strip-data-len uint16 :offset-assert 12) + (poly-count uint16 :offset-assert 14) + (base-trans vector :inline :offset-assert 16) + ;; these go in the w of the vector above. + (vertex-count uint8 :offset 28) // done! + (vertex-data-qwc uint8 :offset 29) // done! + (total-qwc uint8 :offset 30) + (unused uint8 :offset 31) + ) + :method-count-assert 9 + :size-assert #x20 + :flag-assert #x900000020 + ) + */ + +// packed_data: +// (u16x3) per vertex, packed float vtx format. +// diff --git a/goalc/build_level/collide_pack.h b/goalc/build_level/collide_pack.h new file mode 100644 index 0000000000..316b56e9f4 --- /dev/null +++ b/goalc/build_level/collide_pack.h @@ -0,0 +1,21 @@ +#pragma once + +#include "goalc/build_level/collide_bvh.h" + +struct CollideFragMeshData { + math::Vector4f bsphere; // not part of the collide frag, but is part of the drawable wrapping it + std::vector packed_data; + u32 strip_data_len; + u32 poly_count; + math::Vector base_trans_xyz_s32; + u8 vertex_count; + u8 vertex_data_qwc; + u8 total_qwc; +}; + +struct CollideFragMeshDataArray { + std::vector packed_frag_data; + std::vector pats; +}; + +CollideFragMeshDataArray pack_collide_frags(const std::vector& frag_data); \ No newline at end of file diff --git a/goalc/build_level/color_quantization.cpp b/goalc/build_level/color_quantization.cpp new file mode 100644 index 0000000000..91784dc117 --- /dev/null +++ b/goalc/build_level/color_quantization.cpp @@ -0,0 +1,220 @@ +#include +#include "color_quantization.h" +#include "common/util/Assert.h" +#include "common/log/log.h" + +/*! + * Just removes duplicate colors, which can work if there are only a few unique colors. + */ +QuantizedColors quantize_colors_dumb(const std::vector>& in) { + QuantizedColors result; + std::unordered_map color_to_slot; + for (auto& vtx : in) { + u64 key; + memcpy(&key, vtx.data(), sizeof(u64)); + const auto& existing = color_to_slot.find(key); + if (existing == color_to_slot.end()) { + auto cidx = result.final_colors.size(); + result.vtx_to_color.push_back(cidx); + color_to_slot[key] = cidx; + result.final_colors.push_back(vtx); + } else { + result.vtx_to_color.push_back(existing->second); + } + } + fmt::print("quantize_colors_dumb: {} -> {}\n", in.size(), result.final_colors.size()); + ASSERT(result.final_colors.size() < 8192); + return result; +} + +namespace { + +using Color = math::Vector; + +// An octree node. +// Represents a color in the output if rgb_sum_count > 0. +// Otherwise, just organizational. +struct Node { + u32 r_sum = 0; + u32 g_sum = 0; + u32 b_sum = 0; + u32 rgb_sum_count = 0; + u8 depth = 0xff; // 0 for root, 7 deepest + + // children stuff + u32 leaves_under_me = 0; + std::vector children; + Node* parent = nullptr; + + u32 final_idx = UINT32_MAX; +}; + +u8 child_index(Color color, u8 depth) { + u8 r_bit = (color.x() >> (7 - depth)) & 1; + u8 g_bit = (color.y() >> (7 - depth)) & 1; + u8 b_bit = (color.z() >> (7 - depth)) & 1; + return (r_bit) + (g_bit * 2) + (b_bit * 4); +} + +void insert(Node& root, Color color, u8 current_depth) { + if (current_depth == 7) { + root.r_sum += color.x(); + root.g_sum += color.y(); + root.b_sum += color.z(); + if (root.rgb_sum_count == 0) { + for (auto* up = root.parent; up; up = up->parent) { + up->leaves_under_me++; + } + } + root.rgb_sum_count++; + } else { + if (root.children.empty()) { + root.children.resize(8); + } + auto& next_node = root.children[child_index(color, current_depth)]; + if (next_node.depth == 0xff) { + next_node.depth = current_depth + 1; + next_node.parent = &root; + } + insert(next_node, color, current_depth + 1); + } +} + +template +void for_each_node(Node& root, T&& func) { + func(root); + for (auto& child : root.children) { + for_each_node(child, func); + } +} + +u32 count_leaves(Node& root) { + u32 result = 0; + for_each_node(root, [&](Node& n) { + if (n.rgb_sum_count) { + ASSERT(n.children.empty()); + result++; + } + }); + return result; +} + +void collapse1(Node& root) { + ASSERT(!root.children.empty()); + u32 total_children_removed = 0; + u32 total_rgb_sum_moved_up = 0; + bool started_as_leaf = root.rgb_sum_count; + for (auto& child : root.children) { + if (child.depth != 0xff) { + ASSERT(child.children.empty()); + ASSERT(child.rgb_sum_count); + total_children_removed++; + root.r_sum += child.r_sum; + root.g_sum += child.g_sum; + root.b_sum += child.b_sum; + root.rgb_sum_count += child.rgb_sum_count; + total_rgb_sum_moved_up += child.rgb_sum_count; + } + } + ASSERT(total_children_removed == root.leaves_under_me); + + if (!started_as_leaf && root.rgb_sum_count) { + total_children_removed--; + } + root.children.clear(); + root.leaves_under_me = 0; + if (total_children_removed) { + for (auto* up = root.parent; up; up = up->parent) { + up->leaves_under_me -= total_children_removed; + } + } + + ASSERT(count_leaves(root) == 1); +} + +void find_nodes_at_level(Node& n, std::vector& out, u8 level) { + if (n.depth == level) { + out.push_back(&n); + } else if (n.depth < level) { + for (auto& child : n.children) { + find_nodes_at_level(child, out, level); + } + } +} + +void collapse_at_level(Node& root, u8 level, u32 target_leaf_count) { + std::vector nodes_at_level; + find_nodes_at_level(root, nodes_at_level, level); + std::stable_sort(nodes_at_level.begin(), nodes_at_level.end(), + [](Node* a, Node* b) { return a->leaves_under_me < b->leaves_under_me; }); + + size_t at_level_to_try = 0; + while (root.leaves_under_me > target_leaf_count && at_level_to_try < nodes_at_level.size()) { + collapse1(*nodes_at_level[at_level_to_try++]); + } +} + +void collapse_as_needed(Node& root, u32 target_leaf_count) { + u32 level_to_reduce = 6; + while (root.leaves_under_me > target_leaf_count) { + collapse_at_level(root, level_to_reduce--, target_leaf_count); + } +} + +void assign_colors(Node& root, std::vector& palette_out) { + u32 idx = 0; + for_each_node(root, [&](Node& n) { + if (n.rgb_sum_count) { + n.final_idx = idx++; + palette_out.emplace_back(n.r_sum / n.rgb_sum_count, n.g_sum / n.rgb_sum_count, + n.b_sum / n.rgb_sum_count); + } + }); +} + +u32 lookup_node_for_color(Node& root, Color c, u8 depth) { + if (root.children.empty()) { + return root.final_idx; + } else { + return lookup_node_for_color(root.children[child_index(c, depth)], c, depth + 1); + } +} + +} // namespace + +/*! + * Quantize colors using an octree for clustering. + */ +QuantizedColors quantize_colors_octree(const std::vector>& in, + u32 target_count) { + Node root; + root.depth = 0; + for (auto& color : in) { + insert(root, color, 0); + } + + collapse_as_needed(root, target_count); + + QuantizedColors out; + assign_colors(root, out.final_colors); + for (auto& color : in) { + out.vtx_to_color.push_back(lookup_node_for_color(root, color, 0)); + } + + float total_error[3] = {0, 0, 0}; + for (size_t i = 0; i < in.size(); i++) { + // fmt::print(" {} -> {}\n", in[i].to_string_hex_byte(), + // out.final_colors[out.vtx_to_color[i]].to_string_hex_byte()); + auto diff = in[i].cast() - out.final_colors[out.vtx_to_color[i]].cast(); + + for (int j = 0; j < 3; j++) { + total_error[j] += std::abs(diff[j]); + } + } + + lg::info("Octree quantize average error (as 8-bit ints): r: {}, g: {} b: {}", + total_error[0] / in.size(), total_error[1] / in.size(), total_error[2] / in.size()); + lg::info("Final palette size: {}", out.final_colors.size()); + + return out; +} \ No newline at end of file diff --git a/goalc/build_level/color_quantization.h b/goalc/build_level/color_quantization.h new file mode 100644 index 0000000000..44b5ba68cf --- /dev/null +++ b/goalc/build_level/color_quantization.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include "common/common_types.h" +#include "common/math/Vector.h" + +// TODO: come up with something for time of day colors. +// time of day colors make the colors effectively 24-channel instead of just 3. +// the octree approach doesn't work too well here (we'd need a 16777216-tree) +// but a k-d tree seems like the right approach. + +struct QuantizedColors { + std::vector> final_colors; + std::vector vtx_to_color; +}; + +QuantizedColors quantize_colors_dumb(const std::vector>& in); + +QuantizedColors quantize_colors_octree(const std::vector>& in, + u32 target_count); \ No newline at end of file diff --git a/goalc/build_level/gltf_mesh_extract.cpp b/goalc/build_level/gltf_mesh_extract.cpp new file mode 100644 index 0000000000..a81463c052 --- /dev/null +++ b/goalc/build_level/gltf_mesh_extract.cpp @@ -0,0 +1,688 @@ +/*! + * Mesh extraction for GLTF meshes. + */ + +#include "gltf_mesh_extract.h" +#include "goalc/build_level/color_quantization.h" +#include "third-party/tiny_gltf/tiny_gltf.h" +#include "common/log/log.h" +#include "common/util/Timer.h" +#include "common/math/geometry.h" + +namespace gltf_mesh_extract { + +namespace { + +/*! + * Convert a GLTF index buffer to std::vector + */ +template +std::vector index_list_to_u32(const u8* data, u32 num_verts, u32 offset, u32 stride) { + std::vector result; + result.reserve(num_verts); + for (u32 i = 0; i < num_verts; i++) { + T val; + memcpy(&val, data, sizeof(T)); + result.push_back(offset + val); + data += stride; + } + return result; +} + +/*! + * Convert a GLTF position buffer or similar to std::vector + */ +std::vector extract_vec3f(const u8* data, u32 count, u32 stride) { + std::vector result; + result.reserve(count); + for (u32 i = 0; i < count; i++) { + memcpy(&result.emplace_back(), data, sizeof(math::Vector3f)); + data += stride; + } + return result; +} + +std::vector extract_vec2f(const u8* data, u32 count, u32 stride) { + std::vector result; + result.reserve(count); + for (u32 i = 0; i < count; i++) { + memcpy(&result.emplace_back(), data, sizeof(math::Vector2f)); + data += stride; + } + return result; +} + +/*! + * Convert a GLTF color buffer to u8 colors. + */ +std::vector> extract_color_from_vec4_u16(const u8* data, + u32 count, + u32 stride) { + std::vector> result; + result.reserve(count); + for (u32 i = 0; i < count; i++) { + math::Vector temp; + memcpy(&temp, data, sizeof(math::Vector)); + data += stride; + result.emplace_back(temp.x() >> 8, temp.y() >> 8, temp.z() >> 8, temp.w() >> 8); + } + return result; +} + +/*! + * Convert a GLTF index buffer + */ +std::vector gltf_index_buffer(const tinygltf::Model& model, + int indices_idx, + u32 index_offset) { + const auto& indices_accessor = model.accessors[indices_idx]; + const auto& buffer_view = model.bufferViews[indices_accessor.bufferView]; + const auto& buffer = model.buffers[buffer_view.buffer]; + const auto data_ptr = buffer.data.data() + buffer_view.byteOffset + indices_accessor.byteOffset; + const auto stride = indices_accessor.ByteStride(buffer_view); + const auto count = indices_accessor.count; + + switch (indices_accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_BYTE: + return index_list_to_u32(data_ptr, count, index_offset, stride); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + return index_list_to_u32(data_ptr, count, index_offset, stride); + case TINYGLTF_COMPONENT_TYPE_SHORT: + return index_list_to_u32(data_ptr, count, index_offset, stride); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + return index_list_to_u32(data_ptr, count, index_offset, stride); + case TINYGLTF_COMPONENT_TYPE_INT: + return index_list_to_u32(data_ptr, count, index_offset, stride); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + return index_list_to_u32(data_ptr, count, index_offset, stride); + default: + ASSERT_MSG(false, "unsupported component type"); + } +} + +struct ExtractedVertices { + std::vector vtx; + std::vector> vtx_colors; + std::vector normals; +}; + +ExtractedVertices gltf_vertices(const tinygltf::Model& model, + const std::map& attributes, + const math::Matrix4f& w_T_local, + bool get_colors, + bool get_normals, + const std::string& debug_name) { + std::vector result; + std::vector> vtx_colors; + + { + const auto& position_attrib = attributes.find("POSITION"); + ASSERT_MSG(position_attrib != attributes.end(), "Did not find position attribute."); + + const auto attrib_accessor = model.accessors[position_attrib->second]; + const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView]; + const auto& buffer = model.buffers[buffer_view.buffer]; + const auto data_ptr = buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset; + const auto byte_stride = attrib_accessor.ByteStride(buffer_view); + const auto count = attrib_accessor.count; + + ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC3, "POSITION wasn't vec3"); + ASSERT_MSG(attrib_accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT, + "POSITION wasn't float"); + // for (auto& attrib : attributes) { + // fmt::print("attrib: {}\n", attrib.first); + //} + auto mesh_verts = extract_vec3f(data_ptr, count, byte_stride); + result.reserve(mesh_verts.size()); + for (auto& vert : mesh_verts) { + auto& new_vert = result.emplace_back(); + math::Vector4f v_in(vert.x(), vert.y(), vert.z(), 1); + math::Vector4f v_w = w_T_local * v_in; + new_vert.x = v_w.x() * 4096; + new_vert.y = v_w.y() * 4096; + new_vert.z = v_w.z() * 4096; + } + } + + if (get_colors) { + const auto& color_attrib = attributes.find("COLOR_0"); + if (color_attrib == attributes.end()) { + lg::error("Mesh {} didn't have any colors, using white", debug_name); + for (size_t i = 0; i < result.size(); i++) { + vtx_colors.emplace_back(0x80, 0x80, 0x80, 0xff); + } + } else { + const auto attrib_accessor = model.accessors[color_attrib->second]; + const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView]; + const auto& buffer = model.buffers[buffer_view.buffer]; + const auto data_ptr = + buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset; + const auto byte_stride = attrib_accessor.ByteStride(buffer_view); + const auto count = attrib_accessor.count; + + ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC4, "COLOR_0 wasn't vec4"); + ASSERT_MSG( + attrib_accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT, + fmt::format("COLOR_0 wasn't float, got {} instead", attrib_accessor.componentType)); + auto colors = extract_color_from_vec4_u16(data_ptr, count, byte_stride); + vtx_colors.insert(vtx_colors.end(), colors.begin(), colors.end()); + } + + // ASSERT_MSG(color_attrib != attributes.end(), "Did not find color attribute."); + } + + bool got_texture = false; + { + const auto& texcoord_attrib = attributes.find("TEXCOORD_0"); + if (texcoord_attrib != attributes.end()) { + const auto attrib_accessor = model.accessors[texcoord_attrib->second]; + const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView]; + const auto& buffer = model.buffers[buffer_view.buffer]; + const auto data_ptr = + buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset; + const auto byte_stride = attrib_accessor.ByteStride(buffer_view); + const auto count = attrib_accessor.count; + + ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC2, "TEXCOORD wasn't vec2"); + ASSERT_MSG(attrib_accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT, + "TEXCOORD wasn't float"); + auto mesh_verts = extract_vec2f(data_ptr, count, byte_stride); + ASSERT(mesh_verts.size() == result.size()); + got_texture = true; + for (size_t i = 0; i < mesh_verts.size(); i++) { + result[i].s = mesh_verts[i].x(); + result[i].t = mesh_verts[i].y(); + } + } else { + if (!get_normals) { + // don't warn if we're just getting collision + lg::warn("No texcoord attribute for mesh: {}", debug_name); + } + } + } + + std::vector normals; + if (get_normals) { + const auto& normal_attrib = attributes.find("NORMAL"); + if (normal_attrib != attributes.end()) { + const auto attrib_accessor = model.accessors[normal_attrib->second]; + const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView]; + const auto& buffer = model.buffers[buffer_view.buffer]; + const auto data_ptr = + buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset; + const auto byte_stride = attrib_accessor.ByteStride(buffer_view); + const auto count = attrib_accessor.count; + + ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC3, "NORMAL wasn't vec3"); + ASSERT_MSG(attrib_accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT, + "NORMAL wasn't float"); + normals = extract_vec3f(data_ptr, count, byte_stride); + ASSERT(normals.size() == result.size()); + } else { + lg::error("No NORMAL attribute for mesh: {}", debug_name); + } + } + + for (auto& v : result) { + v.color_index = 0; + if (!got_texture) { + v.s = 0; + v.t = 0; + } + + v.q_unused = 0; + v.pad[0] = 0; + v.pad[1] = 0; + v.pad[2] = 0; + } + // TODO: other properties + return {result, vtx_colors, normals}; +} + +DrawMode make_default_draw_mode() { + DrawMode mode; + mode.set_depth_write_enable(true); + mode.set_depth_test(GsTest::ZTest::GEQUAL); + mode.set_alpha_blend(DrawMode::AlphaBlend::DISABLED); + mode.set_aref(0); + mode.set_alpha_fail(GsTest::AlphaFail::KEEP); + mode.set_clamp_s_enable(true); + mode.set_clamp_t_enable(true); + mode.disable_filt(); // for checkerboard... + mode.enable_tcc(); // ? + mode.disable_at(); + mode.enable_zt(); + mode.disable_ab(); + mode.disable_decal(); + mode.enable_fog(); + return mode; +} + +int texture_pool_debug_checker(TexturePool* pool) { + const auto& existing = pool->textures_by_name.find("DEBUG_CHECKERBOARD"); + if (existing == pool->textures_by_name.end()) { + size_t idx = pool->textures_by_idx.size(); + pool->textures_by_name["DEBUG_CHECKERBOARD"] = idx; + auto& tex = pool->textures_by_idx.emplace_back(); + tex.w = 16; + tex.h = 16; + tex.debug_name = "DEBUG_CHECKERBOARD"; + tex.debug_tpage_name = "DEBUG"; + tex.load_to_pool = false; + tex.combo_id = 0; // doesn't matter, not a pool tex + tex.data.resize(16 * 16); + u32 c0 = 0xa0303030; + u32 c1 = 0xa0e0e0e0; + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + tex.data[i * 16 + j] = (((i / 4) & 1) ^ ((j / 4) & 1)) ? c1 : c0; + } + } + return idx; + } else { + return existing->second; + } +} + +int texture_pool_add_texture(TexturePool* pool, const tinygltf::Image& tex) { + const auto& existing = pool->textures_by_name.find(tex.name); + if (existing != pool->textures_by_name.end()) { + lg::info("Reusing image: {}", tex.name); + return existing->second; + } + + ASSERT(tex.bits == 8); + ASSERT(tex.component == 4); + ASSERT(tex.pixel_type == TINYGLTF_TEXTURE_TYPE_UNSIGNED_BYTE); + + size_t idx = pool->textures_by_idx.size(); + pool->textures_by_name[tex.name] = idx; + auto& tt = pool->textures_by_idx.emplace_back(); + tt.w = tex.width; + tt.h = tex.height; + tt.debug_name = tex.name; + tt.debug_tpage_name = "custom-level"; + tt.load_to_pool = false; + tt.combo_id = 0; // doesn't matter, not a pool tex + tt.data.resize(tt.w * tt.h * 4); + ASSERT(tex.image.size() >= tt.data.size()); + memcpy(tt.data.data(), tex.image.data(), tt.data.size()); + return idx; +} +} // namespace + +math::Matrix4f affine_translation(const math::Vector3f& translation) { + math::Matrix4f result = math::Matrix4f::identity(); + result(0, 3) = translation[0]; + result(1, 3) = translation[1]; + result(2, 3) = translation[2]; + result(3, 3) = 1; + return result; +} + +math::Matrix4f affine_scale(const math::Vector3f& scale) { + math::Matrix4f result = math::Matrix4f::zero(); + result(0, 0) = scale[0]; + result(1, 1) = scale[1]; + result(2, 2) = scale[2]; + result(3, 3) = 1; + return result; +} + +math::Matrix4f affine_rot_qxyzw(const math::Vector4f& quat) { + math::Matrix4f result = math::Matrix4f::zero(); + result(3, 3) = 1; + result(0, 0) = 1.0 - 2.0 * (quat.y() * quat.y() + quat.z() * quat.z()); + result(0, 1) = 2.0 * (quat.x() * quat.y() - quat.z() * quat.w()); + result(0, 2) = 2.0 * (quat.x() * quat.z() + quat.y() * quat.w()); + result(1, 0) = 2.0 * (quat.x() * quat.y() + quat.z() * quat.w()); + result(1, 1) = 1.0 - 2.0 * (quat.x() * quat.x() + quat.z() * quat.z()); + result(1, 2) = 2.0 * (quat.y() * quat.z() - quat.x() * quat.w()); + result(2, 0) = 2.0 * (quat.x() * quat.z() - quat.y() * quat.w()); + result(2, 1) = 2.0 * (quat.y() * quat.z() + quat.x() * quat.w()); + result(2, 2) = 1.0 - 2.0 * (quat.x() * quat.x() + quat.y() * quat.y()); + return result; +} + +math::Vector3f vector3f_from_gltf(const std::vector& in) { + ASSERT(in.size() == 3); + return math::Vector3f{in[0], in[1], in[2]}; +} + +math::Vector4f vector4f_from_gltf(const std::vector& in) { + ASSERT(in.size() == 4); + return math::Vector4f{in[0], in[1], in[2], in[3]}; +} + +math::Matrix4f matrix_from_node(const tinygltf::Node& node) { + if (!node.matrix.empty()) { + math::Matrix4f result; + for (int i = 0; i < 16; i++) { + result.data()[i] = node.matrix[i]; + } + return result; + } else { + // from trs + math::Matrix4f t, r, s; + if (!node.translation.empty()) { + t = affine_translation(vector3f_from_gltf(node.translation)); + } else { + t = math::Matrix4f::identity(); + } + + if (!node.rotation.empty()) { + r = affine_rot_qxyzw(vector4f_from_gltf(node.rotation)); + } else { + r = math::Matrix4f::identity(); + } + + if (!node.scale.empty()) { + s = affine_scale(vector3f_from_gltf(node.scale)); + } else { + s = math::Matrix4f::identity(); + } + + return t * r * s; + } +} + +struct NodeWithTransform { + int node_idx; + math::Matrix4f w_T_node; +}; + +/*! + * Recursively walk the tree of nodes, flatten, and compute w_T_node for each. + */ +void node_find_helper(const tinygltf::Model& model, + const math::Matrix4f& w_T_parent, + int node_idx, + std::vector* out) { + const auto& node = model.nodes.at(node_idx); + math::Matrix4f w_T_node = w_T_parent * matrix_from_node(node); + out->push_back({node_idx, w_T_node}); + for (auto& child : node.children) { + node_find_helper(model, w_T_node, child, out); + } +} + +std::vector flatten_nodes_from_all_scenes(const tinygltf::Model& model) { + std::vector out; + for (auto& scene : model.scenes) { + for (auto& nidx : scene.nodes) { + math::Matrix4f identity = math::Matrix4f::identity(); + node_find_helper(model, identity, nidx, &out); + } + } + return out; +} + +void dedup_vertices(const std::vector& vertices_in, + std::vector& vertices_out, + std::vector& old_to_new_out) { + ASSERT(vertices_out.empty()); + ASSERT(old_to_new_out.empty()); + old_to_new_out.resize(vertices_in.size(), -1); + + std::unordered_map vtx_to_new; + + for (size_t in_idx = 0; in_idx < vertices_in.size(); in_idx++) { + auto& vtx = vertices_in[in_idx]; + const auto& lookup = vtx_to_new.find(vtx); + if (lookup == vtx_to_new.end()) { + // first time seeing this one + size_t new_idx = vertices_out.size(); + vertices_out.push_back(vtx); + old_to_new_out[in_idx] = new_idx; + vtx_to_new[vtx] = new_idx; + } else { + old_to_new_out[in_idx] = lookup->second; + } + } +} + +void dedup_vertices(TfragOutput& data) { + Timer timer; + size_t original_size = data.vertices.size(); + std::vector new_verts; + std::vector old_to_new; + + dedup_vertices(data.vertices, new_verts, old_to_new); + data.vertices = std::move(new_verts); + + for (auto& draw : data.strip_draws) { + 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); +} + +DrawMode draw_mode_from_sampler(const tinygltf::Sampler& sampler) { + DrawMode mode = make_default_draw_mode(); + if (sampler.magFilter == TINYGLTF_TEXTURE_FILTER_NEAREST) { + ASSERT(sampler.minFilter == TINYGLTF_TEXTURE_FILTER_NEAREST); + mode.set_filt_enable(false); + } else { + ASSERT(sampler.minFilter != TINYGLTF_TEXTURE_FILTER_NEAREST); + mode.set_filt_enable(true); + } + + switch (sampler.wrapS) { + case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE: + mode.set_clamp_s_enable(true); + break; + case TINYGLTF_TEXTURE_WRAP_REPEAT: + mode.set_clamp_s_enable(false); + break; + default: + ASSERT(false); + } + + switch (sampler.wrapT) { + case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE: + mode.set_clamp_t_enable(true); + break; + case TINYGLTF_TEXTURE_WRAP_REPEAT: + mode.set_clamp_t_enable(false); + break; + default: + ASSERT(false); + } + + return mode; +} + +void extract(const Input& in, + TfragOutput& out, + const tinygltf::Model& model, + const std::vector& all_nodes) { + std::vector> all_vtx_colors; + ASSERT(out.vertices.empty()); + std::map draw_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.mesh >= 0) { + const auto& mesh = model.meshes[node.mesh]; + if (!mesh.extras.Has("tfrag")) { + // fmt::print("skip tfrag: {}\n", mesh.name); + // continue; + } + mesh_count++; + for (const auto& prim : mesh.primitives) { + prim_count++; + // extract index buffer + std::vector prim_indices = gltf_index_buffer(model, prim.indices, out.vertices.size()); + ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES, "Unsupported triangle mode"); + // extract vertices + auto verts = + gltf_vertices(model, prim.attributes, n.w_T_node, in.get_colors, false, mesh.name); + out.vertices.insert(out.vertices.end(), verts.vtx.begin(), verts.vtx.end()); + if (in.get_colors) { + all_vtx_colors.insert(all_vtx_colors.end(), verts.vtx_colors.begin(), + verts.vtx_colors.end()); + } + + // TODO: just putting it all in one material + auto& draw = draw_by_material[prim.material]; + draw.mode = make_default_draw_mode(); // todo rm + draw.tree_tex_id = texture_pool_debug_checker(in.tex_pool); // todo rm + draw.num_triangles += prim_indices.size() / 3; + if (draw.vis_groups.empty()) { + auto& grp = draw.vis_groups.emplace_back(); + grp.num_inds += prim_indices.size(); + grp.num_tris += draw.num_triangles; + grp.vis_idx_in_pc_bvh = UINT32_MAX; + } else { + auto& grp = draw.vis_groups.back(); + grp.num_inds += prim_indices.size(); + grp.num_tris += draw.num_triangles; + grp.vis_idx_in_pc_bvh = UINT32_MAX; + } + + draw.plain_indices.insert(draw.plain_indices.end(), prim_indices.begin(), + prim_indices.end()); + } + } + } + + for (const auto& [mat_idx, d_] : draw_by_material) { + out.strip_draws.push_back(d_); + auto& draw = out.strip_draws.back(); + 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); + continue; + } + const auto& mat = model.materials[mat_idx]; + 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); + continue; + } + + const auto& tex = model.textures[tex_idx]; + ASSERT(tex.sampler >= 0); + ASSERT(tex.source >= 0); + draw.mode = draw_mode_from_sampler(model.samplers.at(tex.sampler)); + + const auto& img = model.images[tex.source]; + draw.tree_tex_id = texture_pool_add_texture(in.tex_pool, img); + } + lg::info("total of {} unique materials", out.strip_draws.size()); + + lg::info("Merged {} meshes and {} prims into {} vertices", mesh_count, prim_count, + out.vertices.size()); + + if (in.get_colors) { + Timer quantize_timer; + auto quantized = quantize_colors_octree(all_vtx_colors, 1024); + for (size_t i = 0; i < out.vertices.size(); i++) { + out.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_vertices(out); +} + +void extract(const Input& in, + CollideOutput& out, + const tinygltf::Model& model, + const std::vector& all_nodes) { + int mesh_count = 0; + int prim_count = 0; + int suspicious_faces = 0; + + for (const auto& n : all_nodes) { + const auto& node = model.nodes[n.node_idx]; + fmt::print("node: {} {}\n", node.name, node.mesh); + if (node.mesh >= 0) { + const auto& mesh = model.meshes[node.mesh]; + if (!mesh.extras.Has("collide")) { + // fmt::print("skip collide: {}\n", mesh.name); + // continue; + } + mesh_count++; + for (const auto& prim : mesh.primitives) { + prim_count++; + // extract index buffer + std::vector prim_indices = gltf_index_buffer(model, prim.indices, 0); + ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES, "Unsupported triangle mode"); + // extract vertices + auto verts = gltf_vertices(model, prim.attributes, n.w_T_node, false, true, mesh.name); + + for (size_t iidx = 0; iidx < prim_indices.size(); iidx += 3) { + 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++; + std::swap(face.v[2], face.v[1]); + } + + face.bsphere = math::bsphere_of_triangle(face.v); + face.bsphere.w() += 1e-1; + for (int j = 0; j < 3; j++) { + float output_dist = face.bsphere.w() - (face.bsphere.xyz() - face.v[j]).length(); + if (output_dist < 0) { + fmt::print("{}\n", output_dist); + fmt::print("BAD:\n{}\n{}\n{}\n", face.v[0].to_string_aligned(), + face.v[1].to_string_aligned(), face.v[2].to_string_aligned()); + fmt::print("bsphere: {}\n", face.bsphere.to_string_aligned()); + } + } + + out.faces.push_back(face); + } + } + } + } + + lg::info("{} out of {} faces were suspicious (a small number is ok)", suspicious_faces, + out.faces.size()); + // 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); + lg::info("GLTF total took {:.2f} ms", read_timer.getMs()); +} +} // namespace gltf_mesh_extract diff --git a/goalc/build_level/gltf_mesh_extract.h b/goalc/build_level/gltf_mesh_extract.h new file mode 100644 index 0000000000..5bc0e47606 --- /dev/null +++ b/goalc/build_level/gltf_mesh_extract.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "common/custom_data/Tfrag3Data.h" +#include "goalc/build_level/TexturePool.h" +#include "goalc/build_level/collide_common.h" + +namespace gltf_mesh_extract { + +struct Input { + std::string filename; + TexturePool* tex_pool = nullptr; + bool get_colors = true; +}; + +struct TfragOutput { + std::vector strip_draws; + std::vector vertices; + std::vector> color_palette; +}; + +struct CollideOutput { + std::vector faces; +}; + +struct Output { + TfragOutput tfrag; + CollideOutput collide; +}; + +void extract(const Input& in, Output& out); + +} // namespace gltf_mesh_extract \ No newline at end of file diff --git a/goalc/data_compiler/DataObjectGenerator.cpp b/goalc/data_compiler/DataObjectGenerator.cpp index 12fa9767d7..6864bfa2f6 100644 --- a/goalc/data_compiler/DataObjectGenerator.cpp +++ b/goalc/data_compiler/DataObjectGenerator.cpp @@ -51,6 +51,17 @@ int DataObjectGenerator::add_word(u32 word) { return result; } +int DataObjectGenerator::add_word_float(float f) { + auto result = int(m_words.size()); + m_words.push_back(0); + memcpy(&m_words.back(), &f, sizeof(float)); + return result; +} + +void DataObjectGenerator::set_word(u32 word_idx, u32 val) { + m_words.at(word_idx) = val; +} + void DataObjectGenerator::link_word_to_word(int source, int target, int offset) { link_word_to_byte(source, target * 4 + offset); } @@ -68,6 +79,10 @@ int DataObjectGenerator::add_ref_to_string_in_pool(const std::string& str) { return result; } +void DataObjectGenerator::link_word_to_string_in_pool(const std::string& str, int word_idx) { + m_string_pool[str].push_back(word_idx); +} + int DataObjectGenerator::add_type_tag(const std::string& str) { auto result = int(m_words.size()); m_words.push_back(0); @@ -82,12 +97,20 @@ int DataObjectGenerator::add_symbol_link(const std::string& str) { return result; } +void DataObjectGenerator::link_word_to_symbol(const std::string& str, int word_idx) { + m_symbol_links[str].push_back(word_idx); +} + void DataObjectGenerator::align(int alignment_words) { while (m_words.size() % alignment_words) { m_words.push_back(0); } } +void DataObjectGenerator::align_to_basic() { + align(4); +} + int DataObjectGenerator::words() const { return int(m_words.size()); } diff --git a/goalc/data_compiler/DataObjectGenerator.h b/goalc/data_compiler/DataObjectGenerator.h index 3522b9a3b7..97e5b32dd1 100644 --- a/goalc/data_compiler/DataObjectGenerator.h +++ b/goalc/data_compiler/DataObjectGenerator.h @@ -8,15 +8,22 @@ class DataObjectGenerator { public: int add_word(u32 word); + int add_word_float(float f); + void set_word(u32 word_idx, u32 val); void link_word_to_word(int source, int target, int offset = 0); void link_word_to_byte(int source_word, int target_byte); int add_ref_to_string_in_pool(const std::string& str); + void link_word_to_string_in_pool(const std::string& str, int word_idx); int add_type_tag(const std::string& str); int add_symbol_link(const std::string& str); + void link_word_to_symbol(const std::string& str, int word_idx); std::vector generate_v2(); std::vector generate_v4(); void align(int alignment_words); + void align_to_basic(); int words() const; + size_t current_offset_bytes() const { return m_words.size() * sizeof(u32); } + u8* data() { return (u8*)m_words.data(); } private: void add_strings(); diff --git a/goalc/make/MakeSystem.cpp b/goalc/make/MakeSystem.cpp index a59154d44b..21f9c1b8d8 100644 --- a/goalc/make/MakeSystem.cpp +++ b/goalc/make/MakeSystem.cpp @@ -66,6 +66,7 @@ MakeSystem::MakeSystem() { add_tool(); add_tool(); add_tool(); + add_tool(); } /*! diff --git a/goalc/make/Tools.cpp b/goalc/make/Tools.cpp index d387bb75bf..01194a4ceb 100644 --- a/goalc/make/Tools.cpp +++ b/goalc/make/Tools.cpp @@ -1,5 +1,3 @@ - - #include "Tools.h" #include @@ -11,6 +9,7 @@ #include "goalc/data_compiler/dir_tpages.h" #include "goalc/data_compiler/game_count.h" #include "goalc/data_compiler/game_text_common.h" +#include "goalc/build_level/build_level.h" CompilerTool::CompilerTool(Compiler* compiler) : Tool("goalc"), m_compiler(compiler) {} @@ -186,3 +185,20 @@ bool SubtitleTool::run(const ToolInput& task) { } return true; } + +BuildLevelTool::BuildLevelTool() : Tool("build-level") {} + +bool BuildLevelTool::needs_run(const ToolInput& task) { + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } + auto deps = get_build_level_deps(task.input.at(0)); + return Tool::needs_run({task.input, deps, task.output, task.arg}); +} + +bool BuildLevelTool::run(const ToolInput& task) { + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } + return run_build_level(task.input.at(0), task.output.at(0)); +} diff --git a/goalc/make/Tools.h b/goalc/make/Tools.h index e6d61b2b04..966a1820f2 100644 --- a/goalc/make/Tools.h +++ b/goalc/make/Tools.h @@ -62,3 +62,10 @@ class SubtitleTool : public Tool { bool run(const ToolInput& task) override; bool needs_run(const ToolInput& task) override; }; + +class BuildLevelTool : public Tool { + public: + BuildLevelTool(); + bool run(const ToolInput& task) override; + bool needs_run(const ToolInput& task) override; +}; diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 678f90c5fe..900f43bd2c 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,5 +1,4 @@ add_subdirectory(level_tools) -add_subdirectory(build_level) add_executable(dgo_unpacker dgo_unpacker.cpp) diff --git a/tools/build_level/CMakeLists.txt b/tools/build_level/CMakeLists.txt deleted file mode 100644 index 03bca07db3..0000000000 --- a/tools/build_level/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -add_executable(build_level - main.cpp) - -target_link_libraries(build_level common compiler tiny_gltf) \ No newline at end of file diff --git a/tools/build_level/main.cpp b/tools/build_level/main.cpp deleted file mode 100644 index d269c89436..0000000000 --- a/tools/build_level/main.cpp +++ /dev/null @@ -1,3 +0,0 @@ -int main() { - return 0; -} \ No newline at end of file