mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
2 commits
08d40583c9
...
cefb05c027
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cefb05c027 | ||
|
|
d763d71bf3 |
4 changed files with 259 additions and 102 deletions
|
|
@ -4077,18 +4077,24 @@ void Application::buildCreatureDisplayLookups() {
|
||||||
// Col 5: HairStyleID
|
// Col 5: HairStyleID
|
||||||
// Col 6: HairColorID
|
// Col 6: HairColorID
|
||||||
// Col 7: FacialHairID
|
// Col 7: FacialHairID
|
||||||
// Turtle/Vanilla: 19 fields — 10 equip slots (8-17), BakeName=18 (no Flags field)
|
// CreatureDisplayInfoExtra.dbc field layout depends on actual field count:
|
||||||
// WotLK/TBC/Classic: 21 fields — 11 equip slots (8-18), Flags=19, BakeName=20
|
// 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()) {
|
if (auto cdie = assetManager->loadDBC("CreatureDisplayInfoExtra.dbc"); cdie && cdie->isLoaded()) {
|
||||||
const auto* cdieL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureDisplayInfoExtra") : nullptr;
|
const auto* cdieL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureDisplayInfoExtra") : nullptr;
|
||||||
const uint32_t cdieEquip0 = cdieL ? (*cdieL)["EquipDisplay0"] : 8;
|
const uint32_t cdieEquip0 = cdieL ? (*cdieL)["EquipDisplay0"] : 8;
|
||||||
const uint32_t bakeField = cdieL ? (*cdieL)["BakeName"] : 20;
|
// Detect actual field count to determine equip slot count and BakeName position
|
||||||
// Count equipment slots: Vanilla/Turtle has 10, WotLK/TBC has 11
|
const uint32_t dbcFieldCount = cdie->getFieldCount();
|
||||||
int numEquipSlots = 10;
|
int numEquipSlots;
|
||||||
if (cdieL && cdieL->field("EquipDisplay10") != 0xFFFFFFFF) {
|
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;
|
numEquipSlots = 11;
|
||||||
} else if (!cdieL) {
|
bakeField = cdieL ? (*cdieL)["BakeName"] : 20;
|
||||||
numEquipSlots = 11; // Default (WotLK) has 11
|
|
||||||
}
|
}
|
||||||
uint32_t withBakeName = 0;
|
uint32_t withBakeName = 0;
|
||||||
for (uint32_t i = 0; i < cdie->getRecordCount(); i++) {
|
for (uint32_t i = 0; i < cdie->getRecordCount(); i++) {
|
||||||
|
|
@ -4107,8 +4113,9 @@ void Application::buildCreatureDisplayLookups() {
|
||||||
if (!extra.bakeName.empty()) withBakeName++;
|
if (!extra.bakeName.empty()) withBakeName++;
|
||||||
humanoidExtraMap_[cdie->getUInt32(i, cdieL ? (*cdieL)["ID"] : 0)] = extra;
|
humanoidExtraMap_[cdie->getUInt32(i, cdieL ? (*cdieL)["ID"] : 0)] = extra;
|
||||||
}
|
}
|
||||||
LOG_INFO("Loaded ", humanoidExtraMap_.size(), " humanoid display extra entries (",
|
LOG_WARNING("Loaded ", humanoidExtraMap_.size(), " humanoid display extra entries (",
|
||||||
withBakeName, " with baked textures, ", numEquipSlots, " equip slots)");
|
withBakeName, " with baked textures, ", numEquipSlots, " equip slots, ",
|
||||||
|
dbcFieldCount, " DBC fields, bakeField=", bakeField, ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatureModelData.dbc: modelId (col 0) → modelPath (col 2, .mdx → .m2)
|
// 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,
|
LOG_DEBUG(" Found humanoid extra: raceId=", (int)extra.raceId, " sexId=", (int)extra.sexId,
|
||||||
" hairStyle=", (int)extra.hairStyleId, " hairColor=", (int)extra.hairColorId,
|
" hairStyle=", (int)extra.hairStyleId, " hairColor=", (int)extra.hairColorId,
|
||||||
" bakeName='", extra.bakeName, "'");
|
" bakeName='", extra.bakeName, "'");
|
||||||
LOG_DEBUG(" Equipment: helm=", extra.equipDisplayId[0], " shoulder=", extra.equipDisplayId[1],
|
LOG_DEBUG("NPC equip: chest=", extra.equipDisplayId[3],
|
||||||
" shirt=", extra.equipDisplayId[2], " chest=", extra.equipDisplayId[3],
|
" legs=", extra.equipDisplayId[5],
|
||||||
" belt=", extra.equipDisplayId[4], " legs=", extra.equipDisplayId[5],
|
" feet=", extra.equipDisplayId[6],
|
||||||
" feet=", extra.equipDisplayId[6], " wrist=", extra.equipDisplayId[7],
|
" hands=", extra.equipDisplayId[8],
|
||||||
" hands=", extra.equipDisplayId[8], " tabard=", extra.equipDisplayId[9],
|
" bake='", extra.bakeName, "'");
|
||||||
" cape=", extra.equipDisplayId[10]);
|
|
||||||
|
|
||||||
// Build equipment texture region layers from NPC equipment display IDs
|
// Build equipment texture region layers from NPC equipment display IDs
|
||||||
// (texture-only compositing — no geoset changes to avoid invisibility bugs)
|
// (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 {
|
auto regionAllowedForNpcSlotCtx = [&](int eqSlot, int region) -> bool {
|
||||||
// Avoid painting arm regions from shirt/chest when NPC has no arm armor.
|
// Shirt (slot 2) without arm armor: restrict to torso only
|
||||||
if ((eqSlot == 2 || eqSlot == 3) && !npcHasArmArmor) {
|
// 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 (region == 3 || region == 4);
|
||||||
}
|
}
|
||||||
return regionAllowedForNpcSlot(eqSlot, region);
|
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)
|
// Use baked texture for body skin (types 1, 2)
|
||||||
// Type 6 (hair) needs its own texture from CharSections.dbc
|
// Type 6 (hair) needs its own texture from CharSections.dbc
|
||||||
const bool allowNpcRegionComposite = true;
|
const bool allowNpcRegionComposite = true;
|
||||||
|
rendering::VkTexture* bakedSkinTex = nullptr;
|
||||||
if (!extra.bakeName.empty()) {
|
if (!extra.bakeName.empty()) {
|
||||||
std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName;
|
std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName;
|
||||||
|
rendering::VkTexture* finalTex = charRenderer->loadTexture(bakePath);
|
||||||
// Composite equipment textures over baked NPC texture, or just load baked texture
|
bakedSkinTex = finalTex;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalTex && modelData) {
|
if (finalTex && modelData) {
|
||||||
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
uint32_t texType = modelData->textures[ti].type;
|
uint32_t texType = modelData->textures[ti].type;
|
||||||
// Humanoid NPCs typically use creature-skin texture types (11-13).
|
if (texType == 1) {
|
||||||
// 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) {
|
|
||||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), finalTex);
|
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), finalTex);
|
||||||
LOG_DEBUG("Applied baked NPC texture to slot ", ti, " (type ", texType, "): ", bakePath);
|
|
||||||
hasHumanoidTexture = true;
|
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)
|
// Build skin texture from CharSections.dbc (same as player character)
|
||||||
auto csFallbackDbc = assetManager->loadDBC("CharSections.dbc");
|
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()) {
|
if (!npcSkinPath.empty()) {
|
||||||
// Composite skin + face + underwear
|
// Composite skin + face + underwear
|
||||||
std::vector<std::string> skinLayers;
|
std::vector<std::string> skinLayers;
|
||||||
|
|
@ -4775,14 +4778,19 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
}
|
}
|
||||||
|
|
||||||
if (npcSkinTex && modelData) {
|
if (npcSkinTex && modelData) {
|
||||||
|
int slotsSet = 0;
|
||||||
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
uint32_t texType = modelData->textures[ti].type;
|
uint32_t texType = modelData->textures[ti].type;
|
||||||
if (texType == 1 || texType == 11 || texType == 12 || texType == 13) {
|
if (texType == 1 || texType == 11 || texType == 12 || texType == 13) {
|
||||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), npcSkinTex);
|
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), npcSkinTex);
|
||||||
hasHumanoidTexture = true;
|
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()) {
|
if (!hairTexPath.empty()) {
|
||||||
rendering::VkTexture* hairTex = charRenderer->loadTexture(hairTexPath);
|
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++) {
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
if (modelData->textures[ti].type == 6) {
|
if (modelData->textures[ti].type == 6) {
|
||||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), hairTex);
|
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(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<uint32_t>(ti), bakedSkinTex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not apply cape textures at model scope here. Type-2 texture slots are
|
// 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<uint32_t>(extra.sexId);
|
uint32_t tgtSex = static_cast<uint32_t>(extra.sexId);
|
||||||
|
|
||||||
// Look up hair texture (section 3)
|
// Look up hair texture (section 3)
|
||||||
|
rendering::VkTexture* whiteTex = charRenderer->loadTexture("");
|
||||||
for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) {
|
for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) {
|
||||||
uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
||||||
uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
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);
|
std::string hairPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6);
|
||||||
if (!hairPath.empty()) {
|
if (!hairPath.empty()) {
|
||||||
rendering::VkTexture* hairTex = charRenderer->loadTexture(hairPath);
|
rendering::VkTexture* hairTex = charRenderer->loadTexture(hairPath);
|
||||||
if (hairTex) {
|
if (hairTex && hairTex != whiteTex) {
|
||||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||||
if (md->textures[ti].type == 6) {
|
if (md->textures[ti].type == 6) {
|
||||||
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), hairTex);
|
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), hairTex);
|
||||||
|
|
@ -4983,28 +5001,37 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up skin texture (section 0) for per-instance skin color
|
// Look up skin texture (section 0) for per-instance skin color.
|
||||||
for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) {
|
// Skip when the NPC has a baked texture or composited equipment —
|
||||||
uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
// those already encode armor over skin and must not be replaced.
|
||||||
uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
bool hasEquipOrBake = !extra.bakeName.empty();
|
||||||
if (rId != tgtRace || sId != tgtSex) continue;
|
if (!hasEquipOrBake) {
|
||||||
uint32_t sec = charSectionsDbc2->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
for (int s = 0; s < 11 && !hasEquipOrBake; s++)
|
||||||
if (sec != 0) continue;
|
if (extra.equipDisplayId[s] != 0) hasEquipOrBake = true;
|
||||||
uint32_t col = charSectionsDbc2->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
|
}
|
||||||
if (col != static_cast<uint32_t>(extra.skinId)) continue;
|
if (!hasEquipOrBake) {
|
||||||
std::string skinPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6);
|
for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) {
|
||||||
if (!skinPath.empty()) {
|
uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
||||||
rendering::VkTexture* skinTex = charRenderer->loadTexture(skinPath);
|
uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
||||||
if (skinTex) {
|
if (rId != tgtRace || sId != tgtSex) continue;
|
||||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
uint32_t sec = charSectionsDbc2->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
||||||
uint32_t tt = md->textures[ti].type;
|
if (sec != 0) continue;
|
||||||
if (tt == 1 || tt == 11) {
|
uint32_t col = charSectionsDbc2->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
|
||||||
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), skinTex);
|
if (col != static_cast<uint32_t>(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<uint16_t>(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).
|
// aggressive and can make NPCs invisible (targetable but not rendered).
|
||||||
// Keep default model geosets for online creatures until this path is made
|
// Keep default model geosets for online creatures until this path is made
|
||||||
// data-accurate per display model.
|
// data-accurate per display model.
|
||||||
static constexpr bool kEnableNpcHumanoidOverrides = true;
|
static constexpr bool kEnableNpcHumanoidOverrides = false;
|
||||||
|
|
||||||
// Set geosets for humanoid NPCs based on CreatureDisplayInfoExtra
|
// Set geosets for humanoid NPCs based on CreatureDisplayInfoExtra
|
||||||
if (kEnableNpcHumanoidOverrides &&
|
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
|
// 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;
|
const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7;
|
||||||
|
|
||||||
// Chest (slot 3) → group 5 (torso) + group 8 (sleeves/wristbands)
|
auto readGeosetGroup = [&](int slot, const char* slotName) -> uint32_t {
|
||||||
if (extra.equipDisplayId[3] != 0) {
|
uint32_t did = extra.equipDisplayId[slot];
|
||||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[3]);
|
if (did == 0) return 0;
|
||||||
if (idx >= 0) {
|
int32_t idx = itemDisplayDbc->findRecordById(did);
|
||||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
if (idx < 0) {
|
||||||
if (gg > 0) geosetTorso = pickGeoset(static_cast<uint16_t>(501 + gg), 5);
|
LOG_INFO("NPC equip slot ", slotName, " displayId=", did, " NOT FOUND in ItemDisplayInfo.dbc");
|
||||||
if (gg > 0) geosetSleeves = pickGeoset(static_cast<uint16_t>(801 + gg), 8);
|
return 0;
|
||||||
// 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.
|
|
||||||
}
|
}
|
||||||
|
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(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<uint16_t>(501 + gg), 5);
|
||||||
|
if (gg > 0) geosetSleeves = pickGeoset(static_cast<uint16_t>(801 + gg), 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legs (slot 5) → group 13 (trousers)
|
// Legs (slot 5) → group 13 (trousers)
|
||||||
if (extra.equipDisplayId[5] != 0) {
|
{
|
||||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[5]);
|
uint32_t gg = readGeosetGroup(5, "legs");
|
||||||
if (idx >= 0) {
|
if (gg > 0) geosetPants = pickGeoset(static_cast<uint16_t>(1301 + gg), 13);
|
||||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
|
||||||
if (gg > 0) geosetPants = pickGeoset(static_cast<uint16_t>(1301 + gg), 13);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feet (slot 6) → group 4 (boots/shins)
|
// Feet (slot 6) → group 4 (boots/shins)
|
||||||
if (extra.equipDisplayId[6] != 0) {
|
{
|
||||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[6]);
|
uint32_t gg = readGeosetGroup(6, "feet");
|
||||||
if (idx >= 0) {
|
if (gg > 0) geosetBoots = pickGeoset(static_cast<uint16_t>(401 + gg), 4);
|
||||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
|
||||||
if (gg > 0) geosetBoots = pickGeoset(static_cast<uint16_t>(401 + gg), 4);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hands (slot 8) → group 3 (gloves/forearms)
|
// Hands (slot 8) → group 3 (gloves/forearms)
|
||||||
if (extra.equipDisplayId[8] != 0) {
|
{
|
||||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[8]);
|
uint32_t gg = readGeosetGroup(8, "hands");
|
||||||
if (idx >= 0) {
|
if (gg > 0) geosetGloves = pickGeoset(static_cast<uint16_t>(301 + gg), 3);
|
||||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
|
||||||
if (gg > 0) geosetGloves = pickGeoset(static_cast<uint16_t>(301 + gg), 3);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tabard (slot 9) intentionally disabled for now (see geosetTabard TODO above).
|
// 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 selectedFacial300 = 300;
|
||||||
uint16_t selectedFacial300Alt = 300;
|
uint16_t selectedFacial300Alt = 300;
|
||||||
bool wantsFacialHair = false;
|
bool wantsFacialHair = false;
|
||||||
|
uint32_t equipChestGG = 0, equipLegsGG = 0, equipFeetGG = 0;
|
||||||
std::unordered_set<uint16_t> hairScalpGeosetsForRaceSex;
|
std::unordered_set<uint16_t> hairScalpGeosetsForRaceSex;
|
||||||
if (itDisplayData != displayDataMap_.end() &&
|
if (itDisplayData != displayDataMap_.end() &&
|
||||||
itDisplayData->second.extraDisplayId != 0) {
|
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<uint32_t>(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.
|
// Even "bare" variants can produce unwanted looped arm geometry on NPCs.
|
||||||
|
|
||||||
if (hasGroup4) {
|
if (hasGroup4) {
|
||||||
uint16_t forearmSid = pickFromGroup(401, 4);
|
uint16_t wantBoots = (equipFeetGG > 0) ? static_cast<uint16_t>(400 + equipFeetGG) : 401;
|
||||||
if (forearmSid != 0) normalizedGeosets.insert(forearmSid);
|
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<uint16_t>(800 + equipChestGG);
|
||||||
|
uint16_t sleeveSid = pickFromGroup(wantSleeves, 8);
|
||||||
|
if (sleeveSid != 0) normalizedGeosets.insert(sleeveSid);
|
||||||
|
}
|
||||||
|
|
||||||
// Show tabard mesh only when CreatureDisplayInfoExtra equips one.
|
// Show tabard mesh only when CreatureDisplayInfoExtra equips one.
|
||||||
if (hasGroup12 && hasEquippedTabard) {
|
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) {
|
if (hasGroup13) {
|
||||||
uint16_t pantsSid = pickFromGroup(1301, 13);
|
uint16_t wantPants = (equipLegsGG > 0) ? static_cast<uint16_t>(1300 + equipLegsGG) : 1301;
|
||||||
|
uint16_t pantsSid = pickFromGroup(wantPants, 13);
|
||||||
if (pantsSid != 0) normalizedGeosets.insert(pantsSid);
|
if (pantsSid != 0) normalizedGeosets.insert(pantsSid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8809,13 +8809,21 @@ void GameHandler::checkAreaTriggers() {
|
||||||
areaTriggerSuppressFirst_ = false;
|
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<uint32_t> extendedRangeTriggers = {
|
||||||
|
712, 713, // Stormwind/Ironforge → Deeprun Tram
|
||||||
|
2166, 2171, // Tram interior exit triggers
|
||||||
|
};
|
||||||
|
|
||||||
for (const auto& at : areaTriggers_) {
|
for (const auto& at : areaTriggers_) {
|
||||||
if (at.mapId != currentMapId_) continue;
|
if (at.mapId != currentMapId_) continue;
|
||||||
|
|
||||||
|
const bool extended = extendedRangeTriggers.count(at.id) > 0;
|
||||||
bool inside = false;
|
bool inside = false;
|
||||||
if (at.radius > 0.0f) {
|
if (at.radius > 0.0f) {
|
||||||
// Sphere trigger — small minimum so player must be near the portal
|
// 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 dx = px - at.x;
|
||||||
float dy = py - at.y;
|
float dy = py - at.y;
|
||||||
float dz = pz - at.z;
|
float dz = pz - at.z;
|
||||||
|
|
@ -8823,9 +8831,10 @@ void GameHandler::checkAreaTriggers() {
|
||||||
inside = (distSq <= effectiveRadius * effectiveRadius);
|
inside = (distSq <= effectiveRadius * effectiveRadius);
|
||||||
} else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) {
|
} 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
|
// Box trigger — small minimum so player must walk into the portal area
|
||||||
float effLength = std::max(at.boxLength, 16.0f);
|
float boxMin = extended ? 60.0f : 16.0f;
|
||||||
float effWidth = std::max(at.boxWidth, 16.0f);
|
float effLength = std::max(at.boxLength, boxMin);
|
||||||
float effHeight = std::max(at.boxHeight, 16.0f);
|
float effWidth = std::max(at.boxWidth, boxMin);
|
||||||
|
float effHeight = std::max(at.boxHeight, boxMin);
|
||||||
|
|
||||||
float dx = px - at.x;
|
float dx = px - at.x;
|
||||||
float dy = py - at.y;
|
float dy = py - at.y;
|
||||||
|
|
|
||||||
|
|
@ -753,6 +753,44 @@ static void blitOverlayScaled2x(std::vector<uint8_t>& composite, int compW, int
|
||||||
blitOverlayScaledN(composite, compW, compH, overlay, dstX, dstY, 2);
|
blitOverlayScaledN(composite, compW, compH, overlay, dstX, dstY, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nearest-neighbor downscale blit: sample every Nth pixel from overlay
|
||||||
|
static void blitOverlayDownscaleN(std::vector<uint8_t>& 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<size_t>(sy) * overlay.width + sx) * 4;
|
||||||
|
size_t dstIdx = (static_cast<size_t>(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<uint8_t>(overlay.data[srcIdx + 0] * alpha + composite[dstIdx + 0] * invAlpha);
|
||||||
|
composite[dstIdx + 1] = static_cast<uint8_t>(overlay.data[srcIdx + 1] * alpha + composite[dstIdx + 1] * invAlpha);
|
||||||
|
composite[dstIdx + 2] = static_cast<uint8_t>(overlay.data[srcIdx + 2] * alpha + composite[dstIdx + 2] * invAlpha);
|
||||||
|
composite[dstIdx + 3] = std::max(composite[dstIdx + 3], srcA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VkTexture* CharacterRenderer::compositeTextures(const std::vector<std::string>& layerPaths) {
|
VkTexture* CharacterRenderer::compositeTextures(const std::vector<std::string>& layerPaths) {
|
||||||
if (layerPaths.empty() || !assetManager || !assetManager->isInitialized()) {
|
if (layerPaths.empty() || !assetManager || !assetManager->isInitialized()) {
|
||||||
return whiteTexture_.get();
|
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
|
// Expected full-resolution size for this region at current atlas scale
|
||||||
int expectedW = regionSizes256[regionIdx][0] * scaleX;
|
int expectedW = regionSizes256[regionIdx][0] * scaleX;
|
||||||
int expectedH = regionSizes256[regionIdx][1] * scaleY;
|
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);
|
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 {
|
} else {
|
||||||
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
||||||
}
|
}
|
||||||
|
|
||||||
core::Logger::getInstance().debug("compositeWithRegions: region ", regionIdx,
|
core::Logger::getInstance().warning("compositeWithRegions: region ", regionIdx,
|
||||||
" at (", dstX, ",", dstY, ") ", overlay.width, "x", overlay.height, " from ", rl.second);
|
" at (", dstX, ",", dstY, ") overlay=", overlay.width, "x", overlay.height,
|
||||||
|
" expected=", expectedW, "x", expectedH, " from ", rl.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload to GPU via VkTexture
|
// Upload to GPU via VkTexture
|
||||||
|
|
|
||||||
|
|
@ -883,20 +883,40 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
|
||||||
surface.origin.z = adjustedZ;
|
surface.origin.z = adjustedZ;
|
||||||
surface.position.z = adjustedZ;
|
surface.position.z = adjustedZ;
|
||||||
|
|
||||||
|
|
||||||
if (surface.origin.z > 300.0f || surface.origin.z < -100.0f) return;
|
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<size_t>(surface.width) * static_cast<size_t>(surface.height);
|
size_t tileCount = static_cast<size_t>(surface.width) * static_cast<size_t>(surface.height);
|
||||||
size_t maskBytes = (tileCount + 7) / 8;
|
size_t maskBytes = (tileCount + 7) / 8;
|
||||||
surface.mask.assign(maskBytes, 0x00);
|
surface.mask.assign(maskBytes, 0x00);
|
||||||
|
const float baseZ = liquid.basePosition.z;
|
||||||
|
const bool hasHeights = !liquid.heights.empty() &&
|
||||||
|
liquid.heights.size() >= static_cast<size_t>(vertexCount);
|
||||||
for (size_t t = 0; t < tileCount; t++) {
|
for (size_t t = 0; t < tileCount; t++) {
|
||||||
bool hasLiquid = true;
|
bool hasLiquid = true;
|
||||||
|
int tx = static_cast<int>(t) % surface.width;
|
||||||
|
int ty = static_cast<int>(t) / surface.width;
|
||||||
|
|
||||||
|
// Standard WoW check: low nibble 0x0F = "don't render"
|
||||||
if (t < liquid.flags.size()) {
|
if (t < liquid.flags.size()) {
|
||||||
if ((liquid.flags[t] & 0x0F) == 0x0F) {
|
if ((liquid.flags[t] & 0x0F) == 0x0F) {
|
||||||
hasLiquid = false;
|
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<float>(tx) + 0.5f) +
|
||||||
|
surface.stepY * (static_cast<float>(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) {
|
if (hasLiquid) {
|
||||||
size_t byteIdx = t / 8;
|
size_t byteIdx = t / 8;
|
||||||
size_t bitIdx = t % 8;
|
size_t bitIdx = t % 8;
|
||||||
|
|
@ -905,6 +925,32 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
|
||||||
}
|
}
|
||||||
|
|
||||||
createWaterMesh(surface);
|
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<int>(t) % surface.width;
|
||||||
|
int aty = static_cast<int>(t) / surface.width;
|
||||||
|
glm::vec3 tw = surface.origin +
|
||||||
|
surface.stepX * (static_cast<float>(atx) + 0.5f) +
|
||||||
|
surface.stepY * (static_cast<float>(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 (surface.indexCount > 0) {
|
||||||
if (vkCtx) updateMaterialUBO(surface);
|
if (vkCtx) updateMaterialUBO(surface);
|
||||||
surfaces.push_back(std::move(surface));
|
surfaces.push_back(std::move(surface));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue