mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Refine water rendering, swimming, and underwater visuals
This commit is contained in:
parent
1951dbd9e6
commit
d0dac0df07
8 changed files with 440 additions and 56 deletions
|
|
@ -124,7 +124,7 @@ private:
|
||||||
static constexpr float SWIM_GRAVITY = -5.0f;
|
static constexpr float SWIM_GRAVITY = -5.0f;
|
||||||
static constexpr float SWIM_BUOYANCY = 8.0f;
|
static constexpr float SWIM_BUOYANCY = 8.0f;
|
||||||
static constexpr float SWIM_SINK_SPEED = -3.0f;
|
static constexpr float SWIM_SINK_SPEED = -3.0f;
|
||||||
static constexpr float WATER_SURFACE_OFFSET = 1.5f;
|
static constexpr float WATER_SURFACE_OFFSET = 0.9f;
|
||||||
|
|
||||||
// State
|
// State
|
||||||
bool enabled = true;
|
bool enabled = true;
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ class CharacterRenderer;
|
||||||
class WMORenderer;
|
class WMORenderer;
|
||||||
class M2Renderer;
|
class M2Renderer;
|
||||||
class Minimap;
|
class Minimap;
|
||||||
|
class Shader;
|
||||||
|
|
||||||
class Renderer {
|
class Renderer {
|
||||||
public:
|
public:
|
||||||
|
|
@ -153,6 +154,9 @@ private:
|
||||||
std::unique_ptr<audio::FootstepManager> footstepManager;
|
std::unique_ptr<audio::FootstepManager> footstepManager;
|
||||||
std::unique_ptr<audio::ActivitySoundManager> activitySoundManager;
|
std::unique_ptr<audio::ActivitySoundManager> activitySoundManager;
|
||||||
std::unique_ptr<game::ZoneManager> zoneManager;
|
std::unique_ptr<game::ZoneManager> zoneManager;
|
||||||
|
std::unique_ptr<Shader> underwaterOverlayShader;
|
||||||
|
uint32_t underwaterOverlayVAO = 0;
|
||||||
|
uint32_t underwaterOverlayVBO = 0;
|
||||||
|
|
||||||
pipeline::AssetManager* cachedAssetManager = nullptr;
|
pipeline::AssetManager* cachedAssetManager = nullptr;
|
||||||
uint32_t currentZoneId = 0;
|
uint32_t currentZoneId = 0;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <cstdint>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
@ -22,9 +23,12 @@ class Shader;
|
||||||
*/
|
*/
|
||||||
struct WaterSurface {
|
struct WaterSurface {
|
||||||
glm::vec3 position; // World position
|
glm::vec3 position; // World position
|
||||||
|
glm::vec3 origin; // Mesh origin (world)
|
||||||
|
glm::vec3 stepX; // Mesh X step vector in world space
|
||||||
|
glm::vec3 stepY; // Mesh Y step vector in world space
|
||||||
float minHeight; // Minimum water height
|
float minHeight; // Minimum water height
|
||||||
float maxHeight; // Maximum water height
|
float maxHeight; // Maximum water height
|
||||||
uint8_t liquidType; // 0=water, 1=ocean, 2=magma, 3=slime
|
uint16_t liquidType; // LiquidType.dbc ID (WotLK)
|
||||||
|
|
||||||
// Owning tile coordinates (for per-tile removal)
|
// Owning tile coordinates (for per-tile removal)
|
||||||
int tileX = -1, tileY = -1;
|
int tileX = -1, tileY = -1;
|
||||||
|
|
@ -119,6 +123,7 @@ public:
|
||||||
* Returns the highest water surface height at that XY, or nullopt if no water.
|
* Returns the highest water surface height at that XY, or nullopt if no water.
|
||||||
*/
|
*/
|
||||||
std::optional<float> getWaterHeightAt(float glX, float glY) const;
|
std::optional<float> getWaterHeightAt(float glX, float glY) const;
|
||||||
|
std::optional<uint16_t> getWaterTypeAt(float glX, float glY) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get water surface count
|
* Get water surface count
|
||||||
|
|
@ -129,8 +134,8 @@ private:
|
||||||
void createWaterMesh(WaterSurface& surface);
|
void createWaterMesh(WaterSurface& surface);
|
||||||
void destroyWaterMesh(WaterSurface& surface);
|
void destroyWaterMesh(WaterSurface& surface);
|
||||||
|
|
||||||
glm::vec4 getLiquidColor(uint8_t liquidType) const;
|
glm::vec4 getLiquidColor(uint16_t liquidType) const;
|
||||||
float getLiquidAlpha(uint8_t liquidType) const;
|
float getLiquidAlpha(uint16_t liquidType) const;
|
||||||
|
|
||||||
std::unique_ptr<Shader> waterShader;
|
std::unique_ptr<Shader> waterShader;
|
||||||
std::vector<WaterSurface> surfaces;
|
std::vector<WaterSurface> surfaces;
|
||||||
|
|
|
||||||
|
|
@ -553,8 +553,8 @@ void Application::setState(AppState newState) {
|
||||||
gameHandler->sendMovement(static_cast<game::Opcode>(opcode));
|
gameHandler->sendMovement(static_cast<game::Opcode>(opcode));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Use WoW-correct speeds when connected to a server
|
// Keep player locomotion WoW-like in both single-player and online modes.
|
||||||
cc->setUseWoWSpeed(!singlePlayerMode);
|
cc->setUseWoWSpeed(true);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AppState::DISCONNECTED:
|
case AppState::DISCONNECTED:
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ constexpr uint32_t MOBA = 0x4D4F4241; // Batches
|
||||||
constexpr uint32_t MOCV = 0x4D4F4356; // Vertex colors
|
constexpr uint32_t MOCV = 0x4D4F4356; // Vertex colors
|
||||||
constexpr uint32_t MONR = 0x4D4F4E52; // Normals
|
constexpr uint32_t MONR = 0x4D4F4E52; // Normals
|
||||||
constexpr uint32_t MOTV = 0x4D4F5456; // Texture coords
|
constexpr uint32_t MOTV = 0x4D4F5456; // Texture coords
|
||||||
|
constexpr uint32_t MLIQ = 0x4D4C4951; // Liquid
|
||||||
|
|
||||||
// Read utilities
|
// Read utilities
|
||||||
template<typename T>
|
template<typename T>
|
||||||
|
|
@ -533,6 +534,60 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (subChunkId == MLIQ) { // MLIQ - WMO liquid data
|
||||||
|
// Basic WotLK layout:
|
||||||
|
// uint32 xVerts, yVerts, xTiles, yTiles
|
||||||
|
// float baseX, baseY, baseZ
|
||||||
|
// uint16 materialId
|
||||||
|
// (optional pad/unknown bytes)
|
||||||
|
// followed by vertex/tile payload
|
||||||
|
uint32_t parseOffset = mogpOffset;
|
||||||
|
if (parseOffset + 30 <= subChunkEnd) {
|
||||||
|
group.liquid.xVerts = read<uint32_t>(groupData, parseOffset);
|
||||||
|
group.liquid.yVerts = read<uint32_t>(groupData, parseOffset);
|
||||||
|
group.liquid.xTiles = read<uint32_t>(groupData, parseOffset);
|
||||||
|
group.liquid.yTiles = read<uint32_t>(groupData, parseOffset);
|
||||||
|
group.liquid.basePosition.x = read<float>(groupData, parseOffset);
|
||||||
|
group.liquid.basePosition.y = read<float>(groupData, parseOffset);
|
||||||
|
group.liquid.basePosition.z = read<float>(groupData, parseOffset);
|
||||||
|
group.liquid.materialId = read<uint16_t>(groupData, parseOffset);
|
||||||
|
if (parseOffset + sizeof(uint16_t) <= subChunkEnd) {
|
||||||
|
// Reserved/flags in some WMO liquid variants.
|
||||||
|
parseOffset += sizeof(uint16_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep parser resilient across minor format variants:
|
||||||
|
// prefer explicit per-vertex floats, otherwise fall back to flat.
|
||||||
|
const size_t vertexCount =
|
||||||
|
static_cast<size_t>(group.liquid.xVerts) * static_cast<size_t>(group.liquid.yVerts);
|
||||||
|
const size_t tileCount =
|
||||||
|
static_cast<size_t>(group.liquid.xTiles) * static_cast<size_t>(group.liquid.yTiles);
|
||||||
|
const size_t bytesRemaining = (subChunkEnd > parseOffset) ? (subChunkEnd - parseOffset) : 0;
|
||||||
|
|
||||||
|
group.liquid.heights.clear();
|
||||||
|
group.liquid.flags.clear();
|
||||||
|
|
||||||
|
if (vertexCount > 0 && bytesRemaining >= vertexCount * sizeof(float)) {
|
||||||
|
group.liquid.heights.resize(vertexCount);
|
||||||
|
for (size_t i = 0; i < vertexCount; i++) {
|
||||||
|
group.liquid.heights[i] = read<float>(groupData, parseOffset);
|
||||||
|
}
|
||||||
|
} else if (vertexCount > 0) {
|
||||||
|
group.liquid.heights.resize(vertexCount, group.liquid.basePosition.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tileCount > 0 && parseOffset + tileCount <= subChunkEnd) {
|
||||||
|
group.liquid.flags.resize(tileCount);
|
||||||
|
std::memcpy(group.liquid.flags.data(), &groupData[parseOffset], tileCount);
|
||||||
|
} else if (tileCount > 0) {
|
||||||
|
group.liquid.flags.resize(tileCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.liquid.materialId == 0) {
|
||||||
|
group.liquid.materialId = static_cast<uint16_t>(group.liquidType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mogpOffset = subChunkEnd;
|
mogpOffset = subChunkEnd;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,9 @@ void CameraController::update(float deltaTime) {
|
||||||
if (waterRenderer) {
|
if (waterRenderer) {
|
||||||
waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y);
|
waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y);
|
||||||
}
|
}
|
||||||
bool inWater = waterH && targetPos.z < *waterH;
|
constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f;
|
||||||
|
bool inWater = waterH && targetPos.z < *waterH &&
|
||||||
|
((*waterH - targetPos.z) <= MAX_SWIM_DEPTH_FROM_SURFACE);
|
||||||
|
|
||||||
|
|
||||||
if (inWater) {
|
if (inWater) {
|
||||||
|
|
@ -189,6 +191,7 @@ void CameraController::update(float deltaTime) {
|
||||||
// Swim movement follows look pitch (forward/back), while strafe stays
|
// Swim movement follows look pitch (forward/back), while strafe stays
|
||||||
// lateral for stable control.
|
// lateral for stable control.
|
||||||
float swimSpeed = speed * SWIM_SPEED_FACTOR;
|
float swimSpeed = speed * SWIM_SPEED_FACTOR;
|
||||||
|
float waterSurfaceZ = waterH ? (*waterH - WATER_SURFACE_OFFSET) : targetPos.z;
|
||||||
|
|
||||||
glm::vec3 swimForward = glm::normalize(forward3D);
|
glm::vec3 swimForward = glm::normalize(forward3D);
|
||||||
if (glm::length(swimForward) < 1e-4f) {
|
if (glm::length(swimForward) < 1e-4f) {
|
||||||
|
|
@ -214,6 +217,7 @@ void CameraController::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spacebar = swim up (continuous, not a jump)
|
// Spacebar = swim up (continuous, not a jump)
|
||||||
|
bool diveIntent = nowForward && (forward3D.z < -0.28f);
|
||||||
if (nowJump) {
|
if (nowJump) {
|
||||||
verticalVelocity = SWIM_BUOYANCY;
|
verticalVelocity = SWIM_BUOYANCY;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -222,6 +226,16 @@ void CameraController::update(float deltaTime) {
|
||||||
if (verticalVelocity < SWIM_SINK_SPEED) {
|
if (verticalVelocity < SWIM_SINK_SPEED) {
|
||||||
verticalVelocity = SWIM_SINK_SPEED;
|
verticalVelocity = SWIM_SINK_SPEED;
|
||||||
}
|
}
|
||||||
|
// Strong surface lock while idle/normal swim so buoyancy keeps
|
||||||
|
// you afloat unless you're intentionally diving.
|
||||||
|
if (!diveIntent) {
|
||||||
|
float surfaceErr = (waterSurfaceZ - targetPos.z);
|
||||||
|
verticalVelocity += surfaceErr * 7.0f * deltaTime;
|
||||||
|
verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * deltaTime);
|
||||||
|
if (std::abs(surfaceErr) < 0.06f && std::abs(verticalVelocity) < 0.35f) {
|
||||||
|
verticalVelocity = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
targetPos.z += verticalVelocity * deltaTime;
|
targetPos.z += verticalVelocity * deltaTime;
|
||||||
|
|
@ -636,12 +650,16 @@ void CameraController::update(float deltaTime) {
|
||||||
if (waterRenderer) {
|
if (waterRenderer) {
|
||||||
waterH = waterRenderer->getWaterHeightAt(newPos.x, newPos.y);
|
waterH = waterRenderer->getWaterHeightAt(newPos.x, newPos.y);
|
||||||
}
|
}
|
||||||
bool inWater = waterH && feetZ < *waterH;
|
constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f;
|
||||||
|
bool inWater = waterH && feetZ < *waterH &&
|
||||||
|
((*waterH - feetZ) <= MAX_SWIM_DEPTH_FROM_SURFACE);
|
||||||
|
|
||||||
|
|
||||||
if (inWater) {
|
if (inWater) {
|
||||||
swimming = true;
|
swimming = true;
|
||||||
float swimSpeed = speed * SWIM_SPEED_FACTOR;
|
float swimSpeed = speed * SWIM_SPEED_FACTOR;
|
||||||
|
float waterSurfaceCamZ = waterH ? (*waterH - WATER_SURFACE_OFFSET + eyeHeight) : newPos.z;
|
||||||
|
bool diveIntent = nowForward && (forward3D.z < -0.28f);
|
||||||
|
|
||||||
if (glm::length(movement) > 0.001f) {
|
if (glm::length(movement) > 0.001f) {
|
||||||
movement = glm::normalize(movement);
|
movement = glm::normalize(movement);
|
||||||
|
|
@ -655,6 +673,14 @@ void CameraController::update(float deltaTime) {
|
||||||
if (verticalVelocity < SWIM_SINK_SPEED) {
|
if (verticalVelocity < SWIM_SINK_SPEED) {
|
||||||
verticalVelocity = SWIM_SINK_SPEED;
|
verticalVelocity = SWIM_SINK_SPEED;
|
||||||
}
|
}
|
||||||
|
if (!diveIntent) {
|
||||||
|
float surfaceErr = (waterSurfaceCamZ - newPos.z);
|
||||||
|
verticalVelocity += surfaceErr * 7.0f * deltaTime;
|
||||||
|
verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * deltaTime);
|
||||||
|
if (std::abs(surfaceErr) < 0.06f && std::abs(verticalVelocity) < 0.35f) {
|
||||||
|
verticalVelocity = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newPos.z += verticalVelocity * deltaTime;
|
newPos.z += verticalVelocity * deltaTime;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include "rendering/wmo_renderer.hpp"
|
#include "rendering/wmo_renderer.hpp"
|
||||||
#include "rendering/m2_renderer.hpp"
|
#include "rendering/m2_renderer.hpp"
|
||||||
#include "rendering/minimap.hpp"
|
#include "rendering/minimap.hpp"
|
||||||
|
#include "rendering/shader.hpp"
|
||||||
#include "pipeline/asset_manager.hpp"
|
#include "pipeline/asset_manager.hpp"
|
||||||
#include "pipeline/m2_loader.hpp"
|
#include "pipeline/m2_loader.hpp"
|
||||||
#include "pipeline/wmo_loader.hpp"
|
#include "pipeline/wmo_loader.hpp"
|
||||||
|
|
@ -193,6 +194,37 @@ bool Renderer::initialize(core::Window* win) {
|
||||||
footstepManager = std::make_unique<audio::FootstepManager>();
|
footstepManager = std::make_unique<audio::FootstepManager>();
|
||||||
activitySoundManager = std::make_unique<audio::ActivitySoundManager>();
|
activitySoundManager = std::make_unique<audio::ActivitySoundManager>();
|
||||||
|
|
||||||
|
// Underwater full-screen tint overlay (applies to all world geometry).
|
||||||
|
underwaterOverlayShader = std::make_unique<Shader>();
|
||||||
|
const char* overlayVS = R"(
|
||||||
|
#version 330 core
|
||||||
|
layout (location = 0) in vec2 aPos;
|
||||||
|
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
const char* overlayFS = R"(
|
||||||
|
#version 330 core
|
||||||
|
uniform vec4 uTint;
|
||||||
|
out vec4 FragColor;
|
||||||
|
void main() { FragColor = uTint; }
|
||||||
|
)";
|
||||||
|
if (!underwaterOverlayShader->loadFromSource(overlayVS, overlayFS)) {
|
||||||
|
LOG_WARNING("Failed to initialize underwater overlay shader");
|
||||||
|
underwaterOverlayShader.reset();
|
||||||
|
} else {
|
||||||
|
const float quadVerts[] = {
|
||||||
|
-1.0f, -1.0f, 1.0f, -1.0f,
|
||||||
|
-1.0f, 1.0f, 1.0f, 1.0f
|
||||||
|
};
|
||||||
|
glGenVertexArrays(1, &underwaterOverlayVAO);
|
||||||
|
glGenBuffers(1, &underwaterOverlayVBO);
|
||||||
|
glBindVertexArray(underwaterOverlayVAO);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, underwaterOverlayVBO);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INFO("Renderer initialized");
|
LOG_INFO("Renderer initialized");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -272,6 +304,15 @@ void Renderer::shutdown() {
|
||||||
activitySoundManager->shutdown();
|
activitySoundManager->shutdown();
|
||||||
activitySoundManager.reset();
|
activitySoundManager.reset();
|
||||||
}
|
}
|
||||||
|
if (underwaterOverlayVAO) {
|
||||||
|
glDeleteVertexArrays(1, &underwaterOverlayVAO);
|
||||||
|
underwaterOverlayVAO = 0;
|
||||||
|
}
|
||||||
|
if (underwaterOverlayVBO) {
|
||||||
|
glDeleteBuffers(1, &underwaterOverlayVBO);
|
||||||
|
underwaterOverlayVBO = 0;
|
||||||
|
}
|
||||||
|
underwaterOverlayShader.reset();
|
||||||
|
|
||||||
zoneManager.reset();
|
zoneManager.reset();
|
||||||
|
|
||||||
|
|
@ -851,6 +892,8 @@ void Renderer::renderWorld(game::World* world) {
|
||||||
|
|
||||||
// Get time of day for sky-related rendering
|
// Get time of day for sky-related rendering
|
||||||
float timeOfDay = skybox ? skybox->getTimeOfDay() : 12.0f;
|
float timeOfDay = skybox ? skybox->getTimeOfDay() : 12.0f;
|
||||||
|
bool underwater = false;
|
||||||
|
bool canalUnderwater = false;
|
||||||
|
|
||||||
// Render skybox first (furthest back)
|
// Render skybox first (furthest back)
|
||||||
if (skybox && camera) {
|
if (skybox && camera) {
|
||||||
|
|
@ -880,20 +923,45 @@ void Renderer::renderWorld(game::World* world) {
|
||||||
|
|
||||||
// Render terrain if loaded and enabled
|
// Render terrain if loaded and enabled
|
||||||
if (terrainEnabled && terrainLoaded && terrainRenderer && camera) {
|
if (terrainEnabled && terrainLoaded && terrainRenderer && camera) {
|
||||||
// Check if camera is underwater for fog override
|
// Check if camera/character is underwater for fog override
|
||||||
bool underwater = false;
|
if (cameraController && cameraController->isSwimming() && waterRenderer && camera) {
|
||||||
if (waterRenderer && camera) {
|
|
||||||
glm::vec3 camPos = camera->getPosition();
|
glm::vec3 camPos = camera->getPosition();
|
||||||
auto waterH = waterRenderer->getWaterHeightAt(camPos.x, camPos.y);
|
auto waterH = waterRenderer->getWaterHeightAt(camPos.x, camPos.y);
|
||||||
if (waterH && camPos.z < *waterH) {
|
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;
|
||||||
|
if (waterH &&
|
||||||
|
camPos.z < (*waterH - UNDERWATER_ENTER_EPS) &&
|
||||||
|
(*waterH - camPos.z) <= MAX_UNDERWATER_DEPTH) {
|
||||||
underwater = true;
|
underwater = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (underwater) {
|
if (underwater) {
|
||||||
float fogColor[3] = {0.05f, 0.15f, 0.25f};
|
glm::vec3 camPos = camera->getPosition();
|
||||||
terrainRenderer->setFog(fogColor, 10.0f, 200.0f);
|
std::optional<uint16_t> liquidType = waterRenderer ? waterRenderer->getWaterTypeAt(camPos.x, camPos.y) : std::nullopt;
|
||||||
glClearColor(0.05f, 0.15f, 0.25f, 1.0f);
|
if (!liquidType && cameraController) {
|
||||||
|
const glm::vec3* followTarget = cameraController->getFollowTarget();
|
||||||
|
if (followTarget && waterRenderer) {
|
||||||
|
liquidType = waterRenderer->getWaterTypeAt(followTarget->x, followTarget->y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool canalWater = liquidType && (*liquidType == 5 || *liquidType == 13 || *liquidType == 17);
|
||||||
|
canalUnderwater = canalWater;
|
||||||
|
|
||||||
|
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
|
glClear(GL_COLOR_BUFFER_BIT); // Re-clear with underwater color
|
||||||
} else if (skybox) {
|
} 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)
|
||||||
|
|
@ -907,13 +975,6 @@ void Renderer::renderWorld(game::World* world) {
|
||||||
auto terrainEnd = std::chrono::steady_clock::now();
|
auto terrainEnd = std::chrono::steady_clock::now();
|
||||||
lastTerrainRenderMs = std::chrono::duration<double, std::milli>(terrainEnd - terrainStart).count();
|
lastTerrainRenderMs = std::chrono::duration<double, std::milli>(terrainEnd - terrainStart).count();
|
||||||
|
|
||||||
// Render water after terrain (transparency requires back-to-front rendering)
|
|
||||||
if (waterRenderer) {
|
|
||||||
// Use accumulated time for water animation
|
|
||||||
static float time = 0.0f;
|
|
||||||
time += 0.016f; // Approximate frame time
|
|
||||||
waterRenderer->render(*camera, time);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render weather particles (after terrain/water, before characters)
|
// Render weather particles (after terrain/water, before characters)
|
||||||
|
|
@ -953,6 +1014,31 @@ void Renderer::renderWorld(game::World* world) {
|
||||||
lastM2RenderMs = std::chrono::duration<double, std::milli>(m2End - m2Start).count();
|
lastM2RenderMs = std::chrono::duration<double, std::milli>(m2End - m2Start).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render water after opaque terrain/WMO/M2 so transparent surfaces remain visible.
|
||||||
|
if (waterRenderer && camera) {
|
||||||
|
static float time = 0.0f;
|
||||||
|
time += 0.016f; // Approximate frame time
|
||||||
|
waterRenderer->render(*camera, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full-screen underwater tint so WMO/M2/characters also feel submerged.
|
||||||
|
if (underwater && underwaterOverlayShader && underwaterOverlayVAO) {
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
underwaterOverlayShader->use();
|
||||||
|
if (canalUnderwater) {
|
||||||
|
underwaterOverlayShader->setUniform("uTint", glm::vec4(0.01f, 0.05f, 0.11f, 0.50f));
|
||||||
|
} else {
|
||||||
|
underwaterOverlayShader->setUniform("uTint", glm::vec4(0.02f, 0.08f, 0.15f, 0.30f));
|
||||||
|
}
|
||||||
|
glBindVertexArray(underwaterOverlayVAO);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
}
|
||||||
|
|
||||||
// Render minimap overlay
|
// Render minimap overlay
|
||||||
if (minimap && camera && window) {
|
if (minimap && camera && window) {
|
||||||
minimap->render(*camera, window->getWidth(), window->getHeight());
|
minimap->render(*camera, window->getWidth(), window->getHeight());
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <GL/glew.h>
|
#include <GL/glew.h>
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
@ -34,6 +36,9 @@ bool WaterRenderer::initialize() {
|
||||||
uniform mat4 view;
|
uniform mat4 view;
|
||||||
uniform mat4 projection;
|
uniform mat4 projection;
|
||||||
uniform float time;
|
uniform float time;
|
||||||
|
uniform float waveAmp;
|
||||||
|
uniform float waveFreq;
|
||||||
|
uniform float waveSpeed;
|
||||||
|
|
||||||
out vec3 FragPos;
|
out vec3 FragPos;
|
||||||
out vec3 Normal;
|
out vec3 Normal;
|
||||||
|
|
@ -41,14 +46,18 @@ bool WaterRenderer::initialize() {
|
||||||
out float WaveOffset;
|
out float WaveOffset;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Simple pass-through for debugging (no wave animation)
|
|
||||||
vec3 pos = aPos;
|
vec3 pos = aPos;
|
||||||
|
// Procedural ripple motion (tunable per water profile).
|
||||||
|
float w1 = sin((aPos.x + time * waveSpeed) * waveFreq) * waveAmp;
|
||||||
|
float w2 = cos((aPos.y - time * (waveSpeed * 0.78)) * (waveFreq * 0.82)) * (waveAmp * 0.72);
|
||||||
|
float wave = w1 + w2;
|
||||||
|
pos.z += wave;
|
||||||
|
|
||||||
FragPos = vec3(model * vec4(pos, 1.0));
|
FragPos = vec3(model * vec4(pos, 1.0));
|
||||||
// Use mat3(model) directly - avoids expensive inverse() per vertex
|
// Use mat3(model) directly - avoids expensive inverse() per vertex
|
||||||
Normal = mat3(model) * aNormal;
|
Normal = mat3(model) * aNormal;
|
||||||
TexCoord = aTexCoord;
|
TexCoord = aTexCoord;
|
||||||
WaveOffset = 0.0;
|
WaveOffset = wave;
|
||||||
|
|
||||||
gl_Position = projection * view * vec4(FragPos, 1.0);
|
gl_Position = projection * view * vec4(FragPos, 1.0);
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +75,8 @@ bool WaterRenderer::initialize() {
|
||||||
uniform vec4 waterColor;
|
uniform vec4 waterColor;
|
||||||
uniform float waterAlpha;
|
uniform float waterAlpha;
|
||||||
uniform float time;
|
uniform float time;
|
||||||
|
uniform float shimmerStrength;
|
||||||
|
uniform float alphaScale;
|
||||||
|
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
|
@ -80,7 +91,9 @@ bool WaterRenderer::initialize() {
|
||||||
// Specular highlights (shininess for water)
|
// Specular highlights (shininess for water)
|
||||||
vec3 viewDir = normalize(viewPos - FragPos);
|
vec3 viewDir = normalize(viewPos - FragPos);
|
||||||
vec3 reflectDir = reflect(-lightDir, norm);
|
vec3 reflectDir = reflect(-lightDir, norm);
|
||||||
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 64.0);
|
float specBase = pow(max(dot(viewDir, reflectDir), 0.0), mix(64.0, 180.0, shimmerStrength));
|
||||||
|
float sparkle = 0.65 + 0.35 * sin((TexCoord.x + TexCoord.y + time * 0.4) * 80.0);
|
||||||
|
float spec = specBase * mix(1.0, sparkle, shimmerStrength);
|
||||||
|
|
||||||
// Animated texture coordinates for flowing effect
|
// Animated texture coordinates for flowing effect
|
||||||
vec2 uv1 = TexCoord + vec2(time * 0.02, time * 0.01);
|
vec2 uv1 = TexCoord + vec2(time * 0.02, time * 0.01);
|
||||||
|
|
@ -96,8 +109,10 @@ bool WaterRenderer::initialize() {
|
||||||
|
|
||||||
vec3 result = (ambient + diffuse + specular) * brightness;
|
vec3 result = (ambient + diffuse + specular) * brightness;
|
||||||
|
|
||||||
// Apply transparency
|
// Slight fresnel: more reflective/opaque at grazing angles.
|
||||||
FragColor = vec4(result, waterAlpha);
|
float fresnel = pow(1.0 - max(dot(norm, viewDir), 0.0), 3.0);
|
||||||
|
float alpha = clamp(waterAlpha * alphaScale * (0.68 + fresnel * 0.45), 0.12, 0.82);
|
||||||
|
FragColor = vec4(result, alpha);
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
|
@ -117,6 +132,8 @@ void WaterRenderer::shutdown() {
|
||||||
|
|
||||||
void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append,
|
void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append,
|
||||||
int tileX, int tileY) {
|
int tileX, int tileY) {
|
||||||
|
constexpr float TILE_SIZE = 33.33333f / 8.0f;
|
||||||
|
|
||||||
if (!append) {
|
if (!append) {
|
||||||
LOG_INFO("Loading water from terrain (replacing)");
|
LOG_INFO("Loading water from terrain (replacing)");
|
||||||
clear();
|
clear();
|
||||||
|
|
@ -150,6 +167,13 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
|
||||||
terrainChunk.position[1],
|
terrainChunk.position[1],
|
||||||
layer.minHeight
|
layer.minHeight
|
||||||
);
|
);
|
||||||
|
surface.origin = glm::vec3(
|
||||||
|
surface.position.x - (static_cast<float>(layer.y) * TILE_SIZE),
|
||||||
|
surface.position.y - (static_cast<float>(layer.x) * TILE_SIZE),
|
||||||
|
layer.minHeight
|
||||||
|
);
|
||||||
|
surface.stepX = glm::vec3(0.0f, -TILE_SIZE, 0.0f);
|
||||||
|
surface.stepY = glm::vec3(-TILE_SIZE, 0.0f, 0.0f);
|
||||||
|
|
||||||
// Debug log first few water surfaces
|
// Debug log first few water surfaces
|
||||||
if (totalLayers < 5) {
|
if (totalLayers < 5) {
|
||||||
|
|
@ -170,17 +194,48 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
|
||||||
surface.width = layer.width;
|
surface.width = layer.width;
|
||||||
surface.height = layer.height;
|
surface.height = layer.height;
|
||||||
|
|
||||||
// Copy height data
|
// Prefer per-vertex terrain water heights when sane; fall back to flat
|
||||||
if (!layer.heights.empty()) {
|
// minHeight if data looks malformed (prevents sky-stretch artifacts).
|
||||||
surface.heights = layer.heights;
|
size_t numVertices = (layer.width + 1) * (layer.height + 1);
|
||||||
} else {
|
bool useFlat = true;
|
||||||
// Flat water at minHeight if no height data
|
if (layer.heights.size() == numVertices) {
|
||||||
size_t numVertices = (layer.width + 1) * (layer.height + 1);
|
bool sane = true;
|
||||||
|
for (float h : layer.heights) {
|
||||||
|
if (!std::isfinite(h) || std::abs(h) > 50000.0f) {
|
||||||
|
sane = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Conservative acceptance window around MH2O min/max metadata.
|
||||||
|
if (h < layer.minHeight - 8.0f || h > layer.maxHeight + 8.0f) {
|
||||||
|
sane = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sane) {
|
||||||
|
useFlat = false;
|
||||||
|
surface.heights = layer.heights;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (useFlat) {
|
||||||
surface.heights.resize(numVertices, layer.minHeight);
|
surface.heights.resize(numVertices, layer.minHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
@ -213,11 +268,74 @@ void WaterRenderer::removeTile(int tileX, int tileY) {
|
||||||
void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liquid,
|
void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liquid,
|
||||||
[[maybe_unused]] const glm::mat4& modelMatrix,
|
[[maybe_unused]] const glm::mat4& modelMatrix,
|
||||||
[[maybe_unused]] uint32_t wmoId) {
|
[[maybe_unused]] uint32_t wmoId) {
|
||||||
// WMO liquid rendering not yet implemented
|
if (!liquid.hasLiquid() || liquid.xTiles == 0 || liquid.yTiles == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (liquid.xVerts < 2 || liquid.yVerts < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (liquid.xTiles != liquid.xVerts - 1 || liquid.yTiles != liquid.yVerts - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (liquid.xTiles > 64 || liquid.yTiles > 64) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WaterSurface surface;
|
||||||
|
surface.tileX = -1;
|
||||||
|
surface.tileY = -1;
|
||||||
|
surface.wmoId = wmoId;
|
||||||
|
surface.liquidType = liquid.materialId;
|
||||||
|
surface.xOffset = 0;
|
||||||
|
surface.yOffset = 0;
|
||||||
|
surface.width = static_cast<uint8_t>(std::min<uint32_t>(255, liquid.xTiles));
|
||||||
|
surface.height = static_cast<uint8_t>(std::min<uint32_t>(255, liquid.yTiles));
|
||||||
|
|
||||||
|
constexpr float WMO_LIQUID_TILE_SIZE = 4.1666625f;
|
||||||
|
const glm::vec3 localBase(liquid.basePosition.x, liquid.basePosition.y, liquid.basePosition.z);
|
||||||
|
const glm::vec3 localStepX(WMO_LIQUID_TILE_SIZE, 0.0f, 0.0f);
|
||||||
|
const glm::vec3 localStepY(0.0f, WMO_LIQUID_TILE_SIZE, 0.0f);
|
||||||
|
|
||||||
|
surface.origin = glm::vec3(modelMatrix * glm::vec4(localBase, 1.0f));
|
||||||
|
surface.stepX = glm::vec3(modelMatrix * glm::vec4(localStepX, 0.0f));
|
||||||
|
surface.stepY = glm::vec3(modelMatrix * glm::vec4(localStepY, 0.0f));
|
||||||
|
surface.position = surface.origin;
|
||||||
|
|
||||||
|
const int gridWidth = static_cast<int>(surface.width) + 1;
|
||||||
|
const int gridHeight = static_cast<int>(surface.height) + 1;
|
||||||
|
const int vertexCount = gridWidth * gridHeight;
|
||||||
|
// Keep WMO liquid flat for stability; some files use variant payload layouts
|
||||||
|
// that can produce invalid per-vertex heights if interpreted generically.
|
||||||
|
surface.heights.assign(vertexCount, surface.origin.z);
|
||||||
|
surface.minHeight = surface.origin.z;
|
||||||
|
surface.maxHeight = surface.origin.z;
|
||||||
|
|
||||||
|
size_t tileCount = static_cast<size_t>(surface.width) * static_cast<size_t>(surface.height);
|
||||||
|
size_t maskBytes = (tileCount + 7) / 8;
|
||||||
|
// WMO liquid flags vary across files; for now treat all WMO liquid tiles as
|
||||||
|
// visible for rendering. Swim/gameplay queries already ignore WMO surfaces.
|
||||||
|
surface.mask.assign(maskBytes, 0xFF);
|
||||||
|
|
||||||
|
createWaterMesh(surface);
|
||||||
|
if (surface.indexCount > 0) {
|
||||||
|
surfaces.push_back(surface);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WaterRenderer::removeWMO([[maybe_unused]] uint32_t wmoId) {
|
void WaterRenderer::removeWMO(uint32_t wmoId) {
|
||||||
// WMO liquid rendering not yet implemented
|
if (wmoId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = surfaces.begin();
|
||||||
|
while (it != surfaces.end()) {
|
||||||
|
if (it->wmoId == wmoId) {
|
||||||
|
destroyWaterMesh(*it);
|
||||||
|
it = surfaces.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WaterRenderer::clear() {
|
void WaterRenderer::clear() {
|
||||||
|
|
@ -232,6 +350,11 @@ void WaterRenderer::render(const Camera& camera, float time) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GLboolean cullEnabled = glIsEnabled(GL_CULL_FACE);
|
||||||
|
if (cullEnabled) {
|
||||||
|
glDisable(GL_CULL_FACE);
|
||||||
|
}
|
||||||
|
|
||||||
// Enable alpha blending for transparent water
|
// Enable alpha blending for transparent water
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
@ -264,8 +387,22 @@ void WaterRenderer::render(const Camera& camera, float time) {
|
||||||
glm::vec4 color = getLiquidColor(surface.liquidType);
|
glm::vec4 color = getLiquidColor(surface.liquidType);
|
||||||
float alpha = getLiquidAlpha(surface.liquidType);
|
float alpha = getLiquidAlpha(surface.liquidType);
|
||||||
|
|
||||||
|
// City/canal liquid profile: clearer water + stronger ripples/sun shimmer.
|
||||||
|
// Stormwind canals typically use LiquidType 5 in this data set.
|
||||||
|
bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5);
|
||||||
|
float waveAmp = canalProfile ? 0.07f : 0.038f;
|
||||||
|
float waveFreq = canalProfile ? 0.30f : 0.22f;
|
||||||
|
float waveSpeed = canalProfile ? 1.20f : 0.90f;
|
||||||
|
float shimmerStrength = canalProfile ? 0.95f : 0.35f;
|
||||||
|
float alphaScale = canalProfile ? 0.72f : 1.00f;
|
||||||
|
|
||||||
waterShader->setUniform("waterColor", color);
|
waterShader->setUniform("waterColor", color);
|
||||||
waterShader->setUniform("waterAlpha", alpha);
|
waterShader->setUniform("waterAlpha", alpha);
|
||||||
|
waterShader->setUniform("waveAmp", waveAmp);
|
||||||
|
waterShader->setUniform("waveFreq", waveFreq);
|
||||||
|
waterShader->setUniform("waveSpeed", waveSpeed);
|
||||||
|
waterShader->setUniform("shimmerStrength", shimmerStrength);
|
||||||
|
waterShader->setUniform("alphaScale", alphaScale);
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
glBindVertexArray(surface.vao);
|
glBindVertexArray(surface.vao);
|
||||||
|
|
@ -276,19 +413,21 @@ void WaterRenderer::render(const Camera& camera, float time) {
|
||||||
// Restore state
|
// Restore state
|
||||||
glDepthMask(GL_TRUE);
|
glDepthMask(GL_TRUE);
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
|
if (cullEnabled) {
|
||||||
|
glEnable(GL_CULL_FACE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WaterRenderer::createWaterMesh(WaterSurface& surface) {
|
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;
|
||||||
const float TILE_SIZE = 33.33333f / 8.0f; // Size of one tile (same as terrain unitSize)
|
constexpr float VISUAL_WATER_Z_BIAS = 0.06f; // Prevent z-fighting against city/WMO geometry
|
||||||
|
|
||||||
std::vector<float> vertices;
|
std::vector<float> vertices;
|
||||||
std::vector<uint32_t> indices;
|
std::vector<uint32_t> indices;
|
||||||
|
|
||||||
// Generate vertices
|
// Generate vertices
|
||||||
// Match terrain coordinate transformation: pos[0] = baseX - (y * unitSize), pos[1] = baseY - (x * unitSize)
|
|
||||||
for (int y = 0; y < gridHeight; y++) {
|
for (int y = 0; y < gridHeight; y++) {
|
||||||
for (int x = 0; x < gridWidth; x++) {
|
for (int x = 0; x < gridWidth; x++) {
|
||||||
int index = y * gridWidth + x;
|
int index = y * gridWidth + x;
|
||||||
|
|
@ -301,23 +440,21 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) {
|
||||||
height = surface.minHeight;
|
height = surface.minHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position - match terrain coordinate transformation (swap and negate)
|
glm::vec3 pos = surface.origin +
|
||||||
// Terrain uses: X = baseX - (offsetY * unitSize), Y = baseY - (offsetX * unitSize)
|
surface.stepX * static_cast<float>(x) +
|
||||||
// Also apply layer offset within chunk (xOffset, yOffset)
|
surface.stepY * static_cast<float>(y);
|
||||||
float posX = surface.position.x - ((surface.yOffset + y) * TILE_SIZE);
|
pos.z = height + VISUAL_WATER_Z_BIAS;
|
||||||
float posY = surface.position.y - ((surface.xOffset + x) * TILE_SIZE);
|
|
||||||
float posZ = height;
|
|
||||||
|
|
||||||
// Debug first surface's corner vertices
|
// Debug first surface's corner vertices
|
||||||
static int debugCount = 0;
|
static int debugCount = 0;
|
||||||
if (debugCount < 4 && (x == 0 || x == gridWidth-1) && (y == 0 || y == gridHeight-1)) {
|
if (debugCount < 4 && (x == 0 || x == gridWidth-1) && (y == 0 || y == gridHeight-1)) {
|
||||||
LOG_DEBUG("Water vertex: (", posX, ", ", posY, ", ", posZ, ")");
|
LOG_DEBUG("Water vertex: (", pos.x, ", ", pos.y, ", ", pos.z, ")");
|
||||||
debugCount++;
|
debugCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
vertices.push_back(posX);
|
vertices.push_back(pos.x);
|
||||||
vertices.push_back(posY);
|
vertices.push_back(pos.y);
|
||||||
vertices.push_back(posZ);
|
vertices.push_back(pos.z);
|
||||||
|
|
||||||
// Normal (pointing up for water surface)
|
// Normal (pointing up for water surface)
|
||||||
vertices.push_back(0.0f);
|
vertices.push_back(0.0f);
|
||||||
|
|
@ -419,13 +556,20 @@ void WaterRenderer::destroyWaterMesh(WaterSurface& surface) {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const {
|
std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const {
|
||||||
const float TILE_SIZE = 33.33333f / 8.0f;
|
|
||||||
std::optional<float> best;
|
std::optional<float> best;
|
||||||
|
|
||||||
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];
|
||||||
float gy = (surface.position.x - glX) / TILE_SIZE - static_cast<float>(surface.yOffset);
|
glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y);
|
||||||
float gx = (surface.position.y - glY) / TILE_SIZE - static_cast<float>(surface.xOffset);
|
glm::vec2 stepX(surface.stepX.x, surface.stepX.y);
|
||||||
|
glm::vec2 stepY(surface.stepY.x, surface.stepY.y);
|
||||||
|
float lenSqX = glm::dot(stepX, stepX);
|
||||||
|
float lenSqY = glm::dot(stepY, stepY);
|
||||||
|
if (lenSqX < 1e-6f || lenSqY < 1e-6f) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float gx = glm::dot(rel, stepX) / lenSqX;
|
||||||
|
float gy = glm::dot(rel, stepY) / lenSqY;
|
||||||
|
|
||||||
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
|
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
|
||||||
gy < 0.0f || gy > static_cast<float>(surface.height)) {
|
gy < 0.0f || gy > static_cast<float>(surface.height)) {
|
||||||
|
|
@ -443,6 +587,22 @@ std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const
|
||||||
// Clamp to valid vertex range
|
// Clamp to valid vertex range
|
||||||
if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; }
|
if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; }
|
||||||
if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; }
|
if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; }
|
||||||
|
if (ix < 0 || iy < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 byteIndex = tileIndex / 8;
|
||||||
|
int bitIndex = tileIndex % 8;
|
||||||
|
if (byteIndex < static_cast<int>(surface.mask.size())) {
|
||||||
|
bool renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0;
|
||||||
|
if (!renderTile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int idx00 = iy * gridWidth + ix;
|
int idx00 = iy * gridWidth + ix;
|
||||||
int idx10 = idx00 + 1;
|
int idx10 = idx00 + 1;
|
||||||
|
|
@ -468,7 +628,55 @@ std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
glm::vec4 WaterRenderer::getLiquidColor(uint8_t liquidType) const {
|
std::optional<uint16_t> WaterRenderer::getWaterTypeAt(float glX, float glY) const {
|
||||||
|
std::optional<float> bestHeight;
|
||||||
|
std::optional<uint16_t> bestType;
|
||||||
|
|
||||||
|
for (const auto& surface : surfaces) {
|
||||||
|
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);
|
||||||
|
float lenSqX = glm::dot(stepX, stepX);
|
||||||
|
float lenSqY = glm::dot(stepY, stepY);
|
||||||
|
if (lenSqX < 1e-6f || lenSqY < 1e-6f) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float gx = glm::dot(rel, stepX) / lenSqX;
|
||||||
|
float gy = glm::dot(rel, stepY) / lenSqY;
|
||||||
|
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
|
||||||
|
gy < 0.0f || gy > static_cast<float>(surface.height)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ix = static_cast<int>(gx);
|
||||||
|
int iy = static_cast<int>(gy);
|
||||||
|
if (ix >= surface.width) ix = surface.width - 1;
|
||||||
|
if (iy >= surface.height) iy = surface.height - 1;
|
||||||
|
if (ix < 0 || iy < 0) continue;
|
||||||
|
|
||||||
|
if (!surface.mask.empty()) {
|
||||||
|
int tileIndex = iy * surface.width + ix;
|
||||||
|
int byteIndex = tileIndex / 8;
|
||||||
|
int bitIndex = tileIndex % 8;
|
||||||
|
if (byteIndex < static_cast<int>(surface.mask.size())) {
|
||||||
|
bool renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0;
|
||||||
|
if (!renderTile) continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use minHeight as stable selector for "topmost surface at XY".
|
||||||
|
float h = surface.minHeight;
|
||||||
|
if (!bestHeight || h > *bestHeight) {
|
||||||
|
bestHeight = h;
|
||||||
|
bestType = surface.liquidType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestType;
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const {
|
||||||
// WoW 3.3.5a LiquidType.dbc IDs:
|
// WoW 3.3.5a LiquidType.dbc IDs:
|
||||||
// 1,5,9,13,17 = Water variants (still, slow, fast)
|
// 1,5,9,13,17 = Water variants (still, slow, fast)
|
||||||
// 2,6,10,14 = Ocean
|
// 2,6,10,14 = Ocean
|
||||||
|
|
@ -496,12 +704,12 @@ glm::vec4 WaterRenderer::getLiquidColor(uint8_t liquidType) const {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float WaterRenderer::getLiquidAlpha(uint8_t liquidType) const {
|
float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const {
|
||||||
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
|
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
|
||||||
switch (basicType) {
|
switch (basicType) {
|
||||||
case 2: return 0.85f; // Magma - mostly opaque
|
case 2: return 0.72f; // Magma
|
||||||
case 3: return 0.75f; // Slime - semi-opaque
|
case 3: return 0.62f; // Slime
|
||||||
default: return 0.55f; // Water/Ocean - semi-transparent
|
default: return 0.38f; // Water/Ocean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue