Kelsidavis-WoWee/src/pipeline/adt_loader.cpp
Kelsi 3e6de8485b Fix ground clutter height sampling and terrain shader GPU crash
- Fix HeightMap::getHeight() to use interleaved 17-wide row layout
  matching MCVT storage (was using wrong 9-wide contiguous indexing)
- Guard terrain bump mapping normalize against zero-length vectors
  to prevent NaN propagation and GPU faults
2026-02-23 06:46:31 -08:00

682 lines
24 KiB
C++

#include "pipeline/adt_loader.hpp"
#include "core/logger.hpp"
#include <cstring>
#include <cmath>
#include <algorithm>
namespace wowee {
namespace pipeline {
// HeightMap implementation
float HeightMap::getHeight(int x, int y) const {
if (x < 0 || x > 8 || y < 0 || y > 8) {
return 0.0f;
}
// MCVT heights are stored in interleaved 9x17 row-major layout:
// Row 0: 9 outer (indices 0-8), then 8 inner (indices 9-16)
// Row 1: 9 outer (indices 17-25), then 8 inner (indices 26-33)
// ...
// Outer vertex (x, y) is at index: y * 17 + x
int index = y * 17 + x;
if (index < 0 || index >= 145) return 0.0f;
return heights[index];
}
// ADTLoader implementation
ADTTerrain ADTLoader::load(const std::vector<uint8_t>& adtData) {
ADTTerrain terrain;
if (adtData.empty()) {
LOG_ERROR("Empty ADT data");
return terrain;
}
LOG_DEBUG("Loading ADT terrain (", adtData.size(), " bytes)");
size_t offset = 0;
int chunkIndex = 0;
// Parse chunks
int totalChunks = 0;
while (offset < adtData.size()) {
ChunkHeader header;
if (!readChunkHeader(adtData.data(), offset, adtData.size(), header)) {
break;
}
const uint8_t* chunkData = adtData.data() + offset + 8;
size_t chunkSize = header.size;
totalChunks++;
if (totalChunks <= 5) {
// Log first few chunks for debugging
char magic[5] = {0};
std::memcpy(magic, &header.magic, 4);
}
// Parse based on chunk type
if (header.magic == MVER) {
parseMVER(chunkData, chunkSize, terrain);
}
else if (header.magic == MTEX) {
parseMTEX(chunkData, chunkSize, terrain);
}
else if (header.magic == MMDX) {
parseMMDX(chunkData, chunkSize, terrain);
}
else if (header.magic == MWMO) {
parseMWMO(chunkData, chunkSize, terrain);
}
else if (header.magic == MDDF) {
parseMDDF(chunkData, chunkSize, terrain);
}
else if (header.magic == MODF) {
parseMODF(chunkData, chunkSize, terrain);
}
else if (header.magic == MH2O) {
LOG_DEBUG("Found MH2O chunk (", chunkSize, " bytes)");
parseMH2O(chunkData, chunkSize, terrain);
}
else if (header.magic == MCNK) {
parseMCNK(chunkData, chunkSize, chunkIndex++, terrain);
}
// Move to next chunk
offset += 8 + chunkSize;
}
terrain.loaded = true;
return terrain;
}
bool ADTLoader::readChunkHeader(const uint8_t* data, size_t offset, size_t dataSize, ChunkHeader& header) {
if (offset + 8 > dataSize) {
return false;
}
header.magic = readUInt32(data, offset);
header.size = readUInt32(data, offset + 4);
// Validate chunk size
if (offset + 8 + header.size > dataSize) {
LOG_WARNING("Chunk extends beyond file: magic=0x", std::hex, header.magic,
", size=", std::dec, header.size);
return false;
}
return true;
}
uint32_t ADTLoader::readUInt32(const uint8_t* data, size_t offset) {
uint32_t value;
std::memcpy(&value, data + offset, sizeof(uint32_t));
return value;
}
float ADTLoader::readFloat(const uint8_t* data, size_t offset) {
float value;
std::memcpy(&value, data + offset, sizeof(float));
return value;
}
uint16_t ADTLoader::readUInt16(const uint8_t* data, size_t offset) {
uint16_t value;
std::memcpy(&value, data + offset, sizeof(uint16_t));
return value;
}
void ADTLoader::parseMVER(const uint8_t* data, size_t size, ADTTerrain& terrain) {
if (size < 4) {
LOG_WARNING("MVER chunk too small");
return;
}
terrain.version = readUInt32(data, 0);
LOG_DEBUG("ADT version: ", terrain.version);
}
void ADTLoader::parseMTEX(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MTEX contains null-terminated texture filenames
size_t offset = 0;
while (offset < size) {
const char* textureName = reinterpret_cast<const char*>(data + offset);
size_t nameLen = std::strlen(textureName);
if (nameLen == 0) {
break;
}
terrain.textures.push_back(std::string(textureName, nameLen));
offset += nameLen + 1; // +1 for null terminator
}
LOG_DEBUG("Loaded ", terrain.textures.size(), " texture names");
}
void ADTLoader::parseMMDX(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MMDX contains null-terminated M2 model filenames
size_t offset = 0;
while (offset < size) {
const char* modelName = reinterpret_cast<const char*>(data + offset);
size_t nameLen = std::strlen(modelName);
if (nameLen == 0) {
break;
}
terrain.doodadNames.push_back(std::string(modelName, nameLen));
offset += nameLen + 1;
}
LOG_DEBUG("Loaded ", terrain.doodadNames.size(), " doodad names");
}
void ADTLoader::parseMWMO(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MWMO contains null-terminated WMO filenames
size_t offset = 0;
while (offset < size) {
const char* wmoName = reinterpret_cast<const char*>(data + offset);
size_t nameLen = std::strlen(wmoName);
if (nameLen == 0) {
break;
}
terrain.wmoNames.push_back(std::string(wmoName, nameLen));
offset += nameLen + 1;
}
LOG_DEBUG("Loaded ", terrain.wmoNames.size(), " WMO names from MWMO chunk");
for (size_t i = 0; i < terrain.wmoNames.size(); i++) {
LOG_DEBUG(" WMO[", i, "]: ", terrain.wmoNames[i]);
// Flag potential duplicate cathedral models
if (terrain.wmoNames[i].find("cathedral") != std::string::npos ||
terrain.wmoNames[i].find("Cathedral") != std::string::npos) {
LOG_DEBUG("*** CATHEDRAL WMO FOUND: ", terrain.wmoNames[i]);
}
}
}
void ADTLoader::parseMDDF(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MDDF contains doodad placements (36 bytes each)
const size_t entrySize = 36;
size_t count = size / entrySize;
for (size_t i = 0; i < count; i++) {
size_t offset = i * entrySize;
ADTTerrain::DoodadPlacement placement;
placement.nameId = readUInt32(data, offset);
placement.uniqueId = readUInt32(data, offset + 4);
placement.position[0] = readFloat(data, offset + 8);
placement.position[1] = readFloat(data, offset + 12);
placement.position[2] = readFloat(data, offset + 16);
placement.rotation[0] = readFloat(data, offset + 20);
placement.rotation[1] = readFloat(data, offset + 24);
placement.rotation[2] = readFloat(data, offset + 28);
placement.scale = readUInt16(data, offset + 32);
placement.flags = readUInt16(data, offset + 34);
terrain.doodadPlacements.push_back(placement);
}
LOG_DEBUG("Loaded ", terrain.doodadPlacements.size(), " doodad placements");
}
void ADTLoader::parseMODF(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MODF contains WMO placements (64 bytes each)
const size_t entrySize = 64;
size_t count = size / entrySize;
for (size_t i = 0; i < count; i++) {
size_t offset = i * entrySize;
ADTTerrain::WMOPlacement placement;
placement.nameId = readUInt32(data, offset);
placement.uniqueId = readUInt32(data, offset + 4);
placement.position[0] = readFloat(data, offset + 8);
placement.position[1] = readFloat(data, offset + 12);
placement.position[2] = readFloat(data, offset + 16);
placement.rotation[0] = readFloat(data, offset + 20);
placement.rotation[1] = readFloat(data, offset + 24);
placement.rotation[2] = readFloat(data, offset + 28);
placement.extentLower[0] = readFloat(data, offset + 32);
placement.extentLower[1] = readFloat(data, offset + 36);
placement.extentLower[2] = readFloat(data, offset + 40);
placement.extentUpper[0] = readFloat(data, offset + 44);
placement.extentUpper[1] = readFloat(data, offset + 48);
placement.extentUpper[2] = readFloat(data, offset + 52);
placement.flags = readUInt16(data, offset + 56);
placement.doodadSet = readUInt16(data, offset + 58);
terrain.wmoPlacements.push_back(placement);
// Log STORMWIND.WMO placements to detect duplicates at different Z heights
if (placement.nameId < terrain.wmoNames.size()) {
const std::string& wmoName = terrain.wmoNames[placement.nameId];
std::string upperName = wmoName;
std::transform(upperName.begin(), upperName.end(), upperName.begin(), ::toupper);
if (upperName.find("STORMWIND.WMO") != std::string::npos) {
LOG_DEBUG("*** STORMWIND.WMO PLACEMENT:",
" uniqueId=", placement.uniqueId,
" pos=(", placement.position[0], ", ", placement.position[1], ", ", placement.position[2], ")",
" rot=(", placement.rotation[0], ", ", placement.rotation[1], ", ", placement.rotation[2], ")",
" doodadSet=", placement.doodadSet,
" flags=0x", std::hex, placement.flags, std::dec);
}
}
}
LOG_DEBUG("Loaded ", terrain.wmoPlacements.size(), " WMO placements");
}
void ADTLoader::parseMCNK(const uint8_t* data, size_t size, int chunkIndex, ADTTerrain& terrain) {
if (chunkIndex < 0 || chunkIndex >= 256) {
LOG_WARNING("Invalid chunk index: ", chunkIndex);
return;
}
MapChunk& chunk = terrain.chunks[chunkIndex];
// Read MCNK header (128 bytes)
if (size < 128) {
LOG_WARNING("MCNK chunk too small");
return;
}
chunk.flags = readUInt32(data, 0);
chunk.indexX = readUInt32(data, 4);
chunk.indexY = readUInt32(data, 8);
// Read holes mask (at offset 0x3C = 60 in MCNK header)
// Each bit represents a 2x2 block of the 8x8 quad grid
chunk.holes = readUInt16(data, 60);
// Read layer count and offsets from MCNK header
uint32_t nLayers = readUInt32(data, 12);
uint32_t ofsHeight = readUInt32(data, 20); // MCVT offset
uint32_t ofsNormal = readUInt32(data, 24); // MCNR offset
uint32_t ofsLayer = readUInt32(data, 28); // MCLY offset
uint32_t ofsAlpha = readUInt32(data, 36); // MCAL offset
uint32_t sizeAlpha = readUInt32(data, 40);
// Debug first chunk only
if (chunkIndex == 0) {
LOG_DEBUG("MCNK[0] offsets: nLayers=", nLayers,
" height=", ofsHeight, " normal=", ofsNormal,
" layer=", ofsLayer, " alpha=", ofsAlpha,
" sizeAlpha=", sizeAlpha, " size=", size,
" holes=0x", std::hex, chunk.holes, std::dec);
}
// MCNK position is in canonical WoW coordinates (NOT ADT placement space):
// offset 104: wowY (west axis, horizontal — unused, XY computed from tile indices)
// offset 108: wowX (north axis, horizontal — unused, XY computed from tile indices)
// offset 112: wowZ = HEIGHT BASE (MCVT heights are relative to this)
chunk.position[0] = readFloat(data, 104); // wowY (unused)
chunk.position[1] = readFloat(data, 108); // wowX (unused)
chunk.position[2] = readFloat(data, 112); // wowZ = height base
// Parse sub-chunks using offsets from MCNK header
// WoW ADT sub-chunks may have their own 8-byte headers (magic+size)
// Check by inspecting the first 4 bytes at the offset
// Height map (MCVT) - 145 floats = 580 bytes
if (ofsHeight > 0 && ofsHeight + 580 <= size) {
// Check if this points to a sub-chunk header (magic "MCVT" = 0x4D435654)
uint32_t possibleMagic = readUInt32(data, ofsHeight);
uint32_t headerSkip = 0;
if (possibleMagic == MCVT) {
headerSkip = 8; // Skip magic + size
if (chunkIndex == 0) {
LOG_DEBUG("MCNK sub-chunks have headers (MCVT magic found at offset ", ofsHeight, ")");
}
}
parseMCVT(data + ofsHeight + headerSkip, 580, chunk);
}
// Normals (MCNR) - 145 normals (3 bytes each) + 13 padding = 448 bytes
if (ofsNormal > 0 && ofsNormal + 448 <= size) {
uint32_t possibleMagic = readUInt32(data, ofsNormal);
uint32_t skip = (possibleMagic == MCNR) ? 8 : 0;
parseMCNR(data + ofsNormal + skip, 448, chunk);
}
// Texture layers (MCLY) - 16 bytes per layer
if (ofsLayer > 0 && nLayers > 0) {
size_t layerSize = nLayers * 16;
uint32_t possibleMagic = readUInt32(data, ofsLayer);
uint32_t skip = (possibleMagic == MCLY) ? 8 : 0;
if (ofsLayer + skip + layerSize <= size) {
parseMCLY(data + ofsLayer + skip, layerSize, chunk);
}
}
// Alpha maps (MCAL) - variable size from header
if (ofsAlpha > 0 && sizeAlpha > 0 && ofsAlpha + sizeAlpha <= size) {
uint32_t possibleMagic = readUInt32(data, ofsAlpha);
uint32_t skip = (possibleMagic == MCAL) ? 8 : 0;
parseMCAL(data + ofsAlpha + skip, sizeAlpha - skip, chunk);
}
// Liquid (MCLQ) - vanilla/TBC per-chunk water (no MH2O in these expansions)
// ofsLiquid at MCNK header offset 0x60, sizeLiquid at 0x64
uint32_t ofsLiquid = readUInt32(data, 0x60);
uint32_t sizeLiquid = readUInt32(data, 0x64);
if (ofsLiquid > 0 && sizeLiquid > 8 && ofsLiquid + sizeLiquid <= size) {
uint32_t possibleMagic = readUInt32(data, ofsLiquid);
uint32_t skip = (possibleMagic == MCLQ) ? 8 : 0;
parseMCLQ(data + ofsLiquid + skip, sizeLiquid - skip,
chunkIndex, chunk.flags, terrain);
}
}
void ADTLoader::parseMCVT(const uint8_t* data, size_t size, MapChunk& chunk) {
// MCVT contains 145 height values (floats)
if (size < 145 * sizeof(float)) {
LOG_WARNING("MCVT chunk too small: ", size, " bytes");
return;
}
float minHeight = 999999.0f;
float maxHeight = -999999.0f;
for (int i = 0; i < 145; i++) {
float height = readFloat(data, i * sizeof(float));
chunk.heightMap.heights[i] = height;
if (height < minHeight) minHeight = height;
if (height > maxHeight) maxHeight = height;
}
chunk.heightMap.loaded = true;
// Log height range for first chunk only
static bool logged = false;
if (!logged) {
LOG_INFO("MCVT height range: [", minHeight, ", ", maxHeight, "]",
" (heights[0]=", chunk.heightMap.heights[0],
" heights[8]=", chunk.heightMap.heights[8],
" heights[136]=", chunk.heightMap.heights[136],
" heights[144]=", chunk.heightMap.heights[144], ")");
logged = true;
}
}
void ADTLoader::parseMCNR(const uint8_t* data, size_t size, MapChunk& chunk) {
// MCNR contains 145 normals (3 bytes each, signed)
if (size < 145 * 3) {
LOG_WARNING("MCNR chunk too small: ", size, " bytes");
return;
}
for (int i = 0; i < 145 * 3; i++) {
chunk.normals[i] = static_cast<int8_t>(data[i]);
}
}
void ADTLoader::parseMCLY(const uint8_t* data, size_t size, MapChunk& chunk) {
// MCLY contains texture layer definitions (16 bytes each)
size_t layerCount = size / 16;
if (layerCount > 4) {
LOG_WARNING("More than 4 texture layers: ", layerCount);
layerCount = 4;
}
static int layerLogCount = 0;
for (size_t i = 0; i < layerCount; i++) {
TextureLayer layer;
layer.textureId = readUInt32(data, i * 16 + 0);
layer.flags = readUInt32(data, i * 16 + 4);
layer.offsetMCAL = readUInt32(data, i * 16 + 8);
layer.effectId = readUInt32(data, i * 16 + 12);
if (layerLogCount < 10) {
LOG_DEBUG(" MCLY[", i, "]: texId=", layer.textureId,
" flags=0x", std::hex, layer.flags, std::dec,
" alphaOfs=", layer.offsetMCAL,
" useAlpha=", layer.useAlpha(),
" compressed=", layer.compressedAlpha());
layerLogCount++;
}
chunk.layers.push_back(layer);
}
}
void ADTLoader::parseMCAL(const uint8_t* data, size_t size, MapChunk& chunk) {
// MCAL contains alpha maps for texture layers
// Store raw data; decompression happens per-layer during mesh generation
chunk.alphaMap.resize(size);
std::memcpy(chunk.alphaMap.data(), data, size);
}
void ADTLoader::parseMCLQ(const uint8_t* data, size_t size, int chunkIndex,
uint32_t mcnkFlags, ADTTerrain& terrain) {
// MCLQ: Vanilla/TBC per-chunk liquid data (inside MCNK)
// Layout:
// float minHeight, maxHeight (8 bytes)
// SLiquidVertex[9*9] (81 * 8 = 648 bytes)
// water: uint8 depth, flow0, flow1, filler, float height
// magma: uint16 s, uint16 t, float height
// uint8 tiles[8*8] (64 bytes)
// Total minimum: 720 bytes
if (size < 720) {
return; // Not enough data for a valid MCLQ
}
float minHeight = readFloat(data, 0);
float maxHeight = readFloat(data, 4);
// Determine liquid type from MCNK flags
// 0x04 = has liquid (river/lake), 0x08 = ocean, 0x10 = magma, 0x20 = slime
uint16_t liquidType = 0; // water
if (mcnkFlags & 0x08) liquidType = 1; // ocean
else if (mcnkFlags & 0x10) liquidType = 2; // magma
else if (mcnkFlags & 0x20) liquidType = 3; // slime
// Read 9x9 height values (skip depth/flow bytes, just read the float height)
const uint8_t* vertData = data + 8;
std::vector<float> heights(81);
for (int i = 0; i < 81; i++) {
heights[i] = readFloat(vertData, i * 8 + 4); // float at offset 4 within each 8-byte vertex
}
// Read 8x8 tile flags
const uint8_t* tileData = data + 8 + 648;
std::vector<uint8_t> tileMask(64);
bool anyVisible = false;
for (int i = 0; i < 64; i++) {
uint8_t tileFlag = tileData[i];
// Bit 0x0F = liquid type, bit 0x40 = fatigue, bit 0x80 = hidden
// A tile is visible if NOT hidden (0x80 not set) and type is non-zero or has base flag
bool hidden = (tileFlag & 0x80) != 0;
tileMask[i] = hidden ? 0 : 1;
if (!hidden) anyVisible = true;
}
if (!anyVisible) {
return; // All tiles hidden, no visible water
}
// Validate heights - if all heights are 0 or unreasonable, skip
bool validHeights = false;
for (float h : heights) {
if (h != 0.0f && std::isfinite(h)) {
validHeights = true;
break;
}
}
// If heights are all zero, use maxHeight as flat water level
if (!validHeights) {
for (float& h : heights) h = maxHeight;
}
// Build a WaterLayer matching the MH2O format
ADTTerrain::WaterLayer layer;
layer.liquidType = liquidType;
layer.flags = 0;
layer.minHeight = minHeight;
layer.maxHeight = maxHeight;
layer.x = 0;
layer.y = 0;
layer.width = 8; // 8 tiles = 9 vertices per axis
layer.height = 8;
layer.heights = std::move(heights);
layer.mask.resize(8); // 8 bytes = 64 bits for 8x8 tiles
for (int row = 0; row < 8; row++) {
uint8_t rowBits = 0;
for (int col = 0; col < 8; col++) {
if (tileMask[row * 8 + col]) {
rowBits |= (1 << col);
}
}
layer.mask[row] = rowBits;
}
terrain.waterData[chunkIndex].layers.push_back(std::move(layer));
static int mclqLogCount = 0;
if (mclqLogCount < 5) {
LOG_INFO("MCLQ[", chunkIndex, "]: type=", liquidType,
" height=[", minHeight, ",", maxHeight, "]");
mclqLogCount++;
}
}
void ADTLoader::parseMH2O(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MH2O contains water/liquid data for all 256 map chunks
// Structure: 256 SMLiquidChunk headers followed by instance data
// Each SMLiquidChunk header is 12 bytes (WotLK 3.3.5a):
// - uint32_t offsetInstances (offset from MH2O chunk start)
// - uint32_t layerCount
// - uint32_t offsetAttributes (offset from MH2O chunk start)
const size_t headerSize = 12; // SMLiquidChunk size for WotLK
const size_t totalHeaderSize = 256 * headerSize;
if (size < totalHeaderSize) {
LOG_WARNING("MH2O chunk too small for headers: ", size, " bytes");
return;
}
int totalLayers = 0;
for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) {
size_t headerOffset = chunkIdx * headerSize;
uint32_t offsetInstances = readUInt32(data, headerOffset);
uint32_t layerCount = readUInt32(data, headerOffset + 4);
// uint32_t offsetAttributes = readUInt32(data, headerOffset + 8); // Not used
if (layerCount == 0 || offsetInstances == 0) {
continue; // No water in this chunk
}
// Sanity checks
if (offsetInstances >= size) {
continue;
}
if (layerCount > 16) {
// Sanity check - max 16 layers per chunk is reasonable
LOG_WARNING("MH2O: Invalid layer count ", layerCount, " for chunk ", chunkIdx);
continue;
}
// Parse each liquid layer (SMLiquidInstance - 24 bytes)
for (uint32_t layerIdx = 0; layerIdx < layerCount; layerIdx++) {
size_t instanceOffset = offsetInstances + layerIdx * 24;
if (instanceOffset + 24 > size) {
break;
}
ADTTerrain::WaterLayer layer;
layer.liquidType = readUInt16(data, instanceOffset);
uint16_t liquidObject = readUInt16(data, instanceOffset + 2); // LVF format flags
layer.minHeight = readFloat(data, instanceOffset + 4);
layer.maxHeight = readFloat(data, instanceOffset + 8);
layer.x = data[instanceOffset + 12];
layer.y = data[instanceOffset + 13];
layer.width = data[instanceOffset + 14];
layer.height = data[instanceOffset + 15];
uint32_t offsetExistsBitmap = readUInt32(data, instanceOffset + 16);
uint32_t offsetVertexData = readUInt32(data, instanceOffset + 20);
// Skip invalid layers
if (layer.width == 0 || layer.height == 0) {
continue;
}
// Clamp dimensions to valid range
if (layer.width > 8) layer.width = 8;
if (layer.height > 8) layer.height = 8;
if (layer.x + layer.width > 8) layer.width = 8 - layer.x;
if (layer.y + layer.height > 8) layer.height = 8 - layer.y;
// Read exists bitmap (which tiles have water).
// In WotLK MH2O this is chunk-wide 8x8 tile flags (64 bits = 8 bytes),
// even when the layer covers a sub-rect.
constexpr size_t bitmapBytes = 8;
// Note: offsets in SMLiquidInstance are relative to MH2O chunk start
if (offsetExistsBitmap > 0) {
size_t bitmapOffset = offsetExistsBitmap;
if (bitmapOffset + bitmapBytes <= size) {
layer.mask.resize(bitmapBytes);
std::memcpy(layer.mask.data(), data + bitmapOffset, bitmapBytes);
}
} else {
// No bitmap means all tiles in chunk are valid for this layer.
layer.mask.resize(bitmapBytes, 0xFF);
}
// Read vertex heights
// Number of vertices is (width+1) * (height+1)
size_t numVertices = (layer.width + 1) * (layer.height + 1);
// Check liquid object flags (LVF) to determine vertex format
bool hasHeightData = (liquidObject != 2); // LVF_height_depth or LVF_height_texcoord
if (hasHeightData && offsetVertexData > 0) {
size_t vertexOffset = offsetVertexData;
size_t vertexDataSize = numVertices * sizeof(float);
if (vertexOffset + vertexDataSize <= size) {
layer.heights.resize(numVertices);
for (size_t i = 0; i < numVertices; i++) {
layer.heights[i] = readFloat(data, vertexOffset + i * sizeof(float));
}
} else {
// Offset out of bounds - use flat water
layer.heights.resize(numVertices, layer.minHeight);
}
} else {
// No height data - use flat surface at minHeight
layer.heights.resize(numVertices, layer.minHeight);
}
// Default flags
layer.flags = 0;
terrain.waterData[chunkIdx].layers.push_back(layer);
totalLayers++;
}
}
LOG_DEBUG("Loaded MH2O water data: ", totalLayers, " liquid layers across ", size, " bytes");
}
} // namespace pipeline
} // namespace wowee