Fix NPC clothing geoset selection and cape visibility rules

- normalize humanoid NPC clothing geosets at spawn time to avoid conflicting overlays\n- force pants-first selection for group 13 to prevent robe/kilt meshes on trouser NPCs\n- hide cloak groups for NPCs unless a renderable cape texture can actually be resolved\n- avoid falling back to missing equipment texture paths during region compositing\n- stop model-scope type-2 cape texture writes that could leak cape/white textures across shared model IDs\n- add group texture override usage in character renderer draw path\n- update character preview equipment application to reuse preview applyEquipment path\n- keep hand-only keybone fallback for attachments to prevent helmet/anchor misbinds
This commit is contained in:
Kelsi 2026-02-20 21:50:32 -08:00
parent 7e1a463061
commit 40ce3ec97d
4 changed files with 474 additions and 243 deletions

View file

@ -8,6 +8,7 @@
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
#include <unordered_set>
namespace wowee {
@ -495,12 +496,15 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
std::string genderPath = base + genderSuffix;
std::string unisexPath = base + "_U.blp";
std::string fullPath;
std::string basePath = base + ".blp";
if (assetManager_->fileExists(genderPath)) {
fullPath = genderPath;
} else if (assetManager_->fileExists(unisexPath)) {
fullPath = unisexPath;
} else if (assetManager_->fileExists(basePath)) {
fullPath = basePath;
} else {
fullPath = base + ".blp";
continue;
}
regionLayers.emplace_back(region, fullPath);
}
@ -513,6 +517,91 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
}
}
// Cloak texture (group 15) is separate from body compositing.
if (hasInvType({16})) {
uint32_t capeDisplayId = findDisplayId({16});
if (capeDisplayId != 0) {
int32_t capeRecIdx = displayInfoDbc->findRecordById(capeDisplayId);
if (capeRecIdx >= 0) {
std::vector<std::string> capeNames;
auto addName = [&](const std::string& n) {
if (!n.empty() && std::find(capeNames.begin(), capeNames.end(), n) == capeNames.end()) {
capeNames.push_back(n);
}
};
std::string leftName = displayInfoDbc->getString(static_cast<uint32_t>(capeRecIdx), 3);
std::string rightName = displayInfoDbc->getString(static_cast<uint32_t>(capeRecIdx), 4);
if (gender_ == game::Gender::FEMALE) {
addName(rightName);
addName(leftName);
} else {
addName(leftName);
addName(rightName);
}
auto hasBlpExt = [](const std::string& p) {
if (p.size() < 4) return false;
std::string ext = p.substr(p.size() - 4);
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return ext == ".blp";
};
std::vector<std::string> candidates;
auto addCandidate = [&](const std::string& p) {
if (!p.empty() && std::find(candidates.begin(), candidates.end(), p) == candidates.end()) {
candidates.push_back(p);
}
};
for (const auto& nameRaw : capeNames) {
std::string name = nameRaw;
std::replace(name.begin(), name.end(), '/', '\\');
bool hasDir = (name.find('\\') != std::string::npos);
bool hasExt = hasBlpExt(name);
if (hasDir) {
addCandidate(name);
if (!hasExt) addCandidate(name + ".blp");
} else {
std::string baseObj = "Item\\ObjectComponents\\Cape\\" + name;
std::string baseTex = "Item\\TextureComponents\\Cape\\" + name;
addCandidate(baseObj);
addCandidate(baseTex);
if (!hasExt) {
addCandidate(baseObj + ".blp");
addCandidate(baseTex + ".blp");
}
addCandidate(baseObj + (gender_ == game::Gender::FEMALE ? "_F.blp" : "_M.blp"));
addCandidate(baseObj + "_U.blp");
addCandidate(baseTex + (gender_ == game::Gender::FEMALE ? "_F.blp" : "_M.blp"));
addCandidate(baseTex + "_U.blp");
}
}
const GLuint whiteTex = charRenderer_->loadTexture("");
for (const auto& c : candidates) {
GLuint capeTex = charRenderer_->loadTexture(c);
if (capeTex != 0 && capeTex != whiteTex) {
charRenderer_->setGroupTextureOverride(instanceId_, 15, capeTex);
if (const auto* md = charRenderer_->getModelData(PREVIEW_MODEL_ID)) {
for (size_t ti = 0; ti < md->textures.size(); ti++) {
if (md->textures[ti].type == 2) {
charRenderer_->setTextureSlotOverride(instanceId_, static_cast<uint16_t>(ti), capeTex);
}
}
}
break;
}
}
}
}
} else {
if (const auto* md = charRenderer_->getModelData(PREVIEW_MODEL_ID)) {
for (size_t ti = 0; ti < md->textures.size(); ti++) {
if (md->textures[ti].type == 2) {
charRenderer_->clearTextureSlotOverride(instanceId_, static_cast<uint16_t>(ti));
}
}
}
}
return true;
}

View file

@ -1587,6 +1587,11 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
// Resolve texture for this batch (prefer hair textures for hair geosets).
GLuint texId = resolveBatchTexture(instance, gpuModel, batch);
const uint16_t batchGroup = static_cast<uint16_t>(batch.submeshId / 100);
auto groupTexIt = instance.groupTextureOverrides.find(batchGroup);
if (groupTexIt != instance.groupTextureOverrides.end() && groupTexIt->second != 0) {
texId = groupTexIt->second;
}
// Respect M2 material blend mode for creature/character submeshes.
uint16_t blendMode = 0;
@ -1611,7 +1616,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
// Groups that share the body skin atlas: 0=body, 3=gloves, 4=boots, 5=chest,
// 8=wristbands, 9=pelvis, 13=pants. Hair (group 1) and facial hair (group 2) do NOT.
if (texId == whiteTexture) {
uint16_t group = batch.submeshId / 100;
uint16_t group = batchGroup;
bool isSkinGroup = (group == 0 || group == 3 || group == 4 || group == 5 ||
group == 8 || group == 9 || group == 13);
if (isSkinGroup) {
@ -2080,8 +2085,8 @@ bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmen
}
}
}
// Fallback: scan bones for keyBoneId 26 (right hand) / 27 (left hand)
if (!found) {
// Fallback to key-bone lookup only for weapon hand attachment IDs.
if (!found && (attachmentId == 1 || attachmentId == 2)) {
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
for (size_t i = 0; i < charModel.bones.size(); i++) {
if (charModel.bones[i].keyBoneId == targetKeyBone) {
@ -2096,7 +2101,7 @@ bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmen
if (found && boneIndex >= charModel.bones.size()) {
found = false;
}
if (!found) {
if (!found && (attachmentId == 1 || attachmentId == 2)) {
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
for (size_t i = 0; i < charModel.bones.size(); i++) {
if (charModel.bones[i].keyBoneId == targetKeyBone) {
@ -2247,18 +2252,22 @@ bool CharacterRenderer::getAttachmentTransform(uint32_t instanceId, uint32_t att
// Validate bone index; invalid indices bind attachments to origin (looks like weapons at feet).
if (boneIndex >= model.bones.size()) {
// Fallback: key bones (26/27) for hand attachments.
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
found = false;
for (size_t i = 0; i < model.bones.size(); i++) {
if (model.bones[i].keyBoneId == targetKeyBone) {
boneIndex = static_cast<uint16_t>(i);
offset = glm::vec3(0.0f);
found = true;
break;
// Fallback: key bones (26/27) only for hand attachments.
if (attachmentId == 1 || attachmentId == 2) {
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
found = false;
for (size_t i = 0; i < model.bones.size(); i++) {
if (model.bones[i].keyBoneId == targetKeyBone) {
boneIndex = static_cast<uint16_t>(i);
offset = glm::vec3(0.0f);
found = true;
break;
}
}
if (!found) return false;
} else {
return false;
}
if (!found) return false;
}
// Get bone matrix