mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +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 <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace pipeline { class AssetManager; }
|
namespace pipeline { class AssetManager; }
|
||||||
|
|
@ -57,6 +58,7 @@ private:
|
||||||
|
|
||||||
std::string tempFilePath = "/tmp/wowee_footstep.wav";
|
std::string tempFilePath = "/tmp/wowee_footstep.wav";
|
||||||
pid_t playerPid = -1;
|
pid_t playerPid = -1;
|
||||||
|
std::chrono::steady_clock::time_point lastPlayTime = std::chrono::steady_clock::time_point{};
|
||||||
|
|
||||||
std::mt19937 rng;
|
std::mt19937 rng;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,8 @@ private:
|
||||||
static constexpr float PIVOT_HEIGHT = 1.8f; // Pivot at head height
|
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_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 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_THIRD_PERSON = 42.0f;
|
||||||
static constexpr float COLLISION_FOCUS_RADIUS_FREE_FLY = 70.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 MIN_PITCH = -88.0f; // Look almost straight down
|
||||||
static constexpr float MAX_PITCH = 35.0f; // Limited upward look
|
static constexpr float MAX_PITCH = 35.0f; // Limited upward look
|
||||||
glm::vec3* followTarget = nullptr;
|
glm::vec3* followTarget = nullptr;
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ struct M2ModelGPU {
|
||||||
float boundRadius = 0.0f;
|
float boundRadius = 0.0f;
|
||||||
bool collisionSteppedFountain = false;
|
bool collisionSteppedFountain = false;
|
||||||
bool collisionSteppedLowPlatform = false;
|
bool collisionSteppedLowPlatform = false;
|
||||||
|
bool collisionPlanter = false;
|
||||||
bool collisionNoBlock = false;
|
bool collisionNoBlock = false;
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <GL/glew.h>
|
#include <GL/glew.h>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
#include <chrono>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
@ -48,6 +49,11 @@ private:
|
||||||
int mapSize = 200;
|
int mapSize = 200;
|
||||||
float viewRadius = 500.0f;
|
float viewRadius = 500.0f;
|
||||||
bool enabled = false;
|
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
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
#include <vector>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
|
@ -246,8 +247,8 @@ private:
|
||||||
|
|
||||||
// Streaming parameters
|
// Streaming parameters
|
||||||
bool streamingEnabled = true;
|
bool streamingEnabled = true;
|
||||||
int loadRadius = 2; // Load tiles within this radius (5x5 grid for performance)
|
int loadRadius = 1; // Load tiles within this radius (3x3 grid for better CPU/GPU perf)
|
||||||
int unloadRadius = 3; // Unload tiles beyond this radius
|
int unloadRadius = 2; // Unload tiles beyond this radius
|
||||||
float updateInterval = 0.1f; // Check streaming every 0.1 seconds
|
float updateInterval = 0.1f; // Check streaming every 0.1 seconds
|
||||||
float timeSinceLastUpdate = 0.0f;
|
float timeSinceLastUpdate = 0.0f;
|
||||||
|
|
||||||
|
|
@ -257,8 +258,9 @@ private:
|
||||||
static constexpr float TILE_SIZE = 533.33333f; // One tile = 533.33 units
|
static constexpr float TILE_SIZE = 533.33333f; // One tile = 533.33 units
|
||||||
static constexpr float CHUNK_SIZE = 33.33333f; // One chunk = 33.33 units
|
static constexpr float CHUNK_SIZE = 33.33333f; // One chunk = 33.33 units
|
||||||
|
|
||||||
// Background loading thread
|
// Background loading worker pool
|
||||||
std::thread workerThread;
|
std::vector<std::thread> workerThreads;
|
||||||
|
int workerCount = 0;
|
||||||
std::mutex queueMutex;
|
std::mutex queueMutex;
|
||||||
std::condition_variable queueCV;
|
std::condition_variable queueCV;
|
||||||
std::queue<TileCoord> loadQueue;
|
std::queue<TileCoord> loadQueue;
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,15 @@ void FootstepManager::reapFinishedProcess() {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
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;
|
auto& list = surfaces[static_cast<size_t>(surface)].clips;
|
||||||
if (list.empty()) {
|
if (list.empty()) {
|
||||||
list = surfaces[static_cast<size_t>(FootstepSurface::STONE)].clips;
|
list = surfaces[static_cast<size_t>(FootstepSurface::STONE)].clips;
|
||||||
|
|
@ -181,6 +190,7 @@ bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
||||||
_exit(1);
|
_exit(1);
|
||||||
} else if (pid > 0) {
|
} else if (pid > 0) {
|
||||||
playerPid = pid;
|
playerPid = pid;
|
||||||
|
lastPlayTime = now;
|
||||||
return true;
|
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);
|
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;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -235,9 +235,9 @@ void CameraController::update(float deltaTime) {
|
||||||
glm::vec3 desiredPos = targetPos;
|
glm::vec3 desiredPos = targetPos;
|
||||||
float moveDist = glm::length(desiredPos - startPos);
|
float moveDist = glm::length(desiredPos - startPos);
|
||||||
// Adaptive CCD: keep per-step movement short, especially on low FPS spikes.
|
// 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) {
|
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 stepPos = startPos;
|
||||||
glm::vec3 stepDelta = (desiredPos - startPos) / static_cast<float>(sweepSteps);
|
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)
|
// WoW-style slope limiting (50 degrees, with sliding)
|
||||||
// dot(normal, up) >= 0.64 is walkable, otherwise slide
|
// 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 MAX_WALK_SLOPE_DOT = 0.6428f; // cos(50°)
|
||||||
constexpr float SAMPLE_DIST = 0.3f; // Distance to sample for normal calculation
|
constexpr float SAMPLE_DIST = 0.3f; // Distance to sample for normal calculation
|
||||||
{
|
if (ENABLE_SLOPE_SLIDE) {
|
||||||
glm::vec3 oldPos = *followTarget;
|
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 {
|
// Get ground height at target position
|
||||||
std::optional<float> height;
|
auto center = getGroundAt(targetPos.x, targetPos.y);
|
||||||
bool fromM2 = false;
|
bool skipSlopeCheck = center.height && center.fromM2;
|
||||||
};
|
if (center.height && !skipSlopeCheck) {
|
||||||
// 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) {
|
|
||||||
|
|
||||||
// Calculate ground normal using height samples
|
// Calculate ground normal using height samples
|
||||||
auto hPosX = getGroundAt(targetPos.x + SAMPLE_DIST, targetPos.y);
|
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;
|
std::optional<float> groundH;
|
||||||
constexpr float FOOTPRINT = 0.28f;
|
constexpr float FOOTPRINT = 0.28f;
|
||||||
const glm::vec2 offsets[] = {
|
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) {
|
for (const auto& o : offsets) {
|
||||||
auto h = sampleGround(targetPos.x + o.x, targetPos.y + o.y);
|
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 startFeet = camera->getPosition() - glm::vec3(0, 0, eyeHeight);
|
||||||
glm::vec3 desiredFeet = newPos - glm::vec3(0, 0, eyeHeight);
|
glm::vec3 desiredFeet = newPos - glm::vec3(0, 0, eyeHeight);
|
||||||
float moveDist = glm::length(desiredFeet - startFeet);
|
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) {
|
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 stepPos = startFeet;
|
||||||
glm::vec3 stepDelta = (desiredFeet - startFeet) / static_cast<float>(sweepSteps);
|
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
|
// use edge distance (not radial) so corner blocks don't become too low and
|
||||||
// clip-through at diagonals.
|
// clip-through at diagonals.
|
||||||
float edge = std::max(std::abs(nx), std::abs(ny));
|
float edge = std::max(std::abs(nx), std::abs(ny));
|
||||||
if (edge > 0.92f) return localMin.z + h * 0.10f;
|
if (edge > 0.92f) return localMin.z + h * 0.06f;
|
||||||
if (edge > 0.72f) return localMin.z + h * 0.46f;
|
if (edge > 0.72f) return localMin.z + h * 0.30f;
|
||||||
return localMin.z + h * 0.74f;
|
return localMin.z + h * 0.62f;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool segmentIntersectsAABB(const glm::vec3& from, const glm::vec3& to,
|
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)));
|
(likelyCurbName && (lowPlatformShape || lowWideShape)));
|
||||||
|
|
||||||
bool isPlanter = (lowerName.find("planter") != std::string::npos);
|
bool isPlanter = (lowerName.find("planter") != std::string::npos);
|
||||||
|
gpuModel.collisionPlanter = isPlanter;
|
||||||
bool foliageName =
|
bool foliageName =
|
||||||
(lowerName.find("bush") != std::string::npos) ||
|
(lowerName.find("bush") != std::string::npos) ||
|
||||||
(lowerName.find("grass") != 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 softTree = treeLike && !hardTreePart && (canopyLike || vert > horiz * 1.35f);
|
||||||
bool smallSoftShape = (horiz < 2.2f && vert < 2.4f);
|
bool smallSoftShape = (horiz < 2.2f && vert < 2.4f);
|
||||||
bool mediumFoliageShape = (horiz < 4.5f && vert < 4.5f);
|
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) &&
|
gpuModel.collisionNoBlock = ((((foliageName && smallSoftShape) || (foliageName && mediumFoliageShape)) || softTree) &&
|
||||||
!forceSolidCurb);
|
!forceSolidCurb);
|
||||||
}
|
}
|
||||||
|
|
@ -593,7 +594,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
lastDrawCallCount = 0;
|
lastDrawCallCount = 0;
|
||||||
|
|
||||||
// Distance-based culling threshold for M2 models
|
// 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 float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
|
||||||
const glm::vec3 camPos = camera.getPosition();
|
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 distSq = glm::dot(toCam, toCam);
|
||||||
float worldRadius = model.boundRadius * instance.scale;
|
float worldRadius = model.boundRadius * instance.scale;
|
||||||
// Cull small objects (radius < 20) at distance, keep larger objects visible longer
|
// 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) {
|
if (distSq > effectiveMaxDistSq) {
|
||||||
continue;
|
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));
|
glm::vec3 worldTop = glm::vec3(instance.modelMatrix * glm::vec4(localTop, 1.0f));
|
||||||
|
|
||||||
// Reachability filter: allow a bit more climb for stepped low platforms.
|
// 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 (worldTop.z > glZ + maxStepUp) continue;
|
||||||
|
|
||||||
if (!bestFloor || worldTop.z > *bestFloor) {
|
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.
|
// Swept hard clamp for taller blockers only.
|
||||||
// Low/stepable objects should be climbable and not "shove" the player off.
|
// 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 stepableLowObject = (effectiveTop <= localFrom.z + maxStepUp);
|
||||||
bool climbingAttempt = (localPos.z > localFrom.z + 0.18f);
|
bool climbingAttempt = (localPos.z > localFrom.z + 0.18f);
|
||||||
bool nearTop = (localFrom.z >= effectiveTop - 0.30f);
|
bool nearTop = (localFrom.z >= effectiveTop - 0.30f);
|
||||||
bool climbingTowardTop = climbingAttempt && (localFrom.z + 0.35f >= effectiveTop);
|
bool climbingTowardTop = climbingAttempt && (localFrom.z + (model.collisionPlanter ? 0.95f : 0.60f) >= effectiveTop);
|
||||||
bool forceHardLateral = model.collisionSteppedLowPlatform && !nearTop && !climbingTowardTop;
|
bool forceHardLateral = model.collisionSteppedLowPlatform && !model.collisionPlanter && !nearTop && !climbingTowardTop;
|
||||||
if (!stepableLowObject || forceHardLateral) {
|
if (!stepableLowObject || forceHardLateral) {
|
||||||
float tEnter = 0.0f;
|
float tEnter = 0.0f;
|
||||||
glm::vec3 sweepMax = localMax;
|
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.
|
// Gentle fallback push for overlapping cases.
|
||||||
float pushAmount;
|
float pushAmount;
|
||||||
if (model.collisionSteppedLowPlatform) {
|
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) {
|
} else if (stepableLowObject) {
|
||||||
pushAmount = std::clamp(minPush * 0.12f, 0.002f, 0.015f);
|
pushAmount = std::clamp(minPush * 0.12f, 0.002f, 0.015f);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,22 @@ void Minimap::shutdown() {
|
||||||
void Minimap::render(const Camera& playerCamera, int screenWidth, int screenHeight) {
|
void Minimap::render(const Camera& playerCamera, int screenWidth, int screenHeight) {
|
||||||
if (!enabled || !terrainRenderer || !fbo) return;
|
if (!enabled || !terrainRenderer || !fbo) return;
|
||||||
|
|
||||||
// 1. Render terrain from top-down into FBO
|
const auto now = std::chrono::steady_clock::now();
|
||||||
renderTerrainToFBO(playerCamera);
|
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
|
// 2. Draw the minimap quad on screen
|
||||||
renderQuad(screenWidth, screenHeight);
|
renderQuad(screenWidth, screenHeight);
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,12 @@ TerrainManager::~TerrainManager() {
|
||||||
if (workerRunning.load()) {
|
if (workerRunning.load()) {
|
||||||
workerRunning.store(false);
|
workerRunning.store(false);
|
||||||
queueCV.notify_all();
|
queueCV.notify_all();
|
||||||
if (workerThread.joinable()) {
|
for (auto& t : workerThreads) {
|
||||||
workerThread.join();
|
if (t.joinable()) {
|
||||||
|
t.join();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
workerThreads.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,14 +112,20 @@ bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer*
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start background worker thread
|
// Start background worker pool
|
||||||
workerRunning.store(true);
|
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("Terrain manager initialized (async loading enabled)");
|
||||||
LOG_INFO(" Map: ", mapName);
|
LOG_INFO(" Map: ", mapName);
|
||||||
LOG_INFO(" Load radius: ", loadRadius, " tiles");
|
LOG_INFO(" Load radius: ", loadRadius, " tiles");
|
||||||
LOG_INFO(" Unload radius: ", unloadRadius, " tiles");
|
LOG_INFO(" Unload radius: ", unloadRadius, " tiles");
|
||||||
|
LOG_INFO(" Workers: ", workerCount);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +161,7 @@ void TerrainManager::update(const Camera& camera, float deltaTime) {
|
||||||
|
|
||||||
// Stream tiles if we've moved significantly or initial load
|
// Stream tiles if we've moved significantly or initial load
|
||||||
if (newTile.x != lastStreamTile.x || newTile.y != lastStreamTile.y) {
|
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,
|
") tile=[", newTile.x, ",", newTile.y,
|
||||||
"] loaded=", loadedTiles.size());
|
"] loaded=", loadedTiles.size());
|
||||||
streamTiles();
|
streamTiles();
|
||||||
|
|
@ -189,7 +198,7 @@ bool TerrainManager::loadTile(int x, int y) {
|
||||||
std::unique_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
std::unique_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
||||||
TileCoord coord = {x, y};
|
TileCoord coord = {x, y};
|
||||||
|
|
||||||
LOG_INFO("Preparing tile [", x, ",", y, "] (CPU work)");
|
LOG_DEBUG("Preparing tile [", x, ",", y, "] (CPU work)");
|
||||||
|
|
||||||
// Load ADT file
|
// Load ADT file
|
||||||
std::string adtPath = getADTPath(coord);
|
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->m2Models.size(), " M2 models, ",
|
||||||
pending->m2Placements.size(), " M2 placements, ",
|
pending->m2Placements.size(), " M2 placements, ",
|
||||||
pending->wmoModels.size(), " WMOs, ",
|
pending->wmoModels.size(), " WMOs, ",
|
||||||
|
|
@ -459,7 +468,7 @@ void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
|
||||||
int y = pending->coord.y;
|
int y = pending->coord.y;
|
||||||
TileCoord coord = pending->coord;
|
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
|
// Check if tile was already loaded (race condition guard) or failed
|
||||||
if (loadedTiles.find(coord) != loadedTiles.end()) {
|
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, ",
|
loadedDoodads, " instances (", uploadedModelIds.size(), " new models, ",
|
||||||
skippedDedup, " dedup skipped)");
|
skippedDedup, " dedup skipped)");
|
||||||
}
|
}
|
||||||
|
|
@ -558,7 +567,7 @@ void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (loadedLiquids > 0) {
|
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
|
// Upload WMO doodad M2 models
|
||||||
|
|
@ -572,7 +581,7 @@ void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadedWMOs > 0) {
|
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);
|
loadedTiles[coord] = std::move(tile);
|
||||||
|
|
||||||
LOG_INFO(" Finalized tile [", x, ",", y, "]");
|
LOG_DEBUG(" Finalized tile [", x, ",", y, "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TerrainManager::workerLoop() {
|
void TerrainManager::workerLoop() {
|
||||||
|
|
@ -728,9 +737,12 @@ void TerrainManager::unloadAll() {
|
||||||
if (workerRunning.load()) {
|
if (workerRunning.load()) {
|
||||||
workerRunning.store(false);
|
workerRunning.store(false);
|
||||||
queueCV.notify_all();
|
queueCV.notify_all();
|
||||||
if (workerThread.joinable()) {
|
for (auto& t : workerThreads) {
|
||||||
workerThread.join();
|
if (t.joinable()) {
|
||||||
|
t.join();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
workerThreads.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear queues
|
// Clear queues
|
||||||
|
|
@ -809,59 +821,88 @@ std::optional<float> TerrainManager::getHeightAt(float glX, float glY) const {
|
||||||
|
|
||||||
const float unitSize = CHUNK_SIZE / 8.0f;
|
const float unitSize = CHUNK_SIZE / 8.0f;
|
||||||
|
|
||||||
// Query coordinates are the same as what we pass (terrain uses identity model matrix)
|
auto sampleTileHeight = [&](const TerrainTile* tile) -> std::optional<float> {
|
||||||
float queryX = glX;
|
if (!tile || !tile->loaded) return std::nullopt;
|
||||||
float queryY = glY;
|
|
||||||
|
|
||||||
for (const auto& [coord, tile] : loadedTiles) {
|
auto sampleChunk = [&](int cx, int cy) -> std::optional<float> {
|
||||||
if (!tile || !tile->loaded) continue;
|
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++) {
|
float chunkMaxX = chunk.position[0];
|
||||||
for (int cx = 0; cx < 16; cx++) {
|
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
|
||||||
const auto& chunk = tile->terrain.getChunk(cx, cy);
|
float chunkMaxY = chunk.position[1];
|
||||||
if (!chunk.hasHeightMap()) continue;
|
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
|
||||||
|
|
||||||
float chunkMaxX = chunk.position[0];
|
if (glX < chunkMinX || glX > chunkMaxX ||
|
||||||
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
|
glY < chunkMinY || glY > chunkMaxY) {
|
||||||
float chunkMaxY = chunk.position[1];
|
return std::nullopt;
|
||||||
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
|
}
|
||||||
|
|
||||||
if (queryX < chunkMinX || queryX > chunkMaxX ||
|
// Fractional position within chunk (0-8 range)
|
||||||
queryY < chunkMinY || queryY > chunkMaxY) {
|
float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY
|
||||||
continue;
|
float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX
|
||||||
}
|
|
||||||
|
|
||||||
// Fractional position within chunk (0-8 range)
|
fracX = glm::clamp(fracX, 0.0f, 8.0f);
|
||||||
float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY
|
fracY = glm::clamp(fracY, 0.0f, 8.0f);
|
||||||
float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX
|
|
||||||
|
|
||||||
fracX = glm::clamp(fracX, 0.0f, 8.0f);
|
// Bilinear interpolation on 9x9 outer grid
|
||||||
fracY = glm::clamp(fracY, 0.0f, 8.0f);
|
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
|
float tx = fracX - gx0;
|
||||||
int gx0 = static_cast<int>(std::floor(fracX));
|
float ty = fracY - gy0;
|
||||||
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 h00 = chunk.heightMap.heights[gy0 * 17 + gx0];
|
||||||
float ty = fracY - gy0;
|
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
|
float h = h00 * (1 - tx) * (1 - ty) +
|
||||||
// Outer vertex (gx, gy) is at index: gy * 17 + gx
|
h10 * tx * (1 - ty) +
|
||||||
float h00 = chunk.heightMap.heights[gy0 * 17 + gx0];
|
h01 * (1 - tx) * ty +
|
||||||
float h10 = chunk.heightMap.heights[gy0 * 17 + gx1];
|
h11 * tx * ty;
|
||||||
float h01 = chunk.heightMap.heights[gy1 * 17 + gx0];
|
|
||||||
float h11 = chunk.heightMap.heights[gy1 * 17 + gx1];
|
|
||||||
|
|
||||||
float h = h00 * (1 - tx) * (1 - ty) +
|
return chunk.position[2] + h;
|
||||||
h10 * tx * (1 - ty) +
|
};
|
||||||
h01 * (1 - tx) * ty +
|
|
||||||
h11 * tx * ty;
|
|
||||||
|
|
||||||
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;
|
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 {
|
std::optional<std::string> TerrainManager::getDominantTextureAt(float glX, float glY) const {
|
||||||
const float unitSize = CHUNK_SIZE / 8.0f;
|
const float unitSize = CHUNK_SIZE / 8.0f;
|
||||||
std::vector<uint8_t> alphaScratch;
|
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) {
|
auto sampleChunkTexture = [&](int cx, int cy) -> std::optional<std::string> {
|
||||||
(void)coord;
|
if (cx < 0 || cx >= 16 || cy < 0 || cy >= 16) return std::nullopt;
|
||||||
if (!tile || !tile->loaded) continue;
|
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 cy = 0; cy < 16; cy++) {
|
||||||
for (int cx = 0; cx < 16; cx++) {
|
for (int cx = 0; cx < 16; cx++) {
|
||||||
const auto& chunk = tile->terrain.getChunk(cx, cy);
|
auto tex = sampleChunkTexture(cx, cy);
|
||||||
if (!chunk.hasHeightMap() || chunk.layers.empty()) continue;
|
if (tex) {
|
||||||
|
return tex;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return std::nullopt;
|
||||||
|
|
@ -958,8 +1030,8 @@ void TerrainManager::streamTiles() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify worker thread that there's work
|
// Notify workers that there's work
|
||||||
queueCV.notify_one();
|
queueCV.notify_all();
|
||||||
|
|
||||||
// Unload tiles beyond unload radius (well past the camera far clip)
|
// Unload tiles beyond unload radius (well past the camera far clip)
|
||||||
std::vector<TileCoord> tilesToUnload;
|
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
|
// Render all instances with instance-level culling
|
||||||
const glm::vec3 camPos = camera.getPosition();
|
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;
|
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
|
||||||
|
|
||||||
for (const auto& instance : instances) {
|
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;
|
if (normalLen < 0.001f) continue;
|
||||||
normal /= normalLen;
|
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;
|
if (std::abs(normal.z) > 0.85f) continue;
|
||||||
|
|
||||||
// Get triangle Z range
|
// 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
|
// Only collide with walls in player's vertical range
|
||||||
if (triMaxZ < localFeetZ + 0.3f) continue;
|
if (triMaxZ < localFeetZ + 0.3f) continue;
|
||||||
if (triMinZ > localFeetZ + PLAYER_HEIGHT) 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.
|
// Swept test: prevent tunneling when crossing a wall between frames.
|
||||||
float fromDist = glm::dot(localFrom - v0, normal);
|
float fromDist = glm::dot(localFrom - v0, normal);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue