mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-28 09:33:52 +00:00
Fix character appearance, previews, mount seat, and online unequip
This commit is contained in:
parent
4a023e773b
commit
275914b4db
19 changed files with 743 additions and 113 deletions
|
|
@ -150,11 +150,12 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
uint32_t targetRaceId = static_cast<uint32_t>(race);
|
||||
uint32_t targetSexId = (gender == game::Gender::FEMALE) ? 1u : 0u;
|
||||
|
||||
std::string bodySkinPath;
|
||||
std::string faceLowerPath;
|
||||
std::string faceUpperPath;
|
||||
std::string hairScalpPath;
|
||||
std::vector<std::string> underwearPaths;
|
||||
bodySkinPath_.clear();
|
||||
baseLayers_.clear();
|
||||
|
||||
auto charSectionsDbc = assetManager_->loadDBC("CharSections.dbc");
|
||||
if (charSectionsDbc) {
|
||||
|
|
@ -177,7 +178,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
|
||||
std::string tex1 = charSectionsDbc->getString(r, 4);
|
||||
if (!tex1.empty()) {
|
||||
bodySkinPath = tex1;
|
||||
bodySkinPath_ = tex1;
|
||||
foundSkin = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -217,8 +218,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
|
||||
// Assign texture filenames on model before GPU upload
|
||||
for (auto& tex : model.textures) {
|
||||
if (tex.type == 1 && tex.filename.empty() && !bodySkinPath.empty()) {
|
||||
tex.filename = bodySkinPath;
|
||||
if (tex.type == 1 && tex.filename.empty() && !bodySkinPath_.empty()) {
|
||||
tex.filename = bodySkinPath_;
|
||||
} else if (tex.type == 6 && tex.filename.empty() && !hairScalpPath.empty()) {
|
||||
tex.filename = hairScalpPath;
|
||||
}
|
||||
|
|
@ -247,9 +248,9 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
}
|
||||
|
||||
// Composite body skin + face + underwear overlays
|
||||
if (!bodySkinPath.empty()) {
|
||||
if (!bodySkinPath_.empty()) {
|
||||
std::vector<std::string> layers;
|
||||
layers.push_back(bodySkinPath);
|
||||
layers.push_back(bodySkinPath_);
|
||||
// Face lower texture composited onto body at the face region
|
||||
if (!faceLowerPath.empty()) {
|
||||
layers.push_back(faceLowerPath);
|
||||
|
|
@ -261,6 +262,12 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
layers.push_back(up);
|
||||
}
|
||||
|
||||
// Cache for later equipment compositing.
|
||||
// Keep baseLayers_ without the base skin (compositeWithRegions takes basePath separately).
|
||||
if (!faceLowerPath.empty()) baseLayers_.push_back(faceLowerPath);
|
||||
if (!faceUpperPath.empty()) baseLayers_.push_back(faceUpperPath);
|
||||
for (const auto& up : underwearPaths) baseLayers_.push_back(up);
|
||||
|
||||
if (layers.size() > 1) {
|
||||
GLuint compositeTex = charRenderer_->compositeTextures(layers);
|
||||
if (compositeTex != 0) {
|
||||
|
|
@ -319,6 +326,22 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
// Play idle animation (Stand = animation ID 0)
|
||||
charRenderer_->playAnimation(instanceId_, 0, true);
|
||||
|
||||
// Cache core appearance for later equipment geosets.
|
||||
race_ = race;
|
||||
gender_ = gender;
|
||||
useFemaleModel_ = useFemaleModel;
|
||||
hairStyle_ = hairStyle;
|
||||
facialHair_ = facialHair;
|
||||
|
||||
// Cache the type-1 texture slot index so applyEquipment can update it.
|
||||
skinTextureSlotIndex_ = 0;
|
||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
||||
if (model.textures[ti].type == 1) {
|
||||
skinTextureSlotIndex_ = static_cast<uint32_t>(ti);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
modelLoaded_ = true;
|
||||
LOG_INFO("CharacterPreview: loaded ", m2Path,
|
||||
" skin=", (int)skin, " face=", (int)face,
|
||||
|
|
@ -327,6 +350,150 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
return true;
|
||||
}
|
||||
|
||||
bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& equipment) {
|
||||
if (!modelLoaded_ || instanceId_ == 0 || !charRenderer_ || !assetManager_ || !assetManager_->isInitialized()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
||||
if (!displayInfoDbc || !displayInfoDbc->isLoaded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto hasInvType = [&](std::initializer_list<uint8_t> types) -> bool {
|
||||
for (const auto& it : equipment) {
|
||||
if (it.displayModel == 0) continue;
|
||||
for (uint8_t t : types) {
|
||||
if (it.inventoryType == t) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
auto findDisplayId = [&](std::initializer_list<uint8_t> types) -> uint32_t {
|
||||
for (const auto& it : equipment) {
|
||||
if (it.displayModel == 0) continue;
|
||||
for (uint8_t t : types) {
|
||||
if (it.inventoryType == t) return it.displayModel; // ItemDisplayInfo ID (3.3.5a char enum)
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
auto getGeosetGroup = [&](uint32_t displayInfoId, int groupField) -> uint32_t {
|
||||
if (displayInfoId == 0) return 0;
|
||||
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
||||
if (recIdx < 0) return 0;
|
||||
return displayInfoDbc->getUInt32(static_cast<uint32_t>(recIdx), 7 + groupField);
|
||||
};
|
||||
|
||||
// --- Geosets ---
|
||||
std::unordered_set<uint16_t> geosets;
|
||||
for (uint16_t i = 0; i <= 18; i++) geosets.insert(i);
|
||||
geosets.insert(static_cast<uint16_t>(100 + hairStyle_ + 1)); // Hair style
|
||||
geosets.insert(static_cast<uint16_t>(200 + facialHair_ + 1)); // Facial hair
|
||||
geosets.insert(701); // Ears
|
||||
|
||||
// Default naked geosets
|
||||
uint16_t geosetGloves = 301;
|
||||
uint16_t geosetBoots = 401;
|
||||
uint16_t geosetChest = 501;
|
||||
uint16_t geosetPants = 1301;
|
||||
|
||||
// Chest/Shirt/Robe
|
||||
{
|
||||
uint32_t did = findDisplayId({4, 5, 20});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
if (gg > 0) geosetChest = static_cast<uint16_t>(501 + gg);
|
||||
// Robe kilt legs
|
||||
uint32_t gg3 = getGeosetGroup(did, 2);
|
||||
if (gg3 > 0) geosetPants = static_cast<uint16_t>(1301 + gg3);
|
||||
}
|
||||
// Legs
|
||||
{
|
||||
uint32_t did = findDisplayId({7});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
if (gg > 0) geosetPants = static_cast<uint16_t>(1301 + gg);
|
||||
}
|
||||
// Feet
|
||||
{
|
||||
uint32_t did = findDisplayId({8});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
if (gg > 0) geosetBoots = static_cast<uint16_t>(401 + gg);
|
||||
}
|
||||
// Hands
|
||||
{
|
||||
uint32_t did = findDisplayId({10});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
if (gg > 0) geosetGloves = static_cast<uint16_t>(301 + gg);
|
||||
}
|
||||
|
||||
geosets.insert(geosetGloves);
|
||||
geosets.insert(geosetBoots);
|
||||
geosets.insert(geosetChest);
|
||||
geosets.insert(geosetPants);
|
||||
geosets.insert(hasInvType({16}) ? 1502 : 1501); // Cloak mesh toggle (visual may still be limited)
|
||||
if (hasInvType({19})) geosets.insert(1201); // Tabard mesh toggle
|
||||
|
||||
// Hide hair under helmets (helmets are separate models; this still avoids hair clipping)
|
||||
if (hasInvType({1})) {
|
||||
geosets.erase(static_cast<uint16_t>(100 + hairStyle_ + 1));
|
||||
geosets.insert(1); // Bald scalp cap
|
||||
geosets.insert(101); // Default group-1 connector
|
||||
}
|
||||
|
||||
charRenderer_->setActiveGeosets(instanceId_, geosets);
|
||||
|
||||
// --- Textures (equipment overlays onto body skin) ---
|
||||
if (bodySkinPath_.empty()) return true; // geosets applied, but can't composite
|
||||
|
||||
static const char* componentDirs[] = {
|
||||
"ArmUpperTexture", "ArmLowerTexture", "HandTexture",
|
||||
"TorsoUpperTexture", "TorsoLowerTexture",
|
||||
"LegUpperTexture", "LegLowerTexture", "FootTexture",
|
||||
};
|
||||
|
||||
std::vector<std::pair<int, std::string>> regionLayers;
|
||||
regionLayers.reserve(32);
|
||||
|
||||
for (const auto& it : equipment) {
|
||||
if (it.displayModel == 0) continue;
|
||||
int32_t recIdx = displayInfoDbc->findRecordById(it.displayModel);
|
||||
if (recIdx < 0) continue;
|
||||
|
||||
for (int region = 0; region < 8; region++) {
|
||||
uint32_t fieldIdx = 15 + region; // texture_1..texture_8
|
||||
std::string texName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), fieldIdx);
|
||||
if (texName.empty()) continue;
|
||||
|
||||
std::string base = "Item\\TextureComponents\\" +
|
||||
std::string(componentDirs[region]) + "\\" + texName;
|
||||
|
||||
std::string genderSuffix = (gender_ == game::Gender::FEMALE) ? "_F.blp" : "_M.blp";
|
||||
std::string genderPath = base + genderSuffix;
|
||||
std::string unisexPath = base + "_U.blp";
|
||||
std::string fullPath;
|
||||
if (assetManager_->fileExists(genderPath)) {
|
||||
fullPath = genderPath;
|
||||
} else if (assetManager_->fileExists(unisexPath)) {
|
||||
fullPath = unisexPath;
|
||||
} else {
|
||||
fullPath = base + ".blp";
|
||||
}
|
||||
regionLayers.emplace_back(region, fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!regionLayers.empty()) {
|
||||
GLuint newTex = charRenderer_->compositeWithRegions(bodySkinPath_, baseLayers_, regionLayers);
|
||||
if (newTex != 0) {
|
||||
charRenderer_->setModelTexture(PREVIEW_MODEL_ID, skinTextureSlotIndex_, newTex);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CharacterPreview::update(float deltaTime) {
|
||||
if (charRenderer_ && modelLoaded_) {
|
||||
charRenderer_->update(deltaTime);
|
||||
|
|
|
|||
|
|
@ -1257,16 +1257,16 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
// Bind VAO and draw
|
||||
glBindVertexArray(gpuModel.vao);
|
||||
|
||||
if (!gpuModel.data.batches.empty()) {
|
||||
bool applyGeosetFilter = !instance.activeGeosets.empty();
|
||||
if (applyGeosetFilter) {
|
||||
bool hasRenderableGeoset = false;
|
||||
for (const auto& batch : gpuModel.data.batches) {
|
||||
if (instance.activeGeosets.find(batch.submeshId) != instance.activeGeosets.end()) {
|
||||
hasRenderableGeoset = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!gpuModel.data.batches.empty()) {
|
||||
bool applyGeosetFilter = !instance.activeGeosets.empty();
|
||||
if (applyGeosetFilter) {
|
||||
bool hasRenderableGeoset = false;
|
||||
for (const auto& batch : gpuModel.data.batches) {
|
||||
if (instance.activeGeosets.find(batch.submeshId) != instance.activeGeosets.end()) {
|
||||
hasRenderableGeoset = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasRenderableGeoset) {
|
||||
static std::unordered_set<uint32_t> loggedGeosetFallback;
|
||||
if (loggedGeosetFallback.insert(instance.id).second) {
|
||||
|
|
@ -1274,13 +1274,64 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
instance.id, " (model ", instance.modelId,
|
||||
"); rendering all batches as fallback");
|
||||
}
|
||||
applyGeosetFilter = false;
|
||||
}
|
||||
}
|
||||
applyGeosetFilter = false;
|
||||
}
|
||||
}
|
||||
|
||||
// One-time debug dump of rendered batches per model
|
||||
static std::unordered_set<uint32_t> dumpedModels;
|
||||
if (dumpedModels.find(instance.modelId) == dumpedModels.end()) {
|
||||
auto resolveBatchTexture = [&](const M2ModelGPU& gm, const pipeline::M2Batch& b) -> GLuint {
|
||||
// A skin batch can reference multiple textures (b.textureCount) starting at b.textureIndex.
|
||||
// We currently bind only a single texture, so pick the most appropriate one.
|
||||
//
|
||||
// This matters for hair: the first texture in the combo can be a mask/empty slot,
|
||||
// causing the hair to render as solid white.
|
||||
if (b.textureIndex == 0xFFFF) return whiteTexture;
|
||||
if (gm.data.textureLookup.empty() || gm.textureIds.empty()) return whiteTexture;
|
||||
|
||||
uint32_t comboCount = b.textureCount ? static_cast<uint32_t>(b.textureCount) : 1u;
|
||||
comboCount = std::min<uint32_t>(comboCount, 8u);
|
||||
|
||||
struct Candidate { GLuint id; uint32_t type; };
|
||||
Candidate first{whiteTexture, 0};
|
||||
bool hasFirst = false;
|
||||
Candidate firstNonWhite{whiteTexture, 0};
|
||||
bool hasFirstNonWhite = false;
|
||||
|
||||
for (uint32_t i = 0; i < comboCount; i++) {
|
||||
uint32_t lookupPos = static_cast<uint32_t>(b.textureIndex) + i;
|
||||
if (lookupPos >= gm.data.textureLookup.size()) break;
|
||||
uint16_t texSlot = gm.data.textureLookup[lookupPos];
|
||||
if (texSlot >= gm.textureIds.size()) continue;
|
||||
|
||||
GLuint texId = gm.textureIds[texSlot];
|
||||
uint32_t texType = (texSlot < gm.data.textures.size()) ? gm.data.textures[texSlot].type : 0;
|
||||
|
||||
if (!hasFirst) {
|
||||
first = {texId, texType};
|
||||
hasFirst = true;
|
||||
}
|
||||
|
||||
if (texId == 0 || texId == whiteTexture) continue;
|
||||
|
||||
// Prefer the hair texture slot (type 6) whenever present in the combo.
|
||||
// Humanoid scalp meshes can live in group 0, so group-based checks are insufficient.
|
||||
if (texType == 6) {
|
||||
return texId;
|
||||
}
|
||||
|
||||
if (!hasFirstNonWhite) {
|
||||
firstNonWhite = {texId, texType};
|
||||
hasFirstNonWhite = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasFirstNonWhite) return firstNonWhite.id;
|
||||
if (hasFirst && first.id != 0) return first.id;
|
||||
return whiteTexture;
|
||||
};
|
||||
|
||||
// One-time debug dump of rendered batches per model
|
||||
static std::unordered_set<uint32_t> dumpedModels;
|
||||
if (dumpedModels.find(instance.modelId) == dumpedModels.end()) {
|
||||
dumpedModels.insert(instance.modelId);
|
||||
int bIdx = 0;
|
||||
int rendered = 0, skipped = 0;
|
||||
|
|
@ -1289,24 +1340,11 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
(b.submeshId / 100 != 0) &&
|
||||
instance.activeGeosets.find(b.submeshId) == instance.activeGeosets.end();
|
||||
|
||||
GLuint resolvedTex = whiteTexture;
|
||||
std::string texInfo = "white(fallback)";
|
||||
if (b.textureIndex != 0xFFFF && b.textureIndex < gpuModel.data.textureLookup.size()) {
|
||||
uint16_t lk = gpuModel.data.textureLookup[b.textureIndex];
|
||||
if (lk < gpuModel.textureIds.size()) {
|
||||
resolvedTex = gpuModel.textureIds[lk];
|
||||
texInfo = "lookup[" + std::to_string(b.textureIndex) + "]->tex[" + std::to_string(lk) + "]=GL" + std::to_string(resolvedTex);
|
||||
} else {
|
||||
texInfo = "lookup[" + std::to_string(b.textureIndex) + "]->OOB(" + std::to_string(lk) + ")";
|
||||
}
|
||||
} else if (b.textureIndex == 0xFFFF) {
|
||||
texInfo = "texIdx=FFFF";
|
||||
} else {
|
||||
texInfo = "texIdx=" + std::to_string(b.textureIndex) + " OOB(lookupSz=" + std::to_string(gpuModel.data.textureLookup.size()) + ")";
|
||||
}
|
||||
GLuint resolvedTex = resolveBatchTexture(gpuModel, b);
|
||||
std::string texInfo = "GL" + std::to_string(resolvedTex);
|
||||
|
||||
if (filtered) skipped++; else rendered++;
|
||||
LOG_DEBUG("Batch ", bIdx, ": submesh=", b.submeshId,
|
||||
if (filtered) skipped++; else rendered++;
|
||||
LOG_DEBUG("Batch ", bIdx, ": submesh=", b.submeshId,
|
||||
" level=", b.submeshLevel,
|
||||
" idxStart=", b.indexStart, " idxCount=", b.indexCount,
|
||||
" tex=", texInfo,
|
||||
|
|
@ -1317,28 +1355,22 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
gpuModel.textureIds.size(), " textures loaded, ",
|
||||
gpuModel.data.textureLookup.size(), " in lookup table");
|
||||
for (size_t t = 0; t < gpuModel.data.textures.size(); t++) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw batches (submeshes) with per-batch textures
|
||||
// Geoset filtering: skip batches whose submeshId is not in activeGeosets.
|
||||
// For character models, group 0 (body/scalp) is also filtered so that only
|
||||
// the correct scalp mesh renders (not all overlapping variants).
|
||||
for (const auto& batch : gpuModel.data.batches) {
|
||||
// Draw batches (submeshes) with per-batch textures
|
||||
// Geoset filtering: skip batches whose submeshId is not in activeGeosets.
|
||||
// For character models, group 0 (body/scalp) is also filtered so that only
|
||||
// the correct scalp mesh renders (not all overlapping variants).
|
||||
for (const auto& batch : gpuModel.data.batches) {
|
||||
if (applyGeosetFilter) {
|
||||
if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve texture for this batch
|
||||
GLuint texId = whiteTexture;
|
||||
if (batch.textureIndex < gpuModel.data.textureLookup.size()) {
|
||||
uint16_t lookupIdx = gpuModel.data.textureLookup[batch.textureIndex];
|
||||
if (lookupIdx < gpuModel.textureIds.size()) {
|
||||
texId = gpuModel.textureIds[lookupIdx];
|
||||
}
|
||||
}
|
||||
// Resolve texture for this batch (prefer hair textures for hair geosets).
|
||||
GLuint texId = resolveBatchTexture(gpuModel, batch);
|
||||
|
||||
// For body parts with white/fallback texture, use skin (type 1) texture
|
||||
// This handles humanoid models where some body parts use different texture slots
|
||||
|
|
|
|||
|
|
@ -1231,7 +1231,12 @@ void Renderer::updateCharacterAnimation() {
|
|||
// Keep seat offset minimal; large offsets amplify visible bobble.
|
||||
glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, taxiFlight_ ? 0.04f : 0.08f);
|
||||
glm::vec3 targetRiderPos = mountSeatPos + seatOffset;
|
||||
if (!mountSeatSmoothingInit_) {
|
||||
// When moving, smoothing the seat position produces visible lag that looks like
|
||||
// the rider sliding toward the rump. Anchor rigidly while moving.
|
||||
if (moving) {
|
||||
mountSeatSmoothingInit_ = false;
|
||||
smoothedMountSeatPos_ = targetRiderPos;
|
||||
} else if (!mountSeatSmoothingInit_) {
|
||||
smoothedMountSeatPos_ = targetRiderPos;
|
||||
mountSeatSmoothingInit_ = true;
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue