Fix NPC apparel fallback and reduce world-entry stutter

Hide NPC cloak/object-skin mesh when no cape texture resolves by using a transparent texture fallback, preventing skin-texture bleed on cloaks. Tighten NPC equipment region compositing by slot and add safe humanoid geoset selection to avoid robe-over-pants conflicts and odd pants texturing.

Reduce login/runtime hitching by deferring non-critical world-system initialization across frames, lowering per-frame transport doodad spawn budget, and demoting high-volume transport/MO_TRANSPORT diagnostics to debug. Gate M2 glow diagnostics behind WOWEE_M2_GLOW_DIAG and make zone music prewarm opt-in via WOWEE_PREWARM_ZONE_MUSIC.
This commit is contained in:
Kelsi 2026-02-20 20:31:04 -08:00
parent 48d9de810d
commit 3368dbb9ec
10 changed files with 369 additions and 91 deletions

View file

@ -303,6 +303,14 @@ bool CharacterRenderer::initialize() {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// Create 1x1 transparent fallback texture for hidden texture slots.
uint8_t transparent[] = { 0, 0, 0, 0 };
glGenTextures(1, &transparentTexture);
glBindTexture(GL_TEXTURE_2D, transparentTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, transparent);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
// Diagnostics-only: cache lifetime is currently tied to renderer lifetime.
@ -345,6 +353,10 @@ void CharacterRenderer::shutdown() {
glDeleteTextures(1, &whiteTexture);
whiteTexture = 0;
}
if (transparentTexture) {
glDeleteTextures(1, &transparentTexture);
transparentTexture = 0;
}
models.clear();
instances.clear();

View file

@ -15,6 +15,7 @@
#include <functional>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <future>
#include <thread>
@ -24,6 +25,16 @@ namespace rendering {
namespace {
bool envFlagEnabled(const char* key, bool defaultValue) {
const char* raw = std::getenv(key);
if (!raw || !*raw) return defaultValue;
std::string v(raw);
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return !(v == "0" || v == "false" || v == "off" || v == "no");
}
static constexpr uint32_t kParticleFlagRandomized = 0x40;
static constexpr uint32_t kParticleFlagTiled = 0x80;
@ -1248,19 +1259,21 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
}
}
// Diagnostic: log batch details for light/lamp models to debug glow rendering
if (lowerName.find("light") != std::string::npos ||
lowerName.find("lamp") != std::string::npos ||
lowerName.find("lantern") != std::string::npos) {
LOG_INFO("M2 GLOW DIAG '", model.name, "' batch ", gpuModel.batches.size(),
": blend=", bgpu.blendMode, " matFlags=0x",
std::hex, bgpu.materialFlags, std::dec,
" colorKey=", bgpu.colorKeyBlack ? "Y" : "N",
" hasAlpha=", bgpu.hasAlpha ? "Y" : "N",
" unlit=", (bgpu.materialFlags & 0x01) ? "Y" : "N",
" glowSize=", bgpu.glowSize,
" tex=", bgpu.texture,
" idxCount=", bgpu.indexCount);
// Optional diagnostics for glow/light batches (disabled by default).
static const bool kGlowDiag = envFlagEnabled("WOWEE_M2_GLOW_DIAG", false);
if (kGlowDiag &&
(lowerName.find("light") != std::string::npos ||
lowerName.find("lamp") != std::string::npos ||
lowerName.find("lantern") != std::string::npos)) {
LOG_DEBUG("M2 GLOW DIAG '", model.name, "' batch ", gpuModel.batches.size(),
": blend=", bgpu.blendMode, " matFlags=0x",
std::hex, bgpu.materialFlags, std::dec,
" colorKey=", bgpu.colorKeyBlack ? "Y" : "N",
" hasAlpha=", bgpu.hasAlpha ? "Y" : "N",
" unlit=", (bgpu.materialFlags & 0x01) ? "Y" : "N",
" glowSize=", bgpu.glowSize,
" tex=", bgpu.texture,
" idxCount=", bgpu.indexCount);
}
gpuModel.batches.push_back(bgpu);
}

View file

@ -57,6 +57,7 @@
#include <cctype>
#include <cmath>
#include <chrono>
#include <cstdlib>
#include <optional>
#include <unordered_map>
#include <unordered_set>
@ -80,6 +81,16 @@ static std::unordered_map<std::string, EmoteInfo> EMOTE_TABLE;
static std::unordered_map<uint32_t, const EmoteInfo*> EMOTE_BY_DBCID; // reverse lookup: dbcId → EmoteInfo*
static bool emoteTableLoaded = false;
static bool envFlagEnabled(const char* key, bool defaultValue) {
const char* raw = std::getenv(key);
if (!raw || !*raw) return defaultValue;
std::string v(raw);
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return !(v == "0" || v == "false" || v == "off" || v == "no");
}
static std::vector<std::string> parseEmoteCommands(const std::string& raw) {
std::vector<std::string> out;
std::string cur;
@ -250,6 +261,7 @@ Renderer::~Renderer() = default;
bool Renderer::initialize(core::Window* win) {
window = win;
deferredWorldInitEnabled_ = envFlagEnabled("WOWEE_DEFER_WORLD_SYSTEMS", true);
LOG_INFO("Initializing renderer");
// Create camera (in front of Stormwind gate, looking north)
@ -1909,6 +1921,7 @@ void Renderer::update(float deltaTime) {
if (musicSwitchCooldown_ > 0.0f) {
musicSwitchCooldown_ = std::max(0.0f, musicSwitchCooldown_ - deltaTime);
}
runDeferredWorldInitStep(deltaTime);
auto updateStart = std::chrono::steady_clock::now();
lastDeltaTime_ = deltaTime; // Cache for use in updateCharacterAnimation()
@ -2455,6 +2468,46 @@ void Renderer::update(float deltaTime) {
}
}
void Renderer::runDeferredWorldInitStep(float deltaTime) {
if (!deferredWorldInitEnabled_ || !deferredWorldInitPending_ || !cachedAssetManager) return;
if (deferredWorldInitCooldown_ > 0.0f) {
deferredWorldInitCooldown_ = std::max(0.0f, deferredWorldInitCooldown_ - deltaTime);
if (deferredWorldInitCooldown_ > 0.0f) return;
}
switch (deferredWorldInitStage_) {
case 0:
if (ambientSoundManager) {
ambientSoundManager->initialize(cachedAssetManager);
}
if (terrainManager && ambientSoundManager) {
terrainManager->setAmbientSoundManager(ambientSoundManager.get());
}
break;
case 1:
if (uiSoundManager) uiSoundManager->initialize(cachedAssetManager);
break;
case 2:
if (combatSoundManager) combatSoundManager->initialize(cachedAssetManager);
break;
case 3:
if (spellSoundManager) spellSoundManager->initialize(cachedAssetManager);
break;
case 4:
if (movementSoundManager) movementSoundManager->initialize(cachedAssetManager);
break;
case 5:
if (questMarkerRenderer) questMarkerRenderer->initialize(cachedAssetManager);
break;
default:
deferredWorldInitPending_ = false;
return;
}
deferredWorldInitStage_++;
deferredWorldInitCooldown_ = 0.12f;
}
// ============================================================
// Selection Circle
// ============================================================
@ -3197,39 +3250,46 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
if (npcVoiceManager) {
npcVoiceManager->initialize(assetManager);
}
if (ambientSoundManager) {
ambientSoundManager->initialize(assetManager);
}
if (uiSoundManager) {
uiSoundManager->initialize(assetManager);
}
if (combatSoundManager) {
combatSoundManager->initialize(assetManager);
}
if (spellSoundManager) {
spellSoundManager->initialize(assetManager);
}
if (movementSoundManager) {
movementSoundManager->initialize(assetManager);
}
if (questMarkerRenderer) {
questMarkerRenderer->initialize(assetManager);
}
// Prewarm frequently used zone/tavern music so zone transitions don't stall on MPQ I/O.
if (zoneManager) {
for (const auto& musicPath : zoneManager->getAllMusicPaths()) {
musicManager->preloadMusic(musicPath);
if (!deferredWorldInitEnabled_) {
if (ambientSoundManager) {
ambientSoundManager->initialize(assetManager);
}
}
static const std::vector<std::string> tavernTracks = {
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance01.mp3",
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance02.mp3",
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern1A.mp3",
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3",
};
for (const auto& musicPath : tavernTracks) {
musicManager->preloadMusic(musicPath);
if (uiSoundManager) {
uiSoundManager->initialize(assetManager);
}
if (combatSoundManager) {
combatSoundManager->initialize(assetManager);
}
if (spellSoundManager) {
spellSoundManager->initialize(assetManager);
}
if (movementSoundManager) {
movementSoundManager->initialize(assetManager);
}
if (questMarkerRenderer) {
questMarkerRenderer->initialize(assetManager);
}
if (envFlagEnabled("WOWEE_PREWARM_ZONE_MUSIC", false)) {
if (zoneManager) {
for (const auto& musicPath : zoneManager->getAllMusicPaths()) {
musicManager->preloadMusic(musicPath);
}
}
static const std::vector<std::string> tavernTracks = {
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance01.mp3",
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance02.mp3",
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern1A.mp3",
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3",
};
for (const auto& musicPath : tavernTracks) {
musicManager->preloadMusic(musicPath);
}
}
} else {
deferredWorldInitPending_ = true;
deferredWorldInitStage_ = 0;
deferredWorldInitCooldown_ = 0.25f;
}
cachedAssetManager = assetManager;
@ -3316,23 +3376,29 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent
if (npcVoiceManager && cachedAssetManager) {
npcVoiceManager->initialize(cachedAssetManager);
}
if (ambientSoundManager && cachedAssetManager) {
ambientSoundManager->initialize(cachedAssetManager);
}
if (uiSoundManager && cachedAssetManager) {
uiSoundManager->initialize(cachedAssetManager);
}
if (combatSoundManager && cachedAssetManager) {
combatSoundManager->initialize(cachedAssetManager);
}
if (spellSoundManager && cachedAssetManager) {
spellSoundManager->initialize(cachedAssetManager);
}
if (movementSoundManager && cachedAssetManager) {
movementSoundManager->initialize(cachedAssetManager);
}
if (questMarkerRenderer && cachedAssetManager) {
questMarkerRenderer->initialize(cachedAssetManager);
if (!deferredWorldInitEnabled_) {
if (ambientSoundManager && cachedAssetManager) {
ambientSoundManager->initialize(cachedAssetManager);
}
if (uiSoundManager && cachedAssetManager) {
uiSoundManager->initialize(cachedAssetManager);
}
if (combatSoundManager && cachedAssetManager) {
combatSoundManager->initialize(cachedAssetManager);
}
if (spellSoundManager && cachedAssetManager) {
spellSoundManager->initialize(cachedAssetManager);
}
if (movementSoundManager && cachedAssetManager) {
movementSoundManager->initialize(cachedAssetManager);
}
if (questMarkerRenderer && cachedAssetManager) {
questMarkerRenderer->initialize(cachedAssetManager);
}
} else {
deferredWorldInitPending_ = true;
deferredWorldInitStage_ = 0;
deferredWorldInitCooldown_ = 0.1f;
}
// Wire ambient sound manager to terrain manager for emitter registration