mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Fix character geoset mapping and texture corruption on equipment change
Corrected CharGeosets group assignments verified via vertex bounding boxes: - Group 4 (401+) = gloves/forearms, Group 5 (501+) = boots/shins, Group 8 (801+) = sleeves (chest-controlled), Group 9 = kneepads, Group 13 (1301+) = pants/trousers, Group 20 (2002) = bare feet - Changed bare shin default from 501 to 502 for better width match with thigh mesh (0.39 vs 0.32, thighs are 0.42) - Added clearCompositeCache() to prevent stale composite textures from being reused across equipment changes - Fixed character preview geoset defaults to match corrected mapping
This commit is contained in:
parent
27d6150ecb
commit
1a5c43d18a
10 changed files with 424 additions and 169 deletions
|
|
@ -12,8 +12,9 @@
|
||||||
},
|
},
|
||||||
"CharSections": {
|
"CharSections": {
|
||||||
"RaceID": 1, "SexID": 2, "BaseSection": 3,
|
"RaceID": 1, "SexID": 2, "BaseSection": 3,
|
||||||
"Texture1": 4, "Texture2": 5, "Texture3": 6,
|
"VariationIndex": 4, "ColorIndex": 5,
|
||||||
"Flags": 7, "VariationIndex": 8, "ColorIndex": 9
|
"Texture1": 6, "Texture2": 7, "Texture3": 8,
|
||||||
|
"Flags": 9
|
||||||
},
|
},
|
||||||
"SpellIcon": { "ID": 0, "Path": 1 },
|
"SpellIcon": { "ID": 0, "Path": 1 },
|
||||||
"FactionTemplate": {
|
"FactionTemplate": {
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,9 @@ struct M2Model {
|
||||||
std::vector<M2Sequence> sequences;
|
std::vector<M2Sequence> sequences;
|
||||||
std::vector<uint32_t> globalSequenceDurations; // Per-global-sequence loop durations (ms)
|
std::vector<uint32_t> globalSequenceDurations; // Per-global-sequence loop durations (ms)
|
||||||
|
|
||||||
|
// Bone lookup table (vertex bone indices reference this to get global bone index)
|
||||||
|
std::vector<uint16_t> boneLookupTable;
|
||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
std::vector<M2Batch> batches;
|
std::vector<M2Batch> batches;
|
||||||
std::vector<M2Texture> textures;
|
std::vector<M2Texture> textures;
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,9 @@ public:
|
||||||
const std::vector<std::string>& baseLayers,
|
const std::vector<std::string>& baseLayers,
|
||||||
const std::vector<std::pair<int, std::string>>& regionLayers);
|
const std::vector<std::pair<int, std::string>>& regionLayers);
|
||||||
|
|
||||||
|
/** Clear the composite texture cache (forces re-compositing on next call). */
|
||||||
|
void clearCompositeCache();
|
||||||
|
|
||||||
/** Load a BLP texture from MPQ and return the GL texture ID (cached). */
|
/** Load a BLP texture from MPQ and return the GL texture ID (cached). */
|
||||||
GLuint loadTexture(const std::string& path);
|
GLuint loadTexture(const std::string& path);
|
||||||
|
|
||||||
|
|
@ -259,7 +262,8 @@ private:
|
||||||
uint32_t nextInstanceId = 1;
|
uint32_t nextInstanceId = 1;
|
||||||
|
|
||||||
// Maximum bones supported (GPU uniform limit)
|
// Maximum bones supported (GPU uniform limit)
|
||||||
static constexpr int MAX_BONES = 200;
|
// WoW character models can have 210+ bones; GPU reports 4096 components (~256 mat4)
|
||||||
|
static constexpr int MAX_BONES = 240;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -1191,6 +1191,14 @@ void Application::setupUICallbacks() {
|
||||||
uint32_t appearanceBytes,
|
uint32_t appearanceBytes,
|
||||||
uint8_t facialFeatures,
|
uint8_t facialFeatures,
|
||||||
float x, float y, float z, float orientation) {
|
float x, float y, float z, float orientation) {
|
||||||
|
// Skip local player — already spawned as the main character
|
||||||
|
uint64_t localGuid = gameHandler ? gameHandler->getPlayerGuid() : 0;
|
||||||
|
uint64_t activeGuid = gameHandler ? gameHandler->getActiveCharacterGuid() : 0;
|
||||||
|
if ((localGuid != 0 && guid == localGuid) ||
|
||||||
|
(activeGuid != 0 && guid == activeGuid) ||
|
||||||
|
(spawnedPlayerGuid_ != 0 && guid == spawnedPlayerGuid_)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (playerInstances_.count(guid)) return;
|
if (playerInstances_.count(guid)) return;
|
||||||
if (pendingPlayerSpawnGuids_.count(guid)) return;
|
if (pendingPlayerSpawnGuids_.count(guid)) return;
|
||||||
pendingPlayerSpawns_.push_back({guid, raceId, genderId, appearanceBytes, facialFeatures, x, y, z, orientation});
|
pendingPlayerSpawns_.push_back({guid, raceId, genderId, appearanceBytes, facialFeatures, x, y, z, orientation});
|
||||||
|
|
@ -1947,8 +1955,12 @@ void Application::spawnPlayerCharacter() {
|
||||||
|
|
||||||
if (useCharSections) {
|
if (useCharSections) {
|
||||||
// Save skin composite state for re-compositing on equipment changes
|
// Save skin composite state for re-compositing on equipment changes
|
||||||
|
// Include face textures so compositeWithRegions can rebuild the full base
|
||||||
bodySkinPath_ = bodySkinPath;
|
bodySkinPath_ = bodySkinPath;
|
||||||
underwearPaths_ = underwearPaths;
|
underwearPaths_.clear();
|
||||||
|
if (!faceLowerTexturePath.empty()) underwearPaths_.push_back(faceLowerTexturePath);
|
||||||
|
if (!faceUpperTexturePath.empty()) underwearPaths_.push_back(faceUpperTexturePath);
|
||||||
|
for (const auto& up : underwearPaths) underwearPaths_.push_back(up);
|
||||||
|
|
||||||
// Composite body skin + face + underwear overlays
|
// Composite body skin + face + underwear overlays
|
||||||
{
|
{
|
||||||
|
|
@ -2080,8 +2092,8 @@ void Application::spawnPlayerCharacter() {
|
||||||
// Default geosets for the active character (match CharacterPreview logic).
|
// Default geosets for the active character (match CharacterPreview logic).
|
||||||
// Previous hardcoded values (notably always inserting 101) caused wrong hair meshes in-world.
|
// Previous hardcoded values (notably always inserting 101) caused wrong hair meshes in-world.
|
||||||
std::unordered_set<uint16_t> activeGeosets;
|
std::unordered_set<uint16_t> activeGeosets;
|
||||||
// Body parts (group 0)
|
// Body parts (group 0: IDs 0-99, some models use up to 27)
|
||||||
for (uint16_t i = 0; i <= 18; i++) activeGeosets.insert(i);
|
for (uint16_t i = 0; i <= 99; i++) activeGeosets.insert(i);
|
||||||
|
|
||||||
uint8_t hairStyleId = 0;
|
uint8_t hairStyleId = 0;
|
||||||
uint8_t facialId = 0;
|
uint8_t facialId = 0;
|
||||||
|
|
@ -2095,13 +2107,14 @@ void Application::spawnPlayerCharacter() {
|
||||||
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
|
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
|
||||||
// Facial hair geoset: group 2 = 200 + variation + 1
|
// Facial hair geoset: group 2 = 200 + variation + 1
|
||||||
activeGeosets.insert(static_cast<uint16_t>(200 + facialId + 1));
|
activeGeosets.insert(static_cast<uint16_t>(200 + facialId + 1));
|
||||||
activeGeosets.insert(302); // Gloves: bare hands
|
activeGeosets.insert(401); // Bare forearms (no gloves) — group 4
|
||||||
activeGeosets.insert(401); // Boots: bare feet
|
activeGeosets.insert(502); // Bare shins (no boots) — group 5 (wider mesh matches thighs)
|
||||||
activeGeosets.insert(501); // Chest: bare
|
|
||||||
activeGeosets.insert(702); // Ears: default
|
activeGeosets.insert(702); // Ears: default
|
||||||
activeGeosets.insert(802); // Wristbands: default
|
activeGeosets.insert(801); // Bare wrists (no chest armor sleeves) — group 8
|
||||||
activeGeosets.insert(1301); // Trousers: bare legs
|
activeGeosets.insert(902); // Kneepads: default — group 9
|
||||||
activeGeosets.insert(1502); // Back body (cloak=none)
|
activeGeosets.insert(1301); // Bare legs (no pants) — group 13
|
||||||
|
activeGeosets.insert(1502); // No cloak — group 15
|
||||||
|
activeGeosets.insert(2002); // Bare feet — group 20
|
||||||
// 1703 = DK eye glow mesh — skip for normal characters
|
// 1703 = DK eye glow mesh — skip for normal characters
|
||||||
// Normal eyes are part of the face texture on the body mesh
|
// Normal eyes are part of the face texture on the body mesh
|
||||||
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
||||||
|
|
@ -3105,15 +3118,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
" hands=", extra.equipDisplayId[8], " tabard=", extra.equipDisplayId[9],
|
" hands=", extra.equipDisplayId[8], " tabard=", extra.equipDisplayId[9],
|
||||||
" cape=", extra.equipDisplayId[10]);
|
" cape=", extra.equipDisplayId[10]);
|
||||||
|
|
||||||
// Use baked texture for body skin only (types 1, 2)
|
// Build equipment texture region layers from NPC equipment display IDs
|
||||||
// Type 6 (hair) needs its own texture from CharSections.dbc
|
// (texture-only compositing — no geoset changes to avoid invisibility bugs)
|
||||||
if (!extra.bakeName.empty()) {
|
std::vector<std::pair<int, std::string>> npcRegionLayers;
|
||||||
std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName;
|
auto npcItemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
||||||
|
|
||||||
// Build equipment texture region layers from NPC equipment display IDs
|
|
||||||
// (texture-only compositing — no geoset changes to avoid invisibility bugs)
|
|
||||||
std::vector<std::pair<int, std::string>> npcRegionLayers;
|
|
||||||
auto npcItemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
|
||||||
if (npcItemDisplayDbc) {
|
if (npcItemDisplayDbc) {
|
||||||
static const char* npcComponentDirs[] = {
|
static const char* npcComponentDirs[] = {
|
||||||
"ArmUpperTexture", "ArmLowerTexture", "HandTexture",
|
"ArmUpperTexture", "ArmLowerTexture", "HandTexture",
|
||||||
|
|
@ -3162,6 +3170,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use baked texture for body skin (types 1, 2)
|
||||||
|
// Type 6 (hair) needs its own texture from CharSections.dbc
|
||||||
|
if (!extra.bakeName.empty()) {
|
||||||
|
std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName;
|
||||||
|
|
||||||
// Composite equipment textures over baked NPC texture, or just load baked texture
|
// Composite equipment textures over baked NPC texture, or just load baked texture
|
||||||
GLuint finalTex = 0;
|
GLuint finalTex = 0;
|
||||||
if (!npcRegionLayers.empty()) {
|
if (!npcRegionLayers.empty()) {
|
||||||
|
|
@ -3188,6 +3201,79 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG_DEBUG(" Humanoid extra has empty bakeName, trying CharSections fallback");
|
LOG_DEBUG(" Humanoid extra has empty bakeName, trying CharSections fallback");
|
||||||
|
|
||||||
|
// Build skin texture from CharSections.dbc (same as player character)
|
||||||
|
auto csFallbackDbc = assetManager->loadDBC("CharSections.dbc");
|
||||||
|
if (csFallbackDbc) {
|
||||||
|
const auto* csFL = pipeline::getActiveDBCLayout()
|
||||||
|
? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
||||||
|
uint32_t npcRace = static_cast<uint32_t>(extra.raceId);
|
||||||
|
uint32_t npcSex = static_cast<uint32_t>(extra.sexId);
|
||||||
|
uint32_t npcSkin = static_cast<uint32_t>(extra.skinId);
|
||||||
|
uint32_t npcFace = static_cast<uint32_t>(extra.faceId);
|
||||||
|
std::string npcSkinPath, npcFaceLower, npcFaceUpper;
|
||||||
|
std::vector<std::string> npcUnderwear;
|
||||||
|
|
||||||
|
for (uint32_t r = 0; r < csFallbackDbc->getRecordCount(); r++) {
|
||||||
|
uint32_t rId = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["RaceID"] : 1);
|
||||||
|
uint32_t sId = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["SexID"] : 2);
|
||||||
|
if (rId != npcRace || sId != npcSex) continue;
|
||||||
|
|
||||||
|
uint32_t section = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["BaseSection"] : 3);
|
||||||
|
uint32_t variation = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["VariationIndex"] : 8);
|
||||||
|
uint32_t color = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["ColorIndex"] : 9);
|
||||||
|
uint32_t tex1F = csFL ? (*csFL)["Texture1"] : 4;
|
||||||
|
|
||||||
|
// Section 0 = skin: match colorIndex = skinId
|
||||||
|
if (section == 0 && npcSkinPath.empty() && color == npcSkin) {
|
||||||
|
npcSkinPath = csFallbackDbc->getString(r, tex1F);
|
||||||
|
}
|
||||||
|
// Section 1 = face: match variation=faceId, color=skinId
|
||||||
|
else if (section == 1 && npcFaceLower.empty() &&
|
||||||
|
variation == npcFace && color == npcSkin) {
|
||||||
|
npcFaceLower = csFallbackDbc->getString(r, tex1F);
|
||||||
|
npcFaceUpper = csFallbackDbc->getString(r, tex1F + 1);
|
||||||
|
}
|
||||||
|
// Section 4 = underwear: match color=skinId
|
||||||
|
else if (section == 4 && npcUnderwear.empty() && color == npcSkin) {
|
||||||
|
for (uint32_t f = tex1F; f <= tex1F + 2; f++) {
|
||||||
|
std::string tex = csFallbackDbc->getString(r, f);
|
||||||
|
if (!tex.empty()) npcUnderwear.push_back(tex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!npcSkinPath.empty()) {
|
||||||
|
// Composite skin + face + underwear
|
||||||
|
std::vector<std::string> skinLayers;
|
||||||
|
skinLayers.push_back(npcSkinPath);
|
||||||
|
if (!npcFaceLower.empty()) skinLayers.push_back(npcFaceLower);
|
||||||
|
if (!npcFaceUpper.empty()) skinLayers.push_back(npcFaceUpper);
|
||||||
|
for (const auto& uw : npcUnderwear) skinLayers.push_back(uw);
|
||||||
|
|
||||||
|
GLuint npcSkinTex = 0;
|
||||||
|
if (!npcRegionLayers.empty()) {
|
||||||
|
npcSkinTex = charRenderer->compositeWithRegions(npcSkinPath,
|
||||||
|
std::vector<std::string>(skinLayers.begin() + 1, skinLayers.end()),
|
||||||
|
npcRegionLayers);
|
||||||
|
} else if (skinLayers.size() > 1) {
|
||||||
|
npcSkinTex = charRenderer->compositeTextures(skinLayers);
|
||||||
|
} else {
|
||||||
|
npcSkinTex = charRenderer->loadTexture(npcSkinPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (npcSkinTex != 0 && modelData) {
|
||||||
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
|
uint32_t texType = modelData->textures[ti].type;
|
||||||
|
if (texType == 1 || texType == 2 || texType == 11 || texType == 12 || texType == 13) {
|
||||||
|
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), npcSkinTex);
|
||||||
|
hasHumanoidTexture = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG_DEBUG("Applied CharSections skin to NPC: ", npcSkinPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load hair texture from CharSections.dbc (section 3)
|
// Load hair texture from CharSections.dbc (section 3)
|
||||||
|
|
@ -3329,12 +3415,13 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default equipment geosets (bare/no armor)
|
// Default equipment geosets (bare/no armor)
|
||||||
uint16_t geosetGloves = 302; // Bare hands
|
// CharGeosets: group 4=gloves(forearm), 5=boots(shin), 8=sleeves, 9=kneepads, 13=pants
|
||||||
uint16_t geosetBoots = 401; // Bare feet
|
uint16_t geosetGloves = 401; // Bare forearms (group 4)
|
||||||
uint16_t geosetChest = 501; // Bare chest
|
uint16_t geosetBoots = 502; // Bare shins (group 5, wider mesh)
|
||||||
uint16_t geosetPants = 1301; // Bare legs
|
uint16_t geosetSleeves = 801; // Bare wrists (group 8, controlled by chest)
|
||||||
uint16_t geosetCape = 1502; // No cape
|
uint16_t geosetPants = 1301; // Bare legs (group 13)
|
||||||
uint16_t geosetTabard = 1201; // No tabard
|
uint16_t geosetCape = 1502; // No cape (group 15)
|
||||||
|
uint16_t geosetTabard = 1201; // No tabard (group 12)
|
||||||
|
|
||||||
// Load equipment geosets from ItemDisplayInfo.dbc
|
// Load equipment geosets from ItemDisplayInfo.dbc
|
||||||
// DBC columns: 7=GeosetGroup[0], 8=GeosetGroup[1], 9=GeosetGroup[2]
|
// DBC columns: 7=GeosetGroup[0], 8=GeosetGroup[1], 9=GeosetGroup[2]
|
||||||
|
|
@ -3345,21 +3432,19 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7;
|
const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7;
|
||||||
const uint32_t fGG3 = idiL ? (*idiL)["GeosetGroup3"] : 9;
|
const uint32_t fGG3 = idiL ? (*idiL)["GeosetGroup3"] : 9;
|
||||||
|
|
||||||
// Helm (slot 0) - noted for helmet model attachment below
|
// Chest (slot 3) → group 8 (sleeves/wristbands)
|
||||||
|
|
||||||
// Chest (slot 3) - geoset group 5xx
|
|
||||||
if (extra.equipDisplayId[3] != 0) {
|
if (extra.equipDisplayId[3] != 0) {
|
||||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[3]);
|
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[3]);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
||||||
if (gg > 0) geosetChest = static_cast<uint16_t>(501 + gg);
|
if (gg > 0) geosetSleeves = static_cast<uint16_t>(801 + gg);
|
||||||
// Robes: GeosetGroup[2] > 0 shows kilt legs
|
// Robes: GeosetGroup[2] > 0 shows kilt legs
|
||||||
uint32_t gg3 = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG3);
|
uint32_t gg3 = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG3);
|
||||||
if (gg3 > 0) geosetPants = static_cast<uint16_t>(1301 + gg3);
|
if (gg3 > 0) geosetPants = static_cast<uint16_t>(1301 + gg3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legs (slot 5) - geoset group 13xx
|
// Legs (slot 5) → group 13 (trousers)
|
||||||
if (extra.equipDisplayId[5] != 0) {
|
if (extra.equipDisplayId[5] != 0) {
|
||||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[5]);
|
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[5]);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
|
|
@ -3368,37 +3453,37 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feet (slot 6) - geoset group 4xx
|
// Feet (slot 6) → group 5 (boots/shins)
|
||||||
if (extra.equipDisplayId[6] != 0) {
|
if (extra.equipDisplayId[6] != 0) {
|
||||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[6]);
|
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[6]);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
||||||
if (gg > 0) geosetBoots = static_cast<uint16_t>(401 + gg);
|
if (gg > 0) geosetBoots = static_cast<uint16_t>(501 + gg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hands (slot 8) - geoset group 3xx
|
// Hands (slot 8) → group 4 (gloves/forearms)
|
||||||
if (extra.equipDisplayId[8] != 0) {
|
if (extra.equipDisplayId[8] != 0) {
|
||||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[8]);
|
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[8]);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
||||||
if (gg > 0) geosetGloves = static_cast<uint16_t>(301 + gg);
|
if (gg > 0) geosetGloves = static_cast<uint16_t>(401 + gg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tabard (slot 9) - geoset group 12xx
|
// Tabard (slot 9) → group 12
|
||||||
if (extra.equipDisplayId[9] != 0) {
|
if (extra.equipDisplayId[9] != 0) {
|
||||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[9]);
|
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[9]);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
geosetTabard = 1202; // Show tabard mesh
|
geosetTabard = 1202;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cape (slot 10) - geoset group 15xx
|
// Cape (slot 10) → group 15
|
||||||
if (extra.equipDisplayId[10] != 0) {
|
if (extra.equipDisplayId[10] != 0) {
|
||||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[10]);
|
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[10]);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
geosetCape = 1502; // Show cloak mesh
|
geosetCape = 1502;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3406,12 +3491,13 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
// Apply equipment geosets
|
// Apply equipment geosets
|
||||||
activeGeosets.insert(geosetGloves);
|
activeGeosets.insert(geosetGloves);
|
||||||
activeGeosets.insert(geosetBoots);
|
activeGeosets.insert(geosetBoots);
|
||||||
activeGeosets.insert(geosetChest);
|
activeGeosets.insert(geosetSleeves);
|
||||||
activeGeosets.insert(geosetPants);
|
activeGeosets.insert(geosetPants);
|
||||||
activeGeosets.insert(geosetCape);
|
activeGeosets.insert(geosetCape);
|
||||||
activeGeosets.insert(geosetTabard);
|
activeGeosets.insert(geosetTabard);
|
||||||
activeGeosets.insert(702); // Ears: default
|
activeGeosets.insert(702); // Ears: default
|
||||||
activeGeosets.insert(802); // Wristbands: default
|
activeGeosets.insert(902); // Kneepads: default
|
||||||
|
activeGeosets.insert(2002); // Bare feet mesh
|
||||||
|
|
||||||
// Hide hair under helmets: replace style-specific scalp with bald scalp
|
// Hide hair under helmets: replace style-specific scalp with bald scalp
|
||||||
if (extra.equipDisplayId[0] != 0 && hairGeoset > 1) {
|
if (extra.equipDisplayId[0] != 0 && hairGeoset > 1) {
|
||||||
|
|
@ -3440,7 +3526,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
LOG_INFO("NPC geosets for instance ", instanceId, ": [", geosetList, "]");
|
LOG_INFO("NPC geosets for instance ", instanceId, ": [", geosetList, "]");
|
||||||
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
||||||
LOG_DEBUG("Set humanoid geosets: hair=", (int)hairGeoset,
|
LOG_DEBUG("Set humanoid geosets: hair=", (int)hairGeoset,
|
||||||
" chest=", geosetChest, " pants=", geosetPants,
|
" sleeves=", geosetSleeves, " pants=", geosetPants,
|
||||||
" boots=", geosetBoots, " gloves=", geosetGloves);
|
" boots=", geosetBoots, " gloves=", geosetGloves);
|
||||||
|
|
||||||
// Load and attach helmet model if equipped
|
// Load and attach helmet model if equipped
|
||||||
|
|
@ -3617,6 +3703,16 @@ void Application::spawnOnlinePlayer(uint64_t guid,
|
||||||
if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !assetManager->isInitialized()) return;
|
if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !assetManager->isInitialized()) return;
|
||||||
if (playerInstances_.count(guid)) return;
|
if (playerInstances_.count(guid)) return;
|
||||||
|
|
||||||
|
// Skip local player — already spawned as the main character
|
||||||
|
if (gameHandler) {
|
||||||
|
uint64_t localGuid = gameHandler->getPlayerGuid();
|
||||||
|
uint64_t activeGuid = gameHandler->getActiveCharacterGuid();
|
||||||
|
if ((localGuid != 0 && guid == localGuid) ||
|
||||||
|
(activeGuid != 0 && guid == activeGuid) ||
|
||||||
|
(spawnedPlayerGuid_ != 0 && guid == spawnedPlayerGuid_)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
auto* charRenderer = renderer->getCharacterRenderer();
|
auto* charRenderer = renderer->getCharacterRenderer();
|
||||||
|
|
||||||
// Base geometry model: cache by (race, gender)
|
// Base geometry model: cache by (race, gender)
|
||||||
|
|
@ -3828,16 +3924,18 @@ void Application::spawnOnlinePlayer(uint64_t guid,
|
||||||
|
|
||||||
// Geosets: body + hair/facial hair selections
|
// Geosets: body + hair/facial hair selections
|
||||||
std::unordered_set<uint16_t> activeGeosets;
|
std::unordered_set<uint16_t> activeGeosets;
|
||||||
for (uint16_t i = 0; i <= 18; i++) activeGeosets.insert(i);
|
// Body parts (group 0: IDs 0-99, some models use up to 27)
|
||||||
|
for (uint16_t i = 0; i <= 99; i++) activeGeosets.insert(i);
|
||||||
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
|
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
|
||||||
activeGeosets.insert(static_cast<uint16_t>(200 + facialFeatures + 1));
|
activeGeosets.insert(static_cast<uint16_t>(200 + facialFeatures + 1));
|
||||||
activeGeosets.insert(302);
|
activeGeosets.insert(401); // Bare forearms (no gloves) — group 4
|
||||||
activeGeosets.insert(401);
|
activeGeosets.insert(502); // Bare shins (no boots) — group 5 (wider mesh)
|
||||||
activeGeosets.insert(501);
|
activeGeosets.insert(702); // Ears
|
||||||
activeGeosets.insert(702);
|
activeGeosets.insert(801); // Bare wrists (no sleeves) — group 8
|
||||||
activeGeosets.insert(802);
|
activeGeosets.insert(902); // Kneepads — group 9
|
||||||
activeGeosets.insert(1301);
|
activeGeosets.insert(1301); // Bare legs — group 13
|
||||||
activeGeosets.insert(1502);
|
activeGeosets.insert(1502); // No cloak — group 15
|
||||||
|
activeGeosets.insert(2002); // Bare feet — group 20
|
||||||
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
||||||
|
|
||||||
charRenderer->playAnimation(instanceId, 0, true);
|
charRenderer->playAnimation(instanceId, 0, true);
|
||||||
|
|
@ -3851,7 +3949,10 @@ void Application::spawnOnlinePlayer(uint64_t guid,
|
||||||
st.appearanceBytes = appearanceBytes;
|
st.appearanceBytes = appearanceBytes;
|
||||||
st.facialFeatures = facialFeatures;
|
st.facialFeatures = facialFeatures;
|
||||||
st.bodySkinPath = bodySkinPath;
|
st.bodySkinPath = bodySkinPath;
|
||||||
st.underwearPaths = underwearPaths;
|
// Include face textures so compositeWithRegions can rebuild the full base
|
||||||
|
if (!faceLowerPath.empty()) st.underwearPaths.push_back(faceLowerPath);
|
||||||
|
if (!faceUpperPath.empty()) st.underwearPaths.push_back(faceUpperPath);
|
||||||
|
for (const auto& up : underwearPaths) st.underwearPaths.push_back(up);
|
||||||
onlinePlayerAppearance_[guid] = std::move(st);
|
onlinePlayerAppearance_[guid] = std::move(st);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3860,6 +3961,13 @@ void Application::setOnlinePlayerEquipment(uint64_t guid,
|
||||||
const std::array<uint8_t, 19>& inventoryTypes) {
|
const std::array<uint8_t, 19>& inventoryTypes) {
|
||||||
if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !assetManager->isInitialized()) return;
|
if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !assetManager->isInitialized()) return;
|
||||||
|
|
||||||
|
// Skip local player — equipment handled by GameScreen::updateCharacterGeosets/Textures
|
||||||
|
// via consumeOnlineEquipmentDirty(), which fires on the same server update.
|
||||||
|
if (gameHandler) {
|
||||||
|
uint64_t localGuid = gameHandler->getPlayerGuid();
|
||||||
|
if (localGuid != 0 && guid == localGuid) return;
|
||||||
|
}
|
||||||
|
|
||||||
// If the player isn't spawned yet, store equipment until spawn.
|
// If the player isn't spawned yet, store equipment until spawn.
|
||||||
if (!playerInstances_.count(guid) || !onlinePlayerAppearance_.count(guid)) {
|
if (!playerInstances_.count(guid) || !onlinePlayerAppearance_.count(guid)) {
|
||||||
pendingOnlinePlayerEquipment_[guid] = {displayInfoIds, inventoryTypes};
|
pendingOnlinePlayerEquipment_[guid] = {displayInfoIds, inventoryTypes};
|
||||||
|
|
@ -3911,12 +4019,17 @@ void Application::setOnlinePlayerEquipment(uint64_t guid,
|
||||||
|
|
||||||
// --- Geosets ---
|
// --- Geosets ---
|
||||||
std::unordered_set<uint16_t> geosets;
|
std::unordered_set<uint16_t> geosets;
|
||||||
for (uint16_t i = 0; i <= 18; i++) geosets.insert(i);
|
// Body parts (group 0: IDs 0-99, some models use up to 27)
|
||||||
|
for (uint16_t i = 0; i <= 99; i++) geosets.insert(i);
|
||||||
|
|
||||||
uint8_t hairStyleId = static_cast<uint8_t>((st.appearanceBytes >> 16) & 0xFF);
|
uint8_t hairStyleId = static_cast<uint8_t>((st.appearanceBytes >> 16) & 0xFF);
|
||||||
geosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
|
geosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
|
||||||
geosets.insert(static_cast<uint16_t>(200 + st.facialFeatures + 1));
|
geosets.insert(static_cast<uint16_t>(200 + st.facialFeatures + 1));
|
||||||
geosets.insert(701);
|
geosets.insert(401); // Body joint patches (knees)
|
||||||
|
geosets.insert(402); // Body joint patches (elbows)
|
||||||
|
geosets.insert(701); // Ears
|
||||||
|
geosets.insert(902); // Kneepads
|
||||||
|
geosets.insert(2002); // Bare feet mesh
|
||||||
|
|
||||||
const uint32_t geosetGroup1Field = idiL ? (*idiL)["GeosetGroup1"] : 7;
|
const uint32_t geosetGroup1Field = idiL ? (*idiL)["GeosetGroup1"] : 7;
|
||||||
const uint32_t geosetGroup3Field = idiL ? (*idiL)["GeosetGroup3"] : 9;
|
const uint32_t geosetGroup3Field = idiL ? (*idiL)["GeosetGroup3"] : 9;
|
||||||
|
|
@ -3940,11 +4053,11 @@ void Application::setOnlinePlayerEquipment(uint64_t guid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feet (invType 8)
|
// Feet (invType 8): 401/402 are body patches (always on), 403+ are boot meshes
|
||||||
{
|
{
|
||||||
uint32_t did = findDisplayIdByInvType({8});
|
uint32_t did = findDisplayIdByInvType({8});
|
||||||
uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field);
|
uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field);
|
||||||
geosets.insert(static_cast<uint16_t>(gg1 > 0 ? 401 + gg1 : 401));
|
if (gg1 > 0) geosets.insert(static_cast<uint16_t>(402 + gg1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hands (invType 10)
|
// Hands (invType 10)
|
||||||
|
|
|
||||||
|
|
@ -3347,7 +3347,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
// Trigger creature spawn callback for units/players with displayId
|
// Trigger creature spawn callback for units/players with displayId
|
||||||
if ((block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) && unit->getDisplayId() != 0) {
|
if ((block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) && unit->getDisplayId() != 0) {
|
||||||
if (block.objectType == ObjectType::PLAYER && block.guid != playerGuid) {
|
if (block.objectType == ObjectType::PLAYER && block.guid == playerGuid) {
|
||||||
|
// Skip local player — spawned separately via spawnPlayerCharacter()
|
||||||
|
} else if (block.objectType == ObjectType::PLAYER) {
|
||||||
if (playerSpawnCallback_) {
|
if (playerSpawnCallback_) {
|
||||||
uint8_t race = 0, gender = 0, facial = 0;
|
uint8_t race = 0, gender = 0, facial = 0;
|
||||||
uint32_t appearanceBytes = 0;
|
uint32_t appearanceBytes = 0;
|
||||||
|
|
@ -3725,7 +3727,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
displayIdChanged &&
|
displayIdChanged &&
|
||||||
unit->getDisplayId() != 0 &&
|
unit->getDisplayId() != 0 &&
|
||||||
unit->getDisplayId() != oldDisplayId) {
|
unit->getDisplayId() != oldDisplayId) {
|
||||||
if (entity->getType() == ObjectType::PLAYER && block.guid != playerGuid) {
|
if (entity->getType() == ObjectType::PLAYER && block.guid == playerGuid) {
|
||||||
|
// Skip local player — spawned separately
|
||||||
|
} else if (entity->getType() == ObjectType::PLAYER) {
|
||||||
if (playerSpawnCallback_) {
|
if (playerSpawnCallback_) {
|
||||||
uint8_t race = 0, gender = 0, facial = 0;
|
uint8_t race = 0, gender = 0, facial = 0;
|
||||||
uint32_t appearanceBytes = 0;
|
uint32_t appearanceBytes = 0;
|
||||||
|
|
|
||||||
|
|
@ -966,6 +966,12 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
||||||
model.textureLookup = readArray<uint16_t>(m2Data, header.ofsTexLookup, header.nTexLookup);
|
model.textureLookup = readArray<uint16_t>(m2Data, header.ofsTexLookup, header.nTexLookup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read bone lookup table (vertex bone indices reference this to get actual bone index)
|
||||||
|
if (header.nBoneLookupTable > 0 && header.ofsBoneLookupTable > 0) {
|
||||||
|
model.boneLookupTable = readArray<uint16_t>(m2Data, header.ofsBoneLookupTable, header.nBoneLookupTable);
|
||||||
|
core::Logger::getInstance().debug(" BoneLookupTable: ", model.boneLookupTable.size(), " entries");
|
||||||
|
}
|
||||||
|
|
||||||
// Read render flags / materials (blend modes)
|
// Read render flags / materials (blend modes)
|
||||||
if (header.nRenderFlags > 0 && header.ofsRenderFlags > 0) {
|
if (header.nRenderFlags > 0 && header.ofsRenderFlags > 0) {
|
||||||
struct M2MaterialDisk { uint16_t flags; uint16_t blendMode; };
|
struct M2MaterialDisk { uint16_t flags; uint16_t blendMode; };
|
||||||
|
|
|
||||||
|
|
@ -333,13 +333,14 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
||||||
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyle + 1));
|
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyle + 1));
|
||||||
// Facial hair geoset: group 2 = 200 + variation + 1
|
// Facial hair geoset: group 2 = 200 + variation + 1
|
||||||
activeGeosets.insert(static_cast<uint16_t>(200 + facialHair + 1));
|
activeGeosets.insert(static_cast<uint16_t>(200 + facialHair + 1));
|
||||||
activeGeosets.insert(302); // Gloves: bare hands
|
activeGeosets.insert(401); // Bare forearms (no gloves) — group 4
|
||||||
activeGeosets.insert(401); // Boots: bare feet
|
activeGeosets.insert(502); // Bare shins (no boots) — group 5 (wider mesh)
|
||||||
activeGeosets.insert(501); // Chest: bare
|
|
||||||
activeGeosets.insert(702); // Ears: default
|
activeGeosets.insert(702); // Ears: default
|
||||||
activeGeosets.insert(802); // Wristbands: default
|
activeGeosets.insert(801); // Bare wrists (no sleeves) — group 8
|
||||||
activeGeosets.insert(1301); // Trousers: bare legs
|
activeGeosets.insert(902); // Kneepads: default — group 9
|
||||||
activeGeosets.insert(1502); // Back body (cloak=none)
|
activeGeosets.insert(1301); // Bare legs (no pants) — group 13
|
||||||
|
activeGeosets.insert(1502); // No cloak — group 15
|
||||||
|
activeGeosets.insert(2002); // Bare feet mesh — group 20
|
||||||
charRenderer_->setActiveGeosets(instanceId_, activeGeosets);
|
charRenderer_->setActiveGeosets(instanceId_, activeGeosets);
|
||||||
|
|
||||||
// Play idle animation (Stand = animation ID 0)
|
// Play idle animation (Stand = animation ID 0)
|
||||||
|
|
@ -412,44 +413,46 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
|
||||||
geosets.insert(static_cast<uint16_t>(100 + hairStyle_ + 1)); // Hair style
|
geosets.insert(static_cast<uint16_t>(100 + hairStyle_ + 1)); // Hair style
|
||||||
geosets.insert(static_cast<uint16_t>(200 + facialHair_ + 1)); // Facial hair
|
geosets.insert(static_cast<uint16_t>(200 + facialHair_ + 1)); // Facial hair
|
||||||
geosets.insert(701); // Ears
|
geosets.insert(701); // Ears
|
||||||
|
geosets.insert(902); // Kneepads: default (group 9)
|
||||||
|
geosets.insert(2002); // Bare feet mesh (group 20 = CG_FEET)
|
||||||
|
|
||||||
// Default naked geosets
|
// CharGeosets: group 4=gloves(forearm), 5=boots(shin), 8=sleeves, 13=pants
|
||||||
uint16_t geosetGloves = 301;
|
uint16_t geosetGloves = 401; // Bare forearms (group 4)
|
||||||
uint16_t geosetBoots = 401;
|
uint16_t geosetBoots = 502; // Bare shins (group 5, wider mesh)
|
||||||
uint16_t geosetChest = 501;
|
uint16_t geosetSleeves = 801; // Bare wrists (group 8)
|
||||||
uint16_t geosetPants = 1301;
|
uint16_t geosetPants = 1301; // Bare legs (group 13)
|
||||||
|
|
||||||
// Chest/Shirt/Robe
|
// Chest/Shirt/Robe → group 8 (sleeves)
|
||||||
{
|
{
|
||||||
uint32_t did = findDisplayId({4, 5, 20});
|
uint32_t did = findDisplayId({4, 5, 20});
|
||||||
uint32_t gg = getGeosetGroup(did, 0);
|
uint32_t gg = getGeosetGroup(did, 0);
|
||||||
if (gg > 0) geosetChest = static_cast<uint16_t>(501 + gg);
|
if (gg > 0) geosetSleeves = static_cast<uint16_t>(801 + gg);
|
||||||
// Robe kilt legs
|
// Robe kilt legs
|
||||||
uint32_t gg3 = getGeosetGroup(did, 2);
|
uint32_t gg3 = getGeosetGroup(did, 2);
|
||||||
if (gg3 > 0) geosetPants = static_cast<uint16_t>(1301 + gg3);
|
if (gg3 > 0) geosetPants = static_cast<uint16_t>(1301 + gg3);
|
||||||
}
|
}
|
||||||
// Legs
|
// Legs → group 13 (trousers)
|
||||||
{
|
{
|
||||||
uint32_t did = findDisplayId({7});
|
uint32_t did = findDisplayId({7});
|
||||||
uint32_t gg = getGeosetGroup(did, 0);
|
uint32_t gg = getGeosetGroup(did, 0);
|
||||||
if (gg > 0) geosetPants = static_cast<uint16_t>(1301 + gg);
|
if (gg > 0) geosetPants = static_cast<uint16_t>(1301 + gg);
|
||||||
}
|
}
|
||||||
// Feet
|
// Boots → group 5 (shins)
|
||||||
{
|
{
|
||||||
uint32_t did = findDisplayId({8});
|
uint32_t did = findDisplayId({8});
|
||||||
uint32_t gg = getGeosetGroup(did, 0);
|
uint32_t gg = getGeosetGroup(did, 0);
|
||||||
if (gg > 0) geosetBoots = static_cast<uint16_t>(401 + gg);
|
if (gg > 0) geosetBoots = static_cast<uint16_t>(501 + gg);
|
||||||
}
|
}
|
||||||
// Hands
|
// Gloves → group 4 (forearms)
|
||||||
{
|
{
|
||||||
uint32_t did = findDisplayId({10});
|
uint32_t did = findDisplayId({10});
|
||||||
uint32_t gg = getGeosetGroup(did, 0);
|
uint32_t gg = getGeosetGroup(did, 0);
|
||||||
if (gg > 0) geosetGloves = static_cast<uint16_t>(301 + gg);
|
if (gg > 0) geosetGloves = static_cast<uint16_t>(401 + gg);
|
||||||
}
|
}
|
||||||
|
|
||||||
geosets.insert(geosetGloves);
|
geosets.insert(geosetGloves);
|
||||||
geosets.insert(geosetBoots);
|
geosets.insert(geosetBoots);
|
||||||
geosets.insert(geosetChest);
|
geosets.insert(geosetSleeves);
|
||||||
geosets.insert(geosetPants);
|
geosets.insert(geosetPants);
|
||||||
geosets.insert(hasInvType({16}) ? 1502 : 1501); // Cloak mesh toggle (visual may still be limited)
|
geosets.insert(hasInvType({16}) ? 1502 : 1501); // Cloak mesh toggle (visual may still be limited)
|
||||||
if (hasInvType({19})) geosets.insert(1201); // Tabard mesh toggle
|
if (hasInvType({19})) geosets.insert(1201); // Tabard mesh toggle
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ bool CharacterRenderer::initialize() {
|
||||||
uniform mat4 uModel;
|
uniform mat4 uModel;
|
||||||
uniform mat4 uView;
|
uniform mat4 uView;
|
||||||
uniform mat4 uProjection;
|
uniform mat4 uProjection;
|
||||||
uniform mat4 uBones[200];
|
uniform mat4 uBones[240];
|
||||||
|
|
||||||
out vec3 FragPos;
|
out vec3 FragPos;
|
||||||
out vec3 Normal;
|
out vec3 Normal;
|
||||||
|
|
@ -205,7 +205,7 @@ bool CharacterRenderer::initialize() {
|
||||||
|
|
||||||
uniform mat4 uLightSpaceMatrix;
|
uniform mat4 uLightSpaceMatrix;
|
||||||
uniform mat4 uModel;
|
uniform mat4 uModel;
|
||||||
uniform mat4 uBones[200];
|
uniform mat4 uBones[240];
|
||||||
|
|
||||||
out vec2 vTexCoord;
|
out vec2 vTexCoord;
|
||||||
|
|
||||||
|
|
@ -556,20 +556,7 @@ GLuint CharacterRenderer::compositeTextures(const std::vector<std::string>& laye
|
||||||
// Debug dump removed: it was always-on and could stall badly under load.
|
// Debug dump removed: it was always-on and could stall badly under load.
|
||||||
|
|
||||||
// Debug: dump first composite to /tmp for visual inspection
|
// Debug: dump first composite to /tmp for visual inspection
|
||||||
{
|
|
||||||
static bool dumped = false;
|
|
||||||
if (!dumped && layerPaths.size() > 1) {
|
|
||||||
dumped = true;
|
|
||||||
std::string dumpPath = "/tmp/wowee_composite_debug.raw";
|
|
||||||
FILE* f = fopen(dumpPath.c_str(), "wb");
|
|
||||||
if (f) {
|
|
||||||
fwrite(composite.data(), 1, composite.size(), f);
|
|
||||||
fclose(f);
|
|
||||||
core::Logger::getInstance().info("DEBUG: dumped composite ", width, "x", height,
|
|
||||||
" RGBA to ", dumpPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload composite to GPU
|
// Upload composite to GPU
|
||||||
GLuint texId;
|
GLuint texId;
|
||||||
|
|
@ -588,6 +575,26 @@ GLuint CharacterRenderer::compositeTextures(const std::vector<std::string>& laye
|
||||||
return texId;
|
return texId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CharacterRenderer::clearCompositeCache() {
|
||||||
|
// Delete GPU textures that aren't referenced by any model's texture slots
|
||||||
|
for (auto& [key, texId] : compositeCache_) {
|
||||||
|
if (texId && texId != whiteTexture) {
|
||||||
|
// Check if any model still references this texture
|
||||||
|
bool inUse = false;
|
||||||
|
for (const auto& [mid, gm] : models) {
|
||||||
|
for (GLuint tid : gm.textureIds) {
|
||||||
|
if (tid == texId) { inUse = true; break; }
|
||||||
|
}
|
||||||
|
if (inUse) break;
|
||||||
|
}
|
||||||
|
if (!inUse) {
|
||||||
|
glDeleteTextures(1, &texId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compositeCache_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
const std::vector<std::string>& baseLayers,
|
const std::vector<std::string>& baseLayers,
|
||||||
const std::vector<std::pair<int, std::string>>& regionLayers) {
|
const std::vector<std::pair<int, std::string>>& regionLayers) {
|
||||||
|
|
@ -606,16 +613,17 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
return cacheIt->second;
|
return cacheIt->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Region index → pixel coordinates on the 512x512 atlas
|
// Region index → pixel coordinates on the 256x256 base atlas
|
||||||
static const int regionCoords[][2] = {
|
// These are scaled up by (width/256, height/256) for larger textures (512x512, 1024x1024)
|
||||||
|
static const int regionCoords256[][2] = {
|
||||||
{ 0, 0 }, // 0 = ArmUpper
|
{ 0, 0 }, // 0 = ArmUpper
|
||||||
{ 0, 128 }, // 1 = ArmLower
|
{ 0, 64 }, // 1 = ArmLower
|
||||||
{ 0, 256 }, // 2 = Hand
|
{ 0, 128 }, // 2 = Hand
|
||||||
{ 256, 0 }, // 3 = TorsoUpper
|
{ 128, 0 }, // 3 = TorsoUpper
|
||||||
{ 256, 128 }, // 4 = TorsoLower
|
{ 128, 64 }, // 4 = TorsoLower
|
||||||
{ 256, 192 }, // 5 = LegUpper
|
{ 128, 96 }, // 5 = LegUpper
|
||||||
{ 256, 320 }, // 6 = LegLower
|
{ 128, 160 }, // 6 = LegLower
|
||||||
{ 256, 448 }, // 7 = Foot
|
{ 128, 224 }, // 7 = Foot
|
||||||
};
|
};
|
||||||
|
|
||||||
// First, build base skin + underwear using existing compositeTextures
|
// First, build base skin + underwear using existing compositeTextures
|
||||||
|
|
@ -638,6 +646,8 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
int width = base.width;
|
int width = base.width;
|
||||||
int height = base.height;
|
int height = base.height;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// If base texture is 256x256 (e.g., baked NPC texture), upscale to 512x512
|
// If base texture is 256x256 (e.g., baked NPC texture), upscale to 512x512
|
||||||
// so equipment regions can be composited at correct coordinates
|
// so equipment regions can be composited at correct coordinates
|
||||||
if (width == 256 && height == 256 && !regionLayers.empty()) {
|
if (width == 256 && height == 256 && !regionLayers.empty()) {
|
||||||
|
|
@ -663,8 +673,8 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blend face + underwear overlays
|
// Blend face + underwear overlays
|
||||||
// These are native-resolution textures (designed for 256x256 base).
|
// If we upscaled from 256→512, scale coords and texels with blitOverlayScaled2x.
|
||||||
// If we upscaled the base to 512x512, use blitOverlayScaled2x and 2x coords.
|
// For native 512/1024 textures, face overlays are full atlas size (hit width==width branch).
|
||||||
bool upscaled = (base.width == 256 && base.height == 256 && width == 512);
|
bool upscaled = (base.width == 256 && base.height == 256 && width == 512);
|
||||||
for (const auto& ul : baseLayers) {
|
for (const auto& ul : baseLayers) {
|
||||||
if (ul.empty()) continue;
|
if (ul.empty()) continue;
|
||||||
|
|
@ -679,6 +689,11 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
std::string pathLower = ul;
|
std::string pathLower = ul;
|
||||||
for (auto& c : pathLower) c = std::tolower(c);
|
for (auto& c : pathLower) c = std::tolower(c);
|
||||||
|
|
||||||
|
// Scale factor from 256-base coordinates to actual canvas size
|
||||||
|
int coordScale = width / 256;
|
||||||
|
if (coordScale < 1) coordScale = 1;
|
||||||
|
bool useScale = true;
|
||||||
|
|
||||||
if (pathLower.find("faceupper") != std::string::npos) {
|
if (pathLower.find("faceupper") != std::string::npos) {
|
||||||
dstX = 0; dstY = 160;
|
dstX = 0; dstY = 160;
|
||||||
} else if (pathLower.find("facelower") != std::string::npos) {
|
} else if (pathLower.find("facelower") != std::string::npos) {
|
||||||
|
|
@ -698,14 +713,19 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
} else if (pathLower.find("legupper") != std::string::npos || pathLower.find("leg") != std::string::npos) {
|
} else if (pathLower.find("legupper") != std::string::npos || pathLower.find("leg") != std::string::npos) {
|
||||||
dstX = 128; dstY = 160;
|
dstX = 128; dstY = 160;
|
||||||
} else {
|
} else {
|
||||||
dstX = (base.width - overlay.width) / 2;
|
// Fallback: center overlay on canvas (already in canvas coords)
|
||||||
dstY = (base.height - overlay.height) / 2;
|
dstX = (width - overlay.width) / 2;
|
||||||
|
dstY = (height - overlay.height) / 2;
|
||||||
|
useScale = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useScale) {
|
||||||
|
dstX *= coordScale;
|
||||||
|
dstY *= coordScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (upscaled) {
|
if (upscaled) {
|
||||||
// Scale coords and texels to match 512x512 canvas
|
// Overlay is 256-base sized, needs 2x texel scaling for 512 canvas
|
||||||
dstX *= 2;
|
|
||||||
dstY *= 2;
|
|
||||||
blitOverlayScaled2x(composite, width, height, overlay, dstX, dstY);
|
blitOverlayScaled2x(composite, width, height, overlay, dstX, dstY);
|
||||||
} else {
|
} else {
|
||||||
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
||||||
|
|
@ -713,18 +733,24 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expected region sizes on the 512x512 atlas
|
// Expected region sizes on the 256x256 base atlas (scaled like coords)
|
||||||
static const int regionSizes[][2] = {
|
static const int regionSizes256[][2] = {
|
||||||
{ 256, 128 }, // 0 = ArmUpper
|
{ 128, 64 }, // 0 = ArmUpper
|
||||||
{ 256, 128 }, // 1 = ArmLower
|
{ 128, 64 }, // 1 = ArmLower
|
||||||
{ 256, 64 }, // 2 = Hand
|
{ 128, 32 }, // 2 = Hand
|
||||||
{ 256, 128 }, // 3 = TorsoUpper
|
{ 128, 64 }, // 3 = TorsoUpper
|
||||||
{ 256, 64 }, // 4 = TorsoLower
|
{ 128, 32 }, // 4 = TorsoLower
|
||||||
{ 256, 128 }, // 5 = LegUpper
|
{ 128, 64 }, // 5 = LegUpper
|
||||||
{ 256, 128 }, // 6 = LegLower
|
{ 128, 64 }, // 6 = LegLower
|
||||||
{ 256, 64 }, // 7 = Foot
|
{ 128, 32 }, // 7 = Foot
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Scale factor from 256-base to actual texture size
|
||||||
|
int scaleX = width / 256;
|
||||||
|
int scaleY = height / 256;
|
||||||
|
if (scaleX < 1) scaleX = 1;
|
||||||
|
if (scaleY < 1) scaleY = 1;
|
||||||
|
|
||||||
// Now blit equipment region textures at explicit coordinates
|
// Now blit equipment region textures at explicit coordinates
|
||||||
for (const auto& rl : regionLayers) {
|
for (const auto& rl : regionLayers) {
|
||||||
int regionIdx = rl.first;
|
int regionIdx = rl.first;
|
||||||
|
|
@ -736,12 +762,12 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int dstX = regionCoords[regionIdx][0];
|
int dstX = regionCoords256[regionIdx][0] * scaleX;
|
||||||
int dstY = regionCoords[regionIdx][1];
|
int dstY = regionCoords256[regionIdx][1] * scaleY;
|
||||||
|
|
||||||
// Component textures are stored at half resolution — scale 2x if needed
|
// Expected full-resolution size for this region at current atlas scale
|
||||||
int expectedW = regionSizes[regionIdx][0];
|
int expectedW = regionSizes256[regionIdx][0] * scaleX;
|
||||||
int expectedH = regionSizes[regionIdx][1];
|
int expectedH = regionSizes256[regionIdx][1] * scaleY;
|
||||||
if (overlay.width * 2 == expectedW && overlay.height * 2 == expectedH) {
|
if (overlay.width * 2 == expectedW && overlay.height * 2 == expectedH) {
|
||||||
blitOverlayScaled2x(composite, width, height, overlay, dstX, dstY);
|
blitOverlayScaled2x(composite, width, height, overlay, dstX, dstY);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -752,6 +778,26 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
" at (", dstX, ",", dstY, ") ", overlay.width, "x", overlay.height, " from ", rl.second);
|
" at (", dstX, ",", dstY, ") ", overlay.width, "x", overlay.height, " from ", rl.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug: dump composite to /tmp for visual inspection
|
||||||
|
{
|
||||||
|
static int dumpCount = 0;
|
||||||
|
if (dumpCount < 6) {
|
||||||
|
dumpCount++;
|
||||||
|
std::string dumpPath = "/tmp/wowee_composite_" + std::to_string(dumpCount) + ".ppm";
|
||||||
|
FILE* f = fopen(dumpPath.c_str(), "wb");
|
||||||
|
if (f) {
|
||||||
|
fprintf(f, "P6\n%d %d\n255\n", width, height);
|
||||||
|
for (int i = 0; i < width * height; i++) {
|
||||||
|
fputc(composite[i * 4 + 0], f);
|
||||||
|
fputc(composite[i * 4 + 1], f);
|
||||||
|
fputc(composite[i * 4 + 2], f);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
core::Logger::getInstance().info("compositeWithRegions: dumped to ", dumpPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Upload to GPU
|
// Upload to GPU
|
||||||
GLuint texId;
|
GLuint texId;
|
||||||
glGenTextures(1, &texId);
|
glGenTextures(1, &texId);
|
||||||
|
|
@ -841,6 +887,37 @@ bool CharacterRenderer::loadModel(const pipeline::M2Model& model, uint32_t id) {
|
||||||
" verts, ", model.bones.size(), " bones, ", model.sequences.size(),
|
" verts, ", model.bones.size(), " bones, ", model.sequences.size(),
|
||||||
" anims, ", model.textures.size(), " textures)");
|
" anims, ", model.textures.size(), " textures)");
|
||||||
|
|
||||||
|
// Debug: dump vertex bounding boxes per submesh group for player model
|
||||||
|
if (id == 1) {
|
||||||
|
core::Logger::getInstance().info("MODEL1_VERSION: ", model.version);
|
||||||
|
std::map<uint16_t, std::array<float,6>> groupBounds; // group -> minX,minY,minZ,maxX,maxY,maxZ
|
||||||
|
for (const auto& b : model.batches) {
|
||||||
|
uint16_t grp = b.submeshId;
|
||||||
|
for (uint32_t idx = b.indexStart; idx < b.indexStart + b.indexCount && idx < model.indices.size(); idx++) {
|
||||||
|
uint16_t vi = model.indices[idx];
|
||||||
|
if (vi >= model.vertices.size()) continue;
|
||||||
|
const auto& v = model.vertices[vi];
|
||||||
|
auto it = groupBounds.find(grp);
|
||||||
|
if (it == groupBounds.end()) {
|
||||||
|
groupBounds[grp] = {v.position.x, v.position.y, v.position.z,
|
||||||
|
v.position.x, v.position.y, v.position.z};
|
||||||
|
} else {
|
||||||
|
auto& bb = it->second;
|
||||||
|
bb[0] = std::min(bb[0], v.position.x);
|
||||||
|
bb[1] = std::min(bb[1], v.position.y);
|
||||||
|
bb[2] = std::min(bb[2], v.position.z);
|
||||||
|
bb[3] = std::max(bb[3], v.position.x);
|
||||||
|
bb[4] = std::max(bb[4], v.position.y);
|
||||||
|
bb[5] = std::max(bb[5], v.position.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& [grp, bb] : groupBounds) {
|
||||||
|
core::Logger::getInstance().info("MODEL1_BOUNDS: submesh=", grp,
|
||||||
|
" X[", bb[0], "..", bb[3], "] Y[", bb[1], "..", bb[4], "] Z[", bb[2], "..", bb[5], "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1367,11 +1444,24 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
||||||
if (texSlot >= gm.textureIds.size()) continue;
|
if (texSlot >= gm.textureIds.size()) continue;
|
||||||
|
|
||||||
GLuint texId = gm.textureIds[texSlot];
|
GLuint texId = gm.textureIds[texSlot];
|
||||||
auto itO = inst.textureSlotOverrides.find(texSlot);
|
|
||||||
if (itO != inst.textureSlotOverrides.end() && itO->second != 0) {
|
|
||||||
texId = itO->second;
|
|
||||||
}
|
|
||||||
uint32_t texType = (texSlot < gm.data.textures.size()) ? gm.data.textures[texSlot].type : 0;
|
uint32_t texType = (texSlot < gm.data.textures.size()) ? gm.data.textures[texSlot].type : 0;
|
||||||
|
// Apply texture slot overrides.
|
||||||
|
// For type-1 (skin) overrides, only apply to skin-group batches
|
||||||
|
// to prevent the skin composite from bleeding onto cloak/hair.
|
||||||
|
{
|
||||||
|
auto itO = inst.textureSlotOverrides.find(texSlot);
|
||||||
|
if (itO != inst.textureSlotOverrides.end() && itO->second != 0) {
|
||||||
|
if (texType == 1) {
|
||||||
|
// Only apply skin override to skin groups
|
||||||
|
uint16_t grp = b.submeshId / 100;
|
||||||
|
bool isSkinGroup = (grp == 0 || grp == 3 || grp == 4 || grp == 5 ||
|
||||||
|
grp == 8 || grp == 9 || grp == 13 || grp == 20);
|
||||||
|
if (isSkinGroup) texId = itO->second;
|
||||||
|
} else {
|
||||||
|
texId = itO->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasFirst) {
|
if (!hasFirst) {
|
||||||
first = {texId, texType};
|
first = {texId, texType};
|
||||||
|
|
@ -1440,14 +1530,17 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
||||||
// Resolve texture for this batch (prefer hair textures for hair geosets).
|
// Resolve texture for this batch (prefer hair textures for hair geosets).
|
||||||
GLuint texId = resolveBatchTexture(instance, gpuModel, batch);
|
GLuint texId = resolveBatchTexture(instance, 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
|
|
||||||
// that may not be set (e.g., baked NPC textures only set slot 0)
|
// For body/equipment parts with white/fallback texture, use skin (type 1) texture.
|
||||||
// Only apply to body skin slots (type 1), NOT hair (type 6) or other types
|
// 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) {
|
if (texId == whiteTexture) {
|
||||||
uint16_t group = batch.submeshId / 100;
|
uint16_t group = batch.submeshId / 100;
|
||||||
if (group == 0) {
|
bool isSkinGroup = (group == 0 || group == 3 || group == 4 || group == 5 ||
|
||||||
// Check if this batch's texture slot is a body skin type
|
group == 8 || group == 9 || group == 13);
|
||||||
|
if (isSkinGroup) {
|
||||||
|
// Check if this batch's texture slot is a hair type (don't override hair)
|
||||||
uint32_t texType = 0;
|
uint32_t texType = 0;
|
||||||
if (batch.textureIndex < gpuModel.data.textureLookup.size()) {
|
if (batch.textureIndex < gpuModel.data.textureLookup.size()) {
|
||||||
uint16_t lk = gpuModel.data.textureLookup[batch.textureIndex];
|
uint16_t lk = gpuModel.data.textureLookup[batch.textureIndex];
|
||||||
|
|
@ -1455,7 +1548,6 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
||||||
texType = gpuModel.data.textures[lk].type;
|
texType = gpuModel.data.textures[lk].type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only fall back for body skin (type 1), underwear (type 8), or cloak (type 2)
|
|
||||||
// Do NOT apply skin composite to hair (type 6) batches
|
// Do NOT apply skin composite to hair (type 6) batches
|
||||||
if (texType != 6) {
|
if (texType != 6) {
|
||||||
for (size_t ti = 0; ti < gpuModel.textureIds.size(); ti++) {
|
for (size_t ti = 0; ti < gpuModel.textureIds.size(); ti++) {
|
||||||
|
|
|
||||||
|
|
@ -2628,9 +2628,9 @@ void GameScreen::updateCharacterGeosets(game::Inventory& inventory) {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Base geosets always present
|
// Base geosets always present (group 0: IDs 0-99, some models use up to 27)
|
||||||
std::unordered_set<uint16_t> geosets;
|
std::unordered_set<uint16_t> geosets;
|
||||||
for (uint16_t i = 0; i <= 18; i++) {
|
for (uint16_t i = 0; i <= 99; i++) {
|
||||||
geosets.insert(i);
|
geosets.insert(i);
|
||||||
}
|
}
|
||||||
// Hair/facial geosets must match the active character's appearance, otherwise
|
// Hair/facial geosets must match the active character's appearance, otherwise
|
||||||
|
|
@ -2647,52 +2647,67 @@ void GameScreen::updateCharacterGeosets(game::Inventory& inventory) {
|
||||||
geosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1)); // Group 1 hair
|
geosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1)); // Group 1 hair
|
||||||
geosets.insert(static_cast<uint16_t>(200 + facialId + 1)); // Group 2 facial
|
geosets.insert(static_cast<uint16_t>(200 + facialId + 1)); // Group 2 facial
|
||||||
}
|
}
|
||||||
geosets.insert(701); // Ears
|
geosets.insert(702); // Ears: visible (default)
|
||||||
|
geosets.insert(2002); // Bare feet mesh (group 20 = CG_FEET, always on)
|
||||||
|
|
||||||
|
// CharGeosets mapping (verified via vertex bounding boxes):
|
||||||
|
// Group 4 (401+) = GLOVES (forearm area, Z~1.1-1.4)
|
||||||
|
// Group 5 (501+) = BOOTS (shin area, Z~0.1-0.6)
|
||||||
|
// Group 8 (801+) = WRISTBANDS/SLEEVES (controlled by chest armor)
|
||||||
|
// Group 9 (901+) = KNEEPADS
|
||||||
|
// Group 13 (1301+) = TROUSERS/PANTS
|
||||||
|
// Group 15 (1501+) = CAPE/CLOAK
|
||||||
|
// Group 20 (2002) = FEET
|
||||||
|
|
||||||
|
// Gloves: inventoryType 10 → group 4 (forearms)
|
||||||
|
// 401=bare forearms, 402+=glove styles covering forearm
|
||||||
|
{
|
||||||
|
uint32_t did = findEquippedDisplayId({10});
|
||||||
|
uint32_t gg = getGeosetGroup(did, 0);
|
||||||
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 401 + gg : 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boots: inventoryType 8 → group 5 (shins/lower legs)
|
||||||
|
// 501=narrow bare shin, 502=wider (matches thigh width better). Use 502 as bare default.
|
||||||
|
// When boots equipped, gg selects boot style: 501+gg (gg=1→502, gg=2→503, etc.)
|
||||||
|
{
|
||||||
|
uint32_t did = findEquippedDisplayId({8});
|
||||||
|
uint32_t gg = getGeosetGroup(did, 0);
|
||||||
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 501 + gg : 502));
|
||||||
|
}
|
||||||
|
|
||||||
// Chest/Shirt: inventoryType 4 (shirt), 5 (chest), 20 (robe)
|
// Chest/Shirt: inventoryType 4 (shirt), 5 (chest), 20 (robe)
|
||||||
// geosetGroup_1 > 0 → use mesh variant (502+), otherwise bare (501) + texture only
|
// Controls group 8 (wristbands/sleeve length): 801=bare wrists, 802+=sleeve styles
|
||||||
|
// Also controls group 13 (trousers) via GeosetGroup[2] for robes
|
||||||
{
|
{
|
||||||
uint32_t did = findEquippedDisplayId({4, 5, 20});
|
uint32_t did = findEquippedDisplayId({4, 5, 20});
|
||||||
uint32_t gg = getGeosetGroup(did, 0);
|
uint32_t gg = getGeosetGroup(did, 0);
|
||||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 501 + gg : (did > 0 ? 501 : 501)));
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 801 + gg : 801));
|
||||||
// geosetGroup_3 > 0 on robes also shows kilt legs (1302)
|
// Robe kilt: GeosetGroup[2] > 0 → show kilt legs (1302+)
|
||||||
uint32_t gg3 = getGeosetGroup(did, 2);
|
uint32_t gg3 = getGeosetGroup(did, 2);
|
||||||
if (gg3 > 0) {
|
if (gg3 > 0) {
|
||||||
geosets.insert(static_cast<uint16_t>(1301 + gg3));
|
geosets.insert(static_cast<uint16_t>(1301 + gg3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legs: inventoryType 7
|
// Kneepads: group 9 (always default 902)
|
||||||
// geosetGroup_1 > 0 → kilt/skirt mesh (1302+), otherwise bare legs (1301) + texture
|
geosets.insert(902);
|
||||||
|
|
||||||
|
// Legs/Pants: inventoryType 7 → group 13 (trousers/thighs)
|
||||||
|
// 1301=bare legs, 1302+=pant/kilt styles
|
||||||
{
|
{
|
||||||
uint32_t did = findEquippedDisplayId({7});
|
uint32_t did = findEquippedDisplayId({7});
|
||||||
uint32_t gg = getGeosetGroup(did, 0);
|
uint32_t gg = getGeosetGroup(did, 0);
|
||||||
// Only add leg geoset if robe hasn't already set a kilt geoset
|
// Only add if robe hasn't already set a kilt geoset
|
||||||
if (geosets.count(1302) == 0 && geosets.count(1303) == 0) {
|
if (geosets.count(1302) == 0 && geosets.count(1303) == 0) {
|
||||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 1301 + gg : 1301));
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 1301 + gg : 1301));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feet/Boots: inventoryType 8
|
// Back/Cloak: inventoryType 16 → group 15
|
||||||
// geosetGroup_1 > 0 → boot mesh (402+), otherwise bare feet (401) + texture
|
|
||||||
{
|
|
||||||
uint32_t did = findEquippedDisplayId({8});
|
|
||||||
uint32_t gg = getGeosetGroup(did, 0);
|
|
||||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 401 + gg : 401));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gloves/Hands: inventoryType 10
|
|
||||||
// geosetGroup_1 > 0 → glove mesh (302+), otherwise bare hands (301)
|
|
||||||
{
|
|
||||||
uint32_t did = findEquippedDisplayId({10});
|
|
||||||
uint32_t gg = getGeosetGroup(did, 0);
|
|
||||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 301 + gg : 301));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Back/Cloak: inventoryType 16 — geoset only, no skin texture (cloaks are separate models)
|
|
||||||
geosets.insert(hasEquippedType({16}) ? 1502 : 1501);
|
geosets.insert(hasEquippedType({16}) ? 1502 : 1501);
|
||||||
|
|
||||||
// Tabard: inventoryType 19
|
// Tabard: inventoryType 19 → group 12
|
||||||
if (hasEquippedType({19})) {
|
if (hasEquippedType({19})) {
|
||||||
geosets.insert(1201);
|
geosets.insert(1201);
|
||||||
}
|
}
|
||||||
|
|
@ -2789,15 +2804,28 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TEMP: log region layers for debugging
|
||||||
|
{
|
||||||
|
static const char* regionNames[] = {"ArmUpper","ArmLower","Hand","TorsoUpper","TorsoLower","LegUpper","LegLower","Foot"};
|
||||||
|
for (const auto& rl : regionLayers) {
|
||||||
|
LOG_INFO("TEX_REGION: region=", rl.first, "(", (rl.first < 8 ? regionNames[rl.first] : "?"), ") path=", rl.second);
|
||||||
|
}
|
||||||
|
LOG_INFO("TEX_REGION: total=", regionLayers.size(), " regions, baseSkin=", bodySkinPath);
|
||||||
|
}
|
||||||
|
|
||||||
// Re-composite: base skin + underwear + equipment regions
|
// Re-composite: base skin + underwear + equipment regions
|
||||||
|
// Clear composite cache first to prevent stale textures from being reused
|
||||||
|
charRenderer->clearCompositeCache();
|
||||||
|
// Use per-instance texture override (not model-level) to avoid deleting cached composites.
|
||||||
|
uint32_t instanceId = renderer->getCharacterInstanceId();
|
||||||
GLuint newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers);
|
GLuint newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers);
|
||||||
if (newTex != 0) {
|
if (newTex != 0 && instanceId != 0) {
|
||||||
charRenderer->setModelTexture(1, skinSlot, newTex);
|
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(skinSlot), newTex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cloak cape texture — separate from skin atlas, uses texture slot type-2 (Object Skin)
|
// Cloak cape texture — separate from skin atlas, uses texture slot type-2 (Object Skin)
|
||||||
uint32_t cloakSlot = app.getCloakTextureSlotIndex();
|
uint32_t cloakSlot = app.getCloakTextureSlotIndex();
|
||||||
if (cloakSlot > 0) {
|
if (cloakSlot > 0 && instanceId != 0) {
|
||||||
// Find equipped cloak (inventoryType 16)
|
// Find equipped cloak (inventoryType 16)
|
||||||
uint32_t cloakDisplayId = 0;
|
uint32_t cloakDisplayId = 0;
|
||||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||||
|
|
@ -2818,14 +2846,14 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
|
||||||
std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp";
|
std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp";
|
||||||
GLuint capeTex = charRenderer->loadTexture(capePath);
|
GLuint capeTex = charRenderer->loadTexture(capePath);
|
||||||
if (capeTex != 0) {
|
if (capeTex != 0) {
|
||||||
charRenderer->setModelTexture(1, cloakSlot, capeTex);
|
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(cloakSlot), capeTex);
|
||||||
LOG_INFO("Cloak texture applied: ", capePath);
|
LOG_INFO("Cloak texture applied: ", capePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No cloak equipped — reset to white fallback
|
// No cloak equipped — clear override so model's default (white) shows
|
||||||
charRenderer->resetModelTexture(1, cloakSlot);
|
charRenderer->clearTextureSlotOverride(instanceId, static_cast<uint16_t>(cloakSlot));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,8 @@ void InventoryScreen::updatePreviewEquipment(game::Inventory& inventory) {
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unordered_set<uint16_t> geosets;
|
std::unordered_set<uint16_t> geosets;
|
||||||
for (uint16_t i = 0; i <= 18; i++) geosets.insert(i);
|
// Body parts (group 0: IDs 0-99, some models use up to 27)
|
||||||
|
for (uint16_t i = 0; i <= 99; i++) geosets.insert(i);
|
||||||
|
|
||||||
// Hair geoset: group 1 = 100 + hairStyle + 1
|
// Hair geoset: group 1 = 100 + hairStyle + 1
|
||||||
geosets.insert(static_cast<uint16_t>(100 + playerHairStyle_ + 1));
|
geosets.insert(static_cast<uint16_t>(100 + playerHairStyle_ + 1));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue