From fb4ff46fe305c58b41eed3b818cea9b80226b0bf Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 23 Feb 2026 00:18:32 -0800 Subject: [PATCH] Fix WMO water rendering: correct MLIQ parsing, tile masking, and depth effects - Fix MLIQ vertex stride: each vertex is 8 bytes (4 flow + 4 height), not 4 - Use MLIQ tile flags to mask out tiles with no liquid (bridges, covered areas) - Disable wave displacement on WMO water to prevent edge slosh artifacts - Convert screen-space depth to vertical depth for shoreline foam and water transparency, preventing false shoreline effects on occluding geometry - Add underwater blue fog overlay and scene fog shift (terrain water only) - Add getNearestWaterHeightAt to avoid false underwater detection from elevated WMO water surfaces - Tint refracted scene toward water color to mask occlusion edge artifacts - Lower WMO water by 1 unit to match terrain water level --- assets/shaders/water.frag.glsl | 33 +++++-- assets/shaders/water.frag.spv | Bin 29828 -> 30696 bytes include/rendering/water_renderer.hpp | 5 + src/pipeline/wmo_loader.cpp | 12 ++- src/rendering/renderer.cpp | 44 ++++++--- src/rendering/water_renderer.cpp | 140 +++++++++++++++++++++++---- 6 files changed, 193 insertions(+), 41 deletions(-) diff --git a/assets/shaders/water.frag.glsl b/assets/shaders/water.frag.glsl index acc0566e..73ddf5fc 100644 --- a/assets/shaders/water.frag.glsl +++ b/assets/shaders/water.frag.glsl @@ -193,6 +193,13 @@ void main() { float waterLinDepth = linearizeDepth(gl_FragCoord.z, near, far); float depthDiff = max(sceneLinDepth - waterLinDepth, 0.0); + // Convert screen-space depth difference to approximate vertical water depth. + // depthDiff is along the view ray; multiply by the vertical component of + // the view direction so grazing angles don't falsely trigger shoreline foam + // on occluding geometry (bridges, posts) that isn't at the waterline. + float verticalFactor = abs(viewDir.z); // 1.0 looking straight down, ~0 at grazing + float verticalDepth = depthDiff * max(verticalFactor, 0.05); + // ============================================================ // Beer-Lambert absorption // ============================================================ @@ -200,18 +207,24 @@ void main() { if (basicType > 0.5 && basicType < 1.5) { absorptionCoeff = vec3(0.35, 0.06, 0.04); } - vec3 absorbed = exp(-absorptionCoeff * depthDiff); + vec3 absorbed = exp(-absorptionCoeff * verticalDepth); + + // Underwater blue fog — geometry below the waterline fades to a blue haze + // with depth, masking occlusion edge artifacts and giving a natural look. + vec3 underwaterFogColor = waterColor.rgb * 0.5 + vec3(0.04, 0.10, 0.20); + float underwaterFogFade = 1.0 - exp(-verticalDepth * 0.35); + vec3 foggedScene = mix(sceneRefract, underwaterFogColor, underwaterFogFade); vec3 shallowColor = waterColor.rgb * 1.2; vec3 deepColor = waterColor.rgb * vec3(0.3, 0.5, 0.7); - float depthFade = 1.0 - exp(-depthDiff * 0.15); + float depthFade = 1.0 - exp(-verticalDepth * 0.15); vec3 waterBody = mix(shallowColor, deepColor, depthFade); - vec3 refractedColor = mix(sceneRefract * absorbed, waterBody, depthFade * 0.7); + vec3 refractedColor = mix(foggedScene * absorbed, waterBody, depthFade * 0.7); - if (depthDiff < 0.01) { + if (verticalDepth < 0.01) { float opticalDepth = 1.0 - exp(-dist * 0.004); - refractedColor = mix(sceneRefract, waterBody, opticalDepth * 0.6); + refractedColor = mix(foggedScene, waterBody, opticalDepth * 0.6); } vec3 litBase = waterBody * (ambientColor.rgb * 0.7 + NdotL * lightColor.rgb * 0.5); @@ -280,9 +293,11 @@ void main() { // ============================================================ // Shoreline foam — scattered particles, not smooth bands + // Only on terrain water (waveAmp > 0); WMO water (canals, indoor) + // has waveAmp == 0 and should not show shoreline interaction. // ============================================================ - if (basicType < 1.5 && depthDiff > 0.01) { - float foamDepthMask = 1.0 - smoothstep(0.0, 1.8, depthDiff); + if (basicType < 1.5 && verticalDepth > 0.01 && push.waveAmp > 0.0) { + float foamDepthMask = 1.0 - smoothstep(0.0, 1.8, verticalDepth); // Fine scattered particles float cells1 = cellularFoam(FragPos.xy * 14.0 + time * vec2(0.15, 0.08)); @@ -300,14 +315,14 @@ void main() { float noiseMask = noiseValue(FragPos.xy * 3.0 + time * 0.15); float foam = (foam1 + foam2 + foam3) * foamDepthMask * smoothstep(0.3, 0.6, noiseMask); - foam *= smoothstep(0.0, 0.1, depthDiff); + foam *= smoothstep(0.0, 0.1, verticalDepth); color = mix(color, vec3(0.92, 0.95, 0.98), clamp(foam, 0.0, 0.45)); } // ============================================================ // Wave crest foam (ocean only) — particle-based // ============================================================ - if (basicType > 0.5 && basicType < 1.5) { + if (basicType > 0.5 && basicType < 1.5 && push.waveAmp > 0.0) { float crestMask = smoothstep(0.5, 1.0, WaveOffset); float crestCells = cellularFoam(FragPos.xy * 6.0 + time * vec2(0.12, 0.08)); float crestFoam = (1.0 - smoothstep(0.0, 0.18, crestCells)) * crestMask; diff --git a/assets/shaders/water.frag.spv b/assets/shaders/water.frag.spv index f094c68e79de38a552a285eed7f9ca264d6822ec..c437d847f782e0d759583c9d2e35da5f48c5a3a3 100644 GIT binary patch literal 30696 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 x}ejJnUmwbnT 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 Vw$JtDwUBZB5^T<%X=2Z`{|D`|UjYCB literal 29828 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 getWaterHeightAt(float glX, float glY) const; + /// Like getWaterHeightAt but only returns water surfaces whose height is + /// close to the query Z (within maxAbove units above). Avoids false + /// underwater detection from elevated WMO water far above the camera. + std::optional getNearestWaterHeightAt(float glX, float glY, float queryZ, float maxAbove = 15.0f) const; std::optional getWaterTypeAt(float glX, float glY) const; + bool isWmoWaterAt(float glX, float glY) const; int getSurfaceCount() const { return static_cast(surfaces.size()); } diff --git a/src/pipeline/wmo_loader.cpp b/src/pipeline/wmo_loader.cpp index 1c32f0e7..b947004e 100644 --- a/src/pipeline/wmo_loader.cpp +++ b/src/pipeline/wmo_loader.cpp @@ -597,7 +597,17 @@ bool WMOLoader::loadGroup(const std::vector& groupData, group.liquid.heights.clear(); group.liquid.flags.clear(); - if (vertexCount > 0 && bytesRemaining >= vertexCount * sizeof(float)) { + // MLIQ vertex data: each vertex is 8 bytes — + // 4 bytes flow/unknown data + 4 bytes float height. + const size_t VERTEX_STRIDE = 8; // bytes per vertex + if (vertexCount > 0 && bytesRemaining >= vertexCount * VERTEX_STRIDE) { + group.liquid.heights.resize(vertexCount); + for (size_t i = 0; i < vertexCount; i++) { + parseOffset += 4; // skip flow/unknown data + group.liquid.heights[i] = read(groupData, parseOffset); + } + } else if (vertexCount > 0 && bytesRemaining >= vertexCount * sizeof(float)) { + // Fallback: try reading as plain floats if stride doesn't fit group.liquid.heights.resize(vertexCount); for (size_t i = 0; i < vertexCount; i++) { group.liquid.heights[i] = read(groupData, parseOffset); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index f6b2387b..035f1b12 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -584,6 +584,23 @@ void Renderer::updatePerFrameUBO() { currentFrameData.fogColor = glm::vec4(lp.fogColor, 1.0f); currentFrameData.fogParams.x = lp.fogStart; currentFrameData.fogParams.y = lp.fogEnd; + + // Shift fog to blue when camera is significantly underwater (terrain water only). + if (waterRenderer && camera) { + glm::vec3 camPos = camera->getPosition(); + auto waterH = waterRenderer->getNearestWaterHeightAt(camPos.x, camPos.y, camPos.z); + constexpr float MIN_SUBMERSION = 2.0f; + if (waterH && camPos.z < (*waterH - MIN_SUBMERSION) + && !waterRenderer->isWmoWaterAt(camPos.x, camPos.y)) { + float depth = *waterH - camPos.z - MIN_SUBMERSION; + float blend = glm::clamp(1.0f - std::exp(-depth * 0.08f), 0.0f, 0.7f); + glm::vec3 underwaterFog(0.03f, 0.09f, 0.18f); + glm::vec3 blendedFog = glm::mix(lp.fogColor, underwaterFog, blend); + currentFrameData.fogColor = glm::vec4(blendedFog, 1.0f); + currentFrameData.fogParams.x = glm::mix(lp.fogStart, 20.0f, blend); + currentFrameData.fogParams.y = glm::mix(lp.fogEnd, 200.0f, blend); + } + } } currentFrameData.shadowParams = glm::vec4(shadowsEnabled ? 1.0f : 0.0f, 0.5f, 0.0f, 0.0f); @@ -3293,22 +3310,27 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { questMarkerRenderer->render(currentCmd, perFrameSet, *camera); } - // Underwater tint overlay — detect camera position relative to water surface - if (overlayPipeline && cameraController && cameraController->isSwimming() - && waterRenderer && camera) { + // Underwater blue fog overlay — only for terrain water, not WMO water. + if (overlayPipeline && waterRenderer && camera) { glm::vec3 camPos = camera->getPosition(); - auto waterH = waterRenderer->getWaterHeightAt(camPos.x, camPos.y); - constexpr float UNDERWATER_EPS = 1.10f; - constexpr float MAX_DEPTH = 12.0f; - if (waterH && camPos.z < (*waterH - UNDERWATER_EPS) - && (*waterH - camPos.z) <= MAX_DEPTH) { - // Check for canal (liquid type 5, 13, 17) vs open water + auto waterH = waterRenderer->getNearestWaterHeightAt(camPos.x, camPos.y, camPos.z); + constexpr float MIN_SUBMERSION_OVERLAY = 1.5f; + if (waterH && camPos.z < (*waterH - MIN_SUBMERSION_OVERLAY) + && !waterRenderer->isWmoWaterAt(camPos.x, camPos.y)) { + float depth = *waterH - camPos.z - MIN_SUBMERSION_OVERLAY; + + // Check for canal (liquid type 5, 13, 17) — denser/darker fog bool canal = false; if (auto lt = waterRenderer->getWaterTypeAt(camPos.x, camPos.y)) canal = (*lt == 5 || *lt == 13 || *lt == 17); + + // Fog opacity increases with depth: thin at surface, thick deep down + float fogStrength = 1.0f - std::exp(-depth * (canal ? 0.25f : 0.12f)); + fogStrength = glm::clamp(fogStrength, 0.0f, 0.75f); + glm::vec4 tint = canal - ? glm::vec4(0.01f, 0.05f, 0.11f, 0.50f) - : glm::vec4(0.02f, 0.08f, 0.15f, 0.30f); + ? glm::vec4(0.01f, 0.04f, 0.10f, fogStrength) + : glm::vec4(0.03f, 0.09f, 0.18f, fogStrength); renderOverlay(tint); } } diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index 62d42907..e10ebbed 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -691,30 +691,39 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu const int gridWidth = static_cast(surface.width) + 1; const int gridHeight = static_cast(surface.height) + 1; const int vertexCount = gridWidth * gridHeight; - surface.heights.assign(vertexCount, surface.origin.z); - surface.minHeight = surface.origin.z; - surface.maxHeight = surface.origin.z; - // Stormwind WMO water lowering - int tilePosX = static_cast(std::floor((32.0f - surface.origin.x / 533.33333f))); - int tilePosY = static_cast(std::floor((32.0f - surface.origin.y / 533.33333f))); - bool isStormwindArea = (tilePosX >= 28 && tilePosX <= 50 && tilePosY >= 28 && tilePosY <= 52); - if (isStormwindArea && surface.origin.z > 94.0f) { - 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) { - for (float& h : surface.heights) h -= 1.0f; - surface.minHeight -= 1.0f; - surface.maxHeight -= 1.0f; - } - } + // WMO liquid base heights sit ~2 units above the visual waterline. + // Lower them to match surrounding terrain water and prevent clipping + // at bridge edges and walkways. + constexpr float WMO_WATER_Z_OFFSET = -1.0f; + float adjustedZ = surface.origin.z + WMO_WATER_Z_OFFSET; + surface.heights.assign(vertexCount, adjustedZ); + surface.minHeight = adjustedZ; + surface.maxHeight = adjustedZ; + surface.origin.z = adjustedZ; + surface.position.z = adjustedZ; + if (surface.origin.z > 300.0f || surface.origin.z < -100.0f) return; + // Build tile mask from MLIQ flags — tiles with (flag & 0x0F) == 0x0F have no liquid size_t tileCount = static_cast(surface.width) * static_cast(surface.height); size_t maskBytes = (tileCount + 7) / 8; - surface.mask.assign(maskBytes, 0xFF); + surface.mask.assign(maskBytes, 0x00); + for (size_t t = 0; t < tileCount; t++) { + bool hasLiquid = true; + if (t < liquid.flags.size()) { + // In WoW MLIQ format, (flags & 0x0F) == 0x0F means "no liquid" for this tile + if ((liquid.flags[t] & 0x0F) == 0x0F) { + hasLiquid = false; + } + } + if (hasLiquid) { + size_t byteIdx = t / 8; + size_t bitIdx = t % 8; + surface.mask[byteIdx] |= (1 << bitIdx); + } + } createWaterMesh(surface); if (surface.indexCount > 0) { @@ -768,9 +777,12 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, if (surface.vertexBuffer == VK_NULL_HANDLE || surface.indexCount == 0) continue; if (!surface.materialSet) continue; - bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5); + bool isWmoWater = (surface.wmoId != 0); + bool canalProfile = isWmoWater || (surface.liquidType == 5); uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4); - float waveAmp = canalProfile ? 0.10f : (basicType == 1 ? 0.35f : 0.18f); + // WMO water gets no wave displacement — prevents visible slosh at + // geometry edges (bridges, docks) where water is far below the surface. + float waveAmp = isWmoWater ? 0.0f : (basicType == 1 ? 0.35f : 0.18f); float waveFreq = canalProfile ? 0.35f : (basicType == 1 ? 0.20f : 0.30f); float waveSpeed = canalProfile ? 1.00f : (basicType == 1 ? 1.20f : 1.40f); @@ -1121,6 +1133,76 @@ std::optional WaterRenderer::getWaterHeightAt(float glX, float glY) const return best; } +std::optional WaterRenderer::getNearestWaterHeightAt(float glX, float glY, float queryZ, float maxAbove) const { + std::optional best; + float bestDist = 1e9f; + + for (const auto& surface : surfaces) { + glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y); + glm::vec2 sX(surface.stepX.x, surface.stepX.y); + glm::vec2 sY(surface.stepY.x, surface.stepY.y); + float lenSqX = glm::dot(sX, sX); + float lenSqY = glm::dot(sY, sY); + if (lenSqX < 1e-6f || lenSqY < 1e-6f) continue; + float gx = glm::dot(rel, sX) / lenSqX; + float gy = glm::dot(rel, sY) / lenSqY; + + if (gx < 0.0f || gx > static_cast(surface.width) || + gy < 0.0f || gy > static_cast(surface.height)) continue; + + int gridWidth = surface.width + 1; + int ix = static_cast(gx); + int iy = static_cast(gy); + float fx = gx - ix; + float fy = gy - iy; + + 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; + + if (!surface.mask.empty()) { + int tileIndex; + if (surface.wmoId == 0 && surface.mask.size() >= 8) { + tileIndex = (static_cast(surface.yOffset) + iy) * 8 + + (static_cast(surface.xOffset) + ix); + } else { + tileIndex = iy * surface.width + ix; + } + int byteIndex = tileIndex / 8; + int bitIndex = tileIndex % 8; + if (byteIndex < static_cast(surface.mask.size())) { + uint8_t maskByte = surface.mask[byteIndex]; + bool renderTile = (maskByte & (1 << bitIndex)) || (maskByte & (1 << (7 - bitIndex))); + if (!renderTile) continue; + } + } + + int idx00 = iy * gridWidth + ix; + int idx10 = idx00 + 1; + int idx01 = idx00 + gridWidth; + int idx11 = idx01 + 1; + + int total = static_cast(surface.heights.size()); + if (idx11 >= total) continue; + + float h00 = surface.heights[idx00], h10 = surface.heights[idx10]; + float h01 = surface.heights[idx01], h11 = surface.heights[idx11]; + float h = h00*(1-fx)*(1-fy) + h10*fx*(1-fy) + h01*(1-fx)*fy + h11*fx*fy; + + // Only consider water that's above queryZ but not too far above + if (h < queryZ - 2.0f) continue; // water below camera, skip + if (h > queryZ + maxAbove) continue; // water way above camera, skip + + float dist = std::abs(h - queryZ); + if (!best || dist < bestDist) { + best = h; + bestDist = dist; + } + } + + return best; +} + std::optional WaterRenderer::getWaterTypeAt(float glX, float glY) const { std::optional bestHeight; std::optional bestType; @@ -1171,6 +1253,24 @@ std::optional WaterRenderer::getWaterTypeAt(float glX, float glY) cons return bestType; } +bool WaterRenderer::isWmoWaterAt(float glX, float glY) const { + for (const auto& surface : surfaces) { + if (surface.wmoId == 0) continue; + glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y); + glm::vec2 sX(surface.stepX.x, surface.stepX.y); + glm::vec2 sY(surface.stepY.x, surface.stepY.y); + float lenSqX = glm::dot(sX, sX); + float lenSqY = glm::dot(sY, sY); + if (lenSqX < 1e-6f || lenSqY < 1e-6f) continue; + float gx = glm::dot(rel, sX) / lenSqX; + float gy = glm::dot(rel, sY) / lenSqY; + if (gx >= 0.0f && gx <= static_cast(surface.width) && + gy >= 0.0f && gy <= static_cast(surface.height)) + return true; + } + return false; +} + glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const { uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); switch (basicType) {