Performance optimizations and collision improvements

Performance:
- Remove expensive inverse() from all vertex shaders (terrain, WMO, M2, water, character)
- Add uniform location caching to avoid repeated glGetUniformLocation calls
- Add proper frustum culling for WMO groups using AABB intersection
- Add distance-based culling for WMO and M2 instances
- Add cleanup of unused M2/WMO models when tiles unload

Collision & Movement:
- Add M2 doodad collision detection (fences, boxes, etc.)
- Reduce character eye height (5.0 -> 1.8) and collision radius (2.5 -> 0.5)
- Enable WoW-style movement speed by default (14 units/sec run, 5 walk, 9 back)
- Fix emote grammar ("You waves." -> "You wave.")

Misc:
- Rename window title to "Wowee"
This commit is contained in:
Kelsi 2026-02-02 23:03:45 -08:00
parent 0c85fcd444
commit 4287878a73
16 changed files with 258 additions and 32 deletions

View file

@ -14,7 +14,8 @@ uniform mat4 uProjection;
void main() { void main() {
FragPos = vec3(uModel * vec4(aPosition, 1.0)); FragPos = vec3(uModel * vec4(aPosition, 1.0));
Normal = mat3(transpose(inverse(uModel))) * aNormal; // Use mat3(uModel) directly - avoids expensive inverse() per vertex
Normal = mat3(uModel) * aNormal;
TexCoord = aTexCoord; TexCoord = aTexCoord;
gl_Position = uProjection * uView * vec4(FragPos, 1.0); gl_Position = uProjection * uView * vec4(FragPos, 1.0);

View file

@ -18,8 +18,8 @@ void main() {
vec4 worldPos = uModel * vec4(aPosition, 1.0); vec4 worldPos = uModel * vec4(aPosition, 1.0);
FragPos = worldPos.xyz; FragPos = worldPos.xyz;
// Transform normal to world space // Terrain uses identity model matrix, so normal passes through directly
Normal = mat3(transpose(inverse(uModel))) * aNormal; Normal = aNormal;
TexCoord = aTexCoord; TexCoord = aTexCoord;
LayerUV = aLayerUV; LayerUV = aLayerUV;

View file

@ -10,6 +10,7 @@ namespace rendering {
class TerrainManager; class TerrainManager;
class WMORenderer; class WMORenderer;
class M2Renderer;
class WaterRenderer; class WaterRenderer;
class CameraController { class CameraController {
@ -25,6 +26,7 @@ public:
void setEnabled(bool enabled) { this->enabled = enabled; } void setEnabled(bool enabled) { this->enabled = enabled; }
void setTerrainManager(TerrainManager* tm) { terrainManager = tm; } void setTerrainManager(TerrainManager* tm) { terrainManager = tm; }
void setWMORenderer(WMORenderer* wmo) { wmoRenderer = wmo; } void setWMORenderer(WMORenderer* wmo) { wmoRenderer = wmo; }
void setM2Renderer(M2Renderer* m2) { m2Renderer = m2; }
void setWaterRenderer(WaterRenderer* wr) { waterRenderer = wr; } void setWaterRenderer(WaterRenderer* wr) { waterRenderer = wr; }
void processMouseWheel(float delta); void processMouseWheel(float delta);
@ -54,6 +56,7 @@ private:
Camera* camera; Camera* camera;
TerrainManager* terrainManager = nullptr; TerrainManager* terrainManager = nullptr;
WMORenderer* wmoRenderer = nullptr; WMORenderer* wmoRenderer = nullptr;
M2Renderer* m2Renderer = nullptr;
WaterRenderer* waterRenderer = nullptr; WaterRenderer* waterRenderer = nullptr;
// Stored rotation (avoids lossy forward-vector round-trip) // Stored rotation (avoids lossy forward-vector round-trip)
@ -82,7 +85,7 @@ private:
// Gravity / grounding // Gravity / grounding
float verticalVelocity = 0.0f; float verticalVelocity = 0.0f;
bool grounded = false; bool grounded = false;
float eyeHeight = 5.0f; float eyeHeight = 1.8f; // WoW human eye height (~2 yard tall character)
float lastGroundZ = 0.0f; // Last known ground height (fallback when no terrain) float lastGroundZ = 0.0f; // Last known ground height (fallback when no terrain)
static constexpr float GRAVITY = -30.0f; static constexpr float GRAVITY = -30.0f;
static constexpr float JUMP_VELOCITY = 15.0f; static constexpr float JUMP_VELOCITY = 15.0f;
@ -112,11 +115,11 @@ private:
// Movement callback // Movement callback
MovementCallback movementCallback; MovementCallback movementCallback;
// WoW-correct speeds // Movement speeds (scaled up for better feel)
bool useWoWSpeed = false; bool useWoWSpeed = false;
static constexpr float WOW_RUN_SPEED = 7.0f; static constexpr float WOW_RUN_SPEED = 14.0f; // Double base WoW speed for responsiveness
static constexpr float WOW_WALK_SPEED = 2.5f; static constexpr float WOW_WALK_SPEED = 5.0f; // Walk (hold Shift)
static constexpr float WOW_BACK_SPEED = 4.5f; static constexpr float WOW_BACK_SPEED = 9.0f; // Backpedal
static constexpr float WOW_GRAVITY = -19.29f; static constexpr float WOW_GRAVITY = -19.29f;
static constexpr float WOW_JUMP_VELOCITY = 7.96f; static constexpr float WOW_JUMP_VELOCITY = 7.96f;

View file

@ -116,6 +116,23 @@ public:
*/ */
void clear(); void clear();
/**
* Remove models that have no instances referencing them
* Call periodically to free GPU memory
*/
void cleanupUnusedModels();
/**
* Check collision with M2 objects and adjust position
* @param from Starting position
* @param to Desired position
* @param adjustedPos Output adjusted position
* @param playerRadius Collision radius of player
* @return true if collision occurred
*/
bool checkCollision(const glm::vec3& from, const glm::vec3& to,
glm::vec3& adjustedPos, float playerRadius = 0.5f) const;
// Stats // Stats
uint32_t getModelCount() const { return static_cast<uint32_t>(models.size()); } uint32_t getModelCount() const { return static_cast<uint32_t>(models.size()); }
uint32_t getInstanceCount() const { return static_cast<uint32_t>(instances.size()); } uint32_t getInstanceCount() const { return static_cast<uint32_t>(instances.size()); }

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <string> #include <string>
#include <unordered_map>
#include <GL/glew.h> #include <GL/glew.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
@ -25,6 +26,7 @@ public:
void setUniform(const std::string& name, const glm::vec4& value); void setUniform(const std::string& name, const glm::vec4& value);
void setUniform(const std::string& name, const glm::mat3& value); void setUniform(const std::string& name, const glm::mat3& value);
void setUniform(const std::string& name, const glm::mat4& value); void setUniform(const std::string& name, const glm::mat4& value);
void setUniformMatrixArray(const std::string& name, const glm::mat4* matrices, int count);
GLuint getProgram() const { return program; } GLuint getProgram() const { return program; }
@ -35,6 +37,9 @@ private:
GLuint program = 0; GLuint program = 0;
GLuint vertexShader = 0; GLuint vertexShader = 0;
GLuint fragmentShader = 0; GLuint fragmentShader = 0;
// Cache uniform locations to avoid expensive glGetUniformLocation calls
mutable std::unordered_map<std::string, GLint> uniformLocationCache;
}; };
} // namespace rendering } // namespace rendering

View file

@ -102,6 +102,12 @@ public:
*/ */
uint32_t getInstanceCount() const { return instances.size(); } uint32_t getInstanceCount() const { return instances.size(); }
/**
* Remove models that have no instances referencing them
* Call periodically to free GPU memory
*/
void cleanupUnusedModels();
/** /**
* Get total triangle count (all instances) * Get total triangle count (all instances)
*/ */

View file

@ -51,10 +51,10 @@ bool Application::initialize() {
// Create window // Create window
WindowConfig windowConfig; WindowConfig windowConfig;
windowConfig.title = "Wowser - World of Warcraft Client"; windowConfig.title = "Wowee";
windowConfig.width = 1920; windowConfig.width = 1280;
windowConfig.height = 1080; windowConfig.height = 720;
windowConfig.vsync = true; windowConfig.vsync = false;
window = std::make_unique<Window>(windowConfig); window = std::make_unique<Window>(windowConfig);
if (!window->initialize()) { if (!window->initialize()) {

View file

@ -1,6 +1,7 @@
#include "rendering/camera_controller.hpp" #include "rendering/camera_controller.hpp"
#include "rendering/terrain_manager.hpp" #include "rendering/terrain_manager.hpp"
#include "rendering/wmo_renderer.hpp" #include "rendering/wmo_renderer.hpp"
#include "rendering/m2_renderer.hpp"
#include "rendering/water_renderer.hpp" #include "rendering/water_renderer.hpp"
#include "game/opcodes.hpp" #include "game/opcodes.hpp"
#include "core/logger.hpp" #include "core/logger.hpp"
@ -152,7 +153,7 @@ void CameraController::update(float deltaTime) {
targetPos.z += verticalVelocity * deltaTime; targetPos.z += verticalVelocity * deltaTime;
} }
// Wall collision for character // Wall collision for character (WMO buildings)
if (wmoRenderer) { if (wmoRenderer) {
glm::vec3 feetPos = targetPos; glm::vec3 feetPos = targetPos;
glm::vec3 oldFeetPos = *followTarget; glm::vec3 oldFeetPos = *followTarget;
@ -164,6 +165,15 @@ void CameraController::update(float deltaTime) {
} }
} }
// Collision with M2 doodads (fences, boxes, etc.)
if (m2Renderer) {
glm::vec3 adjusted;
if (m2Renderer->checkCollision(*followTarget, targetPos, adjusted)) {
targetPos.x = adjusted.x;
targetPos.y = adjusted.y;
}
}
// Ground the character to terrain or WMO floor // Ground the character to terrain or WMO floor
{ {
std::optional<float> terrainH; std::optional<float> terrainH;

View file

@ -72,7 +72,9 @@ bool CharacterRenderer::initialize() {
vec4 worldPos = uModel * skinnedPos; vec4 worldPos = uModel * skinnedPos;
FragPos = worldPos.xyz; FragPos = worldPos.xyz;
Normal = mat3(transpose(inverse(uModel * boneTransform))) * aNormal; // Use mat3 directly - avoid expensive inverse() in shader
// Works correctly for uniform scaling; normalize in fragment shader handles the rest
Normal = mat3(uModel) * mat3(boneTransform) * aNormal;
TexCoord = aTexCoord; TexCoord = aTexCoord;
gl_Position = uProjection * uView * worldPos; gl_Position = uProjection * uView * worldPos;
@ -984,11 +986,10 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
: getModelMatrix(instance); : getModelMatrix(instance);
characterShader->setUniform("uModel", modelMat); characterShader->setUniform("uModel", modelMat);
// Set bone matrices // Set bone matrices (upload all at once for performance)
int numBones = std::min(static_cast<int>(instance.boneMatrices.size()), MAX_BONES); int numBones = std::min(static_cast<int>(instance.boneMatrices.size()), MAX_BONES);
for (int i = 0; i < numBones; i++) { if (numBones > 0) {
std::string uniformName = "uBones[" + std::to_string(i) + "]"; characterShader->setUniformMatrixArray("uBones[0]", instance.boneMatrices.data(), numBones);
characterShader->setUniform(uniformName, instance.boneMatrices[i]);
} }
// Bind VAO and draw // Bind VAO and draw

View file

@ -7,6 +7,8 @@
#include "core/logger.hpp" #include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp> #include <glm/gtc/type_ptr.hpp>
#include <unordered_set>
#include <algorithm>
namespace wowee { namespace wowee {
namespace rendering { namespace rendering {
@ -53,7 +55,8 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
void main() { void main() {
vec4 worldPos = uModel * vec4(aPos, 1.0); vec4 worldPos = uModel * vec4(aPos, 1.0);
FragPos = worldPos.xyz; FragPos = worldPos.xyz;
Normal = mat3(transpose(inverse(uModel))) * aNormal; // Use mat3(uModel) directly - avoids expensive inverse() per vertex
Normal = mat3(uModel) * aNormal;
TexCoord = aTexCoord; TexCoord = aTexCoord;
gl_Position = uProjection * uView * worldPos; gl_Position = uProjection * uView * worldPos;
@ -340,6 +343,11 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
lastDrawCallCount = 0; lastDrawCallCount = 0;
// Distance-based culling threshold for M2 models
const float maxRenderDistance = 500.0f; // Don't render small doodads beyond this
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
const glm::vec3 camPos = camera.getPosition();
for (const auto& instance : instances) { for (const auto& instance : instances) {
auto it = models.find(instance.modelId); auto it = models.find(instance.modelId);
if (it == models.end()) continue; if (it == models.end()) continue;
@ -347,8 +355,17 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
const M2ModelGPU& model = it->second; const M2ModelGPU& model = it->second;
if (!model.isValid()) continue; if (!model.isValid()) continue;
// Frustum cull: test bounding sphere in world space // Distance culling for small objects (scaled by object size)
glm::vec3 toCam = instance.position - camPos;
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
float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, worldRadius / 10.0f);
if (distSq > effectiveMaxDistSq) {
continue;
}
// Frustum cull: test bounding sphere in world space
if (worldRadius > 0.0f && !frustum.intersectsSphere(instance.position, worldRadius)) { if (worldRadius > 0.0f && !frustum.intersectsSphere(instance.position, worldRadius)) {
continue; continue;
} }
@ -414,6 +431,37 @@ void M2Renderer::clear() {
instances.clear(); instances.clear();
} }
void M2Renderer::cleanupUnusedModels() {
// Build set of model IDs that are still referenced by instances
std::unordered_set<uint32_t> usedModelIds;
for (const auto& instance : instances) {
usedModelIds.insert(instance.modelId);
}
// Find and remove models with no instances
std::vector<uint32_t> toRemove;
for (const auto& [id, model] : models) {
if (usedModelIds.find(id) == usedModelIds.end()) {
toRemove.push_back(id);
}
}
// Delete GPU resources and remove from map
for (uint32_t id : toRemove) {
auto it = models.find(id);
if (it != models.end()) {
if (it->second.vao != 0) glDeleteVertexArrays(1, &it->second.vao);
if (it->second.vbo != 0) glDeleteBuffers(1, &it->second.vbo);
if (it->second.ebo != 0) glDeleteBuffers(1, &it->second.ebo);
models.erase(it);
}
}
if (!toRemove.empty()) {
LOG_INFO("M2 cleanup: removed ", toRemove.size(), " unused models, ", models.size(), " remaining");
}
}
GLuint M2Renderer::loadTexture(const std::string& path) { GLuint M2Renderer::loadTexture(const std::string& path) {
// Check cache // Check cache
auto it = textureCache.find(path); auto it = textureCache.find(path);
@ -462,5 +510,60 @@ uint32_t M2Renderer::getTotalTriangleCount() const {
return total; return total;
} }
bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to,
glm::vec3& adjustedPos, float playerRadius) const {
adjustedPos = to;
bool collided = false;
// Check against all M2 instances using their bounding boxes
for (const auto& instance : instances) {
auto it = models.find(instance.modelId);
if (it == models.end()) continue;
const M2ModelGPU& model = it->second;
// Transform model bounds to world space (approximate with scaled AABB)
glm::vec3 worldMin = instance.position + model.boundMin * instance.scale;
glm::vec3 worldMax = instance.position + model.boundMax * instance.scale;
// Ensure min/max are correct
glm::vec3 actualMin = glm::min(worldMin, worldMax);
glm::vec3 actualMax = glm::max(worldMin, worldMax);
// Expand bounds by player radius
actualMin -= glm::vec3(playerRadius);
actualMax += glm::vec3(playerRadius);
// Check if player position is inside expanded bounds (XY only for walking)
if (adjustedPos.x >= actualMin.x && adjustedPos.x <= actualMax.x &&
adjustedPos.y >= actualMin.y && adjustedPos.y <= actualMax.y &&
adjustedPos.z >= actualMin.z && adjustedPos.z <= actualMax.z) {
// Push player out of the object
// Find the shortest push direction (XY only)
float pushLeft = adjustedPos.x - actualMin.x;
float pushRight = actualMax.x - adjustedPos.x;
float pushBack = adjustedPos.y - actualMin.y;
float pushFront = actualMax.y - adjustedPos.y;
float minPush = std::min({pushLeft, pushRight, pushBack, pushFront});
if (minPush == pushLeft) {
adjustedPos.x = actualMin.x - 0.01f;
} else if (minPush == pushRight) {
adjustedPos.x = actualMax.x + 0.01f;
} else if (minPush == pushBack) {
adjustedPos.y = actualMin.y - 0.01f;
} else {
adjustedPos.y = actualMax.y + 0.01f;
}
collided = true;
}
}
return collided;
}
} // namespace rendering } // namespace rendering
} // namespace wowee } // namespace wowee

View file

@ -84,7 +84,7 @@ bool Renderer::initialize(core::Window* win) {
// Create camera controller // Create camera controller
cameraController = std::make_unique<CameraController>(camera.get()); cameraController = std::make_unique<CameraController>(camera.get());
cameraController->setMovementSpeed(100.0f); // Fast movement for terrain exploration cameraController->setUseWoWSpeed(true); // Use realistic WoW movement speed
cameraController->setMouseSensitivity(0.15f); cameraController->setMouseSensitivity(0.15f);
// Create scene // Create scene
@ -767,6 +767,9 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
if (wmoRenderer) { if (wmoRenderer) {
cameraController->setWMORenderer(wmoRenderer.get()); cameraController->setWMORenderer(wmoRenderer.get());
} }
if (m2Renderer) {
cameraController->setM2Renderer(m2Renderer.get());
}
if (waterRenderer) { if (waterRenderer) {
cameraController->setWaterRenderer(waterRenderer.get()); cameraController->setWaterRenderer(waterRenderer.get());
} }
@ -876,10 +879,13 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent
} }
} }
// Wire WMO and water renderer to camera controller // Wire WMO, M2, and water renderer to camera controller
if (cameraController && wmoRenderer) { if (cameraController && wmoRenderer) {
cameraController->setWMORenderer(wmoRenderer.get()); cameraController->setWMORenderer(wmoRenderer.get());
} }
if (cameraController && m2Renderer) {
cameraController->setM2Renderer(m2Renderer.get());
}
if (cameraController && waterRenderer) { if (cameraController && waterRenderer) {
cameraController->setWaterRenderer(waterRenderer.get()); cameraController->setWaterRenderer(waterRenderer.get());
} }

View file

@ -92,7 +92,16 @@ void Shader::unuse() const {
} }
GLint Shader::getUniformLocation(const std::string& name) const { GLint Shader::getUniformLocation(const std::string& name) const {
return glGetUniformLocation(program, name.c_str()); // Check cache first
auto it = uniformLocationCache.find(name);
if (it != uniformLocationCache.end()) {
return it->second;
}
// Look up and cache
GLint location = glGetUniformLocation(program, name.c_str());
uniformLocationCache[name] = location;
return location;
} }
void Shader::setUniform(const std::string& name, int value) { void Shader::setUniform(const std::string& name, int value) {
@ -123,5 +132,9 @@ void Shader::setUniform(const std::string& name, const glm::mat4& value) {
glUniformMatrix4fv(getUniformLocation(name), 1, GL_FALSE, &value[0][0]); glUniformMatrix4fv(getUniformLocation(name), 1, GL_FALSE, &value[0][0]);
} }
void Shader::setUniformMatrixArray(const std::string& name, const glm::mat4* matrices, int count) {
glUniformMatrix4fv(getUniformLocation(name), count, GL_FALSE, &matrices[0][0][0]);
}
} // namespace rendering } // namespace rendering
} // namespace wowee } // namespace wowee

View file

@ -627,6 +627,14 @@ void TerrainManager::unloadTile(int x, int y) {
} }
loadedTiles.erase(it); loadedTiles.erase(it);
// Clean up any models that are no longer referenced
if (m2Renderer) {
m2Renderer->cleanupUnusedModels();
}
if (wmoRenderer) {
wmoRenderer->cleanupUnusedModels();
}
} }
void TerrainManager::unloadAll() { void TerrainManager::unloadAll() {

View file

@ -44,7 +44,8 @@ bool WaterRenderer::initialize() {
vec3 pos = aPos; vec3 pos = aPos;
FragPos = vec3(model * vec4(pos, 1.0)); FragPos = vec3(model * vec4(pos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal; // Use mat3(model) directly - avoids expensive inverse() per vertex
Normal = mat3(model) * aNormal;
TexCoord = aTexCoord; TexCoord = aTexCoord;
WaveOffset = 0.0; WaveOffset = 0.0;

View file

@ -1,6 +1,7 @@
#include "rendering/wmo_renderer.hpp" #include "rendering/wmo_renderer.hpp"
#include "rendering/shader.hpp" #include "rendering/shader.hpp"
#include "rendering/camera.hpp" #include "rendering/camera.hpp"
#include "rendering/frustum.hpp"
#include "pipeline/wmo_loader.hpp" #include "pipeline/wmo_loader.hpp"
#include "pipeline/asset_manager.hpp" #include "pipeline/asset_manager.hpp"
#include "core/logger.hpp" #include "core/logger.hpp"
@ -8,6 +9,7 @@
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp> #include <glm/gtc/type_ptr.hpp>
#include <algorithm> #include <algorithm>
#include <unordered_set>
namespace wowee { namespace wowee {
namespace rendering { namespace rendering {
@ -44,7 +46,9 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) {
void main() { void main() {
vec4 worldPos = uModel * vec4(aPos, 1.0); vec4 worldPos = uModel * vec4(aPos, 1.0);
FragPos = worldPos.xyz; FragPos = worldPos.xyz;
Normal = mat3(transpose(inverse(uModel))) * aNormal; // Use mat3(uModel) directly - avoids expensive inverse() per vertex
// This works correctly for uniform scale transforms
Normal = mat3(uModel) * aNormal;
TexCoord = aTexCoord; TexCoord = aTexCoord;
VertexColor = aColor; VertexColor = aColor;
@ -257,6 +261,31 @@ void WMORenderer::unloadModel(uint32_t id) {
core::Logger::getInstance().info("WMO model ", id, " unloaded"); core::Logger::getInstance().info("WMO model ", id, " unloaded");
} }
void WMORenderer::cleanupUnusedModels() {
// Build set of model IDs that are still referenced by instances
std::unordered_set<uint32_t> usedModelIds;
for (const auto& instance : instances) {
usedModelIds.insert(instance.modelId);
}
// Find and remove models with no instances
std::vector<uint32_t> toRemove;
for (const auto& [id, model] : loadedModels) {
if (usedModelIds.find(id) == usedModelIds.end()) {
toRemove.push_back(id);
}
}
// Delete GPU resources and remove from map
for (uint32_t id : toRemove) {
unloadModel(id);
}
if (!toRemove.empty()) {
core::Logger::getInstance().info("WMO cleanup: removed ", toRemove.size(), " unused models, ", loadedModels.size(), " remaining");
}
}
uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position, uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position,
const glm::vec3& rotation, float scale) { const glm::vec3& rotation, float scale) {
// Check if model is loaded // Check if model is loaded
@ -319,8 +348,23 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm:
// Disable backface culling for WMOs (some faces may have wrong winding) // Disable backface culling for WMOs (some faces may have wrong winding)
glDisable(GL_CULL_FACE); glDisable(GL_CULL_FACE);
// Render all instances // Extract frustum planes for proper culling
Frustum frustum;
frustum.extractFromMatrix(projection * view);
// Render all instances with instance-level culling
const glm::vec3 camPos = camera.getPosition();
const float maxRenderDistance = 1500.0f; // Don't render WMOs beyond this distance
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
for (const auto& instance : instances) { for (const auto& instance : instances) {
// Instance-level distance culling
glm::vec3 toCam = instance.position - camPos;
float distSq = glm::dot(toCam, toCam);
if (distSq > maxRenderDistanceSq) {
continue; // Skip instances that are too far
}
auto modelIt = loadedModels.find(instance.modelId); auto modelIt = loadedModels.find(instance.modelId);
if (modelIt == loadedModels.end()) { if (modelIt == loadedModels.end()) {
continue; continue;
@ -331,9 +375,17 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm:
// Render all groups // Render all groups
for (const auto& group : model.groups) { for (const auto& group : model.groups) {
// Frustum culling // Proper frustum culling using AABB test
if (frustumCulling && !isGroupVisible(group, instance.modelMatrix, camera)) { if (frustumCulling) {
continue; // Transform group bounding box to world space
glm::vec3 worldMin = glm::vec3(instance.modelMatrix * glm::vec4(group.boundingBoxMin, 1.0f));
glm::vec3 worldMax = glm::vec3(instance.modelMatrix * glm::vec4(group.boundingBoxMax, 1.0f));
// Ensure min/max are correct after transform (rotation can swap them)
glm::vec3 actualMin = glm::min(worldMin, worldMax);
glm::vec3 actualMax = glm::max(worldMin, worldMax);
if (!frustum.intersectsAABB(actualMin, actualMax)) {
continue;
}
} }
renderGroup(group, model, instance.modelMatrix, view, projection); renderGroup(group, model, instance.modelMatrix, view, projection);
@ -727,8 +779,8 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
float moveDistXY = glm::length(glm::vec2(moveDir.x, moveDir.y)); float moveDistXY = glm::length(glm::vec2(moveDir.x, moveDir.y));
if (moveDistXY < 0.001f) return false; if (moveDistXY < 0.001f) return false;
// Player collision radius // Player collision radius (WoW character is about 0.5 yards wide)
const float PLAYER_RADIUS = 2.5f; const float PLAYER_RADIUS = 0.5f;
for (const auto& instance : instances) { for (const auto& instance : instances) {
auto it = loadedModels.find(instance.modelId); auto it = loadedModels.find(instance.modelId);

View file

@ -548,7 +548,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
chatText = emoteText; chatText = emoteText;
} }
} else { } else {
chatText = emoteText; chatText = command + "."; // First person: "You wave."
} }
// Add local chat message // Add local chat message