From 585d0bf50ed7142a8677d665745035344b2a8958 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Mar 2026 18:02:56 -0800 Subject: [PATCH] Instance portal glow, spin, and transparent additive rendering --- include/rendering/m2_renderer.hpp | 4 +++ src/core/application.cpp | 1 + src/rendering/m2_renderer.cpp | 47 +++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 6b8ebc15..62e984fd 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -112,6 +112,7 @@ struct M2ModelGPU { bool hasAnimation = false; // True if any bone has keyframes bool isSmoke = false; // True for smoke models (UV scroll animation) bool isSpellEffect = false; // True for spell effect models (skip particle dampeners) + bool isInstancePortal = false; // Instance portal model (spin + glow) bool disableAnimation = false; // Keep foliage/tree doodads visually stable bool shadowWindFoliage = false; // Apply wind sway in shadow pass for foliage/tree cards bool isFoliageLike = false; // Model name matches foliage/tree/bush/grass etc (precomputed) @@ -181,9 +182,11 @@ struct M2Instance { bool cachedHasParticleEmitters = false; bool cachedIsGroundDetail = false; bool cachedIsInvisibleTrap = false; + bool cachedIsInstancePortal = false; bool cachedIsValid = false; bool skipCollision = false; // WMO interior doodads — skip player wall collision float cachedBoundRadius = 0.0f; + float portalSpinAngle = 0.0f; // Accumulated spin angle for portal rotation // Frame-skip optimization (update distant animations less frequently) uint8_t frameSkipCounter = 0; @@ -476,6 +479,7 @@ private: // Smoke particle system std::vector smokeParticles; std::vector smokeInstanceIndices_; // Indices into instances[] for smoke emitters + std::vector portalInstanceIndices_; // Indices into instances[] for spinning portals static constexpr int MAX_SMOKE_PARTICLES = 1000; float smokeEmitAccum = 0.0f; std::mt19937 smokeRng{42}; diff --git a/src/core/application.cpp b/src/core/application.cpp index d99f99f1..8450dfb8 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -6568,6 +6568,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t std::string lowerPath = modelPath; std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), ::tolower); bool isAnimatedEffect = (lowerPath.find("instanceportal") != std::string::npos || + lowerPath.find("instancenewportal") != std::string::npos || lowerPath.find("portalfx") != std::string::npos || lowerPath.find("spellportal") != std::string::npos); if (!isAnimatedEffect) { diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 0998694a..690a5234 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1122,6 +1122,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("lightshaft") != std::string::npos) || (lowerName.find("volumetriclight") != std::string::npos) || (lowerName.find("instanceportal") != std::string::npos) || + (lowerName.find("instancenewportal") != std::string::npos) || (lowerName.find("mageportal") != std::string::npos) || (lowerName.find("worldtreeportal") != std::string::npos) || (lowerName.find("particleemitter") != std::string::npos) || @@ -1134,6 +1135,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { gpuModel.isSpellEffect = effectByName || (hasParticles && model.vertices.size() <= 200 && model.particleEmitters.size() >= 3); + gpuModel.isInstancePortal = + (lowerName.find("instanceportal") != std::string::npos) || + (lowerName.find("instancenewportal") != std::string::npos) || + (lowerName.find("portalfx") != std::string::npos) || + (lowerName.find("spellportal") != std::string::npos); + // Instance portals are spell effects too (additive blend, no collision) + if (gpuModel.isInstancePortal) { + gpuModel.isSpellEffect = true; + } // Water vegetation: cattails, reeds, bulrushes, kelp, seaweed, lilypad near water gpuModel.isWaterVegetation = (lowerName.find("cattail") != std::string::npos) || @@ -1634,6 +1644,7 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, instance.cachedBoundRadius = mdlRef.boundRadius; instance.cachedIsGroundDetail = mdlRef.isGroundDetail; instance.cachedIsInvisibleTrap = mdlRef.isInvisibleTrap; + instance.cachedIsInstancePortal = mdlRef.isInstancePortal; instance.cachedIsValid = mdlRef.isValid(); // Initialize animation: play first sequence (usually Stand/Idle) @@ -1652,6 +1663,9 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, if (mdlRef.isSmoke) { smokeInstanceIndices_.push_back(idx); } + if (mdlRef.isInstancePortal) { + portalInstanceIndices_.push_back(idx); + } if (!mdlRef.particleEmitters.empty()) { particleInstanceIndices_.push_back(idx); } @@ -1941,6 +1955,18 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: ++i; } + // --- Spin instance portals --- + static constexpr float PORTAL_SPIN_SPEED = 1.2f; // radians/sec + for (size_t idx : portalInstanceIndices_) { + if (idx >= instances.size()) continue; + auto& inst = instances[idx]; + inst.portalSpinAngle += PORTAL_SPIN_SPEED * deltaTime; + if (inst.portalSpinAngle > 6.2831853f) + inst.portalSpinAngle -= 6.2831853f; + inst.rotation.z = inst.portalSpinAngle; + inst.updateModelMatrix(); + } + // --- Normal M2 animation update --- // Advance animTime for ALL instances (needed for texture UV animation on static doodads). // This is a tight loop touching only one float per instance — no hash lookups. @@ -2251,6 +2277,22 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const if (model.isGroundDetail) { instanceFadeAlpha *= 0.82f; } + if (model.isInstancePortal) { + // Render mesh at low alpha + emit glow sprite at center + instanceFadeAlpha *= 0.12f; + if (entry.distSq < 400.0f * 400.0f) { + glm::vec3 center = glm::vec3(instance.modelMatrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + GlowSprite gs; + gs.worldPos = center; + gs.color = glm::vec4(0.35f, 0.5f, 1.0f, 1.1f); + gs.size = instance.scale * 5.0f; + glowSprites_.push_back(gs); + GlowSprite halo = gs; + halo.color.a *= 0.3f; + halo.size *= 2.2f; + glowSprites_.push_back(halo); + } + } // Upload bone matrices to SSBO if model has skeletal animation bool useBones = model.hasAnimation && !model.disableAnimation && !instance.boneMatrices.empty(); @@ -3419,6 +3461,7 @@ void M2Renderer::clear() { instanceIndexById.clear(); smokeParticles.clear(); smokeInstanceIndices_.clear(); + portalInstanceIndices_.clear(); animatedInstanceIndices_.clear(); particleOnlyInstanceIndices_.clear(); particleInstanceIndices_.clear(); @@ -3454,6 +3497,7 @@ void M2Renderer::rebuildSpatialIndex() { instanceIndexById.clear(); instanceIndexById.reserve(instances.size()); smokeInstanceIndices_.clear(); + portalInstanceIndices_.clear(); animatedInstanceIndices_.clear(); particleOnlyInstanceIndices_.clear(); particleInstanceIndices_.clear(); @@ -3465,6 +3509,9 @@ void M2Renderer::rebuildSpatialIndex() { if (inst.cachedIsSmoke) { smokeInstanceIndices_.push_back(i); } + if (inst.cachedIsInstancePortal) { + portalInstanceIndices_.push_back(i); + } if (inst.cachedHasParticleEmitters) { particleInstanceIndices_.push_back(i); }