mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Optimize M2 update loop: skip static doodads, incremental spatial index
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
- Split M2 instances into fast-path index lists (animated, particle-only, particle-all, smoke) to avoid iterating all 46K instances per frame - Cache model flags (hasAnimation, disableAnimation, isSmoke, etc.) on M2Instance struct to eliminate per-frame hash lookups - Replace full rebuildSpatialIndex on position/transform updates with incremental grid cell remove+add, preventing 8.5ms/frame rebuild cost - Advance animTime for all instances (texture UV animation) but only compute bones and particles for the ~3K that need it M2_UPDATE: 10.7ms → 2.0ms, FPS: 35 → 55-59
This commit is contained in:
parent
7535084652
commit
3482dacea8
4 changed files with 177 additions and 62 deletions
|
|
@ -174,6 +174,13 @@ struct M2Instance {
|
||||||
std::vector<float> emitterAccumulators; // fractional particle counter per emitter
|
std::vector<float> emitterAccumulators; // fractional particle counter per emitter
|
||||||
std::vector<M2Particle> particles;
|
std::vector<M2Particle> particles;
|
||||||
|
|
||||||
|
// Cached model flags (set at creation to avoid per-frame hash lookups)
|
||||||
|
bool cachedHasAnimation = false;
|
||||||
|
bool cachedDisableAnimation = false;
|
||||||
|
bool cachedIsSmoke = false;
|
||||||
|
bool cachedHasParticleEmitters = false;
|
||||||
|
float cachedBoundRadius = 0.0f;
|
||||||
|
|
||||||
// Frame-skip optimization (update distant animations less frequently)
|
// Frame-skip optimization (update distant animations less frequently)
|
||||||
uint8_t frameSkipCounter = 0;
|
uint8_t frameSkipCounter = 0;
|
||||||
|
|
||||||
|
|
@ -451,8 +458,14 @@ private:
|
||||||
std::vector<std::future<void>> animFutures_; // Reused each frame
|
std::vector<std::future<void>> animFutures_; // Reused each frame
|
||||||
bool spatialIndexDirty_ = false;
|
bool spatialIndexDirty_ = false;
|
||||||
|
|
||||||
|
// Fast-path instance index lists (rebuilt in rebuildSpatialIndex / on create)
|
||||||
|
std::vector<size_t> animatedInstanceIndices_; // hasAnimation && !disableAnimation
|
||||||
|
std::vector<size_t> particleOnlyInstanceIndices_; // !hasAnimation && hasParticleEmitters
|
||||||
|
std::vector<size_t> particleInstanceIndices_; // ALL instances with particle emitters
|
||||||
|
|
||||||
// Smoke particle system
|
// Smoke particle system
|
||||||
std::vector<SmokeParticle> smokeParticles;
|
std::vector<SmokeParticle> smokeParticles;
|
||||||
|
std::vector<size_t> smokeInstanceIndices_; // Indices into instances[] for smoke emitters
|
||||||
static constexpr int MAX_SMOKE_PARTICLES = 1000;
|
static constexpr int MAX_SMOKE_PARTICLES = 1000;
|
||||||
float smokeEmitAccum = 0.0f;
|
float smokeEmitAccum = 0.0f;
|
||||||
std::mt19937 smokeRng{42};
|
std::mt19937 smokeRng{42};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include "core/coordinates.hpp"
|
#include "core/coordinates.hpp"
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <chrono>
|
||||||
#include "core/spawn_presets.hpp"
|
#include "core/spawn_presets.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include "core/memory_monitor.hpp"
|
#include "core/memory_monitor.hpp"
|
||||||
|
|
@ -47,7 +48,6 @@
|
||||||
|
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
// GL/glew.h removed — Vulkan migration Phase 1
|
// GL/glew.h removed — Vulkan migration Phase 1
|
||||||
#include <chrono>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
|
|
||||||
|
|
@ -1605,6 +1605,13 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position,
|
||||||
getTightCollisionBounds(mdlRef, localMin, localMax);
|
getTightCollisionBounds(mdlRef, localMin, localMax);
|
||||||
transformAABB(instance.modelMatrix, localMin, localMax, instance.worldBoundsMin, instance.worldBoundsMax);
|
transformAABB(instance.modelMatrix, localMin, localMax, instance.worldBoundsMin, instance.worldBoundsMax);
|
||||||
|
|
||||||
|
// Cache model flags on instance to avoid per-frame hash lookups
|
||||||
|
instance.cachedHasAnimation = mdlRef.hasAnimation;
|
||||||
|
instance.cachedDisableAnimation = mdlRef.disableAnimation;
|
||||||
|
instance.cachedIsSmoke = mdlRef.isSmoke;
|
||||||
|
instance.cachedHasParticleEmitters = !mdlRef.particleEmitters.empty();
|
||||||
|
instance.cachedBoundRadius = mdlRef.boundRadius;
|
||||||
|
|
||||||
// Initialize animation: play first sequence (usually Stand/Idle)
|
// Initialize animation: play first sequence (usually Stand/Idle)
|
||||||
const auto& mdl = mdlRef;
|
const auto& mdl = mdlRef;
|
||||||
if (mdl.hasAnimation && !mdl.disableAnimation && !mdl.sequences.empty()) {
|
if (mdl.hasAnimation && !mdl.disableAnimation && !mdl.sequences.empty()) {
|
||||||
|
|
@ -1617,6 +1624,18 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position,
|
||||||
|
|
||||||
instances.push_back(instance);
|
instances.push_back(instance);
|
||||||
size_t idx = instances.size() - 1;
|
size_t idx = instances.size() - 1;
|
||||||
|
// Track special instances for fast-path iteration
|
||||||
|
if (mdlRef.isSmoke) {
|
||||||
|
smokeInstanceIndices_.push_back(idx);
|
||||||
|
}
|
||||||
|
if (!mdlRef.particleEmitters.empty()) {
|
||||||
|
particleInstanceIndices_.push_back(idx);
|
||||||
|
}
|
||||||
|
if (mdlRef.hasAnimation && !mdlRef.disableAnimation) {
|
||||||
|
animatedInstanceIndices_.push_back(idx);
|
||||||
|
} else if (!mdlRef.particleEmitters.empty()) {
|
||||||
|
particleOnlyInstanceIndices_.push_back(idx);
|
||||||
|
}
|
||||||
instanceIndexById[instance.id] = idx;
|
instanceIndexById[instance.id] = idx;
|
||||||
GridCell minCell = toCell(instance.worldBoundsMin);
|
GridCell minCell = toCell(instance.worldBoundsMin);
|
||||||
GridCell maxCell = toCell(instance.worldBoundsMax);
|
GridCell maxCell = toCell(instance.worldBoundsMax);
|
||||||
|
|
@ -1659,8 +1678,15 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4&
|
||||||
glm::vec3 localMin, localMax;
|
glm::vec3 localMin, localMax;
|
||||||
getTightCollisionBounds(models[modelId], localMin, localMax);
|
getTightCollisionBounds(models[modelId], localMin, localMax);
|
||||||
transformAABB(instance.modelMatrix, localMin, localMax, instance.worldBoundsMin, instance.worldBoundsMax);
|
transformAABB(instance.modelMatrix, localMin, localMax, instance.worldBoundsMin, instance.worldBoundsMax);
|
||||||
// Initialize animation
|
// Cache model flags on instance to avoid per-frame hash lookups
|
||||||
const auto& mdl2 = models[modelId];
|
const auto& mdl2 = models[modelId];
|
||||||
|
instance.cachedHasAnimation = mdl2.hasAnimation;
|
||||||
|
instance.cachedDisableAnimation = mdl2.disableAnimation;
|
||||||
|
instance.cachedIsSmoke = mdl2.isSmoke;
|
||||||
|
instance.cachedHasParticleEmitters = !mdl2.particleEmitters.empty();
|
||||||
|
instance.cachedBoundRadius = mdl2.boundRadius;
|
||||||
|
|
||||||
|
// Initialize animation
|
||||||
if (mdl2.hasAnimation && !mdl2.disableAnimation && !mdl2.sequences.empty()) {
|
if (mdl2.hasAnimation && !mdl2.disableAnimation && !mdl2.sequences.empty()) {
|
||||||
instance.currentSequenceIndex = 0;
|
instance.currentSequenceIndex = 0;
|
||||||
instance.idleSequenceIndex = 0;
|
instance.idleSequenceIndex = 0;
|
||||||
|
|
@ -1673,6 +1699,17 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4&
|
||||||
|
|
||||||
instances.push_back(instance);
|
instances.push_back(instance);
|
||||||
size_t idx = instances.size() - 1;
|
size_t idx = instances.size() - 1;
|
||||||
|
if (mdl2.isSmoke) {
|
||||||
|
smokeInstanceIndices_.push_back(idx);
|
||||||
|
}
|
||||||
|
if (!mdl2.particleEmitters.empty()) {
|
||||||
|
particleInstanceIndices_.push_back(idx);
|
||||||
|
}
|
||||||
|
if (mdl2.hasAnimation && !mdl2.disableAnimation) {
|
||||||
|
animatedInstanceIndices_.push_back(idx);
|
||||||
|
} else if (!mdl2.particleEmitters.empty()) {
|
||||||
|
particleOnlyInstanceIndices_.push_back(idx);
|
||||||
|
}
|
||||||
instanceIndexById[instance.id] = idx;
|
instanceIndexById[instance.id] = idx;
|
||||||
GridCell minCell = toCell(instance.worldBoundsMin);
|
GridCell minCell = toCell(instance.worldBoundsMin);
|
||||||
GridCell maxCell = toCell(instance.worldBoundsMax);
|
GridCell maxCell = toCell(instance.worldBoundsMax);
|
||||||
|
|
@ -1818,7 +1855,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
Frustum updateFrustum;
|
Frustum updateFrustum;
|
||||||
updateFrustum.extractFromMatrix(viewProjection);
|
updateFrustum.extractFromMatrix(viewProjection);
|
||||||
|
|
||||||
// --- Smoke particle spawning ---
|
// --- Smoke particle spawning (only iterate tracked smoke instances) ---
|
||||||
std::uniform_real_distribution<float> distXY(-0.4f, 0.4f);
|
std::uniform_real_distribution<float> distXY(-0.4f, 0.4f);
|
||||||
std::uniform_real_distribution<float> distVelXY(-0.3f, 0.3f);
|
std::uniform_real_distribution<float> distVelXY(-0.3f, 0.3f);
|
||||||
std::uniform_real_distribution<float> distVelZ(3.0f, 5.0f);
|
std::uniform_real_distribution<float> distVelZ(3.0f, 5.0f);
|
||||||
|
|
@ -1828,24 +1865,20 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
smokeEmitAccum += deltaTime;
|
smokeEmitAccum += deltaTime;
|
||||||
float emitInterval = 1.0f / 8.0f; // 8 particles per second per emitter
|
float emitInterval = 1.0f / 8.0f; // 8 particles per second per emitter
|
||||||
|
|
||||||
for (auto& instance : instances) {
|
if (smokeEmitAccum >= emitInterval &&
|
||||||
auto it = models.find(instance.modelId);
|
static_cast<int>(smokeParticles.size()) < MAX_SMOKE_PARTICLES) {
|
||||||
if (it == models.end()) continue;
|
for (size_t si : smokeInstanceIndices_) {
|
||||||
const M2ModelGPU& model = it->second;
|
if (si >= instances.size()) continue;
|
||||||
|
auto& instance = instances[si];
|
||||||
|
|
||||||
if (model.isSmoke && smokeEmitAccum >= emitInterval &&
|
|
||||||
static_cast<int>(smokeParticles.size()) < MAX_SMOKE_PARTICLES) {
|
|
||||||
// Emission point: model origin in world space (model matrix already positions at chimney)
|
|
||||||
glm::vec3 emitWorld = glm::vec3(instance.modelMatrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
|
glm::vec3 emitWorld = glm::vec3(instance.modelMatrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
|
||||||
|
|
||||||
// Occasionally spawn a spark instead of smoke (~1 in 8)
|
|
||||||
bool spark = (smokeRng() % 8 == 0);
|
bool spark = (smokeRng() % 8 == 0);
|
||||||
|
|
||||||
SmokeParticle p;
|
SmokeParticle p;
|
||||||
p.position = emitWorld + glm::vec3(distXY(smokeRng), distXY(smokeRng), 0.0f);
|
p.position = emitWorld + glm::vec3(distXY(smokeRng), distXY(smokeRng), 0.0f);
|
||||||
if (spark) {
|
if (spark) {
|
||||||
p.velocity = glm::vec3(distVelXY(smokeRng) * 2.0f, distVelXY(smokeRng) * 2.0f, distVelZ(smokeRng) * 1.5f);
|
p.velocity = glm::vec3(distVelXY(smokeRng) * 2.0f, distVelXY(smokeRng) * 2.0f, distVelZ(smokeRng) * 1.5f);
|
||||||
p.maxLife = 0.8f + static_cast<float>(smokeRng() % 100) / 100.0f * 1.2f; // 0.8-2.0s
|
p.maxLife = 0.8f + static_cast<float>(smokeRng() % 100) / 100.0f * 1.2f;
|
||||||
p.size = 0.5f;
|
p.size = 0.5f;
|
||||||
p.isSpark = 1.0f;
|
p.isSpark = 1.0f;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1857,10 +1890,8 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
p.life = 0.0f;
|
p.life = 0.0f;
|
||||||
p.instanceId = instance.id;
|
p.instanceId = instance.id;
|
||||||
smokeParticles.push_back(p);
|
smokeParticles.push_back(p);
|
||||||
|
if (static_cast<int>(smokeParticles.size()) >= MAX_SMOKE_PARTICLES) break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (smokeEmitAccum >= emitInterval) {
|
|
||||||
smokeEmitAccum = 0.0f;
|
smokeEmitAccum = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1882,32 +1913,37 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Normal M2 animation update ---
|
// --- Normal M2 animation update ---
|
||||||
// Phase 1: Update animation state (cheap, sequential)
|
// Advance animTime for ALL instances (needed for texture UV animation on static doodads).
|
||||||
// Collect indices of instances that need bone matrix computation.
|
// This is a tight loop touching only one float per instance — no hash lookups.
|
||||||
// Reuse persistent vector to avoid allocation stutter
|
for (auto& instance : instances) {
|
||||||
boneWorkIndices_.clear();
|
instance.animTime += dtMs;
|
||||||
if (boneWorkIndices_.capacity() < instances.size()) {
|
}
|
||||||
boneWorkIndices_.reserve(instances.size());
|
// Wrap animTime for particle-only instances so emission rate tracks keep looping
|
||||||
|
for (size_t idx : particleOnlyInstanceIndices_) {
|
||||||
|
if (idx >= instances.size()) continue;
|
||||||
|
auto& instance = instances[idx];
|
||||||
|
if (instance.animTime > 3333.0f) {
|
||||||
|
instance.animTime = std::fmod(instance.animTime, 3333.0f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t idx = 0; idx < instances.size(); ++idx) {
|
boneWorkIndices_.clear();
|
||||||
|
boneWorkIndices_.reserve(animatedInstanceIndices_.size());
|
||||||
|
|
||||||
|
// Update animated instances (full animation state + bone computation culling)
|
||||||
|
// Note: animTime was already advanced by dtMs in the global loop above.
|
||||||
|
// Here we apply the speed factor: subtract the base dtMs and add dtMs*speed.
|
||||||
|
for (size_t idx : animatedInstanceIndices_) {
|
||||||
|
if (idx >= instances.size()) continue;
|
||||||
auto& instance = instances[idx];
|
auto& instance = instances[idx];
|
||||||
|
|
||||||
|
instance.animTime += dtMs * (instance.animSpeed - 1.0f);
|
||||||
|
|
||||||
|
// For animation looping/variation, we need the actual model data.
|
||||||
auto it = models.find(instance.modelId);
|
auto it = models.find(instance.modelId);
|
||||||
if (it == models.end()) continue;
|
if (it == models.end()) continue;
|
||||||
const M2ModelGPU& model = it->second;
|
const M2ModelGPU& model = it->second;
|
||||||
|
|
||||||
if (!model.hasAnimation || model.disableAnimation) {
|
|
||||||
instance.animTime += dtMs;
|
|
||||||
// Wrap animation time for models with particle emitters so emission
|
|
||||||
// rate tracks keep looping instead of running past their keyframes.
|
|
||||||
if (!model.particleEmitters.empty() && instance.animTime > 3333.0f) {
|
|
||||||
instance.animTime = std::fmod(instance.animTime, 3333.0f);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.animTime += dtMs * instance.animSpeed;
|
|
||||||
|
|
||||||
// Validate sequence index
|
// Validate sequence index
|
||||||
if (instance.currentSequenceIndex < 0 ||
|
if (instance.currentSequenceIndex < 0 ||
|
||||||
instance.currentSequenceIndex >= static_cast<int>(model.sequences.size())) {
|
instance.currentSequenceIndex >= static_cast<int>(model.sequences.size())) {
|
||||||
|
|
@ -1918,14 +1954,11 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle animation looping / variation transitions
|
// Handle animation looping / variation transitions
|
||||||
// When animDuration is 0 (e.g. "Stand" with infinite loop) but the model
|
if (instance.animDuration <= 0.0f && instance.cachedHasParticleEmitters) {
|
||||||
// has particle emitters, wrap time so particle emission tracks keep looping.
|
instance.animDuration = 3333.0f;
|
||||||
if (instance.animDuration <= 0.0f && !model.particleEmitters.empty()) {
|
|
||||||
instance.animDuration = 3333.0f; // ~3.3s loop for continuous particle effects
|
|
||||||
}
|
}
|
||||||
if (instance.animDuration > 0.0f && instance.animTime >= instance.animDuration) {
|
if (instance.animDuration > 0.0f && instance.animTime >= instance.animDuration) {
|
||||||
if (instance.playingVariation) {
|
if (instance.playingVariation) {
|
||||||
// Variation finished — return to idle
|
|
||||||
instance.playingVariation = false;
|
instance.playingVariation = false;
|
||||||
instance.currentSequenceIndex = instance.idleSequenceIndex;
|
instance.currentSequenceIndex = instance.idleSequenceIndex;
|
||||||
if (instance.idleSequenceIndex < static_cast<int>(model.sequences.size())) {
|
if (instance.idleSequenceIndex < static_cast<int>(model.sequences.size())) {
|
||||||
|
|
@ -1934,12 +1967,11 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
instance.animTime = 0.0f;
|
instance.animTime = 0.0f;
|
||||||
instance.variationTimer = 4000.0f + static_cast<float>(rand() % 6000);
|
instance.variationTimer = 4000.0f + static_cast<float>(rand() % 6000);
|
||||||
} else {
|
} else {
|
||||||
// Loop idle
|
|
||||||
instance.animTime = std::fmod(instance.animTime, std::max(1.0f, instance.animDuration));
|
instance.animTime = std::fmod(instance.animTime, std::max(1.0f, instance.animDuration));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Idle variation timer — occasionally play a different idle sequence
|
// Idle variation timer
|
||||||
if (!instance.playingVariation && model.idleVariationIndices.size() > 1) {
|
if (!instance.playingVariation && model.idleVariationIndices.size() > 1) {
|
||||||
instance.variationTimer -= dtMs;
|
instance.variationTimer -= dtMs;
|
||||||
if (instance.variationTimer <= 0.0f) {
|
if (instance.variationTimer <= 0.0f) {
|
||||||
|
|
@ -1957,19 +1989,11 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frustum + distance cull: skip expensive bone computation for off-screen instances.
|
// Frustum + distance cull: skip expensive bone computation for off-screen instances.
|
||||||
// Keep thresholds aligned with render culling so visible distant ambient actors
|
float worldRadius = instance.cachedBoundRadius * instance.scale;
|
||||||
// (fish/seagulls/etc.) continue animating instead of freezing in idle poses.
|
|
||||||
float worldRadius = model.boundRadius * instance.scale;
|
|
||||||
float cullRadius = worldRadius;
|
float cullRadius = worldRadius;
|
||||||
if (model.disableAnimation) {
|
|
||||||
cullRadius = std::max(cullRadius, 3.0f);
|
|
||||||
}
|
|
||||||
glm::vec3 toCam = instance.position - cachedCamPos_;
|
glm::vec3 toCam = instance.position - cachedCamPos_;
|
||||||
float distSq = glm::dot(toCam, toCam);
|
float distSq = glm::dot(toCam, toCam);
|
||||||
float effectiveMaxDistSq = cachedMaxRenderDistSq_ * std::max(1.0f, cullRadius / 12.0f);
|
float effectiveMaxDistSq = cachedMaxRenderDistSq_ * std::max(1.0f, cullRadius / 12.0f);
|
||||||
if (model.disableAnimation) {
|
|
||||||
effectiveMaxDistSq *= 2.6f;
|
|
||||||
}
|
|
||||||
if (distSq > effectiveMaxDistSq) continue;
|
if (distSq > effectiveMaxDistSq) continue;
|
||||||
float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f);
|
float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f);
|
||||||
if (cullRadius > 0.0f && !updateFrustum.intersectsSphere(instance.position, paddedRadius)) continue;
|
if (cullRadius > 0.0f && !updateFrustum.intersectsSphere(instance.position, paddedRadius)) continue;
|
||||||
|
|
@ -2041,21 +2065,20 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3: Particle update (sequential — uses RNG, not thread-safe)
|
// Phase 3: Particle update (sequential — uses RNG, not thread-safe)
|
||||||
// Run for ALL nearby instances with particle emitters, not just those in
|
// Only iterate instances that have particle emitters (pre-built list).
|
||||||
// boneWorkIndices_, so particles keep animating even when bone updates are culled.
|
for (size_t idx : particleInstanceIndices_) {
|
||||||
for (size_t idx = 0; idx < instances.size(); ++idx) {
|
if (idx >= instances.size()) continue;
|
||||||
auto& instance = instances[idx];
|
auto& instance = instances[idx];
|
||||||
auto mdlIt = models.find(instance.modelId);
|
|
||||||
if (mdlIt == models.end()) continue;
|
|
||||||
const auto& model = mdlIt->second;
|
|
||||||
if (model.particleEmitters.empty()) continue;
|
|
||||||
// Distance cull: only update particles within visible range
|
// Distance cull: only update particles within visible range
|
||||||
glm::vec3 toCam = instance.position - cachedCamPos_;
|
glm::vec3 toCam = instance.position - cachedCamPos_;
|
||||||
float distSq = glm::dot(toCam, toCam);
|
float distSq = glm::dot(toCam, toCam);
|
||||||
if (distSq > cachedMaxRenderDistSq_) continue;
|
if (distSq > cachedMaxRenderDistSq_) continue;
|
||||||
emitParticles(instance, model, deltaTime);
|
auto mdlIt = models.find(instance.modelId);
|
||||||
|
if (mdlIt == models.end()) continue;
|
||||||
|
emitParticles(instance, mdlIt->second, deltaTime);
|
||||||
updateParticles(instance, deltaTime);
|
updateParticles(instance, deltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) {
|
void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) {
|
||||||
|
|
@ -3168,6 +3191,11 @@ void M2Renderer::setInstancePosition(uint32_t instanceId, const glm::vec3& posit
|
||||||
auto idxIt = instanceIndexById.find(instanceId);
|
auto idxIt = instanceIndexById.find(instanceId);
|
||||||
if (idxIt == instanceIndexById.end()) return;
|
if (idxIt == instanceIndexById.end()) return;
|
||||||
auto& inst = instances[idxIt->second];
|
auto& inst = instances[idxIt->second];
|
||||||
|
|
||||||
|
// Save old grid cells
|
||||||
|
GridCell oldMinCell = toCell(inst.worldBoundsMin);
|
||||||
|
GridCell oldMaxCell = toCell(inst.worldBoundsMax);
|
||||||
|
|
||||||
inst.position = position;
|
inst.position = position;
|
||||||
inst.updateModelMatrix();
|
inst.updateModelMatrix();
|
||||||
auto modelIt = models.find(inst.modelId);
|
auto modelIt = models.find(inst.modelId);
|
||||||
|
|
@ -3176,7 +3204,31 @@ void M2Renderer::setInstancePosition(uint32_t instanceId, const glm::vec3& posit
|
||||||
getTightCollisionBounds(modelIt->second, localMin, localMax);
|
getTightCollisionBounds(modelIt->second, localMin, localMax);
|
||||||
transformAABB(inst.modelMatrix, localMin, localMax, inst.worldBoundsMin, inst.worldBoundsMax);
|
transformAABB(inst.modelMatrix, localMin, localMax, inst.worldBoundsMin, inst.worldBoundsMax);
|
||||||
}
|
}
|
||||||
spatialIndexDirty_ = true;
|
|
||||||
|
// Incrementally update spatial grid
|
||||||
|
GridCell newMinCell = toCell(inst.worldBoundsMin);
|
||||||
|
GridCell newMaxCell = toCell(inst.worldBoundsMax);
|
||||||
|
if (oldMinCell.x != newMinCell.x || oldMinCell.y != newMinCell.y || oldMinCell.z != newMinCell.z ||
|
||||||
|
oldMaxCell.x != newMaxCell.x || oldMaxCell.y != newMaxCell.y || oldMaxCell.z != newMaxCell.z) {
|
||||||
|
for (int z = oldMinCell.z; z <= oldMaxCell.z; z++) {
|
||||||
|
for (int y = oldMinCell.y; y <= oldMaxCell.y; y++) {
|
||||||
|
for (int x = oldMinCell.x; x <= oldMaxCell.x; x++) {
|
||||||
|
auto it = spatialGrid.find(GridCell{x, y, z});
|
||||||
|
if (it != spatialGrid.end()) {
|
||||||
|
auto& vec = it->second;
|
||||||
|
vec.erase(std::remove(vec.begin(), vec.end(), instanceId), vec.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int z = newMinCell.z; z <= newMaxCell.z; z++) {
|
||||||
|
for (int y = newMinCell.y; y <= newMaxCell.y; y++) {
|
||||||
|
for (int x = newMinCell.x; x <= newMaxCell.x; x++) {
|
||||||
|
spatialGrid[GridCell{x, y, z}].push_back(instanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void M2Renderer::setInstanceAnimationFrozen(uint32_t instanceId, bool frozen) {
|
void M2Renderer::setInstanceAnimationFrozen(uint32_t instanceId, bool frozen) {
|
||||||
|
|
@ -3194,6 +3246,10 @@ void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& tran
|
||||||
if (idxIt == instanceIndexById.end()) return;
|
if (idxIt == instanceIndexById.end()) return;
|
||||||
auto& inst = instances[idxIt->second];
|
auto& inst = instances[idxIt->second];
|
||||||
|
|
||||||
|
// Remove old grid cells before updating bounds
|
||||||
|
GridCell oldMinCell = toCell(inst.worldBoundsMin);
|
||||||
|
GridCell oldMaxCell = toCell(inst.worldBoundsMax);
|
||||||
|
|
||||||
// Update model matrix directly
|
// Update model matrix directly
|
||||||
inst.modelMatrix = transform;
|
inst.modelMatrix = transform;
|
||||||
inst.invModelMatrix = glm::inverse(transform);
|
inst.invModelMatrix = glm::inverse(transform);
|
||||||
|
|
@ -3208,7 +3264,34 @@ void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& tran
|
||||||
getTightCollisionBounds(modelIt->second, localMin, localMax);
|
getTightCollisionBounds(modelIt->second, localMin, localMax);
|
||||||
transformAABB(inst.modelMatrix, localMin, localMax, inst.worldBoundsMin, inst.worldBoundsMax);
|
transformAABB(inst.modelMatrix, localMin, localMax, inst.worldBoundsMin, inst.worldBoundsMax);
|
||||||
}
|
}
|
||||||
spatialIndexDirty_ = true;
|
|
||||||
|
// Incrementally update spatial grid (remove old cells, add new cells)
|
||||||
|
GridCell newMinCell = toCell(inst.worldBoundsMin);
|
||||||
|
GridCell newMaxCell = toCell(inst.worldBoundsMax);
|
||||||
|
if (oldMinCell.x != newMinCell.x || oldMinCell.y != newMinCell.y || oldMinCell.z != newMinCell.z ||
|
||||||
|
oldMaxCell.x != newMaxCell.x || oldMaxCell.y != newMaxCell.y || oldMaxCell.z != newMaxCell.z) {
|
||||||
|
// Remove from old cells
|
||||||
|
for (int z = oldMinCell.z; z <= oldMaxCell.z; z++) {
|
||||||
|
for (int y = oldMinCell.y; y <= oldMaxCell.y; y++) {
|
||||||
|
for (int x = oldMinCell.x; x <= oldMaxCell.x; x++) {
|
||||||
|
auto it = spatialGrid.find(GridCell{x, y, z});
|
||||||
|
if (it != spatialGrid.end()) {
|
||||||
|
auto& vec = it->second;
|
||||||
|
vec.erase(std::remove(vec.begin(), vec.end(), instanceId), vec.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add to new cells
|
||||||
|
for (int z = newMinCell.z; z <= newMaxCell.z; z++) {
|
||||||
|
for (int y = newMinCell.y; y <= newMaxCell.y; y++) {
|
||||||
|
for (int x = newMinCell.x; x <= newMaxCell.x; x++) {
|
||||||
|
spatialGrid[GridCell{x, y, z}].push_back(instanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No spatialIndexDirty_ = true — handled incrementally
|
||||||
}
|
}
|
||||||
|
|
||||||
void M2Renderer::removeInstance(uint32_t instanceId) {
|
void M2Renderer::removeInstance(uint32_t instanceId) {
|
||||||
|
|
@ -3289,6 +3372,10 @@ void M2Renderer::clear() {
|
||||||
spatialGrid.clear();
|
spatialGrid.clear();
|
||||||
instanceIndexById.clear();
|
instanceIndexById.clear();
|
||||||
smokeParticles.clear();
|
smokeParticles.clear();
|
||||||
|
smokeInstanceIndices_.clear();
|
||||||
|
animatedInstanceIndices_.clear();
|
||||||
|
particleOnlyInstanceIndices_.clear();
|
||||||
|
particleInstanceIndices_.clear();
|
||||||
smokeEmitAccum = 0.0f;
|
smokeEmitAccum = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3320,11 +3407,27 @@ void M2Renderer::rebuildSpatialIndex() {
|
||||||
spatialGrid.clear();
|
spatialGrid.clear();
|
||||||
instanceIndexById.clear();
|
instanceIndexById.clear();
|
||||||
instanceIndexById.reserve(instances.size());
|
instanceIndexById.reserve(instances.size());
|
||||||
|
smokeInstanceIndices_.clear();
|
||||||
|
animatedInstanceIndices_.clear();
|
||||||
|
particleOnlyInstanceIndices_.clear();
|
||||||
|
particleInstanceIndices_.clear();
|
||||||
|
|
||||||
for (size_t i = 0; i < instances.size(); i++) {
|
for (size_t i = 0; i < instances.size(); i++) {
|
||||||
const auto& inst = instances[i];
|
const auto& inst = instances[i];
|
||||||
instanceIndexById[inst.id] = i;
|
instanceIndexById[inst.id] = i;
|
||||||
|
|
||||||
|
if (inst.cachedIsSmoke) {
|
||||||
|
smokeInstanceIndices_.push_back(i);
|
||||||
|
}
|
||||||
|
if (inst.cachedHasParticleEmitters) {
|
||||||
|
particleInstanceIndices_.push_back(i);
|
||||||
|
}
|
||||||
|
if (inst.cachedHasAnimation && !inst.cachedDisableAnimation) {
|
||||||
|
animatedInstanceIndices_.push_back(i);
|
||||||
|
} else if (inst.cachedHasParticleEmitters) {
|
||||||
|
particleOnlyInstanceIndices_.push_back(i);
|
||||||
|
}
|
||||||
|
|
||||||
GridCell minCell = toCell(inst.worldBoundsMin);
|
GridCell minCell = toCell(inst.worldBoundsMin);
|
||||||
GridCell maxCell = toCell(inst.worldBoundsMax);
|
GridCell maxCell = toCell(inst.worldBoundsMax);
|
||||||
for (int z = minCell.z; z <= maxCell.z; z++) {
|
for (int z = minCell.z; z <= maxCell.z; z++) {
|
||||||
|
|
|
||||||
|
|
@ -3346,7 +3346,6 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||||
|
|
||||||
auto renderEnd = std::chrono::steady_clock::now();
|
auto renderEnd = std::chrono::steady_clock::now();
|
||||||
lastRenderMs = std::chrono::duration<double, std::milli>(renderEnd - renderStart).count();
|
lastRenderMs = std::chrono::duration<double, std::milli>(renderEnd - renderStart).count();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initPostProcess(), resizePostProcess(), shutdownPostProcess() removed —
|
// initPostProcess(), resizePostProcess(), shutdownPostProcess() removed —
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue