mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-24 08:00:14 +00:00
Improve performance and tune ramp/planter collision behavior
This commit is contained in:
parent
f43e6bf834
commit
f00d13bfc0
12 changed files with 310 additions and 164 deletions
|
|
@ -5,6 +5,7 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
#include <sys/types.h>
|
||||
#include <chrono>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline { class AssetManager; }
|
||||
|
|
@ -57,6 +58,7 @@ private:
|
|||
|
||||
std::string tempFilePath = "/tmp/wowee_footstep.wav";
|
||||
pid_t playerPid = -1;
|
||||
std::chrono::steady_clock::time_point lastPlayTime = std::chrono::steady_clock::time_point{};
|
||||
|
||||
std::mt19937 rng;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ private:
|
|||
static constexpr float PIVOT_HEIGHT = 1.8f; // Pivot at head height
|
||||
static constexpr float CAM_SPHERE_RADIUS = 0.32f; // Keep camera farther from geometry to avoid clipping-through surfaces
|
||||
static constexpr float CAM_EPSILON = 0.22f; // Extra wall offset to avoid near-plane clipping artifacts
|
||||
static constexpr float COLLISION_FOCUS_RADIUS_THIRD_PERSON = 90.0f;
|
||||
static constexpr float COLLISION_FOCUS_RADIUS_FREE_FLY = 70.0f;
|
||||
static constexpr float COLLISION_FOCUS_RADIUS_THIRD_PERSON = 42.0f;
|
||||
static constexpr float COLLISION_FOCUS_RADIUS_FREE_FLY = 34.0f;
|
||||
static constexpr float MIN_PITCH = -88.0f; // Look almost straight down
|
||||
static constexpr float MAX_PITCH = 35.0f; // Limited upward look
|
||||
glm::vec3* followTarget = nullptr;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ struct M2ModelGPU {
|
|||
float boundRadius = 0.0f;
|
||||
bool collisionSteppedFountain = false;
|
||||
bool collisionSteppedLowPlatform = false;
|
||||
bool collisionPlanter = false;
|
||||
bool collisionNoBlock = false;
|
||||
|
||||
std::string name;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <GL/glew.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -48,6 +49,11 @@ private:
|
|||
int mapSize = 200;
|
||||
float viewRadius = 500.0f;
|
||||
bool enabled = false;
|
||||
float updateIntervalSec = 0.25f;
|
||||
float updateDistance = 6.0f;
|
||||
std::chrono::steady_clock::time_point lastUpdateTime = std::chrono::steady_clock::time_point{};
|
||||
glm::vec3 lastUpdatePos = glm::vec3(0.0f);
|
||||
bool hasCachedFrame = false;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
#include <condition_variable>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
|
|
@ -246,8 +247,8 @@ private:
|
|||
|
||||
// Streaming parameters
|
||||
bool streamingEnabled = true;
|
||||
int loadRadius = 2; // Load tiles within this radius (5x5 grid for performance)
|
||||
int unloadRadius = 3; // Unload tiles beyond this radius
|
||||
int loadRadius = 1; // Load tiles within this radius (3x3 grid for better CPU/GPU perf)
|
||||
int unloadRadius = 2; // Unload tiles beyond this radius
|
||||
float updateInterval = 0.1f; // Check streaming every 0.1 seconds
|
||||
float timeSinceLastUpdate = 0.0f;
|
||||
|
||||
|
|
@ -257,8 +258,9 @@ private:
|
|||
static constexpr float TILE_SIZE = 533.33333f; // One tile = 533.33 units
|
||||
static constexpr float CHUNK_SIZE = 33.33333f; // One chunk = 33.33 units
|
||||
|
||||
// Background loading thread
|
||||
std::thread workerThread;
|
||||
// Background loading worker pool
|
||||
std::vector<std::thread> workerThreads;
|
||||
int workerCount = 0;
|
||||
std::mutex queueMutex;
|
||||
std::condition_variable queueCV;
|
||||
std::queue<TileCoord> loadQueue;
|
||||
|
|
|
|||
|
|
@ -135,6 +135,15 @@ void FootstepManager::reapFinishedProcess() {
|
|||
}
|
||||
|
||||
bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (lastPlayTime.time_since_epoch().count() != 0) {
|
||||
float elapsed = std::chrono::duration<float>(now - lastPlayTime).count();
|
||||
float minInterval = sprinting ? 0.09f : 0.14f;
|
||||
if (elapsed < minInterval) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto& list = surfaces[static_cast<size_t>(surface)].clips;
|
||||
if (list.empty()) {
|
||||
list = surfaces[static_cast<size_t>(FootstepSurface::STONE)].clips;
|
||||
|
|
@ -181,6 +190,7 @@ bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
|||
_exit(1);
|
||||
} else if (pid > 0) {
|
||||
playerPid = pid;
|
||||
lastPlayTime = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -523,7 +523,10 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
model.attachmentLookup = readArray<uint16_t>(m2Data, header.ofsAttachmentLookup, header.nAttachmentLookup);
|
||||
}
|
||||
|
||||
core::Logger::getInstance().debug("M2 model loaded: ", model.name);
|
||||
static int m2LoadLogBudget = 200;
|
||||
if (m2LoadLogBudget-- > 0) {
|
||||
core::Logger::getInstance().debug("M2 model loaded: ", model.name);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -235,9 +235,9 @@ void CameraController::update(float deltaTime) {
|
|||
glm::vec3 desiredPos = targetPos;
|
||||
float moveDist = glm::length(desiredPos - startPos);
|
||||
// Adaptive CCD: keep per-step movement short, especially on low FPS spikes.
|
||||
int sweepSteps = std::max(1, std::min(24, static_cast<int>(std::ceil(moveDist / 0.18f))));
|
||||
int sweepSteps = std::max(1, std::min(14, static_cast<int>(std::ceil(moveDist / 0.24f))));
|
||||
if (deltaTime > 0.04f) {
|
||||
sweepSteps = std::min(28, std::max(sweepSteps, static_cast<int>(std::ceil(deltaTime / 0.016f)) * 3));
|
||||
sweepSteps = std::min(16, std::max(sweepSteps, static_cast<int>(std::ceil(deltaTime / 0.016f)) * 2));
|
||||
}
|
||||
glm::vec3 stepPos = startPos;
|
||||
glm::vec3 stepDelta = (desiredPos - startPos) / static_cast<float>(sweepSteps);
|
||||
|
|
@ -270,42 +270,44 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
// WoW-style slope limiting (50 degrees, with sliding)
|
||||
// dot(normal, up) >= 0.64 is walkable, otherwise slide
|
||||
constexpr bool ENABLE_SLOPE_SLIDE = false;
|
||||
constexpr float MAX_WALK_SLOPE_DOT = 0.6428f; // cos(50°)
|
||||
constexpr float SAMPLE_DIST = 0.3f; // Distance to sample for normal calculation
|
||||
{
|
||||
if (ENABLE_SLOPE_SLIDE) {
|
||||
glm::vec3 oldPos = *followTarget;
|
||||
float moveXY = glm::length(glm::vec2(targetPos.x - oldPos.x, targetPos.y - oldPos.y));
|
||||
if (moveXY >= 0.03f) {
|
||||
struct GroundSample {
|
||||
std::optional<float> height;
|
||||
bool fromM2 = false;
|
||||
};
|
||||
// Helper to get ground height at a position and whether M2 provided the top floor.
|
||||
auto getGroundAt = [&](float x, float y) -> GroundSample {
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
std::optional<float> m2H;
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(x, y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(x, y, targetPos.z + 5.0f);
|
||||
}
|
||||
if (m2Renderer) {
|
||||
m2H = m2Renderer->getFloorHeight(x, y, targetPos.z);
|
||||
}
|
||||
auto base = selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f);
|
||||
bool fromM2 = false;
|
||||
if (m2H && *m2H <= targetPos.z + 1.0f && (!base || *m2H > *base)) {
|
||||
base = m2H;
|
||||
fromM2 = true;
|
||||
}
|
||||
return GroundSample{base, fromM2};
|
||||
};
|
||||
|
||||
struct GroundSample {
|
||||
std::optional<float> height;
|
||||
bool fromM2 = false;
|
||||
};
|
||||
// Helper to get ground height at a position and whether M2 provided the top floor.
|
||||
auto getGroundAt = [&](float x, float y) -> GroundSample {
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
std::optional<float> m2H;
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(x, y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(x, y, targetPos.z + 5.0f);
|
||||
}
|
||||
if (m2Renderer) {
|
||||
m2H = m2Renderer->getFloorHeight(x, y, targetPos.z);
|
||||
}
|
||||
auto base = selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f);
|
||||
bool fromM2 = false;
|
||||
if (m2H && *m2H <= targetPos.z + 1.0f && (!base || *m2H > *base)) {
|
||||
base = m2H;
|
||||
fromM2 = true;
|
||||
}
|
||||
return GroundSample{base, fromM2};
|
||||
};
|
||||
|
||||
// Get ground height at target position
|
||||
auto center = getGroundAt(targetPos.x, targetPos.y);
|
||||
bool skipSlopeCheck = center.height && center.fromM2;
|
||||
if (center.height && !skipSlopeCheck) {
|
||||
// Get ground height at target position
|
||||
auto center = getGroundAt(targetPos.x, targetPos.y);
|
||||
bool skipSlopeCheck = center.height && center.fromM2;
|
||||
if (center.height && !skipSlopeCheck) {
|
||||
|
||||
// Calculate ground normal using height samples
|
||||
auto hPosX = getGroundAt(targetPos.x + SAMPLE_DIST, targetPos.y);
|
||||
|
|
@ -361,6 +363,7 @@ void CameraController::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -390,7 +393,7 @@ void CameraController::update(float deltaTime) {
|
|||
std::optional<float> groundH;
|
||||
constexpr float FOOTPRINT = 0.28f;
|
||||
const glm::vec2 offsets[] = {
|
||||
{0.0f, 0.0f}, {FOOTPRINT, 0.0f}, {-FOOTPRINT, 0.0f}, {0.0f, FOOTPRINT}, {0.0f, -FOOTPRINT}
|
||||
{0.0f, 0.0f}, {FOOTPRINT, 0.0f}, {0.0f, FOOTPRINT}
|
||||
};
|
||||
for (const auto& o : offsets) {
|
||||
auto h = sampleGround(targetPos.x + o.x, targetPos.y + o.y);
|
||||
|
|
@ -603,9 +606,9 @@ void CameraController::update(float deltaTime) {
|
|||
glm::vec3 startFeet = camera->getPosition() - glm::vec3(0, 0, eyeHeight);
|
||||
glm::vec3 desiredFeet = newPos - glm::vec3(0, 0, eyeHeight);
|
||||
float moveDist = glm::length(desiredFeet - startFeet);
|
||||
int sweepSteps = std::max(1, std::min(24, static_cast<int>(std::ceil(moveDist / 0.18f))));
|
||||
int sweepSteps = std::max(1, std::min(14, static_cast<int>(std::ceil(moveDist / 0.24f))));
|
||||
if (deltaTime > 0.04f) {
|
||||
sweepSteps = std::min(28, std::max(sweepSteps, static_cast<int>(std::ceil(deltaTime / 0.016f)) * 3));
|
||||
sweepSteps = std::min(16, std::max(sweepSteps, static_cast<int>(std::ceil(deltaTime / 0.016f)) * 2));
|
||||
}
|
||||
glm::vec3 stepPos = startFeet;
|
||||
glm::vec3 stepDelta = (desiredFeet - startFeet) / static_cast<float>(sweepSteps);
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ float getEffectiveCollisionTopLocal(const M2ModelGPU& model,
|
|||
// use edge distance (not radial) so corner blocks don't become too low and
|
||||
// clip-through at diagonals.
|
||||
float edge = std::max(std::abs(nx), std::abs(ny));
|
||||
if (edge > 0.92f) return localMin.z + h * 0.10f;
|
||||
if (edge > 0.72f) return localMin.z + h * 0.46f;
|
||||
return localMin.z + h * 0.74f;
|
||||
if (edge > 0.92f) return localMin.z + h * 0.06f;
|
||||
if (edge > 0.72f) return localMin.z + h * 0.30f;
|
||||
return localMin.z + h * 0.62f;
|
||||
}
|
||||
|
||||
bool segmentIntersectsAABB(const glm::vec3& from, const glm::vec3& to,
|
||||
|
|
@ -351,6 +351,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
(likelyCurbName && (lowPlatformShape || lowWideShape)));
|
||||
|
||||
bool isPlanter = (lowerName.find("planter") != std::string::npos);
|
||||
gpuModel.collisionPlanter = isPlanter;
|
||||
bool foliageName =
|
||||
(lowerName.find("bush") != std::string::npos) ||
|
||||
(lowerName.find("grass") != std::string::npos) ||
|
||||
|
|
@ -371,7 +372,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
bool softTree = treeLike && !hardTreePart && (canopyLike || vert > horiz * 1.35f);
|
||||
bool smallSoftShape = (horiz < 2.2f && vert < 2.4f);
|
||||
bool mediumFoliageShape = (horiz < 4.5f && vert < 4.5f);
|
||||
bool forceSolidCurb = gpuModel.collisionSteppedLowPlatform || knownStormwindPlanter || likelyCurbName;
|
||||
bool forceSolidCurb = gpuModel.collisionSteppedLowPlatform || knownStormwindPlanter || likelyCurbName || gpuModel.collisionPlanter;
|
||||
gpuModel.collisionNoBlock = ((((foliageName && smallSoftShape) || (foliageName && mediumFoliageShape)) || softTree) &&
|
||||
!forceSolidCurb);
|
||||
}
|
||||
|
|
@ -593,7 +594,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
lastDrawCallCount = 0;
|
||||
|
||||
// Distance-based culling threshold for M2 models
|
||||
const float maxRenderDistance = 400.0f; // Balance between performance and visibility
|
||||
const float maxRenderDistance = 180.0f; // Aggressive culling for city performance
|
||||
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
|
||||
const glm::vec3 camPos = camera.getPosition();
|
||||
|
||||
|
|
@ -609,7 +610,12 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
float distSq = glm::dot(toCam, toCam);
|
||||
float worldRadius = model.boundRadius * instance.scale;
|
||||
// Cull small objects (radius < 20) at distance, keep larger objects visible longer
|
||||
float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, worldRadius / 10.0f);
|
||||
float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, worldRadius / 12.0f);
|
||||
if (worldRadius < 0.8f) {
|
||||
effectiveMaxDistSq = std::min(effectiveMaxDistSq, 65.0f * 65.0f);
|
||||
} else if (worldRadius < 1.5f) {
|
||||
effectiveMaxDistSq = std::min(effectiveMaxDistSq, 95.0f * 95.0f);
|
||||
}
|
||||
if (distSq > effectiveMaxDistSq) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -886,7 +892,7 @@ std::optional<float> M2Renderer::getFloorHeight(float glX, float glY, float glZ)
|
|||
glm::vec3 worldTop = glm::vec3(instance.modelMatrix * glm::vec4(localTop, 1.0f));
|
||||
|
||||
// Reachability filter: allow a bit more climb for stepped low platforms.
|
||||
float maxStepUp = model.collisionSteppedLowPlatform ? 1.8f : 1.0f;
|
||||
float maxStepUp = model.collisionSteppedLowPlatform ? (model.collisionPlanter ? 2.2f : 1.8f) : 1.0f;
|
||||
if (worldTop.z > glZ + maxStepUp) continue;
|
||||
|
||||
if (!bestFloor || worldTop.z > *bestFloor) {
|
||||
|
|
@ -948,12 +954,12 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
|
||||
// Swept hard clamp for taller blockers only.
|
||||
// Low/stepable objects should be climbable and not "shove" the player off.
|
||||
float maxStepUp = model.collisionSteppedLowPlatform ? 2.0f : 1.20f;
|
||||
float maxStepUp = model.collisionSteppedLowPlatform ? (model.collisionPlanter ? 2.8f : 2.4f) : 1.20f;
|
||||
bool stepableLowObject = (effectiveTop <= localFrom.z + maxStepUp);
|
||||
bool climbingAttempt = (localPos.z > localFrom.z + 0.18f);
|
||||
bool nearTop = (localFrom.z >= effectiveTop - 0.30f);
|
||||
bool climbingTowardTop = climbingAttempt && (localFrom.z + 0.35f >= effectiveTop);
|
||||
bool forceHardLateral = model.collisionSteppedLowPlatform && !nearTop && !climbingTowardTop;
|
||||
bool climbingTowardTop = climbingAttempt && (localFrom.z + (model.collisionPlanter ? 0.95f : 0.60f) >= effectiveTop);
|
||||
bool forceHardLateral = model.collisionSteppedLowPlatform && !model.collisionPlanter && !nearTop && !climbingTowardTop;
|
||||
if (!stepableLowObject || forceHardLateral) {
|
||||
float tEnter = 0.0f;
|
||||
glm::vec3 sweepMax = localMax;
|
||||
|
|
@ -988,7 +994,11 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
// Gentle fallback push for overlapping cases.
|
||||
float pushAmount;
|
||||
if (model.collisionSteppedLowPlatform) {
|
||||
pushAmount = std::clamp(minPush * 0.18f, 0.006f, 0.020f);
|
||||
if (model.collisionPlanter && stepableLowObject) {
|
||||
pushAmount = std::clamp(minPush * 0.06f, 0.001f, 0.006f);
|
||||
} else {
|
||||
pushAmount = std::clamp(minPush * 0.12f, 0.003f, 0.012f);
|
||||
}
|
||||
} else if (stepableLowObject) {
|
||||
pushAmount = std::clamp(minPush * 0.12f, 0.002f, 0.015f);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -140,8 +140,22 @@ void Minimap::shutdown() {
|
|||
void Minimap::render(const Camera& playerCamera, int screenWidth, int screenHeight) {
|
||||
if (!enabled || !terrainRenderer || !fbo) return;
|
||||
|
||||
// 1. Render terrain from top-down into FBO
|
||||
renderTerrainToFBO(playerCamera);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
glm::vec3 playerPos = playerCamera.getPosition();
|
||||
bool needsRefresh = !hasCachedFrame;
|
||||
if (!needsRefresh) {
|
||||
float moved = glm::length(glm::vec2(playerPos.x - lastUpdatePos.x, playerPos.y - lastUpdatePos.y));
|
||||
float elapsed = std::chrono::duration<float>(now - lastUpdateTime).count();
|
||||
needsRefresh = (moved >= updateDistance) || (elapsed >= updateIntervalSec);
|
||||
}
|
||||
|
||||
// 1. Render terrain from top-down into FBO (throttled)
|
||||
if (needsRefresh) {
|
||||
renderTerrainToFBO(playerCamera);
|
||||
lastUpdateTime = now;
|
||||
lastUpdatePos = playerPos;
|
||||
hasCachedFrame = true;
|
||||
}
|
||||
|
||||
// 2. Draw the minimap quad on screen
|
||||
renderQuad(screenWidth, screenHeight);
|
||||
|
|
|
|||
|
|
@ -89,9 +89,12 @@ TerrainManager::~TerrainManager() {
|
|||
if (workerRunning.load()) {
|
||||
workerRunning.store(false);
|
||||
queueCV.notify_all();
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
for (auto& t : workerThreads) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
workerThreads.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,14 +112,20 @@ bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer*
|
|||
return false;
|
||||
}
|
||||
|
||||
// Start background worker thread
|
||||
// Start background worker pool
|
||||
workerRunning.store(true);
|
||||
workerThread = std::thread(&TerrainManager::workerLoop, this);
|
||||
unsigned hc = std::thread::hardware_concurrency();
|
||||
workerCount = static_cast<int>(hc > 0 ? std::min(4u, std::max(2u, hc - 1)) : 2u);
|
||||
workerThreads.reserve(workerCount);
|
||||
for (int i = 0; i < workerCount; i++) {
|
||||
workerThreads.emplace_back(&TerrainManager::workerLoop, this);
|
||||
}
|
||||
|
||||
LOG_INFO("Terrain manager initialized (async loading enabled)");
|
||||
LOG_INFO(" Map: ", mapName);
|
||||
LOG_INFO(" Load radius: ", loadRadius, " tiles");
|
||||
LOG_INFO(" Unload radius: ", unloadRadius, " tiles");
|
||||
LOG_INFO(" Workers: ", workerCount);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -152,7 +161,7 @@ void TerrainManager::update(const Camera& camera, float deltaTime) {
|
|||
|
||||
// Stream tiles if we've moved significantly or initial load
|
||||
if (newTile.x != lastStreamTile.x || newTile.y != lastStreamTile.y) {
|
||||
LOG_INFO("Streaming: cam=(", camPos.x, ",", camPos.y, ",", camPos.z,
|
||||
LOG_DEBUG("Streaming: cam=(", camPos.x, ",", camPos.y, ",", camPos.z,
|
||||
") tile=[", newTile.x, ",", newTile.y,
|
||||
"] loaded=", loadedTiles.size());
|
||||
streamTiles();
|
||||
|
|
@ -189,7 +198,7 @@ bool TerrainManager::loadTile(int x, int y) {
|
|||
std::unique_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
||||
TileCoord coord = {x, y};
|
||||
|
||||
LOG_INFO("Preparing tile [", x, ",", y, "] (CPU work)");
|
||||
LOG_DEBUG("Preparing tile [", x, ",", y, "] (CPU work)");
|
||||
|
||||
// Load ADT file
|
||||
std::string adtPath = getADTPath(coord);
|
||||
|
|
@ -445,7 +454,7 @@ std::unique_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Prepared tile [", x, ",", y, "]: ",
|
||||
LOG_DEBUG("Prepared tile [", x, ",", y, "]: ",
|
||||
pending->m2Models.size(), " M2 models, ",
|
||||
pending->m2Placements.size(), " M2 placements, ",
|
||||
pending->wmoModels.size(), " WMOs, ",
|
||||
|
|
@ -459,7 +468,7 @@ void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
|
|||
int y = pending->coord.y;
|
||||
TileCoord coord = pending->coord;
|
||||
|
||||
LOG_INFO("Finalizing tile [", x, ",", y, "] (GPU upload)");
|
||||
LOG_DEBUG("Finalizing tile [", x, ",", y, "] (GPU upload)");
|
||||
|
||||
// Check if tile was already loaded (race condition guard) or failed
|
||||
if (loadedTiles.find(coord) != loadedTiles.end()) {
|
||||
|
|
@ -517,7 +526,7 @@ void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
|
|||
}
|
||||
}
|
||||
|
||||
LOG_INFO(" Loaded doodads for tile [", x, ",", y, "]: ",
|
||||
LOG_DEBUG(" Loaded doodads for tile [", x, ",", y, "]: ",
|
||||
loadedDoodads, " instances (", uploadedModelIds.size(), " new models, ",
|
||||
skippedDedup, " dedup skipped)");
|
||||
}
|
||||
|
|
@ -558,7 +567,7 @@ void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
|
|||
}
|
||||
}
|
||||
if (loadedLiquids > 0) {
|
||||
LOG_INFO(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids);
|
||||
LOG_DEBUG(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids);
|
||||
}
|
||||
|
||||
// Upload WMO doodad M2 models
|
||||
|
|
@ -572,7 +581,7 @@ void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
|
|||
}
|
||||
|
||||
if (loadedWMOs > 0) {
|
||||
LOG_INFO(" Loaded WMOs for tile [", x, ",", y, "]: ", loadedWMOs);
|
||||
LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ", loadedWMOs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -591,7 +600,7 @@ void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
|
|||
|
||||
loadedTiles[coord] = std::move(tile);
|
||||
|
||||
LOG_INFO(" Finalized tile [", x, ",", y, "]");
|
||||
LOG_DEBUG(" Finalized tile [", x, ",", y, "]");
|
||||
}
|
||||
|
||||
void TerrainManager::workerLoop() {
|
||||
|
|
@ -728,9 +737,12 @@ void TerrainManager::unloadAll() {
|
|||
if (workerRunning.load()) {
|
||||
workerRunning.store(false);
|
||||
queueCV.notify_all();
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
for (auto& t : workerThreads) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
workerThreads.clear();
|
||||
}
|
||||
|
||||
// Clear queues
|
||||
|
|
@ -809,59 +821,88 @@ std::optional<float> TerrainManager::getHeightAt(float glX, float glY) const {
|
|||
|
||||
const float unitSize = CHUNK_SIZE / 8.0f;
|
||||
|
||||
// Query coordinates are the same as what we pass (terrain uses identity model matrix)
|
||||
float queryX = glX;
|
||||
float queryY = glY;
|
||||
auto sampleTileHeight = [&](const TerrainTile* tile) -> std::optional<float> {
|
||||
if (!tile || !tile->loaded) return std::nullopt;
|
||||
|
||||
for (const auto& [coord, tile] : loadedTiles) {
|
||||
if (!tile || !tile->loaded) continue;
|
||||
auto sampleChunk = [&](int cx, int cy) -> std::optional<float> {
|
||||
if (cx < 0 || cx >= 16 || cy < 0 || cy >= 16) return std::nullopt;
|
||||
const auto& chunk = tile->terrain.getChunk(cx, cy);
|
||||
if (!chunk.hasHeightMap()) return std::nullopt;
|
||||
|
||||
for (int cy = 0; cy < 16; cy++) {
|
||||
for (int cx = 0; cx < 16; cx++) {
|
||||
const auto& chunk = tile->terrain.getChunk(cx, cy);
|
||||
if (!chunk.hasHeightMap()) continue;
|
||||
float chunkMaxX = chunk.position[0];
|
||||
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
|
||||
float chunkMaxY = chunk.position[1];
|
||||
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
|
||||
|
||||
float chunkMaxX = chunk.position[0];
|
||||
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
|
||||
float chunkMaxY = chunk.position[1];
|
||||
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
|
||||
if (glX < chunkMinX || glX > chunkMaxX ||
|
||||
glY < chunkMinY || glY > chunkMaxY) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (queryX < chunkMinX || queryX > chunkMaxX ||
|
||||
queryY < chunkMinY || queryY > chunkMaxY) {
|
||||
continue;
|
||||
}
|
||||
// Fractional position within chunk (0-8 range)
|
||||
float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY
|
||||
float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX
|
||||
|
||||
// Fractional position within chunk (0-8 range)
|
||||
float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY
|
||||
float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX
|
||||
fracX = glm::clamp(fracX, 0.0f, 8.0f);
|
||||
fracY = glm::clamp(fracY, 0.0f, 8.0f);
|
||||
|
||||
fracX = glm::clamp(fracX, 0.0f, 8.0f);
|
||||
fracY = glm::clamp(fracY, 0.0f, 8.0f);
|
||||
// Bilinear interpolation on 9x9 outer grid
|
||||
int gx0 = static_cast<int>(std::floor(fracX));
|
||||
int gy0 = static_cast<int>(std::floor(fracY));
|
||||
int gx1 = std::min(gx0 + 1, 8);
|
||||
int gy1 = std::min(gy0 + 1, 8);
|
||||
|
||||
// Bilinear interpolation on 9x9 outer grid
|
||||
int gx0 = static_cast<int>(std::floor(fracX));
|
||||
int gy0 = static_cast<int>(std::floor(fracY));
|
||||
int gx1 = std::min(gx0 + 1, 8);
|
||||
int gy1 = std::min(gy0 + 1, 8);
|
||||
float tx = fracX - gx0;
|
||||
float ty = fracY - gy0;
|
||||
|
||||
float tx = fracX - gx0;
|
||||
float ty = fracY - gy0;
|
||||
float h00 = chunk.heightMap.heights[gy0 * 17 + gx0];
|
||||
float h10 = chunk.heightMap.heights[gy0 * 17 + gx1];
|
||||
float h01 = chunk.heightMap.heights[gy1 * 17 + gx0];
|
||||
float h11 = chunk.heightMap.heights[gy1 * 17 + gx1];
|
||||
|
||||
// Outer vertex heights from the 9x17 layout
|
||||
// Outer vertex (gx, gy) is at index: gy * 17 + gx
|
||||
float h00 = chunk.heightMap.heights[gy0 * 17 + gx0];
|
||||
float h10 = chunk.heightMap.heights[gy0 * 17 + gx1];
|
||||
float h01 = chunk.heightMap.heights[gy1 * 17 + gx0];
|
||||
float h11 = chunk.heightMap.heights[gy1 * 17 + gx1];
|
||||
float h = h00 * (1 - tx) * (1 - ty) +
|
||||
h10 * tx * (1 - ty) +
|
||||
h01 * (1 - tx) * ty +
|
||||
h11 * tx * ty;
|
||||
|
||||
float h = h00 * (1 - tx) * (1 - ty) +
|
||||
h10 * tx * (1 - ty) +
|
||||
h01 * (1 - tx) * ty +
|
||||
h11 * tx * ty;
|
||||
return chunk.position[2] + h;
|
||||
};
|
||||
|
||||
return chunk.position[2] + h;
|
||||
// Fast path: infer likely chunk index and probe 3x3 neighborhood.
|
||||
int guessCy = glm::clamp(static_cast<int>(std::floor((tile->maxX - glX) / CHUNK_SIZE)), 0, 15);
|
||||
int guessCx = glm::clamp(static_cast<int>(std::floor((tile->maxY - glY) / CHUNK_SIZE)), 0, 15);
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
auto h = sampleChunk(guessCx + dx, guessCy + dy);
|
||||
if (h) return h;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback full scan for robustness at seams/unusual coords.
|
||||
for (int cy = 0; cy < 16; cy++) {
|
||||
for (int cx = 0; cx < 16; cx++) {
|
||||
auto h = sampleChunk(cx, cy);
|
||||
if (h) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
// Fast path: sample the expected containing tile first.
|
||||
TileCoord tc = worldToTile(glX, glY);
|
||||
auto it = loadedTiles.find(tc);
|
||||
if (it != loadedTiles.end()) {
|
||||
auto h = sampleTileHeight(it->second.get());
|
||||
if (h) return h;
|
||||
}
|
||||
|
||||
// Fallback: check all loaded tiles (handles seam/edge coordinate ambiguity).
|
||||
for (const auto& [coord, tile] : loadedTiles) {
|
||||
if (coord == tc) continue;
|
||||
auto h = sampleTileHeight(tile.get());
|
||||
if (h) return h;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
|
|
@ -870,61 +911,92 @@ std::optional<float> TerrainManager::getHeightAt(float glX, float glY) const {
|
|||
std::optional<std::string> TerrainManager::getDominantTextureAt(float glX, float glY) const {
|
||||
const float unitSize = CHUNK_SIZE / 8.0f;
|
||||
std::vector<uint8_t> alphaScratch;
|
||||
auto sampleTileTexture = [&](const TerrainTile* tile) -> std::optional<std::string> {
|
||||
if (!tile || !tile->loaded) return std::nullopt;
|
||||
|
||||
for (const auto& [coord, tile] : loadedTiles) {
|
||||
(void)coord;
|
||||
if (!tile || !tile->loaded) continue;
|
||||
auto sampleChunkTexture = [&](int cx, int cy) -> std::optional<std::string> {
|
||||
if (cx < 0 || cx >= 16 || cy < 0 || cy >= 16) return std::nullopt;
|
||||
const auto& chunk = tile->terrain.getChunk(cx, cy);
|
||||
if (!chunk.hasHeightMap() || chunk.layers.empty()) return std::nullopt;
|
||||
|
||||
float chunkMaxX = chunk.position[0];
|
||||
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
|
||||
float chunkMaxY = chunk.position[1];
|
||||
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
|
||||
if (glX < chunkMinX || glX > chunkMaxX || glY < chunkMinY || glY > chunkMaxY) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
float fracY = (chunk.position[0] - glX) / unitSize;
|
||||
float fracX = (chunk.position[1] - glY) / unitSize;
|
||||
fracX = glm::clamp(fracX, 0.0f, 8.0f);
|
||||
fracY = glm::clamp(fracY, 0.0f, 8.0f);
|
||||
|
||||
int alphaX = glm::clamp(static_cast<int>((fracX / 8.0f) * 63.0f), 0, 63);
|
||||
int alphaY = glm::clamp(static_cast<int>((fracY / 8.0f) * 63.0f), 0, 63);
|
||||
int alphaIndex = alphaY * 64 + alphaX;
|
||||
|
||||
std::vector<int> weights(chunk.layers.size(), 0);
|
||||
int accum = 0;
|
||||
for (size_t layerIdx = 1; layerIdx < chunk.layers.size(); layerIdx++) {
|
||||
int alpha = 0;
|
||||
if (decodeLayerAlpha(chunk, layerIdx, alphaScratch) && alphaIndex < static_cast<int>(alphaScratch.size())) {
|
||||
alpha = alphaScratch[alphaIndex];
|
||||
}
|
||||
weights[layerIdx] = alpha;
|
||||
accum += alpha;
|
||||
}
|
||||
weights[0] = glm::clamp(255 - accum, 0, 255);
|
||||
|
||||
size_t bestLayer = 0;
|
||||
int bestWeight = weights[0];
|
||||
for (size_t i = 1; i < weights.size(); i++) {
|
||||
if (weights[i] > bestWeight) {
|
||||
bestWeight = weights[i];
|
||||
bestLayer = i;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t texId = chunk.layers[bestLayer].textureId;
|
||||
if (texId < tile->terrain.textures.size()) {
|
||||
return tile->terrain.textures[texId];
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
int guessCy = glm::clamp(static_cast<int>(std::floor((tile->maxX - glX) / CHUNK_SIZE)), 0, 15);
|
||||
int guessCx = glm::clamp(static_cast<int>(std::floor((tile->maxY - glY) / CHUNK_SIZE)), 0, 15);
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
auto tex = sampleChunkTexture(guessCx + dx, guessCy + dy);
|
||||
if (tex) return tex;
|
||||
}
|
||||
}
|
||||
|
||||
for (int cy = 0; cy < 16; cy++) {
|
||||
for (int cx = 0; cx < 16; cx++) {
|
||||
const auto& chunk = tile->terrain.getChunk(cx, cy);
|
||||
if (!chunk.hasHeightMap() || chunk.layers.empty()) continue;
|
||||
|
||||
float chunkMaxX = chunk.position[0];
|
||||
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
|
||||
float chunkMaxY = chunk.position[1];
|
||||
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
|
||||
if (glX < chunkMinX || glX > chunkMaxX || glY < chunkMinY || glY > chunkMaxY) {
|
||||
continue;
|
||||
auto tex = sampleChunkTexture(cx, cy);
|
||||
if (tex) {
|
||||
return tex;
|
||||
}
|
||||
|
||||
float fracY = (chunk.position[0] - glX) / unitSize;
|
||||
float fracX = (chunk.position[1] - glY) / unitSize;
|
||||
fracX = glm::clamp(fracX, 0.0f, 8.0f);
|
||||
fracY = glm::clamp(fracY, 0.0f, 8.0f);
|
||||
|
||||
int alphaX = glm::clamp(static_cast<int>((fracX / 8.0f) * 63.0f), 0, 63);
|
||||
int alphaY = glm::clamp(static_cast<int>((fracY / 8.0f) * 63.0f), 0, 63);
|
||||
int alphaIndex = alphaY * 64 + alphaX;
|
||||
|
||||
std::vector<int> weights(chunk.layers.size(), 0);
|
||||
int accum = 0;
|
||||
for (size_t layerIdx = 1; layerIdx < chunk.layers.size(); layerIdx++) {
|
||||
int alpha = 0;
|
||||
if (decodeLayerAlpha(chunk, layerIdx, alphaScratch) && alphaIndex < static_cast<int>(alphaScratch.size())) {
|
||||
alpha = alphaScratch[alphaIndex];
|
||||
}
|
||||
weights[layerIdx] = alpha;
|
||||
accum += alpha;
|
||||
}
|
||||
weights[0] = glm::clamp(255 - accum, 0, 255);
|
||||
|
||||
size_t bestLayer = 0;
|
||||
int bestWeight = weights[0];
|
||||
for (size_t i = 1; i < weights.size(); i++) {
|
||||
if (weights[i] > bestWeight) {
|
||||
bestWeight = weights[i];
|
||||
bestLayer = i;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t texId = chunk.layers[bestLayer].textureId;
|
||||
if (texId < tile->terrain.textures.size()) {
|
||||
return tile->terrain.textures[texId];
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
// Fast path: check expected containing tile first.
|
||||
TileCoord tc = worldToTile(glX, glY);
|
||||
auto it = loadedTiles.find(tc);
|
||||
if (it != loadedTiles.end()) {
|
||||
auto tex = sampleTileTexture(it->second.get());
|
||||
if (tex) return tex;
|
||||
}
|
||||
|
||||
// Fallback: seam/edge case.
|
||||
for (const auto& [coord, tile] : loadedTiles) {
|
||||
if (coord == tc) continue;
|
||||
auto tex = sampleTileTexture(tile.get());
|
||||
if (tex) return tex;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
|
|
@ -958,8 +1030,8 @@ void TerrainManager::streamTiles() {
|
|||
}
|
||||
}
|
||||
|
||||
// Notify worker thread that there's work
|
||||
queueCV.notify_one();
|
||||
// Notify workers that there's work
|
||||
queueCV.notify_all();
|
||||
|
||||
// Unload tiles beyond unload radius (well past the camera far clip)
|
||||
std::vector<TileCoord> tilesToUnload;
|
||||
|
|
|
|||
|
|
@ -460,7 +460,7 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm:
|
|||
|
||||
// Render all instances with instance-level culling
|
||||
const glm::vec3 camPos = camera.getPosition();
|
||||
const float maxRenderDistance = 500.0f; // Reduced for performance
|
||||
const float maxRenderDistance = 320.0f; // More aggressive culling for city performance
|
||||
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
|
||||
|
||||
for (const auto& instance : instances) {
|
||||
|
|
@ -1094,7 +1094,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
if (normalLen < 0.001f) continue;
|
||||
normal /= normalLen;
|
||||
|
||||
// Skip near-horizontal triangles (floors/ceilings), keep sloped tunnel walls.
|
||||
// Skip near-horizontal triangles (floors/ceilings).
|
||||
if (std::abs(normal.z) > 0.85f) continue;
|
||||
|
||||
// Get triangle Z range
|
||||
|
|
@ -1104,7 +1104,30 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
// Only collide with walls in player's vertical range
|
||||
if (triMaxZ < localFeetZ + 0.3f) continue;
|
||||
if (triMinZ > localFeetZ + PLAYER_HEIGHT) continue;
|
||||
if (triMaxZ <= localFeetZ + MAX_STEP_HEIGHT) continue; // Treat as step-up, not hard wall
|
||||
|
||||
// Lower parts of ramps should be stepable from the side.
|
||||
// Allow a larger step-up budget for ramp-like triangles.
|
||||
// Allow running off/onto lower ramp side geometry without invisible wall blocks.
|
||||
if (normal.z > 0.20f && triMaxZ <= localFeetZ + 1.60f) continue;
|
||||
// Ignore short near-vertical side strips around ramps/edges.
|
||||
// These commonly act like invisible side guard rails.
|
||||
float triHeight = triMaxZ - triMinZ;
|
||||
if (std::abs(normal.z) < 0.25f &&
|
||||
triHeight < 1.8f &&
|
||||
triMaxZ <= localFeetZ + 1.4f) {
|
||||
continue;
|
||||
}
|
||||
// Let players run off ramp sides: ignore lower side-wall strips
|
||||
// that sit around foot height and are not true tall building walls.
|
||||
if (std::abs(normal.z) < 0.45f &&
|
||||
triMinZ <= localFeetZ + 0.20f &&
|
||||
triHeight < 4.0f &&
|
||||
triMaxZ <= localFeetZ + 4.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float stepHeightLimit = MAX_STEP_HEIGHT;
|
||||
if (triMaxZ <= localFeetZ + stepHeightLimit) continue; // Treat as step-up, not hard wall
|
||||
|
||||
// Swept test: prevent tunneling when crossing a wall between frames.
|
||||
float fromDist = glm::dot(localFrom - v0, normal);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue