mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-04 16:23:52 +00:00
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:
parent
6dcc06697b
commit
34c0e3ca28
49 changed files with 29113 additions and 28109 deletions
File diff suppressed because it is too large
Load diff
1271
src/rendering/m2_renderer_instance.cpp
Normal file
1271
src/rendering/m2_renderer_instance.cpp
Normal file
File diff suppressed because it is too large
Load diff
364
src/rendering/m2_renderer_internal.h
Normal file
364
src/rendering/m2_renderer_internal.h
Normal 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öller–Trumbore 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
|
||||
618
src/rendering/m2_renderer_particles.cpp
Normal file
618
src/rendering/m2_renderer_particles.cpp
Normal 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
|
||||
1637
src/rendering/m2_renderer_render.cpp
Normal file
1637
src/rendering/m2_renderer_render.cpp
Normal file
File diff suppressed because it is too large
Load diff
235
src/rendering/overlay_system.cpp
Normal file
235
src/rendering/overlay_system.cpp
Normal 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
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue