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 f094c68e..c437d847 100644 Binary files a/assets/shaders/water.frag.spv and b/assets/shaders/water.frag.spv differ diff --git a/include/rendering/water_renderer.hpp b/include/rendering/water_renderer.hpp index 12b04347..af255ca5 100644 --- a/include/rendering/water_renderer.hpp +++ b/include/rendering/water_renderer.hpp @@ -125,7 +125,12 @@ public: bool isEnabled() const { return renderingEnabled; } std::optional 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) {