mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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
This commit is contained in:
parent
6563eebb60
commit
fb4ff46fe3
6 changed files with 193 additions and 41 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -125,7 +125,12 @@ public:
|
|||
bool isEnabled() const { return renderingEnabled; }
|
||||
|
||||
std::optional<float> 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<float> getNearestWaterHeightAt(float glX, float glY, float queryZ, float maxAbove = 15.0f) const;
|
||||
std::optional<uint16_t> getWaterTypeAt(float glX, float glY) const;
|
||||
bool isWmoWaterAt(float glX, float glY) const;
|
||||
|
||||
int getSurfaceCount() const { return static_cast<int>(surfaces.size()); }
|
||||
|
||||
|
|
|
|||
|
|
@ -597,7 +597,17 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& 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<float>(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<float>(groupData, parseOffset);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -691,30 +691,39 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
|
|||
const int gridWidth = static_cast<int>(surface.width) + 1;
|
||||
const int gridHeight = static_cast<int>(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<int>(std::floor((32.0f - surface.origin.x / 533.33333f)));
|
||||
int tilePosY = static_cast<int>(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<size_t>(surface.width) * static_cast<size_t>(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<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const
|
|||
return best;
|
||||
}
|
||||
|
||||
std::optional<float> WaterRenderer::getNearestWaterHeightAt(float glX, float glY, float queryZ, float maxAbove) const {
|
||||
std::optional<float> 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<float>(surface.width) ||
|
||||
gy < 0.0f || gy > static_cast<float>(surface.height)) continue;
|
||||
|
||||
int gridWidth = surface.width + 1;
|
||||
int ix = static_cast<int>(gx);
|
||||
int iy = static_cast<int>(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<int>(surface.yOffset) + iy) * 8 +
|
||||
(static_cast<int>(surface.xOffset) + ix);
|
||||
} else {
|
||||
tileIndex = iy * surface.width + ix;
|
||||
}
|
||||
int byteIndex = tileIndex / 8;
|
||||
int bitIndex = tileIndex % 8;
|
||||
if (byteIndex < static_cast<int>(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<int>(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<uint16_t> WaterRenderer::getWaterTypeAt(float glX, float glY) const {
|
||||
std::optional<float> bestHeight;
|
||||
std::optional<uint16_t> bestType;
|
||||
|
|
@ -1171,6 +1253,24 @@ std::optional<uint16_t> 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<float>(surface.width) &&
|
||||
gy >= 0.0f && gy <= static_cast<float>(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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue