Stabilize city rendering and water/collision behavior

This commit is contained in:
Kelsi 2026-02-03 21:11:10 -08:00
parent d0dac0df07
commit c825dbd752
5 changed files with 94 additions and 57 deletions

View file

@ -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.x + layer.width > 8) layer.width = 8 - layer.x;
if (layer.y + layer.height > 8) layer.height = 8 - layer.y; if (layer.y + layer.height > 8) layer.height = 8 - layer.y;
// Read exists bitmap (which tiles have water) // Read exists bitmap (which tiles have water).
// The bitmap is (width * height) bits, packed into bytes // In WotLK MH2O this is chunk-wide 8x8 tile flags (64 bits = 8 bytes),
size_t numTiles = layer.width * layer.height; // even when the layer covers a sub-rect.
size_t bitmapBytes = (numTiles + 7) / 8; constexpr size_t bitmapBytes = 8;
// Note: offsets in SMLiquidInstance are relative to MH2O chunk start // Note: offsets in SMLiquidInstance are relative to MH2O chunk start
if (offsetExistsBitmap > 0) { 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); std::memcpy(layer.mask.data(), data + bitmapOffset, bitmapBytes);
} }
} else { } 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); layer.mask.resize(bitmapBytes, 0xFF);
} }

View file

@ -32,6 +32,20 @@ std::optional<float> selectReachableFloor(const std::optional<float>& terrainH,
return best; return best;
} }
std::optional<float> selectHighestFloor(const std::optional<float>& a,
const std::optional<float>& b,
const std::optional<float>& c) {
std::optional<float> best;
auto consider = [&](const std::optional<float>& h) {
if (!h) return;
if (!best || *h > *best) best = *h;
};
consider(a);
consider(b);
consider(c);
return best;
}
} // namespace } // namespace
CameraController::CameraController(Camera* cam) : camera(cam) { CameraController::CameraController(Camera* cam) : camera(cam) {
@ -182,8 +196,19 @@ void CameraController::update(float deltaTime) {
waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y); waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y);
} }
constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f; constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f;
bool inWater = waterH && targetPos.z < *waterH && bool inWater = false;
((*waterH - targetPos.z) <= MAX_SWIM_DEPTH_FROM_SURFACE); if (waterH && targetPos.z < *waterH &&
((*waterH - targetPos.z) <= MAX_SWIM_DEPTH_FROM_SURFACE)) {
std::optional<float> terrainH;
std::optional<float> wmoH;
std::optional<float> 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) { if (inWater) {
@ -651,8 +676,19 @@ void CameraController::update(float deltaTime) {
waterH = waterRenderer->getWaterHeightAt(newPos.x, newPos.y); waterH = waterRenderer->getWaterHeightAt(newPos.x, newPos.y);
} }
constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f; constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f;
bool inWater = waterH && feetZ < *waterH && bool inWater = false;
((*waterH - feetZ) <= MAX_SWIM_DEPTH_FROM_SURFACE); if (waterH && feetZ < *waterH &&
((*waterH - feetZ) <= MAX_SWIM_DEPTH_FROM_SURFACE)) {
std::optional<float> terrainH;
std::optional<float> wmoH;
std::optional<float> 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) { if (inWater) {

View file

@ -930,7 +930,7 @@ void Renderer::renderWorld(game::World* world) {
constexpr float MAX_UNDERWATER_DEPTH = 12.0f; constexpr float MAX_UNDERWATER_DEPTH = 12.0f;
// Require camera to be meaningfully below the surface before // Require camera to be meaningfully below the surface before
// underwater fog/tint kicks in (avoids "wrong plane" near surface). // 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 && if (waterH &&
camPos.z < (*waterH - UNDERWATER_ENTER_EPS) && camPos.z < (*waterH - UNDERWATER_ENTER_EPS) &&
(*waterH - camPos.z) <= MAX_UNDERWATER_DEPTH) { (*waterH - camPos.z) <= MAX_UNDERWATER_DEPTH) {
@ -947,23 +947,10 @@ void Renderer::renderWorld(game::World* world) {
liquidType = waterRenderer->getWaterTypeAt(followTarget->x, followTarget->y); liquidType = waterRenderer->getWaterTypeAt(followTarget->x, followTarget->y);
} }
} }
bool canalWater = liquidType && (*liquidType == 5 || *liquidType == 13 || *liquidType == 17); canalUnderwater = liquidType && (*liquidType == 5 || *liquidType == 13 || *liquidType == 17);
canalUnderwater = canalWater; }
float fogColor[3] = {0.04f, 0.12f, 0.22f}; if (skybox) {
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) // Update terrain fog based on time of day (match sky color)
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay); glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
float fogColorArray[3] = {horizonColor.r, horizonColor.g, horizonColor.b}; 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. // Full-screen underwater tint so WMO/M2/characters also feel submerged.
if (underwater && underwaterOverlayShader && underwaterOverlayVAO) { if (false && underwater && underwaterOverlayShader && underwaterOverlayVAO) {
glDisable(GL_DEPTH_TEST); glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

View file

@ -222,20 +222,6 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
// Copy render mask // Copy render mask
surface.mask = layer.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.tileX = tileX;
surface.tileY = tileY; surface.tileY = tileY;
@ -375,6 +361,11 @@ void WaterRenderer::render(const Camera& camera, float time) {
// Render each water surface // Render each water surface
for (const auto& surface : surfaces) { 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) { if (surface.vao == 0) {
continue; continue;
} }
@ -422,7 +413,7 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) {
// Variable-size grid based on water layer dimensions // Variable-size grid based on water layer dimensions
const int gridWidth = surface.width + 1; // Vertices = tiles + 1 const int gridWidth = surface.width + 1; // Vertices = tiles + 1
const int gridHeight = surface.height + 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<float> vertices; std::vector<float> vertices;
std::vector<uint32_t> indices; std::vector<uint32_t> indices;
@ -473,11 +464,23 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) {
// Check render mask - each bit represents a tile // Check render mask - each bit represents a tile
bool renderTile = true; bool renderTile = true;
if (!surface.mask.empty()) { 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<int>(surface.xOffset) + x;
int cy = static_cast<int>(surface.yOffset) + y;
tileIndex = cy * 8 + cx;
} else {
// Local mask indexing (WMO/custom).
tileIndex = y * surface.width + x;
}
int byteIndex = tileIndex / 8; int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8; int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) { if (byteIndex < static_cast<int>(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<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const
for (size_t si = 0; si < surfaces.size(); si++) { for (size_t si = 0; si < surfaces.size(); si++) {
const auto& surface = surfaces[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 rel(glX - surface.origin.x, glY - surface.origin.y);
glm::vec2 stepX(surface.stepX.x, surface.stepX.y); glm::vec2 stepX(surface.stepX.x, surface.stepX.y);
glm::vec2 stepY(surface.stepY.x, surface.stepY.y); glm::vec2 stepY(surface.stepY.x, surface.stepY.y);
@ -593,11 +601,21 @@ std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const
// Respect per-tile mask so holes/non-liquid tiles do not count as swimmable. // Respect per-tile mask so holes/non-liquid tiles do not count as swimmable.
if (!surface.mask.empty()) { if (!surface.mask.empty()) {
int tileIndex = iy * surface.width + ix; int tileIndex;
if (surface.wmoId == 0 && surface.mask.size() >= 8) {
int cx = static_cast<int>(surface.xOffset) + ix;
int cy = static_cast<int>(surface.yOffset) + iy;
tileIndex = cy * 8 + cx;
} else {
tileIndex = iy * surface.width + ix;
}
int byteIndex = tileIndex / 8; int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8; int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) { if (byteIndex < static_cast<int>(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) { if (!renderTile) {
continue; continue;
} }
@ -633,6 +651,9 @@ std::optional<uint16_t> WaterRenderer::getWaterTypeAt(float glX, float glY) cons
std::optional<uint16_t> bestType; std::optional<uint16_t> bestType;
for (const auto& surface : surfaces) { for (const auto& surface : surfaces) {
if (surface.wmoId != 0) {
continue;
}
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y); glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
glm::vec2 stepX(surface.stepX.x, surface.stepX.y); glm::vec2 stepX(surface.stepX.x, surface.stepX.y);
glm::vec2 stepY(surface.stepY.x, surface.stepY.y); glm::vec2 stepY(surface.stepY.x, surface.stepY.y);

View file

@ -459,17 +459,10 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm:
frustum.extractFromMatrix(projection * view); frustum.extractFromMatrix(projection * view);
// Render all instances with instance-level culling // 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) { for (const auto& instance : instances) {
// Instance-level distance culling // NOTE: Disabled hard instance-distance culling for WMOs.
glm::vec3 toCam = instance.position - camPos; // Large city WMOs can have instance origins far from local camera position,
float distSq = glm::dot(toCam, toCam); // causing whole city sections to disappear unexpectedly.
if (distSq > maxRenderDistanceSq) {
continue; // Skip instances that are too far
}
auto modelIt = loadedModels.find(instance.modelId); auto modelIt = loadedModels.find(instance.modelId);
if (modelIt == loadedModels.end()) { 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; if (moveDistXY < 0.001f) return false;
// Player collision parameters // 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 PLAYER_HEIGHT = 2.0f; // Player height for wall checks
const float MAX_STEP_HEIGHT = 0.85f; // Balanced step-up without wall pass-through const float MAX_STEP_HEIGHT = 0.85f; // Balanced step-up without wall pass-through