Initial implementation of custom level tool (#1482)

* wip

* learning about colors

* gltf node stuff working

* cleanup

* support textures

* bvh generation seems reasonable

* tree layout

* frag packer, untested and doesnt do real stripping yet

* temp

* working collide frags

* handle bad inputs better

* clean up

* format

* include

* another include

* reorganize for release build use
This commit is contained in:
water111 2022-06-19 20:44:07 -04:00 committed by GitHub
parent 196c09a232
commit c13934708a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 3184 additions and 259 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,5 @@
#include <functional>
#include <algorithm>
#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<int, MemoryUsageCategory::NUM_CATEGORIES> 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<int, MemoryUsageCategory::NUM_CATEGORIES> 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<std::pair<std::string, int>> 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<float>()(v.x) ^ std::hash<float>()(v.y) ^ std::hash<float>()(v.z) ^
std::hash<float>()(v.s) ^ std::hash<float>()(v.t) ^ std::hash<u16>()(v.color_index);
}
} // namespace tfrag3

View File

@ -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<VertexRun> runs;
std::vector<u32> 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<TimeOfDayColor> colors; // vertex colors (pre-interpolation)
BVH bvh; // the bvh for frustum culling
bool use_strips = true;
struct {
std::vector<PreloadedVertex> vertices; // mesh vertices
@ -397,4 +409,6 @@ struct Level {
std::array<int, MemoryUsageCategory::NUM_CATEGORIES> get_memory_usage() const;
};
void print_memory_usage(const tfrag3::Level& lev, int uncompressed_data_size);
} // namespace tfrag3

View File

@ -0,0 +1,71 @@
#include "pack_helpers.h"
#include <map>
constexpr float kClusterSize = 4096 * 40; // 100 in-game meters
constexpr float kMasterOffset = 12000 * 4096;
std::pair<u64, u16> 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<tfrag3::PreloadedVertex>& vertices) {
u32 next_cluster_idx = 0;
std::map<u64, u32> 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);
}

View File

@ -0,0 +1,6 @@
#pragma once
#include "common/custom_data/Tfrag3Data.h"
void pack_tfrag_vertices(tfrag3::PackedTfragVertices* result,
const std::vector<tfrag3::PreloadedVertex>& vertices);

View File

@ -89,6 +89,14 @@ class Vector {
return result;
}
Vector<T, Size> operator+(const T& other) const {
Vector<T, Size> result;
for (int i = 0; i < Size; i++) {
result[i] = m_data[i] + other;
}
return result;
}
Vector<T, Size>& operator+=(const Vector<T, Size>& other) {
for (int i = 0; i < Size; i++) {
m_data[i] += other[i];
@ -103,6 +111,13 @@ class Vector {
return *this;
}
Vector<T, Size>& operator-=(const T& other) {
for (int i = 0; i < Size; i++) {
m_data[i] -= other;
}
return *this;
}
Vector<T, Size> elementwise_multiply(const Vector<T, Size>& other) const {
Vector<T, Size> 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<T, Size>& other) {
for (int i = 0; i < Size; i++) {
m_data[i] = std::max(m_data[i], other[i]);
}
}
void min_in_place(const Vector<T, Size>& 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<T, Rows> col(int c) const {
Vector<T, Rows> result;
@ -274,6 +319,31 @@ struct Matrix {
return result;
}
template <int OtherCols>
Matrix<T, Rows, OtherCols> operator*(const Matrix<T, Cols, OtherCols>& y) const {
Matrix<T, Rows, OtherCols> 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<T, Rows> operator*(const Vector<T, Cols>& y) const {
Vector<T, Rows> 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];
};

View File

@ -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

View File

@ -44,4 +44,10 @@ RaySphereResult<T> ray_sphere_intersect(const Vector3<T>& 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

36
custom_levels/README.md Normal file
View File

@ -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.

View File

@ -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"
}

Binary file not shown.

View File

@ -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")
)

View File

@ -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<std::pair<std::string, int>> 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) {

View File

@ -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<u32, std::vector<GroupedDraw>>& 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<tfrag3::StripDraw::VisGroup>& grps) {
} // namespace
constexpr float kClusterSize = 4096 * 40; // 100 in-game meters
constexpr float kMasterOffset = 12000 * 4096;
std::pair<u64, u16> 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<tfrag3::PreloadedVertex>& vertices) {
u32 next_cluster_idx = 0;
std::map<u64, u32> 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<tfrag3::PackedTfragVertices::Vertex, tfrag3::PackedTfragVertices::Vertex::hash>
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<level_tools::TextureRemap>& map,
@ -2282,7 +2204,7 @@ void extract_tfrag(const level_tools::DrawableTreeTfrag* tree,
std::vector<tfrag3::PreloadedVertex> 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) {

View File

@ -78,6 +78,7 @@ void Tfrag3::update_load(const std::vector<tfrag3::TFragmentTreeKind>& 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);
}

View File

@ -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;

View File

@ -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++;

View File

@ -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)

View File

@ -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)
)

View File

@ -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.

View File

@ -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)

View File

@ -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
)

View File

@ -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)
)
)
)

View File

@ -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
;;;;;;;;;;;;;;;;;;;;;

View File

@ -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)

View File

@ -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;
}

View File

@ -0,0 +1,28 @@
#pragma once
#include <string>
#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);

View File

@ -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<u8> 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();
}

View File

@ -0,0 +1,140 @@
#pragma once
#include <vector>
#include <string>
#include <array>
#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<u8> bytes;
};
struct DrawableTreeInstanceTie {};
struct DrawableTreeActor {};
struct DrawableTreeAmbient {};
struct DrawableTreeInstanceShrub {};
struct DrawableTreeArray {
std::vector<DrawableTreeTfrag> tfrags;
std::vector<DrawableTreeInstanceTie> ties;
std::vector<DrawableTreeActor> actors; // unused?
std::vector<DrawableTreeCollideFragment> collides;
std::vector<DrawableTreeAmbient> ambients;
std::vector<DrawableTreeInstanceShrub> 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<PatSurface> pat;
// (texture-remap-table (pointer uint64) :offset-assert 52)
// (texture-remap-table-len int32 :offset-assert 56)
std::vector<TextureRemap> texture_remap_table;
// (texture-ids (pointer texture-id) :offset-assert 60)
// (texture-page-count int32 :offset-assert 64)
std::vector<TextureId> 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<VisInfo, 8> vis_infos;
// (actors drawable-inline-array-actor :offset-assert 112)
DrawableInlineArrayActor actors;
// (cameras (array entity-camera) :offset-assert 116)
std::vector<EntityCamera> cameras;
// (nodes (inline-array bsp-node) :offset-assert 120)
std::vector<BspNode> 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<Box8s> 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<u32> actor_birth_order;
// (split-box-indices (pointer uint16) :offset-assert 176)
std::vector<u16> split_box_indices;
// (unk-data-8 uint32 55 :offset-assert 180)
std::vector<u8> save_object_file() const;
};

View File

@ -0,0 +1,4 @@
#include "ResLump.h"
#include "third-party/fmt/core.h"
#include "goalc/data_compiler/DataObjectGenerator.h"

View File

@ -0,0 +1,7 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include "common/common_types.h"

View File

@ -0,0 +1,12 @@
#pragma once
#include <vector>
#include <unordered_map>
#include <string>
#include "common/custom_data/Tfrag3Data.h"
struct TexturePool {
std::unordered_map<std::string, int> textures_by_name;
std::vector<tfrag3::Texture> textures_by_idx;
};

View File

@ -0,0 +1,99 @@
#include <iostream>
#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;
}

17
goalc/build_level/Tfrag.h Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include <string>
#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);

View File

@ -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<std::string> 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<std::string>()};
}
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<std::string>()});
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<std::string>();
// nick
file.nickname = level_json.at("nickname").get<std::string>();
// 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;
}

View File

@ -0,0 +1,7 @@
#pragma once
#include <vector>
#include <string>
bool run_build_level(const std::string& input_file, const std::string& output_file);
std::vector<std::string> get_build_level_deps(const std::string& input_file);

View File

