2026-05-09 05:57:25 -07:00
|
|
|
|
#include "cli_bake.hpp"
|
2026-05-09 11:05:54 -07:00
|
|
|
|
#include "cli_weld.hpp"
|
2026-05-09 05:57:25 -07:00
|
|
|
|
|
|
|
|
|
|
#include "pipeline/wowee_model.hpp"
|
|
|
|
|
|
#include "pipeline/wowee_building.hpp"
|
2026-05-09 11:01:45 -07:00
|
|
|
|
#include "pipeline/wowee_collision.hpp"
|
2026-05-09 05:57:25 -07:00
|
|
|
|
#include "pipeline/wowee_terrain_loader.hpp"
|
|
|
|
|
|
#include "object_placer.hpp"
|
|
|
|
|
|
#include "zone_manifest.hpp"
|
|
|
|
|
|
#include <glm/glm.hpp>
|
|
|
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
#include <chrono>
|
|
|
|
|
|
#include <cmath>
|
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
|
#include <cstring>
|
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
|
#include <fstream>
|
2026-05-09 11:05:54 -07:00
|
|
|
|
#include <limits>
|
2026-05-09 05:57:25 -07:00
|
|
|
|
#include <map>
|
|
|
|
|
|
#include <set>
|
|
|
|
|
|
#include <sstream>
|
|
|
|
|
|
#include <string>
|
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
|
namespace editor {
|
|
|
|
|
|
namespace cli {
|
|
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
|
|
int handleBakeZoneGlb(int& i, int argc, char** argv) {
|
|
|
|
|
|
// Bake every WHM tile in a zone into ONE .glb so the whole
|
|
|
|
|
|
// multi-tile zone opens in three.js / model-viewer with one
|
|
|
|
|
|
// file. Each tile becomes its own mesh+node so they can be
|
|
|
|
|
|
// toggled independently. v1: terrain only — object/WOB
|
|
|
|
|
|
// instances are a follow-up that needs careful per-mesh
|
|
|
|
|
|
// bufferView slicing.
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
std::string outPath;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
std::string manifestPath = zoneDir + "/zone.json";
|
|
|
|
|
|
if (!fs::exists(manifestPath)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-zone-glb: %s has no zone.json\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::editor::ZoneManifest zm;
|
|
|
|
|
|
if (!zm.load(manifestPath)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-zone-glb: failed to parse zone.json\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (outPath.empty()) outPath = zoneDir + "/" + zm.mapName + ".glb";
|
|
|
|
|
|
if (zm.tiles.empty()) {
|
|
|
|
|
|
std::fprintf(stderr, "bake-zone-glb: zone has no tiles\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
constexpr float kTileSize = 533.33333f;
|
|
|
|
|
|
constexpr float kChunkSize = kTileSize / 16.0f;
|
|
|
|
|
|
constexpr float kVertSpacing = kChunkSize / 8.0f;
|
|
|
|
|
|
// Per-tile mesh metadata so we can create one node per tile
|
|
|
|
|
|
// and slice its index range from the shared bufferView.
|
|
|
|
|
|
struct TileMesh {
|
|
|
|
|
|
int tx, ty;
|
|
|
|
|
|
uint32_t vertOff, vertCount;
|
|
|
|
|
|
uint32_t idxOff, idxCount;
|
|
|
|
|
|
};
|
|
|
|
|
|
std::vector<TileMesh> tileMeshes;
|
|
|
|
|
|
std::vector<glm::vec3> positions;
|
|
|
|
|
|
std::vector<uint32_t> indices;
|
|
|
|
|
|
int loadedTiles = 0;
|
|
|
|
|
|
glm::vec3 bMin{1e30f}, bMax{-1e30f};
|
|
|
|
|
|
for (const auto& [tx, ty] : zm.tiles) {
|
|
|
|
|
|
std::string tileBase = zoneDir + "/" + zm.mapName + "_" +
|
|
|
|
|
|
std::to_string(tx) + "_" + std::to_string(ty);
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::exists(tileBase)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-zone-glb: tile (%d,%d) WHM/WOT missing — skipping\n",
|
|
|
|
|
|
tx, ty);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::pipeline::ADTTerrain terrain;
|
|
|
|
|
|
wowee::pipeline::WoweeTerrainLoader::load(tileBase, terrain);
|
|
|
|
|
|
TileMesh tm{tx, ty, 0, 0, 0, 0};
|
|
|
|
|
|
tm.vertOff = static_cast<uint32_t>(positions.size());
|
|
|
|
|
|
tm.idxOff = static_cast<uint32_t>(indices.size());
|
|
|
|
|
|
// Same per-chunk outer-grid layout as --export-whm-glb,
|
|
|
|
|
|
// but accumulated across all tiles so they share one
|
|
|
|
|
|
// global vertex+index pool.
|
|
|
|
|
|
for (int cx = 0; cx < 16; ++cx) {
|
|
|
|
|
|
for (int cy = 0; cy < 16; ++cy) {
|
|
|
|
|
|
const auto& chunk = terrain.getChunk(cx, cy);
|
|
|
|
|
|
if (!chunk.heightMap.isLoaded()) continue;
|
|
|
|
|
|
float chunkBaseX = (32.0f - terrain.coord.y) * kTileSize - cy * kChunkSize;
|
|
|
|
|
|
float chunkBaseY = (32.0f - terrain.coord.x) * kTileSize - cx * kChunkSize;
|
|
|
|
|
|
uint32_t chunkVertOff =
|
|
|
|
|
|
static_cast<uint32_t>(positions.size());
|
|
|
|
|
|
for (int row = 0; row < 9; ++row) {
|
|
|
|
|
|
for (int col = 0; col < 9; ++col) {
|
|
|
|
|
|
glm::vec3 p{
|
|
|
|
|
|
chunkBaseX - row * kVertSpacing,
|
|
|
|
|
|
chunkBaseY - col * kVertSpacing,
|
|
|
|
|
|
chunk.position[2] +
|
|
|
|
|
|
chunk.heightMap.heights[row * 17 + col]
|
|
|
|
|
|
};
|
|
|
|
|
|
positions.push_back(p);
|
|
|
|
|
|
bMin = glm::min(bMin, p);
|
|
|
|
|
|
bMax = glm::max(bMax, p);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
bool isHoleChunk = (chunk.holes != 0);
|
|
|
|
|
|
for (int row = 0; row < 8; ++row) {
|
|
|
|
|
|
for (int col = 0; col < 8; ++col) {
|
|
|
|
|
|
if (isHoleChunk) {
|
|
|
|
|
|
int hx = col / 2, hy = row / 2;
|
|
|
|
|
|
if (chunk.holes & (1 << (hy * 4 + hx))) continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto idx = [&](int r, int c) {
|
|
|
|
|
|
return chunkVertOff + r * 9 + c;
|
|
|
|
|
|
};
|
|
|
|
|
|
indices.push_back(idx(row, col));
|
|
|
|
|
|
indices.push_back(idx(row, col + 1));
|
|
|
|
|
|
indices.push_back(idx(row + 1, col + 1));
|
|
|
|
|
|
indices.push_back(idx(row, col));
|
|
|
|
|
|
indices.push_back(idx(row + 1, col + 1));
|
|
|
|
|
|
indices.push_back(idx(row + 1, col));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
tm.vertCount = static_cast<uint32_t>(positions.size()) - tm.vertOff;
|
|
|
|
|
|
tm.idxCount = static_cast<uint32_t>(indices.size()) - tm.idxOff;
|
|
|
|
|
|
if (tm.vertCount > 0 && tm.idxCount > 0) {
|
|
|
|
|
|
tileMeshes.push_back(tm);
|
|
|
|
|
|
loadedTiles++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (loadedTiles == 0) {
|
|
|
|
|
|
std::fprintf(stderr, "bake-zone-glb: no tiles loaded\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Pack BIN chunk same way as --export-whm-glb (positions +
|
|
|
|
|
|
// synthetic +Z normals + indices). Per-tile accessors slice
|
|
|
|
|
|
// their index region via byteOffset.
|
|
|
|
|
|
const uint32_t totalV = static_cast<uint32_t>(positions.size());
|
|
|
|
|
|
const uint32_t totalI = static_cast<uint32_t>(indices.size());
|
|
|
|
|
|
const uint32_t posOff = 0;
|
|
|
|
|
|
const uint32_t nrmOff = posOff + totalV * 12;
|
|
|
|
|
|
const uint32_t idxOff = nrmOff + totalV * 12;
|
|
|
|
|
|
const uint32_t binSize = idxOff + totalI * 4;
|
|
|
|
|
|
std::vector<uint8_t> bin(binSize);
|
|
|
|
|
|
for (uint32_t v = 0; v < totalV; ++v) {
|
|
|
|
|
|
std::memcpy(&bin[posOff + v * 12 + 0], &positions[v].x, 4);
|
|
|
|
|
|
std::memcpy(&bin[posOff + v * 12 + 4], &positions[v].y, 4);
|
|
|
|
|
|
std::memcpy(&bin[posOff + v * 12 + 8], &positions[v].z, 4);
|
|
|
|
|
|
float nx = 0, ny = 0, nz = 1;
|
|
|
|
|
|
std::memcpy(&bin[nrmOff + v * 12 + 0], &nx, 4);
|
|
|
|
|
|
std::memcpy(&bin[nrmOff + v * 12 + 4], &ny, 4);
|
|
|
|
|
|
std::memcpy(&bin[nrmOff + v * 12 + 8], &nz, 4);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::memcpy(&bin[idxOff], indices.data(), totalI * 4);
|
|
|
|
|
|
// Build glTF JSON. One mesh + one node per tile so they can
|
|
|
|
|
|
// be toggled in viewers.
|
|
|
|
|
|
nlohmann::json gj;
|
|
|
|
|
|
gj["asset"] = {{"version", "2.0"},
|
|
|
|
|
|
{"generator", "wowee_editor --bake-zone-glb"}};
|
|
|
|
|
|
gj["scene"] = 0;
|
|
|
|
|
|
gj["buffers"] = nlohmann::json::array({nlohmann::json{
|
|
|
|
|
|
{"byteLength", binSize}
|
|
|
|
|
|
}});
|
|
|
|
|
|
// Three shared bufferViews — pos, nrm, idx — sliced into
|
|
|
|
|
|
// per-tile primitives via byteOffset on the index accessor.
|
|
|
|
|
|
nlohmann::json bufferViews = nlohmann::json::array();
|
|
|
|
|
|
bufferViews.push_back({{"buffer", 0}, {"byteOffset", posOff},
|
|
|
|
|
|
{"byteLength", totalV * 12}, {"target", 34962}});
|
|
|
|
|
|
bufferViews.push_back({{"buffer", 0}, {"byteOffset", nrmOff},
|
|
|
|
|
|
{"byteLength", totalV * 12}, {"target", 34962}});
|
|
|
|
|
|
bufferViews.push_back({{"buffer", 0}, {"byteOffset", idxOff},
|
|
|
|
|
|
{"byteLength", totalI * 4}, {"target", 34963}});
|
|
|
|
|
|
gj["bufferViews"] = bufferViews;
|
|
|
|
|
|
// Shared position+normal accessors (covering the full pool;
|
|
|
|
|
|
// primitives reference them, the index accessor does the
|
|
|
|
|
|
// per-tile slicing).
|
|
|
|
|
|
nlohmann::json accessors = nlohmann::json::array();
|
|
|
|
|
|
accessors.push_back({
|
|
|
|
|
|
{"bufferView", 0}, {"componentType", 5126},
|
|
|
|
|
|
{"count", totalV}, {"type", "VEC3"},
|
|
|
|
|
|
{"min", {bMin.x, bMin.y, bMin.z}},
|
|
|
|
|
|
{"max", {bMax.x, bMax.y, bMax.z}}
|
|
|
|
|
|
});
|
|
|
|
|
|
accessors.push_back({{"bufferView", 1}, {"componentType", 5126},
|
|
|
|
|
|
{"count", totalV}, {"type", "VEC3"}});
|
|
|
|
|
|
// Per-tile mesh + node + indices accessor.
|
|
|
|
|
|
nlohmann::json meshes = nlohmann::json::array();
|
|
|
|
|
|
nlohmann::json nodes = nlohmann::json::array();
|
|
|
|
|
|
nlohmann::json sceneNodes = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& tm : tileMeshes) {
|
|
|
|
|
|
uint32_t accIdx = static_cast<uint32_t>(accessors.size());
|
|
|
|
|
|
accessors.push_back({
|
|
|
|
|
|
{"bufferView", 2},
|
|
|
|
|
|
{"byteOffset", tm.idxOff * 4},
|
|
|
|
|
|
{"componentType", 5125},
|
|
|
|
|
|
{"count", tm.idxCount},
|
|
|
|
|
|
{"type", "SCALAR"}
|
|
|
|
|
|
});
|
|
|
|
|
|
uint32_t meshIdx = static_cast<uint32_t>(meshes.size());
|
|
|
|
|
|
meshes.push_back({
|
|
|
|
|
|
{"primitives", nlohmann::json::array({nlohmann::json{
|
|
|
|
|
|
{"attributes", {{"POSITION", 0}, {"NORMAL", 1}}},
|
|
|
|
|
|
{"indices", accIdx}, {"mode", 4}
|
|
|
|
|
|
}})}
|
|
|
|
|
|
});
|
|
|
|
|
|
std::string nodeName = "tile_" + std::to_string(tm.tx) +
|
|
|
|
|
|
"_" + std::to_string(tm.ty);
|
|
|
|
|
|
uint32_t nodeIdx = static_cast<uint32_t>(nodes.size());
|
|
|
|
|
|
nodes.push_back({{"name", nodeName}, {"mesh", meshIdx}});
|
|
|
|
|
|
sceneNodes.push_back(nodeIdx);
|
|
|
|
|
|
}
|
|
|
|
|
|
gj["accessors"] = accessors;
|
|
|
|
|
|
gj["meshes"] = meshes;
|
|
|
|
|
|
gj["nodes"] = nodes;
|
|
|
|
|
|
gj["scenes"] = nlohmann::json::array({nlohmann::json{
|
|
|
|
|
|
{"nodes", sceneNodes}
|
|
|
|
|
|
}});
|
|
|
|
|
|
std::string jsonStr = gj.dump();
|
|
|
|
|
|
while (jsonStr.size() % 4 != 0) jsonStr += ' ';
|
|
|
|
|
|
uint32_t jsonLen = static_cast<uint32_t>(jsonStr.size());
|
|
|
|
|
|
uint32_t binLen = binSize;
|
|
|
|
|
|
uint32_t totalLen = 12 + 8 + jsonLen + 8 + binLen;
|
|
|
|
|
|
std::ofstream out(outPath, std::ios::binary);
|
|
|
|
|
|
if (!out) {
|
|
|
|
|
|
std::fprintf(stderr, "Failed to open output: %s\n", outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
uint32_t magic = 0x46546C67, version = 2;
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&magic), 4);
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&version), 4);
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&totalLen), 4);
|
|
|
|
|
|
uint32_t jsonChunkType = 0x4E4F534A;
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&jsonLen), 4);
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&jsonChunkType), 4);
|
|
|
|
|
|
out.write(jsonStr.data(), jsonLen);
|
|
|
|
|
|
uint32_t binChunkType = 0x004E4942;
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&binLen), 4);
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&binChunkType), 4);
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(bin.data()), binLen);
|
|
|
|
|
|
out.close();
|
|
|
|
|
|
std::printf("Baked %s -> %s\n", zoneDir.c_str(), outPath.c_str());
|
|
|
|
|
|
std::printf(" %d tile(s), %u verts, %u tris, %zu meshes, %u-byte BIN\n",
|
|
|
|
|
|
loadedTiles, totalV, totalI / 3,
|
|
|
|
|
|
meshes.size(), binLen);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int handleBakeZoneStl(int& i, int argc, char** argv) {
|
|
|
|
|
|
// STL counterpart to --bake-zone-glb. Designers can 3D-print a
|
|
|
|
|
|
// miniature of an entire multi-tile zone in one slicer load —
|
|
|
|
|
|
// useful for tabletop RPG props or a physical reference of a
|
|
|
|
|
|
// playtest area.
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
std::string outPath;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
std::string manifestPath = zoneDir + "/zone.json";
|
|
|
|
|
|
if (!fs::exists(manifestPath)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-zone-stl: %s has no zone.json\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::editor::ZoneManifest zm;
|
|
|
|
|
|
if (!zm.load(manifestPath)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-zone-stl: failed to parse zone.json\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (outPath.empty()) outPath = zoneDir + "/" + zm.mapName + ".stl";
|
|
|
|
|
|
if (zm.tiles.empty()) {
|
|
|
|
|
|
std::fprintf(stderr, "bake-zone-stl: zone has no tiles\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::ofstream out(outPath);
|
|
|
|
|
|
if (!out) {
|
|
|
|
|
|
std::fprintf(stderr, "bake-zone-stl: cannot write %s\n", outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
constexpr float kTileSize = 533.33333f;
|
|
|
|
|
|
constexpr float kChunkSize = kTileSize / 16.0f;
|
|
|
|
|
|
constexpr float kVertSpacing = kChunkSize / 8.0f;
|
|
|
|
|
|
// Solid name sanitized to alphanum + underscore.
|
|
|
|
|
|
std::string solidName = zm.mapName;
|
|
|
|
|
|
for (auto& c : solidName) {
|
|
|
|
|
|
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
|
|
|
|
(c >= '0' && c <= '9') || c == '_')) c = '_';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (solidName.empty()) solidName = "wowee_zone";
|
|
|
|
|
|
out << "solid " << solidName << "\n";
|
|
|
|
|
|
int loadedTiles = 0, holesSkipped = 0;
|
|
|
|
|
|
uint64_t triCount = 0;
|
|
|
|
|
|
// For each tile, generate the same 9x9 outer-grid mesh and
|
|
|
|
|
|
// emit per-triangle facets directly (STL has no shared
|
|
|
|
|
|
// vertex pool — each triangle stands alone). Compute face
|
|
|
|
|
|
// normal from cross product (slicers use it for orientation).
|
|
|
|
|
|
for (const auto& [tx, ty] : zm.tiles) {
|
|
|
|
|
|
std::string tileBase = zoneDir + "/" + zm.mapName + "_" +
|
|
|
|
|
|
std::to_string(tx) + "_" + std::to_string(ty);
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::exists(tileBase)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-zone-stl: tile (%d, %d) WHM/WOT missing — skipping\n",
|
|
|
|
|
|
tx, ty);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::pipeline::ADTTerrain terrain;
|
|
|
|
|
|
wowee::pipeline::WoweeTerrainLoader::load(tileBase, terrain);
|
|
|
|
|
|
loadedTiles++;
|
|
|
|
|
|
for (int cx = 0; cx < 16; ++cx) {
|
|
|
|
|
|
for (int cy = 0; cy < 16; ++cy) {
|
|
|
|
|
|
const auto& chunk = terrain.getChunk(cx, cy);
|
|
|
|
|
|
if (!chunk.heightMap.isLoaded()) continue;
|
|
|
|
|
|
float chunkBaseX = (32.0f - terrain.coord.y) * kTileSize - cy * kChunkSize;
|
|
|
|
|
|
float chunkBaseY = (32.0f - terrain.coord.x) * kTileSize - cx * kChunkSize;
|
|
|
|
|
|
// Pre-compute the 9x9 vertex grid for this chunk.
|
|
|
|
|
|
glm::vec3 V[9][9];
|
|
|
|
|
|
for (int row = 0; row < 9; ++row) {
|
|
|
|
|
|
for (int col = 0; col < 9; ++col) {
|
|
|
|
|
|
V[row][col] = {
|
|
|
|
|
|
chunkBaseX - row * kVertSpacing,
|
|
|
|
|
|
chunkBaseY - col * kVertSpacing,
|
|
|
|
|
|
chunk.position[2] +
|
|
|
|
|
|
chunk.heightMap.heights[row * 17 + col]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
bool isHoleChunk = (chunk.holes != 0);
|
|
|
|
|
|
auto emitTri = [&](const glm::vec3& a,
|
|
|
|
|
|
const glm::vec3& b,
|
|
|
|
|
|
const glm::vec3& c) {
|
|
|
|
|
|
glm::vec3 e1 = b - a, e2 = c - a;
|
|
|
|
|
|
glm::vec3 n = glm::cross(e1, e2);
|
|
|
|
|
|
float len = glm::length(n);
|
|
|
|
|
|
if (len > 1e-12f) n /= len;
|
|
|
|
|
|
else n = {0, 0, 1};
|
|
|
|
|
|
out << " facet normal " << n.x << " " << n.y << " " << n.z << "\n"
|
|
|
|
|
|
<< " outer loop\n"
|
|
|
|
|
|
<< " vertex " << a.x << " " << a.y << " " << a.z << "\n"
|
|
|
|
|
|
<< " vertex " << b.x << " " << b.y << " " << b.z << "\n"
|
|
|
|
|
|
<< " vertex " << c.x << " " << c.y << " " << c.z << "\n"
|
|
|
|
|
|
<< " endloop\n"
|
|
|
|
|
|
<< " endfacet\n";
|
|
|
|
|
|
triCount++;
|
|
|
|
|
|
};
|
|
|
|
|
|
for (int row = 0; row < 8; ++row) {
|
|
|
|
|
|
for (int col = 0; col < 8; ++col) {
|
|
|
|
|
|
if (isHoleChunk) {
|
|
|
|
|
|
int hx = col / 2, hy = row / 2;
|
|
|
|
|
|
if (chunk.holes & (1 << (hy * 4 + hx))) {
|
|
|
|
|
|
holesSkipped++;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
emitTri(V[row][col], V[row][col + 1], V[row + 1][col + 1]);
|
|
|
|
|
|
emitTri(V[row][col], V[row + 1][col + 1], V[row + 1][col]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out << "endsolid " << solidName << "\n";
|
|
|
|
|
|
out.close();
|
|
|
|
|
|
if (loadedTiles == 0) {
|
|
|
|
|
|
std::fprintf(stderr, "bake-zone-stl: no tiles loaded\n");
|
|
|
|
|
|
std::filesystem::remove(outPath);
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Baked %s -> %s\n", zoneDir.c_str(), outPath.c_str());
|
|
|
|
|
|
std::printf(" %d tile(s), %llu facets, %d hole quads skipped\n",
|
|
|
|
|
|
loadedTiles, static_cast<unsigned long long>(triCount),
|
|
|
|
|
|
holesSkipped);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int handleBakeZoneObj(int& i, int argc, char** argv) {
|
|
|
|
|
|
// OBJ companion to --bake-zone-glb / --bake-zone-stl. Same
|
|
|
|
|
|
// multi-tile WHM aggregation, but as Wavefront OBJ — opens
|
|
|
|
|
|
// directly in Blender / MeshLab / 3DS Max for hand-editing.
|
|
|
|
|
|
// Each tile becomes its own 'g' block so designers can hide
|
|
|
|
|
|
// tiles independently.
|
|
|
|
|
|
std::string zoneDir = argv[++i];
|
|
|
|
|
|
std::string outPath;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
std::string manifestPath = zoneDir + "/zone.json";
|
|
|
|
|
|
if (!fs::exists(manifestPath)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-zone-obj: %s has no zone.json\n", zoneDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::editor::ZoneManifest zm;
|
|
|
|
|
|
if (!zm.load(manifestPath)) {
|
|
|
|
|
|
std::fprintf(stderr, "bake-zone-obj: parse failed\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (outPath.empty()) outPath = zoneDir + "/" + zm.mapName + ".obj";
|
|
|
|
|
|
if (zm.tiles.empty()) {
|
|
|
|
|
|
std::fprintf(stderr, "bake-zone-obj: zone has no tiles\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::ofstream out(outPath);
|
|
|
|
|
|
if (!out) {
|
|
|
|
|
|
std::fprintf(stderr, "bake-zone-obj: cannot write %s\n", outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
constexpr float kTileSize = 533.33333f;
|
|
|
|
|
|
constexpr float kChunkSize = kTileSize / 16.0f;
|
|
|
|
|
|
constexpr float kVertSpacing = kChunkSize / 8.0f;
|
|
|
|
|
|
out << "# Wavefront OBJ generated by wowee_editor --bake-zone-obj\n";
|
|
|
|
|
|
out << "# Zone: " << zm.mapName << " (" << zm.tiles.size()
|
|
|
|
|
|
<< " tiles)\n";
|
|
|
|
|
|
out << "o " << zm.mapName << "\n";
|
|
|
|
|
|
// OBJ uses a single global vertex pool with per-tile g-blocks
|
|
|
|
|
|
// and per-tile face index offsetting. We accumulate per-tile
|
|
|
|
|
|
// vertex blocks first (so face indices know their offsets),
|
|
|
|
|
|
// then per-tile face blocks at the end.
|
|
|
|
|
|
// Layout: emit ALL verts first (organized by tile, in order),
|
|
|
|
|
|
// then emit ALL face blocks. OBJ requires verts before faces
|
|
|
|
|
|
// that reference them.
|
|
|
|
|
|
int loadedTiles = 0;
|
|
|
|
|
|
int totalVerts = 0;
|
|
|
|
|
|
// Per-tile bookkeeping: vertex base index (1-based for OBJ)
|
|
|
|
|
|
// and which faces reference it.
|
|
|
|
|
|
struct TileMeta {
|
|
|
|
|
|
int tx, ty;
|
|
|
|
|
|
uint32_t vertBase; // 1-based OBJ index of first vert
|
|
|
|
|
|
uint32_t vertCount;
|
|
|
|
|
|
std::vector<uint32_t> faceI0, faceI1, faceI2; // local indices
|
|
|
|
|
|
};
|
|
|
|
|
|
std::vector<TileMeta> tiles;
|
|
|
|
|
|
for (const auto& [tx, ty] : zm.tiles) {
|
|
|
|
|
|
std::string tileBase = zoneDir + "/" + zm.mapName + "_" +
|
|
|
|
|
|
std::to_string(tx) + "_" + std::to_string(ty);
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::exists(tileBase)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-zone-obj: tile (%d, %d) WHM/WOT missing — skipping\n",
|
|
|
|
|
|
tx, ty);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::pipeline::ADTTerrain terrain;
|
|
|
|
|
|
wowee::pipeline::WoweeTerrainLoader::load(tileBase, terrain);
|
|
|
|
|
|
TileMeta tm{tx, ty, static_cast<uint32_t>(totalVerts + 1), 0, {}, {}, {}};
|
|
|
|
|
|
// Walk chunks; emit verts to file as we go (so we don't
|
|
|
|
|
|
// hold a giant vector in memory). Track local indices for
|
|
|
|
|
|
// face emission afterwards.
|
|
|
|
|
|
uint32_t tileLocalIdx = 0;
|
|
|
|
|
|
for (int cx = 0; cx < 16; ++cx) {
|
|
|
|
|
|
for (int cy = 0; cy < 16; ++cy) {
|
|
|
|
|
|
const auto& chunk = terrain.getChunk(cx, cy);
|
|
|
|
|
|
if (!chunk.heightMap.isLoaded()) continue;
|
|
|
|
|
|
float chunkBaseX = (32.0f - terrain.coord.y) * kTileSize - cy * kChunkSize;
|
|
|
|
|
|
float chunkBaseY = (32.0f - terrain.coord.x) * kTileSize - cx * kChunkSize;
|
|
|
|
|
|
uint32_t chunkBaseLocal = tileLocalIdx;
|
|
|
|
|
|
for (int row = 0; row < 9; ++row) {
|
|
|
|
|
|
for (int col = 0; col < 9; ++col) {
|
|
|
|
|
|
float x = chunkBaseX - row * kVertSpacing;
|
|
|
|
|
|
float y = chunkBaseY - col * kVertSpacing;
|
|
|
|
|
|
float z = chunk.position[2] +
|
|
|
|
|
|
chunk.heightMap.heights[row * 17 + col];
|
|
|
|
|
|
out << "v " << x << " " << y << " " << z << "\n";
|
|
|
|
|
|
tileLocalIdx++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
bool isHoleChunk = (chunk.holes != 0);
|
|
|
|
|
|
for (int row = 0; row < 8; ++row) {
|
|
|
|
|
|
for (int col = 0; col < 8; ++col) {
|
|
|
|
|
|
if (isHoleChunk) {
|
|
|
|
|
|
int hx = col / 2, hy = row / 2;
|
|
|
|
|
|
if (chunk.holes & (1 << (hy * 4 + hx))) continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto idx = [&](int r, int c) {
|
|
|
|
|
|
return chunkBaseLocal + r * 9 + c;
|
|
|
|
|
|
};
|
|
|
|
|
|
tm.faceI0.push_back(idx(row, col));
|
|
|
|
|
|
tm.faceI1.push_back(idx(row, col + 1));
|
|
|
|
|
|
tm.faceI2.push_back(idx(row + 1, col + 1));
|
|
|
|
|
|
tm.faceI0.push_back(idx(row, col));
|
|
|
|
|
|
tm.faceI1.push_back(idx(row + 1, col + 1));
|
|
|
|
|
|
tm.faceI2.push_back(idx(row + 1, col));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
tm.vertCount = tileLocalIdx;
|
|
|
|
|
|
totalVerts += tm.vertCount;
|
|
|
|
|
|
if (tm.vertCount > 0) {
|
|
|
|
|
|
tiles.push_back(std::move(tm));
|
|
|
|
|
|
loadedTiles++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Now emit per-tile face groups (after all verts are written).
|
|
|
|
|
|
uint64_t totalFaces = 0;
|
|
|
|
|
|
for (const auto& tm : tiles) {
|
|
|
|
|
|
out << "g tile_" << tm.tx << "_" << tm.ty << "\n";
|
|
|
|
|
|
for (size_t k = 0; k < tm.faceI0.size(); ++k) {
|
|
|
|
|
|
uint32_t a = tm.faceI0[k] + tm.vertBase;
|
|
|
|
|
|
uint32_t b = tm.faceI1[k] + tm.vertBase;
|
|
|
|
|
|
uint32_t c = tm.faceI2[k] + tm.vertBase;
|
|
|
|
|
|
out << "f " << a << " " << b << " " << c << "\n";
|
|
|
|
|
|
totalFaces++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out.close();
|
|
|
|
|
|
if (loadedTiles == 0) {
|
|
|
|
|
|
std::fprintf(stderr, "bake-zone-obj: no tiles loaded\n");
|
|
|
|
|
|
std::filesystem::remove(outPath);
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Baked %s -> %s\n", zoneDir.c_str(), outPath.c_str());
|
|
|
|
|
|
std::printf(" %d tile(s), %d verts, %llu tris\n",
|
|
|
|
|
|
loadedTiles, totalVerts,
|
|
|
|
|
|
static_cast<unsigned long long>(totalFaces));
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int handleBakeProjectObj(int& i, int argc, char** argv) {
|
|
|
|
|
|
// Project-level OBJ bake: every zone in <projectDir> gets
|
|
|
|
|
|
// emitted into one giant OBJ with one 'g zone_NAME' block
|
|
|
|
|
|
// per zone. Useful for previewing an entire project's terrain
|
|
|
|
|
|
// in MeshLab/Blender at once, or for printing the whole map.
|
|
|
|
|
|
std::string projectDir = argv[++i];
|
|
|
|
|
|
std::string outPath;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-project-obj: %s is not a directory\n",
|
|
|
|
|
|
projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (outPath.empty()) outPath = projectDir + "/project.obj";
|
|
|
|
|
|
std::vector<std::string> zoneDirs;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
|
|
|
|
|
if (!entry.is_directory()) continue;
|
|
|
|
|
|
if (!fs::exists(entry.path() / "zone.json")) continue;
|
|
|
|
|
|
zoneDirs.push_back(entry.path().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(zoneDirs.begin(), zoneDirs.end());
|
|
|
|
|
|
if (zoneDirs.empty()) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-project-obj: no zones found in %s\n",
|
|
|
|
|
|
projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::ofstream out(outPath);
|
|
|
|
|
|
if (!out) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-project-obj: cannot write %s\n", outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
constexpr float kTileSize = 533.33333f;
|
|
|
|
|
|
constexpr float kChunkSize = kTileSize / 16.0f;
|
|
|
|
|
|
constexpr float kVertSpacing = kChunkSize / 8.0f;
|
|
|
|
|
|
out << "# Wavefront OBJ generated by wowee_editor --bake-project-obj\n";
|
|
|
|
|
|
out << "# Project: " << projectDir << " (" << zoneDirs.size() << " zones)\n";
|
|
|
|
|
|
// Single global vertex pool. Per-zone we accumulate verts then
|
|
|
|
|
|
// emit faces; same shape as --bake-zone-obj.
|
|
|
|
|
|
int totalZones = 0, totalTiles = 0;
|
|
|
|
|
|
int totalVerts = 0;
|
|
|
|
|
|
uint64_t totalFaces = 0;
|
|
|
|
|
|
struct Pending {
|
|
|
|
|
|
std::string zoneName;
|
|
|
|
|
|
uint32_t vertBase; // 1-based OBJ index
|
|
|
|
|
|
std::vector<uint32_t> faceI0, faceI1, faceI2;
|
|
|
|
|
|
};
|
|
|
|
|
|
std::vector<Pending> queues;
|
|
|
|
|
|
for (const auto& zoneDir : zoneDirs) {
|
|
|
|
|
|
wowee::editor::ZoneManifest zm;
|
|
|
|
|
|
if (!zm.load(zoneDir + "/zone.json")) continue;
|
|
|
|
|
|
Pending pq;
|
|
|
|
|
|
pq.zoneName = zm.mapName;
|
|
|
|
|
|
pq.vertBase = static_cast<uint32_t>(totalVerts + 1);
|
|
|
|
|
|
int zoneTiles = 0;
|
|
|
|
|
|
uint32_t zoneLocalIdx = 0;
|
|
|
|
|
|
for (const auto& [tx, ty] : zm.tiles) {
|
|
|
|
|
|
std::string tileBase = zoneDir + "/" + zm.mapName + "_" +
|
|
|
|
|
|
std::to_string(tx) + "_" +
|
|
|
|
|
|
std::to_string(ty);
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::exists(tileBase)) continue;
|
|
|
|
|
|
wowee::pipeline::ADTTerrain terrain;
|
|
|
|
|
|
wowee::pipeline::WoweeTerrainLoader::load(tileBase, terrain);
|
|
|
|
|
|
zoneTiles++;
|
|
|
|
|
|
for (int cx = 0; cx < 16; ++cx) {
|
|
|
|
|
|
for (int cy = 0; cy < 16; ++cy) {
|
|
|
|
|
|
const auto& chunk = terrain.getChunk(cx, cy);
|
|
|
|
|
|
if (!chunk.heightMap.isLoaded()) continue;
|
|
|
|
|
|
float chunkBaseX = (32.0f - terrain.coord.y) * kTileSize - cy * kChunkSize;
|
|
|
|
|
|
float chunkBaseY = (32.0f - terrain.coord.x) * kTileSize - cx * kChunkSize;
|
|
|
|
|
|
uint32_t chunkBaseLocal = zoneLocalIdx;
|
|
|
|
|
|
for (int row = 0; row < 9; ++row) {
|
|
|
|
|
|
for (int col = 0; col < 9; ++col) {
|
|
|
|
|
|
float x = chunkBaseX - row * kVertSpacing;
|
|
|
|
|
|
float y = chunkBaseY - col * kVertSpacing;
|
|
|
|
|
|
float z = chunk.position[2] +
|
|
|
|
|
|
chunk.heightMap.heights[row * 17 + col];
|
|
|
|
|
|
out << "v " << x << " " << y << " " << z << "\n";
|
|
|
|
|
|
zoneLocalIdx++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
bool isHoleChunk = (chunk.holes != 0);
|
|
|
|
|
|
for (int row = 0; row < 8; ++row) {
|
|
|
|
|
|
for (int col = 0; col < 8; ++col) {
|
|
|
|
|
|
if (isHoleChunk) {
|
|
|
|
|
|
int hx = col / 2, hy = row / 2;
|
|
|
|
|
|
if (chunk.holes & (1 << (hy * 4 + hx))) continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto idx = [&](int r, int c) {
|
|
|
|
|
|
return chunkBaseLocal + r * 9 + c;
|
|
|
|
|
|
};
|
|
|
|
|
|
pq.faceI0.push_back(idx(row, col));
|
|
|
|
|
|
pq.faceI1.push_back(idx(row, col + 1));
|
|
|
|
|
|
pq.faceI2.push_back(idx(row + 1, col + 1));
|
|
|
|
|
|
pq.faceI0.push_back(idx(row, col));
|
|
|
|
|
|
pq.faceI1.push_back(idx(row + 1, col + 1));
|
|
|
|
|
|
pq.faceI2.push_back(idx(row + 1, col));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (zoneLocalIdx == 0) continue;
|
|
|
|
|
|
totalVerts += zoneLocalIdx;
|
|
|
|
|
|
totalTiles += zoneTiles;
|
|
|
|
|
|
totalZones++;
|
|
|
|
|
|
queues.push_back(std::move(pq));
|
|
|
|
|
|
}
|
|
|
|
|
|
// After all verts written, emit faces grouped by zone.
|
|
|
|
|
|
for (const auto& pq : queues) {
|
|
|
|
|
|
out << "g zone_" << pq.zoneName << "\n";
|
|
|
|
|
|
for (size_t k = 0; k < pq.faceI0.size(); ++k) {
|
|
|
|
|
|
out << "f " << (pq.faceI0[k] + pq.vertBase) << " "
|
|
|
|
|
|
<< (pq.faceI1[k] + pq.vertBase) << " "
|
|
|
|
|
|
<< (pq.faceI2[k] + pq.vertBase) << "\n";
|
|
|
|
|
|
totalFaces++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out.close();
|
|
|
|
|
|
std::printf("Baked %s -> %s\n", projectDir.c_str(), outPath.c_str());
|
|
|
|
|
|
std::printf(" %d zone(s), %d tiles, %d verts, %llu tris\n",
|
|
|
|
|
|
totalZones, totalTiles, totalVerts,
|
|
|
|
|
|
static_cast<unsigned long long>(totalFaces));
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int handleBakeProjectStlOrGlb(int& i, int argc, char** argv) {
|
|
|
|
|
|
// STL + glTF project bakes share the per-zone walking logic
|
|
|
|
|
|
// with --bake-project-obj. Only the output emission differs:
|
|
|
|
|
|
// STL → per-triangle 'facet normal'+'outer loop'+vertex×3
|
|
|
|
|
|
// GLB → packed BIN chunk + JSON describing per-zone meshes
|
|
|
|
|
|
// Coords match across all three exporters so an .obj/.stl/
|
|
|
|
|
|
// .glb of the same source line up spatially when overlaid.
|
|
|
|
|
|
bool isStl = (std::strcmp(argv[i], "--bake-project-stl") == 0);
|
|
|
|
|
|
const char* cmdName = isStl ? "bake-project-stl" : "bake-project-glb";
|
|
|
|
|
|
std::string projectDir = argv[++i];
|
|
|
|
|
|
std::string outPath;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i];
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"%s: %s is not a directory\n", cmdName, projectDir.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (outPath.empty()) {
|
|
|
|
|
|
outPath = projectDir + "/project." + (isStl ? "stl" : "glb");
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> zoneDirs;
|
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
|
|
|
|
|
if (!entry.is_directory()) continue;
|
|
|
|
|
|
if (!fs::exists(entry.path() / "zone.json")) continue;
|
|
|
|
|
|
zoneDirs.push_back(entry.path().string());
|
|
|
|
|
|
}
|
|
|
|
|
|
std::sort(zoneDirs.begin(), zoneDirs.end());
|
|
|
|
|
|
if (zoneDirs.empty()) {
|
|
|
|
|
|
std::fprintf(stderr, "%s: no zones found\n", cmdName);
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
constexpr float kTileSize = 533.33333f;
|
|
|
|
|
|
constexpr float kChunkSize = kTileSize / 16.0f;
|
|
|
|
|
|
constexpr float kVertSpacing = kChunkSize / 8.0f;
|
|
|
|
|
|
// Common pass: collect per-zone vertex+index pools. STL emits
|
|
|
|
|
|
// per-triangle facets directly; GLB packs everything into BIN.
|
|
|
|
|
|
struct ZonePool {
|
|
|
|
|
|
std::string name;
|
|
|
|
|
|
std::vector<glm::vec3> verts;
|
|
|
|
|
|
std::vector<uint32_t> indices;
|
|
|
|
|
|
};
|
|
|
|
|
|
std::vector<ZonePool> zones;
|
|
|
|
|
|
int totalZones = 0, totalTiles = 0;
|
|
|
|
|
|
glm::vec3 bMin{1e30f}, bMax{-1e30f};
|
|
|
|
|
|
for (const auto& zoneDir : zoneDirs) {
|
|
|
|
|
|
wowee::editor::ZoneManifest zm;
|
|
|
|
|
|
if (!zm.load(zoneDir + "/zone.json")) continue;
|
|
|
|
|
|
ZonePool zp;
|
|
|
|
|
|
zp.name = zm.mapName;
|
|
|
|
|
|
int zoneTiles = 0;
|
|
|
|
|
|
for (const auto& [tx, ty] : zm.tiles) {
|
|
|
|
|
|
std::string tileBase = zoneDir + "/" + zm.mapName + "_" +
|
|
|
|
|
|
std::to_string(tx) + "_" + std::to_string(ty);
|
|
|
|
|
|
if (!wowee::pipeline::WoweeTerrainLoader::exists(tileBase)) continue;
|
|
|
|
|
|
wowee::pipeline::ADTTerrain terrain;
|
|
|
|
|
|
wowee::pipeline::WoweeTerrainLoader::load(tileBase, terrain);
|
|
|
|
|
|
zoneTiles++;
|
|
|
|
|
|
for (int cx = 0; cx < 16; ++cx) {
|
|
|
|
|
|
for (int cy = 0; cy < 16; ++cy) {
|
|
|
|
|
|
const auto& chunk = terrain.getChunk(cx, cy);
|
|
|
|
|
|
if (!chunk.heightMap.isLoaded()) continue;
|
|
|
|
|
|
float chunkBaseX = (32.0f - terrain.coord.y) * kTileSize - cy * kChunkSize;
|
|
|
|
|
|
float chunkBaseY = (32.0f - terrain.coord.x) * kTileSize - cx * kChunkSize;
|
|
|
|
|
|
uint32_t chunkBase = static_cast<uint32_t>(zp.verts.size());
|
|
|
|
|
|
for (int row = 0; row < 9; ++row) {
|
|
|
|
|
|
for (int col = 0; col < 9; ++col) {
|
|
|
|
|
|
glm::vec3 p{
|
|
|
|
|
|
chunkBaseX - row * kVertSpacing,
|
|
|
|
|
|
chunkBaseY - col * kVertSpacing,
|
|
|
|
|
|
chunk.position[2] +
|
|
|
|
|
|
chunk.heightMap.heights[row * 17 + col]
|
|
|
|
|
|
};
|
|
|
|
|
|
zp.verts.push_back(p);
|
|
|
|
|
|
bMin = glm::min(bMin, p);
|
|
|
|
|
|
bMax = glm::max(bMax, p);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
bool isHoleChunk = (chunk.holes != 0);
|
|
|
|
|
|
for (int row = 0; row < 8; ++row) {
|
|
|
|
|
|
for (int col = 0; col < 8; ++col) {
|
|
|
|
|
|
if (isHoleChunk) {
|
|
|
|
|
|
int hx = col / 2, hy = row / 2;
|
|
|
|
|
|
if (chunk.holes & (1 << (hy * 4 + hx))) continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto idx = [&](int r, int c) {
|
|
|
|
|
|
return chunkBase + r * 9 + c;
|
|
|
|
|
|
};
|
|
|
|
|
|
zp.indices.push_back(idx(row, col));
|
|
|
|
|
|
zp.indices.push_back(idx(row, col + 1));
|
|
|
|
|
|
zp.indices.push_back(idx(row + 1, col + 1));
|
|
|
|
|
|
zp.indices.push_back(idx(row, col));
|
|
|
|
|
|
zp.indices.push_back(idx(row + 1, col + 1));
|
|
|
|
|
|
zp.indices.push_back(idx(row + 1, col));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (zp.verts.empty()) continue;
|
|
|
|
|
|
totalTiles += zoneTiles;
|
|
|
|
|
|
totalZones++;
|
|
|
|
|
|
zones.push_back(std::move(zp));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (zones.empty()) {
|
|
|
|
|
|
std::fprintf(stderr, "%s: no loadable terrain found\n", cmdName);
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isStl) {
|
|
|
|
|
|
std::ofstream out(outPath);
|
|
|
|
|
|
if (!out) {
|
|
|
|
|
|
std::fprintf(stderr, "%s: cannot write %s\n", cmdName, outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
out << "solid wowee_project\n";
|
|
|
|
|
|
uint64_t triCount = 0;
|
|
|
|
|
|
for (const auto& zp : zones) {
|
|
|
|
|
|
for (size_t k = 0; k + 2 < zp.indices.size(); k += 3) {
|
|
|
|
|
|
const auto& v0 = zp.verts[zp.indices[k]];
|
|
|
|
|
|
const auto& v1 = zp.verts[zp.indices[k + 1]];
|
|
|
|
|
|
const auto& v2 = zp.verts[zp.indices[k + 2]];
|
|
|
|
|
|
glm::vec3 n = glm::cross(v1 - v0, v2 - v0);
|
|
|
|
|
|
float len = glm::length(n);
|
|
|
|
|
|
if (len > 1e-12f) n /= len; else n = {0, 0, 1};
|
|
|
|
|
|
out << " facet normal " << n.x << " " << n.y << " " << n.z << "\n"
|
|
|
|
|
|
<< " outer loop\n"
|
|
|
|
|
|
<< " vertex " << v0.x << " " << v0.y << " " << v0.z << "\n"
|
|
|
|
|
|
<< " vertex " << v1.x << " " << v1.y << " " << v1.z << "\n"
|
|
|
|
|
|
<< " vertex " << v2.x << " " << v2.y << " " << v2.z << "\n"
|
|
|
|
|
|
<< " endloop\n"
|
|
|
|
|
|
<< " endfacet\n";
|
|
|
|
|
|
triCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out << "endsolid wowee_project\n";
|
|
|
|
|
|
out.close();
|
|
|
|
|
|
std::printf("Baked %s -> %s\n", projectDir.c_str(), outPath.c_str());
|
|
|
|
|
|
std::printf(" %d zone(s), %d tiles, %llu facets\n",
|
|
|
|
|
|
totalZones, totalTiles,
|
|
|
|
|
|
static_cast<unsigned long long>(triCount));
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
// GLB path: pack positions+normals+indices into one BIN chunk,
|
|
|
|
|
|
// one mesh+node per zone with sliced index accessor.
|
|
|
|
|
|
uint32_t totalV = 0, totalI = 0;
|
|
|
|
|
|
for (const auto& zp : zones) {
|
|
|
|
|
|
totalV += static_cast<uint32_t>(zp.verts.size());
|
|
|
|
|
|
totalI += static_cast<uint32_t>(zp.indices.size());
|
|
|
|
|
|
}
|
|
|
|
|
|
const uint32_t posOff = 0;
|
|
|
|
|
|
const uint32_t nrmOff = posOff + totalV * 12;
|
|
|
|
|
|
const uint32_t idxOff = nrmOff + totalV * 12;
|
|
|
|
|
|
const uint32_t binSize = idxOff + totalI * 4;
|
|
|
|
|
|
std::vector<uint8_t> bin(binSize);
|
|
|
|
|
|
uint32_t vCursor = 0, iCursor = 0;
|
|
|
|
|
|
// Per-zone bookkeeping for accessor slicing.
|
|
|
|
|
|
struct ZoneSlice { std::string name; uint32_t vOff, vCnt, iOff, iCnt; };
|
|
|
|
|
|
std::vector<ZoneSlice> slices;
|
|
|
|
|
|
for (const auto& zp : zones) {
|
|
|
|
|
|
ZoneSlice s{zp.name, vCursor, static_cast<uint32_t>(zp.verts.size()),
|
|
|
|
|
|
iCursor, static_cast<uint32_t>(zp.indices.size())};
|
|
|
|
|
|
for (const auto& v : zp.verts) {
|
|
|
|
|
|
std::memcpy(&bin[posOff + vCursor * 12 + 0], &v.x, 4);
|
|
|
|
|
|
std::memcpy(&bin[posOff + vCursor * 12 + 4], &v.y, 4);
|
|
|
|
|
|
std::memcpy(&bin[posOff + vCursor * 12 + 8], &v.z, 4);
|
|
|
|
|
|
float nx = 0, ny = 0, nz = 1;
|
|
|
|
|
|
std::memcpy(&bin[nrmOff + vCursor * 12 + 0], &nx, 4);
|
|
|
|
|
|
std::memcpy(&bin[nrmOff + vCursor * 12 + 4], &ny, 4);
|
|
|
|
|
|
std::memcpy(&bin[nrmOff + vCursor * 12 + 8], &nz, 4);
|
|
|
|
|
|
vCursor++;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Offset zone indices by the global vertBase so they
|
|
|
|
|
|
// resolve into the merged pool.
|
|
|
|
|
|
for (uint32_t idx : zp.indices) {
|
|
|
|
|
|
uint32_t global = idx + s.vOff;
|
|
|
|
|
|
std::memcpy(&bin[idxOff + iCursor * 4], &global, 4);
|
|
|
|
|
|
iCursor++;
|
|
|
|
|
|
}
|
|
|
|
|
|
slices.push_back(s);
|
|
|
|
|
|
}
|
|
|
|
|
|
nlohmann::json gj;
|
|
|
|
|
|
gj["asset"] = {{"version", "2.0"},
|
|
|
|
|
|
{"generator", "wowee_editor --bake-project-glb"}};
|
|
|
|
|
|
gj["scene"] = 0;
|
|
|
|
|
|
gj["buffers"] = nlohmann::json::array({{{"byteLength", binSize}}});
|
|
|
|
|
|
nlohmann::json bvs = nlohmann::json::array();
|
|
|
|
|
|
bvs.push_back({{"buffer", 0}, {"byteOffset", posOff},
|
|
|
|
|
|
{"byteLength", totalV * 12}, {"target", 34962}});
|
|
|
|
|
|
bvs.push_back({{"buffer", 0}, {"byteOffset", nrmOff},
|
|
|
|
|
|
{"byteLength", totalV * 12}, {"target", 34962}});
|
|
|
|
|
|
bvs.push_back({{"buffer", 0}, {"byteOffset", idxOff},
|
|
|
|
|
|
{"byteLength", totalI * 4}, {"target", 34963}});
|
|
|
|
|
|
gj["bufferViews"] = bvs;
|
|
|
|
|
|
nlohmann::json accessors = nlohmann::json::array();
|
|
|
|
|
|
accessors.push_back({{"bufferView", 0}, {"componentType", 5126},
|
|
|
|
|
|
{"count", totalV}, {"type", "VEC3"},
|
|
|
|
|
|
{"min", {bMin.x, bMin.y, bMin.z}},
|
|
|
|
|
|
{"max", {bMax.x, bMax.y, bMax.z}}});
|
|
|
|
|
|
accessors.push_back({{"bufferView", 1}, {"componentType", 5126},
|
|
|
|
|
|
{"count", totalV}, {"type", "VEC3"}});
|
|
|
|
|
|
nlohmann::json meshes = nlohmann::json::array();
|
|
|
|
|
|
nlohmann::json nodes = nlohmann::json::array();
|
|
|
|
|
|
nlohmann::json sceneNodes = nlohmann::json::array();
|
|
|
|
|
|
for (const auto& s : slices) {
|
|
|
|
|
|
uint32_t accIdx = static_cast<uint32_t>(accessors.size());
|
|
|
|
|
|
accessors.push_back({{"bufferView", 2},
|
|
|
|
|
|
{"byteOffset", s.iOff * 4},
|
|
|
|
|
|
{"componentType", 5125},
|
|
|
|
|
|
{"count", s.iCnt}, {"type", "SCALAR"}});
|
|
|
|
|
|
uint32_t meshIdx = static_cast<uint32_t>(meshes.size());
|
|
|
|
|
|
meshes.push_back({{"primitives", nlohmann::json::array({nlohmann::json{
|
|
|
|
|
|
{"attributes", {{"POSITION", 0}, {"NORMAL", 1}}},
|
|
|
|
|
|
{"indices", accIdx}, {"mode", 4}}})}});
|
|
|
|
|
|
uint32_t nodeIdx = static_cast<uint32_t>(nodes.size());
|
|
|
|
|
|
nodes.push_back({{"name", "zone_" + s.name}, {"mesh", meshIdx}});
|
|
|
|
|
|
sceneNodes.push_back(nodeIdx);
|
|
|
|
|
|
}
|
|
|
|
|
|
gj["accessors"] = accessors;
|
|
|
|
|
|
gj["meshes"] = meshes;
|
|
|
|
|
|
gj["nodes"] = nodes;
|
|
|
|
|
|
gj["scenes"] = nlohmann::json::array({{{"nodes", sceneNodes}}});
|
|
|
|
|
|
std::string jsonStr = gj.dump();
|
|
|
|
|
|
while (jsonStr.size() % 4 != 0) jsonStr += ' ';
|
|
|
|
|
|
uint32_t jsonLen = static_cast<uint32_t>(jsonStr.size());
|
|
|
|
|
|
uint32_t binLen = binSize;
|
|
|
|
|
|
uint32_t totalLen = 12 + 8 + jsonLen + 8 + binLen;
|
|
|
|
|
|
std::ofstream out(outPath, std::ios::binary);
|
|
|
|
|
|
if (!out) {
|
|
|
|
|
|
std::fprintf(stderr, "%s: cannot write %s\n", cmdName, outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
uint32_t magic = 0x46546C67, version = 2;
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&magic), 4);
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&version), 4);
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&totalLen), 4);
|
|
|
|
|
|
uint32_t jt = 0x4E4F534A;
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&jsonLen), 4);
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&jt), 4);
|
|
|
|
|
|
out.write(jsonStr.data(), jsonLen);
|
|
|
|
|
|
uint32_t bt = 0x004E4942;
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&binLen), 4);
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(&bt), 4);
|
|
|
|
|
|
out.write(reinterpret_cast<const char*>(bin.data()), binLen);
|
|
|
|
|
|
out.close();
|
|
|
|
|
|
std::printf("Baked %s -> %s\n", projectDir.c_str(), outPath.c_str());
|
|
|
|
|
|
std::printf(" %d zone(s), %d tiles, %u verts, %u tris, %u-byte BIN\n",
|
|
|
|
|
|
totalZones, totalTiles, totalV, totalI / 3, binLen);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
2026-05-09 11:01:45 -07:00
|
|
|
|
int handleBakeWomCollision(int& i, int argc, char** argv) {
|
|
|
|
|
|
// Convert a single WOM into a WOC collision file. Optional
|
|
|
|
|
|
// --weld <eps> first welds vertices that share a position so
|
|
|
|
|
|
// adjacent per-face-shaded faces land in the same triangle
|
|
|
|
|
|
// network for collision queries — without it, the WOC still
|
|
|
|
|
|
// has the right triangles but they're authored independently
|
|
|
|
|
|
// (which is fine for raycast/walkability but loses the edge
|
|
|
|
|
|
// adjacency info that some physics queries want).
|
|
|
|
|
|
std::string base = argv[++i];
|
|
|
|
|
|
std::string outPath;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
|
|
|
|
|
outPath = argv[++i];
|
|
|
|
|
|
}
|
|
|
|
|
|
bool useWeld = false;
|
|
|
|
|
|
float weldEps = 1e-5f;
|
|
|
|
|
|
float steepAngle = 50.0f;
|
|
|
|
|
|
while (i + 1 < argc && argv[i + 1][0] == '-') {
|
|
|
|
|
|
if (std::strcmp(argv[i + 1], "--weld") == 0 && i + 2 < argc) {
|
|
|
|
|
|
useWeld = true;
|
|
|
|
|
|
try { weldEps = std::stof(argv[i + 2]); } catch (...) {}
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
} else if (std::strcmp(argv[i + 1], "--steep") == 0 && i + 2 < argc) {
|
|
|
|
|
|
try { steepAngle = std::stof(argv[i + 2]); } catch (...) {}
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") {
|
|
|
|
|
|
base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!wowee::pipeline::WoweeModelLoader::exists(base)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-wom-collision: %s.wom does not exist\n", base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto wom = wowee::pipeline::WoweeModelLoader::load(base);
|
|
|
|
|
|
if (!wom.isValid() || wom.indices.size() % 3 != 0) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-wom-collision: invalid WOM (no geometry or "
|
|
|
|
|
|
"indices%%3 != 0)\n");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (outPath.empty()) outPath = base + ".woc";
|
|
|
|
|
|
std::vector<glm::vec3> positions;
|
|
|
|
|
|
std::vector<uint32_t> indices;
|
|
|
|
|
|
if (useWeld) {
|
2026-05-09 11:05:54 -07:00
|
|
|
|
// Run cli_weld to map vertex i → canonical (lowest-index)
|
|
|
|
|
|
// representative of its equivalence class, then compact
|
|
|
|
|
|
// positions to one entry per unique class and renumber
|
|
|
|
|
|
// indices accordingly. The collision mesh ends up properly
|
|
|
|
|
|
// indexed so raycasts can share edges between faces.
|
|
|
|
|
|
std::vector<glm::vec3> srcPositions;
|
|
|
|
|
|
srcPositions.reserve(wom.vertices.size());
|
|
|
|
|
|
for (const auto& vert : wom.vertices) srcPositions.push_back(vert.position);
|
|
|
|
|
|
std::size_t uniq = 0;
|
|
|
|
|
|
std::vector<uint32_t> canon = buildWeldMap(srcPositions, weldEps, uniq);
|
|
|
|
|
|
// Build canon→compactedIndex remap as we walk vertices in order.
|
|
|
|
|
|
std::vector<uint32_t> remap(wom.vertices.size(),
|
|
|
|
|
|
std::numeric_limits<uint32_t>::max());
|
|
|
|
|
|
positions.reserve(uniq);
|
2026-05-09 11:01:45 -07:00
|
|
|
|
for (std::size_t v = 0; v < wom.vertices.size(); ++v) {
|
2026-05-09 11:05:54 -07:00
|
|
|
|
uint32_t c = canon[v];
|
|
|
|
|
|
if (remap[c] == std::numeric_limits<uint32_t>::max()) {
|
|
|
|
|
|
remap[c] = static_cast<uint32_t>(positions.size());
|
|
|
|
|
|
positions.push_back(srcPositions[c]);
|
2026-05-09 11:01:45 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
indices.reserve(wom.indices.size());
|
2026-05-09 11:05:54 -07:00
|
|
|
|
for (uint32_t orig : wom.indices) indices.push_back(remap[canon[orig]]);
|
2026-05-09 11:01:45 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
positions.reserve(wom.vertices.size());
|
|
|
|
|
|
for (const auto& vert : wom.vertices) positions.push_back(vert.position);
|
|
|
|
|
|
indices = wom.indices;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::pipeline::WoweeCollision collision;
|
|
|
|
|
|
glm::mat4 identity(1.0f);
|
|
|
|
|
|
wowee::pipeline::WoweeCollisionBuilder::addMesh(
|
|
|
|
|
|
collision, positions, indices, identity, 0, steepAngle);
|
|
|
|
|
|
if (!wowee::pipeline::WoweeCollisionBuilder::save(collision, outPath)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-wom-collision: failed to write %s\n", outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Wrote %s\n", outPath.c_str());
|
|
|
|
|
|
std::printf(" source : %s.wom (%zu verts -> %zu)\n",
|
|
|
|
|
|
base.c_str(), wom.vertices.size(), positions.size());
|
|
|
|
|
|
std::printf(" triangles : %zu (%zu walkable, %zu steep)\n",
|
|
|
|
|
|
collision.triangles.size(),
|
|
|
|
|
|
collision.walkableCount(),
|
|
|
|
|
|
collision.steepCount());
|
|
|
|
|
|
std::printf(" steep cut : %.1f° from horizontal\n", steepAngle);
|
|
|
|
|
|
if (useWeld) {
|
|
|
|
|
|
std::printf(" weld eps : %.6f\n", weldEps);
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 11:19:09 -07:00
|
|
|
|
int handleBakeWobCollision(int& i, int argc, char** argv) {
|
|
|
|
|
|
// Convert a multi-group WOB into a single WOC collision file.
|
|
|
|
|
|
// Each group's triangles are appended via WoweeCollisionBuilder
|
|
|
|
|
|
// ::addMesh. Optional --weld <eps> is applied PER GROUP — groups
|
|
|
|
|
|
// are intentionally separate (rooms with portals between them),
|
|
|
|
|
|
// so welding across groups would fuse walls that should remain
|
|
|
|
|
|
// distinct collision surfaces.
|
|
|
|
|
|
std::string base = argv[++i];
|
|
|
|
|
|
std::string outPath;
|
|
|
|
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
|
|
|
|
|
outPath = argv[++i];
|
|
|
|
|
|
}
|
|
|
|
|
|
bool useWeld = false;
|
|
|
|
|
|
float weldEps = 1e-5f;
|
|
|
|
|
|
float steepAngle = 50.0f;
|
|
|
|
|
|
while (i + 1 < argc && argv[i + 1][0] == '-') {
|
|
|
|
|
|
if (std::strcmp(argv[i + 1], "--weld") == 0 && i + 2 < argc) {
|
|
|
|
|
|
useWeld = true;
|
|
|
|
|
|
try { weldEps = std::stof(argv[i + 2]); } catch (...) {}
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
} else if (std::strcmp(argv[i + 1], "--steep") == 0 && i + 2 < argc) {
|
|
|
|
|
|
try { steepAngle = std::stof(argv[i + 2]); } catch (...) {}
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (base.size() >= 4 && base.substr(base.size() - 4) == ".wob") {
|
|
|
|
|
|
base = base.substr(0, base.size() - 4);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!wowee::pipeline::WoweeBuildingLoader::exists(base)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-wob-collision: %s.wob does not exist\n", base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto bld = wowee::pipeline::WoweeBuildingLoader::load(base);
|
|
|
|
|
|
if (!bld.isValid()) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-wob-collision: %s.wob has no groups\n", base.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (outPath.empty()) outPath = base + ".woc";
|
|
|
|
|
|
wowee::pipeline::WoweeCollision collision;
|
|
|
|
|
|
glm::mat4 identity(1.0f);
|
|
|
|
|
|
std::size_t totalSrc = 0, totalUniq = 0;
|
|
|
|
|
|
for (const auto& g : bld.groups) {
|
|
|
|
|
|
if (g.indices.size() % 3 != 0) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-wob-collision: group '%s' has indices %% 3 != 0\n",
|
|
|
|
|
|
g.name.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::vector<glm::vec3> positions;
|
|
|
|
|
|
std::vector<uint32_t> indices;
|
|
|
|
|
|
if (useWeld) {
|
|
|
|
|
|
std::vector<glm::vec3> srcPositions;
|
|
|
|
|
|
srcPositions.reserve(g.vertices.size());
|
|
|
|
|
|
for (const auto& v : g.vertices) srcPositions.push_back(v.position);
|
|
|
|
|
|
std::size_t uniq = 0;
|
|
|
|
|
|
std::vector<uint32_t> canon = buildWeldMap(srcPositions, weldEps, uniq);
|
|
|
|
|
|
std::vector<uint32_t> remap(g.vertices.size(),
|
|
|
|
|
|
std::numeric_limits<uint32_t>::max());
|
|
|
|
|
|
positions.reserve(uniq);
|
|
|
|
|
|
for (std::size_t v = 0; v < g.vertices.size(); ++v) {
|
|
|
|
|
|
uint32_t c = canon[v];
|
|
|
|
|
|
if (remap[c] == std::numeric_limits<uint32_t>::max()) {
|
|
|
|
|
|
remap[c] = static_cast<uint32_t>(positions.size());
|
|
|
|
|
|
positions.push_back(srcPositions[c]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
indices.reserve(g.indices.size());
|
|
|
|
|
|
for (uint32_t orig : g.indices) indices.push_back(remap[canon[orig]]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
positions.reserve(g.vertices.size());
|
|
|
|
|
|
for (const auto& v : g.vertices) positions.push_back(v.position);
|
|
|
|
|
|
indices = g.indices;
|
|
|
|
|
|
}
|
|
|
|
|
|
wowee::pipeline::WoweeCollisionBuilder::addMesh(
|
|
|
|
|
|
collision, positions, indices, identity, 0, steepAngle);
|
|
|
|
|
|
totalSrc += g.vertices.size();
|
|
|
|
|
|
totalUniq += positions.size();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!wowee::pipeline::WoweeCollisionBuilder::save(collision, outPath)) {
|
|
|
|
|
|
std::fprintf(stderr,
|
|
|
|
|
|
"bake-wob-collision: failed to write %s\n", outPath.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::printf("Wrote %s\n", outPath.c_str());
|
|
|
|
|
|
std::printf(" source : %s.wob (%zu groups, %zu verts -> %zu)\n",
|
|
|
|
|
|
base.c_str(), bld.groups.size(), totalSrc, totalUniq);
|
|
|
|
|
|
std::printf(" triangles : %zu (%zu walkable, %zu steep)\n",
|
|
|
|
|
|
collision.triangles.size(),
|
|
|
|
|
|
collision.walkableCount(),
|
|
|
|
|
|
collision.steepCount());
|
|
|
|
|
|
std::printf(" steep cut : %.1f° from horizontal\n", steepAngle);
|
|
|
|
|
|
if (useWeld) {
|
|
|
|
|
|
std::printf(" weld eps : %.6f (per group)\n", weldEps);
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 05:57:25 -07:00
|
|
|
|
bool handleBake(int& i, int argc, char** argv, int& outRc) {
|
|
|
|
|
|
if (std::strcmp(argv[i], "--bake-zone-glb") == 0 && i + 1 < argc) {
|
|
|
|
|
|
outRc = handleBakeZoneGlb(i, argc, argv); return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (std::strcmp(argv[i], "--bake-zone-stl") == 0 && i + 1 < argc) {
|
|
|
|
|
|
outRc = handleBakeZoneStl(i, argc, argv); return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (std::strcmp(argv[i], "--bake-zone-obj") == 0 && i + 1 < argc) {
|
|
|
|
|
|
outRc = handleBakeZoneObj(i, argc, argv); return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (std::strcmp(argv[i], "--bake-project-obj") == 0 && i + 1 < argc) {
|
|
|
|
|
|
outRc = handleBakeProjectObj(i, argc, argv); return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ((std::strcmp(argv[i], "--bake-project-stl") == 0 ||
|
|
|
|
|
|
std::strcmp(argv[i], "--bake-project-glb") == 0) &&
|
|
|
|
|
|
i + 1 < argc) {
|
|
|
|
|
|
outRc = handleBakeProjectStlOrGlb(i, argc, argv); return true;
|
|
|
|
|
|
}
|
2026-05-09 11:01:45 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--bake-wom-collision") == 0 && i + 1 < argc) {
|
|
|
|
|
|
outRc = handleBakeWomCollision(i, argc, argv); return true;
|
|
|
|
|
|
}
|
2026-05-09 11:19:09 -07:00
|
|
|
|
if (std::strcmp(argv[i], "--bake-wob-collision") == 0 && i + 1 < argc) {
|
|
|
|
|
|
outRc = handleBakeWobCollision(i, argc, argv); return true;
|
|
|
|
|
|
}
|
2026-05-09 05:57:25 -07:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace cli
|
|
|
|
|
|
} // namespace editor
|
|
|
|
|
|
} // namespace wowee
|