diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 5ed0b325..e9ec0f69 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -124,7 +124,7 @@ private: static constexpr float SWIM_GRAVITY = -5.0f; static constexpr float SWIM_BUOYANCY = 8.0f; static constexpr float SWIM_SINK_SPEED = -3.0f; - static constexpr float WATER_SURFACE_OFFSET = 1.5f; + static constexpr float WATER_SURFACE_OFFSET = 0.9f; // State bool enabled = true; diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 3963c31c..c3d27582 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -31,6 +31,7 @@ class CharacterRenderer; class WMORenderer; class M2Renderer; class Minimap; +class Shader; class Renderer { public: @@ -153,6 +154,9 @@ private: std::unique_ptr footstepManager; std::unique_ptr activitySoundManager; std::unique_ptr zoneManager; + std::unique_ptr underwaterOverlayShader; + uint32_t underwaterOverlayVAO = 0; + uint32_t underwaterOverlayVBO = 0; pipeline::AssetManager* cachedAssetManager = nullptr; uint32_t currentZoneId = 0; diff --git a/include/rendering/water_renderer.hpp b/include/rendering/water_renderer.hpp index f9ec86cf..42b0a8c2 100644 --- a/include/rendering/water_renderer.hpp +++ b/include/rendering/water_renderer.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace wowee { @@ -22,9 +23,12 @@ class Shader; */ struct WaterSurface { glm::vec3 position; // World position + glm::vec3 origin; // Mesh origin (world) + glm::vec3 stepX; // Mesh X step vector in world space + glm::vec3 stepY; // Mesh Y step vector in world space float minHeight; // Minimum water height float maxHeight; // Maximum water height - uint8_t liquidType; // 0=water, 1=ocean, 2=magma, 3=slime + uint16_t liquidType; // LiquidType.dbc ID (WotLK) // Owning tile coordinates (for per-tile removal) int tileX = -1, tileY = -1; @@ -119,6 +123,7 @@ public: * Returns the highest water surface height at that XY, or nullopt if no water. */ std::optional getWaterHeightAt(float glX, float glY) const; + std::optional getWaterTypeAt(float glX, float glY) const; /** * Get water surface count @@ -129,8 +134,8 @@ private: void createWaterMesh(WaterSurface& surface); void destroyWaterMesh(WaterSurface& surface); - glm::vec4 getLiquidColor(uint8_t liquidType) const; - float getLiquidAlpha(uint8_t liquidType) const; + glm::vec4 getLiquidColor(uint16_t liquidType) const; + float getLiquidAlpha(uint16_t liquidType) const; std::unique_ptr waterShader; std::vector surfaces; diff --git a/src/core/application.cpp b/src/core/application.cpp index b9778537..3c0f41f3 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -553,8 +553,8 @@ void Application::setState(AppState newState) { gameHandler->sendMovement(static_cast(opcode)); } }); - // Use WoW-correct speeds when connected to a server - cc->setUseWoWSpeed(!singlePlayerMode); + // Keep player locomotion WoW-like in both single-player and online modes. + cc->setUseWoWSpeed(true); } break; case AppState::DISCONNECTED: diff --git a/src/pipeline/wmo_loader.cpp b/src/pipeline/wmo_loader.cpp index 19186848..8d330d86 100644 --- a/src/pipeline/wmo_loader.cpp +++ b/src/pipeline/wmo_loader.cpp @@ -32,6 +32,7 @@ constexpr uint32_t MOBA = 0x4D4F4241; // Batches constexpr uint32_t MOCV = 0x4D4F4356; // Vertex colors constexpr uint32_t MONR = 0x4D4F4E52; // Normals constexpr uint32_t MOTV = 0x4D4F5456; // Texture coords +constexpr uint32_t MLIQ = 0x4D4C4951; // Liquid // Read utilities template @@ -533,6 +534,60 @@ bool WMOLoader::loadGroup(const std::vector& groupData, } } } + else if (subChunkId == MLIQ) { // MLIQ - WMO liquid data + // Basic WotLK layout: + // uint32 xVerts, yVerts, xTiles, yTiles + // float baseX, baseY, baseZ + // uint16 materialId + // (optional pad/unknown bytes) + // followed by vertex/tile payload + uint32_t parseOffset = mogpOffset; + if (parseOffset + 30 <= subChunkEnd) { + group.liquid.xVerts = read(groupData, parseOffset); + group.liquid.yVerts = read(groupData, parseOffset); + group.liquid.xTiles = read(groupData, parseOffset); + group.liquid.yTiles = read(groupData, parseOffset); + group.liquid.basePosition.x = read(groupData, parseOffset); + group.liquid.basePosition.y = read(groupData, parseOffset); + group.liquid.basePosition.z = read(groupData, parseOffset); + group.liquid.materialId = read(groupData, parseOffset); + if (parseOffset + sizeof(uint16_t) <= subChunkEnd) { + // Reserved/flags in some WMO liquid variants. + parseOffset += sizeof(uint16_t); + } + + // Keep parser resilient across minor format variants: + // prefer explicit per-vertex floats, otherwise fall back to flat. + const size_t vertexCount = + static_cast(group.liquid.xVerts) * static_cast(group.liquid.yVerts); + const size_t tileCount = + static_cast(group.liquid.xTiles) * static_cast(group.liquid.yTiles); + const size_t bytesRemaining = (subChunkEnd > parseOffset) ? (subChunkEnd - parseOffset) : 0; + + group.liquid.heights.clear(); + group.liquid.flags.clear(); + + if (vertexCount > 0 && bytesRemaining >= vertexCount * sizeof(float)) { + group.liquid.heights.resize(vertexCount); + for (size_t i = 0; i < vertexCount; i++) { + group.liquid.heights[i] = read(groupData, parseOffset); + } + } else if (vertexCount > 0) { + group.liquid.heights.resize(vertexCount, group.liquid.basePosition.z); + } + + if (tileCount > 0 && parseOffset + tileCount <= subChunkEnd) { + group.liquid.flags.resize(tileCount); + std::memcpy(group.liquid.flags.data(), &groupData[parseOffset], tileCount); + } else if (tileCount > 0) { + group.liquid.flags.resize(tileCount, 0); + } + + if (group.liquid.materialId == 0) { + group.liquid.materialId = static_cast(group.liquidType); + } + } + } mogpOffset = subChunkEnd; } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index f3592fe3..8b9fbae6 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -181,7 +181,9 @@ void CameraController::update(float deltaTime) { if (waterRenderer) { waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y); } - bool inWater = waterH && targetPos.z < *waterH; + constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f; + bool inWater = waterH && targetPos.z < *waterH && + ((*waterH - targetPos.z) <= MAX_SWIM_DEPTH_FROM_SURFACE); if (inWater) { @@ -189,6 +191,7 @@ void CameraController::update(float deltaTime) { // Swim movement follows look pitch (forward/back), while strafe stays // lateral for stable control. float swimSpeed = speed * SWIM_SPEED_FACTOR; + float waterSurfaceZ = waterH ? (*waterH - WATER_SURFACE_OFFSET) : targetPos.z; glm::vec3 swimForward = glm::normalize(forward3D); if (glm::length(swimForward) < 1e-4f) { @@ -214,6 +217,7 @@ void CameraController::update(float deltaTime) { } // Spacebar = swim up (continuous, not a jump) + bool diveIntent = nowForward && (forward3D.z < -0.28f); if (nowJump) { verticalVelocity = SWIM_BUOYANCY; } else { @@ -222,6 +226,16 @@ void CameraController::update(float deltaTime) { if (verticalVelocity < SWIM_SINK_SPEED) { verticalVelocity = SWIM_SINK_SPEED; } + // Strong surface lock while idle/normal swim so buoyancy keeps + // you afloat unless you're intentionally diving. + if (!diveIntent) { + float surfaceErr = (waterSurfaceZ - targetPos.z); + verticalVelocity += surfaceErr * 7.0f * deltaTime; + verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * deltaTime); + if (std::abs(surfaceErr) < 0.06f && std::abs(verticalVelocity) < 0.35f) { + verticalVelocity = 0.0f; + } + } } targetPos.z += verticalVelocity * deltaTime; @@ -636,12 +650,16 @@ void CameraController::update(float deltaTime) { if (waterRenderer) { waterH = waterRenderer->getWaterHeightAt(newPos.x, newPos.y); } - bool inWater = waterH && feetZ < *waterH; + constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f; + bool inWater = waterH && feetZ < *waterH && + ((*waterH - feetZ) <= MAX_SWIM_DEPTH_FROM_SURFACE); if (inWater) { swimming = true; float swimSpeed = speed * SWIM_SPEED_FACTOR; + float waterSurfaceCamZ = waterH ? (*waterH - WATER_SURFACE_OFFSET + eyeHeight) : newPos.z; + bool diveIntent = nowForward && (forward3D.z < -0.28f); if (glm::length(movement) > 0.001f) { movement = glm::normalize(movement); @@ -655,6 +673,14 @@ void CameraController::update(float deltaTime) { if (verticalVelocity < SWIM_SINK_SPEED) { verticalVelocity = SWIM_SINK_SPEED; } + if (!diveIntent) { + float surfaceErr = (waterSurfaceCamZ - newPos.z); + verticalVelocity += surfaceErr * 7.0f * deltaTime; + verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * deltaTime); + if (std::abs(surfaceErr) < 0.06f && std::abs(verticalVelocity) < 0.35f) { + verticalVelocity = 0.0f; + } + } } newPos.z += verticalVelocity * deltaTime; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index c1beb872..9f2d7ce1 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -17,6 +17,7 @@ #include "rendering/wmo_renderer.hpp" #include "rendering/m2_renderer.hpp" #include "rendering/minimap.hpp" +#include "rendering/shader.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/m2_loader.hpp" #include "pipeline/wmo_loader.hpp" @@ -193,6 +194,37 @@ bool Renderer::initialize(core::Window* win) { footstepManager = std::make_unique(); activitySoundManager = std::make_unique(); + // Underwater full-screen tint overlay (applies to all world geometry). + underwaterOverlayShader = std::make_unique(); + const char* overlayVS = R"( + #version 330 core + layout (location = 0) in vec2 aPos; + void main() { gl_Position = vec4(aPos, 0.0, 1.0); } + )"; + const char* overlayFS = R"( + #version 330 core + uniform vec4 uTint; + out vec4 FragColor; + void main() { FragColor = uTint; } + )"; + if (!underwaterOverlayShader->loadFromSource(overlayVS, overlayFS)) { + LOG_WARNING("Failed to initialize underwater overlay shader"); + underwaterOverlayShader.reset(); + } else { + const float quadVerts[] = { + -1.0f, -1.0f, 1.0f, -1.0f, + -1.0f, 1.0f, 1.0f, 1.0f + }; + glGenVertexArrays(1, &underwaterOverlayVAO); + glGenBuffers(1, &underwaterOverlayVBO); + glBindVertexArray(underwaterOverlayVAO); + glBindBuffer(GL_ARRAY_BUFFER, underwaterOverlayVBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + glBindVertexArray(0); + } + LOG_INFO("Renderer initialized"); return true; } @@ -272,6 +304,15 @@ void Renderer::shutdown() { activitySoundManager->shutdown(); activitySoundManager.reset(); } + if (underwaterOverlayVAO) { + glDeleteVertexArrays(1, &underwaterOverlayVAO); + underwaterOverlayVAO = 0; + } + if (underwaterOverlayVBO) { + glDeleteBuffers(1, &underwaterOverlayVBO); + underwaterOverlayVBO = 0; + } + underwaterOverlayShader.reset(); zoneManager.reset(); @@ -851,6 +892,8 @@ void Renderer::renderWorld(game::World* world) { // Get time of day for sky-related rendering float timeOfDay = skybox ? skybox->getTimeOfDay() : 12.0f; + bool underwater = false; + bool canalUnderwater = false; // Render skybox first (furthest back) if (skybox && camera) { @@ -880,20 +923,45 @@ void Renderer::renderWorld(game::World* world) { // Render terrain if loaded and enabled if (terrainEnabled && terrainLoaded && terrainRenderer && camera) { - // Check if camera is underwater for fog override - bool underwater = false; - if (waterRenderer && camera) { + // Check if camera/character is underwater for fog override + if (cameraController && cameraController->isSwimming() && waterRenderer && camera) { glm::vec3 camPos = camera->getPosition(); auto waterH = waterRenderer->getWaterHeightAt(camPos.x, camPos.y); - if (waterH && camPos.z < *waterH) { + constexpr float MAX_UNDERWATER_DEPTH = 12.0f; + // Require camera to be meaningfully below the surface before + // underwater fog/tint kicks in (avoids "wrong plane" near surface). + constexpr float UNDERWATER_ENTER_EPS = 0.45f; + if (waterH && + camPos.z < (*waterH - UNDERWATER_ENTER_EPS) && + (*waterH - camPos.z) <= MAX_UNDERWATER_DEPTH) { underwater = true; } } if (underwater) { - float fogColor[3] = {0.05f, 0.15f, 0.25f}; - terrainRenderer->setFog(fogColor, 10.0f, 200.0f); - glClearColor(0.05f, 0.15f, 0.25f, 1.0f); + glm::vec3 camPos = camera->getPosition(); + std::optional liquidType = waterRenderer ? waterRenderer->getWaterTypeAt(camPos.x, camPos.y) : std::nullopt; + if (!liquidType && cameraController) { + const glm::vec3* followTarget = cameraController->getFollowTarget(); + if (followTarget && waterRenderer) { + liquidType = waterRenderer->getWaterTypeAt(followTarget->x, followTarget->y); + } + } + bool canalWater = liquidType && (*liquidType == 5 || *liquidType == 13 || *liquidType == 17); + canalUnderwater = canalWater; + + float fogColor[3] = {0.04f, 0.12f, 0.22f}; + float fogStart = 8.0f; + float fogEnd = 140.0f; + if (canalWater) { + fogColor[0] = 0.012f; + fogColor[1] = 0.055f; + fogColor[2] = 0.12f; + fogStart = 2.5f; + fogEnd = 55.0f; + } + terrainRenderer->setFog(fogColor, fogStart, fogEnd); + glClearColor(fogColor[0], fogColor[1], fogColor[2], 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Re-clear with underwater color } else if (skybox) { // Update terrain fog based on time of day (match sky color) @@ -907,13 +975,6 @@ void Renderer::renderWorld(game::World* world) { auto terrainEnd = std::chrono::steady_clock::now(); lastTerrainRenderMs = std::chrono::duration(terrainEnd - terrainStart).count(); - // Render water after terrain (transparency requires back-to-front rendering) - if (waterRenderer) { - // Use accumulated time for water animation - static float time = 0.0f; - time += 0.016f; // Approximate frame time - waterRenderer->render(*camera, time); - } } // Render weather particles (after terrain/water, before characters) @@ -953,6 +1014,31 @@ void Renderer::renderWorld(game::World* world) { lastM2RenderMs = std::chrono::duration(m2End - m2Start).count(); } + // Render water after opaque terrain/WMO/M2 so transparent surfaces remain visible. + if (waterRenderer && camera) { + static float time = 0.0f; + time += 0.016f; // Approximate frame time + waterRenderer->render(*camera, time); + } + + // Full-screen underwater tint so WMO/M2/characters also feel submerged. + if (underwater && underwaterOverlayShader && underwaterOverlayVAO) { + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + underwaterOverlayShader->use(); + if (canalUnderwater) { + underwaterOverlayShader->setUniform("uTint", glm::vec4(0.01f, 0.05f, 0.11f, 0.50f)); + } else { + underwaterOverlayShader->setUniform("uTint", glm::vec4(0.02f, 0.08f, 0.15f, 0.30f)); + } + glBindVertexArray(underwaterOverlayVAO); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + } + // Render minimap overlay if (minimap && camera && window) { minimap->render(*camera, window->getWidth(), window->getHeight()); diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index 09832531..6a7e629b 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -6,7 +6,9 @@ #include "core/logger.hpp" #include #include +#include #include +#include namespace wowee { namespace rendering { @@ -34,6 +36,9 @@ bool WaterRenderer::initialize() { uniform mat4 view; uniform mat4 projection; uniform float time; + uniform float waveAmp; + uniform float waveFreq; + uniform float waveSpeed; out vec3 FragPos; out vec3 Normal; @@ -41,14 +46,18 @@ bool WaterRenderer::initialize() { out float WaveOffset; void main() { - // Simple pass-through for debugging (no wave animation) vec3 pos = aPos; + // 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; FragPos = vec3(model * vec4(pos, 1.0)); // Use mat3(model) directly - avoids expensive inverse() per vertex Normal = mat3(model) * aNormal; TexCoord = aTexCoord; - WaveOffset = 0.0; + WaveOffset = wave; gl_Position = projection * view * vec4(FragPos, 1.0); } @@ -66,6 +75,8 @@ bool WaterRenderer::initialize() { uniform vec4 waterColor; uniform float waterAlpha; uniform float time; + uniform float shimmerStrength; + uniform float alphaScale; out vec4 FragColor; @@ -80,7 +91,9 @@ bool WaterRenderer::initialize() { // Specular highlights (shininess for water) vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm); - float spec = pow(max(dot(viewDir, reflectDir), 0.0), 64.0); + 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); @@ -96,8 +109,10 @@ bool WaterRenderer::initialize() { vec3 result = (ambient + diffuse + specular) * brightness; - // Apply transparency - FragColor = vec4(result, waterAlpha); + // 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); } )"; @@ -117,6 +132,8 @@ void WaterRenderer::shutdown() { 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(); @@ -150,6 +167,13 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap terrainChunk.position[1], layer.minHeight ); + surface.origin = glm::vec3( + surface.position.x - (static_cast(layer.y) * TILE_SIZE), + surface.position.y - (static_cast(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) { @@ -170,17 +194,48 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap surface.width = layer.width; surface.height = layer.height; - // Copy height data - if (!layer.heights.empty()) { - surface.heights = layer.heights; - } else { - // Flat water at minHeight if no height data - size_t numVertices = (layer.width + 1) * (layer.height + 1); + // 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); } // Copy render mask surface.mask = layer.mask; + 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); + } + } surface.tileX = tileX; surface.tileY = tileY; @@ -213,11 +268,74 @@ void WaterRenderer::removeTile(int tileX, int tileY) { void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liquid, [[maybe_unused]] const glm::mat4& modelMatrix, [[maybe_unused]] uint32_t wmoId) { - // WMO liquid rendering not yet implemented + 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(std::min(255, liquid.xTiles)); + surface.height = static_cast(std::min(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(surface.width) + 1; + const int gridHeight = static_cast(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(surface.width) * static_cast(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([[maybe_unused]] uint32_t wmoId) { - // WMO liquid rendering not yet implemented +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() { @@ -232,6 +350,11 @@ void WaterRenderer::render(const Camera& camera, float time) { return; } + GLboolean cullEnabled = glIsEnabled(GL_CULL_FACE); + if (cullEnabled) { + glDisable(GL_CULL_FACE); + } + // Enable alpha blending for transparent water glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -264,8 +387,22 @@ void WaterRenderer::render(const Camera& camera, float time) { 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); + 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; + 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); @@ -276,19 +413,21 @@ void WaterRenderer::render(const Camera& camera, float time) { // Restore state glDepthMask(GL_TRUE); glDisable(GL_BLEND); + if (cullEnabled) { + 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; - const float TILE_SIZE = 33.33333f / 8.0f; // Size of one tile (same as terrain unitSize) + constexpr float VISUAL_WATER_Z_BIAS = 0.06f; // Prevent z-fighting against city/WMO geometry std::vector vertices; std::vector indices; // Generate vertices - // Match terrain coordinate transformation: pos[0] = baseX - (y * unitSize), pos[1] = baseY - (x * unitSize) for (int y = 0; y < gridHeight; y++) { for (int x = 0; x < gridWidth; x++) { int index = y * gridWidth + x; @@ -301,23 +440,21 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) { height = surface.minHeight; } - // Position - match terrain coordinate transformation (swap and negate) - // Terrain uses: X = baseX - (offsetY * unitSize), Y = baseY - (offsetX * unitSize) - // Also apply layer offset within chunk (xOffset, yOffset) - float posX = surface.position.x - ((surface.yOffset + y) * TILE_SIZE); - float posY = surface.position.y - ((surface.xOffset + x) * TILE_SIZE); - float posZ = height; + glm::vec3 pos = surface.origin + + surface.stepX * static_cast(x) + + surface.stepY * static_cast(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: (", posX, ", ", posY, ", ", posZ, ")"); + LOG_DEBUG("Water vertex: (", pos.x, ", ", pos.y, ", ", pos.z, ")"); debugCount++; } - vertices.push_back(posX); - vertices.push_back(posY); - vertices.push_back(posZ); + 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); @@ -419,13 +556,20 @@ void WaterRenderer::destroyWaterMesh(WaterSurface& surface) { } std::optional WaterRenderer::getWaterHeightAt(float glX, float glY) const { - const float TILE_SIZE = 33.33333f / 8.0f; std::optional best; for (size_t si = 0; si < surfaces.size(); si++) { const auto& surface = surfaces[si]; - float gy = (surface.position.x - glX) / TILE_SIZE - static_cast(surface.yOffset); - float gx = (surface.position.y - glY) / TILE_SIZE - static_cast(surface.xOffset); + 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(surface.width) || gy < 0.0f || gy > static_cast(surface.height)) { @@ -443,6 +587,22 @@ std::optional WaterRenderer::getWaterHeightAt(float glX, float glY) const // 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 = iy * surface.width + ix; + int byteIndex = tileIndex / 8; + int bitIndex = tileIndex % 8; + if (byteIndex < static_cast(surface.mask.size())) { + bool renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0; + if (!renderTile) { + continue; + } + } + } int idx00 = iy * gridWidth + ix; int idx10 = idx00 + 1; @@ -468,7 +628,55 @@ std::optional WaterRenderer::getWaterHeightAt(float glX, float glY) const return best; } -glm::vec4 WaterRenderer::getLiquidColor(uint8_t liquidType) const { +std::optional WaterRenderer::getWaterTypeAt(float glX, float glY) const { + std::optional bestHeight; + std::optional 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(surface.width) || + gy < 0.0f || gy > static_cast(surface.height)) { + continue; + } + + int ix = static_cast(gx); + int iy = static_cast(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(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 { // WoW 3.3.5a LiquidType.dbc IDs: // 1,5,9,13,17 = Water variants (still, slow, fast) // 2,6,10,14 = Ocean @@ -496,12 +704,12 @@ glm::vec4 WaterRenderer::getLiquidColor(uint8_t liquidType) const { } } -float WaterRenderer::getLiquidAlpha(uint8_t liquidType) const { +float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const { uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); switch (basicType) { - case 2: return 0.85f; // Magma - mostly opaque - case 3: return 0.75f; // Slime - semi-opaque - default: return 0.55f; // Water/Ocean - semi-transparent + case 2: return 0.72f; // Magma + case 3: return 0.62f; // Slime + default: return 0.38f; // Water/Ocean } }