Add 3D level-up effect using LevelUp.m2 spell model

Replace 2D screen-space ding rings with real WoW LevelUp.m2 particle/geometry
effect. Fix FBlock particle color parsing (C3Vector floats, not CImVector bytes)
which was producing blue/red instead of golden yellow. Spell effect models bypass
particle dampeners, glow sprite conversion, Mod→Additive blend override, and all
collision (floor/wall/camera) to prevent camera zoom-in. Other players' level-ups
trigger the 3D effect at their position with group chat notification. F7 hotkey
for testing.
This commit is contained in:
Kelsi 2026-02-19 20:36:25 -08:00
parent 1fb1daea7f
commit da49593268
13 changed files with 321 additions and 128 deletions

View file

@ -16,6 +16,7 @@
#include "rendering/sky_system.hpp"
#include "rendering/swim_effects.hpp"
#include "rendering/mount_dust.hpp"
#include "rendering/levelup_effect.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/m2_renderer.hpp"
@ -355,6 +356,9 @@ bool Renderer::initialize(core::Window* win) {
mountDust.reset();
}
// Create level-up effect (model loaded later via loadLevelUpEffect)
levelUpEffect = std::make_unique<LevelUpEffect>();
// Create character renderer
characterRenderer = std::make_unique<CharacterRenderer>();
if (!characterRenderer->initialize()) {
@ -1603,6 +1607,31 @@ void Renderer::cancelEmote() {
emoteLoop = false;
}
void Renderer::triggerLevelUpEffect(const glm::vec3& position) {
if (!levelUpEffect) return;
// Lazy-load the M2 model on first trigger
if (!levelUpEffect->isModelLoaded() && m2Renderer) {
if (!cachedAssetManager) {
cachedAssetManager = core::Application::getInstance().getAssetManager();
}
if (!cachedAssetManager) {
LOG_WARNING("LevelUpEffect: no asset manager available");
} else {
auto m2Data = cachedAssetManager->readFile("Spells\\LevelUp\\LevelUp.m2");
auto skinData = cachedAssetManager->readFile("Spells\\LevelUp\\LevelUp00.skin");
LOG_INFO("LevelUpEffect: m2Data=", m2Data.size(), " skinData=", skinData.size());
if (!m2Data.empty()) {
levelUpEffect->loadModel(m2Renderer.get(), m2Data, skinData);
} else {
LOG_WARNING("LevelUpEffect: failed to read Spell\\LevelUp\\LevelUp.m2");
}
}
}
levelUpEffect->trigger(position);
}
void Renderer::triggerMeleeSwing() {
if (!characterRenderer || characterInstanceId == 0) return;
if (meleeSwingCooldown > 0.0f) return;
@ -1975,6 +2004,11 @@ void Renderer::update(float deltaTime) {
}
}
}
// Update level-up effect
if (levelUpEffect) {
levelUpEffect->update(deltaTime);
}
auto sky2 = std::chrono::high_resolution_clock::now();
skyTime += std::chrono::duration<float, std::milli>(sky2 - sky1).count();