From c825dbd75294feb333141ac221adeff0bb9c5ae2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 3 Feb 2026 21:11:10 -0800 Subject: [PATCH] Stabilize city rendering and water/collision behavior --- src/pipeline/adt_loader.cpp | 10 ++--- src/rendering/camera_controller.cpp | 44 +++++++++++++++++++-- src/rendering/renderer.cpp | 23 +++-------- src/rendering/water_renderer.cpp | 59 +++++++++++++++++++---------- src/rendering/wmo_renderer.cpp | 15 ++------ 5 files changed, 94 insertions(+), 57 deletions(-) diff --git a/src/pipeline/adt_loader.cpp b/src/pipeline/adt_loader.cpp index dbca22ef..662655e6 100644 --- a/src/pipeline/adt_loader.cpp +++ b/src/pipeline/adt_loader.cpp @@ -507,10 +507,10 @@ void ADTLoader::parseMH2O(const uint8_t* data, size_t size, ADTTerrain& terrain) if (layer.x + layer.width > 8) layer.width = 8 - layer.x; if (layer.y + layer.height > 8) layer.height = 8 - layer.y; - // Read exists bitmap (which tiles have water) - // The bitmap is (width * height) bits, packed into bytes - size_t numTiles = layer.width * layer.height; - size_t bitmapBytes = (numTiles + 7) / 8; + // Read exists bitmap (which tiles have water). + // In WotLK MH2O this is chunk-wide 8x8 tile flags (64 bits = 8 bytes), + // even when the layer covers a sub-rect. + constexpr size_t bitmapBytes = 8; // Note: offsets in SMLiquidInstance are relative to MH2O chunk start if (offsetExistsBitmap > 0) { @@ -520,7 +520,7 @@ void ADTLoader::parseMH2O(const uint8_t* data, size_t size, ADTTerrain& terrain) std::memcpy(layer.mask.data(), data + bitmapOffset, bitmapBytes); } } else { - // No bitmap means all tiles have water + // No bitmap means all tiles in chunk are valid for this layer. layer.mask.resize(bitmapBytes, 0xFF); } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 8b9fbae6..5cbc713a 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -32,6 +32,20 @@ std::optional selectReachableFloor(const std::optional& terrainH, return best; } +std::optional selectHighestFloor(const std::optional& a, + const std::optional& b, + const std::optional& c) { + std::optional best; + auto consider = [&](const std::optional& h) { + if (!h) return; + if (!best || *h > *best) best = *h; + }; + consider(a); + consider(b); + consider(c); + return best; +} + } // namespace CameraController::CameraController(Camera* cam) : camera(cam) { @@ -182,8 +196,19 @@ void CameraController::update(float deltaTime) { waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y); } constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f; - bool inWater = waterH && targetPos.z < *waterH && - ((*waterH - targetPos.z) <= MAX_SWIM_DEPTH_FROM_SURFACE); + bool inWater = false; + if (waterH && targetPos.z < *waterH && + ((*waterH - targetPos.z) <= MAX_SWIM_DEPTH_FROM_SURFACE)) { + std::optional terrainH; + std::optional wmoH; + std::optional m2H; + if (terrainManager) terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y); + if (wmoRenderer) wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 6.0f); + if (m2Renderer) m2H = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 1.0f); + auto floorH = selectHighestFloor(terrainH, wmoH, m2H); + constexpr float MIN_SWIM_WATER_DEPTH = 1.8f; + inWater = floorH && ((*waterH - *floorH) >= MIN_SWIM_WATER_DEPTH); + } if (inWater) { @@ -651,8 +676,19 @@ void CameraController::update(float deltaTime) { waterH = waterRenderer->getWaterHeightAt(newPos.x, newPos.y); } constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f; - bool inWater = waterH && feetZ < *waterH && - ((*waterH - feetZ) <= MAX_SWIM_DEPTH_FROM_SURFACE); + bool inWater = false; + if (waterH && feetZ < *waterH && + ((*waterH - feetZ) <= MAX_SWIM_DEPTH_FROM_SURFACE)) { + std::optional terrainH; + std::optional wmoH; + std::optional m2H; + if (terrainManager) terrainH = terrainManager->getHeightAt(newPos.x, newPos.y); + if (wmoRenderer) wmoH = wmoRenderer->getFloorHeight(newPos.x, newPos.y, feetZ + 6.0f); + if (m2Renderer) m2H = m2Renderer->getFloorHeight(newPos.x, newPos.y, feetZ + 1.0f); + auto floorH = selectHighestFloor(terrainH, wmoH, m2H); + constexpr float MIN_SWIM_WATER_DEPTH = 1.8f; + inWater = floorH && ((*waterH - *floorH) >= MIN_SWIM_WATER_DEPTH); + } if (inWater) { diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 9f2d7ce1..05f69942 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -930,7 +930,7 @@ void Renderer::renderWorld(game::World* world) { 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; + constexpr float UNDERWATER_ENTER_EPS = 1.10f; if (waterH && camPos.z < (*waterH - UNDERWATER_ENTER_EPS) && (*waterH - camPos.z) <= MAX_UNDERWATER_DEPTH) { @@ -947,23 +947,10 @@ void Renderer::renderWorld(game::World* world) { liquidType = waterRenderer->getWaterTypeAt(followTarget->x, followTarget->y); } } - bool canalWater = liquidType && (*liquidType == 5 || *liquidType == 13 || *liquidType == 17); - canalUnderwater = canalWater; + canalUnderwater = liquidType && (*liquidType == 5 || *liquidType == 13 || *liquidType == 17); + } - 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) { + if (skybox) { // Update terrain fog based on time of day (match sky color) glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay); float fogColorArray[3] = {horizonColor.r, horizonColor.g, horizonColor.b}; @@ -1022,7 +1009,7 @@ void Renderer::renderWorld(game::World* world) { } // Full-screen underwater tint so WMO/M2/characters also feel submerged. - if (underwater && underwaterOverlayShader && underwaterOverlayVAO) { + if (false && underwater && underwaterOverlayShader && underwaterOverlayVAO) { glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index 6a7e629b..4ccf9505 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -222,20 +222,6 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap // 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; @@ -375,6 +361,11 @@ void WaterRenderer::render(const Camera& camera, float time) { // Render each water surface for (const auto& surface : surfaces) { + // WMO liquid parsing is still not reliable; render terrain water only + // to avoid large invalid sheets popping over city geometry. + if (surface.wmoId != 0) { + continue; + } if (surface.vao == 0) { continue; } @@ -422,7 +413,7 @@ 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.06f; // Prevent z-fighting against city/WMO geometry + constexpr float VISUAL_WATER_Z_BIAS = 0.02f; // Small bias to avoid obvious overdraw on city meshes std::vector vertices; std::vector indices; @@ -473,11 +464,23 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) { // Check render mask - each bit represents a tile bool renderTile = true; if (!surface.mask.empty()) { - int tileIndex = y * surface.width + x; + int tileIndex; + if (surface.wmoId == 0 && surface.mask.size() >= 8) { + // Terrain MH2O mask is chunk-wide 8x8. + int cx = static_cast(surface.xOffset) + x; + int cy = static_cast(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(surface.mask.size())) { - renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0; + uint8_t maskByte = surface.mask[byteIndex]; + bool lsbOrder = (maskByte & (1 << bitIndex)) != 0; + bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0; + renderTile = lsbOrder || msbOrder; } } @@ -560,6 +563,11 @@ std::optional WaterRenderer::getWaterHeightAt(float glX, float glY) const for (size_t si = 0; si < surfaces.size(); si++) { const auto& surface = surfaces[si]; + // Use terrain/MH2O water for gameplay queries. WMO liquid extents are + // currently render-only and can overlap interiors. + if (surface.wmoId != 0) { + continue; + } 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); @@ -593,11 +601,21 @@ std::optional WaterRenderer::getWaterHeightAt(float glX, float glY) const // 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 tileIndex; + if (surface.wmoId == 0 && surface.mask.size() >= 8) { + int cx = static_cast(surface.xOffset) + ix; + int cy = static_cast(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(surface.mask.size())) { - bool renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0; + 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; } @@ -633,6 +651,9 @@ std::optional WaterRenderer::getWaterTypeAt(float glX, float glY) cons std::optional bestType; for (const auto& surface : surfaces) { + if (surface.wmoId != 0) { + continue; + } 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); diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 0849974a..1f8f8010 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -459,17 +459,10 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: frustum.extractFromMatrix(projection * view); // Render all instances with instance-level culling - const glm::vec3 camPos = camera.getPosition(); - const float maxRenderDistance = 320.0f; // More aggressive culling for city performance - const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; - for (const auto& instance : instances) { - // Instance-level distance culling - glm::vec3 toCam = instance.position - camPos; - float distSq = glm::dot(toCam, toCam); - if (distSq > maxRenderDistanceSq) { - continue; // Skip instances that are too far - } + // NOTE: Disabled hard instance-distance culling for WMOs. + // Large city WMOs can have instance origins far from local camera position, + // causing whole city sections to disappear unexpectedly. auto modelIt = loadedModels.find(instance.modelId); if (modelIt == loadedModels.end()) { @@ -1030,7 +1023,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, if (moveDistXY < 0.001f) return false; // Player collision parameters - const float PLAYER_RADIUS = 0.6f; // Character collision radius + const float PLAYER_RADIUS = 0.50f; // Slightly narrower to pass tight doorways/interiors const float PLAYER_HEIGHT = 2.0f; // Player height for wall checks const float MAX_STEP_HEIGHT = 0.85f; // Balanced step-up without wall pass-through