Kelsidavis-WoWee/src/pipeline/terrain_mesh.cpp
Kelsi e12141a673 Add configurable MSAA anti-aliasing, update auth screen and terrain shader
- MSAA: conditional 2-att (off) vs 3-att (on) render pass with auto-resolve
- MSAA: multisampled color+depth images, query max supported sample count
- MSAA: .setMultisample() on all 25+ main-pass pipelines across 17 renderers
- MSAA: recreatePipelines() on every sub-renderer for runtime MSAA changes
- MSAA: Renderer::setMsaaSamples() orchestrates swapchain+pipeline+ImGui rebuild
- MSAA: Anti-Aliasing combo (Off/2x/4x/8x) in Video settings, persisted
- Update auth screen assets and terrain fragment shader
2026-02-22 02:59:24 -08:00

367 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "pipeline/terrain_mesh.hpp"
#include "core/coordinates.hpp"
#include "core/logger.hpp"
#include <cmath>
namespace wowee {
namespace pipeline {
TerrainMesh TerrainMeshGenerator::generate(const ADTTerrain& terrain) {
TerrainMesh mesh;
if (!terrain.isLoaded()) {
LOG_WARNING("Attempting to generate mesh from unloaded terrain");
return mesh;
}
// Copy texture list
mesh.textures = terrain.textures;
// Generate mesh for each chunk
int validCount = 0;
bool loggedFirstChunk = false;
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
const MapChunk& chunk = terrain.getChunk(x, y);
if (chunk.hasHeightMap()) {
mesh.getChunk(x, y) = generateChunkMesh(chunk, x, y, terrain.coord.x, terrain.coord.y);
validCount++;
// Debug: log first chunk world position
if (!loggedFirstChunk) {
loggedFirstChunk = true;
LOG_DEBUG("First terrain chunk world pos: (", chunk.position[0], ", ",
chunk.position[1], ", ", chunk.position[2], ")");
}
}
}
}
mesh.validChunkCount = validCount;
return mesh;
}
ChunkMesh TerrainMeshGenerator::generateChunkMesh(const MapChunk& chunk, int chunkX, int chunkY, int tileX, int tileY) {
ChunkMesh mesh;
mesh.chunkX = chunkX;
mesh.chunkY = chunkY;
// Compute render-space XY from tile/chunk indices (MCNK position fields are unreliable).
// tileX increases southward (renderY axis), tileY increases eastward (renderX axis).
// NW corner of tile: renderX = (32-tileY)*TILE_SIZE, renderY = (32-tileX)*TILE_SIZE
// Each chunk step goes east (renderX) or south (renderY).
const float tileNW_renderX = (32.0f - static_cast<float>(tileY)) * core::coords::TILE_SIZE;
const float tileNW_renderY = (32.0f - static_cast<float>(tileX)) * core::coords::TILE_SIZE;
mesh.worldX = tileNW_renderX - static_cast<float>(chunkY) * CHUNK_SIZE; // iy controls renderX (east-west)
mesh.worldY = tileNW_renderY - static_cast<float>(chunkX) * CHUNK_SIZE; // ix controls renderY (north-south)
mesh.worldZ = chunk.position[2]; // height base (wowZ) from MCNK offset 112
// Debug: log chunk positions for first tile
static int posLogCount = 0;
if (posLogCount < 5) {
posLogCount++;
LOG_INFO("Terrain chunk: tile(", tileX, ",", tileY, ") ix=", chunkX, " iy=", chunkY,
" worldXY=(", mesh.worldX, ",", mesh.worldY, ",", mesh.worldZ, ")",
" mcnk=(", chunk.position[0], ",", chunk.position[1], ",", chunk.position[2], ")");
}
// Generate vertices from heightmap (pass chunk grid indices and tile coords)
mesh.vertices = generateVertices(chunk, chunkX, chunkY, tileX, tileY);
// Generate triangle indices (checks for holes)
mesh.indices = generateIndices(chunk);
// Debug: verify mesh integrity (one-time)
static bool debugLogged = false;
if (!debugLogged && chunkX == 0 && chunkY == 0) {
debugLogged = true;
LOG_INFO("Terrain mesh debug: ", mesh.vertices.size(), " vertices, ",
mesh.indices.size(), " indices (", mesh.indices.size() / 3, " triangles)");
// Verify all indices are in bounds
int maxIndex = 0;
int minIndex = 9999;
for (auto idx : mesh.indices) {
if (static_cast<int>(idx) > maxIndex) maxIndex = idx;
if (static_cast<int>(idx) < minIndex) minIndex = idx;
}
LOG_INFO("Index range: [", minIndex, ", ", maxIndex, "] (expected [0, 144])");
if (maxIndex >= static_cast<int>(mesh.vertices.size())) {
LOG_ERROR("INDEX OUT OF BOUNDS! Max index ", maxIndex, " >= vertex count ", mesh.vertices.size());
}
// Check for invalid vertex positions
int invalidCount = 0;
for (size_t i = 0; i < mesh.vertices.size(); i++) {
const auto& v = mesh.vertices[i];
if (!std::isfinite(v.position[0]) || !std::isfinite(v.position[1]) || !std::isfinite(v.position[2])) {
invalidCount++;
}
}
if (invalidCount > 0) {
LOG_ERROR("Found ", invalidCount, " vertices with invalid positions!");
}
}
// Copy texture layers
for (size_t layerIdx = 0; layerIdx < chunk.layers.size(); layerIdx++) {
const auto& layer = chunk.layers[layerIdx];
ChunkMesh::LayerInfo layerInfo;
layerInfo.textureId = layer.textureId;
layerInfo.flags = layer.flags;
// Extract alpha data for this layer if it has alpha
if (layer.useAlpha() && layer.offsetMCAL < chunk.alphaMap.size()) {
size_t offset = layer.offsetMCAL;
// Compute actual per-layer size from next layer's offset (not total remaining)
size_t layerSize;
bool foundNext = false;
for (size_t j = layerIdx + 1; j < chunk.layers.size(); j++) {
if (chunk.layers[j].useAlpha()) {
layerSize = chunk.layers[j].offsetMCAL - offset;
foundNext = true;
break;
}
}
if (!foundNext) {
layerSize = chunk.alphaMap.size() - offset;
}
if (layer.compressedAlpha()) {
// Decompress RLE-compressed alpha map to 64x64 = 4096 bytes
layerInfo.alphaData.resize(4096, 0);
size_t readPos = offset;
size_t writePos = 0;
while (writePos < 4096 && readPos < chunk.alphaMap.size()) {
uint8_t cmd = chunk.alphaMap[readPos++];
bool fill = (cmd & 0x80) != 0;
int count = (cmd & 0x7F) + 1;
if (fill) {
if (readPos < chunk.alphaMap.size()) {
uint8_t val = chunk.alphaMap[readPos++];
for (int i = 0; i < count && writePos < 4096; i++) {
layerInfo.alphaData[writePos++] = val;
}
}
} else {
for (int i = 0; i < count && writePos < 4096 && readPos < chunk.alphaMap.size(); i++) {
layerInfo.alphaData[writePos++] = chunk.alphaMap[readPos++];
}
}
}
} else if (layerSize >= 4096) {
// Big alpha: 64x64 at 8-bit = 4096 bytes
layerInfo.alphaData.resize(4096);
std::copy(chunk.alphaMap.begin() + offset,
chunk.alphaMap.begin() + offset + 4096,
layerInfo.alphaData.begin());
} else if (layerSize >= 2048) {
// Non-big alpha: 2048 bytes = 4-bit per texel, 64x64
// Each byte: low nibble = first texel, high nibble = second texel
// Scale 0-15 to 0-255 (multiply by 17)
layerInfo.alphaData.resize(4096);
for (size_t i = 0; i < 2048; i++) {
uint8_t byte = chunk.alphaMap[offset + i];
layerInfo.alphaData[i * 2] = (byte & 0x0F) * 17;
layerInfo.alphaData[i * 2 + 1] = (byte >> 4) * 17;
}
}
}
mesh.layers.push_back(layerInfo);
}
return mesh;
}
std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk& chunk, int chunkX, int chunkY, int tileX, int tileY) {
std::vector<TerrainVertex> vertices;
vertices.reserve(145); // 145 vertices total
const HeightMap& heightMap = chunk.heightMap;
// WoW terrain uses 145 heights stored in a 9x17 row-major grid layout
const float unitSize = CHUNK_SIZE / 8.0f; // 33.333/8 units per vertex step
// Compute render-space base from tile/chunk indices (same formula as generateChunkMesh).
const float tileNW_renderX = (32.0f - static_cast<float>(tileY)) * core::coords::TILE_SIZE;
const float tileNW_renderY = (32.0f - static_cast<float>(tileX)) * core::coords::TILE_SIZE;
float chunkBaseX = tileNW_renderX - static_cast<float>(chunkY) * CHUNK_SIZE; // iy controls renderX (east-west)
float chunkBaseY = tileNW_renderY - static_cast<float>(chunkX) * CHUNK_SIZE; // ix controls renderY (north-south)
float chunkBaseZ = chunk.position[2]; // height base (wowZ) from MCNK offset 112
for (int index = 0; index < 145; index++) {
int y = index / 17; // Row (0-8)
int x = index % 17; // Column (0-16)
// Columns 9-16 are offset by 0.5 units (wowee exact logic)
float offsetX = static_cast<float>(x);
float offsetY = static_cast<float>(y);
if (x > 8) {
offsetY += 0.5f;
offsetX -= 8.5f;
}
TerrainVertex vertex;
// Position in render space:
// MCVT rows (offsetY) go west→east = renderX decreasing
// MCVT columns (offsetX) go north→south = renderY decreasing
vertex.position[0] = chunkBaseX - (offsetY * unitSize); // renderX (row = west→east)
vertex.position[1] = chunkBaseY - (offsetX * unitSize); // renderY (col = north→south)
vertex.position[2] = chunkBaseZ + heightMap.heights[index]; // renderZ
// Normal
if (index * 3 + 2 < static_cast<int>(chunk.normals.size())) {
decompressNormal(&chunk.normals[index * 3], vertex.normal);
} else {
// Default up normal
vertex.normal[0] = 0.0f;
vertex.normal[1] = 0.0f;
vertex.normal[2] = 1.0f;
}
// Texture coordinates: world-aligned so patterns don't reset per chunk.
// Keep one texture repeat per chunk (matches prior scale).
constexpr float texScale = 1.0f / CHUNK_SIZE;
vertex.texCoord[0] = -vertex.position[1] * texScale;
vertex.texCoord[1] = -vertex.position[0] * texScale;
// Layer UV for alpha map sampling (0-1 range per chunk).
// Sample at texel centers of the 64x64 alpha map to avoid edge seams.
constexpr float alphaTexels = 64.0f;
constexpr float alphaStep = (alphaTexels - 1.0f) / 8.0f; // 63 texels across 8 quads
vertex.layerUV[0] = (offsetX * alphaStep + 0.5f) / alphaTexels;
vertex.layerUV[1] = (offsetY * alphaStep + 0.5f) / alphaTexels;
vertices.push_back(vertex);
}
return vertices;
}
std::vector<TerrainIndex> TerrainMeshGenerator::generateIndices(const MapChunk& chunk) {
std::vector<TerrainIndex> indices;
indices.reserve(768); // 8x8 quads * 4 triangles * 3 indices = 768
// Generate indices based on 9x17 grid layout (matching wowee.js)
// Each quad uses a center vertex with 4 surrounding vertices
// Index offsets from center: -9, -8, +9, +8
int holesSkipped = 0;
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++) {
// Skip quads that are marked as holes (cave entrances, etc.)
if (chunk.isHole(y, x)) {
holesSkipped++;
continue;
}
// Center vertex index in the 9x17 grid
int center = 9 + y * 17 + x;
// Four triangles per quad
// Using CCW winding when viewed from +Z (top-down)
int tl = center - 9; // top-left outer
int tr = center - 8; // top-right outer
int bl = center + 8; // bottom-left outer
int br = center + 9; // bottom-right outer
// Triangle 1: top (center, tl, tr)
indices.push_back(center);
indices.push_back(tl);
indices.push_back(tr);
// Triangle 2: right (center, tr, br)
indices.push_back(center);
indices.push_back(tr);
indices.push_back(br);
// Triangle 3: bottom (center, br, bl)
indices.push_back(center);
indices.push_back(br);
indices.push_back(bl);
// Triangle 4: left (center, bl, tl)
indices.push_back(center);
indices.push_back(bl);
indices.push_back(tl);
}
}
// Debug: log if any holes were skipped (one-time per session)
static bool holesLogged = false;
if (!holesLogged && holesSkipped > 0) {
holesLogged = true;
LOG_INFO("Terrain holes: skipped ", holesSkipped, " quads due to hole mask (holes=0x",
std::hex, chunk.holes, std::dec, ")");
}
return indices;
}
void TerrainMeshGenerator::calculateTexCoords(TerrainVertex& vertex, int x, int y) {
// Base texture coordinates (0-1 range across chunk)
vertex.texCoord[0] = x / 16.0f;
vertex.texCoord[1] = y / 16.0f;
// Layer UVs (same as base for now)
vertex.layerUV[0] = vertex.texCoord[0];
vertex.layerUV[1] = vertex.texCoord[1];
}
void TerrainMeshGenerator::decompressNormal(const int8_t* compressedNormal, float* normal) {
// WoW stores normals as signed bytes (-127 to 127)
// Convert to float and normalize
float x = compressedNormal[0] / 127.0f;
float y = compressedNormal[1] / 127.0f;
float z = compressedNormal[2] / 127.0f;
// Normalize
float length = std::sqrt(x * x + y * y + z * z);
if (length > 0.0001f) {
normal[0] = x / length;
normal[1] = y / length;
normal[2] = z / length;
} else {
// Default up normal if degenerate
normal[0] = 0.0f;
normal[1] = 0.0f;
normal[2] = 1.0f;
}
}
int TerrainMeshGenerator::getVertexIndex(int x, int y) {
// Convert virtual grid position (0-16) to actual vertex index (0-144)
// Outer vertices (even positions): 0-80 (9x9 grid)
// Inner vertices (odd positions): 81-144 (8x8 grid)
bool isOuter = (y % 2 == 0) && (x % 2 == 0);
bool isInner = (y % 2 == 1) && (x % 2 == 1);
if (isOuter) {
int gridX = x / 2;
int gridY = y / 2;
return gridY * 9 + gridX; // 0-80
} else if (isInner) {
int gridX = (x - 1) / 2;
int gridY = (y - 1) / 2;
return 81 + gridY * 8 + gridX; // 81-144
}
return -1; // Invalid position
}
} // namespace pipeline
} // namespace wowee