@ -0,0 +1,284 @@
#include <algorithm>
#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<CNode> child_nodes;
std::vector<CollideFace> 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<float>::max());
bbox.maxs.fill(-std::numeric_limits<float>::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<CollideFace>& faces,
int dim,
std::vector<CollideFace>* out0,
std::vector<CollideFace>* 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<CollideFace>& 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

View File

@ -0,0 +1,37 @@
#pragma once
#include <vector>
#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<CollideFace> faces;
};
struct DrawableInlineArrayNode {
std::vector<DrawNode> nodes;
};
struct DrawableInlineArrayCollideFrag {
std::vector<CollideFrag> frags;
};
struct CollideTree {
std::vector<DrawableInlineArrayNode> node_arrays;
DrawableInlineArrayCollideFrag frags;
};
CollideTree construct_collide_bvh(const std::vector<CollideFace>& tris);
} // namespace collide

View File

@ -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;
};

View File

@ -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<PatSurface>& 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<u8>& 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<CollideFragMeshData>& meshes,
const std::vector<size_t>& frag_mesh_locs,
std::vector<size_t>& 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<collide::DrawNode>& nodes,
u32 flag,
const std::vector<size_t>& children,
std::vector<size_t>& 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<size_t> 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<size_t> 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<size_t> array_locs;
array_locs.resize(bvh.node_arrays.size() + 1); // plus one for the frags.
int array_slot = bvh.node_arrays.size();
std::vector<size_t> 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<size_t> 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;
}
}

View File

@ -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;
};

View File

@ -0,0 +1,263 @@
#include <functional>
#include "collide_pack.h"
#include "common/util/Assert.h"
#include "common/log/log.h"
#include "common/util/Timer.h"
struct PackedU16Verts {
math::Vector<s32, 3> base;
std::vector<math::Vector<u16, 3>> 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<math::Vector3f>& 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<s32>();
auto base = result.base.cast<float>();
// 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<u32>()(in.val); }
};
/*!
* pat -> pat index mapping.
* There's a pat "palette" with up 255 unique pats.
*/
struct PatMap {
std::unordered_map<PatSurface, u32, PatSurfaceHash> map;
std::vector<PatSurface> 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<u16, 3> 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<IndexFace> faces; // index in to the two vertex array below:
std::vector<math::Vector3f> vertices_float;
PackedU16Verts vertices_u16;
};
struct Vector3fHash {
size_t operator()(const math::Vector3f& in) const {
return std::hash<float>()(in.x()) ^ std::hash<float>()(in.y()) ^ std::hash<float>()(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<math::Vector3f, u32, Vector3fHash> 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<u8> make_dumb_strip_table(const IndexedFaces& faces) {
std::vector<u8> 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<collide::CollideFrag>& 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.
//

View File

@ -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<u8> packed_data;
u32 strip_data_len;
u32 poly_count;
math::Vector<s32, 3> base_trans_xyz_s32;
u8 vertex_count;
u8 vertex_data_qwc;
u8 total_qwc;
};
struct CollideFragMeshDataArray {
std::vector<CollideFragMeshData> packed_frag_data;
std::vector<PatSurface> pats;
};
CollideFragMeshDataArray pack_collide_frags(const std::vector<collide::CollideFrag>& frag_data);

View File

@ -0,0 +1,220 @@
#include <algorithm>
#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<math::Vector<u8, 4>>& in) {
QuantizedColors result;
std::unordered_map<u64, u32> 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<u8, 4>;
// 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<Node> 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 <typename T>
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<Node*>& 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<Node*> 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<Color>& 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<math::Vector<u8, 4>>& 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<int>() - out.final_colors[out.vtx_to_color[i]].cast<int>();
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;
}

View File

@ -0,0 +1,20 @@
#pragma once
#include <vector>
#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<math::Vector<u8, 4>> final_colors;
std::vector<u32> vtx_to_color;
};
QuantizedColors quantize_colors_dumb(const std::vector<math::Vector<u8, 4>>& in);
QuantizedColors quantize_colors_octree(const std::vector<math::Vector<u8, 4>>& in,
u32 target_count);

View File

@ -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<u32>
*/
template <typename T>
std::vector<u32> index_list_to_u32(const u8* data, u32 num_verts, u32 offset, u32 stride) {
std::vector<u32> 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<Vec3f>
*/
std::vector<math::Vector3f> extract_vec3f(const u8* data, u32 count, u32 stride) {
std::vector<math::Vector3f> 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<math::Vector2f> extract_vec2f(const u8* data, u32 count, u32 stride) {
std::vector<math::Vector2f> 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<math::Vector<u8, 4>> extract_color_from_vec4_u16(const u8* data,
u32 count,
u32 stride) {
std::vector<math::Vector<u8, 4>> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
math::Vector<u16, 4> temp;
memcpy(&temp, data, sizeof(math::Vector<u16, 4>));
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<u32> 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<s8>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
return index_list_to_u32<u8>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_SHORT:
return index_list_to_u32<s16>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
return index_list_to_u32<u16>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_INT:
return index_list_to_u32<s32>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT:
return index_list_to_u32<u32>(data_ptr, count, index_offset, stride);
default:
ASSERT_MSG(false, "unsupported component type");
}
}
struct ExtractedVertices {
std::vector<tfrag3::PreloadedVertex> vtx;
std::vector<math::Vector<u8, 4>> vtx_colors;
std::vector<math::Vector3f> normals;
};
ExtractedVertices gltf_vertices(const tinygltf::Model& model,
const std::map<std::string, int>& attributes,
const math::Matrix4f& w_T_local,
bool get_colors,
bool get_normals,
const std::string& debug_name) {
std::vector<tfrag3::PreloadedVertex> result;
std::vector<math::Vector<u8, 4>> 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<math::Vector3f> 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<double>& in) {
ASSERT(in.size() == 3);
return math::Vector3f{in[0], in[1], in[2]};
}
math::Vector4f vector4f_from_gltf(const std::vector<double>& 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<NodeWithTransform>* 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<NodeWithTransform> flatten_nodes_from_all_scenes(const tinygltf::Model& model) {
std::vector<NodeWithTransform> 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<tfrag3::PreloadedVertex>& vertices_in,
std::vector<tfrag3::PreloadedVertex>& vertices_out,
std::vector<u32>& 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<tfrag3::PreloadedVertex, u32, tfrag3::PreloadedVertex::hash> 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<tfrag3::PreloadedVertex> new_verts;
std::vector<u32> 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<NodeWithTransform>& all_nodes) {
std::vector<math::Vector<u8, 4>> all_vtx_colors;
ASSERT(out.vertices.empty());
std::map<int, tfrag3::StripDraw> 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<u32> prim_indices = gltf_index_buffer(model, prim.indices, out.vertices.size());
ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES, "Unsupported triangle mode");
// extract vertices
auto verts =
gltf_vertices(model, prim.attributes, n.w_T_node, 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<NodeWithTransform>& 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<u32> prim_indices = gltf_index_buffer(model, prim.indices, 0);
ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES, "Unsupported triangle mode");
// extract vertices
auto verts = gltf_vertices(model, prim.attributes, n.w_T_node, false, true, mesh.name);
for (size_t iidx = 0; iidx < prim_indices.size(); iidx += 3) {
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

View File

@ -0,0 +1,34 @@
#pragma once
#include <string>
#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<tfrag3::StripDraw> strip_draws;
std::vector<tfrag3::PreloadedVertex> vertices;
std::vector<math::Vector<u8, 4>> color_palette;
};
struct CollideOutput {
std::vector<CollideFace> faces;
};
struct Output {
TfragOutput tfrag;
CollideOutput collide;
};
void extract(const Input& in, Output& out);
} // namespace gltf_mesh_extract

View File

@ -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());
}

View File

@ -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<u8> generate_v2();
std::vector<u8> 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();

View File

@ -66,6 +66,7 @@ MakeSystem::MakeSystem() {
add_tool<GroupTool>();
add_tool<TextTool>();
add_tool<SubtitleTool>();
add_tool<BuildLevelTool>();
}
/*!

View File

@ -1,5 +1,3 @@
#include "Tools.h"
#include <filesystem>
@ -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));
}

View File

@ -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;
};

View File

@ -1,5 +1,4 @@
add_subdirectory(level_tools)
add_subdirectory(build_level)
add_executable(dgo_unpacker
dgo_unpacker.cpp)

View File

@ -1,4 +0,0 @@
add_executable(build_level
main.cpp)
target_link_libraries(build_level common compiler tiny_gltf)

View File

@ -1,3 +0,0 @@
int main() {
return 0;
}