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:
Kelsi 2026-03-07 22:29:06 -08:00
parent a4966e486f
commit 4cb03c38fe
9 changed files with 160 additions and 87 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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

View file

@ -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]];

View file

@ -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);