2026-02-02 12:24:50 -08:00
|
|
|
#include "rendering/water_renderer.hpp"
|
|
|
|
|
#include "rendering/shader.hpp"
|
|
|
|
|
#include "rendering/camera.hpp"
|
|
|
|
|
#include "pipeline/adt_loader.hpp"
|
2026-02-03 13:33:31 -08:00
|
|
|
#include "pipeline/wmo_loader.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <GL/glew.h>
|
|
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
2026-02-03 20:40:59 -08:00
|
|
|
#include <algorithm>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <cmath>
|
2026-02-03 20:40:59 -08:00
|
|
|
#include <limits>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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;
|
2026-02-03 20:40:59 -08:00
|
|
|
uniform float waveAmp;
|
|
|
|
|
uniform float waveFreq;
|
|
|
|
|
uniform float waveSpeed;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
out vec3 FragPos;
|
|
|
|
|
out vec3 Normal;
|
|
|
|
|
out vec2 TexCoord;
|
|
|
|
|
out float WaveOffset;
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
vec3 pos = aPos;
|
2026-02-03 20:40:59 -08:00
|
|
|
// Procedural ripple motion (tunable per water profile).
|
|
|
|
|
float w1 = sin((aPos.x + time * waveSpeed) * waveFreq) * waveAmp;
|
|
|
|
|
float w2 = cos((aPos.y - time * (waveSpeed * 0.78)) * (waveFreq * 0.82)) * (waveAmp * 0.72);
|
|
|
|
|
float wave = w1 + w2;
|
|
|
|
|
pos.z += wave;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
FragPos = vec3(model * vec4(pos, 1.0));
|
2026-02-02 23:03:45 -08:00
|
|
|
// Use mat3(model) directly - avoids expensive inverse() per vertex
|
|
|
|
|
Normal = mat3(model) * aNormal;
|
2026-02-02 12:24:50 -08:00
|
|
|
TexCoord = aTexCoord;
|
2026-02-03 20:40:59 -08:00
|
|
|
WaveOffset = wave;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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;
|
2026-02-03 20:40:59 -08:00
|
|
|
uniform float shimmerStrength;
|
|
|
|
|
uniform float alphaScale;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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);
|
2026-02-03 20:40:59 -08:00
|
|
|
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);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
// Slight fresnel: more reflective/opaque at grazing angles.
|
|
|
|
|
float fresnel = pow(1.0 - max(dot(norm, viewDir), 0.0), 3.0);
|
|
|
|
|
float alpha = clamp(waterAlpha * alphaScale * (0.68 + fresnel * 0.45), 0.12, 0.82);
|
|
|
|
|
FragColor = vec4(result, alpha);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
)";
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-02-03 20:40:59 -08:00
|
|
|
constexpr float TILE_SIZE = 33.33333f / 8.0f;
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
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
|
|
|
|
|
);
|
2026-02-03 20:40:59 -08:00
|
|
|
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);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
// 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) {
|
2026-02-02 12:24:50 -08:00
|
|
|
surface.heights.resize(numVertices, layer.minHeight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy render mask
|
|
|
|
|
surface.mask = layer.mask;
|
2026-02-03 20:40:59 -08:00
|
|
|
if (!surface.mask.empty()) {
|
|
|
|
|
bool anyVisible = false;
|
|
|
|
|
for (uint8_t b : surface.mask) {
|
|
|
|
|
if (b != 0) {
|
|
|
|
|
anyVisible = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Some tiles appear to have malformed/unsupported MH2O masks.
|
|
|
|
|
// Fall back to full coverage so canal water is still visible.
|
|
|
|
|
if (!anyVisible) {
|
|
|
|
|
std::fill(surface.mask.begin(), surface.mask.end(), 0xFF);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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, "]");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 13:33:31 -08:00
|
|
|
void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liquid,
|
|
|
|
|
[[maybe_unused]] const glm::mat4& modelMatrix,
|
|
|
|
|
[[maybe_unused]] uint32_t wmoId) {
|
2026-02-03 20:40:59 -08:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-02-03 13:33:31 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-03 13:33:31 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
GLboolean cullEnabled = glIsEnabled(GL_CULL_FACE);
|
|
|
|
|
if (cullEnabled) {
|
|
|
|
|
glDisable(GL_CULL_FACE);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
// 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);
|
|
|
|
|
float waveAmp = canalProfile ? 0.07f : 0.038f;
|
|
|
|
|
float waveFreq = canalProfile ? 0.30f : 0.22f;
|
|
|
|
|
float waveSpeed = canalProfile ? 1.20f : 0.90f;
|
|
|
|
|
float shimmerStrength = canalProfile ? 0.95f : 0.35f;
|
|
|
|
|
float alphaScale = canalProfile ? 0.72f : 1.00f;
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
waterShader->setUniform("waterColor", color);
|
|
|
|
|
waterShader->setUniform("waterAlpha", alpha);
|
2026-02-03 20:40:59 -08:00
|
|
|
waterShader->setUniform("waveAmp", waveAmp);
|
|
|
|
|
waterShader->setUniform("waveFreq", waveFreq);
|
|
|
|
|
waterShader->setUniform("waveSpeed", waveSpeed);
|
|
|
|
|
waterShader->setUniform("shimmerStrength", shimmerStrength);
|
|
|
|
|
waterShader->setUniform("alphaScale", alphaScale);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Render
|
|
|
|
|
glBindVertexArray(surface.vao);
|
|
|
|
|
glDrawElements(GL_TRIANGLES, surface.indexCount, GL_UNSIGNED_INT, nullptr);
|
|
|
|
|
glBindVertexArray(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Restore state
|
|
|
|
|
glDepthMask(GL_TRUE);
|
|
|
|
|
glDisable(GL_BLEND);
|
2026-02-03 20:40:59 -08:00
|
|
|
if (cullEnabled) {
|
|
|
|
|
glEnable(GL_CULL_FACE);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-02-03 20:40:59 -08:00
|
|
|
constexpr float VISUAL_WATER_Z_BIAS = 0.06f; // Prevent z-fighting against city/WMO geometry
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
glm::vec3 pos = surface.origin +
|
|
|
|
|
surface.stepX * static_cast<float>(x) +
|
|
|
|
|
surface.stepY * static_cast<float>(y);
|
|
|
|
|
pos.z = height + VISUAL_WATER_Z_BIAS;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Debug first surface's corner vertices
|
|
|
|
|
static int debugCount = 0;
|
|
|
|
|
if (debugCount < 4 && (x == 0 || x == gridWidth-1) && (y == 0 || y == gridHeight-1)) {
|
2026-02-03 20:40:59 -08:00
|
|
|
LOG_DEBUG("Water vertex: (", pos.x, ", ", pos.y, ", ", pos.z, ")");
|
2026-02-02 12:24:50 -08:00
|
|
|
debugCount++;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
vertices.push_back(pos.x);
|
|
|
|
|
vertices.push_back(pos.y);
|
|
|
|
|
vertices.push_back(pos.z);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
bool renderTile = true;
|
|
|
|
|
if (!surface.mask.empty()) {
|
|
|
|
|
int tileIndex = y * surface.width + x;
|
|
|
|
|
int byteIndex = tileIndex / 8;
|
|
|
|
|
int bitIndex = tileIndex % 8;
|
|
|
|
|
if (byteIndex < static_cast<int>(surface.mask.size())) {
|
|
|
|
|
renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()) {
|
|
|
|
|
// No visible tiles
|
|
|
|
|
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];
|
2026-02-03 20:40:59 -08:00
|
|
|
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;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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; }
|
2026-02-03 20:40:59 -08:00
|
|
|
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 = iy * surface.width + ix;
|
|
|
|
|
int byteIndex = tileIndex / 8;
|
|
|
|
|
int bitIndex = tileIndex % 8;
|
|
|
|
|
if (byteIndex < static_cast<int>(surface.mask.size())) {
|
|
|
|
|
bool renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0;
|
|
|
|
|
if (!renderTile) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
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 = iy * surface.width + ix;
|
|
|
|
|
int byteIndex = tileIndex / 8;
|
|
|
|
|
int bitIndex = tileIndex % 8;
|
|
|
|
|
if (byteIndex < static_cast<int>(surface.mask.size())) {
|
|
|
|
|
bool renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0;
|
|
|
|
|
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 {
|
2026-02-02 12:24:50 -08:00
|
|
|
// 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.1f, 0.3f, 0.5f, 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 20:40:59 -08:00
|
|
|
float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const {
|
2026-02-02 12:24:50 -08:00
|
|
|
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
|
|
|
|
|
switch (basicType) {
|
2026-02-03 20:40:59 -08:00
|
|
|
case 2: return 0.72f; // Magma
|
|
|
|
|
case 3: return 0.62f; // Slime
|
|
|
|
|
default: return 0.38f; // Water/Ocean
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace rendering
|
|
|
|
|
} // namespace wowee
|