Kelsidavis-WoWee/src/rendering/water_renderer.cpp
Kelsi fe728300a9 Add water height adjustment for Stormwind canal overflow
PROBLEM:
Canal water surfaces in Stormwind extend spatially beyond their intended
boundaries, causing water to appear in tunnels and buildings where it
shouldn't be visible. This is likely due to oversized water mesh extents
in the original WoW data.

SOLUTION:
Lower Stormwind canal water by 1 unit to hide it below tunnel/building floors
while keeping boats at reasonable floating height:
- Only affects water in Stormwind area (tiles 28-50, 28-52)
- Only affects water above 94 height (canal level)
- Moonwell exclusion: 20-unit radius around (-8755.9, 1108.9, 96.1)
  to preserve functional moonwell water

HARDCODED VALUES:
- Stormwind area bounds: tiles (28-50, 28-52)
- Height threshold: >94 units (canal level)
- Moonwell position: (-8755.9, 1108.9, 96.1) with 20-unit exclusion
- Lowering amount: 1 unit

WHY THIS IS HACKY:
- Zone-specific logic hardcoded for Stormwind coordinates
- Position-based moonwell exclusion uses hardcoded world coordinates
- Height threshold is a magic number tuned by trial
- Doesn't fix root cause (oversized water surface meshes)
- Some park water may still be visible

PROPER FIX WOULD BE:
- Trim water surface meshes to actual canal boundaries in ADT/WMO data
- Or implement spatial clipping of water surfaces at render time

This is a pragmatic workaround that improves the situation.
2026-02-09 21:39:33 -08:00

894 lines
34 KiB
C++

#include "rendering/water_renderer.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "pipeline/adt_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
#include <cmath>
#include <limits>
namespace wowee {
namespace rendering {
WaterRenderer::WaterRenderer() = default;
WaterRenderer::~WaterRenderer() {
shutdown();
}
bool WaterRenderer::initialize() {
LOG_INFO("Initializing water renderer");
// Create water shader
waterShader = std::make_unique<Shader>();
// Vertex shader
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform float time;
uniform float waveAmp;
uniform float waveFreq;
uniform float waveSpeed;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
out float WaveOffset;
void main() {
vec3 pos = aPos;
// Pseudo-random phase offsets to break up regular pattern
float hash1 = fract(sin(dot(aPos.xy, vec2(12.9898, 78.233))) * 43758.5453);
float hash2 = fract(sin(dot(aPos.xy, vec2(93.9898, 67.345))) * 27153.5328);
// Multiple wave octaves with randomized phases for natural variation
float w1 = sin((aPos.x + time * waveSpeed + hash1 * 6.28) * waveFreq) * waveAmp;
float w2 = cos((aPos.y - time * (waveSpeed * 0.78) + hash2 * 6.28) * (waveFreq * 0.82)) * (waveAmp * 0.72);
// Add higher frequency detail waves (smaller amplitude)
float w3 = sin((aPos.x * 1.7 - time * waveSpeed * 1.3 + hash1 * 3.14) * waveFreq * 2.1) * (waveAmp * 0.35);
float w4 = cos((aPos.y * 1.4 + time * waveSpeed * 0.9 + hash2 * 3.14) * waveFreq * 1.8) * (waveAmp * 0.28);
float wave = w1 + w2 + w3 + w4;
pos.z += wave;
FragPos = vec3(model * vec4(pos, 1.0));
// Use mat3(model) directly - avoids expensive inverse() per vertex
Normal = mat3(model) * aNormal;
TexCoord = aTexCoord;
WaveOffset = wave;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
)";
// Fragment shader
const char* fragmentShaderSource = R"(
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
in float WaveOffset;
uniform vec3 viewPos;
uniform vec4 waterColor;
uniform float waterAlpha;
uniform float time;
uniform float shimmerStrength;
uniform float alphaScale;
uniform vec3 uFogColor;
uniform float uFogStart;
uniform float uFogEnd;
out vec4 FragColor;
void main() {
// Normalize interpolated normal
vec3 norm = normalize(Normal);
// Simple directional light (sun)
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3));
float diff = max(dot(norm, lightDir), 0.0);
// Specular highlights (shininess for water)
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float specBase = pow(max(dot(viewDir, reflectDir), 0.0), mix(64.0, 180.0, shimmerStrength));
float sparkle = 0.65 + 0.35 * sin((TexCoord.x + TexCoord.y + time * 0.4) * 80.0);
float spec = specBase * mix(1.0, sparkle, shimmerStrength);
// Animated texture coordinates for flowing effect
vec2 uv1 = TexCoord + vec2(time * 0.02, time * 0.01);
vec2 uv2 = TexCoord + vec2(-time * 0.01, time * 0.015);
// Combine lighting
vec3 ambient = vec3(0.3) * waterColor.rgb;
vec3 diffuse = vec3(0.6) * diff * waterColor.rgb;
vec3 specular = vec3(1.0) * spec;
// Add wave offset to brightness
float brightness = 1.0 + WaveOffset * 0.1;
vec3 result = (ambient + diffuse + specular) * brightness;
// Add a subtle sky tint and luminance floor so large ocean sheets
// never turn black at grazing angles.
float horizon = pow(1.0 - max(dot(norm, viewDir), 0.0), 1.6);
vec3 skyTint = vec3(0.22, 0.35, 0.48) * (0.25 + 0.55 * shimmerStrength) * horizon;
result += skyTint;
result = max(result, waterColor.rgb * 0.24);
// Subtle foam on wave crests only (no grid artifacts)
float wavePeak = smoothstep(0.35, 0.6, WaveOffset); // Only highest peaks
float foam = wavePeak * 0.25; // Subtle white highlight
result += vec3(foam);
// Slight fresnel: more reflective/opaque at grazing angles.
float fresnel = pow(1.0 - max(dot(norm, viewDir), 0.0), 3.0);
// Distance-based opacity: distant water is more opaque to hide underwater objects
float dist = length(viewPos - FragPos);
float distFade = smoothstep(40.0, 300.0, dist); // Start at 40 units, full opaque at 300
float distAlpha = mix(0.0, 0.75, distFade); // Add up to 75% opacity at distance
float alpha = clamp(waterAlpha * alphaScale * (0.80 + fresnel * 0.45) + distAlpha, 0.20, 0.98);
// Apply distance fog
float fogDist = length(viewPos - FragPos);
float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0);
vec3 finalColor = mix(uFogColor, result, fogFactor);
FragColor = vec4(finalColor, alpha);
}
)";
if (!waterShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create water shader");
return false;
}
LOG_INFO("Water renderer initialized");
return true;
}
void WaterRenderer::shutdown() {
clear();
waterShader.reset();
}
void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append,
int tileX, int tileY) {
constexpr float TILE_SIZE = 33.33333f / 8.0f;
if (!append) {
LOG_INFO("Loading water from terrain (replacing)");
clear();
} else {
LOG_INFO("Loading water from terrain (appending)");
}
// Load water surfaces from MH2O data
int totalLayers = 0;
for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) {
const auto& chunkWater = terrain.waterData[chunkIdx];
if (!chunkWater.hasWater()) {
continue;
}
// Get the terrain chunk for position reference
int chunkX = chunkIdx % 16;
int chunkY = chunkIdx / 16;
const auto& terrainChunk = terrain.getChunk(chunkX, chunkY);
// Process each water layer in this chunk
for (const auto& layer : chunkWater.layers) {
WaterSurface surface;
// Use the chunk base position - layer offsets will be applied in mesh generation
// to match terrain's coordinate transformation
surface.position = glm::vec3(
terrainChunk.position[0],
terrainChunk.position[1],
layer.minHeight
);
surface.origin = glm::vec3(
surface.position.x - (static_cast<float>(layer.y) * TILE_SIZE),
surface.position.y - (static_cast<float>(layer.x) * TILE_SIZE),
layer.minHeight
);
surface.stepX = glm::vec3(0.0f, -TILE_SIZE, 0.0f);
surface.stepY = glm::vec3(-TILE_SIZE, 0.0f, 0.0f);
// Debug log first few water surfaces
if (totalLayers < 5) {
LOG_DEBUG("Water layer ", totalLayers, ": chunk=", chunkIdx,
" liquidType=", layer.liquidType,
" offset=(", (int)layer.x, ",", (int)layer.y, ")",
" size=", (int)layer.width, "x", (int)layer.height,
" height range=[", layer.minHeight, ",", layer.maxHeight, "]");
}
surface.minHeight = layer.minHeight;
surface.maxHeight = layer.maxHeight;
surface.liquidType = layer.liquidType;
// Store dimensions
surface.xOffset = layer.x;
surface.yOffset = layer.y;
surface.width = layer.width;
surface.height = layer.height;
// Prefer per-vertex terrain water heights when sane; fall back to flat
// minHeight if data looks malformed (prevents sky-stretch artifacts).
size_t numVertices = (layer.width + 1) * (layer.height + 1);
bool useFlat = true;
if (layer.heights.size() == numVertices) {
bool sane = true;
for (float h : layer.heights) {
if (!std::isfinite(h) || std::abs(h) > 50000.0f) {
sane = false;
break;
}
// Conservative acceptance window around MH2O min/max metadata.
if (h < layer.minHeight - 8.0f || h > layer.maxHeight + 8.0f) {
sane = false;
break;
}
}
if (sane) {
useFlat = false;
surface.heights = layer.heights;
}
}
if (useFlat) {
surface.heights.resize(numVertices, layer.minHeight);
}
// Lower all terrain water in Stormwind area to prevent it from showing in tunnels/buildings/parks
// Only apply to Stormwind to avoid affecting water elsewhere
// Expanded bounds to cover all of Stormwind including outlying areas and park
bool isStormwindArea = (tileX >= 28 && tileX <= 50 && tileY >= 28 && tileY <= 52);
// Only lower high water (canal level >94) to avoid affecting moonwell and other low features
if (isStormwindArea && layer.minHeight > 94.0f) {
// Calculate approximate world position from tile coordinates
float tileWorldX = (32.0f - tileX) * 533.33333f;
float tileWorldY = (32.0f - tileY) * 533.33333f;
// Exclude moonwell area at (-8755.9, 1108.9) - don't lower water within 50 units
glm::vec3 moonwellPos(-8755.9f, 1108.9f, 96.1f);
float distToMoonwell = glm::distance(glm::vec2(tileWorldX, tileWorldY),
glm::vec2(moonwellPos.x, moonwellPos.y));
if (distToMoonwell > 300.0f) { // Terrain tiles are large, use bigger exclusion radius
LOG_INFO(" -> LOWERING water at tile (", tileX, ",", tileY, ") from height ", layer.minHeight, " by 1 unit");
for (float& h : surface.heights) {
h -= 1.0f;
}
surface.minHeight -= 1.0f;
surface.maxHeight -= 1.0f;
} else {
LOG_INFO(" -> SKIPPING tile (", tileX, ",", tileY, ") - moonwell exclusion (dist: ", distToMoonwell, ")");
}
}
// Copy render mask
surface.mask = layer.mask;
surface.tileX = tileX;
surface.tileY = tileY;
createWaterMesh(surface);
surfaces.push_back(surface);
totalLayers++;
}
}
LOG_INFO("Loaded ", totalLayers, " water layers from MH2O data");
}
void WaterRenderer::removeTile(int tileX, int tileY) {
int removed = 0;
auto it = surfaces.begin();
while (it != surfaces.end()) {
if (it->tileX == tileX && it->tileY == tileY) {
destroyWaterMesh(*it);
it = surfaces.erase(it);
removed++;
} else {
++it;
}
}
if (removed > 0) {
LOG_DEBUG("Removed ", removed, " water surfaces for tile [", tileX, ",", tileY, "]");
}
}
void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liquid,
[[maybe_unused]] const glm::mat4& modelMatrix,
[[maybe_unused]] uint32_t wmoId) {
if (!liquid.hasLiquid() || liquid.xTiles == 0 || liquid.yTiles == 0) {
return;
}
if (liquid.xVerts < 2 || liquid.yVerts < 2) {
return;
}
if (liquid.xTiles != liquid.xVerts - 1 || liquid.yTiles != liquid.yVerts - 1) {
return;
}
if (liquid.xTiles > 64 || liquid.yTiles > 64) {
return;
}
WaterSurface surface;
surface.tileX = -1;
surface.tileY = -1;
surface.wmoId = wmoId;
surface.liquidType = liquid.materialId;
surface.xOffset = 0;
surface.yOffset = 0;
surface.width = static_cast<uint8_t>(std::min<uint32_t>(255, liquid.xTiles));
surface.height = static_cast<uint8_t>(std::min<uint32_t>(255, liquid.yTiles));
constexpr float WMO_LIQUID_TILE_SIZE = 4.1666625f;
const glm::vec3 localBase(liquid.basePosition.x, liquid.basePosition.y, liquid.basePosition.z);
const glm::vec3 localStepX(WMO_LIQUID_TILE_SIZE, 0.0f, 0.0f);
const glm::vec3 localStepY(0.0f, WMO_LIQUID_TILE_SIZE, 0.0f);
surface.origin = glm::vec3(modelMatrix * glm::vec4(localBase, 1.0f));
surface.stepX = glm::vec3(modelMatrix * glm::vec4(localStepX, 0.0f));
surface.stepY = glm::vec3(modelMatrix * glm::vec4(localStepY, 0.0f));
surface.position = surface.origin;
// Guard against malformed transforms that produce giant/vertical sheets.
float stepXLen = glm::length(surface.stepX);
float stepYLen = glm::length(surface.stepY);
glm::vec3 planeN = glm::cross(surface.stepX, surface.stepY);
float nz = (glm::length(planeN) > 1e-4f) ? std::abs(glm::normalize(planeN).z) : 0.0f;
float spanX = stepXLen * static_cast<float>(surface.width);
float spanY = stepYLen * static_cast<float>(surface.height);
if (stepXLen < 0.2f || stepXLen > 12.0f ||
stepYLen < 0.2f || stepYLen > 12.0f ||
nz < 0.60f ||
spanX > 450.0f || spanY > 450.0f) {
return;
}
const int gridWidth = static_cast<int>(surface.width) + 1;
const int gridHeight = static_cast<int>(surface.height) + 1;
const int vertexCount = gridWidth * gridHeight;
// Keep WMO liquid flat for stability; some files use variant payload layouts
// that can produce invalid per-vertex heights if interpreted generically.
surface.heights.assign(vertexCount, surface.origin.z);
surface.minHeight = surface.origin.z;
surface.maxHeight = surface.origin.z;
// Lower WMO water in Stormwind area to prevent it from showing in tunnels/buildings/parks
// Calculate tile coordinates from world position
int tileX = static_cast<int>(std::floor((32.0f - surface.origin.x / 533.33333f)));
int tileY = static_cast<int>(std::floor((32.0f - surface.origin.y / 533.33333f)));
// Log all WMO water to debug park issue
LOG_INFO("WMO water at pos=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z,
") tile=(", tileX, ",", tileY, ") wmoId=", wmoId);
// Expanded bounds to cover all of Stormwind including outlying areas and park
bool isStormwindArea = (tileX >= 28 && tileX <= 50 && tileY >= 28 && tileY <= 52);
// Only lower high WMO water (canal level >94) to avoid affecting moonwell and other low features
if (isStormwindArea && surface.origin.z > 94.0f) {
// Exclude moonwell area at (-8755.9, 1108.9) - don't lower water within 20 units
glm::vec3 moonwellPos(-8755.9f, 1108.9f, 96.1f);
float distToMoonwell = glm::distance(glm::vec2(surface.origin.x, surface.origin.y),
glm::vec2(moonwellPos.x, moonwellPos.y));
if (distToMoonwell > 20.0f) {
LOG_INFO(" -> LOWERING by 1 unit (dist to moonwell: ", distToMoonwell, ")");
for (float& h : surface.heights) {
h -= 1.0f;
}
surface.minHeight -= 1.0f;
surface.maxHeight -= 1.0f;
} else {
LOG_INFO(" -> SKIPPING (moonwell exclusion zone, dist: ", distToMoonwell, ")");
}
}
// Skip WMO water that's clearly invalid (extremely high - above 300 units)
// This is a conservative global filter that won't affect normal gameplay
if (surface.origin.z > 300.0f) {
LOG_INFO("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too high)");
return;
}
// Skip WMO water that's extremely low (deep underground where it shouldn't be)
if (surface.origin.z < -100.0f) {
LOG_INFO("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too low)");
return;
}
size_t tileCount = static_cast<size_t>(surface.width) * static_cast<size_t>(surface.height);
size_t maskBytes = (tileCount + 7) / 8;
// WMO liquid flags vary across files; for now treat all WMO liquid tiles as
// visible for rendering. Swim/gameplay queries already ignore WMO surfaces.
surface.mask.assign(maskBytes, 0xFF);
createWaterMesh(surface);
if (surface.indexCount > 0) {
surfaces.push_back(surface);
}
}
void WaterRenderer::removeWMO(uint32_t wmoId) {
if (wmoId == 0) {
return;
}
auto it = surfaces.begin();
while (it != surfaces.end()) {
if (it->wmoId == wmoId) {
destroyWaterMesh(*it);
it = surfaces.erase(it);
} else {
++it;
}
}
}
void WaterRenderer::clear() {
for (auto& surface : surfaces) {
destroyWaterMesh(surface);
}
surfaces.clear();
}
void WaterRenderer::render(const Camera& camera, float time) {
if (!renderingEnabled || surfaces.empty() || !waterShader) {
return;
}
glDisable(GL_CULL_FACE);
// Enable alpha blending for transparent water
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Disable depth writing so terrain shows through water
glDepthMask(GL_FALSE);
waterShader->use();
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
waterShader->setUniform("view", view);
waterShader->setUniform("projection", projection);
waterShader->setUniform("viewPos", camera.getPosition());
waterShader->setUniform("time", time);
waterShader->setUniform("uFogColor", fogColor);
waterShader->setUniform("uFogStart", fogStart);
waterShader->setUniform("uFogEnd", fogEnd);
// Render each water surface
for (const auto& surface : surfaces) {
if (surface.vao == 0) {
continue;
}
// Model matrix (identity, position already in vertices)
glm::mat4 model = glm::mat4(1.0f);
waterShader->setUniform("model", model);
// Set liquid-specific color and alpha
glm::vec4 color = getLiquidColor(surface.liquidType);
float alpha = getLiquidAlpha(surface.liquidType);
// City/canal liquid profile: clearer water + stronger ripples/sun shimmer.
// Stormwind canals typically use LiquidType 5 in this data set.
bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5);
// Reduced wave amplitude to prevent tile seam gaps (tiles don't share wave state)
float waveAmp = canalProfile ? 0.04f : 0.06f; // Subtle waves to avoid boundary gaps
float waveFreq = canalProfile ? 0.30f : 0.22f; // Frequency maintained for visual
float waveSpeed = canalProfile ? 1.20f : 2.00f; // Speed maintained for animation
float shimmerStrength = canalProfile ? 0.95f : 0.50f;
float alphaScale = canalProfile ? 0.90f : 1.00f; // Increased from 0.72 to make canal water less transparent
waterShader->setUniform("waterColor", color);
waterShader->setUniform("waterAlpha", alpha);
waterShader->setUniform("waveAmp", waveAmp);
waterShader->setUniform("waveFreq", waveFreq);
waterShader->setUniform("waveSpeed", waveSpeed);
waterShader->setUniform("shimmerStrength", shimmerStrength);
waterShader->setUniform("alphaScale", alphaScale);
// Render
glBindVertexArray(surface.vao);
glDrawElements(GL_TRIANGLES, surface.indexCount, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
// Restore state
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
glEnable(GL_CULL_FACE);
}
void WaterRenderer::createWaterMesh(WaterSurface& surface) {
// Variable-size grid based on water layer dimensions
const int gridWidth = surface.width + 1; // Vertices = tiles + 1
const int gridHeight = surface.height + 1;
constexpr float VISUAL_WATER_Z_BIAS = 0.02f; // Small bias to avoid obvious overdraw on city meshes
std::vector<float> vertices;
std::vector<uint32_t> indices;
// Generate vertices
for (int y = 0; y < gridHeight; y++) {
for (int x = 0; x < gridWidth; x++) {
int index = y * gridWidth + x;
// Use per-vertex height data if available, otherwise flat at minHeight
float height;
if (index < static_cast<int>(surface.heights.size())) {
height = surface.heights[index];
} else {
height = surface.minHeight;
}
glm::vec3 pos = surface.origin +
surface.stepX * static_cast<float>(x) +
surface.stepY * static_cast<float>(y);
pos.z = height + VISUAL_WATER_Z_BIAS;
// Debug first surface's corner vertices
static int debugCount = 0;
if (debugCount < 4 && (x == 0 || x == gridWidth-1) && (y == 0 || y == gridHeight-1)) {
LOG_DEBUG("Water vertex: (", pos.x, ", ", pos.y, ", ", pos.z, ")");
debugCount++;
}
vertices.push_back(pos.x);
vertices.push_back(pos.y);
vertices.push_back(pos.z);
// Normal (pointing up for water surface)
vertices.push_back(0.0f);
vertices.push_back(0.0f);
vertices.push_back(1.0f);
// Texture coordinates
vertices.push_back(static_cast<float>(x) / std::max(1, gridWidth - 1));
vertices.push_back(static_cast<float>(y) / std::max(1, gridHeight - 1));
}
}
// Generate indices (triangles), respecting the render mask
for (int y = 0; y < gridHeight - 1; y++) {
for (int x = 0; x < gridWidth - 1; x++) {
// Check render mask - each bit represents a tile
// Also render edge tiles to blend coastlines (avoid square gaps)
bool renderTile = true;
if (!surface.mask.empty()) {
int tileIndex;
if (surface.wmoId == 0 && surface.mask.size() >= 8) {
// Terrain MH2O mask is chunk-wide 8x8.
int cx = static_cast<int>(surface.xOffset) + x;
int cy = static_cast<int>(surface.yOffset) + y;
tileIndex = cy * 8 + cx;
} else {
// Local mask indexing (WMO/custom).
tileIndex = y * surface.width + x;
}
int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) {
uint8_t maskByte = surface.mask[byteIndex];
bool lsbOrder = (maskByte & (1 << bitIndex)) != 0;
bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0;
renderTile = lsbOrder || msbOrder;
// If this tile is masked out, check neighbors to fill gaps
if (!renderTile && x > 0 && y > 0 && x < gridWidth-2 && y < gridHeight-2) {
// Check adjacent tiles - render if any neighbor is water (blend coastline)
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
if (dx == 0 && dy == 0) continue;
int neighborIdx = (y + dy) * surface.width + (x + dx);
int nByteIdx = neighborIdx / 8;
int nBitIdx = neighborIdx % 8;
if (nByteIdx < static_cast<int>(surface.mask.size())) {
uint8_t nMask = surface.mask[nByteIdx];
if ((nMask & (1 << nBitIdx)) || (nMask & (1 << (7 - nBitIdx)))) {
renderTile = true;
goto found_neighbor;
}
}
}
}
found_neighbor:;
}
}
}
if (!renderTile) {
continue; // Skip this tile
}
int topLeft = y * gridWidth + x;
int topRight = topLeft + 1;
int bottomLeft = (y + 1) * gridWidth + x;
int bottomRight = bottomLeft + 1;
// First triangle
indices.push_back(topLeft);
indices.push_back(bottomLeft);
indices.push_back(topRight);
// Second triangle
indices.push_back(topRight);
indices.push_back(bottomLeft);
indices.push_back(bottomRight);
}
}
if (indices.empty() && surface.wmoId == 0) {
// Terrain MH2O masks can be inconsistent in some tiles. If a terrain layer
// produces no visible tiles, fall back to its full local rect for rendering.
for (int y = 0; y < gridHeight - 1; y++) {
for (int x = 0; x < gridWidth - 1; x++) {
int topLeft = y * gridWidth + x;
int topRight = topLeft + 1;
int bottomLeft = (y + 1) * gridWidth + x;
int bottomRight = bottomLeft + 1;
indices.push_back(topLeft);
indices.push_back(bottomLeft);
indices.push_back(topRight);
indices.push_back(topRight);
indices.push_back(bottomLeft);
indices.push_back(bottomRight);
}
}
}
if (indices.empty()) return;
surface.indexCount = static_cast<int>(indices.size());
// Create OpenGL buffers
glGenVertexArrays(1, &surface.vao);
glGenBuffers(1, &surface.vbo);
glGenBuffers(1, &surface.ebo);
glBindVertexArray(surface.vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, surface.vbo);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, surface.ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW);
// Set vertex attributes
// Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// Normal
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// Texture coordinates
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);
}
void WaterRenderer::destroyWaterMesh(WaterSurface& surface) {
if (surface.vao != 0) {
glDeleteVertexArrays(1, &surface.vao);
surface.vao = 0;
}
if (surface.vbo != 0) {
glDeleteBuffers(1, &surface.vbo);
surface.vbo = 0;
}
if (surface.ebo != 0) {
glDeleteBuffers(1, &surface.ebo);
surface.ebo = 0;
}
}
std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const {
std::optional<float> best;
for (size_t si = 0; si < surfaces.size(); si++) {
const auto& surface = surfaces[si];
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
glm::vec2 stepX(surface.stepX.x, surface.stepX.y);
glm::vec2 stepY(surface.stepY.x, surface.stepY.y);
float lenSqX = glm::dot(stepX, stepX);
float lenSqY = glm::dot(stepY, stepY);
if (lenSqX < 1e-6f || lenSqY < 1e-6f) {
continue;
}
float gx = glm::dot(rel, stepX) / lenSqX;
float gy = glm::dot(rel, stepY) / lenSqY;
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
gy < 0.0f || gy > static_cast<float>(surface.height)) {
continue;
}
int gridWidth = surface.width + 1;
// Bilinear interpolation
int ix = static_cast<int>(gx);
int iy = static_cast<int>(gy);
float fx = gx - ix;
float fy = gy - iy;
// Clamp to valid vertex range
if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; }
if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; }
if (ix < 0 || iy < 0) {
continue;
}
// Respect per-tile mask so holes/non-liquid tiles do not count as swimmable.
if (!surface.mask.empty()) {
int tileIndex;
if (surface.wmoId == 0 && surface.mask.size() >= 8) {
int cx = static_cast<int>(surface.xOffset) + ix;
int cy = static_cast<int>(surface.yOffset) + iy;
tileIndex = cy * 8 + cx;
} else {
tileIndex = iy * surface.width + ix;
}
int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) {
uint8_t maskByte = surface.mask[byteIndex];
bool lsbOrder = (maskByte & (1 << bitIndex)) != 0;
bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0;
bool renderTile = lsbOrder || msbOrder;
if (!renderTile) {
continue;
}
}
}
int idx00 = iy * gridWidth + ix;
int idx10 = idx00 + 1;
int idx01 = idx00 + gridWidth;
int idx11 = idx01 + 1;
int total = static_cast<int>(surface.heights.size());
if (idx11 >= total) continue;
float h00 = surface.heights[idx00];
float h10 = surface.heights[idx10];
float h01 = surface.heights[idx01];
float h11 = surface.heights[idx11];
float h = h00 * (1-fx) * (1-fy) + h10 * fx * (1-fy) +
h01 * (1-fx) * fy + h11 * fx * fy;
if (!best || h > *best) {
best = h;
}
}
return best;
}
std::optional<uint16_t> WaterRenderer::getWaterTypeAt(float glX, float glY) const {
std::optional<float> bestHeight;
std::optional<uint16_t> bestType;
for (const auto& surface : surfaces) {
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
glm::vec2 stepX(surface.stepX.x, surface.stepX.y);
glm::vec2 stepY(surface.stepY.x, surface.stepY.y);
float lenSqX = glm::dot(stepX, stepX);
float lenSqY = glm::dot(stepY, stepY);
if (lenSqX < 1e-6f || lenSqY < 1e-6f) {
continue;
}
float gx = glm::dot(rel, stepX) / lenSqX;
float gy = glm::dot(rel, stepY) / lenSqY;
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
gy < 0.0f || gy > static_cast<float>(surface.height)) {
continue;
}
int ix = static_cast<int>(gx);
int iy = static_cast<int>(gy);
if (ix >= surface.width) ix = surface.width - 1;
if (iy >= surface.height) iy = surface.height - 1;
if (ix < 0 || iy < 0) continue;
if (!surface.mask.empty()) {
int tileIndex;
if (surface.wmoId == 0 && surface.mask.size() >= 8) {
int cx = static_cast<int>(surface.xOffset) + ix;
int cy = static_cast<int>(surface.yOffset) + iy;
tileIndex = cy * 8 + cx;
} else {
tileIndex = iy * surface.width + ix;
}
int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) {
uint8_t maskByte = surface.mask[byteIndex];
bool lsbOrder = (maskByte & (1 << bitIndex)) != 0;
bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0;
bool renderTile = lsbOrder || msbOrder;
if (!renderTile) continue;
}
}
// Use minHeight as stable selector for "topmost surface at XY".
float h = surface.minHeight;
if (!bestHeight || h > *bestHeight) {
bestHeight = h;
bestType = surface.liquidType;
}
}
return bestType;
}
glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const {
// WoW 3.3.5a LiquidType.dbc IDs:
// 1,5,9,13,17 = Water variants (still, slow, fast)
// 2,6,10,14 = Ocean
// 3,7,11,15 = Magma
// 4,8,12 = Slime
// Map to basic type using (id - 1) % 4 for standard IDs, or handle ranges
uint8_t basicType;
if (liquidType == 0) {
basicType = 0; // Water (fallback)
} else {
basicType = ((liquidType - 1) % 4);
}
switch (basicType) {
case 0: // Water
return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f);
case 1: // Ocean
return glm::vec4(0.06f, 0.18f, 0.34f, 1.0f);
case 2: // Magma
return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f);
case 3: // Slime
return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f);
default:
return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); // Water fallback
}
}
float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const {
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
switch (basicType) {
case 1: return 0.68f; // Ocean
case 2: return 0.72f; // Magma
case 3: return 0.62f; // Slime
default: return 0.38f; // Water
}
}
} // namespace rendering
} // namespace wowee