From 84b04446c1e55ad0438fbb212a3858dcdb479295 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 4 Mar 2026 09:19:02 -0800 Subject: [PATCH] Per-instance NPC hair/skin textures, fix binary search float comparison - NPC hair/skin textures now use per-instance overrides instead of shared model-level textures, so each NPC shows its own hair color/style - Hair/skin DBC lookup runs for every NPC instance (including cached models) rather than only on first load - Fix keyframe binary search to use float comparison matching original linear scan semantics --- src/core/application.cpp | 71 ++++++++++++++++++++++++++++ src/rendering/character_renderer.cpp | 6 +-- src/rendering/m2_renderer.cpp | 6 +-- 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/core/application.cpp b/src/core/application.cpp index 2fa4b360..da49887d 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -4790,6 +4790,77 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x return; } + // Per-instance hair/skin texture overrides — runs for ALL NPCs (including cached models) + // so that each NPC gets its own hair/skin color regardless of model sharing. + { + auto itDD = displayDataMap_.find(displayId); + if (itDD != displayDataMap_.end() && itDD->second.extraDisplayId != 0) { + auto itExtra2 = humanoidExtraMap_.find(itDD->second.extraDisplayId); + if (itExtra2 != humanoidExtraMap_.end()) { + const auto& extra = itExtra2->second; + const auto* md = charRenderer->getModelData(modelId); + if (md) { + auto charSectionsDbc2 = assetManager->loadDBC("CharSections.dbc"); + if (charSectionsDbc2) { + const auto* csL = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + uint32_t tgtRace = static_cast(extra.raceId); + uint32_t tgtSex = static_cast(extra.sexId); + + // Look up hair texture (section 3) + for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) { + uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1); + uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2); + if (rId != tgtRace || sId != tgtSex) continue; + uint32_t sec = charSectionsDbc2->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); + if (sec != 3) continue; + uint32_t var = charSectionsDbc2->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t col = charSectionsDbc2->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + if (var != static_cast(extra.hairStyleId)) continue; + if (col != static_cast(extra.hairColorId)) continue; + std::string hairPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6); + if (!hairPath.empty()) { + rendering::VkTexture* hairTex = charRenderer->loadTexture(hairPath); + if (hairTex) { + for (size_t ti = 0; ti < md->textures.size(); ti++) { + if (md->textures[ti].type == 6) { + charRenderer->setTextureSlotOverride(instanceId, static_cast(ti), hairTex); + } + } + } + } + break; + } + + // Look up skin texture (section 0) for per-instance skin color + for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) { + uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1); + uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2); + if (rId != tgtRace || sId != tgtSex) continue; + uint32_t sec = charSectionsDbc2->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); + if (sec != 0) continue; + uint32_t col = charSectionsDbc2->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + if (col != static_cast(extra.skinId)) continue; + std::string skinPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6); + if (!skinPath.empty()) { + rendering::VkTexture* skinTex = charRenderer->loadTexture(skinPath); + if (skinTex) { + for (size_t ti = 0; ti < md->textures.size(); ti++) { + uint32_t tt = md->textures[ti].type; + if (tt == 1 || tt == 11) { + charRenderer->setTextureSlotOverride(instanceId, static_cast(ti), skinTex); + } + } + } + } + break; + } + } + } + } + } + } + // Optional humanoid NPC geoset mask. Disabled by default because forcing geosets // causes long-standing visual artifacts on some models (missing waist, phantom // bracers, flickering apron overlays). Prefer model defaults. diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 0c52e43b..11fa2ae5 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -1580,9 +1580,9 @@ int CharacterRenderer::findKeyframeIndex(const std::vector& timestamps if (timestamps.empty()) return -1; if (timestamps.size() == 1) return 0; - // Binary search: find first element > t, then back up one - uint32_t t = static_cast(time); - auto it = std::upper_bound(timestamps.begin(), timestamps.end(), t); + // Binary search using float comparison to match original semantics exactly + auto it = std::upper_bound(timestamps.begin(), timestamps.end(), time, + [](float t, uint32_t ts) { return t < static_cast(ts); }); if (it == timestamps.begin()) return 0; size_t idx = static_cast(it - timestamps.begin()) - 1; return static_cast(std::min(idx, timestamps.size() - 2)); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index edf02243..2ea99420 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1741,9 +1741,9 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& static int findKeyframeIndex(const std::vector& timestamps, float time) { if (timestamps.empty()) return -1; if (timestamps.size() == 1) return 0; - uint32_t t = static_cast(time); - // Binary search: find first element > t, then back up one - auto it = std::upper_bound(timestamps.begin(), timestamps.end(), t); + // Binary search using float comparison to match original semantics exactly + auto it = std::upper_bound(timestamps.begin(), timestamps.end(), time, + [](float t, uint32_t ts) { return t < static_cast(ts); }); if (it == timestamps.begin()) return 0; size_t idx = static_cast(it - timestamps.begin()) - 1; return static_cast(std::min(idx, timestamps.size() - 2));