mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Parallel animation updates, thread-safe collision, M2 pop-in fix, shadow stabilization
- Overlap M2 and character animation updates via std::async (~2-5ms saved) - Thread-local collision scratch buffers for concurrent floor queries - Parallel terrain/WMO/M2 floor queries in camera controller - Seed new M2 instance bones from existing siblings to eliminate pop-in flash - Fix shadow flicker: snap center along stable light axes instead of in view space - Increase shadow distance default to 300 units (slider max 500)
This commit is contained in:
parent
a4966e486f
commit
4cb03c38fe
9 changed files with 160 additions and 87 deletions
|
|
@ -475,9 +475,7 @@ private:
|
|||
static constexpr float SPATIAL_CELL_SIZE = 64.0f;
|
||||
std::unordered_map<GridCell, std::vector<uint32_t>, GridCellHash> spatialGrid;
|
||||
std::unordered_map<uint32_t, size_t> instanceIndexById;
|
||||
mutable std::vector<size_t> candidateScratch;
|
||||
mutable std::unordered_set<uint32_t> candidateIdScratch;
|
||||
mutable std::vector<uint32_t> collisionTriScratch_;
|
||||
// Collision scratch buffers are thread_local (see m2_renderer.cpp) for thread-safety.
|
||||
|
||||
// Collision query profiling (per frame).
|
||||
mutable double queryTimeMs = 0.0;
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ private:
|
|||
glm::vec3 shadowCenter = glm::vec3(0.0f);
|
||||
bool shadowCenterInitialized = false;
|
||||
bool shadowsEnabled = true;
|
||||
float shadowDistance_ = 72.0f; // Shadow frustum half-extent (default: 72 units)
|
||||
float shadowDistance_ = 300.0f; // Shadow frustum half-extent (default: 300 units)
|
||||
uint32_t shadowFrameCounter_ = 0;
|
||||
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ public:
|
|||
|
||||
void setShadowsEnabled(bool enabled) { shadowsEnabled = enabled; }
|
||||
bool areShadowsEnabled() const { return shadowsEnabled; }
|
||||
void setShadowDistance(float dist) { shadowDistance_ = glm::clamp(dist, 40.0f, 200.0f); }
|
||||
void setShadowDistance(float dist) { shadowDistance_ = glm::clamp(dist, 40.0f, 500.0f); }
|
||||
float getShadowDistance() const { return shadowDistance_; }
|
||||
void setMsaaSamples(VkSampleCountFlagBits samples);
|
||||
|
||||
|
|
|
|||
|
|
@ -711,9 +711,7 @@ private:
|
|||
static constexpr float SPATIAL_CELL_SIZE = 64.0f;
|
||||
std::unordered_map<GridCell, std::vector<uint32_t>, GridCellHash> spatialGrid;
|
||||
std::unordered_map<uint32_t, size_t> instanceIndexById;
|
||||
mutable std::vector<size_t> candidateScratch;
|
||||
mutable std::vector<uint32_t> triScratch_; // Scratch for collision grid queries
|
||||
mutable std::unordered_set<uint32_t> candidateIdScratch;
|
||||
// Collision scratch buffers are thread_local (see wmo_renderer.cpp) for thread-safety.
|
||||
|
||||
// Parallel visibility culling
|
||||
uint32_t numCullThreads_ = 1;
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ private:
|
|||
bool pendingVsync = false;
|
||||
int pendingResIndex = 0;
|
||||
bool pendingShadows = true;
|
||||
float pendingShadowDistance = 72.0f;
|
||||
float pendingShadowDistance = 300.0f;
|
||||
bool pendingWaterRefraction = false;
|
||||
int pendingMasterVolume = 100;
|
||||
int pendingMusicVolume = 30;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "rendering/camera_controller.hpp"
|
||||
#include <algorithm>
|
||||
#include <future>
|
||||
#include <imgui.h>
|
||||
#include "rendering/terrain_manager.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
|
|
@ -808,25 +809,53 @@ void CameraController::update(float deltaTime) {
|
|||
if (useCached) {
|
||||
groundH = cachedFloorHeight_;
|
||||
} else {
|
||||
// Full collision check
|
||||
// Full collision check — run terrain/WMO/M2 queries in parallel
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
std::optional<float> m2H;
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y);
|
||||
}
|
||||
// When airborne, anchor probe to last ground level so the
|
||||
// ceiling doesn't rise with the jump and catch roof geometry.
|
||||
float wmoBaseZ = grounded ? std::max(targetPos.z, lastGroundZ) : lastGroundZ;
|
||||
float wmoProbeZ = wmoBaseZ + stepUpBudget + 0.5f;
|
||||
float wmoNormalZ = 1.0f;
|
||||
|
||||
// Launch WMO + M2 floor queries asynchronously while terrain runs on this thread.
|
||||
// Collision scratch buffers are thread_local so concurrent calls are safe.
|
||||
using FloorResult = std::pair<std::optional<float>, float>;
|
||||
std::future<FloorResult> wmoFuture;
|
||||
std::future<FloorResult> m2Future;
|
||||
bool wmoAsync = false, m2Async = false;
|
||||
float px = targetPos.x, py = targetPos.y;
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, wmoProbeZ, &wmoNormalZ);
|
||||
wmoAsync = true;
|
||||
wmoFuture = std::async(std::launch::async,
|
||||
[this, px, py, wmoProbeZ]() -> FloorResult {
|
||||
float nz = 1.0f;
|
||||
auto h = wmoRenderer->getFloorHeight(px, py, wmoProbeZ, &nz);
|
||||
return {h, nz};
|
||||
});
|
||||
}
|
||||
if (m2Renderer && !externalFollow_) {
|
||||
float m2NormalZ = 1.0f;
|
||||
m2H = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, wmoProbeZ, &m2NormalZ);
|
||||
if (m2H && m2NormalZ < MIN_WALKABLE_NORMAL_M2) {
|
||||
m2Async = true;
|
||||
m2Future = std::async(std::launch::async,
|
||||
[this, px, py, wmoProbeZ]() -> FloorResult {
|
||||
float nz = 1.0f;
|
||||
auto h = m2Renderer->getFloorHeight(px, py, wmoProbeZ, &nz);
|
||||
return {h, nz};
|
||||
});
|
||||
}
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y);
|
||||
}
|
||||
if (wmoAsync) {
|
||||
auto [h, nz] = wmoFuture.get();
|
||||
wmoH = h;
|
||||
wmoNormalZ = nz;
|
||||
}
|
||||
if (m2Async) {
|
||||
auto [h, nz] = m2Future.get();
|
||||
m2H = h;
|
||||
if (m2H && nz < MIN_WALKABLE_NORMAL_M2) {
|
||||
m2H = std::nullopt;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -282,6 +282,14 @@ glm::vec3 closestPointOnTriangle(const glm::vec3& p,
|
|||
|
||||
} // namespace
|
||||
|
||||
// Thread-local scratch buffers for collision queries (allows concurrent getFloorHeight calls)
|
||||
static thread_local std::vector<size_t> tl_m2_candidateScratch;
|
||||
static thread_local std::unordered_set<uint32_t> tl_m2_candidateIdScratch;
|
||||
static thread_local std::vector<uint32_t> tl_m2_collisionTriScratch;
|
||||
|
||||
// Forward declaration (defined after animation helpers)
|
||||
static void computeBoneMatrices(const M2ModelGPU& model, M2Instance& instance);
|
||||
|
||||
void M2Instance::updateModelMatrix() {
|
||||
modelMatrix = glm::mat4(1.0f);
|
||||
modelMatrix = glm::translate(modelMatrix, position);
|
||||
|
|
@ -1673,6 +1681,21 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position,
|
|||
instance.animDuration = static_cast<float>(mdl.sequences[0].duration);
|
||||
instance.animTime = static_cast<float>(rand() % std::max(1u, mdl.sequences[0].duration));
|
||||
instance.variationTimer = 3000.0f + static_cast<float>(rand() % 8000);
|
||||
|
||||
// Seed bone matrices from an existing instance of the same model so the
|
||||
// new instance renders immediately instead of being invisible until the
|
||||
// next update() computes bones (prevents pop-in flash).
|
||||
for (const auto& existing : instances) {
|
||||
if (existing.modelId == modelId && !existing.boneMatrices.empty()) {
|
||||
instance.boneMatrices = existing.boneMatrices;
|
||||
instance.bonesDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If no sibling exists yet, compute bones immediately
|
||||
if (instance.boneMatrices.empty()) {
|
||||
computeBoneMatrices(mdlRef, instance);
|
||||
}
|
||||
}
|
||||
|
||||
// Register in dedup map before pushing (uses original position, not ground-adjusted)
|
||||
|
|
@ -1764,6 +1787,18 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4&
|
|||
instance.animDuration = static_cast<float>(mdl2.sequences[0].duration);
|
||||
instance.animTime = static_cast<float>(rand() % std::max(1u, mdl2.sequences[0].duration));
|
||||
instance.variationTimer = 3000.0f + static_cast<float>(rand() % 8000);
|
||||
|
||||
// Seed bone matrices from an existing sibling so the instance renders immediately
|
||||
for (const auto& existing : instances) {
|
||||
if (existing.modelId == modelId && !existing.boneMatrices.empty()) {
|
||||
instance.boneMatrices = existing.boneMatrices;
|
||||
instance.bonesDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (instance.boneMatrices.empty()) {
|
||||
computeBoneMatrices(mdl2, instance);
|
||||
}
|
||||
} else {
|
||||
instance.animTime = static_cast<float>(rand()) / RAND_MAX * 10000.0f;
|
||||
}
|
||||
|
|
@ -2380,12 +2415,15 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
}
|
||||
|
||||
// Upload bone matrices to SSBO if model has skeletal animation.
|
||||
// Bone buffers are pre-allocated by prepareRender() on the main thread.
|
||||
// If not yet allocated (race/timing), skip this instance entirely to avoid
|
||||
// a bind-pose flash — it will render correctly next frame.
|
||||
bool needsBones = model.hasAnimation && !model.disableAnimation && !instance.boneMatrices.empty();
|
||||
// Skip animated instances entirely until bones are computed + buffers allocated
|
||||
// to prevent bind-pose/T-pose flash on first appearance.
|
||||
bool modelNeedsAnimation = model.hasAnimation && !model.disableAnimation;
|
||||
if (modelNeedsAnimation && instance.boneMatrices.empty()) {
|
||||
continue; // Bones not yet computed — skip to avoid bind-pose flash
|
||||
}
|
||||
bool needsBones = modelNeedsAnimation && !instance.boneMatrices.empty();
|
||||
if (needsBones && (!instance.boneBuffer[frameIndex] || !instance.boneSet[frameIndex])) {
|
||||
continue;
|
||||
continue; // Bone buffers not yet allocated — skip to avoid bind-pose flash
|
||||
}
|
||||
bool useBones = needsBones;
|
||||
if (useBones) {
|
||||
|
|
@ -3620,7 +3658,7 @@ void M2Renderer::rebuildSpatialIndex() {
|
|||
void M2Renderer::gatherCandidates(const glm::vec3& queryMin, const glm::vec3& queryMax,
|
||||
std::vector<size_t>& outIndices) const {
|
||||
outIndices.clear();
|
||||
candidateIdScratch.clear();
|
||||
tl_m2_candidateIdScratch.clear();
|
||||
|
||||
GridCell minCell = toCell(queryMin);
|
||||
GridCell maxCell = toCell(queryMax);
|
||||
|
|
@ -3630,7 +3668,7 @@ void M2Renderer::gatherCandidates(const glm::vec3& queryMin, const glm::vec3& qu
|
|||
auto it = spatialGrid.find(GridCell{x, y, z});
|
||||
if (it == spatialGrid.end()) continue;
|
||||
for (uint32_t id : it->second) {
|
||||
if (!candidateIdScratch.insert(id).second) continue;
|
||||
if (!tl_m2_candidateIdScratch.insert(id).second) continue;
|
||||
auto idxIt = instanceIndexById.find(id);
|
||||
if (idxIt != instanceIndexById.end()) {
|
||||
outIndices.push_back(idxIt->second);
|
||||
|
|
@ -3803,9 +3841,9 @@ std::optional<float> M2Renderer::getFloorHeight(float glX, float glY, float glZ,
|
|||
|
||||
glm::vec3 queryMin(glX - 2.0f, glY - 2.0f, glZ - 6.0f);
|
||||
glm::vec3 queryMax(glX + 2.0f, glY + 2.0f, glZ + 8.0f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
gatherCandidates(queryMin, queryMax, tl_m2_candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
for (size_t idx : tl_m2_candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
|
|
@ -3827,14 +3865,14 @@ std::optional<float> M2Renderer::getFloorHeight(float glX, float glY, float glZ,
|
|||
model.collision.getFloorTrisInRange(
|
||||
localPos.x - 1.0f, localPos.y - 1.0f,
|
||||
localPos.x + 1.0f, localPos.y + 1.0f,
|
||||
collisionTriScratch_);
|
||||
tl_m2_collisionTriScratch);
|
||||
|
||||
glm::vec3 rayOrigin(localPos.x, localPos.y, localPos.z + 5.0f);
|
||||
glm::vec3 rayDir(0.0f, 0.0f, -1.0f);
|
||||
float bestHitZ = -std::numeric_limits<float>::max();
|
||||
bool hitAny = false;
|
||||
|
||||
for (uint32_t ti : collisionTriScratch_) {
|
||||
for (uint32_t ti : tl_m2_collisionTriScratch) {
|
||||
if (ti >= model.collision.triCount) continue;
|
||||
if (model.collision.triBounds[ti].maxZ < localPos.z - 10.0f ||
|
||||
model.collision.triBounds[ti].minZ > localPos.z + 5.0f) continue;
|
||||
|
|
@ -3949,10 +3987,10 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
|
||||
glm::vec3 queryMin = glm::min(from, to) - glm::vec3(7.0f, 7.0f, 5.0f);
|
||||
glm::vec3 queryMax = glm::max(from, to) + glm::vec3(7.0f, 7.0f, 5.0f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
gatherCandidates(queryMin, queryMax, tl_m2_candidateScratch);
|
||||
|
||||
// Check against all M2 instances in local space (rotation-aware).
|
||||
for (size_t idx : candidateScratch) {
|
||||
for (size_t idx : tl_m2_candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
|
|
@ -3985,14 +4023,14 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
std::min(localFrom.y, localPos.y) - localRadius - 1.0f,
|
||||
std::max(localFrom.x, localPos.x) + localRadius + 1.0f,
|
||||
std::max(localFrom.y, localPos.y) + localRadius + 1.0f,
|
||||
collisionTriScratch_);
|
||||
tl_m2_collisionTriScratch);
|
||||
|
||||
constexpr float PLAYER_HEIGHT = 2.0f;
|
||||
constexpr float MAX_TOTAL_PUSH = 0.02f; // Cap total push per instance
|
||||
bool pushed = false;
|
||||
float totalPushX = 0.0f, totalPushY = 0.0f;
|
||||
|
||||
for (uint32_t ti : collisionTriScratch_) {
|
||||
for (uint32_t ti : tl_m2_collisionTriScratch) {
|
||||
if (ti >= model.collision.triCount) continue;
|
||||
if (localPos.z + PLAYER_HEIGHT < model.collision.triBounds[ti].minZ ||
|
||||
localPos.z > model.collision.triBounds[ti].maxZ) continue;
|
||||
|
|
@ -4190,9 +4228,9 @@ float M2Renderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3&
|
|||
glm::vec3 rayEnd = origin + direction * maxDistance;
|
||||
glm::vec3 queryMin = glm::min(origin, rayEnd) - glm::vec3(1.0f);
|
||||
glm::vec3 queryMax = glm::max(origin, rayEnd) + glm::vec3(1.0f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
gatherCandidates(queryMin, queryMax, tl_m2_candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
for (size_t idx : tl_m2_candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <set>
|
||||
#include <future>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
|
@ -2678,16 +2679,23 @@ void Renderer::update(float deltaTime) {
|
|||
}
|
||||
|
||||
|
||||
// Update character animations
|
||||
// Launch M2 doodad animation on background thread (overlaps with character animation + audio)
|
||||
std::future<void> m2AnimFuture;
|
||||
bool m2AnimLaunched = false;
|
||||
if (m2Renderer && camera) {
|
||||
float m2DeltaTime = deltaTime;
|
||||
glm::vec3 m2CamPos = camera->getPosition();
|
||||
glm::mat4 m2ViewProj = camera->getProjectionMatrix() * camera->getViewMatrix();
|
||||
m2AnimFuture = std::async(std::launch::async,
|
||||
[this, m2DeltaTime, m2CamPos, m2ViewProj]() {
|
||||
m2Renderer->update(m2DeltaTime, m2CamPos, m2ViewProj);
|
||||
});
|
||||
m2AnimLaunched = true;
|
||||
}
|
||||
|
||||
// Update character animations (runs in parallel with M2 animation above)
|
||||
if (characterRenderer && camera) {
|
||||
auto charAnimStart = std::chrono::steady_clock::now();
|
||||
characterRenderer->update(deltaTime, camera->getPosition());
|
||||
float charAnimMs = std::chrono::duration<float, std::milli>(
|
||||
std::chrono::steady_clock::now() - charAnimStart).count();
|
||||
if (charAnimMs > 5.0f) {
|
||||
LOG_WARNING("SLOW characterRenderer->update: ", charAnimMs, "ms (",
|
||||
characterRenderer->getInstanceCount(), " instances)");
|
||||
}
|
||||
}
|
||||
|
||||
// Update AudioEngine (cleanup finished sounds, etc.)
|
||||
|
|
@ -2872,17 +2880,9 @@ void Renderer::update(float deltaTime) {
|
|||
ambientSoundManager->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith);
|
||||
}
|
||||
|
||||
// Update M2 doodad animations (pass camera for frustum-culling bone computation)
|
||||
if (m2Renderer && camera) {
|
||||
auto m2Start = std::chrono::steady_clock::now();
|
||||
m2Renderer->update(deltaTime, camera->getPosition(),
|
||||
camera->getProjectionMatrix() * camera->getViewMatrix());
|
||||
float m2Ms = std::chrono::duration<float, std::milli>(
|
||||
std::chrono::steady_clock::now() - m2Start).count();
|
||||
if (m2Ms > 3.0f) {
|
||||
LOG_WARNING("SLOW m2Renderer->update: ", m2Ms, "ms (",
|
||||
m2Renderer->getInstanceCount(), " instances)");
|
||||
}
|
||||
// Wait for M2 doodad animation to finish (was launched earlier in parallel with character anim)
|
||||
if (m2AnimLaunched) {
|
||||
m2AnimFuture.get();
|
||||
}
|
||||
|
||||
// Helper: play zone music, dispatching local files (file: prefix) vs MPQ paths
|
||||
|
|
@ -4338,27 +4338,32 @@ glm::mat4 Renderer::computeLightSpaceMatrix() {
|
|||
shadowCenter = desiredCenter;
|
||||
glm::vec3 center = shadowCenter;
|
||||
|
||||
// Snap to shadow texel grid to keep projection stable while moving.
|
||||
// Snap shadow frustum to texel grid so the projection is perfectly stable
|
||||
// while moving. We compute the light's right/up axes from the sun direction
|
||||
// (these are constant per frame regardless of center) and snap center along
|
||||
// them before building the view matrix.
|
||||
float halfExtent = kShadowHalfExtent;
|
||||
float texelWorld = (2.0f * halfExtent) / static_cast<float>(SHADOW_MAP_SIZE);
|
||||
|
||||
// Build light view to get stable axes
|
||||
// Stable light-space axes (independent of center position)
|
||||
glm::vec3 up(0.0f, 0.0f, 1.0f);
|
||||
// If sunDir is nearly parallel to up, pick a different up vector
|
||||
if (std::abs(glm::dot(sunDir, up)) > 0.99f) {
|
||||
up = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
}
|
||||
glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
|
||||
glm::vec3 lightRight = glm::normalize(glm::cross(sunDir, up));
|
||||
glm::vec3 lightUp = glm::normalize(glm::cross(lightRight, sunDir));
|
||||
|
||||
// Stable texel snapping in light space removes movement shimmer.
|
||||
glm::vec4 centerLS = lightView * glm::vec4(center, 1.0f);
|
||||
centerLS.x = std::round(centerLS.x / texelWorld) * texelWorld;
|
||||
centerLS.y = std::round(centerLS.y / texelWorld) * texelWorld;
|
||||
glm::vec4 snappedCenter = glm::inverse(lightView) * centerLS;
|
||||
center = glm::vec3(snappedCenter);
|
||||
// Snap center along light's right and up axes to align with texel grid.
|
||||
// This eliminates sub-texel shifts that cause shadow shimmer.
|
||||
float dotR = glm::dot(center, lightRight);
|
||||
float dotU = glm::dot(center, lightUp);
|
||||
dotR = std::floor(dotR / texelWorld) * texelWorld;
|
||||
dotU = std::floor(dotU / texelWorld) * texelWorld;
|
||||
float dotD = glm::dot(center, sunDir); // depth axis unchanged
|
||||
center = lightRight * dotR + lightUp * dotU + sunDir * dotD;
|
||||
shadowCenter = center;
|
||||
lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
|
||||
|
||||
glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
|
||||
glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent,
|
||||
kShadowNearPlane, kShadowFarPlane);
|
||||
lightProj[1][1] *= -1.0f; // Vulkan Y-flip for shadow pass
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ size_t envSizeOrDefault(const char* name, size_t defValue) {
|
|||
}
|
||||
} // namespace
|
||||
|
||||
// Thread-local scratch buffers for collision queries (allows concurrent getFloorHeight/checkWallCollision calls)
|
||||
static thread_local std::vector<size_t> tl_candidateScratch;
|
||||
static thread_local std::vector<uint32_t> tl_triScratch;
|
||||
static thread_local std::unordered_set<uint32_t> tl_candidateIdScratch;
|
||||
|
||||
static void transformAABB(const glm::mat4& modelMatrix,
|
||||
const glm::vec3& localMin,
|
||||
const glm::vec3& localMax,
|
||||
|
|
@ -1288,7 +1293,7 @@ void WMORenderer::rebuildSpatialIndex() {
|
|||
void WMORenderer::gatherCandidates(const glm::vec3& queryMin, const glm::vec3& queryMax,
|
||||
std::vector<size_t>& outIndices) const {
|
||||
outIndices.clear();
|
||||
candidateIdScratch.clear();
|
||||
tl_candidateIdScratch.clear();
|
||||
|
||||
GridCell minCell = toCell(queryMin);
|
||||
GridCell maxCell = toCell(queryMax);
|
||||
|
|
@ -1298,7 +1303,7 @@ void WMORenderer::gatherCandidates(const glm::vec3& queryMin, const glm::vec3& q
|
|||
auto it = spatialGrid.find(GridCell{x, y, z});
|
||||
if (it == spatialGrid.end()) continue;
|
||||
for (uint32_t id : it->second) {
|
||||
if (!candidateIdScratch.insert(id).second) continue;
|
||||
if (!tl_candidateIdScratch.insert(id).second) continue;
|
||||
auto idxIt = instanceIndexById.find(id);
|
||||
if (idxIt != instanceIndexById.end()) {
|
||||
outIndices.push_back(idxIt->second);
|
||||
|
|
@ -2830,9 +2835,9 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
group.getTrianglesInRange(
|
||||
localOrigin.x - 1.0f, localOrigin.y - 1.0f,
|
||||
localOrigin.x + 1.0f, localOrigin.y + 1.0f,
|
||||
triScratch_);
|
||||
tl_triScratch);
|
||||
|
||||
for (uint32_t triStart : triScratch_) {
|
||||
for (uint32_t triStart : tl_triScratch) {
|
||||
const glm::vec3& v0 = verts[indices[triStart]];
|
||||
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
||||
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
||||
|
|
@ -2906,9 +2911,9 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
// early-returned because overlapping WMO instances need full coverage).
|
||||
glm::vec3 queryMin(glX - 2.0f, glY - 2.0f, glZ - 8.0f);
|
||||
glm::vec3 queryMax(glX + 2.0f, glY + 2.0f, glZ + 10.0f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
gatherCandidates(queryMin, queryMax, tl_candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
for (size_t idx : tl_candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
|
|
@ -3081,9 +3086,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
|
||||
glm::vec3 queryMin = glm::min(from, to) - glm::vec3(8.0f, 8.0f, 5.0f);
|
||||
glm::vec3 queryMax = glm::max(from, to) + glm::vec3(8.0f, 8.0f, 5.0f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
gatherCandidates(queryMin, queryMax, tl_candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
for (size_t idx : tl_candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
|
|
@ -3149,9 +3154,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
float rangeMinY = std::min(localFrom.y, localTo.y) - PLAYER_RADIUS - 1.5f;
|
||||
float rangeMaxX = std::max(localFrom.x, localTo.x) + PLAYER_RADIUS + 1.5f;
|
||||
float rangeMaxY = std::max(localFrom.y, localTo.y) + PLAYER_RADIUS + 1.5f;
|
||||
group.getTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, triScratch_);
|
||||
group.getTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, tl_triScratch);
|
||||
|
||||
for (uint32_t triStart : triScratch_) {
|
||||
for (uint32_t triStart : tl_triScratch) {
|
||||
// Use pre-computed Z bounds for fast vertical reject
|
||||
const auto& tb = group.triBounds[triStart / 3];
|
||||
|
||||
|
|
@ -3319,9 +3324,9 @@ void WMORenderer::updateActiveGroup(float glX, float glY, float glZ) {
|
|||
|
||||
glm::vec3 queryMin(glX - 0.5f, glY - 0.5f, glZ - 0.5f);
|
||||
glm::vec3 queryMax(glX + 0.5f, glY + 0.5f, glZ + 0.5f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
gatherCandidates(queryMin, queryMax, tl_candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
for (size_t idx : tl_candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x ||
|
||||
glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y ||
|
||||
|
|
@ -3365,9 +3370,9 @@ bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outMode
|
|||
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
||||
glm::vec3 queryMin(glX - 0.5f, glY - 0.5f, glZ - 0.5f);
|
||||
glm::vec3 queryMax(glX + 0.5f, glY + 0.5f, glZ + 0.5f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
gatherCandidates(queryMin, queryMax, tl_candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
for (size_t idx : tl_candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
|
|
@ -3414,9 +3419,9 @@ bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outMode
|
|||
bool WMORenderer::isInsideInteriorWMO(float glX, float glY, float glZ) const {
|
||||
glm::vec3 queryMin(glX - 0.5f, glY - 0.5f, glZ - 0.5f);
|
||||
glm::vec3 queryMax(glX + 0.5f, glY + 0.5f, glZ + 0.5f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
gatherCandidates(queryMin, queryMax, tl_candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
for (size_t idx : tl_candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
|
|
@ -3470,9 +3475,9 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
|
|||
glm::vec3 rayEnd = origin + direction * maxDistance;
|
||||
glm::vec3 queryMin = glm::min(origin, rayEnd) - glm::vec3(1.0f);
|
||||
glm::vec3 queryMax = glm::max(origin, rayEnd) + glm::vec3(1.0f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
gatherCandidates(queryMin, queryMax, tl_candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
for (size_t idx : tl_candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
|
|
@ -3526,9 +3531,9 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
|
|||
float rMinY = std::min(localOrigin.y, localEnd.y) - 1.0f;
|
||||
float rMaxX = std::max(localOrigin.x, localEnd.x) + 1.0f;
|
||||
float rMaxY = std::max(localOrigin.y, localEnd.y) + 1.0f;
|
||||
group.getWallTrianglesInRange(rMinX, rMinY, rMaxX, rMaxY, triScratch_);
|
||||
group.getWallTrianglesInRange(rMinX, rMinY, rMaxX, rMaxY, tl_triScratch);
|
||||
|
||||
for (uint32_t triStart : triScratch_) {
|
||||
for (uint32_t triStart : tl_triScratch) {
|
||||
const glm::vec3& v0 = verts[indices[triStart]];
|
||||
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
||||
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
||||
|
|
|
|||
|
|
@ -6270,7 +6270,7 @@ void GameScreen::renderSettingsWindow() {
|
|||
if (pendingShadows) {
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(150.0f);
|
||||
if (ImGui::SliderFloat("Distance##shadow", &pendingShadowDistance, 40.0f, 200.0f, "%.0f")) {
|
||||
if (ImGui::SliderFloat("Distance##shadow", &pendingShadowDistance, 40.0f, 500.0f, "%.0f")) {
|
||||
if (renderer) renderer->setShadowDistance(pendingShadowDistance);
|
||||
saveSettings();
|
||||
}
|
||||
|
|
@ -6387,7 +6387,7 @@ void GameScreen::renderSettingsWindow() {
|
|||
pendingFullscreen = kDefaultFullscreen;
|
||||
pendingVsync = kDefaultVsync;
|
||||
pendingShadows = kDefaultShadows;
|
||||
pendingShadowDistance = 72.0f;
|
||||
pendingShadowDistance = 300.0f;
|
||||
pendingGroundClutterDensity = kDefaultGroundClutterDensity;
|
||||
pendingAntiAliasing = 0;
|
||||
pendingNormalMapping = true;
|
||||
|
|
@ -7505,7 +7505,7 @@ void GameScreen::loadSettings() {
|
|||
else if (key == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0);
|
||||
else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150);
|
||||
else if (key == "shadows") pendingShadows = (std::stoi(val) != 0);
|
||||
else if (key == "shadow_distance") pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 200.0f);
|
||||
else if (key == "shadow_distance") pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 500.0f);
|
||||
else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0);
|
||||
else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3);
|
||||
else if (key == "normal_mapping") pendingNormalMapping = (std::stoi(val) != 0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue