diff --git a/include/core/application.hpp b/include/core/application.hpp index 4c0486de..de6a01c1 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -146,6 +146,11 @@ private: std::unordered_map displayDataMap_; // displayId → display data std::unordered_map humanoidExtraMap_; // extraDisplayId → humanoid data std::unordered_map modelIdToPath_; // modelId → M2 path (from CreatureModelData.dbc) + // CharHairGeosets.dbc: key = (raceId<<16)|(sexId<<8)|variationId → geosetId (skinSectionId) + std::unordered_map hairGeosetMap_; + // CharFacialHairStyles.dbc: key = (raceId<<16)|(sexId<<8)|variationId → {geoset100, geoset300, geoset200} + struct FacialHairGeosets { uint16_t geoset100 = 0; uint16_t geoset300 = 0; uint16_t geoset200 = 0; }; + std::unordered_map facialHairGeosetMap_; std::unordered_map creatureInstances_; // guid → render instanceId std::unordered_map creatureModelIds_; // guid → loaded modelId uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 0a3cea28..30310f6f 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -62,6 +62,7 @@ public: void setInstancePosition(uint32_t instanceId, const glm::vec3& position); void setInstanceRotation(uint32_t instanceId, const glm::vec3& rotation); void setActiveGeosets(uint32_t instanceId, const std::unordered_set& geosets); + void setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, GLuint textureId); void setInstanceVisible(uint32_t instanceId, bool visible); void removeInstance(uint32_t instanceId); bool getAnimationState(uint32_t instanceId, uint32_t& animationId, float& animationTimeMs, float& animationDurationMs) const; @@ -123,6 +124,9 @@ private: // Empty = render all (for non-character models) std::unordered_set activeGeosets; + // Per-geoset-group texture overrides (group → GL texture ID) + std::unordered_map groupTextureOverrides; + // Weapon attachments (weapons parented to this instance's bones) std::vector weaponAttachments; diff --git a/src/core/application.cpp b/src/core/application.cpp index 7e430d57..5cbae5d7 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1681,6 +1681,46 @@ void Application::buildCreatureDisplayLookups() { LOG_INFO("Loaded ", modelIdToPath_.size(), " model→path mappings"); } + // CharHairGeosets.dbc: maps (race, sex, hairStyleId) → skinSectionId for hair mesh + // Col 0: ID, Col 1: RaceID, Col 2: SexID, Col 3: VariationID, Col 4: GeosetID, Col 5: Showscalp + if (auto chg = assetManager->loadDBC("CharHairGeosets.dbc"); chg && chg->isLoaded()) { + for (uint32_t i = 0; i < chg->getRecordCount(); i++) { + uint32_t raceId = chg->getUInt32(i, 1); + uint32_t sexId = chg->getUInt32(i, 2); + uint32_t variation = chg->getUInt32(i, 3); + uint32_t geosetId = chg->getUInt32(i, 4); + uint32_t key = (raceId << 16) | (sexId << 8) | variation; + hairGeosetMap_[key] = static_cast(geosetId); + } + LOG_INFO("Loaded ", hairGeosetMap_.size(), " hair geoset mappings from CharHairGeosets.dbc"); + // Debug: dump Human Male (race=1, sex=0) hair geoset mappings + for (uint32_t v = 0; v < 20; v++) { + uint32_t k = (1u << 16) | (0u << 8) | v; + auto it = hairGeosetMap_.find(k); + if (it != hairGeosetMap_.end()) { + LOG_INFO(" HairGeoset Human Male style ", v, " → geosetId ", it->second); + } + } + } + + // CharFacialHairStyles.dbc: maps (race, sex, facialHairId) → geoset IDs for groups 1xx, 3xx, 2xx + // Col 0: ID, Col 1: RaceID, Col 2: SexID, Col 3: VariationID + // Col 4: Geoset100, Col 5: Geoset300, Col 6: Geoset200 + if (auto cfh = assetManager->loadDBC("CharacterFacialHairStyles.dbc"); cfh && cfh->isLoaded()) { + for (uint32_t i = 0; i < cfh->getRecordCount(); i++) { + uint32_t raceId = cfh->getUInt32(i, 1); + uint32_t sexId = cfh->getUInt32(i, 2); + uint32_t variation = cfh->getUInt32(i, 3); + uint32_t key = (raceId << 16) | (sexId << 8) | variation; + FacialHairGeosets fhg; + fhg.geoset100 = static_cast(cfh->getUInt32(i, 4)); + fhg.geoset300 = static_cast(cfh->getUInt32(i, 5)); + fhg.geoset200 = static_cast(cfh->getUInt32(i, 6)); + facialHairGeosetMap_[key] = fhg; + } + LOG_INFO("Loaded ", facialHairGeosetMap_.size(), " facial hair geoset mappings from CharFacialHairStyles.dbc"); + } + creatureLookupsBuilt_ = true; } @@ -1791,9 +1831,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x " hands=", extra.equipDisplayId[8], " tabard=", extra.equipDisplayId[9], " cape=", extra.equipDisplayId[10]); - // Use baked texture as-is (baked textures already include full NPC appearance) - // Apply to all skin-related slots: type 1 (char skin), type 2 (object skin), type 6 (hair) - // This ensures all body part batches render with the baked texture + // Use baked texture for body skin only (types 1, 2) + // Type 6 (hair) needs its own texture from CharSections.dbc if (!extra.bakeName.empty()) { std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName; GLuint finalTex = charRenderer->loadTexture(bakePath); @@ -1801,8 +1840,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (finalTex != 0) { for (size_t ti = 0; ti < model.textures.size(); ti++) { uint32_t texType = model.textures[ti].type; - // Apply baked texture to all skin-related slots - if (texType == 1 || texType == 2 || texType == 6) { + if (texType == 1 || texType == 2) { charRenderer->setModelTexture(modelId, static_cast(ti), finalTex); LOG_DEBUG("Applied baked NPC texture to slot ", ti, " (type ", texType, "): ", bakePath); hasHumanoidTexture = true; @@ -1814,6 +1852,42 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } else { LOG_DEBUG(" Humanoid extra has empty bakeName, trying CharSections fallback"); } + + // Load hair texture from CharSections.dbc (section 3) + auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc"); + if (charSectionsDbc) { + uint32_t targetRace = static_cast(extra.raceId); + uint32_t targetSex = static_cast(extra.sexId); + std::string hairTexPath; + + for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { + uint32_t raceId = charSectionsDbc->getUInt32(r, 1); + uint32_t sexId = charSectionsDbc->getUInt32(r, 2); + uint32_t section = charSectionsDbc->getUInt32(r, 3); + uint32_t variation = charSectionsDbc->getUInt32(r, 8); + uint32_t colorIdx = charSectionsDbc->getUInt32(r, 9); + + if (raceId != targetRace || sexId != targetSex) continue; + if (section != 3) continue; // Section 3 = hair + if (variation != static_cast(extra.hairStyleId)) continue; + if (colorIdx != static_cast(extra.hairColorId)) continue; + + hairTexPath = charSectionsDbc->getString(r, 4); + break; + } + + if (!hairTexPath.empty()) { + GLuint hairTex = charRenderer->loadTexture(hairTexPath); + if (hairTex != 0) { + for (size_t ti = 0; ti < model.textures.size(); ti++) { + if (model.textures[ti].type == 6) { + charRenderer->setModelTexture(modelId, static_cast(ti), hairTex); + LOG_DEBUG("Applied hair texture to slot ", ti, ": ", hairTexPath); + } + } + } + } + } } else { LOG_WARNING(" extraDisplayId ", dispData.extraDisplayId, " not found in humanoidExtraMap"); } @@ -1864,16 +1938,37 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x const auto& extra = itExtra->second; std::unordered_set activeGeosets; - // Body parts (group 0: IDs 0-99) - humanoid models may have many body submeshes - for (uint16_t i = 0; i < 100; i++) { - activeGeosets.insert(i); + // Group 0: body base (id=0 always) + hair scalp mesh from CharHairGeosets.dbc + activeGeosets.insert(0); // Body base mesh + + // Hair: CharHairGeosets.dbc maps (race, sex, hairStyleId) → group 0 scalp submeshId + uint32_t hairKey = (static_cast(extra.raceId) << 16) | + (static_cast(extra.sexId) << 8) | + static_cast(extra.hairStyleId); + auto itHairGeo = hairGeosetMap_.find(hairKey); + uint16_t hairScalpId = (itHairGeo != hairGeosetMap_.end()) ? itHairGeo->second : 0; + if (hairScalpId > 0) { + activeGeosets.insert(hairScalpId); // Group 0 scalp/hair mesh + activeGeosets.insert(static_cast(100 + hairScalpId)); // Group 1 connector (if exists) + } else { + activeGeosets.insert(101); // Bald — default group 1 } + uint16_t hairGeoset = (hairScalpId > 0) ? hairScalpId : 0; // For helmet hiding - // Hair style geoset: 100 + hairStyleId + 1 (101 = style 0, 102 = style 1, etc.) - activeGeosets.insert(static_cast(101 + extra.hairStyleId)); - - // Facial hair geoset: 200 + facialHairId + 1 (201 = none/style 0, 202 = style 1, etc.) - activeGeosets.insert(static_cast(201 + extra.facialHairId)); + // Facial hair geosets from CharFacialHairStyles.dbc lookup + uint32_t facialKey = (static_cast(extra.raceId) << 16) | + (static_cast(extra.sexId) << 8) | + static_cast(extra.facialHairId); + auto itFacial = facialHairGeosetMap_.find(facialKey); + if (itFacial != facialHairGeosetMap_.end()) { + const auto& fhg = itFacial->second; + if (fhg.geoset100 > 0) activeGeosets.insert(fhg.geoset100); + if (fhg.geoset300 > 0) activeGeosets.insert(fhg.geoset300); + if (fhg.geoset200 > 0) activeGeosets.insert(fhg.geoset200); + } else { + activeGeosets.insert(201); // Default: no facial hair + activeGeosets.insert(301); // Default facial group 3 + } // Default equipment geosets (bare/no armor) uint16_t geosetGloves = 301; // Bare hands @@ -1885,26 +1980,28 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x bool hideHair = false; // Load equipment geosets from ItemDisplayInfo.dbc + // DBC columns: 7=GeosetGroup[0], 8=GeosetGroup[1], 9=GeosetGroup[2] auto itemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc"); if (itemDisplayDbc) { // Equipment slots: 0=helm, 1=shoulder, 2=shirt, 3=chest, 4=belt, 5=legs, 6=feet, 7=wrist, 8=hands, 9=tabard, 10=cape - // ItemDisplayInfo geoset columns: 5=GeosetGroup[0], 6=GeosetGroup[1], 7=GeosetGroup[2] // Helm (slot 0) - may hide hair if (extra.equipDisplayId[0] != 0) { int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[0]); if (idx >= 0) { - // Check helmet vis flags (col 12-13) or just hide hair if helm exists hideHair = true; } } - // Chest (slot 3) - geoset group 5xx/8xx + // Chest (slot 3) - geoset group 5xx if (extra.equipDisplayId[3] != 0) { int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[3]); if (idx >= 0) { - uint32_t geoGroup = itemDisplayDbc->getUInt32(static_cast(idx), 5); - if (geoGroup > 0) geosetChest = static_cast(500 + geoGroup); + uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), 7); + if (gg > 0) geosetChest = static_cast(501 + gg); + // Robes: GeosetGroup[2] > 0 shows kilt legs + uint32_t gg3 = itemDisplayDbc->getUInt32(static_cast(idx), 9); + if (gg3 > 0) geosetPants = static_cast(1301 + gg3); } } @@ -1912,8 +2009,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (extra.equipDisplayId[5] != 0) { int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[5]); if (idx >= 0) { - uint32_t geoGroup = itemDisplayDbc->getUInt32(static_cast(idx), 5); - if (geoGroup > 0) geosetPants = static_cast(1300 + geoGroup); + uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), 7); + if (gg > 0) geosetPants = static_cast(1301 + gg); } } @@ -1921,8 +2018,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (extra.equipDisplayId[6] != 0) { int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[6]); if (idx >= 0) { - uint32_t geoGroup = itemDisplayDbc->getUInt32(static_cast(idx), 5); - if (geoGroup > 0) geosetBoots = static_cast(400 + geoGroup); + uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), 7); + if (gg > 0) geosetBoots = static_cast(401 + gg); } } @@ -1930,8 +2027,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (extra.equipDisplayId[8] != 0) { int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[8]); if (idx >= 0) { - uint32_t geoGroup = itemDisplayDbc->getUInt32(static_cast(idx), 5); - if (geoGroup > 0) geosetGloves = static_cast(300 + geoGroup); + uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), 7); + if (gg > 0) geosetGloves = static_cast(301 + gg); } } @@ -1947,8 +2044,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (extra.equipDisplayId[10] != 0) { int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[10]); if (idx >= 0) { - uint32_t geoGroup = itemDisplayDbc->getUInt32(static_cast(idx), 5); - if (geoGroup > 0) geosetCape = static_cast(1500 + geoGroup); + geosetCape = 1502; // Show cloak mesh } } } @@ -1962,9 +2058,23 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x activeGeosets.insert(geosetTabard); activeGeosets.insert(701); // Ears: default - // Hide hair if wearing helm - if (hideHair) { - activeGeosets.erase(static_cast(101 + extra.hairStyleId)); + // Hide hair if wearing helm: remove group 0 scalp mesh and group 1 connector + if (hideHair && hairScalpId > 0) { + activeGeosets.erase(hairScalpId); + activeGeosets.erase(static_cast(100 + hairScalpId)); + activeGeosets.insert(101); // Show bald scalp instead + LOG_INFO("Hiding hair geoset ", hairScalpId, "/", (100 + hairScalpId), + " (helmDisplayId=", extra.equipDisplayId[0], ")"); + } + + // Log model's actual submesh IDs for debugging geoset mismatches + { + std::string batchIds; + for (const auto& b : model.batches) { + if (!batchIds.empty()) batchIds += ","; + batchIds += std::to_string(b.submeshId); + } + LOG_INFO("Model batches submeshIds: [", batchIds, "]"); } // Log what geosets we're setting for debugging @@ -1975,8 +2085,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } LOG_INFO("NPC geosets for instance ", instanceId, ": [", geosetList, "]"); charRenderer->setActiveGeosets(instanceId, activeGeosets); - LOG_DEBUG("Set humanoid geosets: hair=", hideHair ? 0 : (101 + extra.hairStyleId), - " facial=", 201 + extra.facialHairId, + LOG_DEBUG("Set humanoid geosets: hair=", hideHair ? 0 : (int)hairGeoset, " chest=", geosetChest, " pants=", geosetPants, " boots=", geosetBoots, " gloves=", geosetGloves); diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 8b9fe937..d55cbba6 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -1179,6 +1179,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons int rendered = 0, skipped = 0; for (const auto& b : gpuModel.data.batches) { bool filtered = !instance.activeGeosets.empty() && + (b.submeshId / 100 != 0) && instance.activeGeosets.find(b.submeshId) == instance.activeGeosets.end(); GLuint resolvedTex = whiteTexture; @@ -1216,19 +1217,14 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons } // Draw batches (submeshes) with per-batch textures - // Geoset filtering: Group = submeshId / 100, Variation = submeshId % 100 - // Group 0 (body parts): always render all (different body parts, not variations) - // Other groups: render only if exact submeshId is in activeGeosets + // 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 (!instance.activeGeosets.empty()) { - uint16_t group = batch.submeshId / 100; - if (group != 0) { - // Non-body groups: require exact match in activeGeosets - if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) { - continue; - } + if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) { + continue; } - // Group 0 (body): always render } // Resolve texture for this batch @@ -1390,6 +1386,13 @@ void CharacterRenderer::setActiveGeosets(uint32_t instanceId, const std::unorder } } +void CharacterRenderer::setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, GLuint textureId) { + auto it = instances.find(instanceId); + if (it != instances.end()) { + it->second.groupTextureOverrides[geosetGroup] = textureId; + } +} + void CharacterRenderer::setInstanceVisible(uint32_t instanceId, bool visible) { auto it = instances.find(instanceId); if (it != instances.end()) {