diff --git a/src/core/application.cpp b/src/core/application.cpp index 8b1faed5..a162dba3 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -4077,18 +4077,24 @@ void Application::buildCreatureDisplayLookups() { // Col 5: HairStyleID // Col 6: HairColorID // Col 7: FacialHairID - // Turtle/Vanilla: 19 fields — 10 equip slots (8-17), BakeName=18 (no Flags field) - // WotLK/TBC/Classic: 21 fields — 11 equip slots (8-18), Flags=19, BakeName=20 + // CreatureDisplayInfoExtra.dbc field layout depends on actual field count: + // 19 fields: 10 equip slots (8-17), BakeName=18 (no Flags field) + // 21 fields: 11 equip slots (8-18), Flags=19, BakeName=20 if (auto cdie = assetManager->loadDBC("CreatureDisplayInfoExtra.dbc"); cdie && cdie->isLoaded()) { const auto* cdieL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureDisplayInfoExtra") : nullptr; const uint32_t cdieEquip0 = cdieL ? (*cdieL)["EquipDisplay0"] : 8; - const uint32_t bakeField = cdieL ? (*cdieL)["BakeName"] : 20; - // Count equipment slots: Vanilla/Turtle has 10, WotLK/TBC has 11 - int numEquipSlots = 10; - if (cdieL && cdieL->field("EquipDisplay10") != 0xFFFFFFFF) { + // Detect actual field count to determine equip slot count and BakeName position + const uint32_t dbcFieldCount = cdie->getFieldCount(); + int numEquipSlots; + uint32_t bakeField; + if (dbcFieldCount <= 19) { + // 19 fields: 10 equip slots (8-17), BakeName at 18 + numEquipSlots = 10; + bakeField = 18; + } else { + // 21 fields: 11 equip slots (8-18), Flags=19, BakeName=20 numEquipSlots = 11; - } else if (!cdieL) { - numEquipSlots = 11; // Default (WotLK) has 11 + bakeField = cdieL ? (*cdieL)["BakeName"] : 20; } uint32_t withBakeName = 0; for (uint32_t i = 0; i < cdie->getRecordCount(); i++) { @@ -4107,8 +4113,9 @@ void Application::buildCreatureDisplayLookups() { if (!extra.bakeName.empty()) withBakeName++; humanoidExtraMap_[cdie->getUInt32(i, cdieL ? (*cdieL)["ID"] : 0)] = extra; } - LOG_INFO("Loaded ", humanoidExtraMap_.size(), " humanoid display extra entries (", - withBakeName, " with baked textures, ", numEquipSlots, " equip slots)"); + LOG_WARNING("Loaded ", humanoidExtraMap_.size(), " humanoid display extra entries (", + withBakeName, " with baked textures, ", numEquipSlots, " equip slots, ", + dbcFieldCount, " DBC fields, bakeField=", bakeField, ")"); } // CreatureModelData.dbc: modelId (col 0) → modelPath (col 2, .mdx → .m2) @@ -4508,12 +4515,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x LOG_DEBUG(" Found humanoid extra: raceId=", (int)extra.raceId, " sexId=", (int)extra.sexId, " hairStyle=", (int)extra.hairStyleId, " hairColor=", (int)extra.hairColorId, " bakeName='", extra.bakeName, "'"); - LOG_DEBUG(" Equipment: helm=", extra.equipDisplayId[0], " shoulder=", extra.equipDisplayId[1], - " shirt=", extra.equipDisplayId[2], " chest=", extra.equipDisplayId[3], - " belt=", extra.equipDisplayId[4], " legs=", extra.equipDisplayId[5], - " feet=", extra.equipDisplayId[6], " wrist=", extra.equipDisplayId[7], - " hands=", extra.equipDisplayId[8], " tabard=", extra.equipDisplayId[9], - " cape=", extra.equipDisplayId[10]); + LOG_DEBUG("NPC equip: chest=", extra.equipDisplayId[3], + " legs=", extra.equipDisplayId[5], + " feet=", extra.equipDisplayId[6], + " hands=", extra.equipDisplayId[8], + " bake='", extra.bakeName, "'"); // Build equipment texture region layers from NPC equipment display IDs // (texture-only compositing — no geoset changes to avoid invisibility bugs) @@ -4574,8 +4580,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } }; auto regionAllowedForNpcSlotCtx = [&](int eqSlot, int region) -> bool { - // Avoid painting arm regions from shirt/chest when NPC has no arm armor. - if ((eqSlot == 2 || eqSlot == 3) && !npcHasArmArmor) { + // Shirt (slot 2) without arm armor: restrict to torso only + // to avoid bare-skin shirt textures bleeding onto arms. + // Chest (slot 3) always paints arms — plate/mail chest armor + // must cover the full upper body even without separate gloves. + if (eqSlot == 2 && !npcHasArmArmor) { return (region == 3 || region == 4); } return regionAllowedForNpcSlot(eqSlot, region); @@ -4684,35 +4693,26 @@ 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 const bool allowNpcRegionComposite = true; + rendering::VkTexture* bakedSkinTex = nullptr; if (!extra.bakeName.empty()) { std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName; - - // Composite equipment textures over baked NPC texture, or just load baked texture - rendering::VkTexture* finalTex = nullptr; - if (allowNpcRegionComposite && !npcRegionLayers.empty()) { - finalTex = charRenderer->compositeWithRegions(bakePath, {}, npcRegionLayers); - LOG_DEBUG("Composited NPC baked texture with ", npcRegionLayers.size(), - " equipment regions: ", bakePath); - } else { - finalTex = charRenderer->loadTexture(bakePath); - } - + rendering::VkTexture* finalTex = charRenderer->loadTexture(bakePath); + bakedSkinTex = finalTex; if (finalTex && modelData) { for (size_t ti = 0; ti < modelData->textures.size(); ti++) { uint32_t texType = modelData->textures[ti].type; - // Humanoid NPCs typically use creature-skin texture types (11-13). - // Keep type 2 (object skin) untouched so cloak/cape slots do not get face/body textures. - if (texType == 1 || texType == 11 || texType == 12 || texType == 13) { + if (texType == 1) { charRenderer->setModelTexture(modelId, static_cast(ti), finalTex); - LOG_DEBUG("Applied baked NPC texture to slot ", ti, " (type ", texType, "): ", bakePath); hasHumanoidTexture = true; + LOG_DEBUG("NPC baked type1 slot=", ti, " modelId=", modelId, + " tex=", bakePath); } } - } else { - LOG_WARNING("Failed to load baked NPC texture: ", bakePath); } - } else { - LOG_DEBUG(" Humanoid extra has empty bakeName, trying CharSections fallback"); + } + // Fallback: if baked texture failed or bakeName was empty, build from CharSections + if (!hasHumanoidTexture) { + LOG_DEBUG(" Trying CharSections fallback for NPC skin"); // Build skin texture from CharSections.dbc (same as player character) auto csFallbackDbc = assetManager->loadDBC("CharSections.dbc"); @@ -4755,6 +4755,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } } + LOG_DEBUG("NPC CharSections lookup: race=", npcRace, " sex=", npcSex, + " skin=", npcSkin, " face=", npcFace, + " skinPath='", npcSkinPath, "' faceLower='", npcFaceLower, "'"); if (!npcSkinPath.empty()) { // Composite skin + face + underwear std::vector skinLayers; @@ -4775,14 +4778,19 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } if (npcSkinTex && modelData) { + int slotsSet = 0; for (size_t ti = 0; ti < modelData->textures.size(); ti++) { uint32_t texType = modelData->textures[ti].type; if (texType == 1 || texType == 11 || texType == 12 || texType == 13) { charRenderer->setModelTexture(modelId, static_cast(ti), npcSkinTex); hasHumanoidTexture = true; + slotsSet++; } } - LOG_DEBUG("Applied CharSections skin to NPC: ", npcSkinPath); + LOG_DEBUG("NPC CharSections: skin='", npcSkinPath, "' regions=", + npcRegionLayers.size(), " applied=", hasHumanoidTexture, + " slots=", slotsSet, + " modelId=", modelId, " texCount=", modelData->textures.size()); } } } @@ -4814,15 +4822,24 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (!hairTexPath.empty()) { rendering::VkTexture* hairTex = charRenderer->loadTexture(hairTexPath); - if (hairTex && modelData) { + rendering::VkTexture* whTex = charRenderer->loadTexture(""); + if (hairTex && hairTex != whTex && modelData) { for (size_t ti = 0; ti < modelData->textures.size(); ti++) { if (modelData->textures[ti].type == 6) { charRenderer->setModelTexture(modelId, static_cast(ti), hairTex); - LOG_DEBUG("Applied hair texture to slot ", ti, ": ", hairTexPath); } } } } + // Bald NPCs (hairStyle=0 or no CharSections match): set type-6 to + // the skin/baked texture so the scalp cap renders with skin color. + if (hairTexPath.empty() && bakedSkinTex && modelData) { + for (size_t ti = 0; ti < modelData->textures.size(); ti++) { + if (modelData->textures[ti].type == 6) { + charRenderer->setModelTexture(modelId, static_cast(ti), bakedSkinTex); + } + } + } } // Do not apply cape textures at model scope here. Type-2 texture slots are @@ -4959,6 +4976,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x uint32_t tgtSex = static_cast(extra.sexId); // Look up hair texture (section 3) + rendering::VkTexture* whiteTex = charRenderer->loadTexture(""); for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) { uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1); uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2); @@ -4972,7 +4990,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x std::string hairPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6); if (!hairPath.empty()) { rendering::VkTexture* hairTex = charRenderer->loadTexture(hairPath); - if (hairTex) { + if (hairTex && hairTex != whiteTex) { for (size_t ti = 0; ti < md->textures.size(); ti++) { if (md->textures[ti].type == 6) { charRenderer->setTextureSlotOverride(instanceId, static_cast(ti), hairTex); @@ -4983,28 +5001,37 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x break; } - // Look up skin texture (section 0) for per-instance skin color - for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) { - uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1); - uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2); - if (rId != tgtRace || sId != tgtSex) continue; - uint32_t sec = charSectionsDbc2->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - if (sec != 0) continue; - uint32_t col = charSectionsDbc2->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); - if (col != static_cast(extra.skinId)) continue; - std::string skinPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6); - if (!skinPath.empty()) { - rendering::VkTexture* skinTex = charRenderer->loadTexture(skinPath); - if (skinTex) { - for (size_t ti = 0; ti < md->textures.size(); ti++) { - uint32_t tt = md->textures[ti].type; - if (tt == 1 || tt == 11) { - charRenderer->setTextureSlotOverride(instanceId, static_cast(ti), skinTex); + // Look up skin texture (section 0) for per-instance skin color. + // Skip when the NPC has a baked texture or composited equipment — + // those already encode armor over skin and must not be replaced. + bool hasEquipOrBake = !extra.bakeName.empty(); + if (!hasEquipOrBake) { + for (int s = 0; s < 11 && !hasEquipOrBake; s++) + if (extra.equipDisplayId[s] != 0) hasEquipOrBake = true; + } + if (!hasEquipOrBake) { + for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) { + uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1); + uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2); + if (rId != tgtRace || sId != tgtSex) continue; + uint32_t sec = charSectionsDbc2->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); + if (sec != 0) continue; + uint32_t col = charSectionsDbc2->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + if (col != static_cast(extra.skinId)) continue; + std::string skinPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6); + if (!skinPath.empty()) { + rendering::VkTexture* skinTex = charRenderer->loadTexture(skinPath); + if (skinTex) { + for (size_t ti = 0; ti < md->textures.size(); ti++) { + uint32_t tt = md->textures[ti].type; + if (tt == 1 || tt == 11) { + charRenderer->setTextureSlotOverride(instanceId, static_cast(ti), skinTex); + } } } } + break; } - break; } } } @@ -5111,7 +5138,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // aggressive and can make NPCs invisible (targetable but not rendered). // Keep default model geosets for online creatures until this path is made // data-accurate per display model. - static constexpr bool kEnableNpcHumanoidOverrides = true; + static constexpr bool kEnableNpcHumanoidOverrides = false; // Set geosets for humanoid NPCs based on CreatureDisplayInfoExtra if (kEnableNpcHumanoidOverrides && @@ -5198,44 +5225,42 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // Equipment slots: 0=helm, 1=shoulder, 2=shirt, 3=chest, 4=belt, 5=legs, 6=feet, 7=wrist, 8=hands, 9=tabard, 10=cape const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7; - // Chest (slot 3) → group 5 (torso) + group 8 (sleeves/wristbands) - if (extra.equipDisplayId[3] != 0) { - int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[3]); - if (idx >= 0) { - uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), fGG1); - if (gg > 0) geosetTorso = pickGeoset(static_cast(501 + gg), 5); - if (gg > 0) geosetSleeves = pickGeoset(static_cast(801 + gg), 8); - // Do not derive robe/kilt from chest by default here. - // Some NPC datasets set chest geosets that cause persistent - // apron/robe overlays; prefer explicit legs slot for trousers. + auto readGeosetGroup = [&](int slot, const char* slotName) -> uint32_t { + uint32_t did = extra.equipDisplayId[slot]; + if (did == 0) return 0; + int32_t idx = itemDisplayDbc->findRecordById(did); + if (idx < 0) { + LOG_INFO("NPC equip slot ", slotName, " displayId=", did, " NOT FOUND in ItemDisplayInfo.dbc"); + return 0; } + uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), fGG1); + LOG_INFO("NPC equip slot ", slotName, " displayId=", did, " GeosetGroup1=", gg, " (field=", fGG1, ")"); + return gg; + }; + + // Chest (slot 3) → group 5 (torso) + group 8 (sleeves/wristbands) + { + uint32_t gg = readGeosetGroup(3, "chest"); + if (gg > 0) geosetTorso = pickGeoset(static_cast(501 + gg), 5); + if (gg > 0) geosetSleeves = pickGeoset(static_cast(801 + gg), 8); } // Legs (slot 5) → group 13 (trousers) - if (extra.equipDisplayId[5] != 0) { - int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[5]); - if (idx >= 0) { - uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), fGG1); - if (gg > 0) geosetPants = pickGeoset(static_cast(1301 + gg), 13); - } + { + uint32_t gg = readGeosetGroup(5, "legs"); + if (gg > 0) geosetPants = pickGeoset(static_cast(1301 + gg), 13); } // Feet (slot 6) → group 4 (boots/shins) - if (extra.equipDisplayId[6] != 0) { - int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[6]); - if (idx >= 0) { - uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), fGG1); - if (gg > 0) geosetBoots = pickGeoset(static_cast(401 + gg), 4); - } + { + uint32_t gg = readGeosetGroup(6, "feet"); + if (gg > 0) geosetBoots = pickGeoset(static_cast(401 + gg), 4); } // Hands (slot 8) → group 3 (gloves/forearms) - if (extra.equipDisplayId[8] != 0) { - int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[8]); - if (idx >= 0) { - uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), fGG1); - if (gg > 0) geosetGloves = pickGeoset(static_cast(301 + gg), 3); - } + { + uint32_t gg = readGeosetGroup(8, "hands"); + if (gg > 0) geosetGloves = pickGeoset(static_cast(301 + gg), 3); } // Tabard (slot 9) intentionally disabled for now (see geosetTabard TODO above). @@ -5495,6 +5520,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x uint16_t selectedFacial300 = 300; uint16_t selectedFacial300Alt = 300; bool wantsFacialHair = false; + uint32_t equipChestGG = 0, equipLegsGG = 0, equipFeetGG = 0; std::unordered_set hairScalpGeosetsForRaceSex; if (itDisplayData != displayDataMap_.end() && itDisplayData->second.extraDisplayId != 0) { @@ -5597,6 +5623,20 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } } } + + // Read GeosetGroup1 from equipment to drive clothed mesh selection + if (itemDisplayDbc) { + const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7; + auto readGG = [&](uint32_t did) -> uint32_t { + if (did == 0) return 0; + int32_t idx = itemDisplayDbc->findRecordById(did); + return (idx >= 0) ? itemDisplayDbc->getUInt32(static_cast(idx), fGG1) : 0; + }; + equipChestGG = readGG(itExtra->second.equipDisplayId[3]); + if (equipChestGG == 0) equipChestGG = readGG(itExtra->second.equipDisplayId[2]); // shirt fallback + equipLegsGG = readGG(itExtra->second.equipDisplayId[5]); + equipFeetGG = readGG(itExtra->second.equipDisplayId[6]); + } } } @@ -5650,11 +5690,17 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // Even "bare" variants can produce unwanted looped arm geometry on NPCs. if (hasGroup4) { - uint16_t forearmSid = pickFromGroup(401, 4); - if (forearmSid != 0) normalizedGeosets.insert(forearmSid); + uint16_t wantBoots = (equipFeetGG > 0) ? static_cast(400 + equipFeetGG) : 401; + uint16_t bootsSid = pickFromGroup(wantBoots, 4); + if (bootsSid != 0) normalizedGeosets.insert(bootsSid); } - // Intentionally do not add group 8 (sleeve/wrist accessory meshes). + // Add sleeve/wrist meshes when chest armor calls for them. + if (hasGroup8 && equipChestGG > 0) { + uint16_t wantSleeves = static_cast(800 + equipChestGG); + uint16_t sleeveSid = pickFromGroup(wantSleeves, 8); + if (sleeveSid != 0) normalizedGeosets.insert(sleeveSid); + } // Show tabard mesh only when CreatureDisplayInfoExtra equips one. if (hasGroup12 && hasEquippedTabard) { @@ -5675,9 +5721,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } } - // Prefer trousers geoset, not robe/kilt overlays. + // Prefer trousers geoset; use covered variant when legs armor exists. if (hasGroup13) { - uint16_t pantsSid = pickFromGroup(1301, 13); + uint16_t wantPants = (equipLegsGG > 0) ? static_cast(1300 + equipLegsGG) : 1301; + uint16_t pantsSid = pickFromGroup(wantPants, 13); if (pantsSid != 0) normalizedGeosets.insert(pantsSid); } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index af051b74..71a65c92 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -8809,13 +8809,21 @@ void GameHandler::checkAreaTriggers() { areaTriggerSuppressFirst_ = false; } + // Deeprun Tram entrance triggers need extended range because WMO + // collision walls block the player from reaching the trigger center. + static const std::unordered_set extendedRangeTriggers = { + 712, 713, // Stormwind/Ironforge → Deeprun Tram + 2166, 2171, // Tram interior exit triggers + }; + for (const auto& at : areaTriggers_) { if (at.mapId != currentMapId_) continue; + const bool extended = extendedRangeTriggers.count(at.id) > 0; bool inside = false; if (at.radius > 0.0f) { // Sphere trigger — small minimum so player must be near the portal - float effectiveRadius = std::max(at.radius, 12.0f); + float effectiveRadius = std::max(at.radius, extended ? 45.0f : 12.0f); float dx = px - at.x; float dy = py - at.y; float dz = pz - at.z; @@ -8823,9 +8831,10 @@ void GameHandler::checkAreaTriggers() { inside = (distSq <= effectiveRadius * effectiveRadius); } else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) { // Box trigger — small minimum so player must walk into the portal area - float effLength = std::max(at.boxLength, 16.0f); - float effWidth = std::max(at.boxWidth, 16.0f); - float effHeight = std::max(at.boxHeight, 16.0f); + float boxMin = extended ? 60.0f : 16.0f; + float effLength = std::max(at.boxLength, boxMin); + float effWidth = std::max(at.boxWidth, boxMin); + float effHeight = std::max(at.boxHeight, boxMin); float dx = px - at.x; float dy = py - at.y; diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 11fa2ae5..2126e5e5 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -753,6 +753,44 @@ static void blitOverlayScaled2x(std::vector& composite, int compW, int blitOverlayScaledN(composite, compW, compH, overlay, dstX, dstY, 2); } +// Nearest-neighbor downscale blit: sample every Nth pixel from overlay +static void blitOverlayDownscaleN(std::vector& composite, int compW, int compH, + const pipeline::BLPImage& overlay, int dstX, int dstY, int scale) { + if (scale < 2) { blitOverlay(composite, compW, compH, overlay, dstX, dstY); return; } + int outW = overlay.width / scale; + int outH = overlay.height / scale; + for (int oy = 0; oy < outH; oy++) { + int dy = dstY + oy; + if (dy < 0 || dy >= compH) continue; + for (int ox = 0; ox < outW; ox++) { + int dx = dstX + ox; + if (dx < 0 || dx >= compW) continue; + + int sx = ox * scale; + int sy = oy * scale; + size_t srcIdx = (static_cast(sy) * overlay.width + sx) * 4; + size_t dstIdx = (static_cast(dy) * compW + dx) * 4; + + uint8_t srcA = overlay.data[srcIdx + 3]; + if (srcA == 0) continue; + + if (srcA == 255) { + composite[dstIdx + 0] = overlay.data[srcIdx + 0]; + composite[dstIdx + 1] = overlay.data[srcIdx + 1]; + composite[dstIdx + 2] = overlay.data[srcIdx + 2]; + composite[dstIdx + 3] = 255; + } else { + float alpha = srcA / 255.0f; + float invAlpha = 1.0f - alpha; + composite[dstIdx + 0] = static_cast(overlay.data[srcIdx + 0] * alpha + composite[dstIdx + 0] * invAlpha); + composite[dstIdx + 1] = static_cast(overlay.data[srcIdx + 1] * alpha + composite[dstIdx + 1] * invAlpha); + composite[dstIdx + 2] = static_cast(overlay.data[srcIdx + 2] * alpha + composite[dstIdx + 2] * invAlpha); + composite[dstIdx + 3] = std::max(composite[dstIdx + 3], srcA); + } + } + } +} + VkTexture* CharacterRenderer::compositeTextures(const std::vector& layerPaths) { if (layerPaths.empty() || !assetManager || !assetManager->isInitialized()) { return whiteTexture_.get(); @@ -1116,14 +1154,31 @@ VkTexture* CharacterRenderer::compositeWithRegions(const std::string& basePath, // Expected full-resolution size for this region at current atlas scale int expectedW = regionSizes256[regionIdx][0] * scaleX; int expectedH = regionSizes256[regionIdx][1] * scaleY; - if (overlay.width * 2 == expectedW && overlay.height * 2 == expectedH) { + if (overlay.width == expectedW && overlay.height == expectedH) { + // Exact match — blit 1:1 + blitOverlay(composite, width, height, overlay, dstX, dstY); + } else if (overlay.width * 2 == expectedW && overlay.height * 2 == expectedH) { + // Overlay is half size — upscale 2x blitOverlayScaled2x(composite, width, height, overlay, dstX, dstY); + } else if (overlay.width > expectedW && overlay.height > expectedH && + expectedW > 0 && expectedH > 0) { + // Overlay is larger than region (e.g. HD textures for 1024 atlas on 512 canvas) + // Downscale to fit + int dsX = overlay.width / expectedW; + int dsY = overlay.height / expectedH; + int ds = std::min(dsX, dsY); + if (ds >= 2) { + blitOverlayDownscaleN(composite, width, height, overlay, dstX, dstY, ds); + } else { + blitOverlay(composite, width, height, overlay, dstX, dstY); + } } else { blitOverlay(composite, width, height, overlay, dstX, dstY); } - core::Logger::getInstance().debug("compositeWithRegions: region ", regionIdx, - " at (", dstX, ",", dstY, ") ", overlay.width, "x", overlay.height, " from ", rl.second); + core::Logger::getInstance().warning("compositeWithRegions: region ", regionIdx, + " at (", dstX, ",", dstY, ") overlay=", overlay.width, "x", overlay.height, + " expected=", expectedW, "x", expectedH, " from ", rl.second); } // Upload to GPU via VkTexture diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index f0dfebd0..fd3c3981 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -883,20 +883,40 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu surface.origin.z = adjustedZ; surface.position.z = adjustedZ; - if (surface.origin.z > 300.0f || surface.origin.z < -100.0f) return; - // Build tile mask from MLIQ flags — tiles with (flag & 0x0F) == 0x0F have no liquid + // Build tile mask from MLIQ flags and per-vertex heights size_t tileCount = static_cast(surface.width) * static_cast(surface.height); size_t maskBytes = (tileCount + 7) / 8; surface.mask.assign(maskBytes, 0x00); + const float baseZ = liquid.basePosition.z; + const bool hasHeights = !liquid.heights.empty() && + liquid.heights.size() >= static_cast(vertexCount); for (size_t t = 0; t < tileCount; t++) { bool hasLiquid = true; + int tx = static_cast(t) % surface.width; + int ty = static_cast(t) / surface.width; + + // Standard WoW check: low nibble 0x0F = "don't render" if (t < liquid.flags.size()) { if ((liquid.flags[t] & 0x0F) == 0x0F) { hasLiquid = false; } } + // Suppress water tiles that extend into enclosed WMO areas + // (e.g. Stormwind barracks stairway where canal water pokes through) + // Render coords: x=wowY(west), y=wowX(north) + if (hasLiquid) { + glm::vec3 tileWorld = surface.origin + + surface.stepX * (static_cast(tx) + 0.5f) + + surface.stepY * (static_cast(ty) + 0.5f); + // Stormwind Barracks / Stockade stairway: + // Stockade entrance at approximately render (-8768, 848) + if (tileWorld.x > -8790.0f && tileWorld.x < -8735.0f && + tileWorld.y > 828.0f && tileWorld.y < 878.0f) { + hasLiquid = false; + } + } if (hasLiquid) { size_t byteIdx = t / 8; size_t bitIdx = t % 8; @@ -905,6 +925,32 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu } createWaterMesh(surface); + + // Count how many tiles passed the flag check and compute bounds + size_t activeTiles = 0; + float minWX = 1e9f, maxWX = -1e9f, minWY = 1e9f, maxWY = -1e9f; + for (size_t t = 0; t < tileCount; t++) { + size_t byteIdx = t / 8; + size_t bitIdx = t % 8; + if (surface.mask[byteIdx] & (1 << bitIdx)) { + activeTiles++; + int atx = static_cast(t) % surface.width; + int aty = static_cast(t) / surface.width; + glm::vec3 tw = surface.origin + + surface.stepX * (static_cast(atx) + 0.5f) + + surface.stepY * (static_cast(aty) + 0.5f); + if (tw.x < minWX) minWX = tw.x; + if (tw.x > maxWX) maxWX = tw.x; + if (tw.y < minWY) minWY = tw.y; + if (tw.y > maxWY) maxWY = tw.y; + } + } + LOG_DEBUG("WMO water: origin=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z, + ") tiles=", (int)surface.width, "x", (int)surface.height, + " active=", activeTiles, "/", tileCount, + " wmoId=", wmoId, " indexCount=", surface.indexCount, + " bounds x=[", minWX, "..", maxWX, "] y=[", minWY, "..", maxWY, "]"); + if (surface.indexCount > 0) { if (vkCtx) updateMaterialUBO(surface); surfaces.push_back(std::move(surface));