mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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:
parent
0c85fcd444
commit
4287878a73
16 changed files with 258 additions and 32 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()); }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue