diff --git a/assets/shaders/basic.vert b/assets/shaders/basic.vert index ffad9598..141f2270 100644 --- a/assets/shaders/basic.vert +++ b/assets/shaders/basic.vert @@ -14,7 +14,8 @@ uniform mat4 uProjection; void main() { 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; gl_Position = uProjection * uView * vec4(FragPos, 1.0); diff --git a/assets/shaders/terrain.vert b/assets/shaders/terrain.vert index fe1408f8..f8a57ae8 100644 --- a/assets/shaders/terrain.vert +++ b/assets/shaders/terrain.vert @@ -18,8 +18,8 @@ void main() { vec4 worldPos = uModel * vec4(aPosition, 1.0); FragPos = worldPos.xyz; - // Transform normal to world space - Normal = mat3(transpose(inverse(uModel))) * aNormal; + // Terrain uses identity model matrix, so normal passes through directly + Normal = aNormal; TexCoord = aTexCoord; LayerUV = aLayerUV; diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 2772d8d9..e4d36d84 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -10,6 +10,7 @@ namespace rendering { class TerrainManager; class WMORenderer; +class M2Renderer; class WaterRenderer; class CameraController { @@ -25,6 +26,7 @@ public: void setEnabled(bool enabled) { this->enabled = enabled; } void setTerrainManager(TerrainManager* tm) { terrainManager = tm; } void setWMORenderer(WMORenderer* wmo) { wmoRenderer = wmo; } + void setM2Renderer(M2Renderer* m2) { m2Renderer = m2; } void setWaterRenderer(WaterRenderer* wr) { waterRenderer = wr; } void processMouseWheel(float delta); @@ -54,6 +56,7 @@ private: Camera* camera; TerrainManager* terrainManager = nullptr; WMORenderer* wmoRenderer = nullptr; + M2Renderer* m2Renderer = nullptr; WaterRenderer* waterRenderer = nullptr; // Stored rotation (avoids lossy forward-vector round-trip) @@ -82,7 +85,7 @@ private: // Gravity / grounding float verticalVelocity = 0.0f; 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) static constexpr float GRAVITY = -30.0f; static constexpr float JUMP_VELOCITY = 15.0f; @@ -112,11 +115,11 @@ private: // Movement callback MovementCallback movementCallback; - // WoW-correct speeds + // Movement speeds (scaled up for better feel) bool useWoWSpeed = false; - static constexpr float WOW_RUN_SPEED = 7.0f; - static constexpr float WOW_WALK_SPEED = 2.5f; - static constexpr float WOW_BACK_SPEED = 4.5f; + static constexpr float WOW_RUN_SPEED = 14.0f; // Double base WoW speed for responsiveness + static constexpr float WOW_WALK_SPEED = 5.0f; // Walk (hold Shift) + static constexpr float WOW_BACK_SPEED = 9.0f; // Backpedal static constexpr float WOW_GRAVITY = -19.29f; static constexpr float WOW_JUMP_VELOCITY = 7.96f; diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index bbeeb2fe..6735f991 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -116,6 +116,23 @@ public: */ 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 uint32_t getModelCount() const { return static_cast(models.size()); } uint32_t getInstanceCount() const { return static_cast(instances.size()); } diff --git a/include/rendering/shader.hpp b/include/rendering/shader.hpp index 6c8d736a..cb216e3d 100644 --- a/include/rendering/shader.hpp +++ b/include/rendering/shader.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -25,6 +26,7 @@ public: 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::mat4& value); + void setUniformMatrixArray(const std::string& name, const glm::mat4* matrices, int count); GLuint getProgram() const { return program; } @@ -35,6 +37,9 @@ private: GLuint program = 0; GLuint vertexShader = 0; GLuint fragmentShader = 0; + + // Cache uniform locations to avoid expensive glGetUniformLocation calls + mutable std::unordered_map uniformLocationCache; }; } // namespace rendering diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 1d4261d7..00883195 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -102,6 +102,12 @@ public: */ 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) */ diff --git a/src/core/application.cpp b/src/core/application.cpp index 647d6491..e2800f57 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -51,10 +51,10 @@ bool Application::initialize() { // Create window WindowConfig windowConfig; - windowConfig.title = "Wowser - World of Warcraft Client"; - windowConfig.width = 1920; - windowConfig.height = 1080; - windowConfig.vsync = true; + windowConfig.title = "Wowee"; + windowConfig.width = 1280; + windowConfig.height = 720; + windowConfig.vsync = false; window = std::make_unique(windowConfig); if (!window->initialize()) { diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index d4416dba..8c24743f 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -1,6 +1,7 @@ #include "rendering/camera_controller.hpp" #include "rendering/terrain_manager.hpp" #include "rendering/wmo_renderer.hpp" +#include "rendering/m2_renderer.hpp" #include "rendering/water_renderer.hpp" #include "game/opcodes.hpp" #include "core/logger.hpp" @@ -152,7 +153,7 @@ void CameraController::update(float deltaTime) { targetPos.z += verticalVelocity * deltaTime; } - // Wall collision for character + // Wall collision for character (WMO buildings) if (wmoRenderer) { glm::vec3 feetPos = targetPos; 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 { std::optional terrainH; diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 42839ccd..6fe8295b 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -72,7 +72,9 @@ bool CharacterRenderer::initialize() { vec4 worldPos = uModel * skinnedPos; 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; gl_Position = uProjection * uView * worldPos; @@ -984,11 +986,10 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons : getModelMatrix(instance); characterShader->setUniform("uModel", modelMat); - // Set bone matrices + // Set bone matrices (upload all at once for performance) int numBones = std::min(static_cast(instance.boneMatrices.size()), MAX_BONES); - for (int i = 0; i < numBones; i++) { - std::string uniformName = "uBones[" + std::to_string(i) + "]"; - characterShader->setUniform(uniformName, instance.boneMatrices[i]); + if (numBones > 0) { + characterShader->setUniformMatrixArray("uBones[0]", instance.boneMatrices.data(), numBones); } // Bind VAO and draw diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 12b6e1a4..1926bee4 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -7,6 +7,8 @@ #include "core/logger.hpp" #include #include +#include +#include namespace wowee { namespace rendering { @@ -53,7 +55,8 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { void main() { vec4 worldPos = uModel * vec4(aPos, 1.0); FragPos = worldPos.xyz; - Normal = mat3(transpose(inverse(uModel))) * aNormal; + // Use mat3(uModel) directly - avoids expensive inverse() per vertex + Normal = mat3(uModel) * aNormal; TexCoord = aTexCoord; gl_Position = uProjection * uView * worldPos; @@ -340,6 +343,11 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: 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) { auto it = models.find(instance.modelId); 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; 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; + // 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)) { continue; } @@ -414,6 +431,37 @@ void M2Renderer::clear() { instances.clear(); } +void M2Renderer::cleanupUnusedModels() { + // Build set of model IDs that are still referenced by instances + std::unordered_set usedModelIds; + for (const auto& instance : instances) { + usedModelIds.insert(instance.modelId); + } + + // Find and remove models with no instances + std::vector 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) { // Check cache auto it = textureCache.find(path); @@ -462,5 +510,60 @@ uint32_t M2Renderer::getTotalTriangleCount() const { 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 wowee diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 4b8c21e7..e3d0acf1 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -84,7 +84,7 @@ bool Renderer::initialize(core::Window* win) { // Create camera controller cameraController = std::make_unique(camera.get()); - cameraController->setMovementSpeed(100.0f); // Fast movement for terrain exploration + cameraController->setUseWoWSpeed(true); // Use realistic WoW movement speed cameraController->setMouseSensitivity(0.15f); // Create scene @@ -767,6 +767,9 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: if (wmoRenderer) { cameraController->setWMORenderer(wmoRenderer.get()); } + if (m2Renderer) { + cameraController->setM2Renderer(m2Renderer.get()); + } if (waterRenderer) { 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) { cameraController->setWMORenderer(wmoRenderer.get()); } + if (cameraController && m2Renderer) { + cameraController->setM2Renderer(m2Renderer.get()); + } if (cameraController && waterRenderer) { cameraController->setWaterRenderer(waterRenderer.get()); } diff --git a/src/rendering/shader.cpp b/src/rendering/shader.cpp index f3c22682..2a748539 100644 --- a/src/rendering/shader.cpp +++ b/src/rendering/shader.cpp @@ -92,7 +92,16 @@ void Shader::unuse() 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) { @@ -123,5 +132,9 @@ void Shader::setUniform(const std::string& name, const glm::mat4& value) { 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 wowee diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index 067befde..db235b42 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -627,6 +627,14 @@ void TerrainManager::unloadTile(int x, int y) { } loadedTiles.erase(it); + + // Clean up any models that are no longer referenced + if (m2Renderer) { + m2Renderer->cleanupUnusedModels(); + } + if (wmoRenderer) { + wmoRenderer->cleanupUnusedModels(); + } } void TerrainManager::unloadAll() { diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index 7f2c9be0..9a9c43ce 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -44,7 +44,8 @@ bool WaterRenderer::initialize() { vec3 pos = aPos; 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; WaveOffset = 0.0; diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index be6da7e6..60d7de4c 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -1,6 +1,7 @@ #include "rendering/wmo_renderer.hpp" #include "rendering/shader.hpp" #include "rendering/camera.hpp" +#include "rendering/frustum.hpp" #include "pipeline/wmo_loader.hpp" #include "pipeline/asset_manager.hpp" #include "core/logger.hpp" @@ -8,6 +9,7 @@ #include #include #include +#include namespace wowee { namespace rendering { @@ -44,7 +46,9 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) { void main() { vec4 worldPos = uModel * vec4(aPos, 1.0); 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; VertexColor = aColor; @@ -257,6 +261,31 @@ void WMORenderer::unloadModel(uint32_t id) { 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 usedModelIds; + for (const auto& instance : instances) { + usedModelIds.insert(instance.modelId); + } + + // Find and remove models with no instances + std::vector 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, const glm::vec3& rotation, float scale) { // 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) 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) { + // 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); if (modelIt == loadedModels.end()) { continue; @@ -331,9 +375,17 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: // Render all groups for (const auto& group : model.groups) { - // Frustum culling - if (frustumCulling && !isGroupVisible(group, instance.modelMatrix, camera)) { - continue; + // Proper frustum culling using AABB test + if (frustumCulling) { + // 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); @@ -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)); if (moveDistXY < 0.001f) return false; - // Player collision radius - const float PLAYER_RADIUS = 2.5f; + // Player collision radius (WoW character is about 0.5 yards wide) + const float PLAYER_RADIUS = 0.5f; for (const auto& instance : instances) { auto it = loadedModels.find(instance.modelId); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 22e42046..c8e85ece 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -548,7 +548,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { chatText = emoteText; } } else { - chatText = emoteText; + chatText = command + "."; // First person: "You wave." } // Add local chat message