chore(refactor): god-object decomposition and mega-file splits

Split all mega-files by single-responsibility concern and
partially extracting AudioCoordinator and
OverlaySystem from the Renderer facade. No behavioral changes.

Splits:
- game_handler.cpp (5,247 LOC) → core + callbacks + packets (3 files)
- world_packets.cpp (4,453 LOC) → economy/entity/social/world (4 files)
- game_screen.cpp  (5,786 LOC) → core + frames + hud + minimap (4 files)
- m2_renderer.cpp  (3,343 LOC) → core + instance + particles + render (4 files)
- chat_panel.cpp   (3,140 LOC) → core + commands + utils (3 files)
- entity_spawner.cpp (2,750 LOC) → core + player + processing (3 files)

Extractions:
- AudioCoordinator: include/audio/ + src/audio/ (owned by Renderer)
- OverlaySystem: include/rendering/ + src/rendering/overlay_system.*

CMakeLists.txt: registered all 17 new translation units.
Related handler/callback files: minor include fixups post-split.
This commit is contained in:
Paul 2026-04-05 19:30:44 +03:00
parent 6dcc06697b
commit 34c0e3ca28
49 changed files with 29113 additions and 28109 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,364 @@
// m2_renderer_internal.h — shared helpers for the m2_renderer split files.
// All functions are inline to allow inclusion in multiple translation units.
#pragma once
#include "rendering/m2_renderer.hpp"
#include "pipeline/m2_loader.hpp"
#include "core/profiler.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/quaternion.hpp>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <random>
#include <unordered_set>
#include <vector>
namespace wowee {
namespace rendering {
namespace m2_internal {
// ---- RNG helpers ----
inline std::mt19937& rng() {
static std::mt19937 gen(std::random_device{}());
return gen;
}
inline uint32_t randRange(uint32_t maxExclusive) {
if (maxExclusive == 0) return 0;
return std::uniform_int_distribution<uint32_t>(0, maxExclusive - 1)(rng());
}
inline float randFloat(float lo, float hi) {
return std::uniform_real_distribution<float>(lo, hi)(rng());
}
// ---- Constants ----
inline const auto kLavaAnimStart = std::chrono::steady_clock::now();
inline constexpr uint32_t kParticleFlagRandomized = 0x40;
inline constexpr uint32_t kParticleFlagTiled = 0x80;
inline constexpr float kSmokeEmitInterval = 1.0f / 48.0f;
// ---- Geometry / collision helpers ----
inline float computeGroundDetailDownOffset(const M2ModelGPU& model, float scale) {
const float pivotComp = glm::clamp(std::max(0.0f, model.boundMin.z * scale), 0.0f, 0.10f);
const float terrainSink = 0.03f;
return pivotComp + terrainSink;
}
inline void getTightCollisionBounds(const M2ModelGPU& model, glm::vec3& outMin, glm::vec3& outMax) {
glm::vec3 center = (model.boundMin + model.boundMax) * 0.5f;
glm::vec3 half = (model.boundMax - model.boundMin) * 0.5f;
if (model.collisionTreeTrunk) {
float modelHoriz = std::max(model.boundMax.x - model.boundMin.x,
model.boundMax.y - model.boundMin.y);
float trunkHalf = std::clamp(modelHoriz * 0.05f, 0.5f, 5.0f);
half.x = trunkHalf;
half.y = trunkHalf;
half.z = std::min(trunkHalf * 2.5f, 3.5f);
center.z = model.boundMin.z + half.z;
} else if (model.collisionNarrowVerticalProp) {
half.x *= 0.30f;
half.y *= 0.30f;
half.z *= 0.96f;
} else if (model.collisionSmallSolidProp) {
half.x *= 1.00f;
half.y *= 1.00f;
half.z *= 1.00f;
} else if (model.collisionSteppedLowPlatform) {
half.x *= 0.98f;
half.y *= 0.98f;
half.z *= 0.52f;
} else {
half.x *= 0.66f;
half.y *= 0.66f;
half.z *= 0.76f;
}
outMin = center - half;
outMax = center + half;
}
inline float getEffectiveCollisionTopLocal(const M2ModelGPU& model,
const glm::vec3& localPos,
const glm::vec3& localMin,
const glm::vec3& localMax) {
if (!model.collisionSteppedFountain && !model.collisionSteppedLowPlatform) {
return localMax.z;
}
glm::vec2 center((localMin.x + localMax.x) * 0.5f, (localMin.y + localMax.y) * 0.5f);
glm::vec2 half((localMax.x - localMin.x) * 0.5f, (localMax.y - localMin.y) * 0.5f);
if (half.x < 1e-4f || half.y < 1e-4f) {
return localMax.z;
}
float nx = (localPos.x - center.x) / half.x;
float ny = (localPos.y - center.y) / half.y;
float r = std::sqrt(nx * nx + ny * ny);
float h = localMax.z - localMin.z;
if (model.collisionSteppedFountain) {
if (r > 0.85f) return localMin.z + h * 0.18f;
if (r > 0.65f) return localMin.z + h * 0.36f;
if (r > 0.45f) return localMin.z + h * 0.54f;
if (r > 0.28f) return localMin.z + h * 0.70f;
if (r > 0.14f) return localMin.z + h * 0.84f;
return localMin.z + h * 0.96f;
}
float edge = std::max(std::abs(nx), std::abs(ny));
if (edge > 0.92f) return localMin.z + h * 0.06f;
if (edge > 0.72f) return localMin.z + h * 0.30f;
return localMin.z + h * 0.62f;
}
inline bool segmentIntersectsAABB(const glm::vec3& from, const glm::vec3& to,
const glm::vec3& bmin, const glm::vec3& bmax,
float& outEnterT) {
glm::vec3 d = to - from;
float tEnter = 0.0f;
float tExit = 1.0f;
for (int axis = 0; axis < 3; axis++) {
if (std::abs(d[axis]) < 1e-6f) {
if (from[axis] < bmin[axis] || from[axis] > bmax[axis]) {
return false;
}
continue;
}
float inv = 1.0f / d[axis];
float t0 = (bmin[axis] - from[axis]) * inv;
float t1 = (bmax[axis] - from[axis]) * inv;
if (t0 > t1) std::swap(t0, t1);
tEnter = std::max(tEnter, t0);
tExit = std::min(tExit, t1);
if (tEnter > tExit) return false;
}
outEnterT = tEnter;
return tExit >= 0.0f && tEnter <= 1.0f;
}
inline void transformAABB(const glm::mat4& modelMatrix,
const glm::vec3& localMin,
const glm::vec3& localMax,
glm::vec3& outMin,
glm::vec3& outMax) {
const glm::vec3 corners[8] = {
{localMin.x, localMin.y, localMin.z},
{localMin.x, localMin.y, localMax.z},
{localMin.x, localMax.y, localMin.z},
{localMin.x, localMax.y, localMax.z},
{localMax.x, localMin.y, localMin.z},
{localMax.x, localMin.y, localMax.z},
{localMax.x, localMax.y, localMin.z},
{localMax.x, localMax.y, localMax.z}
};
outMin = glm::vec3(std::numeric_limits<float>::max());
outMax = glm::vec3(-std::numeric_limits<float>::max());
for (const auto& c : corners) {
glm::vec3 wc = glm::vec3(modelMatrix * glm::vec4(c, 1.0f));
outMin = glm::min(outMin, wc);
outMax = glm::max(outMax, wc);
}
}
inline float pointAABBDistanceSq(const glm::vec3& p, const glm::vec3& bmin, const glm::vec3& bmax) {
glm::vec3 q = glm::clamp(p, bmin, bmax);
glm::vec3 d = p - q;
return glm::dot(d, d);
}
// MöllerTrumbore ray-triangle intersection.
// Returns distance along ray if hit, negative if miss.
inline float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) {
constexpr float EPSILON = 1e-6f;
glm::vec3 e1 = v1 - v0;
glm::vec3 e2 = v2 - v0;
glm::vec3 h = glm::cross(dir, e2);
float a = glm::dot(e1, h);
if (a > -EPSILON && a < EPSILON) return -1.0f;
float f = 1.0f / a;
glm::vec3 s = origin - v0;
float u = f * glm::dot(s, h);
if (u < 0.0f || u > 1.0f) return -1.0f;
glm::vec3 q = glm::cross(s, e1);
float v = f * glm::dot(dir, q);
if (v < 0.0f || u + v > 1.0f) return -1.0f;
float t = f * glm::dot(e2, q);
return t > EPSILON ? t : -1.0f;
}
// Closest point on triangle to a point (Ericson, Real-Time Collision Detection §5.1.5).
inline glm::vec3 closestPointOnTriangle(const glm::vec3& p,
const glm::vec3& a, const glm::vec3& b, const glm::vec3& c) {
glm::vec3 ab = b - a, ac = c - a, ap = p - a;
float d1 = glm::dot(ab, ap), d2 = glm::dot(ac, ap);
if (d1 <= 0.0f && d2 <= 0.0f) return a;
glm::vec3 bp = p - b;
float d3 = glm::dot(ab, bp), d4 = glm::dot(ac, bp);
if (d3 >= 0.0f && d4 <= d3) return b;
float vc = d1 * d4 - d3 * d2;
if (vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f) {
float v = d1 / (d1 - d3);
return a + v * ab;
}
glm::vec3 cp = p - c;
float d5 = glm::dot(ab, cp), d6 = glm::dot(ac, cp);
if (d6 >= 0.0f && d5 <= d6) return c;
float vb = d5 * d2 - d1 * d6;
if (vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f) {
float w = d2 / (d2 - d6);
return a + w * ac;
}
float va = d3 * d6 - d5 * d4;
if (va <= 0.0f && (d4 - d3) >= 0.0f && (d5 - d6) >= 0.0f) {
float w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
return b + w * (c - b);
}
float denom = 1.0f / (va + vb + vc);
float v = vb * denom;
float w = vc * denom;
return a + ab * v + ac * w;
}
// ---- Thread-local scratch buffers for collision queries ----
inline thread_local std::vector<size_t> tl_m2_candidateScratch;
inline thread_local std::unordered_set<uint32_t> tl_m2_candidateIdScratch;
inline thread_local std::vector<uint32_t> tl_m2_collisionTriScratch;
// ---- Bone animation helpers ----
inline int findKeyframeIndex(const std::vector<uint32_t>& timestamps, float time) {
if (timestamps.empty()) return -1;
if (timestamps.size() == 1) return 0;
auto it = std::upper_bound(timestamps.begin(), timestamps.end(), time,
[](float t, uint32_t ts) { return t < static_cast<float>(ts); });
if (it == timestamps.begin()) return 0;
size_t idx = static_cast<size_t>(it - timestamps.begin()) - 1;
return static_cast<int>(std::min(idx, timestamps.size() - 2));
}
inline void resolveTrackTime(const pipeline::M2AnimationTrack& track,
int seqIdx, float time,
const std::vector<uint32_t>& globalSeqDurations,
int& outSeqIdx, float& outTime) {
if (track.globalSequence >= 0 &&
static_cast<size_t>(track.globalSequence) < globalSeqDurations.size()) {
outSeqIdx = 0;
float dur = static_cast<float>(globalSeqDurations[track.globalSequence]);
if (dur > 0.0f) {
outTime = time;
while (outTime >= dur) {
outTime -= dur;
}
} else {
outTime = 0.0f;
}
} else {
outSeqIdx = seqIdx;
outTime = time;
}
}
inline glm::vec3 interpVec3(const pipeline::M2AnimationTrack& track,
int seqIdx, float time, const glm::vec3& def,
const std::vector<uint32_t>& globalSeqDurations) {
if (!track.hasData()) return def;
int si; float t;
resolveTrackTime(track, seqIdx, time, globalSeqDurations, si, t);
if (si < 0 || si >= static_cast<int>(track.sequences.size())) return def;
const auto& keys = track.sequences[si];
if (keys.timestamps.empty() || keys.vec3Values.empty()) return def;
auto safe = [&](const glm::vec3& v) -> glm::vec3 {
if (std::isnan(v.x) || std::isnan(v.y) || std::isnan(v.z)) return def;
return v;
};
if (keys.vec3Values.size() == 1) return safe(keys.vec3Values[0]);
int idx = findKeyframeIndex(keys.timestamps, t);
if (idx < 0) return def;
size_t i0 = static_cast<size_t>(idx);
size_t i1 = std::min(i0 + 1, keys.vec3Values.size() - 1);
if (i0 == i1) return safe(keys.vec3Values[i0]);
float t0 = static_cast<float>(keys.timestamps[i0]);
float t1 = static_cast<float>(keys.timestamps[i1]);
float dur = t1 - t0;
float frac = (dur > 0.0f) ? glm::clamp((t - t0) / dur, 0.0f, 1.0f) : 0.0f;
return safe(glm::mix(keys.vec3Values[i0], keys.vec3Values[i1], frac));
}
inline glm::quat interpQuat(const pipeline::M2AnimationTrack& track,
int seqIdx, float time,
const std::vector<uint32_t>& globalSeqDurations) {
glm::quat identity(1.0f, 0.0f, 0.0f, 0.0f);
if (!track.hasData()) return identity;
int si; float t;
resolveTrackTime(track, seqIdx, time, globalSeqDurations, si, t);
if (si < 0 || si >= static_cast<int>(track.sequences.size())) return identity;
const auto& keys = track.sequences[si];
if (keys.timestamps.empty() || keys.quatValues.empty()) return identity;
auto safe = [&](const glm::quat& q) -> glm::quat {
float lenSq = q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w;
if (lenSq < 0.000001f || std::isnan(lenSq)) return identity;
return q;
};
if (keys.quatValues.size() == 1) return safe(keys.quatValues[0]);
int idx = findKeyframeIndex(keys.timestamps, t);
if (idx < 0) return identity;
size_t i0 = static_cast<size_t>(idx);
size_t i1 = std::min(i0 + 1, keys.quatValues.size() - 1);
if (i0 == i1) return safe(keys.quatValues[i0]);
float t0 = static_cast<float>(keys.timestamps[i0]);
float t1 = static_cast<float>(keys.timestamps[i1]);
float dur = t1 - t0;
float frac = (dur > 0.0f) ? glm::clamp((t - t0) / dur, 0.0f, 1.0f) : 0.0f;
return glm::slerp(safe(keys.quatValues[i0]), safe(keys.quatValues[i1]), frac);
}
inline void computeBoneMatrices(const M2ModelGPU& model, M2Instance& instance) {
ZoneScopedN("M2::computeBoneMatrices");
size_t numBones = std::min(model.bones.size(), size_t(128));
if (numBones == 0) return;
instance.boneMatrices.resize(numBones);
const auto& gsd = model.globalSequenceDurations;
for (size_t i = 0; i < numBones; i++) {
const auto& bone = model.bones[i];
glm::vec3 trans = interpVec3(bone.translation, instance.currentSequenceIndex, instance.animTime, glm::vec3(0.0f), gsd);
glm::quat rot = interpQuat(bone.rotation, instance.currentSequenceIndex, instance.animTime, gsd);
glm::vec3 scl = interpVec3(bone.scale, instance.currentSequenceIndex, instance.animTime, glm::vec3(1.0f), gsd);
if (scl.x < 0.001f) scl.x = 1.0f;
if (scl.y < 0.001f) scl.y = 1.0f;
if (scl.z < 0.001f) scl.z = 1.0f;
glm::mat4 local = glm::translate(glm::mat4(1.0f), bone.pivot);
local = glm::translate(local, trans);
local *= glm::toMat4(rot);
local = glm::scale(local, scl);
local = glm::translate(local, -bone.pivot);
if (bone.parentBone >= 0 && static_cast<size_t>(bone.parentBone) < numBones) {
instance.boneMatrices[i] = instance.boneMatrices[bone.parentBone] * local;
} else {
instance.boneMatrices[i] = local;
}
}
instance.bonesDirty[0] = instance.bonesDirty[1] = true;
}
} // namespace m2_internal
// Pull all symbols into the rendering namespace so existing code compiles unchanged
using namespace m2_internal;
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,618 @@
#include "rendering/m2_renderer.hpp"
#include "rendering/m2_renderer_internal.h"
#include "rendering/vk_context.hpp"
#include "rendering/vk_buffer.hpp"
#include "rendering/vk_texture.hpp"
#include "rendering/vk_pipeline.hpp"
#include "rendering/vk_shader.hpp"
#include "rendering/vk_utils.hpp"
#include "rendering/camera.hpp"
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/quaternion.hpp>
#include <algorithm>
#include <cmath>
#include <random>
namespace wowee {
namespace rendering {
// --- M2 Particle Emitter Helpers ---
float M2Renderer::interpFloat(const pipeline::M2AnimationTrack& track, float animTime,
int seqIdx, const std::vector<pipeline::M2Sequence>& /*seqs*/,
const std::vector<uint32_t>& globalSeqDurations) {
if (!track.hasData()) return 0.0f;
int si; float t;
resolveTrackTime(track, seqIdx, animTime, globalSeqDurations, si, t);
if (si < 0 || si >= static_cast<int>(track.sequences.size())) return 0.0f;
const auto& keys = track.sequences[si];
if (keys.timestamps.empty() || keys.floatValues.empty()) return 0.0f;
if (keys.floatValues.size() == 1) return keys.floatValues[0];
int idx = findKeyframeIndex(keys.timestamps, t);
if (idx < 0) return 0.0f;
size_t i0 = static_cast<size_t>(idx);
size_t i1 = std::min(i0 + 1, keys.floatValues.size() - 1);
if (i0 == i1) return keys.floatValues[i0];
float t0 = static_cast<float>(keys.timestamps[i0]);
float t1 = static_cast<float>(keys.timestamps[i1]);
float dur = t1 - t0;
float frac = (dur > 0.0f) ? glm::clamp((t - t0) / dur, 0.0f, 1.0f) : 0.0f;
return glm::mix(keys.floatValues[i0], keys.floatValues[i1], frac);
}
// Interpolate an M2 FBlock (particle lifetime curve) at a given life ratio [0..1].
// FBlocks store per-lifetime keyframes for particle color, alpha, and scale.
// NOTE: interpFBlockFloat and interpFBlockVec3 share identical interpolation logic —
// if you fix a bug in one, update the other to match.
float M2Renderer::interpFBlockFloat(const pipeline::M2FBlock& fb, float lifeRatio) {
if (fb.floatValues.empty()) return 1.0f;
if (fb.floatValues.size() == 1 || fb.timestamps.empty()) return fb.floatValues[0];
lifeRatio = glm::clamp(lifeRatio, 0.0f, 1.0f);
for (size_t i = 0; i < fb.timestamps.size() - 1; i++) {
if (lifeRatio <= fb.timestamps[i + 1]) {
float t0 = fb.timestamps[i];
float t1 = fb.timestamps[i + 1];
float dur = t1 - t0;
float frac = (dur > 0.0f) ? (lifeRatio - t0) / dur : 0.0f;
size_t v0 = std::min(i, fb.floatValues.size() - 1);
size_t v1 = std::min(i + 1, fb.floatValues.size() - 1);
return glm::mix(fb.floatValues[v0], fb.floatValues[v1], frac);
}
}
return fb.floatValues.back();
}
glm::vec3 M2Renderer::interpFBlockVec3(const pipeline::M2FBlock& fb, float lifeRatio) {
if (fb.vec3Values.empty()) return glm::vec3(1.0f);
if (fb.vec3Values.size() == 1 || fb.timestamps.empty()) return fb.vec3Values[0];
lifeRatio = glm::clamp(lifeRatio, 0.0f, 1.0f);
for (size_t i = 0; i < fb.timestamps.size() - 1; i++) {
if (lifeRatio <= fb.timestamps[i + 1]) {
float t0 = fb.timestamps[i];
float t1 = fb.timestamps[i + 1];
float dur = t1 - t0;
float frac = (dur > 0.0f) ? (lifeRatio - t0) / dur : 0.0f;
size_t v0 = std::min(i, fb.vec3Values.size() - 1);
size_t v1 = std::min(i + 1, fb.vec3Values.size() - 1);
return glm::mix(fb.vec3Values[v0], fb.vec3Values[v1], frac);
}
}
return fb.vec3Values.back();
}
std::vector<glm::vec3> M2Renderer::getWaterVegetationPositions(const glm::vec3& camPos, float maxDist) const {
std::vector<glm::vec3> result;
float maxDistSq = maxDist * maxDist;
for (const auto& inst : instances) {
if (!inst.cachedModel || !inst.cachedModel->isWaterVegetation) continue;
glm::vec3 diff = inst.position - camPos;
if (glm::dot(diff, diff) <= maxDistSq) {
result.push_back(inst.position);
}
}
return result;
}
void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt) {
if (inst.emitterAccumulators.size() != gpu.particleEmitters.size()) {
inst.emitterAccumulators.resize(gpu.particleEmitters.size(), 0.0f);
}
std::uniform_real_distribution<float> dist01(0.0f, 1.0f);
std::uniform_real_distribution<float> distN(-1.0f, 1.0f);
std::uniform_int_distribution<int> distTile;
for (size_t ei = 0; ei < gpu.particleEmitters.size(); ei++) {
const auto& em = gpu.particleEmitters[ei];
if (!em.enabled) continue;
float rate = interpFloat(em.emissionRate, inst.animTime, inst.currentSequenceIndex,
gpu.sequences, gpu.globalSequenceDurations);
float life = interpFloat(em.lifespan, inst.animTime, inst.currentSequenceIndex,
gpu.sequences, gpu.globalSequenceDurations);
if (rate <= 0.0f || life <= 0.0f) continue;
inst.emitterAccumulators[ei] += rate * dt;
while (inst.emitterAccumulators[ei] >= 1.0f && inst.particles.size() < MAX_M2_PARTICLES) {
inst.emitterAccumulators[ei] -= 1.0f;
M2Particle p;
p.emitterIndex = static_cast<int>(ei);
p.life = 0.0f;
p.maxLife = life;
p.tileIndex = 0.0f;
// Position: emitter position transformed by bone matrix
glm::vec3 localPos = em.position;
glm::mat4 boneXform = glm::mat4(1.0f);
if (em.bone < inst.boneMatrices.size()) {
boneXform = inst.boneMatrices[em.bone];
}
glm::vec3 worldPos = glm::vec3(inst.modelMatrix * boneXform * glm::vec4(localPos, 1.0f));
p.position = worldPos;
// Velocity: emission speed in upward direction + random spread
float speed = interpFloat(em.emissionSpeed, inst.animTime, inst.currentSequenceIndex,
gpu.sequences, gpu.globalSequenceDurations);
float vRange = interpFloat(em.verticalRange, inst.animTime, inst.currentSequenceIndex,
gpu.sequences, gpu.globalSequenceDurations);
float hRange = interpFloat(em.horizontalRange, inst.animTime, inst.currentSequenceIndex,
gpu.sequences, gpu.globalSequenceDurations);
// Base direction: up in model space, transformed to world
glm::vec3 dir(0.0f, 0.0f, 1.0f);
// Add random spread
dir.x += distN(particleRng_) * hRange;
dir.y += distN(particleRng_) * hRange;
dir.z += distN(particleRng_) * vRange;
float lenSq = glm::dot(dir, dir);
if (lenSq > 0.001f * 0.001f) dir *= glm::inversesqrt(lenSq);
// Transform direction by bone + model orientation (rotation only)
glm::mat3 rotMat = glm::mat3(inst.modelMatrix * boneXform);
p.velocity = rotMat * dir * speed;
// When emission speed is ~0 and bone animation isn't loaded (.anim files),
// particles pile up at the same position. Give them a drift so they
// spread outward like a mist/spray effect instead of clustering.
if (std::abs(speed) < 0.01f) {
if (gpu.isFireflyEffect) {
// Fireflies: gentle random drift in all directions
p.velocity = rotMat * glm::vec3(
distN(particleRng_) * 0.6f,
distN(particleRng_) * 0.6f,
distN(particleRng_) * 0.3f
);
} else {
p.velocity = rotMat * glm::vec3(
distN(particleRng_) * 1.0f,
distN(particleRng_) * 1.0f,
-dist01(particleRng_) * 0.5f
);
}
}
const uint32_t tilesX = std::max<uint16_t>(em.textureCols, 1);
const uint32_t tilesY = std::max<uint16_t>(em.textureRows, 1);
const uint32_t totalTiles = tilesX * tilesY;
if ((em.flags & kParticleFlagTiled) && totalTiles > 1) {
if (em.flags & kParticleFlagRandomized) {
distTile = std::uniform_int_distribution<int>(0, static_cast<int>(totalTiles - 1));
p.tileIndex = static_cast<float>(distTile(particleRng_));
} else {
p.tileIndex = 0.0f;
}
}
inst.particles.push_back(p);
}
// Cap accumulator to avoid bursts after lag
if (inst.emitterAccumulators[ei] > 2.0f) {
inst.emitterAccumulators[ei] = 0.0f;
}
}
}
void M2Renderer::updateParticles(M2Instance& inst, float dt) {
if (!inst.cachedModel) return;
const auto& gpu = *inst.cachedModel;
for (size_t i = 0; i < inst.particles.size(); ) {
auto& p = inst.particles[i];
p.life += dt;
if (p.life >= p.maxLife) {
// Swap-and-pop removal
inst.particles[i] = inst.particles.back();
inst.particles.pop_back();
continue;
}
// Apply gravity
if (p.emitterIndex >= 0 && p.emitterIndex < static_cast<int>(gpu.particleEmitters.size())) {
const auto& pem = gpu.particleEmitters[p.emitterIndex];
float grav = interpFloat(pem.gravity,
inst.animTime, inst.currentSequenceIndex,
gpu.sequences, gpu.globalSequenceDurations);
// When M2 gravity is 0, apply default gravity so particles arc downward.
// Many fountain M2s rely on bone animation (.anim files) we don't load yet.
// Firefly/ambient glow particles intentionally have zero gravity — skip fallback.
if (grav == 0.0f && !gpu.isFireflyEffect) {
float emSpeed = interpFloat(pem.emissionSpeed,
inst.animTime, inst.currentSequenceIndex,
gpu.sequences, gpu.globalSequenceDurations);
if (std::abs(emSpeed) > 0.1f) {
grav = 4.0f; // spray particles
} else {
grav = 1.5f; // mist/drift particles - gentler fall
}
}
p.velocity.z -= grav * dt;
}
p.position += p.velocity * dt;
i++;
}
}
// ---------------------------------------------------------------------------
// Ribbon emitter simulation
// ---------------------------------------------------------------------------
void M2Renderer::updateRibbons(M2Instance& inst, const M2ModelGPU& gpu, float dt) {
const auto& emitters = gpu.ribbonEmitters;
if (emitters.empty()) return;
// Grow per-instance state arrays if needed
if (inst.ribbonEdges.size() != emitters.size()) {
inst.ribbonEdges.resize(emitters.size());
}
if (inst.ribbonEdgeAccumulators.size() != emitters.size()) {
inst.ribbonEdgeAccumulators.resize(emitters.size(), 0.0f);
}
for (size_t ri = 0; ri < emitters.size(); ri++) {
const auto& em = emitters[ri];
auto& edges = inst.ribbonEdges[ri];
auto& accum = inst.ribbonEdgeAccumulators[ri];
// Determine bone world position for spine
glm::vec3 spineWorld = inst.position;
if (em.bone < inst.boneMatrices.size()) {
glm::vec4 local(em.position.x, em.position.y, em.position.z, 1.0f);
spineWorld = glm::vec3(inst.modelMatrix * inst.boneMatrices[em.bone] * local);
} else {
glm::vec4 local(em.position.x, em.position.y, em.position.z, 1.0f);
spineWorld = glm::vec3(inst.modelMatrix * local);
}
// Evaluate animated tracks (use first available sequence key, or fallback value)
auto getFloatVal = [&](const pipeline::M2AnimationTrack& track, float fallback) -> float {
for (const auto& seq : track.sequences) {
if (!seq.floatValues.empty()) return seq.floatValues[0];
}
return fallback;
};
auto getVec3Val = [&](const pipeline::M2AnimationTrack& track, glm::vec3 fallback) -> glm::vec3 {
for (const auto& seq : track.sequences) {
if (!seq.vec3Values.empty()) return seq.vec3Values[0];
}
return fallback;
};
float visibility = getFloatVal(em.visibilityTrack, 1.0f);
float heightAbove = getFloatVal(em.heightAboveTrack, 0.5f);
float heightBelow = getFloatVal(em.heightBelowTrack, 0.5f);
glm::vec3 color = getVec3Val(em.colorTrack, glm::vec3(1.0f));
float alpha = getFloatVal(em.alphaTrack, 1.0f);
// Age existing edges and remove expired ones
for (auto& e : edges) {
e.age += dt;
// Apply gravity
if (em.gravity != 0.0f) {
e.worldPos.z -= em.gravity * dt * dt * 0.5f;
}
}
while (!edges.empty() && edges.front().age >= em.edgeLifetime) {
edges.pop_front();
}
// Emit new edges based on edgesPerSecond
if (visibility > 0.5f) {
accum += em.edgesPerSecond * dt;
while (accum >= 1.0f) {
accum -= 1.0f;
M2Instance::RibbonEdge e;
e.worldPos = spineWorld;
e.color = color;
e.alpha = alpha;
e.heightAbove = heightAbove;
e.heightBelow = heightBelow;
e.age = 0.0f;
edges.push_back(e);
// Cap trail length
if (edges.size() > 128) edges.pop_front();
}
} else {
accum = 0.0f;
}
}
}
// ---------------------------------------------------------------------------
// Ribbon rendering
// ---------------------------------------------------------------------------
void M2Renderer::renderM2Ribbons(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
if (!ribbonPipeline_ || !ribbonAdditivePipeline_ || !ribbonVB_ || !ribbonVBMapped_) return;
// Build camera right vector for billboard orientation
// For ribbons we orient the quad strip along the spine with screen-space up.
// Simple approach: use world-space Z=up for the ribbon cross direction.
const glm::vec3 upWorld(0.0f, 0.0f, 1.0f);
float* dst = static_cast<float*>(ribbonVBMapped_);
size_t written = 0;
ribbonDraws_.clear();
auto& draws = ribbonDraws_;
for (const auto& inst : instances) {
if (!inst.cachedModel) continue;
const auto& gpu = *inst.cachedModel;
if (gpu.ribbonEmitters.empty()) continue;
for (size_t ri = 0; ri < gpu.ribbonEmitters.size(); ri++) {
if (ri >= inst.ribbonEdges.size()) continue;
const auto& edges = inst.ribbonEdges[ri];
if (edges.size() < 2) continue;
const auto& em = gpu.ribbonEmitters[ri];
// Select blend pipeline based on material blend mode
bool additive = false;
if (em.materialIndex < gpu.batches.size()) {
additive = (gpu.batches[em.materialIndex].blendMode >= 3);
}
VkPipeline pipe = additive ? ribbonAdditivePipeline_ : ribbonPipeline_;
// Descriptor set for texture
VkDescriptorSet texSet = (ri < gpu.ribbonTexSets.size())
? gpu.ribbonTexSets[ri] : VK_NULL_HANDLE;
if (!texSet) continue;
uint32_t firstVert = static_cast<uint32_t>(written);
// Emit triangle strip: 2 verts per edge (top + bottom)
for (size_t ei = 0; ei < edges.size(); ei++) {
if (written + 2 > MAX_RIBBON_VERTS) break;
const auto& e = edges[ei];
float t = (em.edgeLifetime > 0.0f)
? 1.0f - (e.age / em.edgeLifetime) : 1.0f;
float a = e.alpha * t;
float u = static_cast<float>(ei) / static_cast<float>(edges.size() - 1);
// Top vertex (above spine along upWorld)
glm::vec3 top = e.worldPos + upWorld * e.heightAbove;
dst[written * 9 + 0] = top.x;
dst[written * 9 + 1] = top.y;
dst[written * 9 + 2] = top.z;
dst[written * 9 + 3] = e.color.r;
dst[written * 9 + 4] = e.color.g;
dst[written * 9 + 5] = e.color.b;
dst[written * 9 + 6] = a;
dst[written * 9 + 7] = u;
dst[written * 9 + 8] = 0.0f; // v = top
written++;
// Bottom vertex (below spine)
glm::vec3 bot = e.worldPos - upWorld * e.heightBelow;
dst[written * 9 + 0] = bot.x;
dst[written * 9 + 1] = bot.y;
dst[written * 9 + 2] = bot.z;
dst[written * 9 + 3] = e.color.r;
dst[written * 9 + 4] = e.color.g;
dst[written * 9 + 5] = e.color.b;
dst[written * 9 + 6] = a;
dst[written * 9 + 7] = u;
dst[written * 9 + 8] = 1.0f; // v = bottom
written++;
}
uint32_t vertCount = static_cast<uint32_t>(written) - firstVert;
if (vertCount >= 4) {
draws.push_back({texSet, pipe, firstVert, vertCount});
} else {
// Rollback if too few verts
written = firstVert;
}
}
}
if (draws.empty() || written == 0) return;
VkExtent2D ext = vkCtx_->getSwapchainExtent();
VkViewport vp{};
vp.x = 0; vp.y = 0;
vp.width = static_cast<float>(ext.width);
vp.height = static_cast<float>(ext.height);
vp.minDepth = 0.0f; vp.maxDepth = 1.0f;
VkRect2D sc{};
sc.offset = {0, 0};
sc.extent = ext;
vkCmdSetViewport(cmd, 0, 1, &vp);
vkCmdSetScissor(cmd, 0, 1, &sc);
VkPipeline lastPipe = VK_NULL_HANDLE;
for (const auto& dc : draws) {
if (dc.pipeline != lastPipe) {
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, dc.pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
ribbonPipelineLayout_, 0, 1, &perFrameSet, 0, nullptr);
lastPipe = dc.pipeline;
}
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
ribbonPipelineLayout_, 1, 1, &dc.texSet, 0, nullptr);
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &ribbonVB_, &offset);
vkCmdDraw(cmd, dc.vertexCount, 1, dc.firstVertex, 0);
}
}
void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
if (!particlePipeline_ || !m2ParticleVB_) return;
// Collect all particles from all instances, grouped by texture+blend
// Reuse persistent map — clear each group's vertex data but keep bucket structure.
for (auto& [k, g] : particleGroups_) {
g.vertexData.clear();
g.preAllocSet = VK_NULL_HANDLE;
}
auto& groups = particleGroups_;
size_t totalParticles = 0;
for (auto& inst : instances) {
if (inst.particles.empty()) continue;
if (!inst.cachedModel) continue;
const auto& gpu = *inst.cachedModel;
for (const auto& p : inst.particles) {
if (p.emitterIndex < 0 || p.emitterIndex >= static_cast<int>(gpu.particleEmitters.size())) continue;
const auto& em = gpu.particleEmitters[p.emitterIndex];
float lifeRatio = p.life / std::max(p.maxLife, 0.001f);
glm::vec3 color = interpFBlockVec3(em.particleColor, lifeRatio);
float alpha = std::min(interpFBlockFloat(em.particleAlpha, lifeRatio), 1.0f);
float rawScale = interpFBlockFloat(em.particleScale, lifeRatio);
if (!gpu.isSpellEffect && !gpu.isFireflyEffect) {
color = glm::mix(color, glm::vec3(1.0f), 0.7f);
if (rawScale > 2.0f) alpha *= 0.02f;
if (em.blendingType == 3 || em.blendingType == 4) alpha *= 0.05f;
}
float scale = (gpu.isSpellEffect || gpu.isFireflyEffect) ? rawScale : std::min(rawScale, 1.5f);
VkTexture* tex = whiteTexture_.get();
if (p.emitterIndex < static_cast<int>(gpu.particleTextures.size())) {
tex = gpu.particleTextures[p.emitterIndex];
}
uint16_t tilesX = std::max<uint16_t>(em.textureCols, 1);
uint16_t tilesY = std::max<uint16_t>(em.textureRows, 1);
uint32_t totalTiles = static_cast<uint32_t>(tilesX) * static_cast<uint32_t>(tilesY);
ParticleGroupKey key{tex, em.blendingType, tilesX, tilesY};
auto& group = groups[key];
group.texture = tex;
group.blendType = em.blendingType;
group.tilesX = tilesX;
group.tilesY = tilesY;
// Capture pre-allocated descriptor set on first insertion for this key
if (group.preAllocSet == VK_NULL_HANDLE &&
p.emitterIndex < static_cast<int>(gpu.particleTexSets.size())) {
group.preAllocSet = gpu.particleTexSets[p.emitterIndex];
}
group.vertexData.push_back(p.position.x);
group.vertexData.push_back(p.position.y);
group.vertexData.push_back(p.position.z);
group.vertexData.push_back(color.r);
group.vertexData.push_back(color.g);
group.vertexData.push_back(color.b);
group.vertexData.push_back(alpha);
group.vertexData.push_back(scale);
float tileIndex = p.tileIndex;
if ((em.flags & kParticleFlagTiled) && totalTiles > 1) {
float animSeconds = inst.animTime / 1000.0f;
uint32_t animFrame = static_cast<uint32_t>(std::floor(animSeconds * totalTiles)) % totalTiles;
tileIndex = p.tileIndex + static_cast<float>(animFrame);
float tilesFloat = static_cast<float>(totalTiles);
// Wrap tile index within totalTiles range
while (tileIndex >= tilesFloat) {
tileIndex -= tilesFloat;
}
}
group.vertexData.push_back(tileIndex);
totalParticles++;
}
}
if (totalParticles == 0) return;
// Bind per-frame set (set 0) for particle pipeline
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
particlePipelineLayout_, 0, 1, &perFrameSet, 0, nullptr);
VkDeviceSize vbOffset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &m2ParticleVB_, &vbOffset);
VkPipeline currentPipeline = VK_NULL_HANDLE;
for (auto& [key, group] : groups) {
if (group.vertexData.empty()) continue;
uint8_t blendType = group.blendType;
VkPipeline desiredPipeline = (blendType == 3 || blendType == 4)
? particleAdditivePipeline_ : particlePipeline_;
if (desiredPipeline != currentPipeline) {
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, desiredPipeline);
currentPipeline = desiredPipeline;
}
// Use pre-allocated stable descriptor set; fall back to per-frame alloc only if unavailable
VkDescriptorSet texSet = group.preAllocSet;
if (texSet == VK_NULL_HANDLE) {
// Fallback: allocate per-frame (pool exhaustion risk — should not happen in practice)
VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
ai.descriptorPool = materialDescPool_;
ai.descriptorSetCount = 1;
ai.pSetLayouts = &particleTexLayout_;
if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &texSet) == VK_SUCCESS) {
VkTexture* tex = group.texture ? group.texture : whiteTexture_.get();
VkDescriptorImageInfo imgInfo = tex->descriptorInfo();
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = texSet;
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &imgInfo;
vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr);
}
}
if (texSet != VK_NULL_HANDLE) {
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
particlePipelineLayout_, 1, 1, &texSet, 0, nullptr);
}
// Push constants: tileCount + alphaKey
struct { float tileX, tileY; int alphaKey; } pc = {
static_cast<float>(group.tilesX), static_cast<float>(group.tilesY),
(blendType == 1) ? 1 : 0
};
vkCmdPushConstants(cmd, particlePipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0,
sizeof(pc), &pc);
// Upload and draw in chunks
size_t count = group.vertexData.size() / 9;
size_t offset = 0;
while (offset < count) {
size_t batch = std::min(count - offset, MAX_M2_PARTICLES);
memcpy(m2ParticleVBMapped_, &group.vertexData[offset * 9], batch * 9 * sizeof(float));
vkCmdDraw(cmd, static_cast<uint32_t>(batch), 1, 0, 0);
offset += batch;
}
}
}
void M2Renderer::renderSmokeParticles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
if (smokeParticles.empty() || !smokePipeline_ || !smokeVB_) return;
// Build vertex data: pos(3) + lifeRatio(1) + size(1) + isSpark(1) per particle
size_t count = std::min(smokeParticles.size(), static_cast<size_t>(MAX_SMOKE_PARTICLES));
float* dst = static_cast<float*>(smokeVBMapped_);
for (size_t i = 0; i < count; i++) {
const auto& p = smokeParticles[i];
*dst++ = p.position.x;
*dst++ = p.position.y;
*dst++ = p.position.z;
*dst++ = p.life / p.maxLife;
*dst++ = p.size;
*dst++ = p.isSpark;
}
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, smokePipeline_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
smokePipelineLayout_, 0, 1, &perFrameSet, 0, nullptr);
// Push constant: screenHeight
float screenHeight = static_cast<float>(vkCtx_->getSwapchainExtent().height);
vkCmdPushConstants(cmd, smokePipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0,
sizeof(float), &screenHeight);
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &smokeVB_, &offset);
vkCmdDraw(cmd, static_cast<uint32_t>(count), 1, 0, 0);
}
} // namespace rendering
} // namespace wowee

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,235 @@
#include "rendering/overlay_system.hpp"
#include "rendering/vk_context.hpp"
#include "rendering/vk_shader.hpp"
#include "rendering/vk_pipeline.hpp"
#include "rendering/vk_utils.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
#include <vector>
namespace wowee {
namespace rendering {
OverlaySystem::OverlaySystem(VkContext* ctx)
: vkCtx_(ctx) {}
OverlaySystem::~OverlaySystem() {
cleanup();
}
void OverlaySystem::cleanup() {
if (!vkCtx_) return;
VkDevice device = vkCtx_->getDevice();
if (selCirclePipeline_) { vkDestroyPipeline(device, selCirclePipeline_, nullptr); selCirclePipeline_ = VK_NULL_HANDLE; }
if (selCirclePipelineLayout_) { vkDestroyPipelineLayout(device, selCirclePipelineLayout_, nullptr); selCirclePipelineLayout_ = VK_NULL_HANDLE; }
if (selCircleVertBuf_) { vmaDestroyBuffer(vkCtx_->getAllocator(), selCircleVertBuf_, selCircleVertAlloc_); selCircleVertBuf_ = VK_NULL_HANDLE; selCircleVertAlloc_ = VK_NULL_HANDLE; }
if (selCircleIdxBuf_) { vmaDestroyBuffer(vkCtx_->getAllocator(), selCircleIdxBuf_, selCircleIdxAlloc_); selCircleIdxBuf_ = VK_NULL_HANDLE; selCircleIdxAlloc_ = VK_NULL_HANDLE; }
if (overlayPipeline_) { vkDestroyPipeline(device, overlayPipeline_, nullptr); overlayPipeline_ = VK_NULL_HANDLE; }
if (overlayPipelineLayout_) { vkDestroyPipelineLayout(device, overlayPipelineLayout_, nullptr); overlayPipelineLayout_ = VK_NULL_HANDLE; }
}
void OverlaySystem::recreatePipelines() {
if (!vkCtx_) return;
VkDevice device = vkCtx_->getDevice();
// Destroy only pipelines (keep geometry buffers)
if (selCirclePipeline_) { vkDestroyPipeline(device, selCirclePipeline_, nullptr); selCirclePipeline_ = VK_NULL_HANDLE; }
if (overlayPipeline_) { vkDestroyPipeline(device, overlayPipeline_, nullptr); overlayPipeline_ = VK_NULL_HANDLE; }
}
void OverlaySystem::setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color) {
selCirclePos_ = pos;
selCircleRadius_ = radius;
selCircleColor_ = color;
selCircleVisible_ = true;
}
void OverlaySystem::clearSelectionCircle() {
selCircleVisible_ = false;
}
void OverlaySystem::initSelectionCircle() {
if (selCirclePipeline_ != VK_NULL_HANDLE) return;
if (!vkCtx_) return;
VkDevice device = vkCtx_->getDevice();
// Load shaders
VkShaderModule vertShader, fragShader;
if (!vertShader.loadFromFile(device, "assets/shaders/selection_circle.vert.spv")) {
LOG_ERROR("OverlaySystem: failed to load selection circle vertex shader");
return;
}
if (!fragShader.loadFromFile(device, "assets/shaders/selection_circle.frag.spv")) {
LOG_ERROR("OverlaySystem: failed to load selection circle fragment shader");
vertShader.destroy();
return;
}
// Pipeline layout: push constants only (mat4 mvp=64 + vec4 color=16), VERTEX|FRAGMENT
VkPushConstantRange pcRange{};
pcRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
pcRange.offset = 0;
pcRange.size = 80;
selCirclePipelineLayout_ = createPipelineLayout(device, {}, {pcRange});
// Vertex input: binding 0, stride 12, vec3 at location 0
VkVertexInputBindingDescription vertBind{0, 12, VK_VERTEX_INPUT_RATE_VERTEX};
VkVertexInputAttributeDescription vertAttr{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0};
// Build disc geometry as TRIANGLE_LIST (N=48 segments)
constexpr int SEGMENTS = 48;
std::vector<float> verts;
verts.reserve((SEGMENTS + 1) * 3);
// Center vertex
verts.insert(verts.end(), {0.0f, 0.0f, 0.0f});
// Ring vertices
for (int i = 0; i <= SEGMENTS; ++i) {
float angle = 2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(SEGMENTS);
verts.push_back(std::cos(angle));
verts.push_back(std::sin(angle));
verts.push_back(0.0f);
}
// Build TRIANGLE_LIST indices
std::vector<uint16_t> indices;
indices.reserve(SEGMENTS * 3);
for (int i = 0; i < SEGMENTS; ++i) {
indices.push_back(0);
indices.push_back(static_cast<uint16_t>(i + 1));
indices.push_back(static_cast<uint16_t>(i + 2));
}
selCircleVertCount_ = SEGMENTS * 3;
// Upload vertex buffer
if (selCircleVertBuf_ == VK_NULL_HANDLE) {
AllocatedBuffer vbuf = uploadBuffer(*vkCtx_, verts.data(),
verts.size() * sizeof(float), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
selCircleVertBuf_ = vbuf.buffer;
selCircleVertAlloc_ = vbuf.allocation;
AllocatedBuffer ibuf = uploadBuffer(*vkCtx_, indices.data(),
indices.size() * sizeof(uint16_t), VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
selCircleIdxBuf_ = ibuf.buffer;
selCircleIdxAlloc_ = ibuf.allocation;
}
// Build pipeline
selCirclePipeline_ = PipelineBuilder()
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
.setVertexInput({vertBind}, {vertAttr})
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setNoDepthTest()
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
.setMultisample(vkCtx_->getMsaaSamples())
.setLayout(selCirclePipelineLayout_)
.setRenderPass(vkCtx_->getImGuiRenderPass())
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
.build(device, vkCtx_->getPipelineCache());
vertShader.destroy();
fragShader.destroy();
if (!selCirclePipeline_) {
LOG_ERROR("OverlaySystem: failed to build selection circle pipeline");
}
}
void OverlaySystem::renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection,
VkCommandBuffer cmd,
HeightQuery2D terrainHeight,
HeightQuery3D wmoHeight,
HeightQuery3D m2Height) {
if (!selCircleVisible_) return;
initSelectionCircle();
if (selCirclePipeline_ == VK_NULL_HANDLE || cmd == VK_NULL_HANDLE) return;
// Keep circle anchored near target foot Z.
const float baseZ = selCirclePos_.z;
float floorZ = baseZ;
auto considerFloor = [&](std::optional<float> sample) {
if (!sample) return;
const float h = *sample;
if (h < baseZ - 1.25f || h > baseZ + 0.85f) return;
floorZ = std::max(floorZ, h);
};
if (terrainHeight) considerFloor(terrainHeight(selCirclePos_.x, selCirclePos_.y));
if (wmoHeight) considerFloor(wmoHeight(selCirclePos_.x, selCirclePos_.y, selCirclePos_.z + 3.0f));
if (m2Height) considerFloor(m2Height(selCirclePos_.x, selCirclePos_.y, selCirclePos_.z + 2.0f));
glm::vec3 raisedPos = selCirclePos_;
raisedPos.z = floorZ + 0.17f;
glm::mat4 model = glm::translate(glm::mat4(1.0f), raisedPos);
model = glm::scale(model, glm::vec3(selCircleRadius_));
glm::mat4 mvp = projection * view * model;
glm::vec4 color4(selCircleColor_, 1.0f);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, selCirclePipeline_);
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &selCircleVertBuf_, &offset);
vkCmdBindIndexBuffer(cmd, selCircleIdxBuf_, 0, VK_INDEX_TYPE_UINT16);
vkCmdPushConstants(cmd, selCirclePipelineLayout_,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
0, 64, &mvp[0][0]);
vkCmdPushConstants(cmd, selCirclePipelineLayout_,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
64, 16, &color4[0]);
vkCmdDrawIndexed(cmd, static_cast<uint32_t>(selCircleVertCount_), 1, 0, 0, 0);
}
void OverlaySystem::initOverlayPipeline() {
if (!vkCtx_) return;
VkDevice device = vkCtx_->getDevice();
VkPushConstantRange pc{};
pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
pc.offset = 0;
pc.size = 16;
VkPipelineLayoutCreateInfo plCI{};
plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
plCI.pushConstantRangeCount = 1;
plCI.pPushConstantRanges = &pc;
vkCreatePipelineLayout(device, &plCI, nullptr, &overlayPipelineLayout_);
VkShaderModule vertMod, fragMod;
if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") ||
!fragMod.loadFromFile(device, "assets/shaders/overlay.frag.spv")) {
LOG_ERROR("OverlaySystem: failed to load overlay shaders");
vertMod.destroy(); fragMod.destroy();
return;
}
overlayPipeline_ = PipelineBuilder()
.setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
.setVertexInput({}, {})
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setNoDepthTest()
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
.setMultisample(vkCtx_->getMsaaSamples())
.setLayout(overlayPipelineLayout_)
.setRenderPass(vkCtx_->getImGuiRenderPass())
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
.build(device, vkCtx_->getPipelineCache());
vertMod.destroy(); fragMod.destroy();
if (overlayPipeline_) LOG_INFO("OverlaySystem: overlay pipeline initialized");
}
void OverlaySystem::renderOverlay(const glm::vec4& color, VkCommandBuffer cmd) {
if (!overlayPipeline_) initOverlayPipeline();
if (!overlayPipeline_ || cmd == VK_NULL_HANDLE) return;
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, overlayPipeline_);
vkCmdPushConstants(cmd, overlayPipelineLayout_,
VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, &color[0]);
vkCmdDraw(cmd, 3, 1, 0, 0);
}
} // namespace rendering
} // namespace wowee

View file

@ -1,5 +1,6 @@
#include "rendering/performance_hud.hpp"
#include "rendering/renderer.hpp"
#include "rendering/post_process_pipeline.hpp"
#include "rendering/vk_context.hpp"
#include "rendering/terrain_renderer.hpp"
#include "rendering/terrain_manager.hpp"
@ -198,38 +199,38 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) {
}
// FSR info
if (renderer->isFSREnabled()) {
if (renderer->getPostProcessPipeline()->isFSREnabled()) {
ImGui::TextColored(colors::kGreen, "FSR 1.0: ON");
auto* ctx = renderer->getVkContext();
if (ctx) {
auto ext = ctx->getSwapchainExtent();
float sf = renderer->getFSRScaleFactor();
float sf = renderer->getPostProcessPipeline()->getFSRScaleFactor();
uint32_t iw = static_cast<uint32_t>(ext.width * sf) & ~1u;
uint32_t ih = static_cast<uint32_t>(ext.height * sf) & ~1u;
ImGui::Text(" %ux%u -> %ux%u (%.0f%%)", iw, ih, ext.width, ext.height, sf * 100.0f);
}
}
if (renderer->isFSR2Enabled()) {
if (renderer->getPostProcessPipeline()->isFSR2Enabled()) {
ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "FSR 3 Upscale: ON");
ImGui::Text(" JitterSign=%.2f", renderer->getFSR2JitterSign());
const bool fgEnabled = renderer->isAmdFsr3FramegenEnabled();
const bool fgReady = renderer->isAmdFsr3FramegenRuntimeReady();
const bool fgActive = renderer->isAmdFsr3FramegenRuntimeActive();
ImGui::Text(" JitterSign=%.2f", renderer->getPostProcessPipeline()->getFSR2JitterSign());
const bool fgEnabled = renderer->getPostProcessPipeline()->isAmdFsr3FramegenEnabled();
const bool fgReady = renderer->getPostProcessPipeline()->isAmdFsr3FramegenRuntimeReady();
const bool fgActive = renderer->getPostProcessPipeline()->isAmdFsr3FramegenRuntimeActive();
const char* fgStatus = "Disabled";
if (fgEnabled) {
fgStatus = fgActive ? "Active" : (fgReady ? "Ready (waiting/fallback)" : "Unavailable");
}
ImGui::Text(" FSR3 FG: %s (%s)", fgStatus, renderer->getAmdFsr3FramegenRuntimePath());
const std::string& fgErr = renderer->getAmdFsr3FramegenRuntimeError();
ImGui::Text(" FSR3 FG: %s (%s)", fgStatus, renderer->getPostProcessPipeline()->getAmdFsr3FramegenRuntimePath());
const std::string& fgErr = renderer->getPostProcessPipeline()->getAmdFsr3FramegenRuntimeError();
if (!fgErr.empty()) {
ImGui::TextWrapped(" FG Last Error: %s", fgErr.c_str());
}
ImGui::Text(" FG Dispatches: %zu", renderer->getAmdFsr3FramegenDispatchCount());
ImGui::Text(" Upscale Dispatches: %zu", renderer->getAmdFsr3UpscaleDispatchCount());
ImGui::Text(" FG Fallbacks: %zu", renderer->getAmdFsr3FallbackCount());
ImGui::Text(" FG Dispatches: %zu", renderer->getPostProcessPipeline()->getAmdFsr3FramegenDispatchCount());
ImGui::Text(" Upscale Dispatches: %zu", renderer->getPostProcessPipeline()->getAmdFsr3UpscaleDispatchCount());
ImGui::Text(" FG Fallbacks: %zu", renderer->getPostProcessPipeline()->getAmdFsr3FallbackCount());
}
if (renderer->isFXAAEnabled()) {
if (renderer->isFSR2Enabled()) {
if (renderer->getPostProcessPipeline()->isFXAAEnabled()) {
if (renderer->getPostProcessPipeline()->isFSR2Enabled()) {
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.8f, 1.0f), "FXAA: ON (FSR3+FXAA combined)");
} else {
ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.6f, 1.0f), "FXAA: ON");

View file

@ -62,6 +62,7 @@
#include "rendering/post_process_pipeline.hpp"
#include "rendering/animation_controller.hpp"
#include "rendering/render_graph.hpp"
#include "rendering/overlay_system.hpp"
#include <imgui.h>
#include <imgui_impl_vulkan.h>
#include <glm/gtc/matrix_transform.hpp>
@ -574,6 +575,9 @@ bool Renderer::initialize(core::Window* win) {
// Create render graph and register virtual resources
renderGraph_ = std::make_unique<RenderGraph>();
// Create overlay system (selection circle + fullscreen overlay)
overlaySystem_ = std::make_unique<OverlaySystem>(vkCtx);
renderGraph_->registerResource("shadow_depth");
renderGraph_->registerResource("reflection_texture");
renderGraph_->registerResource("cull_visibility");
@ -676,15 +680,10 @@ void Renderer::shutdown() {
// Audio shutdown is handled by AudioCoordinator (owned by Application).
audioCoordinator_ = nullptr;
// Cleanup Vulkan selection circle resources
if (vkCtx) {
VkDevice device = vkCtx->getDevice();
if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; }
if (selCirclePipelineLayout) { vkDestroyPipelineLayout(device, selCirclePipelineLayout, nullptr); selCirclePipelineLayout = VK_NULL_HANDLE; }
if (selCircleVertBuf) { vmaDestroyBuffer(vkCtx->getAllocator(), selCircleVertBuf, selCircleVertAlloc); selCircleVertBuf = VK_NULL_HANDLE; selCircleVertAlloc = VK_NULL_HANDLE; }
if (selCircleIdxBuf) { vmaDestroyBuffer(vkCtx->getAllocator(), selCircleIdxBuf, selCircleIdxAlloc); selCircleIdxBuf = VK_NULL_HANDLE; selCircleIdxAlloc = VK_NULL_HANDLE; }
if (overlayPipeline) { vkDestroyPipeline(device, overlayPipeline, nullptr); overlayPipeline = VK_NULL_HANDLE; }
if (overlayPipelineLayout) { vkDestroyPipelineLayout(device, overlayPipelineLayout, nullptr); overlayPipelineLayout = VK_NULL_HANDLE; }
// Cleanup selection circle + overlay resources
if (overlaySystem_) {
overlaySystem_->cleanup();
overlaySystem_.reset();
}
// Shutdown post-process pipeline (FSR/FXAA/FSR2 resources) (§4.3)
@ -800,9 +799,7 @@ void Renderer::applyMsaaChange() {
if (minimap) minimap->recreatePipelines();
// Selection circle + overlay + FSR use lazy init, just destroy them
VkDevice device = vkCtx->getDevice();
if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; }
if (overlayPipeline) { vkDestroyPipeline(device, overlayPipeline, nullptr); overlayPipeline = VK_NULL_HANDLE; }
if (overlaySystem_) overlaySystem_->recreatePipelines();
if (postProcessPipeline_) postProcessPipeline_->destroyAllResources(); // Will be lazily recreated in beginFrame()
// Reinitialize ImGui Vulkan backend with new MSAA sample count
@ -998,74 +995,6 @@ void Renderer::setCharacterFollow(uint32_t instanceId) {
if (animationController_) animationController_->onCharacterFollow(instanceId);
}
void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath) {
if (animationController_) animationController_->setMounted(mountInstId, mountDisplayId, heightOffset, modelPath);
}
void Renderer::clearMount() {
if (animationController_) animationController_->clearMount();
}
void Renderer::playEmote(const std::string& emoteName) {
if (animationController_) animationController_->playEmote(emoteName);
}
void Renderer::cancelEmote() {
if (animationController_) animationController_->cancelEmote();
}
bool Renderer::isEmoteActive() const {
return animationController_ && animationController_->isEmoteActive();
}
void Renderer::setInCombat(bool combat) {
if (animationController_) animationController_->setInCombat(combat);
}
void Renderer::setEquippedWeaponType(uint32_t inventoryType, bool is2HLoose, bool isFist,
bool isDagger, bool hasOffHand, bool hasShield) {
if (animationController_) animationController_->setEquippedWeaponType(inventoryType, is2HLoose, isFist, isDagger, hasOffHand, hasShield);
}
void Renderer::triggerSpecialAttack(uint32_t spellId) {
if (animationController_) animationController_->triggerSpecialAttack(spellId);
}
void Renderer::setEquippedRangedType(RangedWeaponType type) {
if (animationController_) animationController_->setEquippedRangedType(type);
}
void Renderer::triggerRangedShot() {
if (animationController_) animationController_->triggerRangedShot();
}
RangedWeaponType Renderer::getEquippedRangedType() const {
return animationController_ ? animationController_->getEquippedRangedType()
: RangedWeaponType::NONE;
}
void Renderer::setCharging(bool c) {
if (animationController_) animationController_->setCharging(c);
}
bool Renderer::isCharging() const {
return animationController_ && animationController_->isCharging();
}
void Renderer::setTaxiFlight(bool taxi) {
if (animationController_) animationController_->setTaxiFlight(taxi);
}
void Renderer::setMountPitchRoll(float pitch, float roll) {
if (animationController_) animationController_->setMountPitchRoll(pitch, roll);
}
bool Renderer::isMounted() const {
return animationController_ && animationController_->isMounted();
}
bool Renderer::captureScreenshot(const std::string& outputPath) {
if (!vkCtx) return false;
@ -1161,69 +1090,23 @@ bool Renderer::captureScreenshot(const std::string& outputPath) {
return ok != 0;
}
void Renderer::triggerLevelUpEffect(const glm::vec3& position) {
if (animationController_) animationController_->triggerLevelUpEffect(position);
}
void Renderer::startChargeEffect(const glm::vec3& position, const glm::vec3& direction) {
if (animationController_) animationController_->startChargeEffect(position, direction);
}
void Renderer::emitChargeEffect(const glm::vec3& position, const glm::vec3& direction) {
if (animationController_) animationController_->emitChargeEffect(position, direction);
}
void Renderer::stopChargeEffect() {
if (animationController_) animationController_->stopChargeEffect();
}
// ─── Spell Visual Effects — delegated to SpellVisualSystem (§4.4) ────────────
void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
bool useImpactKit) {
if (spellVisualSystem_) spellVisualSystem_->playSpellVisual(visualId, worldPosition, useImpactKit);
}
void Renderer::triggerMeleeSwing() {
if (animationController_) animationController_->triggerMeleeSwing();
}
std::string Renderer::getEmoteText(const std::string& emoteName, const std::string* targetName) {
return AnimationController::getEmoteText(emoteName, targetName);
}
uint32_t Renderer::getEmoteDbcId(const std::string& emoteName) {
return AnimationController::getEmoteDbcId(emoteName);
}
std::string Renderer::getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName,
const std::string* targetName) {
return AnimationController::getEmoteTextByDbcId(dbcId, senderName, targetName);
}
uint32_t Renderer::getEmoteAnimByDbcId(uint32_t dbcId) {
return AnimationController::getEmoteAnimByDbcId(dbcId);
}
void Renderer::setTargetPosition(const glm::vec3* pos) {
if (animationController_) animationController_->setTargetPosition(pos);
}
void Renderer::resetCombatVisualState() {
if (animationController_) animationController_->resetCombatVisualState();
if (spellVisualSystem_) spellVisualSystem_->reset();
}
bool Renderer::isMoving() const {
return cameraController && cameraController->isMoving();
const std::string& Renderer::getCurrentZoneName() const {
static const std::string empty;
return audioCoordinator_ ? audioCoordinator_->getCurrentZoneName() : empty;
}
uint32_t Renderer::getCurrentZoneId() const {
return audioCoordinator_ ? audioCoordinator_->getCurrentZoneId() : 0;
}
void Renderer::update(float deltaTime) {
ZoneScopedN("Renderer::update");
globalTime += deltaTime;
if (musicSwitchCooldown_ > 0.0f) {
musicSwitchCooldown_ = std::max(0.0f, musicSwitchCooldown_ - deltaTime);
}
runDeferredWorldInitStep(deltaTime);
auto updateStart = std::chrono::steady_clock::now();
@ -1281,7 +1164,7 @@ void Renderer::update(float deltaTime) {
weather->setIntensity(wInt);
} else {
// No server weather — use zone-based weather configuration
weather->updateZoneWeather(currentZoneId, deltaTime);
weather->updateZoneWeather(getCurrentZoneId(), deltaTime);
}
weather->setEnabled(true);
@ -1291,7 +1174,7 @@ void Renderer::update(float deltaTime) {
}
} else if (weather) {
// No game handler (single-player without network) — zone weather only
weather->updateZoneWeather(currentZoneId, deltaTime);
weather->updateZoneWeather(getCurrentZoneId(), deltaTime);
weather->setEnabled(true);
}
}
@ -1307,7 +1190,7 @@ void Renderer::update(float deltaTime) {
} else if (cameraController->isMoving() || cameraController->isRightMouseHeld()) {
characterYaw = cameraController->getFacingYaw();
} else if (animationController_ && animationController_->isInCombat() &&
animationController_->getTargetPosition() && !animationController_->isEmoteActive() && !isMounted()) {
animationController_->getTargetPosition() && !animationController_->isEmoteActive() && !(animationController_ && animationController_->isMounted())) {
glm::vec3 toTarget = *animationController_->getTargetPosition() - characterPosition;
if (toTarget.x * toTarget.x + toTarget.y * toTarget.y > 0.01f) {
float targetYaw = glm::degrees(std::atan2(toTarget.y, toTarget.x));
@ -1369,7 +1252,7 @@ void Renderer::update(float deltaTime) {
mountDust->update(deltaTime);
// Spawn dust when mounted and moving on ground
if (isMounted() && camera && cameraController && !(animationController_ && animationController_->isTaxiFlight())) {
if ((animationController_ && animationController_->isMounted()) && camera && cameraController && !(animationController_ && animationController_->isTaxiFlight())) {
bool isMoving = cameraController->isMoving();
bool onGround = cameraController->isGrounded();
@ -1434,45 +1317,31 @@ void Renderer::update(float deltaTime) {
wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &insideWmoId);
playerIndoors_ = insideWmo;
// Ambient environmental sounds: fireplaces, water, birds, etc.
if (audioCoordinator_->getAmbientSoundManager() && camera && wmoRenderer && cameraController) {
bool isIndoor = insideWmo;
bool isSwimming = cameraController->isSwimming();
// Detect blacksmith buildings to play ambient forge/anvil sounds.
// 96048 is the WMO group ID for the Goldshire blacksmith interior.
// TODO: extend to other smithy WMO IDs (Ironforge, Orgrimmar, etc.)
bool isBlacksmith = (insideWmoId == 96048);
// Sync weather audio with visual weather system
// Ambient environmental sounds + zone/music transitions (delegated to AudioCoordinator)
if (audioCoordinator_) {
audio::ZoneAudioContext zctx;
zctx.deltaTime = deltaTime;
zctx.cameraPosition = camPos;
zctx.isSwimming = cameraController ? cameraController->isSwimming() : false;
zctx.insideWmo = insideWmo;
zctx.insideWmoId = insideWmoId;
if (weather) {
auto weatherType = weather->getWeatherType();
float intensity = weather->getIntensity();
audio::AmbientSoundManager::WeatherType audioWeatherType = audio::AmbientSoundManager::WeatherType::NONE;
if (weatherType == Weather::Type::RAIN) {
if (intensity < 0.33f) {
audioWeatherType = audio::AmbientSoundManager::WeatherType::RAIN_LIGHT;
} else if (intensity < 0.66f) {
audioWeatherType = audio::AmbientSoundManager::WeatherType::RAIN_MEDIUM;
} else {
audioWeatherType = audio::AmbientSoundManager::WeatherType::RAIN_HEAVY;
}
} else if (weatherType == Weather::Type::SNOW) {
if (intensity < 0.33f) {
audioWeatherType = audio::AmbientSoundManager::WeatherType::SNOW_LIGHT;
} else if (intensity < 0.66f) {
audioWeatherType = audio::AmbientSoundManager::WeatherType::SNOW_MEDIUM;
} else {
audioWeatherType = audio::AmbientSoundManager::WeatherType::SNOW_HEAVY;
}
}
audioCoordinator_->getAmbientSoundManager()->setWeather(audioWeatherType);
auto wt = weather->getWeatherType();
if (wt == Weather::Type::RAIN) zctx.weatherType = 1;
else if (wt == Weather::Type::SNOW) zctx.weatherType = 2;
else if (wt == Weather::Type::STORM) zctx.weatherType = 3;
zctx.weatherIntensity = weather->getIntensity();
}
audioCoordinator_->getAmbientSoundManager()->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith);
if (terrainManager) {
auto tile = terrainManager->getCurrentTile();
zctx.tileX = tile.x;
zctx.tileY = tile.y;
zctx.hasTile = true;
}
const auto* gh2 = core::Application::getInstance().getGameHandler();
zctx.serverZoneId = gh2 ? gh2->getWorldStateZoneId() : 0;
zctx.zoneManager = zoneManager.get();
audioCoordinator_->updateZoneAudio(zctx);
}
// Wait for M2 doodad animation to finish (was launched earlier in parallel with character anim)
@ -1481,154 +1350,6 @@ void Renderer::update(float deltaTime) {
catch (const std::exception& e) { LOG_ERROR("M2 animation worker: ", e.what()); }
}
// Helper: play zone music, dispatching local files (file: prefix) vs MPQ paths
auto playZoneMusic = [&](const std::string& music) {
if (music.empty()) return;
if (music.rfind("file:", 0) == 0) {
audioCoordinator_->getMusicManager()->crossfadeToFile(music.substr(5));
} else {
audioCoordinator_->getMusicManager()->crossfadeTo(music);
}
};
// Update zone detection and music
if (zoneManager && audioCoordinator_->getMusicManager() && terrainManager && camera) {
// Prefer server-authoritative zone ID (from SMSG_INIT_WORLD_STATES);
// fall back to tile-based lookup for single-player / offline mode.
const auto* gh = core::Application::getInstance().getGameHandler();
uint32_t serverZoneId = gh ? gh->getWorldStateZoneId() : 0;
auto tile = terrainManager->getCurrentTile();
uint32_t zoneId = (serverZoneId != 0) ? serverZoneId : zoneManager->getZoneId(tile.x, tile.y);
bool insideTavern = false;
bool insideBlacksmith = false;
std::string tavernMusic;
// Override with WMO-based detection (e.g., inside Stormwind, taverns, blacksmiths)
if (wmoRenderer) {
uint32_t wmoModelId = insideWmoId;
if (insideWmo) {
// Check if inside Stormwind WMO (model ID 10047)
if (wmoModelId == 10047) {
zoneId = 1519; // Stormwind City
}
// Detect taverns/inns/blacksmiths by WMO model ID
// Log WMO ID for debugging
static uint32_t lastLoggedWmoId = 0;
if (wmoModelId != lastLoggedWmoId) {
LOG_INFO("Inside WMO model ID: ", wmoModelId);
lastLoggedWmoId = wmoModelId;
}
// Detect blacksmith WMO for ambient forge sounds
if (wmoModelId == 96048) { // Goldshire blacksmith interior
insideBlacksmith = true;
LOG_INFO("Detected blacksmith WMO ", wmoModelId);
}
// These IDs represent typical Alliance and Horde inn buildings
if (wmoModelId == 191 || // Goldshire inn (old ID)
wmoModelId == 71414 || // Goldshire inn (actual)
wmoModelId == 190 || // Small inn (common)
wmoModelId == 220 || // Tavern building
wmoModelId == 221 || // Large tavern
wmoModelId == 5392 || // Horde inn
wmoModelId == 5393) { // Another inn variant
insideTavern = true;
// WoW tavern music (cozy ambient tracks) - FIXED PATHS
static const std::vector<std::string> tavernTracks = {
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance01.mp3",
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance02.mp3",
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern1A.mp3",
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3",
};
// Rotate through tracks so the player doesn't always hear the same one.
// Post-increment: first visit plays index 0, next plays 1, etc.
static int tavernTrackIndex = 0;
tavernMusic = tavernTracks[tavernTrackIndex++ % tavernTracks.size()];
LOG_INFO("Detected tavern WMO ", wmoModelId, ", playing: ", tavernMusic);
}
}
}
// Handle tavern music transitions
if (insideTavern) {
if (!inTavern_ && !tavernMusic.empty()) {
inTavern_ = true;
LOG_INFO("Entered tavern");
audioCoordinator_->getMusicManager()->playMusic(tavernMusic, true); // Immediate playback, looping
musicSwitchCooldown_ = 6.0f;
}
} else if (inTavern_) {
// Exited tavern - restore zone music with crossfade
inTavern_ = false;
LOG_INFO("Exited tavern");
auto* info = zoneManager->getZoneInfo(currentZoneId);
if (info) {
std::string music = zoneManager->getRandomMusic(currentZoneId);
if (!music.empty()) {
playZoneMusic(music);
musicSwitchCooldown_ = 6.0f;
}
}
}
// Handle blacksmith music (stop music when entering blacksmith, let ambience play)
if (insideBlacksmith) {
if (!inBlacksmith_) {
inBlacksmith_ = true;
LOG_INFO("Entered blacksmith - stopping music");
audioCoordinator_->getMusicManager()->stopMusic();
}
} else if (inBlacksmith_) {
// Exited blacksmith - restore zone music with crossfade
inBlacksmith_ = false;
LOG_INFO("Exited blacksmith - restoring music");
auto* info = zoneManager->getZoneInfo(currentZoneId);
if (info) {
std::string music = zoneManager->getRandomMusic(currentZoneId);
if (!music.empty()) {
playZoneMusic(music);
musicSwitchCooldown_ = 6.0f;
}
}
}
// Handle normal zone transitions (only if not in tavern or blacksmith)
if (!insideTavern && !insideBlacksmith && zoneId != currentZoneId && zoneId != 0) {
currentZoneId = zoneId;
auto* info = zoneManager->getZoneInfo(zoneId);
if (info) {
currentZoneName = info->name;
LOG_INFO("Entered zone: ", info->name);
if (musicSwitchCooldown_ <= 0.0f) {
std::string music = zoneManager->getRandomMusic(zoneId);
if (!music.empty()) {
playZoneMusic(music);
musicSwitchCooldown_ = 6.0f;
}
}
}
// Update ambient sound manager zone type
if (audioCoordinator_->getAmbientSoundManager()) {
audioCoordinator_->getAmbientSoundManager()->setZoneId(zoneId);
}
}
audioCoordinator_->getMusicManager()->update(deltaTime);
// When a track finishes, pick a new random track from the current zone
if (!audioCoordinator_->getMusicManager()->isPlaying() && !inTavern_ && !inBlacksmith_ &&
currentZoneId != 0 && musicSwitchCooldown_ <= 0.0f) {
std::string music = zoneManager->getRandomMusic(currentZoneId);
if (!music.empty()) {
playZoneMusic(music);
musicSwitchCooldown_ = 2.0f;
}
}
}
// Update performance HUD
if (performanceHUD) {
performanceHUD->update(deltaTime);
@ -1691,215 +1412,12 @@ void Renderer::runDeferredWorldInitStep(float deltaTime) {
deferredWorldInitCooldown_ = 0.12f;
}
// ============================================================
// Selection Circle
// ============================================================
void Renderer::initSelectionCircle() {
if (selCirclePipeline != VK_NULL_HANDLE) return;
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
// Load shaders
VkShaderModule vertShader, fragShader;
if (!vertShader.loadFromFile(device, "assets/shaders/selection_circle.vert.spv")) {
LOG_ERROR("initSelectionCircle: failed to load vertex shader");
return;
}
if (!fragShader.loadFromFile(device, "assets/shaders/selection_circle.frag.spv")) {
LOG_ERROR("initSelectionCircle: failed to load fragment shader");
vertShader.destroy();
return;
}
// Pipeline layout: push constants only (mat4 mvp=64 + vec4 color=16), VERTEX|FRAGMENT
VkPushConstantRange pcRange{};
pcRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
pcRange.offset = 0;
pcRange.size = 80;
selCirclePipelineLayout = createPipelineLayout(device, {}, {pcRange});
// Vertex input: binding 0, stride 12, vec3 at location 0
VkVertexInputBindingDescription vertBind{0, 12, VK_VERTEX_INPUT_RATE_VERTEX};
VkVertexInputAttributeDescription vertAttr{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0};
// Build disc geometry as TRIANGLE_LIST (replaces GL_TRIANGLE_FAN)
// N=48 segments: center at origin + ring verts
constexpr int SEGMENTS = 48;
std::vector<float> verts;
verts.reserve((SEGMENTS + 1) * 3);
// Center vertex
verts.insert(verts.end(), {0.0f, 0.0f, 0.0f});
// Ring vertices
for (int i = 0; i <= SEGMENTS; ++i) {
float angle = 2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(SEGMENTS);
verts.push_back(std::cos(angle));
verts.push_back(std::sin(angle));
verts.push_back(0.0f);
}
// Build TRIANGLE_LIST indices: N triangles (center=0, ring[i]=i+1, ring[i+1]=i+2)
std::vector<uint16_t> indices;
indices.reserve(SEGMENTS * 3);
for (int i = 0; i < SEGMENTS; ++i) {
indices.push_back(0);
indices.push_back(static_cast<uint16_t>(i + 1));
indices.push_back(static_cast<uint16_t>(i + 2));
}
selCircleVertCount = SEGMENTS * 3; // index count for drawing
// Upload vertex buffer
AllocatedBuffer vbuf = uploadBuffer(*vkCtx, verts.data(),
verts.size() * sizeof(float), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
selCircleVertBuf = vbuf.buffer;
selCircleVertAlloc = vbuf.allocation;
// Upload index buffer
AllocatedBuffer ibuf = uploadBuffer(*vkCtx, indices.data(),
indices.size() * sizeof(uint16_t), VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
selCircleIdxBuf = ibuf.buffer;
selCircleIdxAlloc = ibuf.allocation;
// Build pipeline: alpha blend, no depth write/test, TRIANGLE_LIST, CULL_NONE
selCirclePipeline = PipelineBuilder()
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
.setVertexInput({vertBind}, {vertAttr})
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setNoDepthTest()
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
.setMultisample(vkCtx->getMsaaSamples())
.setLayout(selCirclePipelineLayout)
.setRenderPass(vkCtx->getImGuiRenderPass())
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
.build(device, vkCtx->getPipelineCache());
vertShader.destroy();
fragShader.destroy();
if (!selCirclePipeline) {
LOG_ERROR("initSelectionCircle: failed to build pipeline");
}
}
void Renderer::setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color) {
selCirclePos = pos;
selCircleRadius = radius;
selCircleColor = color;
selCircleVisible = true;
if (overlaySystem_) overlaySystem_->setSelectionCircle(pos, radius, color);
}
void Renderer::clearSelectionCircle() {
selCircleVisible = false;
}
void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection, VkCommandBuffer overrideCmd) {
if (!selCircleVisible) return;
initSelectionCircle();
VkCommandBuffer cmd = (overrideCmd != VK_NULL_HANDLE) ? overrideCmd : currentCmd;
if (selCirclePipeline == VK_NULL_HANDLE || cmd == VK_NULL_HANDLE) return;
// Keep circle anchored near target foot Z. Accept nearby floor probes only,
// so distant upper/lower WMO planes don't yank the ring away from feet.
const float baseZ = selCirclePos.z;
float floorZ = baseZ;
auto considerFloor = [&](std::optional<float> sample) {
if (!sample) return;
const float h = *sample;
// Ignore unrelated floors/ceilings far from target feet.
if (h < baseZ - 1.25f || h > baseZ + 0.85f) return;
floorZ = std::max(floorZ, h);
};
if (terrainManager) {
considerFloor(terrainManager->getHeightAt(selCirclePos.x, selCirclePos.y));
}
if (wmoRenderer) {
considerFloor(wmoRenderer->getFloorHeight(selCirclePos.x, selCirclePos.y, selCirclePos.z + 3.0f));
}
if (m2Renderer) {
considerFloor(m2Renderer->getFloorHeight(selCirclePos.x, selCirclePos.y, selCirclePos.z + 2.0f));
}
glm::vec3 raisedPos = selCirclePos;
raisedPos.z = floorZ + 0.17f;
glm::mat4 model = glm::translate(glm::mat4(1.0f), raisedPos);
model = glm::scale(model, glm::vec3(selCircleRadius));
glm::mat4 mvp = projection * view * model;
glm::vec4 color4(selCircleColor, 1.0f);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, selCirclePipeline);
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &selCircleVertBuf, &offset);
vkCmdBindIndexBuffer(cmd, selCircleIdxBuf, 0, VK_INDEX_TYPE_UINT16);
// Push mvp (64 bytes) at offset 0
vkCmdPushConstants(cmd, selCirclePipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
0, 64, &mvp[0][0]);
// Push color (16 bytes) at offset 64
vkCmdPushConstants(cmd, selCirclePipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
64, 16, &color4[0]);
vkCmdDrawIndexed(cmd, static_cast<uint32_t>(selCircleVertCount), 1, 0, 0, 0);
}
// ──────────────────────────────────────────────────────────────
// Fullscreen overlay pipeline (underwater tint, etc.)
// ──────────────────────────────────────────────────────────────
void Renderer::initOverlayPipeline() {
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
// Push constant: vec4 color (16 bytes), visible to both stages
VkPushConstantRange pc{};
pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
pc.offset = 0;
pc.size = 16;
VkPipelineLayoutCreateInfo plCI{};
plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
plCI.pushConstantRangeCount = 1;
plCI.pPushConstantRanges = &pc;
vkCreatePipelineLayout(device, &plCI, nullptr, &overlayPipelineLayout);
VkShaderModule vertMod, fragMod;
if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") ||
!fragMod.loadFromFile(device, "assets/shaders/overlay.frag.spv")) {
LOG_ERROR("Renderer: failed to load overlay shaders");
vertMod.destroy(); fragMod.destroy();
return;
}
overlayPipeline = PipelineBuilder()
.setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
.setVertexInput({}, {}) // fullscreen triangle, no VBOs
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setNoDepthTest()
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
.setMultisample(vkCtx->getMsaaSamples())
.setLayout(overlayPipelineLayout)
.setRenderPass(vkCtx->getImGuiRenderPass())
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
.build(device, vkCtx->getPipelineCache());
vertMod.destroy(); fragMod.destroy();
if (overlayPipeline) LOG_INFO("Renderer: overlay pipeline initialized");
}
void Renderer::renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd) {
if (!overlayPipeline) initOverlayPipeline();
VkCommandBuffer cmd = (overrideCmd != VK_NULL_HANDLE) ? overrideCmd : currentCmd;
if (!overlayPipeline || cmd == VK_NULL_HANDLE) return;
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, overlayPipeline);
vkCmdPushConstants(cmd, overlayPipelineLayout,
VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, &color[0]);
vkCmdDraw(cmd, 3, 1, 0, 0); // fullscreen triangle
if (overlaySystem_) overlaySystem_->clearSelectionCircle();
}
// ========================= PostProcessPipeline delegation stubs (§4.3) =========================
@ -1908,13 +1426,6 @@ PostProcessPipeline* Renderer::getPostProcessPipeline() const {
return postProcessPipeline_.get();
}
void Renderer::setFXAAEnabled(bool enabled) {
if (postProcessPipeline_) postProcessPipeline_->setFXAAEnabled(enabled);
}
bool Renderer::isFXAAEnabled() const {
return postProcessPipeline_ && postProcessPipeline_->isFXAAEnabled();
}
void Renderer::setFSREnabled(bool enabled) {
if (!postProcessPipeline_) return;
auto req = postProcessPipeline_->setFSREnabled(enabled);
@ -1923,22 +1434,6 @@ void Renderer::setFSREnabled(bool enabled) {
msaaChangePending_ = true;
}
}
bool Renderer::isFSREnabled() const {
return postProcessPipeline_ && postProcessPipeline_->isFSREnabled();
}
void Renderer::setFSRQuality(float scaleFactor) {
if (postProcessPipeline_) postProcessPipeline_->setFSRQuality(scaleFactor);
}
void Renderer::setFSRSharpness(float sharpness) {
if (postProcessPipeline_) postProcessPipeline_->setFSRSharpness(sharpness);
}
float Renderer::getFSRScaleFactor() const {
return postProcessPipeline_ ? postProcessPipeline_->getFSRScaleFactor() : 1.0f;
}
float Renderer::getFSRSharpness() const {
return postProcessPipeline_ ? postProcessPipeline_->getFSRSharpness() : 0.0f;
}
void Renderer::setFSR2Enabled(bool enabled) {
if (!postProcessPipeline_) return;
auto req = postProcessPipeline_->setFSR2Enabled(enabled, camera.get());
@ -1952,63 +1447,6 @@ void Renderer::setFSR2Enabled(bool enabled) {
pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT;
}
}
bool Renderer::isFSR2Enabled() const {
return postProcessPipeline_ && postProcessPipeline_->isFSR2Enabled();
}
void Renderer::setFSR2DebugTuning(float jitterSign, float motionVecScaleX, float motionVecScaleY) {
if (postProcessPipeline_) postProcessPipeline_->setFSR2DebugTuning(jitterSign, motionVecScaleX, motionVecScaleY);
}
void Renderer::setAmdFsr3FramegenEnabled(bool enabled) {
if (postProcessPipeline_) postProcessPipeline_->setAmdFsr3FramegenEnabled(enabled);
}
bool Renderer::isAmdFsr3FramegenEnabled() const {
return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenEnabled();
}
float Renderer::getFSR2JitterSign() const {
return postProcessPipeline_ ? postProcessPipeline_->getFSR2JitterSign() : 1.0f;
}
float Renderer::getFSR2MotionVecScaleX() const {
return postProcessPipeline_ ? postProcessPipeline_->getFSR2MotionVecScaleX() : 1.0f;
}
float Renderer::getFSR2MotionVecScaleY() const {
return postProcessPipeline_ ? postProcessPipeline_->getFSR2MotionVecScaleY() : 1.0f;
}
bool Renderer::isAmdFsr2SdkAvailable() const {
return postProcessPipeline_ && postProcessPipeline_->isAmdFsr2SdkAvailable();
}
bool Renderer::isAmdFsr3FramegenSdkAvailable() const {
return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenSdkAvailable();
}
bool Renderer::isAmdFsr3FramegenRuntimeActive() const {
return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenRuntimeActive();
}
bool Renderer::isAmdFsr3FramegenRuntimeReady() const {
return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenRuntimeReady();
}
const char* Renderer::getAmdFsr3FramegenRuntimePath() const {
return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FramegenRuntimePath() : "";
}
const std::string& Renderer::getAmdFsr3FramegenRuntimeError() const {
static const std::string empty;
return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FramegenRuntimeError() : empty;
}
size_t Renderer::getAmdFsr3UpscaleDispatchCount() const {
return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3UpscaleDispatchCount() : 0;
}
size_t Renderer::getAmdFsr3FramegenDispatchCount() const {
return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FramegenDispatchCount() : 0;
}
size_t Renderer::getAmdFsr3FallbackCount() const {
return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FallbackCount() : 0;
}
void Renderer::setBrightness(float b) {
if (postProcessPipeline_) postProcessPipeline_->setBrightness(b);
}
float Renderer::getBrightness() const {
return postProcessPipeline_ ? postProcessPipeline_->getBrightness() : 1.0f;
}
void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
ZoneScopedN("Renderer::renderWorld");
(void)world;
@ -2132,7 +1570,12 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
{
VkCommandBuffer cmd = beginSecondary(SEC_CHARS);
setSecondaryViewportScissor(cmd);
renderSelectionCircle(view, projection, cmd);
if (overlaySystem_) {
overlaySystem_->renderSelectionCircle(view, projection, cmd,
terrainManager ? OverlaySystem::HeightQuery2D([&](float x, float y) { return terrainManager->getHeightAt(x, y); }) : OverlaySystem::HeightQuery2D{},
wmoRenderer ? OverlaySystem::HeightQuery3D([&](float x, float y, float z) { return wmoRenderer->getFloorHeight(x, y, z); }) : OverlaySystem::HeightQuery3D{},
m2Renderer ? OverlaySystem::HeightQuery3D([&](float x, float y, float z) { return m2Renderer->getFloorHeight(x, y, z); }) : OverlaySystem::HeightQuery3D{});
}
if (characterRenderer && camera && !skipChars) {
characterRenderer->render(cmd, perFrameSet, *camera);
}
@ -2164,7 +1607,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
if (questMarkerRenderer && camera) questMarkerRenderer->render(cmd, perFrameSet, *camera);
// Underwater overlay + minimap
if (overlayPipeline && waterRenderer && camera) {
if (overlaySystem_ && waterRenderer && camera) {
glm::vec3 camPos = camera->getPosition();
auto waterH = waterRenderer->getNearestWaterHeightAt(camPos.x, camPos.y, camPos.z);
constexpr float MIN_SUBMERSION_OVERLAY = 1.5f;
@ -2179,21 +1622,21 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
glm::vec4 tint = canal
? glm::vec4(0.01f, 0.04f, 0.10f, fogStrength)
: glm::vec4(0.03f, 0.09f, 0.18f, fogStrength);
renderOverlay(tint, cmd);
if (overlaySystem_) overlaySystem_->renderOverlay(tint, cmd);
}
}
// Ghost mode desaturation: cold blue-grey overlay when dead/ghost
if (ghostMode_) {
renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f), cmd);
if (ghostMode_ && overlaySystem_) {
overlaySystem_->renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f), cmd);
}
// Brightness overlay (applied before minimap so it doesn't affect UI)
{
if (overlaySystem_) {
float br = postProcessPipeline_ ? postProcessPipeline_->getBrightness() : 1.0f;
if (br < 0.99f) {
renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - br), cmd);
overlaySystem_->renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - br), cmd);
} else if (br > 1.01f) {
float alpha = (br - 1.0f) / 1.0f;
renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), cmd);
overlaySystem_->renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), cmd);
}
}
if (minimap && minimap->isEnabled() && camera && window) {
@ -2277,7 +1720,12 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
std::chrono::steady_clock::now() - wmoStart).count();
}
renderSelectionCircle(view, projection);
if (overlaySystem_) {
overlaySystem_->renderSelectionCircle(view, projection, currentCmd,
terrainManager ? OverlaySystem::HeightQuery2D([&](float x, float y) { return terrainManager->getHeightAt(x, y); }) : OverlaySystem::HeightQuery2D{},
wmoRenderer ? OverlaySystem::HeightQuery3D([&](float x, float y, float z) { return wmoRenderer->getFloorHeight(x, y, z); }) : OverlaySystem::HeightQuery3D{},
m2Renderer ? OverlaySystem::HeightQuery3D([&](float x, float y, float z) { return m2Renderer->getFloorHeight(x, y, z); }) : OverlaySystem::HeightQuery3D{});
}
if (characterRenderer && camera && !skipChars) {
characterRenderer->prepareRender(frameIdx);
@ -2312,7 +1760,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
// Underwater overlay and minimap — in the fallback path these run inline;
// in the parallel path they were already recorded into SEC_POST above.
if (!parallelRecordingEnabled_) {
if (overlayPipeline && waterRenderer && camera) {
if (overlaySystem_ && waterRenderer && camera) {
glm::vec3 camPos = camera->getPosition();
auto waterH = waterRenderer->getNearestWaterHeightAt(camPos.x, camPos.y, camPos.z);
constexpr float MIN_SUBMERSION_OVERLAY = 1.5f;
@ -2327,21 +1775,21 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
glm::vec4 tint = canal
? glm::vec4(0.01f, 0.04f, 0.10f, fogStrength)
: glm::vec4(0.03f, 0.09f, 0.18f, fogStrength);
renderOverlay(tint);
if (overlaySystem_) overlaySystem_->renderOverlay(tint, currentCmd);
}
}
// Ghost mode desaturation: cold blue-grey overlay when dead/ghost
if (ghostMode_) {
renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f));
if (ghostMode_ && overlaySystem_) {
overlaySystem_->renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f), currentCmd);
}
// Brightness overlay (applied before minimap so it doesn't affect UI)
{
if (overlaySystem_) {
float br = postProcessPipeline_ ? postProcessPipeline_->getBrightness() : 1.0f;
if (br < 0.99f) {
renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - br));
overlaySystem_->renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - br), currentCmd);
} else if (br > 1.01f) {
float alpha = (br - 1.0f) / 1.0f;
renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha));
overlaySystem_->renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), currentCmd);
}
}
if (minimap && minimap->isEnabled() && camera && window) {