mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Fix interior WMO floor gaps, water exit stair clipping, NPC equipment, portal animation
- Fix Stormwind barracks floor: interior WMO groups named "facade" were incorrectly marked as LOD shells and hidden when close. Add !isIndoor guard to all LOD detection conditions so interior groups always render. - Fix water exit stair clipping: anchor lastGroundZ to current position on swim exit, set grounded=true for full step-up budget, add upward velocity boost to clear stair lip geometry. - Re-enable NPC humanoid equipment geosets (kEnableNpcHumanoidOverrides) so guards render with proper armor instead of underwear. - Keep instance portal GameObjects animated (spinning/glowing) instead of freezing all GO animations indiscriminately. - Fix equipment disappearing after instance round-trip by resetting dirty tracking on world reload. - Fix multi-doodad-set loading: load both set 0 (global) and placement- specific doodad set, with dedup to avoid double-loading. - Clear placedWmoIds in softReset/unloadAll to prevent stale dedup. - Apply MODF rotation to instance WMOs, snap player to WMO floor. - Re-enable rebuildSpatialIndex in setInstanceTransform. - Store precomputeFloorCache results in precomputed grid. - Add F8 debug key for WMO floor diagnostics at player position. - Expand mapIdToName with all Classic/TBC/WotLK instance map IDs.
This commit is contained in:
parent
bec7a678aa
commit
16d44c5bb3
6 changed files with 285 additions and 28 deletions
|
|
@ -281,6 +281,7 @@ public:
|
||||||
Inventory& getInventory() { return inventory; }
|
Inventory& getInventory() { return inventory; }
|
||||||
const Inventory& getInventory() const { return inventory; }
|
const Inventory& getInventory() const { return inventory; }
|
||||||
bool consumeOnlineEquipmentDirty() { bool d = onlineEquipDirty_; onlineEquipDirty_ = false; return d; }
|
bool consumeOnlineEquipmentDirty() { bool d = onlineEquipDirty_; onlineEquipDirty_ = false; return d; }
|
||||||
|
void resetEquipmentDirtyTracking() { lastEquipDisplayIds_ = {}; onlineEquipDirty_ = true; }
|
||||||
void unequipToBackpack(EquipSlot equipSlot);
|
void unequipToBackpack(EquipSlot equipSlot);
|
||||||
|
|
||||||
// Targeting
|
// Targeting
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,7 @@ public:
|
||||||
* Get number of active instances
|
* Get number of active instances
|
||||||
*/
|
*/
|
||||||
uint32_t getInstanceCount() const { return instances.size(); }
|
uint32_t getInstanceCount() const { return instances.size(); }
|
||||||
|
size_t getLoadedModelCount() const { return loadedModels.size(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove models that have no instances referencing them
|
* Remove models that have no instances referencing them
|
||||||
|
|
@ -262,6 +263,9 @@ public:
|
||||||
*/
|
*/
|
||||||
std::optional<float> getFloorHeight(float glX, float glY, float glZ, float* outNormalZ = nullptr) const;
|
std::optional<float> getFloorHeight(float glX, float glY, float glZ, float* outNormalZ = nullptr) const;
|
||||||
|
|
||||||
|
/** Dump diagnostic info about WMO groups overlapping a position */
|
||||||
|
void debugDumpGroupsAtPosition(float glX, float glY, float glZ) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check wall collision and adjust position
|
* Check wall collision and adjust position
|
||||||
* @param from Starting position
|
* @param from Starting position
|
||||||
|
|
|
||||||
|
|
@ -86,12 +86,94 @@ bool envFlagEnabled(const char* key, bool defaultValue = false) {
|
||||||
|
|
||||||
|
|
||||||
const char* Application::mapIdToName(uint32_t mapId) {
|
const char* Application::mapIdToName(uint32_t mapId) {
|
||||||
|
// Fallback when Map.dbc is unavailable. Names must match WDT directory names
|
||||||
|
// (case-insensitive — AssetManager lowercases all paths).
|
||||||
switch (mapId) {
|
switch (mapId) {
|
||||||
|
// Continents
|
||||||
case 0: return "Azeroth";
|
case 0: return "Azeroth";
|
||||||
case 1: return "Kalimdor";
|
case 1: return "Kalimdor";
|
||||||
case 369: return "DeeprunTram";
|
case 530: return "Expansion01";
|
||||||
case 530: return "Outland";
|
|
||||||
case 571: return "Northrend";
|
case 571: return "Northrend";
|
||||||
|
// Classic dungeons/raids
|
||||||
|
case 30: return "PVPZone01";
|
||||||
|
case 33: return "Shadowfang";
|
||||||
|
case 34: return "StormwindJail";
|
||||||
|
case 36: return "DeadminesInstance";
|
||||||
|
case 43: return "WailingCaverns";
|
||||||
|
case 47: return "RazserfenKraulInstance";
|
||||||
|
case 48: return "Blackfathom";
|
||||||
|
case 70: return "Uldaman";
|
||||||
|
case 90: return "GnomeragonInstance";
|
||||||
|
case 109: return "SunkenTemple";
|
||||||
|
case 129: return "RazorfenDowns";
|
||||||
|
case 189: return "MonasteryInstances";
|
||||||
|
case 209: return "TanarisInstance";
|
||||||
|
case 229: return "BlackRockSpire";
|
||||||
|
case 230: return "BlackrockDepths";
|
||||||
|
case 249: return "OnyxiaLairInstance";
|
||||||
|
case 289: return "ScholomanceInstance";
|
||||||
|
case 309: return "Zul'Gurub";
|
||||||
|
case 329: return "Stratholme";
|
||||||
|
case 349: return "Mauradon";
|
||||||
|
case 369: return "DeeprunTram";
|
||||||
|
case 389: return "OrgrimmarInstance";
|
||||||
|
case 409: return "MoltenCore";
|
||||||
|
case 429: return "DireMaul";
|
||||||
|
case 469: return "BlackwingLair";
|
||||||
|
case 489: return "PVPZone03";
|
||||||
|
case 509: return "AhnQiraj";
|
||||||
|
case 529: return "PVPZone04";
|
||||||
|
case 531: return "AhnQirajTemple";
|
||||||
|
case 533: return "Stratholme Raid";
|
||||||
|
// TBC
|
||||||
|
case 532: return "Karazahn";
|
||||||
|
case 534: return "HyjalPast";
|
||||||
|
case 540: return "HellfireMilitary";
|
||||||
|
case 542: return "HellfireDemon";
|
||||||
|
case 543: return "HellfireRampart";
|
||||||
|
case 544: return "HellfireRaid";
|
||||||
|
case 545: return "CoilfangPumping";
|
||||||
|
case 546: return "CoilfangMarsh";
|
||||||
|
case 547: return "CoilfangDraenei";
|
||||||
|
case 548: return "CoilfangRaid";
|
||||||
|
case 550: return "TempestKeepRaid";
|
||||||
|
case 552: return "TempestKeepArcane";
|
||||||
|
case 553: return "TempestKeepAtrium";
|
||||||
|
case 554: return "TempestKeepFactory";
|
||||||
|
case 555: return "AuchindounShadow";
|
||||||
|
case 556: return "AuchindounDraenei";
|
||||||
|
case 557: return "AuchindounEthereal";
|
||||||
|
case 558: return "AuchindounDemon";
|
||||||
|
case 560: return "HillsbradPast";
|
||||||
|
case 564: return "BlackTemple";
|
||||||
|
case 565: return "GruulsLair";
|
||||||
|
case 566: return "PVPZone05";
|
||||||
|
case 568: return "ZulAman";
|
||||||
|
case 580: return "SunwellPlateau";
|
||||||
|
case 585: return "Sunwell5ManFix";
|
||||||
|
// WotLK
|
||||||
|
case 574: return "Valgarde70";
|
||||||
|
case 575: return "UtgardePinnacle";
|
||||||
|
case 576: return "Nexus70";
|
||||||
|
case 578: return "Nexus80";
|
||||||
|
case 595: return "StratholmeCOT";
|
||||||
|
case 599: return "Ulduar70";
|
||||||
|
case 600: return "Ulduar80";
|
||||||
|
case 601: return "DrakTheronKeep";
|
||||||
|
case 602: return "GunDrak";
|
||||||
|
case 603: return "UlduarRaid";
|
||||||
|
case 608: return "DalaranPrison";
|
||||||
|
case 615: return "ChamberOfAspectsBlack";
|
||||||
|
case 617: return "DeathKnightStart";
|
||||||
|
case 619: return "Azjol_Uppercity";
|
||||||
|
case 624: return "WintergraspRaid";
|
||||||
|
case 631: return "IcecrownCitadel";
|
||||||
|
case 632: return "IcecrownCitadel5Man";
|
||||||
|
case 649: return "ArgentTournamentRaid";
|
||||||
|
case 650: return "ArgentTournamentDungeon";
|
||||||
|
case 658: return "QuarryOfTears";
|
||||||
|
case 668: return "HallsOfReflection";
|
||||||
|
case 724: return "ChamberOfAspectsRed";
|
||||||
default: return "";
|
default: return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -379,6 +461,14 @@ void Application::run() {
|
||||||
uiManager->getGameScreen().triggerDing(99);
|
uiManager->getGameScreen().triggerDing(99);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// F8: Debug WMO floor at current position
|
||||||
|
else if (event.key.keysym.scancode == SDL_SCANCODE_F8 && event.key.repeat == 0) {
|
||||||
|
if (renderer && renderer->getWMORenderer()) {
|
||||||
|
glm::vec3 pos = renderer->getCharacterPosition();
|
||||||
|
LOG_WARNING("F8: WMO floor debug at render pos (", pos.x, ", ", pos.y, ", ", pos.z, ")");
|
||||||
|
renderer->getWMORenderer()->debugDumpGroupsAtPosition(pos.x, pos.y, pos.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3255,6 +3345,11 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
cr->clear();
|
cr->clear();
|
||||||
renderer->setCharacterFollow(0);
|
renderer->setCharacterFollow(0);
|
||||||
}
|
}
|
||||||
|
// Reset equipment dirty tracking so composited textures are rebuilt
|
||||||
|
// after spawnPlayerCharacter() recreates the character instance.
|
||||||
|
if (gameHandler) {
|
||||||
|
gameHandler->resetEquipmentDirtyTracking();
|
||||||
|
}
|
||||||
|
|
||||||
if (auto* terrain = renderer->getTerrainManager()) {
|
if (auto* terrain = renderer->getTerrainManager()) {
|
||||||
terrain->softReset();
|
terrain->softReset();
|
||||||
|
|
@ -3509,20 +3604,20 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
LOG_INFO("Loaded ", loadedGroups, " / ", wmoModel.nGroups, " WMO groups for instance");
|
LOG_INFO("Loaded ", loadedGroups, " / ", wmoModel.nGroups, " WMO groups for instance");
|
||||||
}
|
}
|
||||||
|
|
||||||
// WMO-only maps: MODF position is at world origin (always 0,0,0 in practice).
|
// WMO-only maps: MODF uses same format as ADT MODF.
|
||||||
// Unlike ADT MODF which uses placement space, WMO-only maps place the WMO
|
// Apply the same rotation conversion that outdoor WMOs get
|
||||||
// directly in render coordinates with no offset or yaw bias.
|
// (including the implicit +180° Z yaw), but skip the ZEROPOINT
|
||||||
|
// position offset for zero-position instances (server sends
|
||||||
|
// coordinates relative to the WMO, not relative to map corner).
|
||||||
glm::vec3 wmoPos(0.0f);
|
glm::vec3 wmoPos(0.0f);
|
||||||
glm::vec3 wmoRot(0.0f);
|
glm::vec3 wmoRot(
|
||||||
|
-wdtInfo.rotation[2] * 3.14159f / 180.0f,
|
||||||
|
-wdtInfo.rotation[0] * 3.14159f / 180.0f,
|
||||||
|
(wdtInfo.rotation[1] + 180.0f) * 3.14159f / 180.0f
|
||||||
|
);
|
||||||
if (wdtInfo.position[0] != 0.0f || wdtInfo.position[1] != 0.0f || wdtInfo.position[2] != 0.0f) {
|
if (wdtInfo.position[0] != 0.0f || wdtInfo.position[1] != 0.0f || wdtInfo.position[2] != 0.0f) {
|
||||||
// Non-zero placement — convert from ADT space (rare/never happens, but be safe)
|
|
||||||
wmoPos = core::coords::adtToWorld(
|
wmoPos = core::coords::adtToWorld(
|
||||||
wdtInfo.position[0], wdtInfo.position[1], wdtInfo.position[2]);
|
wdtInfo.position[0], wdtInfo.position[1], wdtInfo.position[2]);
|
||||||
wmoRot = glm::vec3(
|
|
||||||
-wdtInfo.rotation[2] * 3.14159f / 180.0f,
|
|
||||||
-wdtInfo.rotation[0] * 3.14159f / 180.0f,
|
|
||||||
(wdtInfo.rotation[1] + 180.0f) * 3.14159f / 180.0f
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showProgress("Uploading instance geometry...", 0.70f);
|
showProgress("Uploading instance geometry...", 0.70f);
|
||||||
|
|
@ -3530,9 +3625,30 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
if (wmoRenderer->loadModel(wmoModel, wmoModelId)) {
|
if (wmoRenderer->loadModel(wmoModel, wmoModelId)) {
|
||||||
uint32_t instanceId = wmoRenderer->createInstance(wmoModelId, wmoPos, wmoRot, 1.0f);
|
uint32_t instanceId = wmoRenderer->createInstance(wmoModelId, wmoPos, wmoRot, 1.0f);
|
||||||
if (instanceId > 0) {
|
if (instanceId > 0) {
|
||||||
LOG_INFO("Instance WMO loaded: modelId=", wmoModelId,
|
LOG_WARNING("Instance WMO loaded: modelId=", wmoModelId,
|
||||||
" instanceId=", instanceId,
|
" instanceId=", instanceId);
|
||||||
" pos=(", wmoPos.x, ", ", wmoPos.y, ", ", wmoPos.z, ")");
|
LOG_WARNING(" MOHD bbox local: (",
|
||||||
|
wmoModel.boundingBoxMin.x, ", ", wmoModel.boundingBoxMin.y, ", ", wmoModel.boundingBoxMin.z,
|
||||||
|
") to (", wmoModel.boundingBoxMax.x, ", ", wmoModel.boundingBoxMax.y, ", ", wmoModel.boundingBoxMax.z, ")");
|
||||||
|
LOG_WARNING(" WMO pos: (", wmoPos.x, ", ", wmoPos.y, ", ", wmoPos.z,
|
||||||
|
") rot: (", wmoRot.x, ", ", wmoRot.y, ", ", wmoRot.z, ")");
|
||||||
|
LOG_WARNING(" Player render pos: (", spawnRender.x, ", ", spawnRender.y, ", ", spawnRender.z, ")");
|
||||||
|
LOG_WARNING(" Player canonical: (", spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z, ")");
|
||||||
|
// Show player position in WMO local space
|
||||||
|
{
|
||||||
|
glm::mat4 instMat(1.0f);
|
||||||
|
instMat = glm::translate(instMat, wmoPos);
|
||||||
|
instMat = glm::rotate(instMat, wmoRot.z, glm::vec3(0,0,1));
|
||||||
|
instMat = glm::rotate(instMat, wmoRot.y, glm::vec3(0,1,0));
|
||||||
|
instMat = glm::rotate(instMat, wmoRot.x, glm::vec3(1,0,0));
|
||||||
|
glm::mat4 invMat = glm::inverse(instMat);
|
||||||
|
glm::vec3 localPlayer = glm::vec3(invMat * glm::vec4(spawnRender, 1.0f));
|
||||||
|
LOG_WARNING(" Player in WMO local: (", localPlayer.x, ", ", localPlayer.y, ", ", localPlayer.z, ")");
|
||||||
|
bool inside = localPlayer.x >= wmoModel.boundingBoxMin.x && localPlayer.x <= wmoModel.boundingBoxMax.x &&
|
||||||
|
localPlayer.y >= wmoModel.boundingBoxMin.y && localPlayer.y <= wmoModel.boundingBoxMax.y &&
|
||||||
|
localPlayer.z >= wmoModel.boundingBoxMin.z && localPlayer.z <= wmoModel.boundingBoxMax.z;
|
||||||
|
LOG_WARNING(" Player inside MOHD bbox: ", inside ? "YES" : "NO");
|
||||||
|
}
|
||||||
|
|
||||||
// Load doodads from the specified doodad set
|
// Load doodads from the specified doodad set
|
||||||
auto* m2Renderer = renderer->getM2Renderer();
|
auto* m2Renderer = renderer->getM2Renderer();
|
||||||
|
|
@ -3619,6 +3735,31 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snap player to WMO floor so they don't fall through on first frame
|
||||||
|
if (wmoRenderer && renderer) {
|
||||||
|
glm::vec3 playerPos = renderer->getCharacterPosition();
|
||||||
|
// Query floor with generous height margin above spawn point
|
||||||
|
auto floor = wmoRenderer->getFloorHeight(playerPos.x, playerPos.y, playerPos.z + 50.0f);
|
||||||
|
if (floor) {
|
||||||
|
playerPos.z = *floor + 0.1f; // Small offset above floor
|
||||||
|
renderer->getCharacterPosition() = playerPos;
|
||||||
|
if (gameHandler) {
|
||||||
|
glm::vec3 canonical = core::coords::renderToCanonical(playerPos);
|
||||||
|
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
||||||
|
}
|
||||||
|
LOG_INFO("Snapped player to instance WMO floor: z=", *floor);
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("Could not find WMO floor at player spawn (",
|
||||||
|
playerPos.x, ", ", playerPos.y, ", ", playerPos.z, ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagnostic: verify WMO renderer state after instance loading
|
||||||
|
LOG_WARNING("=== INSTANCE WMO LOAD COMPLETE ===");
|
||||||
|
LOG_WARNING(" wmoRenderer models loaded: ", wmoRenderer->getLoadedModelCount());
|
||||||
|
LOG_WARNING(" wmoRenderer instances: ", wmoRenderer->getInstanceCount());
|
||||||
|
LOG_WARNING(" wmoRenderer floor cache: ", wmoRenderer->getFloorCacheSize());
|
||||||
|
|
||||||
terrainOk = true; // Mark as OK so post-load setup runs
|
terrainOk = true; // Mark as OK so post-load setup runs
|
||||||
} else {
|
} else {
|
||||||
// ---- Normal ADT-based map ----
|
// ---- Normal ADT-based map ----
|
||||||
|
|
@ -4960,7 +5101,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 = false;
|
static constexpr bool kEnableNpcHumanoidOverrides = true;
|
||||||
|
|
||||||
// Set geosets for humanoid NPCs based on CreatureDisplayInfoExtra
|
// Set geosets for humanoid NPCs based on CreatureDisplayInfoExtra
|
||||||
if (kEnableNpcHumanoidOverrides &&
|
if (kEnableNpcHumanoidOverrides &&
|
||||||
|
|
@ -6360,8 +6501,15 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Freeze animation — gameobjects are static until interacted with
|
// Freeze animation for static gameobjects, but let portals/effects animate
|
||||||
m2Renderer->setInstanceAnimationFrozen(instanceId, true);
|
std::string lowerPath = modelPath;
|
||||||
|
std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), ::tolower);
|
||||||
|
bool isAnimatedEffect = (lowerPath.find("instanceportal") != std::string::npos ||
|
||||||
|
lowerPath.find("portalfx") != std::string::npos ||
|
||||||
|
lowerPath.find("spellportal") != std::string::npos);
|
||||||
|
if (!isAnimatedEffect) {
|
||||||
|
m2Renderer->setInstanceAnimationFrozen(instanceId, true);
|
||||||
|
}
|
||||||
|
|
||||||
gameObjectInstances_[guid] = {modelId, instanceId, false};
|
gameObjectInstances_[guid] = {modelId, instanceId, false};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -659,7 +659,17 @@ void CameraController::update(float deltaTime) {
|
||||||
|
|
||||||
grounded = false;
|
grounded = false;
|
||||||
} else {
|
} else {
|
||||||
// Exiting water — give a small upward boost to help climb onto shore.
|
// Exiting water — boost upward to help climb onto shore/stairs.
|
||||||
|
if (wasSwimming) {
|
||||||
|
// Anchor lastGroundZ to current position so WMO floor probes
|
||||||
|
// start from a sensible height instead of stale pre-swim values.
|
||||||
|
lastGroundZ = targetPos.z;
|
||||||
|
grounded = true; // Treat as grounded so step-up budget is full
|
||||||
|
// Small upward boost to clear stair lip geometry
|
||||||
|
if (verticalVelocity < 1.5f) {
|
||||||
|
verticalVelocity = 1.5f;
|
||||||
|
}
|
||||||
|
}
|
||||||
swimming = false;
|
swimming = false;
|
||||||
|
|
||||||
if (glm::length(movement) > 0.001f) {
|
if (glm::length(movement) > 0.001f) {
|
||||||
|
|
|
||||||
|
|
@ -522,10 +522,18 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
||||||
wmoMatrix = glm::rotate(wmoMatrix, rot.y, glm::vec3(0, 1, 0));
|
wmoMatrix = glm::rotate(wmoMatrix, rot.y, glm::vec3(0, 1, 0));
|
||||||
wmoMatrix = glm::rotate(wmoMatrix, rot.x, glm::vec3(1, 0, 0));
|
wmoMatrix = glm::rotate(wmoMatrix, rot.x, glm::vec3(1, 0, 0));
|
||||||
|
|
||||||
const auto& doodadSet = wmoModel.doodadSets[0];
|
// Load doodads from set 0 (global) + placement-specific set
|
||||||
|
std::vector<uint32_t> setsToLoad = {0};
|
||||||
|
if (placement.doodadSet > 0 && placement.doodadSet < wmoModel.doodadSets.size()) {
|
||||||
|
setsToLoad.push_back(placement.doodadSet);
|
||||||
|
}
|
||||||
|
std::unordered_set<uint32_t> loadedDoodadIndices;
|
||||||
|
for (uint32_t setIdx : setsToLoad) {
|
||||||
|
const auto& doodadSet = wmoModel.doodadSets[setIdx];
|
||||||
for (uint32_t di = 0; di < doodadSet.count; di++) {
|
for (uint32_t di = 0; di < doodadSet.count; di++) {
|
||||||
uint32_t doodadIdx = doodadSet.startIndex + di;
|
uint32_t doodadIdx = doodadSet.startIndex + di;
|
||||||
if (doodadIdx >= wmoModel.doodads.size()) break;
|
if (doodadIdx >= wmoModel.doodads.size()) break;
|
||||||
|
if (!loadedDoodadIndices.insert(doodadIdx).second) continue;
|
||||||
|
|
||||||
const auto& doodad = wmoModel.doodads[doodadIdx];
|
const auto& doodad = wmoModel.doodads[doodadIdx];
|
||||||
auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex);
|
auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex);
|
||||||
|
|
@ -623,6 +631,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
||||||
doodadReady.modelMatrix = worldMatrix;
|
doodadReady.modelMatrix = worldMatrix;
|
||||||
pending->wmoDoodads.push_back(std::move(doodadReady));
|
pending->wmoDoodads.push_back(std::move(doodadReady));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PendingTile::WMOReady ready;
|
PendingTile::WMOReady ready;
|
||||||
|
|
@ -1311,6 +1320,7 @@ void TerrainManager::unloadAll() {
|
||||||
pendingTiles.clear();
|
pendingTiles.clear();
|
||||||
finalizingTiles_.clear();
|
finalizingTiles_.clear();
|
||||||
placedDoodadIds.clear();
|
placedDoodadIds.clear();
|
||||||
|
placedWmoIds.clear();
|
||||||
|
|
||||||
LOG_INFO("Unloading all terrain tiles");
|
LOG_INFO("Unloading all terrain tiles");
|
||||||
loadedTiles.clear();
|
loadedTiles.clear();
|
||||||
|
|
@ -1358,6 +1368,7 @@ void TerrainManager::softReset() {
|
||||||
pendingTiles.clear();
|
pendingTiles.clear();
|
||||||
finalizingTiles_.clear();
|
finalizingTiles_.clear();
|
||||||
placedDoodadIds.clear();
|
placedDoodadIds.clear();
|
||||||
|
placedWmoIds.clear();
|
||||||
|
|
||||||
// Clear tile cache — keys are (x,y) without map name, so stale entries from
|
// Clear tile cache — keys are (x,y) without map name, so stale entries from
|
||||||
// a different map with overlapping coordinates would produce wrong geometry.
|
// a different map with overlapping coordinates would produce wrong geometry.
|
||||||
|
|
|
||||||
|
|
@ -533,10 +533,8 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||||
// "city01" etc are exterior cityscape shells in large WMOs
|
// "city01" etc are exterior cityscape shells in large WMOs
|
||||||
isCityShell = (lower.find("city") == 0 && lower.size() <= 8);
|
isCityShell = (lower.find("city") == 0 && lower.size() <= 8);
|
||||||
}
|
}
|
||||||
// Flag 0x80 on INDOOR groups in large WMOs = interior cathedral shell
|
|
||||||
bool hasFlag80 = (wmoGroup.flags & 0x80) != 0;
|
|
||||||
bool isIndoor = (wmoGroup.flags & 0x2000) != 0;
|
bool isIndoor = (wmoGroup.flags & 0x2000) != 0;
|
||||||
if ((nVerts < 100 && isLargeWmo) || (alwaysDraw && nVerts < 5000) || (isFacade && isLargeWmo) || (isCityShell && isLargeWmo) || (hasFlag80 && isIndoor && isLargeWmo)) {
|
if ((nVerts < 100 && isLargeWmo && !isIndoor) || (alwaysDraw && nVerts < 5000 && isLargeWmo && !isIndoor) || (isFacade && isLargeWmo && !isIndoor) || (isCityShell && !isIndoor && isLargeWmo)) {
|
||||||
resources.isLOD = true;
|
resources.isLOD = true;
|
||||||
}
|
}
|
||||||
modelData.groups.push_back(resources);
|
modelData.groups.push_back(resources);
|
||||||
|
|
@ -954,9 +952,7 @@ void WMORenderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& tra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Don't rebuild spatial index on every transform update - causes flickering
|
rebuildSpatialIndex();
|
||||||
// Spatial grid is only used for collision queries, render iterates all instances
|
|
||||||
// rebuildSpatialIndex();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WMORenderer::addDoodadToInstance(uint32_t instanceId, uint32_t m2InstanceId, const glm::mat4& localTransform) {
|
void WMORenderer::addDoodadToInstance(uint32_t instanceId, uint32_t m2InstanceId, const glm::mat4& localTransform) {
|
||||||
|
|
@ -1228,8 +1224,11 @@ void WMORenderer::precomputeFloorCache() {
|
||||||
|
|
||||||
samplesChecked++;
|
samplesChecked++;
|
||||||
|
|
||||||
// getFloorHeight will compute and cache the result
|
// Query floor height and store result in the precomputed grid
|
||||||
getFloorHeight(sampleX, sampleY, refZ);
|
auto h = getFloorHeight(sampleX, sampleY, refZ);
|
||||||
|
if (h) {
|
||||||
|
precomputedFloorGrid[key] = *h;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2902,6 +2901,90 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
||||||
return bestFloor;
|
return bestFloor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WMORenderer::debugDumpGroupsAtPosition(float glX, float glY, float glZ) const {
|
||||||
|
LOG_WARNING("=== WMO Floor Debug at render(", glX, ", ", glY, ", ", glZ, ") ===");
|
||||||
|
|
||||||
|
glm::vec3 worldOrigin(glX, glY, glZ + 500.0f);
|
||||||
|
glm::vec3 worldDir(0.0f, 0.0f, -1.0f);
|
||||||
|
|
||||||
|
int totalInstancesChecked = 0;
|
||||||
|
int totalGroupsOverlapping = 0;
|
||||||
|
int totalFloorHits = 0;
|
||||||
|
|
||||||
|
for (const auto& instance : instances) {
|
||||||
|
auto it = loadedModels.find(instance.modelId);
|
||||||
|
if (it == loadedModels.end()) continue;
|
||||||
|
const ModelData& model = it->second;
|
||||||
|
|
||||||
|
// Check instance world bounds
|
||||||
|
if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x ||
|
||||||
|
glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y ||
|
||||||
|
glZ < instance.worldBoundsMin.z - 20.0f || glZ > instance.worldBoundsMax.z + 20.0f) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
totalInstancesChecked++;
|
||||||
|
LOG_WARNING(" Instance modelId=", instance.modelId,
|
||||||
|
" worldBounds=(", instance.worldBoundsMin.x, ",", instance.worldBoundsMin.y, ",", instance.worldBoundsMin.z,
|
||||||
|
")-(", instance.worldBoundsMax.x, ",", instance.worldBoundsMax.y, ",", instance.worldBoundsMax.z,
|
||||||
|
") groups=", model.groups.size());
|
||||||
|
|
||||||
|
glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(worldOrigin, 1.0f));
|
||||||
|
glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(worldDir, 0.0f)));
|
||||||
|
|
||||||
|
for (size_t gi = 0; gi < model.groups.size(); ++gi) {
|
||||||
|
// Check world-space group bounds
|
||||||
|
if (gi < instance.worldGroupBounds.size()) {
|
||||||
|
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
||||||
|
if (glX < gMin.x || glX > gMax.x ||
|
||||||
|
glY < gMin.y || glY > gMax.y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalGroupsOverlapping++;
|
||||||
|
const auto& group = model.groups[gi];
|
||||||
|
|
||||||
|
// Count floor triangles in this group under the player
|
||||||
|
int floorTris = 0;
|
||||||
|
float bestHitZ = -999999.0f;
|
||||||
|
const auto& verts = group.collisionVertices;
|
||||||
|
const auto& indices = group.collisionIndices;
|
||||||
|
for (size_t ti = 0; ti + 2 < indices.size(); ti += 3) {
|
||||||
|
const glm::vec3& v0 = verts[indices[ti]];
|
||||||
|
const glm::vec3& v1 = verts[indices[ti + 1]];
|
||||||
|
const glm::vec3& v2 = verts[indices[ti + 2]];
|
||||||
|
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
|
||||||
|
if (t <= 0.0f) t = rayTriangleIntersect(localOrigin, localDir, v0, v2, v1);
|
||||||
|
if (t > 0.0f) {
|
||||||
|
glm::vec3 hitLocal = localOrigin + localDir * t;
|
||||||
|
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
||||||
|
floorTris++;
|
||||||
|
totalFloorHits++;
|
||||||
|
if (hitWorld.z > bestHitZ) bestHitZ = hitWorld.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 gWorldMin(0), gWorldMax(0);
|
||||||
|
if (gi < instance.worldGroupBounds.size()) {
|
||||||
|
gWorldMin = instance.worldGroupBounds[gi].first;
|
||||||
|
gWorldMax = instance.worldGroupBounds[gi].second;
|
||||||
|
}
|
||||||
|
LOG_WARNING(" Group[", gi, "] flags=0x", std::hex, group.groupFlags, std::dec,
|
||||||
|
" verts=", group.collisionVertices.size(),
|
||||||
|
" tris=", group.collisionIndices.size()/3,
|
||||||
|
" batches=", group.mergedBatches.size(),
|
||||||
|
" isLOD=", group.isLOD,
|
||||||
|
" floorHits=", floorTris,
|
||||||
|
" bestHitZ=", bestHitZ,
|
||||||
|
" wBounds=(", gWorldMin.x, ",", gWorldMin.y, ",", gWorldMin.z,
|
||||||
|
")-(", gWorldMax.x, ",", gWorldMax.y, ",", gWorldMax.z, ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_WARNING("=== Total: ", totalInstancesChecked, " instances, ",
|
||||||
|
totalGroupsOverlapping, " overlapping groups, ",
|
||||||
|
totalFloorHits, " floor hits ===");
|
||||||
|
}
|
||||||
|
|
||||||
bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3& adjustedPos, bool insideWMO) const {
|
bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3& adjustedPos, bool insideWMO) const {
|
||||||
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
||||||
adjustedPos = to;
|
adjustedPos = to;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue