mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
- Zone::LoadZoneIntoMemory takes the paths the zone file reader read (lost in the merge of the raw terrain loading) - the terrain reader is main's unchanged: files from before version 32 keep their colour and texture maps as the file has them, and the dashboard's terrain tests expect that (bounds by the span between the first and last point too) - the model component test counts the model block only: the item block in front of it is the ItemComponent's - scene file tests: a file from before chunks keeps only its header, editor settings are a count of colours, and the tests that log have a logger - the settings catalog lists export_terrain_to_obj Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
574 lines
19 KiB
C++
574 lines
19 KiB
C++
#include "Raw.h"
|
|
#include "BinaryIO.h"
|
|
#include "Logger.h"
|
|
#include "SceneColor.h"
|
|
#include <fstream>
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
constexpr uint32_t kMaxResolution = 4096;
|
|
constexpr size_t kMaxBlobBytes = 64ULL * 1024 * 1024; // 64 MiB
|
|
constexpr uint32_t kMaxChunks = 1024;
|
|
} // namespace
|
|
|
|
namespace Raw {
|
|
|
|
bool Chunk::IsValidForSceneLookup() const {
|
|
return !sceneMap.empty() && colorMapResolution > 0 && !heightMap.empty()
|
|
&& scaleFactor > 0.0f && width > 1 && height > 1;
|
|
}
|
|
|
|
uint8_t Chunk::GetSceneIDAtLocal(float localX, float localZ) const {
|
|
// The nearest scene map cell
|
|
const auto toCell = [this](float cell) {
|
|
return static_cast<uint32_t>(std::clamp(static_cast<int32_t>(std::floor(cell + 0.5f)), 0, static_cast<int32_t>(colorMapResolution) - 1));
|
|
};
|
|
const uint32_t sceneIndex = toCell(sceneCellsPerUnitX * localX) * colorMapResolution + toCell(sceneCellsPerUnitZ * localZ);
|
|
if (sceneIndex >= sceneMap.size()) return 0;
|
|
return sceneMap[sceneIndex];
|
|
}
|
|
|
|
uint8_t Chunk::GetSceneIDAtGrid(uint32_t i, uint32_t j) const {
|
|
return GetSceneIDAtLocal(static_cast<float>(i) * scaleFactor, static_cast<float>(j) * scaleFactor);
|
|
}
|
|
|
|
NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const {
|
|
const uint32_t heightIndex = i * height + j;
|
|
const float y = heightIndex < heightMap.size() ? heightMap[heightIndex] : 0.0f;
|
|
return NiPoint3(offsetX + static_cast<float>(i) * scaleFactor, y, offsetZ + static_cast<float>(j) * scaleFactor);
|
|
}
|
|
|
|
/**
|
|
* @brief Read flair attributes from stream
|
|
*/
|
|
static bool ReadFlairAttributes(std::istream& stream, FlairAttributes& flair) {
|
|
try {
|
|
BinaryIO::BinaryRead(stream, flair.id);
|
|
BinaryIO::BinaryRead(stream, flair.scaleFactor);
|
|
BinaryIO::BinaryRead(stream, flair.position.x);
|
|
BinaryIO::BinaryRead(stream, flair.position.y);
|
|
BinaryIO::BinaryRead(stream, flair.position.z);
|
|
BinaryIO::BinaryRead(stream, flair.rotation.x);
|
|
BinaryIO::BinaryRead(stream, flair.rotation.y);
|
|
BinaryIO::BinaryRead(stream, flair.rotation.z);
|
|
BinaryIO::BinaryRead(stream, flair.colorR);
|
|
BinaryIO::BinaryRead(stream, flair.colorG);
|
|
BinaryIO::BinaryRead(stream, flair.colorB);
|
|
BinaryIO::BinaryRead(stream, flair.padding);
|
|
return true;
|
|
} catch (const std::exception&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Read mesh triangle data from stream
|
|
*/
|
|
static bool ReadMeshTri(std::istream& stream, MeshTri& meshTri) {
|
|
try {
|
|
BinaryIO::BinaryRead(stream, meshTri.meshTriListSize);
|
|
meshTri.meshTriList.resize(meshTri.meshTriListSize);
|
|
for (uint16_t i = 0; i < meshTri.meshTriListSize; ++i) {
|
|
BinaryIO::BinaryRead(stream, meshTri.meshTriList[i]);
|
|
}
|
|
return true;
|
|
} catch (const std::exception&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Read a chunk from stream
|
|
*/
|
|
static bool ReadChunkData(std::istream& stream, Chunk& chunk, uint16_t version) {
|
|
try {
|
|
// Read basic chunk info
|
|
BinaryIO::BinaryRead(stream, chunk.id);
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
BinaryIO::BinaryRead(stream, chunk.width);
|
|
BinaryIO::BinaryRead(stream, chunk.height);
|
|
BinaryIO::BinaryRead(stream, chunk.offsetX);
|
|
BinaryIO::BinaryRead(stream, chunk.offsetZ);
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
// For version < 32, shader ID comes before texture IDs
|
|
if (version < 32) {
|
|
BinaryIO::BinaryRead(stream, chunk.shaderId);
|
|
}
|
|
|
|
// Read texture IDs (4 textures)
|
|
chunk.textureIds.resize(4);
|
|
for (int i = 0; i < 4; ++i) {
|
|
BinaryIO::BinaryRead(stream, chunk.textureIds[i]);
|
|
}
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
// Read scale factor
|
|
BinaryIO::BinaryRead(stream, chunk.scaleFactor);
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
// Read heightmap: width * height floats, at most kMaxResolution^2 (64 MiB)
|
|
if (chunk.width == 0 || chunk.height == 0 || chunk.width > kMaxResolution || chunk.height > kMaxResolution) {
|
|
LOG("Chunk %u has invalid heightmap dimensions: width=%u, height=%u (max %u)", chunk.id, chunk.width, chunk.height, kMaxResolution);
|
|
return false;
|
|
}
|
|
|
|
const size_t heightMapSize = static_cast<size_t>(chunk.width) * chunk.height;
|
|
chunk.heightMap.resize(heightMapSize);
|
|
for (size_t i = 0; i < heightMapSize; ++i) {
|
|
BinaryIO::BinaryRead(stream, chunk.heightMap[i]);
|
|
}
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
// ColorMap
|
|
if (version >= 32) {
|
|
BinaryIO::BinaryRead(stream, chunk.colorMapResolution);
|
|
} else {
|
|
chunk.colorMapResolution = chunk.width - 1;
|
|
}
|
|
|
|
if (chunk.colorMapResolution > kMaxResolution) {
|
|
LOG("Chunk colorMapResolution %u exceeds maximum %u", chunk.colorMapResolution, kMaxResolution);
|
|
return false;
|
|
}
|
|
|
|
if (version >= 32) {
|
|
const size_t colorMapPixelCount = static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution * 4;
|
|
if (colorMapPixelCount > kMaxBlobBytes) {
|
|
LOG("Chunk colorMap size %zu exceeds maximum %zu bytes", colorMapPixelCount, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
chunk.colorMap.resize(colorMapPixelCount);
|
|
stream.read(reinterpret_cast<char*>(chunk.colorMap.data()), static_cast<std::streamsize>(colorMapPixelCount));
|
|
} else {
|
|
const size_t legacyColorBytes = static_cast<size_t>(chunk.width) * chunk.width * 4;
|
|
if (legacyColorBytes > kMaxBlobBytes) {
|
|
LOG("Chunk legacy colorMap size %zu exceeds maximum %zu bytes", legacyColorBytes, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
chunk.colorMap.resize(legacyColorBytes);
|
|
stream.read(reinterpret_cast<char*>(chunk.colorMap.data()), static_cast<std::streamsize>(legacyColorBytes));
|
|
}
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
// LightMap/diffusemap.dds (v>=32 only)
|
|
if (version >= 32) {
|
|
uint32_t lightMapSize;
|
|
BinaryIO::BinaryRead(stream, lightMapSize);
|
|
|
|
if (lightMapSize > kMaxBlobBytes) {
|
|
LOG("Chunk lightMap size %u exceeds maximum %zu bytes", lightMapSize, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
chunk.lightMap.resize(lightMapSize);
|
|
stream.read(reinterpret_cast<char*>(chunk.lightMap.data()), static_cast<std::streamsize>(lightMapSize));
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Blend/texture map
|
|
BinaryIO::BinaryRead(stream, chunk.textureMapResolution);
|
|
|
|
if (chunk.textureMapResolution > kMaxResolution) {
|
|
LOG("Chunk textureMapResolution %u exceeds maximum %u", chunk.textureMapResolution, kMaxResolution);
|
|
return false;
|
|
}
|
|
const size_t textureMapPixelCount = static_cast<size_t>(chunk.textureMapResolution) * chunk.textureMapResolution * 4;
|
|
if (textureMapPixelCount > kMaxBlobBytes) {
|
|
LOG("Chunk textureMap size %zu exceeds maximum %zu bytes", textureMapPixelCount, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
chunk.textureMap.resize(textureMapPixelCount);
|
|
stream.read(reinterpret_cast<char*>(chunk.textureMap.data()), static_cast<std::streamsize>(textureMapPixelCount));
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
// Texture settings + blend map DDS (v>=32 only)
|
|
if (version >= 32) {
|
|
BinaryIO::BinaryRead(stream, chunk.textureSettings);
|
|
|
|
uint32_t blendMapDDSSize;
|
|
BinaryIO::BinaryRead(stream, blendMapDDSSize);
|
|
|
|
if (blendMapDDSSize > kMaxBlobBytes) {
|
|
LOG("Chunk blendMap size %u exceeds maximum %zu bytes", blendMapDDSSize, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
chunk.blendMap.resize(blendMapDDSSize);
|
|
stream.read(reinterpret_cast<char*>(chunk.blendMap.data()), static_cast<std::streamsize>(blendMapDDSSize));
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Read flairs
|
|
uint32_t numFlairs;
|
|
BinaryIO::BinaryRead(stream, numFlairs);
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
const size_t flairBytes = static_cast<size_t>(numFlairs) * sizeof(FlairAttributes);
|
|
if (flairBytes > kMaxBlobBytes) {
|
|
LOG("Chunk %u flair count %u exceeds maximum (byte size %zu > %zu)", chunk.id, numFlairs, flairBytes, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
chunk.flairs.resize(numFlairs);
|
|
for (uint32_t i = 0; i < numFlairs; ++i) {
|
|
if (!ReadFlairAttributes(stream, chunk.flairs[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Scene map
|
|
if (version >= 32) {
|
|
const size_t sceneMapSize = static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution;
|
|
|
|
if (sceneMapSize > kMaxBlobBytes) {
|
|
LOG("Chunk sceneMap size %zu exceeds maximum %zu bytes", sceneMapSize, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
chunk.sceneMap.resize(sceneMapSize);
|
|
stream.read(reinterpret_cast<char*>(chunk.sceneMap.data()), static_cast<std::streamsize>(sceneMapSize));
|
|
} else {
|
|
const size_t sceneMapCells = static_cast<size_t>(chunk.colorMapResolution + 1) * (chunk.colorMapResolution + 1);
|
|
if (sceneMapCells > kMaxBlobBytes) {
|
|
LOG("Chunk legacy sceneMap size %zu exceeds maximum %zu bytes", sceneMapCells, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
chunk.sceneMapLegacy.resize(sceneMapCells);
|
|
stream.read(reinterpret_cast<char*>(chunk.sceneMapLegacy.data()), static_cast<std::streamsize>(sceneMapCells));
|
|
// Like the client: version 31 uses the first colorMapResolution rows and columns, older versions are all scene 0
|
|
chunk.sceneMap.assign(static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution, 0);
|
|
if (version == 31) {
|
|
for (uint32_t row = 0; row < chunk.colorMapResolution; ++row) {
|
|
for (uint32_t col = 0; col < chunk.colorMapResolution; ++col) {
|
|
chunk.sceneMap[row * chunk.colorMapResolution + col] = chunk.sceneMapLegacy[row * (chunk.colorMapResolution + 1) + col];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
// Mesh data (v>=32 only)
|
|
if (version < 32) {
|
|
return true;
|
|
}
|
|
|
|
BinaryIO::BinaryRead(stream, chunk.vertSize);
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
if (chunk.vertSize == 0) {
|
|
return true;
|
|
}
|
|
|
|
const size_t vertBytes = static_cast<size_t>(chunk.vertSize) * sizeof(uint16_t);
|
|
if (vertBytes > kMaxBlobBytes) {
|
|
LOG("Chunk %u vertSize %u exceeds maximum (byte size %zu > %zu)", chunk.id, chunk.vertSize, vertBytes, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
chunk.meshVertUsage.resize(chunk.vertSize);
|
|
for (uint32_t i = 0; i < chunk.vertSize; ++i) {
|
|
BinaryIO::BinaryRead(stream, chunk.meshVertUsage[i]);
|
|
}
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
chunk.meshVertSize.resize(16);
|
|
for (int i = 0; i < 16; ++i) {
|
|
BinaryIO::BinaryRead(stream, chunk.meshVertSize[i]);
|
|
}
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
chunk.meshTri.resize(16);
|
|
for (int i = 0; i < 16; ++i) {
|
|
if (!ReadMeshTri(stream, chunk.meshTri[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} catch (const std::exception&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Read a chunk and work out its scene map cell sizes
|
|
*/
|
|
static bool ReadChunk(std::istream& stream, Chunk& chunk, uint16_t version) {
|
|
if (!ReadChunkData(stream, chunk, version)) return false;
|
|
|
|
if (chunk.IsValidForSceneLookup()) {
|
|
chunk.sceneCellSizeX = static_cast<float>(chunk.width - 1) * chunk.scaleFactor / static_cast<float>(chunk.colorMapResolution);
|
|
chunk.sceneCellSizeZ = static_cast<float>(chunk.height - 1) * chunk.scaleFactor / static_cast<float>(chunk.colorMapResolution);
|
|
chunk.sceneCellsPerUnitX = 1.0f / chunk.sceneCellSizeX;
|
|
chunk.sceneCellsPerUnitZ = 1.0f / chunk.sceneCellSizeZ;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Index the chunks by grid cell when they form a regular grid of equal chunks
|
|
*/
|
|
static void BuildChunkGrid(Raw& raw) {
|
|
raw.chunkGrid.clear();
|
|
const auto& first = raw.chunks.front();
|
|
const float spanX = first.GetMaxX() - first.offsetX;
|
|
const float spanZ = first.GetMaxZ() - first.offsetZ;
|
|
if (spanX <= 0.0f || spanZ <= 0.0f) return;
|
|
|
|
const auto countX = static_cast<uint32_t>(std::lround((raw.maxBoundsX - raw.minBoundsX) / spanX));
|
|
const auto countZ = static_cast<uint32_t>(std::lround((raw.maxBoundsZ - raw.minBoundsZ) / spanZ));
|
|
if (countX == 0 || countZ == 0 || static_cast<size_t>(countX) * countZ > kMaxChunks) return;
|
|
|
|
std::vector<int32_t> grid(static_cast<size_t>(countX) * countZ, -1);
|
|
for (size_t i = 0; i < raw.chunks.size(); ++i) {
|
|
const auto& chunk = raw.chunks[i];
|
|
const float cellX = (chunk.offsetX - raw.minBoundsX) / spanX;
|
|
const float cellZ = (chunk.offsetZ - raw.minBoundsZ) / spanZ;
|
|
const long x = std::lround(cellX);
|
|
const long z = std::lround(cellZ);
|
|
const bool sameSize = chunk.width == first.width && chunk.height == first.height && chunk.scaleFactor == first.scaleFactor;
|
|
if (!sameSize || std::abs(cellX - x) > 0.01f || std::abs(cellZ - z) > 0.01f || x < 0 || z < 0
|
|
|| x >= static_cast<long>(countX) || z >= static_cast<long>(countZ)) {
|
|
LOG("Raw chunks are not a regular grid (chunk %u); scene lookups check every chunk", chunk.id);
|
|
return;
|
|
}
|
|
auto& cell = grid[static_cast<size_t>(x) * countZ + z];
|
|
if (cell == -1) cell = static_cast<int32_t>(i); // the client takes the first chunk that matches
|
|
}
|
|
|
|
raw.chunkGrid = std::move(grid);
|
|
raw.chunkGridCountX = countX;
|
|
raw.chunkGridCountZ = countZ;
|
|
raw.chunkSpanX = spanX;
|
|
raw.chunkSpanZ = spanZ;
|
|
}
|
|
|
|
bool ReadRaw(std::istream& stream, Raw& outRaw) {
|
|
// Get stream size
|
|
stream.seekg(0, std::ios::end);
|
|
auto streamSize = stream.tellg();
|
|
stream.seekg(0, std::ios::beg);
|
|
|
|
if (streamSize <= 0) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Read header
|
|
BinaryIO::BinaryRead(stream, outRaw.version);
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
BinaryIO::BinaryRead(stream, outRaw.dev);
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
// Only read chunks if dev == 0
|
|
if (outRaw.dev == 0) {
|
|
BinaryIO::BinaryRead(stream, outRaw.numChunks);
|
|
BinaryIO::BinaryRead(stream, outRaw.numChunksWidth);
|
|
BinaryIO::BinaryRead(stream, outRaw.numChunksHeight);
|
|
|
|
if (outRaw.numChunks > kMaxChunks) {
|
|
LOG("Raw numChunks %u exceeds maximum %u", outRaw.numChunks, kMaxChunks);
|
|
return false;
|
|
}
|
|
|
|
// Read all chunks
|
|
outRaw.chunks.resize(outRaw.numChunks);
|
|
for (uint32_t i = 0; i < outRaw.numChunks; ++i) {
|
|
if (!ReadChunk(stream, outRaw.chunks[i], outRaw.version)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Calculate terrain bounds from all chunks
|
|
if (!outRaw.chunks.empty()) {
|
|
outRaw.minBoundsX = std::numeric_limits<float>::max();
|
|
outRaw.minBoundsZ = std::numeric_limits<float>::max();
|
|
outRaw.maxBoundsX = std::numeric_limits<float>::lowest();
|
|
outRaw.maxBoundsZ = std::numeric_limits<float>::lowest();
|
|
|
|
for (const auto& chunk : outRaw.chunks) {
|
|
outRaw.minBoundsX = std::min(outRaw.minBoundsX, chunk.offsetX);
|
|
outRaw.minBoundsZ = std::min(outRaw.minBoundsZ, chunk.offsetZ);
|
|
outRaw.maxBoundsX = std::max(outRaw.maxBoundsX, chunk.GetMaxX());
|
|
outRaw.maxBoundsZ = std::max(outRaw.maxBoundsZ, chunk.GetMaxZ());
|
|
}
|
|
LOG_DEBUG("Raw terrain bounds: X[%.2f, %.2f], Z[%.2f, %.2f]",
|
|
outRaw.minBoundsX, outRaw.maxBoundsX, outRaw.minBoundsZ, outRaw.maxBoundsZ);
|
|
BuildChunkGrid(outRaw);
|
|
}
|
|
}
|
|
return true;
|
|
} catch (const std::exception&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// The client's TerrainManager::GetSceneAtPos: clamp to the terrain bounds, keep one scene cell inside the far edge,
|
|
// find the chunk containing the position (edges moved in by 0.001) and take its nearest scene map cell
|
|
std::optional<uint8_t> Raw::GetSceneIDAt(float x, float z) const {
|
|
if (chunks.empty()) return std::nullopt;
|
|
|
|
constexpr float kEdgeEpsilon = 0.001f;
|
|
const float posX = std::clamp(x, minBoundsX, maxBoundsX);
|
|
const float posZ = std::clamp(z, minBoundsZ, maxBoundsZ);
|
|
|
|
const auto sceneInChunk = [&](const Chunk& chunk) -> std::optional<uint8_t> {
|
|
if (!chunk.IsValidForSceneLookup()) return std::nullopt;
|
|
const float chunkX = std::min(posX, maxBoundsX - chunk.sceneCellSizeX);
|
|
const float chunkZ = std::min(posZ, maxBoundsZ - chunk.sceneCellSizeZ);
|
|
if (chunk.offsetX - kEdgeEpsilon <= chunkX && chunkX < chunk.GetMaxX() - kEdgeEpsilon &&
|
|
chunk.offsetZ - kEdgeEpsilon <= chunkZ && chunkZ < chunk.GetMaxZ() - kEdgeEpsilon) {
|
|
return chunk.GetSceneIDAtLocal(chunkX - chunk.offsetX, chunkZ - chunk.offsetZ);
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
|
|
if (chunkGrid.empty()) {
|
|
for (const auto& chunk : chunks) {
|
|
if (const auto sceneID = sceneInChunk(chunk)) return sceneID;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
// The grid cell the position falls in, checked with the client's test; for a position on a float edge the
|
|
// neighbouring cells are checked too
|
|
const auto toCell = [](float pos, float min, float span, uint32_t count) {
|
|
const auto cell = static_cast<int32_t>(std::floor((pos + kEdgeEpsilon - min) / span));
|
|
return std::clamp(cell, 0, static_cast<int32_t>(count) - 1);
|
|
};
|
|
const int32_t cellX = toCell(posX, minBoundsX, chunkSpanX, chunkGridCountX);
|
|
const int32_t cellZ = toCell(posZ, minBoundsZ, chunkSpanZ, chunkGridCountZ);
|
|
for (const int32_t offsetX : { 0, -1, 1 }) {
|
|
for (const int32_t offsetZ : { 0, -1, 1 }) {
|
|
const int32_t x = cellX + offsetX;
|
|
const int32_t z = cellZ + offsetZ;
|
|
if (x < 0 || z < 0 || x >= static_cast<int32_t>(chunkGridCountX) || z >= static_cast<int32_t>(chunkGridCountZ)) continue;
|
|
const int32_t index = chunkGrid[static_cast<size_t>(x) * chunkGridCountZ + z];
|
|
if (index < 0) continue;
|
|
if (const auto sceneID = sceneInChunk(chunks[index])) return sceneID;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
void GenerateTerrainMesh(const Raw& raw, TerrainMesh& outMesh) {
|
|
outMesh.vertices.clear();
|
|
outMesh.triangles.clear();
|
|
|
|
if (raw.chunks.empty() || raw.version < 32) {
|
|
return; // No scene data available
|
|
}
|
|
|
|
LOG_DEBUG("GenerateTerrainMesh: Processing %zu chunks", raw.chunks.size());
|
|
|
|
uint32_t vertexOffset = 0;
|
|
|
|
for (const auto& chunk : raw.chunks) {
|
|
if (!chunk.IsValidForSceneLookup()) continue;
|
|
|
|
// One vertex per heightmap point, in [i * height + j] order
|
|
for (uint32_t i = 0; i < chunk.width; ++i) {
|
|
for (uint32_t j = 0; j < chunk.height; ++j) {
|
|
outMesh.vertices.emplace_back(chunk.GridToWorldPos(i, j), chunk.GetSceneIDAtGrid(i, j));
|
|
if (i > 0 && j > 0) {
|
|
const uint32_t currentVert = vertexOffset + chunk.height * i + j;
|
|
const uint32_t leftVert = currentVert - 1;
|
|
const uint32_t bottomLeftVert = vertexOffset + chunk.height * (i - 1) + j - 1;
|
|
const uint32_t bottomVert = vertexOffset + chunk.height * (i - 1) + j;
|
|
|
|
// First triangle
|
|
outMesh.triangles.push_back(currentVert);
|
|
outMesh.triangles.push_back(leftVert);
|
|
outMesh.triangles.push_back(bottomLeftVert);
|
|
|
|
// Second triangle
|
|
outMesh.triangles.push_back(bottomLeftVert);
|
|
outMesh.triangles.push_back(bottomVert);
|
|
outMesh.triangles.push_back(currentVert);
|
|
}
|
|
}
|
|
}
|
|
|
|
vertexOffset += chunk.width * chunk.height;
|
|
}
|
|
}
|
|
|
|
bool WriteTerrainMeshToOBJ(const TerrainMesh& mesh, const std::string& path) {
|
|
try {
|
|
std::ofstream file(path);
|
|
if (!file.is_open()) {
|
|
LOG("Failed to open OBJ file for writing: %s", path.c_str());
|
|
return false;
|
|
}
|
|
|
|
for (const auto& v : mesh.vertices) {
|
|
const NiColor& color = SceneColor::Get(v.sceneID);
|
|
file << "v " << v.position.x << ' ' << v.position.y << ' ' << v.position.z
|
|
<< ' ' << color.m_Red << ' ' << color.m_Green << ' ' << color.m_Blue << '\n';
|
|
}
|
|
|
|
for (size_t i = 0; i < mesh.triangles.size(); i += 3) {
|
|
file << "f " << (mesh.triangles[i] + 1) << ' '
|
|
<< (mesh.triangles[i + 1] + 1) << ' '
|
|
<< (mesh.triangles[i + 2] + 1) << '\n';
|
|
}
|
|
|
|
file.close();
|
|
LOG("Successfully wrote terrain mesh to OBJ: %s (%zu vertices, %zu triangles)",
|
|
path.c_str(), mesh.vertices.size(), mesh.triangles.size() / 3);
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
LOG("Exception while writing OBJ file: %s", e.what());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
} // namespace Raw
|