mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-27 09:03:51 +00:00
Fix NPC visibility and stabilize world transport/taxi updates
This commit is contained in:
parent
5dae994830
commit
f752a4f517
16 changed files with 452 additions and 173 deletions
|
|
@ -323,7 +323,8 @@ GLuint CharacterRenderer::loadTexture(const std::string& path) {
|
|||
auto blpImage = assetManager->loadTexture(path);
|
||||
if (!blpImage.isValid()) {
|
||||
core::Logger::getInstance().warning("Failed to load texture: ", path);
|
||||
textureCache[path] = whiteTexture;
|
||||
// Do not cache failures as white. Some asset reads can fail transiently and
|
||||
// we want later retries (e.g., mount skins loaded shortly after model spawn).
|
||||
return whiteTexture;
|
||||
}
|
||||
|
||||
|
|
@ -1257,6 +1258,26 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
glBindVertexArray(gpuModel.vao);
|
||||
|
||||
if (!gpuModel.data.batches.empty()) {
|
||||
bool applyGeosetFilter = !instance.activeGeosets.empty();
|
||||
if (applyGeosetFilter) {
|
||||
bool hasRenderableGeoset = false;
|
||||
for (const auto& batch : gpuModel.data.batches) {
|
||||
if (instance.activeGeosets.find(batch.submeshId) != instance.activeGeosets.end()) {
|
||||
hasRenderableGeoset = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasRenderableGeoset) {
|
||||
static std::unordered_set<uint32_t> loggedGeosetFallback;
|
||||
if (loggedGeosetFallback.insert(instance.id).second) {
|
||||
LOG_WARNING("Geoset filter matched no batches for instance ",
|
||||
instance.id, " (model ", instance.modelId,
|
||||
"); rendering all batches as fallback");
|
||||
}
|
||||
applyGeosetFilter = false;
|
||||
}
|
||||
}
|
||||
|
||||
// One-time debug dump of rendered batches per model
|
||||
static std::unordered_set<uint32_t> dumpedModels;
|
||||
if (dumpedModels.find(instance.modelId) == dumpedModels.end()) {
|
||||
|
|
@ -1264,7 +1285,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
int bIdx = 0;
|
||||
int rendered = 0, skipped = 0;
|
||||
for (const auto& b : gpuModel.data.batches) {
|
||||
bool filtered = !instance.activeGeosets.empty() &&
|
||||
bool filtered = applyGeosetFilter &&
|
||||
(b.submeshId / 100 != 0) &&
|
||||
instance.activeGeosets.find(b.submeshId) == instance.activeGeosets.end();
|
||||
|
||||
|
|
@ -1304,7 +1325,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
// For character models, group 0 (body/scalp) is also filtered so that only
|
||||
// the correct scalp mesh renders (not all overlapping variants).
|
||||
for (const auto& batch : gpuModel.data.batches) {
|
||||
if (!instance.activeGeosets.empty()) {
|
||||
if (applyGeosetFilter) {
|
||||
if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1661,14 +1661,13 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
shader->setUniform("uProjection", projection);
|
||||
shader->setUniform("uLightDir", lightDir);
|
||||
shader->setUniform("uLightColor", lightColor);
|
||||
shader->setUniform("uSpecularIntensity", onTaxi_ ? 0.0f : 0.5f); // Disable specular during taxi for performance
|
||||
shader->setUniform("uSpecularIntensity", 0.5f);
|
||||
shader->setUniform("uAmbientColor", ambientColor);
|
||||
shader->setUniform("uViewPos", camera.getPosition());
|
||||
shader->setUniform("uFogColor", fogColor);
|
||||
shader->setUniform("uFogStart", fogStart);
|
||||
shader->setUniform("uFogEnd", fogEnd);
|
||||
// Disable shadows during taxi for better performance
|
||||
bool useShadows = shadowEnabled && !onTaxi_;
|
||||
bool useShadows = shadowEnabled;
|
||||
shader->setUniform("uShadowEnabled", useShadows ? 1 : 0);
|
||||
shader->setUniform("uShadowStrength", 0.65f);
|
||||
if (useShadows) {
|
||||
|
|
@ -1681,7 +1680,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
lastDrawCallCount = 0;
|
||||
|
||||
// Adaptive render distance: balanced for performance without excessive pop-in
|
||||
const float maxRenderDistance = onTaxi_ ? 700.0f : (instances.size() > 2000) ? 350.0f : 1000.0f;
|
||||
const float maxRenderDistance = (instances.size() > 2000) ? 350.0f : 1000.0f;
|
||||
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
|
||||
const float fadeStartFraction = 0.75f;
|
||||
const glm::vec3 camPos = camera.getPosition();
|
||||
|
|
@ -1787,22 +1786,6 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
|
||||
const M2ModelGPU& model = *currentModel;
|
||||
|
||||
// Relaxed culling during taxi (VRAM caching eliminates loading hitches)
|
||||
if (onTaxi_) {
|
||||
// Skip tiny props (barrels, crates, small debris)
|
||||
if (model.boundRadius < 2.0f) {
|
||||
continue;
|
||||
}
|
||||
// Skip small ground foliage (bushes, flowers) but keep trees
|
||||
if (model.collisionNoBlock && model.boundRadius < 5.0f) {
|
||||
continue;
|
||||
}
|
||||
// Skip deep underwater objects (opaque water hides them anyway)
|
||||
if (instance.position.z < -10.0f) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Distance-based fade alpha for smooth pop-in (squared-distance, no sqrt)
|
||||
float fadeAlpha = 1.0f;
|
||||
float fadeFrac = model.disableAnimation ? 0.55f : fadeStartFraction;
|
||||
|
|
|
|||
|
|
@ -557,6 +557,9 @@ void Renderer::setCharacterFollow(uint32_t instanceId) {
|
|||
void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) {
|
||||
mountInstanceId_ = mountInstId;
|
||||
mountHeightOffset_ = heightOffset;
|
||||
mountSeatAttachmentId_ = -1;
|
||||
smoothedMountSeatPos_ = characterPosition;
|
||||
mountSeatSmoothingInit_ = false;
|
||||
mountAction_ = MountAction::None; // Clear mount action state
|
||||
mountActionPhase_ = 0;
|
||||
charAnimState = CharAnimState::MOUNT;
|
||||
|
|
@ -796,6 +799,9 @@ void Renderer::clearMount() {
|
|||
mountHeightOffset_ = 0.0f;
|
||||
mountPitch_ = 0.0f;
|
||||
mountRoll_ = 0.0f;
|
||||
mountSeatAttachmentId_ = -1;
|
||||
smoothedMountSeatPos_ = glm::vec3(0.0f);
|
||||
mountSeatSmoothingInit_ = false;
|
||||
mountAction_ = MountAction::None;
|
||||
mountActionPhase_ = 0;
|
||||
charAnimState = CharAnimState::IDLE;
|
||||
|
|
@ -954,6 +960,10 @@ void Renderer::updateCharacterAnimation() {
|
|||
if (mountInstanceId_ > 0) {
|
||||
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
||||
float yawRad = glm::radians(characterYaw);
|
||||
if (taxiFlight_) {
|
||||
// Taxi mounts commonly use a different model-forward axis than player rigs.
|
||||
yawRad += 1.57079632679f;
|
||||
}
|
||||
|
||||
// Procedural lean into turns (ground mounts only, optional enhancement)
|
||||
if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) {
|
||||
|
|
@ -1164,22 +1174,53 @@ void Renderer::updateCharacterAnimation() {
|
|||
|
||||
// Use mount's attachment point for proper bone-driven rider positioning
|
||||
glm::mat4 mountSeatTransform;
|
||||
if (characterRenderer->getAttachmentTransform(mountInstanceId_, 0, mountSeatTransform)) {
|
||||
bool haveSeat = false;
|
||||
if (mountSeatAttachmentId_ >= 0) {
|
||||
haveSeat = characterRenderer->getAttachmentTransform(
|
||||
mountInstanceId_, static_cast<uint32_t>(mountSeatAttachmentId_), mountSeatTransform);
|
||||
} else if (mountSeatAttachmentId_ == -1) {
|
||||
// Probe common rider seat attachment IDs once per mount.
|
||||
static constexpr uint32_t kSeatAttachments[] = {0, 5, 6, 7, 8};
|
||||
for (uint32_t attId : kSeatAttachments) {
|
||||
if (characterRenderer->getAttachmentTransform(mountInstanceId_, attId, mountSeatTransform)) {
|
||||
mountSeatAttachmentId_ = static_cast<int>(attId);
|
||||
haveSeat = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!haveSeat) {
|
||||
mountSeatAttachmentId_ = -2;
|
||||
}
|
||||
}
|
||||
|
||||
if (haveSeat) {
|
||||
// Extract position from mount seat transform (attachment point already includes proper seat height)
|
||||
glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]);
|
||||
|
||||
// Apply small vertical offset to reduce foot clipping (mount attachment point has correct X/Y)
|
||||
glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, 0.2f);
|
||||
// Keep seat offset minimal; large offsets amplify visible bobble.
|
||||
glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, taxiFlight_ ? 0.04f : 0.08f);
|
||||
glm::vec3 targetRiderPos = mountSeatPos + seatOffset;
|
||||
if (!mountSeatSmoothingInit_) {
|
||||
smoothedMountSeatPos_ = targetRiderPos;
|
||||
mountSeatSmoothingInit_ = true;
|
||||
} else {
|
||||
float smoothHz = taxiFlight_ ? 10.0f : 14.0f;
|
||||
float alpha = 1.0f - std::exp(-smoothHz * std::max(lastDeltaTime_, 0.001f));
|
||||
smoothedMountSeatPos_ = glm::mix(smoothedMountSeatPos_, targetRiderPos, alpha);
|
||||
}
|
||||
|
||||
// Position rider at mount seat
|
||||
characterRenderer->setInstancePosition(characterInstanceId, mountSeatPos + seatOffset);
|
||||
characterRenderer->setInstancePosition(characterInstanceId, smoothedMountSeatPos_);
|
||||
|
||||
// Rider uses character facing yaw, not mount bone rotation
|
||||
// (rider faces character direction, seat bone only provides position)
|
||||
float yawRad = glm::radians(characterYaw);
|
||||
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad));
|
||||
float riderPitch = taxiFlight_ ? mountPitch_ * 0.35f : 0.0f;
|
||||
float riderRoll = taxiFlight_ ? mountRoll_ * 0.35f : 0.0f;
|
||||
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, yawRad));
|
||||
} else {
|
||||
// Fallback to old manual positioning if attachment not found
|
||||
mountSeatSmoothingInit_ = false;
|
||||
float yawRad = glm::radians(characterYaw);
|
||||
glm::mat4 mountRotation = glm::mat4(1.0f);
|
||||
mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
|
|
@ -2385,9 +2426,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
}
|
||||
|
||||
// Render weather particles (after terrain/water, before characters)
|
||||
// Skip during taxi flights for performance and visual clarity
|
||||
bool onTaxi = cameraController && cameraController->isOnTaxi();
|
||||
if (weather && camera && !onTaxi) {
|
||||
if (weather && camera) {
|
||||
weather->render(*camera);
|
||||
}
|
||||
|
||||
|
|
@ -2430,11 +2469,8 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
}
|
||||
auto m2Start = std::chrono::steady_clock::now();
|
||||
m2Renderer->render(*camera, view, projection);
|
||||
// Skip particle fog during taxi (expensive and visually distracting)
|
||||
if (!onTaxi) {
|
||||
m2Renderer->renderSmokeParticles(*camera, view, projection);
|
||||
m2Renderer->renderM2Particles(view, projection);
|
||||
}
|
||||
m2Renderer->renderSmokeParticles(*camera, view, projection);
|
||||
m2Renderer->renderM2Particles(view, projection);
|
||||
auto m2End = std::chrono::steady_clock::now();
|
||||
lastM2RenderMs = std::chrono::duration<double, std::milli>(m2End - m2Start).count();
|
||||
}
|
||||
|
|
@ -2807,6 +2843,23 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
|
|||
if (questMarkerRenderer) {
|
||||
questMarkerRenderer->initialize(assetManager);
|
||||
}
|
||||
|
||||
// Prewarm frequently used zone/tavern music so zone transitions don't stall on MPQ I/O.
|
||||
if (zoneManager) {
|
||||
for (const auto& musicPath : zoneManager->getAllMusicPaths()) {
|
||||
musicManager->preloadMusic(musicPath);
|
||||
}
|
||||
}
|
||||
static const std::vector<std::string> tavernTracks = {
|
||||
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance01.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance02.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern1A.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3",
|
||||
};
|
||||
for (const auto& musicPath : tavernTracks) {
|
||||
musicManager->preloadMusic(musicPath);
|
||||
}
|
||||
|
||||
cachedAssetManager = assetManager;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,17 @@ void WorldMap::setMapName(const std::string& name) {
|
|||
viewLevel = ViewLevel::WORLD;
|
||||
}
|
||||
|
||||
void WorldMap::setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData) {
|
||||
if (!hasData || masks.empty()) {
|
||||
hasServerExplorationMask = false;
|
||||
serverExplorationMask.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
hasServerExplorationMask = true;
|
||||
serverExplorationMask = masks;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// GL resource creation
|
||||
// --------------------------------------------------------
|
||||
|
|
@ -195,7 +206,22 @@ void WorldMap::loadZonesFromDBC() {
|
|||
}
|
||||
}
|
||||
|
||||
// Step 2: Load ALL WorldMapArea records for this mapID
|
||||
// Step 2: Load AreaTable explore flags by areaID.
|
||||
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
|
||||
auto areaDbc = assetManager->loadDBC("AreaTable.dbc");
|
||||
if (areaDbc && areaDbc->isLoaded() && areaDbc->getFieldCount() > 3) {
|
||||
for (uint32_t i = 0; i < areaDbc->getRecordCount(); i++) {
|
||||
const uint32_t areaId = areaDbc->getUInt32(i, 0);
|
||||
const uint32_t exploreFlag = areaDbc->getUInt32(i, 3);
|
||||
if (areaId != 0) {
|
||||
exploreFlagByAreaId[areaId] = exploreFlag;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("WorldMap: AreaTable.dbc missing or unexpected format; server exploration may be incomplete");
|
||||
}
|
||||
|
||||
// Step 3: Load ALL WorldMapArea records for this mapID
|
||||
auto wmaDbc = assetManager->loadDBC("WorldMapArea.dbc");
|
||||
if (!wmaDbc || !wmaDbc->isLoaded()) {
|
||||
LOG_WARNING("WorldMap: WorldMapArea.dbc not found");
|
||||
|
|
@ -224,6 +250,10 @@ void WorldMap::loadZonesFromDBC() {
|
|||
zone.locBottom = wmaDbc->getFloat(i, 7);
|
||||
zone.displayMapID = wmaDbc->getUInt32(i, 8);
|
||||
zone.parentWorldMapID = wmaDbc->getUInt32(i, 10);
|
||||
auto exploreIt = exploreFlagByAreaId.find(zone.areaID);
|
||||
if (exploreIt != exploreFlagByAreaId.end()) {
|
||||
zone.exploreFlag = exploreIt->second;
|
||||
}
|
||||
|
||||
int idx = static_cast<int>(zones.size());
|
||||
|
||||
|
|
@ -728,9 +758,66 @@ glm::vec2 WorldMap::renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) co
|
|||
// --------------------------------------------------------
|
||||
|
||||
void WorldMap::updateExploration(const glm::vec3& playerRenderPos) {
|
||||
int zoneIdx = findZoneForPlayer(playerRenderPos);
|
||||
if (zoneIdx >= 0) {
|
||||
exploredZones.insert(zoneIdx);
|
||||
auto isExploreFlagSet = [this](uint32_t flag) -> bool {
|
||||
if (!hasServerExplorationMask || serverExplorationMask.empty() || flag == 0) return false;
|
||||
|
||||
const auto isSet = [this](uint32_t bitIndex) -> bool {
|
||||
const size_t word = bitIndex / 32;
|
||||
if (word >= serverExplorationMask.size()) return false;
|
||||
const uint32_t bit = bitIndex % 32;
|
||||
return (serverExplorationMask[word] & (1u << bit)) != 0;
|
||||
};
|
||||
|
||||
// Most cores use zero-based bit indices; some data behaves one-based.
|
||||
if (isSet(flag)) return true;
|
||||
if (flag > 0 && isSet(flag - 1)) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
bool markedAny = false;
|
||||
if (hasServerExplorationMask) {
|
||||
exploredZones.clear();
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
const auto& z = zones[i];
|
||||
if (z.areaID == 0 || z.exploreFlag == 0) continue;
|
||||
if (isExploreFlagSet(z.exploreFlag)) {
|
||||
exploredZones.insert(i);
|
||||
markedAny = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to local bounds-based reveal if server masks are missing/unusable.
|
||||
if (markedAny) return;
|
||||
|
||||
float wowX = playerRenderPos.y; // north/south
|
||||
float wowY = playerRenderPos.x; // west/east
|
||||
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
const auto& z = zones[i];
|
||||
if (z.areaID == 0) continue; // skip continent-level entries
|
||||
|
||||
float minX = std::min(z.locLeft, z.locRight);
|
||||
float maxX = std::max(z.locLeft, z.locRight);
|
||||
float minY = std::min(z.locTop, z.locBottom);
|
||||
float maxY = std::max(z.locTop, z.locBottom);
|
||||
float spanX = maxX - minX;
|
||||
float spanY = maxY - minY;
|
||||
if (spanX < 0.001f || spanY < 0.001f) continue;
|
||||
|
||||
bool contains = (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY);
|
||||
if (contains) {
|
||||
exploredZones.insert(i);
|
||||
markedAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for imperfect DBC bounds: reveal nearest zone so exploration still progresses.
|
||||
if (!markedAny) {
|
||||
int zoneIdx = findZoneForPlayer(playerRenderPos);
|
||||
if (zoneIdx >= 0) {
|
||||
exploredZones.insert(zoneIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -791,11 +878,16 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr
|
|||
return;
|
||||
}
|
||||
|
||||
// Mouse wheel: scroll up = zoom in, scroll down = zoom out
|
||||
// Mouse wheel: scroll up = zoom in, scroll down = zoom out.
|
||||
// Use both ImGui and raw input wheel deltas for reliability across frame order/capture paths.
|
||||
auto& io = ImGui::GetIO();
|
||||
if (io.MouseWheel > 0.0f) {
|
||||
float wheelDelta = io.MouseWheel;
|
||||
if (std::abs(wheelDelta) < 0.001f) {
|
||||
wheelDelta = input.getMouseWheelDelta();
|
||||
}
|
||||
if (wheelDelta > 0.0f) {
|
||||
zoomIn(playerRenderPos);
|
||||
} else if (io.MouseWheel < 0.0f) {
|
||||
} else if (wheelDelta < 0.0f) {
|
||||
zoomOut();
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue