Add loading screen, fix tree/foliage collision, jump buffering, and fence rotation

- Loading screen stays visible until all terrain tiles finish streaming;
  character spawns only after terrain is loaded and Z-snapped to ground
- Reduce tree trunk collision bounds (5% of canopy, capped at 5.0) and
  make all small/medium trees, bushes, lily pads, and foliage walkthrough
- Add jump input buffering (150ms) and coyote time (100ms) for responsive jumps
- Fix fence orientation by adding +180° heading rotation
- Increase terrain load radius from 1 to 2 (5x5 tile grid)
- Add hearthstone callback for single-player camera reset
This commit is contained in:
Kelsi 2026-02-04 13:29:27 -08:00
parent f7cd871895
commit 6ca9e9024a
9 changed files with 188 additions and 222 deletions

View file

@ -204,6 +204,10 @@ public:
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; }
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; }
// Hearthstone callback (single-player teleport)
using HearthstoneCallback = std::function<void()>;
void setHearthstoneCallback(HearthstoneCallback cb) { hearthstoneCallback = std::move(cb); }
// Cooldowns
float getSpellCooldown(uint32_t spellId) const;
@ -432,6 +436,7 @@ private:
std::vector<CombatTextEntry> combatText;
// ---- Phase 3: Spells ----
HearthstoneCallback hearthstoneCallback;
std::vector<uint32_t> knownSpells;
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
uint8_t castCount = 0;

View file

@ -35,6 +35,7 @@ public:
void reset();
float getMovementSpeed() const { return movementSpeed; }
const glm::vec3& getDefaultPosition() const { return defaultPosition; }
bool isMoving() const;
float getYaw() const { return yaw; }
float getFacingYaw() const { return facingYaw; }
@ -116,6 +117,10 @@ private:
float lastGroundZ = 0.0f; // Last known ground height (fallback when no terrain)
static constexpr float GRAVITY = -30.0f;
static constexpr float JUMP_VELOCITY = 15.0f;
float jumpBufferTimer = 0.0f; // Time since space was pressed
float coyoteTimer = 0.0f; // Time since last grounded
static constexpr float JUMP_BUFFER_TIME = 0.15f; // 150ms input buffer
static constexpr float COYOTE_TIME = 0.10f; // 100ms grace after leaving ground
// Swimming
bool swimming = false;
@ -160,9 +165,9 @@ private:
static constexpr float WOW_GRAVITY = -19.29f;
static constexpr float WOW_JUMP_VELOCITY = 7.96f;
// Default spawn position (Stormwind Trade District)
glm::vec3 defaultPosition = glm::vec3(-8830.0f, 640.0f, 200.0f);
float defaultYaw = 0.0f; // Look north toward canals
// Default spawn position (Goldshire Inn)
glm::vec3 defaultPosition = glm::vec3(-9464.0f, 62.0f, 200.0f);
float defaultYaw = 0.0f;
float defaultPitch = -5.0f;
};

View file

@ -47,6 +47,7 @@ struct M2ModelGPU {
bool collisionPlanter = false;
bool collisionSmallSolidProp = false;
bool collisionNarrowVerticalProp = false;
bool collisionTreeTrunk = false;
bool collisionNoBlock = false;
bool collisionStatue = false;

View file

@ -183,6 +183,7 @@ public:
* Get statistics
*/
int getLoadedTileCount() const { return static_cast<int>(loadedTiles.size()); }
int getPendingTileCount() const { return static_cast<int>(pendingTiles.size()); }
TileCoord getCurrentTile() const { return currentTile; }
private:
@ -247,8 +248,8 @@ private:
// Streaming parameters
bool streamingEnabled = true;
int loadRadius = 1; // Load tiles within this radius (3x3 grid for better CPU/GPU perf)
int unloadRadius = 2; // Unload tiles beyond this radius
int loadRadius = 2; // Load tiles within this radius (5x5 grid)
int unloadRadius = 3; // Unload tiles beyond this radius
float updateInterval = 0.1f; // Check streaming every 0.1 seconds
float timeSinceLastUpdate = 0.0f;

View file

@ -108,43 +108,8 @@ bool Application::initialize() {
void Application::run() {
LOG_INFO("Starting main loop");
// Show loading screen while loading initial data
rendering::LoadingScreen loadingScreen;
if (loadingScreen.initialize()) {
// Render loading screen
loadingScreen.setStatus("Initializing...");
loadingScreen.render();
window->swapBuffers();
// Load terrain data
if (assetManager && assetManager->isInitialized() && renderer) {
loadingScreen.setStatus("Loading terrain...");
loadingScreen.render();
window->swapBuffers();
renderer->loadTestTerrain(assetManager.get(), "World\\Maps\\Azeroth\\Azeroth_32_49.adt");
loadingScreen.setStatus("Spawning character...");
loadingScreen.render();
window->swapBuffers();
// Spawn player character with third-person camera
spawnPlayerCharacter();
}
loadingScreen.setStatus("Ready!");
loadingScreen.render();
window->swapBuffers();
SDL_Delay(500); // Brief pause to show "Ready!"
loadingScreen.shutdown();
} else {
// Fallback: load without loading screen
if (assetManager && assetManager->isInitialized() && renderer) {
renderer->loadTestTerrain(assetManager.get(), "World\\Maps\\Azeroth\\Azeroth_32_49.adt");
spawnPlayerCharacter();
}
}
// Terrain and character are loaded via startSinglePlayer() when the user
// picks single-player mode, so nothing is preloaded here.
auto lastTime = std::chrono::high_resolution_clock::now();
@ -653,8 +618,17 @@ void Application::spawnPlayerCharacter() {
LOG_INFO("Loaded fallback cube model (no MPQ data)");
}
// Spawn character at camera's ground position
glm::vec3 spawnPos = camera->getPosition() - glm::vec3(0.0f, 0.0f, 5.0f);
// Spawn character at the camera controller's default position (matches hearthstone),
// but snap Z to actual terrain height so the character doesn't float.
auto* camCtrl = renderer->getCameraController();
glm::vec3 spawnPos = camCtrl ? camCtrl->getDefaultPosition()
: (camera->getPosition() - glm::vec3(0.0f, 0.0f, 5.0f));
if (renderer->getTerrainManager()) {
auto terrainH = renderer->getTerrainManager()->getHeightAt(spawnPos.x, spawnPos.y);
if (terrainH) {
spawnPos.z = *terrainH + 0.1f;
}
}
uint32_t instanceId = charRenderer->createInstance(1, spawnPos,
glm::vec3(0.0f), 1.0f); // Scale 1.0 = normal WoW character size
@ -861,16 +835,6 @@ void Application::startSinglePlayer() {
LOG_INFO("Single-player world created");
}
// Set up camera for single-player mode
if (renderer && renderer->getCamera()) {
auto* camera = renderer->getCamera();
// Position: high above terrain to see landscape (terrain around origin is ~80-100 units high)
camera->setPosition(glm::vec3(0.0f, 0.0f, 300.0f)); // 300 units up
// Rotation: looking north (yaw 0) with downward tilt to see terrain
camera->setRotation(0.0f, -30.0f); // Look down more to see terrain below
LOG_INFO("Camera positioned for single-player mode");
}
// Populate test inventory for single-player
if (gameHandler) {
gameHandler->getInventory().populateTestItems();
@ -879,134 +843,106 @@ void Application::startSinglePlayer() {
// Load weapon models for equipped items (after inventory is populated)
loadEquippedWeapons();
// --- Loading screen: load terrain and wait for streaming before spawning ---
rendering::LoadingScreen loadingScreen;
bool loadingScreenOk = loadingScreen.initialize();
auto showStatus = [&](const char* msg) {
if (!loadingScreenOk) return;
loadingScreen.setStatus(msg);
loadingScreen.render();
window->swapBuffers();
};
showStatus("Loading terrain...");
// Try to load test terrain if WOW_DATA_PATH is set
bool terrainOk = false;
if (renderer && assetManager && assetManager->isInitialized()) {
LOG_INFO("Loading test terrain for single-player mode...");
// Try to load Elwynn Forest (most common starting zone)
// ADT coordinates: (32, 49) is near Northshire Abbey
std::string adtPath = "World\\Maps\\Azeroth\\Azeroth_32_49.adt";
if (renderer->loadTestTerrain(assetManager.get(), adtPath)) {
LOG_INFO("Test terrain loaded successfully");
} else {
LOG_WARNING("Could not load test terrain - continuing with atmospheric rendering only");
LOG_INFO("Set WOW_DATA_PATH environment variable to load terrain");
terrainOk = renderer->loadTestTerrain(assetManager.get(), adtPath);
if (!terrainOk) {
LOG_WARNING("Could not load test terrain - atmospheric rendering only");
}
} else {
LOG_INFO("Asset manager not available - atmospheric rendering only");
LOG_INFO("Set WOW_DATA_PATH environment variable to enable terrain loading");
}
// Spawn test objects for single-player mode
if (renderer) {
LOG_INFO("Spawning test objects for single-player mode...");
// Wait for surrounding terrain tiles to stream in
if (terrainOk && renderer->getTerrainManager() && renderer->getCamera()) {
auto* terrainMgr = renderer->getTerrainManager();
auto* camera = renderer->getCamera();
// Spawn test characters in a row
auto* characterRenderer = renderer->getCharacterRenderer();
if (characterRenderer) {
// Create test character model (same as K key)
pipeline::M2Model testModel;
float size = 2.0f;
std::vector<glm::vec3> cubePos = {
{-size, -size, -size}, { size, -size, -size},
{ size, size, -size}, {-size, size, -size},
{-size, -size, size}, { size, -size, size},
{ size, size, size}, {-size, size, size}
};
// First update with large dt to trigger streamTiles() immediately
terrainMgr->update(*camera, 1.0f);
for (const auto& pos : cubePos) {
pipeline::M2Vertex v;
v.position = pos;
v.normal = glm::normalize(pos);
v.texCoords[0] = glm::vec2(0.0f);
v.boneWeights[0] = 255;
v.boneWeights[1] = v.boneWeights[2] = v.boneWeights[3] = 0;
v.boneIndices[0] = 0;
v.boneIndices[1] = v.boneIndices[2] = v.boneIndices[3] = 0;
testModel.vertices.push_back(v);
}
auto startTime = std::chrono::high_resolution_clock::now();
const float maxWaitSeconds = 15.0f;
// One bone at origin
pipeline::M2Bone bone;
bone.keyBoneId = -1;
bone.flags = 0;
bone.parentBone = -1;
bone.submeshId = 0;
bone.pivot = glm::vec3(0.0f);
testModel.bones.push_back(bone);
// Simple animation
pipeline::M2Sequence seq{};
seq.id = 0;
seq.duration = 1000;
testModel.sequences.push_back(seq);
// Load model into renderer
if (characterRenderer->loadModel(testModel, 1)) {
// Spawn 5 characters in a row
for (int i = 0; i < 5; i++) {
glm::vec3 pos(i * 15.0f - 30.0f, 80.0f, 0.0f);
characterRenderer->createInstance(1, pos);
while (terrainMgr->getPendingTileCount() > 0) {
// Poll events to keep window responsive
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
window->setShouldClose(true);
loadingScreen.shutdown();
return;
}
LOG_INFO("Spawned 5 test characters");
}
// Process ready tiles from worker threads
terrainMgr->update(*camera, 0.016f);
// Update loading screen with progress
if (loadingScreenOk) {
int loaded = terrainMgr->getLoadedTileCount();
int pending = terrainMgr->getPendingTileCount();
char buf[128];
snprintf(buf, sizeof(buf), "Loading terrain... %d tiles loaded, %d remaining",
loaded, pending);
loadingScreen.setStatus(buf);
loadingScreen.render();
window->swapBuffers();
}
// Timeout safety
auto elapsed = std::chrono::high_resolution_clock::now() - startTime;
if (std::chrono::duration<float>(elapsed).count() > maxWaitSeconds) {
LOG_WARNING("Terrain streaming timeout after ", maxWaitSeconds, "s");
break;
}
SDL_Delay(16); // ~60fps cap for loading screen
}
// Spawn test buildings in a grid
auto* wmoRenderer = renderer->getWMORenderer();
if (wmoRenderer) {
// Create procedural test WMO if not already loaded
pipeline::WMOModel testWMO;
testWMO.version = 17;
LOG_INFO("Terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded");
pipeline::WMOGroup group;
group.vertices = {
{{-5, -5, 0}, {0, 0, 1}, {0, 0}, {0.8f, 0.7f, 0.6f, 1.0f}},
{{5, -5, 0}, {0, 0, 1}, {1, 0}, {0.8f, 0.7f, 0.6f, 1.0f}},
{{5, 5, 0}, {0, 0, 1}, {1, 1}, {0.8f, 0.7f, 0.6f, 1.0f}},
{{-5, 5, 0}, {0, 0, 1}, {0, 1}, {0.8f, 0.7f, 0.6f, 1.0f}},
{{-5, -5, 10}, {0, 0, 1}, {0, 0}, {0.7f, 0.6f, 0.5f, 1.0f}},
{{5, -5, 10}, {0, 0, 1}, {1, 0}, {0.7f, 0.6f, 0.5f, 1.0f}},
{{5, 5, 10}, {0, 0, 1}, {1, 1}, {0.7f, 0.6f, 0.5f, 1.0f}},
{{-5, 5, 10}, {0, 0, 1}, {0, 1}, {0.7f, 0.6f, 0.5f, 1.0f}}
};
pipeline::WMOBatch batch;
batch.startIndex = 0;
batch.indexCount = 36;
batch.materialId = 0;
group.batches.push_back(batch);
group.indices = {
0,1,2, 0,2,3, 4,6,5, 4,7,6,
0,4,5, 0,5,1, 1,5,6, 1,6,2,
2,6,7, 2,7,3, 3,7,4, 3,4,0
};
testWMO.groups.push_back(group);
pipeline::WMOMaterial material;
material.shader = 0;
material.blendMode = 0;
testWMO.materials.push_back(material);
// Load the test model
if (wmoRenderer->loadModel(testWMO, 1)) {
// Spawn buildings in a grid pattern
for (int x = -1; x <= 1; x++) {
for (int y = 0; y <= 2; y++) {
glm::vec3 pos(x * 30.0f, y * 30.0f + 120.0f, 0.0f);
wmoRenderer->createInstance(1, pos);
}
}
LOG_INFO("Spawned 9 test buildings");
}
// Re-snap camera to ground now that all surrounding tiles are loaded
// (the initial reset inside loadTestTerrain only had 1 tile)
if (renderer->getCameraController()) {
renderer->getCameraController()->reset();
}
}
LOG_INFO("Test objects spawned - you should see characters and buildings");
LOG_INFO("Use WASD to fly around, mouse to look");
LOG_INFO("Press K for more characters, O for more buildings");
showStatus("Spawning character...");
// Spawn player character on loaded terrain
spawnPlayerCharacter();
// Final camera reset: now that follow target exists and terrain is loaded,
// snap the third-person camera into the correct orbit position.
if (renderer && renderer->getCameraController()) {
renderer->getCameraController()->reset();
}
if (loadingScreenOk) {
loadingScreen.shutdown();
}
// Wire hearthstone to camera reset (teleport home) in single-player
if (gameHandler && renderer && renderer->getCameraController()) {
auto* camCtrl = renderer->getCameraController();
gameHandler->setHearthstoneCallback([camCtrl]() {
camCtrl->reset();
});
}
// Go directly to game

View file

@ -1101,11 +1101,12 @@ void GameHandler::handleCreatureQueryResponse(network::Packet& packet) {
// ============================================================
void GameHandler::startAutoAttack(uint64_t targetGuid) {
if (state != WorldState::IN_WORLD || !socket) return;
autoAttacking = true;
autoAttackTarget = targetGuid;
auto packet = AttackSwingPacket::build(targetGuid);
socket->send(packet);
if (state == WorldState::IN_WORLD && socket) {
auto packet = AttackSwingPacket::build(targetGuid);
socket->send(packet);
}
LOG_INFO("Starting auto-attack on 0x", std::hex, targetGuid, std::dec);
}
@ -1204,9 +1205,14 @@ void GameHandler::handleSpellHealLog(network::Packet& packet) {
// ============================================================
void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
if (state != WorldState::IN_WORLD || !socket) return;
// Hearthstone (8690) — handle locally when no server connection (single-player)
if (spellId == 8690 && hearthstoneCallback) {
LOG_INFO("Hearthstone: teleporting home");
hearthstoneCallback();
return;
}
// Attack (6603) routes to auto-attack instead of cast
// Attack (6603) routes to auto-attack instead of cast (works without server)
if (spellId == 6603) {
uint64_t target = targetGuid != 0 ? targetGuid : this->targetGuid;
if (target != 0) {
@ -1219,6 +1225,8 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
return;
}
if (state != WorldState::IN_WORLD || !socket) return;
if (casting) return; // Already casting
uint64_t target = targetGuid != 0 ? targetGuid : targetGuid;

View file

@ -154,8 +154,8 @@ void CameraController::update(float deltaTime) {
glm::vec3 forward(std::cos(moveYawRad), std::sin(moveYawRad), 0.0f);
glm::vec3 right(-std::sin(moveYawRad), std::cos(moveYawRad), 0.0f);
// Toggle sit/crouch with X or C key (edge-triggered) — only when UI doesn't want keyboard
bool xDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_X) || input.isKeyPressed(SDL_SCANCODE_C));
// Toggle sit/crouch with X key (edge-triggered) — only when UI doesn't want keyboard
bool xDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_X);
if (xDown && !xKeyWasDown) {
sitting = !sitting;
}
@ -190,37 +190,16 @@ void CameraController::update(float deltaTime) {
m2Renderer->setCollisionFocus(targetPos, COLLISION_FOCUS_RADIUS_THIRD_PERSON);
}
// Check for water at current position
// Check for water at current position — simple submersion test.
// If the player's feet are meaningfully below the water surface, swim.
std::optional<float> waterH;
if (waterRenderer) {
waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y);
}
constexpr float MAX_SWIM_DEPTH_FROM_SURFACE = 12.0f;
bool inWater = false;
if (waterH && targetPos.z < *waterH) {
std::optional<uint16_t> waterType;
if (waterRenderer) {
waterType = waterRenderer->getWaterTypeAt(targetPos.x, targetPos.y);
}
bool isOcean = false;
if (waterType && *waterType != 0) {
isOcean = (((*waterType - 1) % 4) == 1);
}
bool depthAllowed = isOcean || ((*waterH - targetPos.z) <= MAX_SWIM_DEPTH_FROM_SURFACE);
if (!depthAllowed) {
inWater = false;
} else {
std::optional<float> terrainH;
std::optional<float> wmoH;
std::optional<float> m2H;
if (terrainManager) terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y);
if (wmoRenderer) wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 6.0f);
if (m2Renderer) m2H = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 1.0f);
auto floorH = selectHighestFloor(terrainH, wmoH, m2H);
constexpr float MIN_SWIM_WATER_DEPTH = 1.8f;
// Ocean is valid even when ground isn't currently resolved (deep water or streaming gaps).
inWater = (floorH && ((*waterH - *floorH) >= MIN_SWIM_WATER_DEPTH)) || (isOcean && !floorH);
}
bool inWater = waterH && (targetPos.z < (*waterH - 0.3f));
// Keep swimming through water-data gaps (chunk boundaries).
if (!inWater && swimming && !waterH) {
inWater = true;
}
@ -298,7 +277,7 @@ void CameraController::update(float deltaTime) {
if (mh && (!floorH || *mh > *floorH)) floorH = mh;
}
if (floorH) {
float swimFloor = *floorH + 0.30f;
float swimFloor = *floorH + 0.5f;
if (targetPos.z < swimFloor) {
targetPos.z = swimFloor;
if (verticalVelocity < 0.0f) verticalVelocity = 0.0f;
@ -343,6 +322,7 @@ void CameraController::update(float deltaTime) {
grounded = false;
} else {
// Exiting water — give a small upward boost to help climb onto shore.
swimming = false;
if (glm::length(movement) > 0.001f) {
@ -350,12 +330,21 @@ void CameraController::update(float deltaTime) {
targetPos += movement * speed * deltaTime;
}
// Jump
if (nowJump && grounded) {
// Jump with input buffering and coyote time
if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME;
if (grounded) coyoteTimer = COYOTE_TIME;
bool canJump = (coyoteTimer > 0.0f) && (jumpBufferTimer > 0.0f);
if (canJump) {
verticalVelocity = jumpVel;
grounded = false;
jumpBufferTimer = 0.0f;
coyoteTimer = 0.0f;
}
jumpBufferTimer -= deltaTime;
coyoteTimer -= deltaTime;
// Apply gravity
verticalVelocity += gravity * deltaTime;
targetPos.z += verticalVelocity * deltaTime;
@ -501,7 +490,8 @@ void CameraController::update(float deltaTime) {
}
// Ground the character to terrain or WMO floor
{
// Skip entirely while swimming — the swim floor clamp handles vertical bounds.
if (!swimming) {
auto sampleGround = [&](float x, float y) -> std::optional<float> {
std::optional<float> terrainH;
std::optional<float> wmoH;
@ -549,15 +539,14 @@ void CameraController::update(float deltaTime) {
lastGroundZ = *groundH;
}
if (targetPos.z <= lastGroundZ + 0.1f) {
if (targetPos.z <= lastGroundZ + 0.1f && verticalVelocity <= 0.0f) {
targetPos.z = lastGroundZ;
verticalVelocity = 0.0f;
grounded = true;
swimming = false; // Touching ground = wading, not swimming
} else if (!swimming) {
} else {
grounded = false;
}
} else if (!swimming) {
} else {
// No terrain found — hold at last known ground
targetPos.z = lastGroundZ;
verticalVelocity = 0.0f;
@ -762,12 +751,20 @@ void CameraController::update(float deltaTime) {
newPos += movement * speed * deltaTime;
}
// Jump
if (nowJump && grounded) {
// Jump with input buffering and coyote time
if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME;
if (grounded) coyoteTimer = COYOTE_TIME;
if (coyoteTimer > 0.0f && jumpBufferTimer > 0.0f) {
verticalVelocity = jumpVel;
grounded = false;
jumpBufferTimer = 0.0f;
coyoteTimer = 0.0f;
}
jumpBufferTimer -= deltaTime;
coyoteTimer -= deltaTime;
// Apply gravity
verticalVelocity += gravity * deltaTime;
newPos.z += verticalVelocity * deltaTime;

View file

@ -29,7 +29,18 @@ void getTightCollisionBounds(const M2ModelGPU& model, glm::vec3& outMin, glm::ve
// larger than default to prevent walk-through on narrow objects
// - default: tighter fit (avoid oversized blockers)
// - stepped low platforms (tree curbs/planters): wider XY + lower Z
if (model.collisionNarrowVerticalProp) {
if (model.collisionTreeTrunk) {
// Tree trunk: proportional cylinder at the base of the tree.
float modelHoriz = std::max(model.boundMax.x - model.boundMin.x,
model.boundMax.y - model.boundMin.y);
float trunkHalf = std::clamp(modelHoriz * 0.05f, 0.5f, 5.0f);
half.x = trunkHalf;
half.y = trunkHalf;
// Height proportional to trunk width, capped at 3.5 units.
half.z = std::min(trunkHalf * 2.5f, 3.5f);
// Shift center down so collision is at the base (trunk), not mid-canopy.
center.z = model.boundMin.z + half.z;
} else if (model.collisionNarrowVerticalProp) {
// Tall thin props (lamps/posts): keep passable gaps near walls.
half.x *= 0.30f;
half.y *= 0.30f;
@ -396,19 +407,19 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
(lowerName.find("flower") != std::string::npos) ||
(lowerName.find("shrub") != std::string::npos) ||
(lowerName.find("fern") != std::string::npos) ||
(lowerName.find("vine") != std::string::npos);
bool canopyLike =
(lowerName.find("canopy") != std::string::npos) ||
(lowerName.find("leaf") != std::string::npos) ||
(lowerName.find("leaves") != std::string::npos);
(lowerName.find("vine") != std::string::npos) ||
(lowerName.find("lily") != std::string::npos) ||
(lowerName.find("weed") != std::string::npos);
bool treeLike = (lowerName.find("tree") != std::string::npos);
bool hardTreePart =
(lowerName.find("trunk") != std::string::npos) ||
(lowerName.find("stump") != std::string::npos) ||
(lowerName.find("log") != std::string::npos);
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);
// Only large trees (canopy > 20 model units wide) get trunk collision.
// Small/mid trees are walkthrough to avoid getting stuck between them.
// Only large trees get trunk collision; all smaller trees are walkthrough.
bool treeWithTrunk = treeLike && !hardTreePart && !foliageName && horiz > 40.0f;
bool softTree = treeLike && !hardTreePart && !treeWithTrunk;
bool forceSolidCurb = gpuModel.collisionSteppedLowPlatform || knownStormwindPlanter || likelyCurbName || gpuModel.collisionPlanter;
bool narrowVerticalName =
(lowerName.find("lamp") != std::string::npos) ||
@ -417,6 +428,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
(lowerName.find("pole") != std::string::npos);
bool narrowVerticalShape =
(horiz > 0.12f && horiz < 2.0f && vert > 2.2f && vert > horiz * 1.8f);
gpuModel.collisionTreeTrunk = treeWithTrunk;
gpuModel.collisionNarrowVerticalProp =
!gpuModel.collisionSteppedFountain &&
!gpuModel.collisionSteppedLowPlatform &&
@ -435,10 +447,11 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
!gpuModel.collisionSteppedFountain &&
!gpuModel.collisionSteppedLowPlatform &&
!gpuModel.collisionNarrowVerticalProp &&
!gpuModel.collisionTreeTrunk &&
!curbLikeName &&
!lowPlatformLikeShape &&
(smallSolidPropName || (genericSolidPropShape && !foliageName && !softTree));
gpuModel.collisionNoBlock = ((((foliageName && smallSoftShape) || (foliageName && mediumFoliageShape)) || softTree) &&
gpuModel.collisionNoBlock = ((foliageName || softTree) &&
!forceSolidCurb);
}
gpuModel.boundMin = tightMin;
@ -883,7 +896,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
lastDrawCallCount = 0;
// Adaptive render distance: shorter in dense areas (cities), longer in open terrain
const float maxRenderDistance = (instances.size() > 600) ? 180.0f : 350.0f;
const float maxRenderDistance = (instances.size() > 600) ? 180.0f : 2000.0f;
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
const float fadeStartFraction = 0.75f;
const glm::vec3 camPos = camera.getPosition();

View file

@ -310,7 +310,7 @@ std::unique_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
p.rotation = glm::vec3(
-placement.rotation[2] * 3.14159f / 180.0f,
-placement.rotation[0] * 3.14159f / 180.0f,
placement.rotation[1] * 3.14159f / 180.0f
(placement.rotation[1] + 180.0f) * 3.14159f / 180.0f
);
p.scale = placement.scale / 1024.0f;
pending->m2Placements.push_back(p);