From 2e0a7e0039690933cc379a5dc231035807e2aa2a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 8 Feb 2026 22:00:33 -0800 Subject: [PATCH] Fix taxi mount orientation and eliminate tile loading hitches Fixes two critical taxi flight issues: 1. Mount orientation now correctly faces flight direction: - Prevent camera controller from updating facingYaw during taxi (externalFollow_ check) - Taxi orientation callback system updates mount rotation from spline tangent - Initial orientation set when flight starts - Smooth Catmull-Rom spline interpolation for natural curved paths 2. Eliminate frame hitches from tile loading during flight: - New taxiFlightStartCallback uploads ALL precached tiles to GPU before flight begins - Previously tiles loaded async during 3s mount delay but uploaded 1/frame during flight - Now processAllReadyTiles() blocks briefly after mount delay to batch upload everything - Combined with 2.0s terrain update interval and aggressive culling for smooth flight Additional optimizations: - Aggressive taxi culling: skip models <15 units, all foliage/trees, underwater objects - Max render distance reduced to 150 units during taxi - Movement heartbeat packets disabled during taxi (server controls position) - Reduced taxi speed from 32 to 18 units/sec to prevent streaming overload --- include/game/game_handler.hpp | 12 ++++- include/rendering/camera_controller.hpp | 1 + src/core/application.cpp | 25 +++++++-- src/game/game_handler.cpp | 72 +++++++++++++++++++++---- src/rendering/camera_controller.cpp | 3 +- src/rendering/m2_renderer.cpp | 21 ++++++-- src/rendering/renderer.cpp | 6 ++- 7 files changed, 119 insertions(+), 21 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 21b90645..278fa7c5 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -501,6 +501,14 @@ public: using TaxiPrecacheCallback = std::function&)>; void setTaxiPrecacheCallback(TaxiPrecacheCallback cb) { taxiPrecacheCallback_ = std::move(cb); } + // Taxi orientation callback (for mount rotation) + using TaxiOrientationCallback = std::function; + void setTaxiOrientationCallback(TaxiOrientationCallback cb) { taxiOrientationCallback_ = std::move(cb); } + + // Callback for when taxi flight is about to start (after mounting delay, before movement begins) + using TaxiFlightStartCallback = std::function; + void setTaxiFlightStartCallback(TaxiFlightStartCallback cb) { taxiFlightStartCallback_ = std::move(cb); } + bool isMounted() const { return currentMountDisplayId_ != 0; } bool isHostileAttacker(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; } float getServerRunSpeed() const { return serverRunSpeed_; } @@ -959,7 +967,7 @@ private: bool taxiClientActive_ = false; size_t taxiClientIndex_ = 0; std::vector taxiClientPath_; - float taxiClientSpeed_ = 32.0f; + float taxiClientSpeed_ = 18.0f; // Reduced from 32 to prevent loading hitches float taxiClientSegmentProgress_ = 0.0f; bool taxiMountingDelay_ = false; // Delay before flight starts (terrain precache time) float taxiMountingTimer_ = 0.0f; @@ -1023,6 +1031,8 @@ private: NpcSwingCallback npcSwingCallback_; MountCallback mountCallback_; TaxiPrecacheCallback taxiPrecacheCallback_; + TaxiOrientationCallback taxiOrientationCallback_; + TaxiFlightStartCallback taxiFlightStartCallback_; uint32_t currentMountDisplayId_ = 0; float serverRunSpeed_ = 7.0f; bool playerDead_ = false; diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 113092d0..14e1e1af 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -89,6 +89,7 @@ public: void setMountHeightOffset(float offset) { mountHeightOffset_ = offset; } void setExternalFollow(bool enabled) { externalFollow_ = enabled; } void setExternalMoving(bool moving) { externalMoving_ = moving; } + void setFacingYaw(float yaw) { facingYaw = yaw; } // For taxi/scripted movement void clearMovementInputs(); // For first-person player hiding diff --git a/src/core/application.cpp b/src/core/application.cpp index 190e35d3..a1721fcc 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -420,10 +420,9 @@ void Application::update(float deltaTime) { } if (renderer && renderer->getTerrainManager()) { renderer->getTerrainManager()->setStreamingEnabled(true); - // With 8GB tile cache, keep streaming active during taxi at moderate rate. - // Increase load radius to pre-cache tiles ahead of flight path. + // With 8GB tile cache and precaching, minimize streaming during taxi if (onTaxi) { - renderer->getTerrainManager()->setUpdateInterval(0.3f); + renderer->getTerrainManager()->setUpdateInterval(2.0f); // Very infrequent updates - already precached renderer->getTerrainManager()->setLoadRadius(2); // 5x5 grid for taxi (each tile ~533 yards) } else { // Ramp streaming back in after taxi to avoid end-of-flight hitches. @@ -466,7 +465,8 @@ void Application::update(float deltaTime) { } // Send movement heartbeat every 500ms (keeps server position in sync) - if (gameHandler && renderer) { + // Skip during taxi flights - server controls position + if (gameHandler && renderer && !onTaxi) { movementHeartbeatTimer += deltaTime; if (movementHeartbeatTimer >= 0.5f) { movementHeartbeatTimer = 0.0f; @@ -718,6 +718,23 @@ void Application::setupUICallbacks() { renderer->getTerrainManager()->precacheTiles(tilesToLoad); }); + // Taxi orientation callback - update mount rotation during flight + gameHandler->setTaxiOrientationCallback([this](float orientationRadians) { + if (renderer && renderer->getCameraController()) { + // Convert radians to degrees for camera controller + float yawDegrees = glm::degrees(orientationRadians); + renderer->getCameraController()->setFacingYaw(yawDegrees); + } + }); + + // Taxi flight start callback - upload all precached tiles to GPU before flight begins + gameHandler->setTaxiFlightStartCallback([this]() { + if (renderer && renderer->getTerrainManager()) { + LOG_INFO("Uploading all precached tiles to GPU before taxi flight..."); + renderer->getTerrainManager()->processAllReadyTiles(); + } + }); + // Creature move callback (online mode) - update creature positions gameHandler->setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) { auto it = creatureInstances_.find(guid); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index e93225e6..5ead3cb5 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -269,6 +269,10 @@ void GameHandler::update(float deltaTime) { if (taxiMountingTimer_ >= 3.0f) { taxiMountingDelay_ = false; taxiMountingTimer_ = 0.0f; + // Upload all precached tiles to GPU before flight starts + if (taxiFlightStartCallback_) { + taxiFlightStartCallback_(); + } if (!taxiPendingPath_.empty()) { startClientTaxiPath(taxiPendingPath_); taxiPendingPath_.clear(); @@ -5182,6 +5186,24 @@ void GameHandler::startClientTaxiPath(const std::vector& pathNodes) { return; } + // Set initial orientation to face the first flight segment + if (!entityManager.hasEntity(playerGuid)) return; + auto playerEntity = entityManager.getEntity(playerGuid); + if (playerEntity) { + glm::vec3 start = taxiClientPath_[0]; + glm::vec3 end = taxiClientPath_[1]; + glm::vec3 dir = end - start; + float initialOrientation = std::atan2(dir.y, dir.x) - 1.57079632679f; + + playerEntity->setPosition(start.x, start.y, start.z, initialOrientation); + movementInfo.orientation = initialOrientation; + + // Update mount rotation immediately + if (taxiOrientationCallback_) { + taxiOrientationCallback_(initialOrientation); + } + } + LOG_INFO("Taxi flight started with ", taxiClientPath_.size(), " spline waypoints"); taxiClientActive_ = true; } @@ -5234,20 +5256,52 @@ void GameHandler::updateClientTaxi(float deltaTime) { return; } - glm::vec3 dirNorm = dir / segmentLen; - glm::vec3 nextPos = start + dirNorm * (t * segmentLen); + // Use Catmull-Rom spline for smooth interpolation between waypoints + // Get surrounding points for spline curve + glm::vec3 p0 = (taxiClientIndex_ > 0) ? taxiClientPath_[taxiClientIndex_ - 1] : start; + glm::vec3 p1 = start; + glm::vec3 p2 = end; + glm::vec3 p3 = (taxiClientIndex_ + 2 < taxiClientPath_.size()) ? + taxiClientPath_[taxiClientIndex_ + 2] : end; - // Add a flight arc to avoid terrain collisions. - float arcHeight = std::clamp(segmentLen * 0.15f, 20.0f, 120.0f); - float arc = 4.0f * t * (1.0f - t); - nextPos.z = glm::mix(start.z, end.z, t) + arcHeight * arc; + // Catmull-Rom spline formula for smooth curves + float t2 = t * t; + float t3 = t2 * t; + glm::vec3 nextPos = 0.5f * ( + (2.0f * p1) + + (-p0 + p2) * t + + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 + + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3 + ); - float orientation = std::atan2(dir.y, dir.x) - 1.57079632679f; - playerEntity->setPosition(nextPos.x, nextPos.y, nextPos.z, orientation); + // Calculate smooth direction for orientation (tangent to spline) + glm::vec3 tangent = 0.5f * ( + (-p0 + p2) + + 2.0f * (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t + + 3.0f * (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t2 + ); + + // Smooth orientation based on spline tangent + float targetOrientation = std::atan2(tangent.y, tangent.x) - 1.57079632679f; + + // Smooth rotation transition (lerp towards target) + float currentOrientation = movementInfo.orientation; + float orientDiff = targetOrientation - currentOrientation; + // Normalize angle difference to [-PI, PI] + while (orientDiff > 3.14159265f) orientDiff -= 6.28318530f; + while (orientDiff < -3.14159265f) orientDiff += 6.28318530f; + float smoothOrientation = currentOrientation + orientDiff * std::min(1.0f, deltaTime * 3.0f); + + playerEntity->setPosition(nextPos.x, nextPos.y, nextPos.z, smoothOrientation); movementInfo.x = nextPos.x; movementInfo.y = nextPos.y; movementInfo.z = nextPos.z; - movementInfo.orientation = orientation; + movementInfo.orientation = smoothOrientation; + + // Update mount rotation to face flight direction + if (taxiOrientationCallback_) { + taxiOrientationCallback_(smoothOrientation); + } } void GameHandler::handleActivateTaxiReply(network::Packet& packet) { diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 3a99891c..6def7242 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -224,7 +224,8 @@ void CameraController::update(float deltaTime) { // Get camera axes — project forward onto XY plane for walking glm::vec3 forward3D = camera->getForward(); bool cameraDrivesFacing = rightMouseDown || mouseAutorun; - if (cameraDrivesFacing) { + // During taxi flights, orientation is controlled by the flight path, not player input + if (cameraDrivesFacing && !externalFollow_) { facingYaw = yaw; } float moveYaw = cameraDrivesFacing ? yaw : facingYaw; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 3c0b6412..a2b61d35 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1644,7 +1644,8 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: lastDrawCallCount = 0; // Adaptive render distance: keep longer tree/foliage visibility to reduce pop-in. - const float maxRenderDistance = (instances.size() > 600) ? 320.0f : 2800.0f; + // During taxi, use very short render distance to prevent loading hitches + const float maxRenderDistance = onTaxi_ ? 150.0f : (instances.size() > 600) ? 320.0f : 2800.0f; const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; const float fadeStartFraction = 0.75f; const glm::vec3 camPos = camera.getPosition(); @@ -1713,10 +1714,20 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: const M2ModelGPU& model = *currentModel; - // Skip small models when on taxi (performance optimization) - // Small props/foliage aren't visible from flight altitude anyway - if (onTaxi_ && model.boundRadius < 3.0f) { - continue; + // Aggressive culling during taxi for smooth flight + if (onTaxi_) { + // Skip all small/medium models (props, foliage, decorations) + if (model.boundRadius < 15.0f) { + continue; + } + // Skip all foliage and trees (even large ones cause hitching during load) + if (model.collisionNoBlock || model.collisionTreeTrunk) { + continue; + } + // Skip underwater objects (water is opaque from altitude) + if (instance.position.z < -5.0f) { + continue; + } } // Distance-based fade alpha for smooth pop-in (squared-distance, no sqrt) diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index dd9f93e4..d569680c 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -1119,7 +1119,11 @@ void Renderer::update(float deltaTime) { } // Movement-facing comes from camera controller and is decoupled from LMB orbit. - if (cameraController->isMoving() || cameraController->isRightMouseHeld()) { + // During taxi flights, orientation is controlled by the flight path (not player input) + if (taxiFlight_) { + // Taxi flight: use orientation from flight path + characterYaw = cameraController->getFacingYaw(); + } else if (cameraController->isMoving() || cameraController->isRightMouseHeld()) { characterYaw = cameraController->getFacingYaw(); } else if (inCombat_ && targetPosition && !emoteActive && !isMounted()) { // Face target when in combat and idle