mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 01:23:51 +00:00
Initial commit: wowee native WoW 3.3.5a client
This commit is contained in:
commit
ce6cb8f38e
147 changed files with 32347 additions and 0 deletions
56
src/rendering/camera.cpp
Normal file
56
src/rendering/camera.cpp
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#include "rendering/camera.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/constants.hpp>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
Camera::Camera() {
|
||||
updateViewMatrix();
|
||||
updateProjectionMatrix();
|
||||
}
|
||||
|
||||
void Camera::updateViewMatrix() {
|
||||
glm::vec3 front = getForward();
|
||||
// Use Z-up for WoW coordinate system
|
||||
viewMatrix = glm::lookAt(position, position + front, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
void Camera::updateProjectionMatrix() {
|
||||
projectionMatrix = glm::perspective(glm::radians(fov), aspectRatio, nearPlane, farPlane);
|
||||
}
|
||||
|
||||
glm::vec3 Camera::getForward() const {
|
||||
// WoW coordinate system: X/Y horizontal, Z vertical
|
||||
glm::vec3 front;
|
||||
front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
|
||||
front.y = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
|
||||
front.z = sin(glm::radians(pitch));
|
||||
return glm::normalize(front);
|
||||
}
|
||||
|
||||
glm::vec3 Camera::getRight() const {
|
||||
// Use Z-up for WoW coordinate system
|
||||
return glm::normalize(glm::cross(getForward(), glm::vec3(0.0f, 0.0f, 1.0f)));
|
||||
}
|
||||
|
||||
glm::vec3 Camera::getUp() const {
|
||||
return glm::normalize(glm::cross(getRight(), getForward()));
|
||||
}
|
||||
|
||||
Ray Camera::screenToWorldRay(float screenX, float screenY, float screenW, float screenH) const {
|
||||
float ndcX = (2.0f * screenX / screenW) - 1.0f;
|
||||
float ndcY = 1.0f - (2.0f * screenY / screenH);
|
||||
|
||||
glm::mat4 invVP = glm::inverse(projectionMatrix * viewMatrix);
|
||||
|
||||
glm::vec4 nearPt = invVP * glm::vec4(ndcX, ndcY, -1.0f, 1.0f);
|
||||
glm::vec4 farPt = invVP * glm::vec4(ndcX, ndcY, 1.0f, 1.0f);
|
||||
nearPt /= nearPt.w;
|
||||
farPt /= farPt.w;
|
||||
|
||||
return { glm::vec3(nearPt), glm::normalize(glm::vec3(farPt - nearPt)) };
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
525
src/rendering/camera_controller.cpp
Normal file
525
src/rendering/camera_controller.cpp
Normal file
|
|
@ -0,0 +1,525 @@
|
|||
#include "rendering/camera_controller.hpp"
|
||||
#include "rendering/terrain_manager.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/water_renderer.hpp"
|
||||
#include "game/opcodes.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/glm.hpp>
|
||||
#include <imgui.h>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
CameraController::CameraController(Camera* cam) : camera(cam) {
|
||||
yaw = defaultYaw;
|
||||
pitch = defaultPitch;
|
||||
reset();
|
||||
}
|
||||
|
||||
void CameraController::update(float deltaTime) {
|
||||
if (!enabled || !camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& input = core::Input::getInstance();
|
||||
|
||||
// Don't process keyboard input when UI (e.g. chat box) has focus
|
||||
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||
|
||||
// Determine current key states
|
||||
bool nowForward = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_W);
|
||||
bool nowBackward = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_S);
|
||||
bool nowStrafeLeft = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_A);
|
||||
bool nowStrafeRight = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_D);
|
||||
bool nowJump = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_SPACE);
|
||||
|
||||
// Select physics constants based on mode
|
||||
float gravity = useWoWSpeed ? WOW_GRAVITY : GRAVITY;
|
||||
float jumpVel = useWoWSpeed ? WOW_JUMP_VELOCITY : JUMP_VELOCITY;
|
||||
|
||||
// Calculate movement speed based on direction and modifiers
|
||||
float speed;
|
||||
if (useWoWSpeed) {
|
||||
// WoW-correct speeds
|
||||
if (nowBackward && !nowForward) {
|
||||
speed = WOW_BACK_SPEED;
|
||||
} else if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) {
|
||||
speed = WOW_WALK_SPEED; // Shift = walk in WoW mode
|
||||
} else {
|
||||
speed = WOW_RUN_SPEED;
|
||||
}
|
||||
} else {
|
||||
// Exploration mode (original behavior)
|
||||
speed = movementSpeed;
|
||||
if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) {
|
||||
speed *= sprintMultiplier;
|
||||
}
|
||||
if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL))) {
|
||||
speed *= slowMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
// Get camera axes — project forward onto XY plane for walking
|
||||
glm::vec3 forward3D = camera->getForward();
|
||||
glm::vec3 forward = glm::normalize(glm::vec3(forward3D.x, forward3D.y, 0.0f));
|
||||
glm::vec3 right = camera->getRight();
|
||||
right.z = 0.0f;
|
||||
if (glm::length(right) > 0.001f) {
|
||||
right = glm::normalize(right);
|
||||
}
|
||||
|
||||
// Toggle sit 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;
|
||||
}
|
||||
xKeyWasDown = xDown;
|
||||
|
||||
// Calculate horizontal movement vector
|
||||
glm::vec3 movement(0.0f);
|
||||
|
||||
if (nowForward) movement += forward;
|
||||
if (nowBackward) movement -= forward;
|
||||
if (nowStrafeLeft) movement -= right;
|
||||
if (nowStrafeRight) movement += right;
|
||||
|
||||
// Stand up if any movement key is pressed while sitting
|
||||
if (!uiWantsKeyboard && sitting && (input.isKeyPressed(SDL_SCANCODE_W) || input.isKeyPressed(SDL_SCANCODE_S) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_A) || input.isKeyPressed(SDL_SCANCODE_D) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_SPACE))) {
|
||||
sitting = false;
|
||||
}
|
||||
|
||||
// Third-person orbit camera mode
|
||||
if (thirdPerson && followTarget) {
|
||||
// Move the follow target (character position) instead of the camera
|
||||
glm::vec3 targetPos = *followTarget;
|
||||
|
||||
// Check for water at current position
|
||||
std::optional<float> waterH;
|
||||
if (waterRenderer) {
|
||||
waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y);
|
||||
}
|
||||
bool inWater = waterH && targetPos.z < *waterH;
|
||||
|
||||
|
||||
if (inWater) {
|
||||
swimming = true;
|
||||
// Reduce horizontal speed while swimming
|
||||
float swimSpeed = speed * SWIM_SPEED_FACTOR;
|
||||
|
||||
if (glm::length(movement) > 0.001f) {
|
||||
movement = glm::normalize(movement);
|
||||
targetPos += movement * swimSpeed * deltaTime;
|
||||
}
|
||||
|
||||
// Spacebar = swim up (continuous, not a jump)
|
||||
if (nowJump) {
|
||||
verticalVelocity = SWIM_BUOYANCY;
|
||||
} else {
|
||||
// Gentle sink when not pressing space
|
||||
verticalVelocity += SWIM_GRAVITY * deltaTime;
|
||||
if (verticalVelocity < SWIM_SINK_SPEED) {
|
||||
verticalVelocity = SWIM_SINK_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
targetPos.z += verticalVelocity * deltaTime;
|
||||
|
||||
// Don't rise above water surface
|
||||
if (waterH && targetPos.z > *waterH - WATER_SURFACE_OFFSET) {
|
||||
targetPos.z = *waterH - WATER_SURFACE_OFFSET;
|
||||
if (verticalVelocity > 0.0f) verticalVelocity = 0.0f;
|
||||
}
|
||||
|
||||
grounded = false;
|
||||
} else {
|
||||
swimming = false;
|
||||
|
||||
if (glm::length(movement) > 0.001f) {
|
||||
movement = glm::normalize(movement);
|
||||
targetPos += movement * speed * deltaTime;
|
||||
}
|
||||
|
||||
// Jump
|
||||
if (nowJump && grounded) {
|
||||
verticalVelocity = jumpVel;
|
||||
grounded = false;
|
||||
}
|
||||
|
||||
// Apply gravity
|
||||
verticalVelocity += gravity * deltaTime;
|
||||
targetPos.z += verticalVelocity * deltaTime;
|
||||
}
|
||||
|
||||
// Wall collision for character
|
||||
if (wmoRenderer) {
|
||||
glm::vec3 feetPos = targetPos;
|
||||
glm::vec3 oldFeetPos = *followTarget;
|
||||
glm::vec3 adjusted;
|
||||
if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) {
|
||||
targetPos.x = adjusted.x;
|
||||
targetPos.y = adjusted.y;
|
||||
targetPos.z = adjusted.z;
|
||||
}
|
||||
}
|
||||
|
||||
// Ground the character to terrain or WMO floor
|
||||
{
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + eyeHeight);
|
||||
}
|
||||
|
||||
std::optional<float> groundH;
|
||||
if (terrainH && wmoH) {
|
||||
groundH = std::max(*terrainH, *wmoH);
|
||||
} else if (terrainH) {
|
||||
groundH = terrainH;
|
||||
} else if (wmoH) {
|
||||
groundH = wmoH;
|
||||
}
|
||||
|
||||
if (groundH) {
|
||||
lastGroundZ = *groundH;
|
||||
if (targetPos.z <= *groundH) {
|
||||
targetPos.z = *groundH;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
swimming = false; // Touching ground = wading, not swimming
|
||||
} else if (!swimming) {
|
||||
grounded = false;
|
||||
}
|
||||
} else if (!swimming) {
|
||||
// No terrain found — hold at last known ground
|
||||
targetPos.z = lastGroundZ;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update follow target position
|
||||
*followTarget = targetPos;
|
||||
|
||||
// Compute camera position orbiting behind the character
|
||||
glm::vec3 lookAtPoint = targetPos + glm::vec3(0.0f, 0.0f, eyeHeight);
|
||||
glm::vec3 camPos = lookAtPoint - forward3D * orbitDistance;
|
||||
|
||||
// Clamp camera above terrain/WMO floor
|
||||
{
|
||||
float minCamZ = camPos.z;
|
||||
if (terrainManager) {
|
||||
auto h = terrainManager->getHeightAt(camPos.x, camPos.y);
|
||||
if (h) minCamZ = *h + 1.0f; // 1 unit above ground
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
auto wh = wmoRenderer->getFloorHeight(camPos.x, camPos.y, camPos.z + eyeHeight);
|
||||
if (wh && (*wh + 1.0f) > minCamZ) minCamZ = *wh + 1.0f;
|
||||
}
|
||||
if (camPos.z < minCamZ) {
|
||||
camPos.z = minCamZ;
|
||||
}
|
||||
}
|
||||
|
||||
camera->setPosition(camPos);
|
||||
} else {
|
||||
// Free-fly camera mode (original behavior)
|
||||
glm::vec3 newPos = camera->getPosition();
|
||||
float feetZ = newPos.z - eyeHeight;
|
||||
|
||||
// Check for water at feet position
|
||||
std::optional<float> waterH;
|
||||
if (waterRenderer) {
|
||||
waterH = waterRenderer->getWaterHeightAt(newPos.x, newPos.y);
|
||||
}
|
||||
bool inWater = waterH && feetZ < *waterH;
|
||||
|
||||
|
||||
if (inWater) {
|
||||
swimming = true;
|
||||
float swimSpeed = speed * SWIM_SPEED_FACTOR;
|
||||
|
||||
if (glm::length(movement) > 0.001f) {
|
||||
movement = glm::normalize(movement);
|
||||
newPos += movement * swimSpeed * deltaTime;
|
||||
}
|
||||
|
||||
if (nowJump) {
|
||||
verticalVelocity = SWIM_BUOYANCY;
|
||||
} else {
|
||||
verticalVelocity += SWIM_GRAVITY * deltaTime;
|
||||
if (verticalVelocity < SWIM_SINK_SPEED) {
|
||||
verticalVelocity = SWIM_SINK_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
newPos.z += verticalVelocity * deltaTime;
|
||||
|
||||
// Don't rise above water surface (feet at water level)
|
||||
if (waterH && (newPos.z - eyeHeight) > *waterH - WATER_SURFACE_OFFSET) {
|
||||
newPos.z = *waterH - WATER_SURFACE_OFFSET + eyeHeight;
|
||||
if (verticalVelocity > 0.0f) verticalVelocity = 0.0f;
|
||||
}
|
||||
|
||||
grounded = false;
|
||||
} else {
|
||||
swimming = false;
|
||||
|
||||
if (glm::length(movement) > 0.001f) {
|
||||
movement = glm::normalize(movement);
|
||||
newPos += movement * speed * deltaTime;
|
||||
}
|
||||
|
||||
// Jump
|
||||
if (nowJump && grounded) {
|
||||
verticalVelocity = jumpVel;
|
||||
grounded = false;
|
||||
}
|
||||
|
||||
// Apply gravity
|
||||
verticalVelocity += gravity * deltaTime;
|
||||
newPos.z += verticalVelocity * deltaTime;
|
||||
}
|
||||
|
||||
// Wall collision — push out of WMO walls before grounding
|
||||
if (wmoRenderer) {
|
||||
glm::vec3 feetPos = newPos - glm::vec3(0, 0, eyeHeight);
|
||||
glm::vec3 oldFeetPos = camera->getPosition() - glm::vec3(0, 0, eyeHeight);
|
||||
glm::vec3 adjusted;
|
||||
if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) {
|
||||
newPos.x = adjusted.x;
|
||||
newPos.y = adjusted.y;
|
||||
newPos.z = adjusted.z + eyeHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Ground to terrain or WMO floor
|
||||
{
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(newPos.x, newPos.y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(newPos.x, newPos.y, newPos.z);
|
||||
}
|
||||
|
||||
std::optional<float> groundH;
|
||||
if (terrainH && wmoH) {
|
||||
groundH = std::max(*terrainH, *wmoH);
|
||||
} else if (terrainH) {
|
||||
groundH = terrainH;
|
||||
} else if (wmoH) {
|
||||
groundH = wmoH;
|
||||
}
|
||||
|
||||
if (groundH) {
|
||||
lastGroundZ = *groundH;
|
||||
float groundZ = *groundH + eyeHeight;
|
||||
if (newPos.z <= groundZ) {
|
||||
newPos.z = groundZ;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
swimming = false; // Touching ground = wading
|
||||
} else if (!swimming) {
|
||||
grounded = false;
|
||||
}
|
||||
} else if (!swimming) {
|
||||
float groundZ = lastGroundZ + eyeHeight;
|
||||
newPos.z = groundZ;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
}
|
||||
}
|
||||
|
||||
camera->setPosition(newPos);
|
||||
}
|
||||
|
||||
// --- Edge-detection: send movement opcodes on state transitions ---
|
||||
if (movementCallback) {
|
||||
// Forward/backward
|
||||
if (nowForward && !wasMovingForward) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_FORWARD));
|
||||
}
|
||||
if (nowBackward && !wasMovingBackward) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_BACKWARD));
|
||||
}
|
||||
if ((!nowForward && wasMovingForward) || (!nowBackward && wasMovingBackward)) {
|
||||
if (!nowForward && !nowBackward) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_STOP));
|
||||
}
|
||||
}
|
||||
|
||||
// Strafing
|
||||
if (nowStrafeLeft && !wasStrafingLeft) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_STRAFE_LEFT));
|
||||
}
|
||||
if (nowStrafeRight && !wasStrafingRight) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_STRAFE_RIGHT));
|
||||
}
|
||||
if ((!nowStrafeLeft && wasStrafingLeft) || (!nowStrafeRight && wasStrafingRight)) {
|
||||
if (!nowStrafeLeft && !nowStrafeRight) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_STOP_STRAFE));
|
||||
}
|
||||
}
|
||||
|
||||
// Jump
|
||||
if (nowJump && !wasJumping && grounded) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_JUMP));
|
||||
}
|
||||
|
||||
// Fall landing
|
||||
if (wasFalling && grounded) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_FALL_LAND));
|
||||
}
|
||||
}
|
||||
|
||||
// Swimming state transitions
|
||||
if (movementCallback) {
|
||||
if (swimming && !wasSwimming) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_SWIM));
|
||||
} else if (!swimming && wasSwimming) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_STOP_SWIM));
|
||||
}
|
||||
}
|
||||
|
||||
// Update previous-frame state
|
||||
wasSwimming = swimming;
|
||||
wasMovingForward = nowForward;
|
||||
wasMovingBackward = nowBackward;
|
||||
wasStrafingLeft = nowStrafeLeft;
|
||||
wasStrafingRight = nowStrafeRight;
|
||||
wasJumping = nowJump;
|
||||
wasFalling = !grounded && verticalVelocity <= 0.0f;
|
||||
|
||||
// Reset camera (R key)
|
||||
if (!uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R)) {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
|
||||
if (!enabled || !camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mouseButtonDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Directly update stored yaw/pitch (no lossy forward-vector derivation)
|
||||
yaw -= event.xrel * mouseSensitivity;
|
||||
pitch += event.yrel * mouseSensitivity;
|
||||
|
||||
pitch = glm::clamp(pitch, -89.0f, 89.0f);
|
||||
|
||||
camera->setRotation(yaw, pitch);
|
||||
}
|
||||
|
||||
void CameraController::processMouseButton(const SDL_MouseButtonEvent& event) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.button == SDL_BUTTON_LEFT) {
|
||||
leftMouseDown = (event.state == SDL_PRESSED);
|
||||
}
|
||||
if (event.button == SDL_BUTTON_RIGHT) {
|
||||
rightMouseDown = (event.state == SDL_PRESSED);
|
||||
}
|
||||
|
||||
bool anyDown = leftMouseDown || rightMouseDown;
|
||||
if (anyDown && !mouseButtonDown) {
|
||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||
} else if (!anyDown && mouseButtonDown) {
|
||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||
}
|
||||
mouseButtonDown = anyDown;
|
||||
}
|
||||
|
||||
void CameraController::reset() {
|
||||
if (!camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
yaw = defaultYaw;
|
||||
pitch = defaultPitch;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = false;
|
||||
|
||||
glm::vec3 spawnPos = defaultPosition;
|
||||
|
||||
// Snap spawn to terrain or WMO surface
|
||||
std::optional<float> h;
|
||||
if (terrainManager) {
|
||||
h = terrainManager->getHeightAt(spawnPos.x, spawnPos.y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
auto wh = wmoRenderer->getFloorHeight(spawnPos.x, spawnPos.y, spawnPos.z);
|
||||
if (wh && (!h || *wh > *h)) {
|
||||
h = wh;
|
||||
}
|
||||
}
|
||||
if (h) {
|
||||
lastGroundZ = *h;
|
||||
spawnPos.z = *h + eyeHeight;
|
||||
}
|
||||
|
||||
camera->setPosition(spawnPos);
|
||||
camera->setRotation(yaw, pitch);
|
||||
|
||||
LOG_INFO("Camera reset to default position");
|
||||
}
|
||||
|
||||
void CameraController::processMouseWheel(float delta) {
|
||||
orbitDistance -= delta * zoomSpeed;
|
||||
orbitDistance = glm::clamp(orbitDistance, minOrbitDistance, maxOrbitDistance);
|
||||
}
|
||||
|
||||
void CameraController::setFollowTarget(glm::vec3* target) {
|
||||
followTarget = target;
|
||||
if (target) {
|
||||
thirdPerson = true;
|
||||
LOG_INFO("Third-person camera enabled");
|
||||
} else {
|
||||
thirdPerson = false;
|
||||
LOG_INFO("Free-fly camera enabled");
|
||||
}
|
||||
}
|
||||
|
||||
bool CameraController::isMoving() const {
|
||||
if (!enabled || !camera) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ImGui::GetIO().WantCaptureKeyboard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& input = core::Input::getInstance();
|
||||
|
||||
return input.isKeyPressed(SDL_SCANCODE_W) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_S) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_A) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_D);
|
||||
}
|
||||
|
||||
bool CameraController::isSprinting() const {
|
||||
if (!enabled || !camera) {
|
||||
return false;
|
||||
}
|
||||
if (ImGui::GetIO().WantCaptureKeyboard) {
|
||||
return false;
|
||||
}
|
||||
auto& input = core::Input::getInstance();
|
||||
return isMoving() && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT));
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
412
src/rendering/celestial.cpp
Normal file
412
src/rendering/celestial.cpp
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
#include "rendering/celestial.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
Celestial::Celestial() = default;
|
||||
|
||||
Celestial::~Celestial() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool Celestial::initialize() {
|
||||
LOG_INFO("Initializing celestial renderer");
|
||||
|
||||
// Create celestial shader
|
||||
celestialShader = std::make_unique<Shader>();
|
||||
|
||||
// Vertex shader - billboard facing camera
|
||||
const char* vertexShaderSource = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in vec2 aTexCoord;
|
||||
|
||||
uniform mat4 model;
|
||||
uniform mat4 view;
|
||||
uniform mat4 projection;
|
||||
|
||||
out vec2 TexCoord;
|
||||
|
||||
void main() {
|
||||
TexCoord = aTexCoord;
|
||||
|
||||
// Billboard: remove rotation from view matrix, keep only translation
|
||||
mat4 viewNoRotation = view;
|
||||
viewNoRotation[0][0] = 1.0; viewNoRotation[0][1] = 0.0; viewNoRotation[0][2] = 0.0;
|
||||
viewNoRotation[1][0] = 0.0; viewNoRotation[1][1] = 1.0; viewNoRotation[1][2] = 0.0;
|
||||
viewNoRotation[2][0] = 0.0; viewNoRotation[2][1] = 0.0; viewNoRotation[2][2] = 1.0;
|
||||
|
||||
gl_Position = projection * viewNoRotation * model * vec4(aPos, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Fragment shader - disc with glow and moon phase support
|
||||
const char* fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
in vec2 TexCoord;
|
||||
|
||||
uniform vec3 celestialColor;
|
||||
uniform float intensity;
|
||||
uniform float moonPhase; // 0.0 = new moon, 0.5 = full moon, 1.0 = new moon
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
// Create circular disc
|
||||
vec2 center = vec2(0.5, 0.5);
|
||||
float dist = distance(TexCoord, center);
|
||||
|
||||
// Core disc
|
||||
float disc = smoothstep(0.5, 0.4, dist);
|
||||
|
||||
// Glow around disc
|
||||
float glow = smoothstep(0.7, 0.0, dist) * 0.3;
|
||||
|
||||
float alpha = (disc + glow) * intensity;
|
||||
|
||||
// Apply moon phase shadow (only for moon, indicated by low intensity)
|
||||
if (intensity < 0.5) { // Moon has lower intensity than sun
|
||||
// Calculate phase position (-1 to 1, where 0 is center)
|
||||
float phasePos = (moonPhase - 0.5) * 2.0;
|
||||
|
||||
// Distance from phase terminator line
|
||||
float x = (TexCoord.x - 0.5) * 2.0; // -1 to 1
|
||||
|
||||
// Create shadow using smoothstep
|
||||
float shadow = 1.0;
|
||||
|
||||
if (moonPhase < 0.5) {
|
||||
// Waning (right to left shadow)
|
||||
shadow = smoothstep(phasePos - 0.1, phasePos + 0.1, x);
|
||||
} else {
|
||||
// Waxing (left to right shadow)
|
||||
shadow = smoothstep(phasePos - 0.1, phasePos + 0.1, -x);
|
||||
}
|
||||
|
||||
// Apply elliptical terminator for 3D effect
|
||||
float y = (TexCoord.y - 0.5) * 2.0;
|
||||
float ellipse = sqrt(1.0 - y * y);
|
||||
float terminatorX = phasePos / ellipse;
|
||||
|
||||
if (moonPhase < 0.5) {
|
||||
shadow = smoothstep(terminatorX - 0.15, terminatorX + 0.15, x);
|
||||
} else {
|
||||
shadow = smoothstep(terminatorX - 0.15, terminatorX + 0.15, -x);
|
||||
}
|
||||
|
||||
// Darken shadowed area (not completely black, slight glow remains)
|
||||
alpha *= mix(0.05, 1.0, shadow);
|
||||
}
|
||||
|
||||
FragColor = vec4(celestialColor, alpha);
|
||||
}
|
||||
)";
|
||||
|
||||
if (!celestialShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
|
||||
LOG_ERROR("Failed to create celestial shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create billboard quad
|
||||
createCelestialQuad();
|
||||
|
||||
LOG_INFO("Celestial renderer initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Celestial::shutdown() {
|
||||
destroyCelestialQuad();
|
||||
celestialShader.reset();
|
||||
}
|
||||
|
||||
void Celestial::render(const Camera& camera, float timeOfDay) {
|
||||
if (!renderingEnabled || vao == 0 || !celestialShader) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable blending for celestial glow
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Disable depth writing (but keep depth testing)
|
||||
glDepthMask(GL_FALSE);
|
||||
|
||||
// Render sun and moon
|
||||
renderSun(camera, timeOfDay);
|
||||
renderMoon(camera, timeOfDay);
|
||||
|
||||
// Restore state
|
||||
glDepthMask(GL_TRUE);
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
void Celestial::renderSun(const Camera& camera, float timeOfDay) {
|
||||
// Sun visible from 5:00 to 19:00
|
||||
if (timeOfDay < 5.0f || timeOfDay >= 19.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
celestialShader->use();
|
||||
|
||||
// Get sun position
|
||||
glm::vec3 sunPos = getSunPosition(timeOfDay);
|
||||
|
||||
// Create model matrix
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
model = glm::translate(model, sunPos);
|
||||
model = glm::scale(model, glm::vec3(50.0f, 50.0f, 1.0f)); // 50 unit diameter
|
||||
|
||||
// Set uniforms
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 projection = camera.getProjectionMatrix();
|
||||
|
||||
celestialShader->setUniform("model", model);
|
||||
celestialShader->setUniform("view", view);
|
||||
celestialShader->setUniform("projection", projection);
|
||||
|
||||
// Sun color and intensity
|
||||
glm::vec3 color = getSunColor(timeOfDay);
|
||||
float intensity = getSunIntensity(timeOfDay);
|
||||
|
||||
celestialShader->setUniform("celestialColor", color);
|
||||
celestialShader->setUniform("intensity", intensity);
|
||||
celestialShader->setUniform("moonPhase", 0.5f); // Sun doesn't use this, but shader expects it
|
||||
|
||||
// Render quad
|
||||
glBindVertexArray(vao);
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void Celestial::renderMoon(const Camera& camera, float timeOfDay) {
|
||||
// Moon visible from 19:00 to 5:00 (night)
|
||||
if (timeOfDay >= 5.0f && timeOfDay < 19.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
celestialShader->use();
|
||||
|
||||
// Get moon position
|
||||
glm::vec3 moonPos = getMoonPosition(timeOfDay);
|
||||
|
||||
// Create model matrix
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
model = glm::translate(model, moonPos);
|
||||
model = glm::scale(model, glm::vec3(40.0f, 40.0f, 1.0f)); // 40 unit diameter (smaller than sun)
|
||||
|
||||
// Set uniforms
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 projection = camera.getProjectionMatrix();
|
||||
|
||||
celestialShader->setUniform("model", model);
|
||||
celestialShader->setUniform("view", view);
|
||||
celestialShader->setUniform("projection", projection);
|
||||
|
||||
// Moon color (pale blue-white) and intensity
|
||||
glm::vec3 color = glm::vec3(0.8f, 0.85f, 1.0f);
|
||||
|
||||
// Fade in/out at transitions
|
||||
float intensity = 1.0f;
|
||||
if (timeOfDay >= 19.0f && timeOfDay < 21.0f) {
|
||||
// Fade in (19:00-21:00)
|
||||
intensity = (timeOfDay - 19.0f) / 2.0f;
|
||||
}
|
||||
else if (timeOfDay >= 3.0f && timeOfDay < 5.0f) {
|
||||
// Fade out (3:00-5:00)
|
||||
intensity = 1.0f - (timeOfDay - 3.0f) / 2.0f;
|
||||
}
|
||||
|
||||
celestialShader->setUniform("celestialColor", color);
|
||||
celestialShader->setUniform("intensity", intensity);
|
||||
celestialShader->setUniform("moonPhase", moonPhase);
|
||||
|
||||
// Render quad
|
||||
glBindVertexArray(vao);
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
glm::vec3 Celestial::getSunPosition(float timeOfDay) const {
|
||||
// Sun rises at 6:00, peaks at 12:00, sets at 18:00
|
||||
float angle = calculateCelestialAngle(timeOfDay, 6.0f, 18.0f);
|
||||
|
||||
const float radius = 800.0f; // Distance from origin
|
||||
const float height = 600.0f; // Maximum height
|
||||
|
||||
// Arc across sky
|
||||
float x = radius * std::sin(angle);
|
||||
float z = height * std::cos(angle);
|
||||
float y = 0.0f; // Y is horizontal in WoW coordinates
|
||||
|
||||
return glm::vec3(x, y, z);
|
||||
}
|
||||
|
||||
glm::vec3 Celestial::getMoonPosition(float timeOfDay) const {
|
||||
// Moon rises at 18:00, peaks at 0:00 (24:00), sets at 6:00
|
||||
// Adjust time for moon (opposite to sun)
|
||||
float moonTime = timeOfDay + 12.0f;
|
||||
if (moonTime >= 24.0f) moonTime -= 24.0f;
|
||||
|
||||
float angle = calculateCelestialAngle(moonTime, 6.0f, 18.0f);
|
||||
|
||||
const float radius = 800.0f;
|
||||
const float height = 600.0f;
|
||||
|
||||
float x = radius * std::sin(angle);
|
||||
float z = height * std::cos(angle);
|
||||
float y = 0.0f;
|
||||
|
||||
return glm::vec3(x, y, z);
|
||||
}
|
||||
|
||||
glm::vec3 Celestial::getSunColor(float timeOfDay) const {
|
||||
// Sunrise/sunset: orange/red
|
||||
// Midday: bright yellow-white
|
||||
|
||||
if (timeOfDay >= 5.0f && timeOfDay < 7.0f) {
|
||||
// Sunrise: orange
|
||||
return glm::vec3(1.0f, 0.6f, 0.2f);
|
||||
}
|
||||
else if (timeOfDay >= 7.0f && timeOfDay < 9.0f) {
|
||||
// Morning: blend to yellow
|
||||
float t = (timeOfDay - 7.0f) / 2.0f;
|
||||
glm::vec3 orange = glm::vec3(1.0f, 0.6f, 0.2f);
|
||||
glm::vec3 yellow = glm::vec3(1.0f, 1.0f, 0.9f);
|
||||
return glm::mix(orange, yellow, t);
|
||||
}
|
||||
else if (timeOfDay >= 9.0f && timeOfDay < 16.0f) {
|
||||
// Day: bright yellow-white
|
||||
return glm::vec3(1.0f, 1.0f, 0.9f);
|
||||
}
|
||||
else if (timeOfDay >= 16.0f && timeOfDay < 18.0f) {
|
||||
// Evening: blend to orange
|
||||
float t = (timeOfDay - 16.0f) / 2.0f;
|
||||
glm::vec3 yellow = glm::vec3(1.0f, 1.0f, 0.9f);
|
||||
glm::vec3 orange = glm::vec3(1.0f, 0.5f, 0.1f);
|
||||
return glm::mix(yellow, orange, t);
|
||||
}
|
||||
else {
|
||||
// Sunset: deep orange/red
|
||||
return glm::vec3(1.0f, 0.4f, 0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
float Celestial::getSunIntensity(float timeOfDay) const {
|
||||
// Fade in at sunrise (5:00-6:00)
|
||||
if (timeOfDay >= 5.0f && timeOfDay < 6.0f) {
|
||||
return (timeOfDay - 5.0f); // 0 to 1
|
||||
}
|
||||
// Full intensity during day (6:00-18:00)
|
||||
else if (timeOfDay >= 6.0f && timeOfDay < 18.0f) {
|
||||
return 1.0f;
|
||||
}
|
||||
// Fade out at sunset (18:00-19:00)
|
||||
else if (timeOfDay >= 18.0f && timeOfDay < 19.0f) {
|
||||
return 1.0f - (timeOfDay - 18.0f); // 1 to 0
|
||||
}
|
||||
else {
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
float Celestial::calculateCelestialAngle(float timeOfDay, float riseTime, float setTime) const {
|
||||
// Map time to angle (0 to PI)
|
||||
// riseTime: 0 radians (horizon east)
|
||||
// (riseTime + setTime) / 2: PI/2 radians (zenith)
|
||||
// setTime: PI radians (horizon west)
|
||||
|
||||
float duration = setTime - riseTime;
|
||||
float elapsed = timeOfDay - riseTime;
|
||||
|
||||
// Normalize to 0-1
|
||||
float t = elapsed / duration;
|
||||
|
||||
// Map to 0 to PI (arc from east to west)
|
||||
return t * M_PI;
|
||||
}
|
||||
|
||||
void Celestial::createCelestialQuad() {
|
||||
// Simple quad centered at origin
|
||||
float vertices[] = {
|
||||
// Position // TexCoord
|
||||
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f, // Top-left
|
||||
0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // Top-right
|
||||
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // Bottom-right
|
||||
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f // Bottom-left
|
||||
};
|
||||
|
||||
uint32_t indices[] = {
|
||||
0, 1, 2, // First triangle
|
||||
0, 2, 3 // Second triangle
|
||||
};
|
||||
|
||||
// Create OpenGL buffers
|
||||
glGenVertexArrays(1, &vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glGenBuffers(1, &ebo);
|
||||
|
||||
glBindVertexArray(vao);
|
||||
|
||||
// Upload vertex data
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
|
||||
// Upload index data
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
|
||||
|
||||
// Set vertex attributes
|
||||
// Position
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
// Texture coordinates
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void Celestial::destroyCelestialQuad() {
|
||||
if (vao != 0) {
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
vao = 0;
|
||||
}
|
||||
if (vbo != 0) {
|
||||
glDeleteBuffers(1, &vbo);
|
||||
vbo = 0;
|
||||
}
|
||||
if (ebo != 0) {
|
||||
glDeleteBuffers(1, &ebo);
|
||||
ebo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Celestial::update(float deltaTime) {
|
||||
if (!moonPhaseCycling) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update moon phase timer
|
||||
moonPhaseTimer += deltaTime;
|
||||
|
||||
// Moon completes full cycle in MOON_CYCLE_DURATION seconds
|
||||
moonPhase = std::fmod(moonPhaseTimer / MOON_CYCLE_DURATION, 1.0f);
|
||||
}
|
||||
|
||||
void Celestial::setMoonPhase(float phase) {
|
||||
// Clamp phase to 0.0-1.0
|
||||
moonPhase = glm::clamp(phase, 0.0f, 1.0f);
|
||||
|
||||
// Update timer to match phase
|
||||
moonPhaseTimer = moonPhase * MOON_CYCLE_DURATION;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
1240
src/rendering/character_renderer.cpp
Normal file
1240
src/rendering/character_renderer.cpp
Normal file
File diff suppressed because it is too large
Load diff
312
src/rendering/clouds.cpp
Normal file
312
src/rendering/clouds.cpp
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
#include "rendering/clouds.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
Clouds::Clouds() {
|
||||
}
|
||||
|
||||
Clouds::~Clouds() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
bool Clouds::initialize() {
|
||||
LOG_INFO("Initializing cloud system");
|
||||
|
||||
// Generate cloud dome mesh
|
||||
generateMesh();
|
||||
|
||||
// Create VAO
|
||||
glGenVertexArrays(1, &vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glGenBuffers(1, &ebo);
|
||||
|
||||
glBindVertexArray(vao);
|
||||
|
||||
// Upload vertex data
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER,
|
||||
vertices.size() * sizeof(glm::vec3),
|
||||
vertices.data(),
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
// Upload index data
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
|
||||
indices.size() * sizeof(unsigned int),
|
||||
indices.data(),
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
// Position attribute
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Create shader
|
||||
shader = std::make_unique<Shader>();
|
||||
|
||||
// Cloud vertex shader
|
||||
const char* vertexShaderSource = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProjection;
|
||||
|
||||
out vec3 WorldPos;
|
||||
out vec3 LocalPos;
|
||||
|
||||
void main() {
|
||||
LocalPos = aPos;
|
||||
WorldPos = aPos;
|
||||
|
||||
// Remove translation from view matrix (billboard effect)
|
||||
mat4 viewNoTranslation = uView;
|
||||
viewNoTranslation[3][0] = 0.0;
|
||||
viewNoTranslation[3][1] = 0.0;
|
||||
viewNoTranslation[3][2] = 0.0;
|
||||
|
||||
vec4 pos = uProjection * viewNoTranslation * vec4(aPos, 1.0);
|
||||
gl_Position = pos.xyww; // Put at far plane
|
||||
}
|
||||
)";
|
||||
|
||||
// Cloud fragment shader with procedural noise
|
||||
const char* fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
in vec3 WorldPos;
|
||||
in vec3 LocalPos;
|
||||
|
||||
uniform vec3 uCloudColor;
|
||||
uniform float uDensity;
|
||||
uniform float uWindOffset;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
// Simple 3D noise function
|
||||
float hash(vec3 p) {
|
||||
p = fract(p * vec3(0.1031, 0.1030, 0.0973));
|
||||
p += dot(p, p.yxz + 19.19);
|
||||
return fract((p.x + p.y) * p.z);
|
||||
}
|
||||
|
||||
float noise(vec3 p) {
|
||||
vec3 i = floor(p);
|
||||
vec3 f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
return mix(
|
||||
mix(mix(hash(i + vec3(0,0,0)), hash(i + vec3(1,0,0)), f.x),
|
||||
mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y),
|
||||
mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x),
|
||||
mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y),
|
||||
f.z);
|
||||
}
|
||||
|
||||
// Fractal Brownian Motion for cloud-like patterns
|
||||
float fbm(vec3 p) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
float frequency = 1.0;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
value += amplitude * noise(p * frequency);
|
||||
frequency *= 2.0;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Normalize position for noise sampling
|
||||
vec3 pos = normalize(LocalPos);
|
||||
|
||||
// Only render on upper hemisphere
|
||||
if (pos.y < 0.1) {
|
||||
discard;
|
||||
}
|
||||
|
||||
// Apply wind offset to x coordinate
|
||||
vec3 samplePos = vec3(pos.x + uWindOffset, pos.y, pos.z) * 3.0;
|
||||
|
||||
// Generate two cloud layers
|
||||
float clouds1 = fbm(samplePos * 1.0);
|
||||
float clouds2 = fbm(samplePos * 2.0 + vec3(100.0));
|
||||
|
||||
// Combine layers
|
||||
float cloudPattern = clouds1 * 0.6 + clouds2 * 0.4;
|
||||
|
||||
// Apply density threshold to create cloud shapes
|
||||
float cloudMask = smoothstep(0.4 + (1.0 - uDensity) * 0.3, 0.7, cloudPattern);
|
||||
|
||||
// Add some variation to cloud edges
|
||||
float edgeNoise = noise(samplePos * 5.0);
|
||||
cloudMask *= smoothstep(0.3, 0.7, edgeNoise);
|
||||
|
||||
// Fade clouds near horizon
|
||||
float horizonFade = smoothstep(0.0, 0.3, pos.y);
|
||||
cloudMask *= horizonFade;
|
||||
|
||||
// Final alpha
|
||||
float alpha = cloudMask * 0.85;
|
||||
|
||||
if (alpha < 0.05) {
|
||||
discard;
|
||||
}
|
||||
|
||||
FragColor = vec4(uCloudColor, alpha);
|
||||
}
|
||||
)";
|
||||
|
||||
if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
|
||||
LOG_ERROR("Failed to create cloud shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Cloud system initialized: ", triangleCount, " triangles");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Clouds::generateMesh() {
|
||||
vertices.clear();
|
||||
indices.clear();
|
||||
|
||||
// Generate hemisphere mesh for clouds
|
||||
for (int ring = 0; ring <= RINGS; ++ring) {
|
||||
float phi = (ring / static_cast<float>(RINGS)) * (M_PI * 0.5f); // 0 to π/2
|
||||
float y = RADIUS * cos(phi);
|
||||
float ringRadius = RADIUS * sin(phi);
|
||||
|
||||
for (int segment = 0; segment <= SEGMENTS; ++segment) {
|
||||
float theta = (segment / static_cast<float>(SEGMENTS)) * (2.0f * M_PI);
|
||||
float x = ringRadius * cos(theta);
|
||||
float z = ringRadius * sin(theta);
|
||||
|
||||
vertices.push_back(glm::vec3(x, y, z));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate indices
|
||||
for (int ring = 0; ring < RINGS; ++ring) {
|
||||
for (int segment = 0; segment < SEGMENTS; ++segment) {
|
||||
int current = ring * (SEGMENTS + 1) + segment;
|
||||
int next = current + SEGMENTS + 1;
|
||||
|
||||
// Two triangles per quad
|
||||
indices.push_back(current);
|
||||
indices.push_back(next);
|
||||
indices.push_back(current + 1);
|
||||
|
||||
indices.push_back(current + 1);
|
||||
indices.push_back(next);
|
||||
indices.push_back(next + 1);
|
||||
}
|
||||
}
|
||||
|
||||
triangleCount = static_cast<int>(indices.size()) / 3;
|
||||
}
|
||||
|
||||
void Clouds::update(float deltaTime) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Accumulate wind movement
|
||||
windOffset += deltaTime * windSpeed * 0.05f; // Slow drift
|
||||
}
|
||||
|
||||
glm::vec3 Clouds::getCloudColor(float timeOfDay) const {
|
||||
// Base cloud color (white/light gray)
|
||||
glm::vec3 dayColor(0.95f, 0.95f, 1.0f);
|
||||
|
||||
// Dawn clouds (orange tint)
|
||||
if (timeOfDay >= 5.0f && timeOfDay < 7.0f) {
|
||||
float t = (timeOfDay - 5.0f) / 2.0f;
|
||||
glm::vec3 dawnColor(1.0f, 0.7f, 0.5f);
|
||||
return glm::mix(dawnColor, dayColor, t);
|
||||
}
|
||||
// Dusk clouds (orange/pink tint)
|
||||
else if (timeOfDay >= 17.0f && timeOfDay < 19.0f) {
|
||||
float t = (timeOfDay - 17.0f) / 2.0f;
|
||||
glm::vec3 duskColor(1.0f, 0.6f, 0.4f);
|
||||
return glm::mix(dayColor, duskColor, t);
|
||||
}
|
||||
// Night clouds (dark blue-gray)
|
||||
else if (timeOfDay >= 20.0f || timeOfDay < 5.0f) {
|
||||
return glm::vec3(0.15f, 0.15f, 0.25f);
|
||||
}
|
||||
|
||||
return dayColor;
|
||||
}
|
||||
|
||||
void Clouds::render(const Camera& camera, float timeOfDay) {
|
||||
if (!enabled || !shader) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable blending for transparent clouds
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Disable depth write (clouds are in sky)
|
||||
glDepthMask(GL_FALSE);
|
||||
|
||||
// Enable depth test so clouds are behind skybox
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
|
||||
shader->use();
|
||||
|
||||
// Set matrices
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 projection = camera.getProjectionMatrix();
|
||||
|
||||
shader->setUniform("uView", view);
|
||||
shader->setUniform("uProjection", projection);
|
||||
|
||||
// Set cloud parameters
|
||||
glm::vec3 cloudColor = getCloudColor(timeOfDay);
|
||||
shader->setUniform("uCloudColor", cloudColor);
|
||||
shader->setUniform("uDensity", density);
|
||||
shader->setUniform("uWindOffset", windOffset);
|
||||
|
||||
// Render
|
||||
glBindVertexArray(vao);
|
||||
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(indices.size()), GL_UNSIGNED_INT, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Restore state
|
||||
glDisable(GL_BLEND);
|
||||
glDepthMask(GL_TRUE);
|
||||
glDepthFunc(GL_LESS);
|
||||
}
|
||||
|
||||
void Clouds::setDensity(float density) {
|
||||
this->density = glm::clamp(density, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
void Clouds::cleanup() {
|
||||
if (vao) {
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
vao = 0;
|
||||
}
|
||||
if (vbo) {
|
||||
glDeleteBuffers(1, &vbo);
|
||||
vbo = 0;
|
||||
}
|
||||
if (ebo) {
|
||||
glDeleteBuffers(1, &ebo);
|
||||
ebo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
106
src/rendering/frustum.cpp
Normal file
106
src/rendering/frustum.cpp
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#include "rendering/frustum.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
void Frustum::extractFromMatrix(const glm::mat4& vp) {
|
||||
// Extract planes from view-projection matrix
|
||||
// Based on Gribb & Hartmann method (Fast Extraction of Viewing Frustum Planes)
|
||||
|
||||
// Left plane: row4 + row1
|
||||
planes[LEFT].normal.x = vp[0][3] + vp[0][0];
|
||||
planes[LEFT].normal.y = vp[1][3] + vp[1][0];
|
||||
planes[LEFT].normal.z = vp[2][3] + vp[2][0];
|
||||
planes[LEFT].distance = vp[3][3] + vp[3][0];
|
||||
normalizePlane(planes[LEFT]);
|
||||
|
||||
// Right plane: row4 - row1
|
||||
planes[RIGHT].normal.x = vp[0][3] - vp[0][0];
|
||||
planes[RIGHT].normal.y = vp[1][3] - vp[1][0];
|
||||
planes[RIGHT].normal.z = vp[2][3] - vp[2][0];
|
||||
planes[RIGHT].distance = vp[3][3] - vp[3][0];
|
||||
normalizePlane(planes[RIGHT]);
|
||||
|
||||
// Bottom plane: row4 + row2
|
||||
planes[BOTTOM].normal.x = vp[0][3] + vp[0][1];
|
||||
planes[BOTTOM].normal.y = vp[1][3] + vp[1][1];
|
||||
planes[BOTTOM].normal.z = vp[2][3] + vp[2][1];
|
||||
planes[BOTTOM].distance = vp[3][3] + vp[3][1];
|
||||
normalizePlane(planes[BOTTOM]);
|
||||
|
||||
// Top plane: row4 - row2
|
||||
planes[TOP].normal.x = vp[0][3] - vp[0][1];
|
||||
planes[TOP].normal.y = vp[1][3] - vp[1][1];
|
||||
planes[TOP].normal.z = vp[2][3] - vp[2][1];
|
||||
planes[TOP].distance = vp[3][3] - vp[3][1];
|
||||
normalizePlane(planes[TOP]);
|
||||
|
||||
// Near plane: row4 + row3
|
||||
planes[NEAR].normal.x = vp[0][3] + vp[0][2];
|
||||
planes[NEAR].normal.y = vp[1][3] + vp[1][2];
|
||||
planes[NEAR].normal.z = vp[2][3] + vp[2][2];
|
||||
planes[NEAR].distance = vp[3][3] + vp[3][2];
|
||||
normalizePlane(planes[NEAR]);
|
||||
|
||||
// Far plane: row4 - row3
|
||||
planes[FAR].normal.x = vp[0][3] - vp[0][2];
|
||||
planes[FAR].normal.y = vp[1][3] - vp[1][2];
|
||||
planes[FAR].normal.z = vp[2][3] - vp[2][2];
|
||||
planes[FAR].distance = vp[3][3] - vp[3][2];
|
||||
normalizePlane(planes[FAR]);
|
||||
}
|
||||
|
||||
void Frustum::normalizePlane(Plane& plane) {
|
||||
float length = glm::length(plane.normal);
|
||||
if (length > 0.0001f) {
|
||||
plane.normal /= length;
|
||||
plane.distance /= length;
|
||||
}
|
||||
}
|
||||
|
||||
bool Frustum::containsPoint(const glm::vec3& point) const {
|
||||
// Point must be in front of all planes
|
||||
for (const auto& plane : planes) {
|
||||
if (plane.distanceToPoint(point) < 0.0f) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Frustum::intersectsSphere(const glm::vec3& center, float radius) const {
|
||||
// Sphere is visible if distance from center to any plane is >= -radius
|
||||
for (const auto& plane : planes) {
|
||||
float distance = plane.distanceToPoint(center);
|
||||
if (distance < -radius) {
|
||||
// Sphere is completely behind this plane
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Frustum::intersectsAABB(const glm::vec3& min, const glm::vec3& max) const {
|
||||
// Test all 8 corners of the AABB
|
||||
// If all corners are behind any plane, AABB is outside
|
||||
// Otherwise, AABB is at least partially visible
|
||||
|
||||
for (const auto& plane : planes) {
|
||||
// Find the positive vertex (corner furthest in plane normal direction)
|
||||
glm::vec3 positiveVertex;
|
||||
positiveVertex.x = (plane.normal.x >= 0.0f) ? max.x : min.x;
|
||||
positiveVertex.y = (plane.normal.y >= 0.0f) ? max.y : min.y;
|
||||
positiveVertex.z = (plane.normal.z >= 0.0f) ? max.z : min.z;
|
||||
|
||||
// If positive vertex is behind plane, entire box is behind
|
||||
if (plane.distanceToPoint(positiveVertex) < 0.0f) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
288
src/rendering/lens_flare.cpp
Normal file
288
src/rendering/lens_flare.cpp
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
#include "rendering/lens_flare.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
LensFlare::LensFlare() {
|
||||
}
|
||||
|
||||
LensFlare::~LensFlare() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
bool LensFlare::initialize() {
|
||||
LOG_INFO("Initializing lens flare system");
|
||||
|
||||
// Generate flare elements
|
||||
generateFlareElements();
|
||||
|
||||
// Create VAO and VBO for quad rendering
|
||||
glGenVertexArrays(1, &vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
|
||||
glBindVertexArray(vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
|
||||
// Position (x, y) and UV (u, v) for a quad
|
||||
float quadVertices[] = {
|
||||
// Pos UV
|
||||
-0.5f, -0.5f, 0.0f, 0.0f,
|
||||
0.5f, -0.5f, 1.0f, 0.0f,
|
||||
0.5f, 0.5f, 1.0f, 1.0f,
|
||||
-0.5f, -0.5f, 0.0f, 0.0f,
|
||||
0.5f, 0.5f, 1.0f, 1.0f,
|
||||
-0.5f, 0.5f, 0.0f, 1.0f
|
||||
};
|
||||
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
|
||||
|
||||
// Position attribute
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
// UV attribute
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Create shader
|
||||
shader = std::make_unique<Shader>();
|
||||
|
||||
// Lens flare vertex shader (2D screen-space rendering)
|
||||
const char* vertexShaderSource = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 aPos;
|
||||
layout (location = 1) in vec2 aUV;
|
||||
|
||||
uniform vec2 uPosition; // Screen-space position (-1 to 1)
|
||||
uniform float uSize; // Size in screen space
|
||||
uniform float uAspectRatio;
|
||||
|
||||
out vec2 TexCoord;
|
||||
|
||||
void main() {
|
||||
// Scale by size and aspect ratio
|
||||
vec2 scaledPos = aPos * uSize;
|
||||
scaledPos.x /= uAspectRatio;
|
||||
|
||||
// Translate to position
|
||||
vec2 finalPos = scaledPos + uPosition;
|
||||
|
||||
gl_Position = vec4(finalPos, 0.0, 1.0);
|
||||
TexCoord = aUV;
|
||||
}
|
||||
)";
|
||||
|
||||
// Lens flare fragment shader (circular gradient)
|
||||
const char* fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
in vec2 TexCoord;
|
||||
|
||||
uniform vec3 uColor;
|
||||
uniform float uBrightness;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
// Distance from center
|
||||
vec2 center = vec2(0.5);
|
||||
float dist = distance(TexCoord, center);
|
||||
|
||||
// Circular gradient with soft edges
|
||||
float alpha = smoothstep(0.5, 0.0, dist);
|
||||
|
||||
// Add some variation - brighter in center
|
||||
float centerGlow = smoothstep(0.5, 0.0, dist * 2.0);
|
||||
alpha = max(alpha * 0.3, centerGlow);
|
||||
|
||||
// Apply brightness
|
||||
alpha *= uBrightness;
|
||||
|
||||
if (alpha < 0.01) {
|
||||
discard;
|
||||
}
|
||||
|
||||
FragColor = vec4(uColor, alpha);
|
||||
}
|
||||
)";
|
||||
|
||||
if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
|
||||
LOG_ERROR("Failed to create lens flare shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Lens flare system initialized: ", flareElements.size(), " elements");
|
||||
return true;
|
||||
}
|
||||
|
||||
void LensFlare::generateFlareElements() {
|
||||
flareElements.clear();
|
||||
|
||||
// Main sun glow (at sun position)
|
||||
flareElements.push_back({0.0f, 0.3f, glm::vec3(1.0f, 0.95f, 0.8f), 0.8f});
|
||||
|
||||
// Flare ghosts along sun-to-center axis
|
||||
// These appear at various positions between sun and opposite side
|
||||
|
||||
// Bright white ghost near sun
|
||||
flareElements.push_back({0.2f, 0.08f, glm::vec3(1.0f, 1.0f, 1.0f), 0.5f});
|
||||
|
||||
// Blue-tinted ghost
|
||||
flareElements.push_back({0.4f, 0.15f, glm::vec3(0.3f, 0.5f, 1.0f), 0.4f});
|
||||
|
||||
// Small bright spot
|
||||
flareElements.push_back({0.6f, 0.05f, glm::vec3(1.0f, 0.8f, 0.6f), 0.6f});
|
||||
|
||||
// Green-tinted ghost (chromatic aberration)
|
||||
flareElements.push_back({0.8f, 0.12f, glm::vec3(0.4f, 1.0f, 0.5f), 0.3f});
|
||||
|
||||
// Large halo on opposite side
|
||||
flareElements.push_back({-0.5f, 0.25f, glm::vec3(1.0f, 0.7f, 0.4f), 0.2f});
|
||||
|
||||
// Purple ghost far from sun
|
||||
flareElements.push_back({-0.8f, 0.1f, glm::vec3(0.8f, 0.4f, 1.0f), 0.25f});
|
||||
|
||||
// Small red ghost
|
||||
flareElements.push_back({-1.2f, 0.06f, glm::vec3(1.0f, 0.3f, 0.3f), 0.3f});
|
||||
}
|
||||
|
||||
glm::vec2 LensFlare::worldToScreen(const Camera& camera, const glm::vec3& worldPos) const {
|
||||
// Transform to clip space
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 projection = camera.getProjectionMatrix();
|
||||
glm::mat4 viewProj = projection * view;
|
||||
|
||||
glm::vec4 clipPos = viewProj * glm::vec4(worldPos, 1.0f);
|
||||
|
||||
// Perspective divide
|
||||
if (clipPos.w > 0.0f) {
|
||||
glm::vec2 ndc = glm::vec2(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
|
||||
return ndc;
|
||||
}
|
||||
|
||||
// Behind camera
|
||||
return glm::vec2(10.0f, 10.0f); // Off-screen
|
||||
}
|
||||
|
||||
float LensFlare::calculateSunVisibility(const Camera& camera, const glm::vec3& sunPosition) const {
|
||||
// Get sun position in screen space
|
||||
glm::vec2 sunScreen = worldToScreen(camera, sunPosition);
|
||||
|
||||
// Check if sun is behind camera
|
||||
glm::vec3 camPos = camera.getPosition();
|
||||
glm::vec3 camForward = camera.getForward();
|
||||
glm::vec3 toSun = glm::normalize(sunPosition - camPos);
|
||||
float dotProduct = glm::dot(camForward, toSun);
|
||||
|
||||
if (dotProduct < 0.0f) {
|
||||
return 0.0f; // Sun is behind camera
|
||||
}
|
||||
|
||||
// Check if sun is outside screen bounds (with some margin)
|
||||
if (std::abs(sunScreen.x) > 1.5f || std::abs(sunScreen.y) > 1.5f) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Fade based on angle (stronger when looking directly at sun)
|
||||
float angleFactor = glm::smoothstep(0.3f, 1.0f, dotProduct);
|
||||
|
||||
// Fade at screen edges
|
||||
float edgeFade = 1.0f;
|
||||
if (std::abs(sunScreen.x) > 0.8f) {
|
||||
edgeFade *= glm::smoothstep(1.2f, 0.8f, std::abs(sunScreen.x));
|
||||
}
|
||||
if (std::abs(sunScreen.y) > 0.8f) {
|
||||
edgeFade *= glm::smoothstep(1.2f, 0.8f, std::abs(sunScreen.y));
|
||||
}
|
||||
|
||||
return angleFactor * edgeFade;
|
||||
}
|
||||
|
||||
void LensFlare::render(const Camera& camera, const glm::vec3& sunPosition, float timeOfDay) {
|
||||
if (!enabled || !shader) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only render lens flare during daytime (when sun is visible)
|
||||
if (timeOfDay < 5.0f || timeOfDay > 19.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate sun visibility
|
||||
float visibility = calculateSunVisibility(camera, sunPosition);
|
||||
if (visibility < 0.01f) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get sun screen position
|
||||
glm::vec2 sunScreen = worldToScreen(camera, sunPosition);
|
||||
glm::vec2 screenCenter(0.0f, 0.0f);
|
||||
|
||||
// Vector from sun to screen center
|
||||
glm::vec2 sunToCenter = screenCenter - sunScreen;
|
||||
|
||||
// Enable additive blending for flare effect
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending
|
||||
|
||||
// Disable depth test (render on top)
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
shader->use();
|
||||
|
||||
// Set aspect ratio
|
||||
float aspectRatio = camera.getAspectRatio();
|
||||
shader->setUniform("uAspectRatio", aspectRatio);
|
||||
|
||||
glBindVertexArray(vao);
|
||||
|
||||
// Render each flare element
|
||||
for (const auto& element : flareElements) {
|
||||
// Calculate position along sun-to-center axis
|
||||
glm::vec2 position = sunScreen + sunToCenter * element.position;
|
||||
|
||||
// Set uniforms
|
||||
shader->setUniform("uPosition", position);
|
||||
shader->setUniform("uSize", element.size);
|
||||
shader->setUniform("uColor", element.color);
|
||||
|
||||
// Apply visibility and intensity
|
||||
float brightness = element.brightness * visibility * intensityMultiplier;
|
||||
shader->setUniform("uBrightness", brightness);
|
||||
|
||||
// Render quad
|
||||
glDrawArrays(GL_TRIANGLES, 0, VERTICES_PER_QUAD);
|
||||
}
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Restore state
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Restore standard blending
|
||||
}
|
||||
|
||||
void LensFlare::setIntensity(float intensity) {
|
||||
this->intensityMultiplier = glm::clamp(intensity, 0.0f, 2.0f);
|
||||
}
|
||||
|
||||
void LensFlare::cleanup() {
|
||||
if (vao) {
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
vao = 0;
|
||||
}
|
||||
if (vbo) {
|
||||
glDeleteBuffers(1, &vbo);
|
||||
vbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
414
src/rendering/lightning.cpp
Normal file
414
src/rendering/lightning.cpp
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
#include "rendering/lightning.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
namespace {
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
|
||||
|
||||
float randomRange(float min, float max) {
|
||||
return min + dist(gen) * (max - min);
|
||||
}
|
||||
}
|
||||
|
||||
Lightning::Lightning() {
|
||||
flash.active = false;
|
||||
flash.intensity = 0.0f;
|
||||
flash.lifetime = 0.0f;
|
||||
flash.maxLifetime = FLASH_LIFETIME;
|
||||
|
||||
bolts.resize(MAX_BOLTS);
|
||||
for (auto& bolt : bolts) {
|
||||
bolt.active = false;
|
||||
bolt.lifetime = 0.0f;
|
||||
bolt.maxLifetime = BOLT_LIFETIME;
|
||||
bolt.brightness = 1.0f;
|
||||
}
|
||||
|
||||
// Random initial strike time
|
||||
nextStrikeTime = randomRange(MIN_STRIKE_INTERVAL, MAX_STRIKE_INTERVAL);
|
||||
}
|
||||
|
||||
Lightning::~Lightning() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool Lightning::initialize() {
|
||||
core::Logger::getInstance().info("Initializing lightning system...");
|
||||
|
||||
// Create bolt shader
|
||||
const char* boltVertexSrc = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
|
||||
uniform mat4 uViewProjection;
|
||||
uniform float uBrightness;
|
||||
|
||||
out float vBrightness;
|
||||
|
||||
void main() {
|
||||
gl_Position = uViewProjection * vec4(aPos, 1.0);
|
||||
vBrightness = uBrightness;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* boltFragmentSrc = R"(
|
||||
#version 330 core
|
||||
in float vBrightness;
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
// Electric blue-white color
|
||||
vec3 color = mix(vec3(0.6, 0.8, 1.0), vec3(1.0), vBrightness * 0.5);
|
||||
FragColor = vec4(color, vBrightness);
|
||||
}
|
||||
)";
|
||||
|
||||
boltShader = std::make_unique<Shader>();
|
||||
if (!boltShader->loadFromSource(boltVertexSrc, boltFragmentSrc)) {
|
||||
core::Logger::getInstance().error("Failed to create bolt shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create flash shader (fullscreen quad)
|
||||
const char* flashVertexSrc = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 aPos;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* flashFragmentSrc = R"(
|
||||
#version 330 core
|
||||
uniform float uIntensity;
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
// Bright white flash with fade
|
||||
vec3 color = vec3(1.0);
|
||||
FragColor = vec4(color, uIntensity * 0.6);
|
||||
}
|
||||
)";
|
||||
|
||||
flashShader = std::make_unique<Shader>();
|
||||
if (!flashShader->loadFromSource(flashVertexSrc, flashFragmentSrc)) {
|
||||
core::Logger::getInstance().error("Failed to create flash shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create bolt VAO/VBO
|
||||
glGenVertexArrays(1, &boltVAO);
|
||||
glGenBuffers(1, &boltVBO);
|
||||
|
||||
glBindVertexArray(boltVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, boltVBO);
|
||||
|
||||
// Reserve space for segments
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec3) * MAX_SEGMENTS * 2, nullptr, GL_DYNAMIC_DRAW);
|
||||
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
|
||||
|
||||
// Create flash quad VAO/VBO
|
||||
glGenVertexArrays(1, &flashVAO);
|
||||
glGenBuffers(1, &flashVBO);
|
||||
|
||||
float flashQuad[] = {
|
||||
-1.0f, -1.0f,
|
||||
1.0f, -1.0f,
|
||||
-1.0f, 1.0f,
|
||||
1.0f, 1.0f
|
||||
};
|
||||
|
||||
glBindVertexArray(flashVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, flashVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(flashQuad), flashQuad, GL_STATIC_DRAW);
|
||||
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
core::Logger::getInstance().info("Lightning system initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Lightning::shutdown() {
|
||||
if (boltVAO) {
|
||||
glDeleteVertexArrays(1, &boltVAO);
|
||||
glDeleteBuffers(1, &boltVBO);
|
||||
boltVAO = 0;
|
||||
boltVBO = 0;
|
||||
}
|
||||
|
||||
if (flashVAO) {
|
||||
glDeleteVertexArrays(1, &flashVAO);
|
||||
glDeleteBuffers(1, &flashVBO);
|
||||
flashVAO = 0;
|
||||
flashVBO = 0;
|
||||
}
|
||||
|
||||
boltShader.reset();
|
||||
flashShader.reset();
|
||||
}
|
||||
|
||||
void Lightning::update(float deltaTime, const Camera& camera) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update strike timer
|
||||
strikeTimer += deltaTime;
|
||||
|
||||
// Spawn random strikes based on intensity
|
||||
if (strikeTimer >= nextStrikeTime) {
|
||||
spawnRandomStrike(camera.getPosition());
|
||||
strikeTimer = 0.0f;
|
||||
|
||||
// Calculate next strike time (higher intensity = more frequent)
|
||||
float intervalRange = MAX_STRIKE_INTERVAL - MIN_STRIKE_INTERVAL;
|
||||
float adjustedInterval = MIN_STRIKE_INTERVAL + intervalRange * (1.0f - intensity);
|
||||
nextStrikeTime = randomRange(adjustedInterval * 0.8f, adjustedInterval * 1.2f);
|
||||
}
|
||||
|
||||
updateBolts(deltaTime);
|
||||
updateFlash(deltaTime);
|
||||
}
|
||||
|
||||
void Lightning::updateBolts(float deltaTime) {
|
||||
for (auto& bolt : bolts) {
|
||||
if (!bolt.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bolt.lifetime += deltaTime;
|
||||
if (bolt.lifetime >= bolt.maxLifetime) {
|
||||
bolt.active = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fade out
|
||||
float t = bolt.lifetime / bolt.maxLifetime;
|
||||
bolt.brightness = 1.0f - t;
|
||||
}
|
||||
}
|
||||
|
||||
void Lightning::updateFlash(float deltaTime) {
|
||||
if (!flash.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
flash.lifetime += deltaTime;
|
||||
if (flash.lifetime >= flash.maxLifetime) {
|
||||
flash.active = false;
|
||||
flash.intensity = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
// Quick fade
|
||||
float t = flash.lifetime / flash.maxLifetime;
|
||||
flash.intensity = 1.0f - (t * t); // Quadratic fade
|
||||
}
|
||||
|
||||
void Lightning::spawnRandomStrike(const glm::vec3& cameraPos) {
|
||||
// Find inactive bolt
|
||||
LightningBolt* bolt = nullptr;
|
||||
for (auto& b : bolts) {
|
||||
if (!b.active) {
|
||||
bolt = &b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bolt) {
|
||||
return; // All bolts active
|
||||
}
|
||||
|
||||
// Random position around camera
|
||||
float angle = randomRange(0.0f, 2.0f * 3.14159f);
|
||||
float distance = randomRange(50.0f, STRIKE_DISTANCE);
|
||||
|
||||
glm::vec3 strikePos;
|
||||
strikePos.x = cameraPos.x + std::cos(angle) * distance;
|
||||
strikePos.z = cameraPos.z + std::sin(angle) * distance;
|
||||
strikePos.y = cameraPos.y + randomRange(80.0f, 150.0f); // High in sky
|
||||
|
||||
triggerStrike(strikePos);
|
||||
}
|
||||
|
||||
void Lightning::triggerStrike(const glm::vec3& position) {
|
||||
// Find inactive bolt
|
||||
LightningBolt* bolt = nullptr;
|
||||
for (auto& b : bolts) {
|
||||
if (!b.active) {
|
||||
bolt = &b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bolt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup bolt
|
||||
bolt->active = true;
|
||||
bolt->lifetime = 0.0f;
|
||||
bolt->brightness = 1.0f;
|
||||
bolt->startPos = position;
|
||||
bolt->endPos = position;
|
||||
bolt->endPos.y = position.y - randomRange(100.0f, 200.0f); // Strike downward
|
||||
|
||||
// Generate segments
|
||||
bolt->segments.clear();
|
||||
bolt->branches.clear();
|
||||
generateLightningBolt(*bolt);
|
||||
|
||||
// Trigger screen flash
|
||||
flash.active = true;
|
||||
flash.lifetime = 0.0f;
|
||||
flash.intensity = 1.0f;
|
||||
}
|
||||
|
||||
void Lightning::generateLightningBolt(LightningBolt& bolt) {
|
||||
generateBoltSegments(bolt.startPos, bolt.endPos, bolt.segments, 0);
|
||||
}
|
||||
|
||||
void Lightning::generateBoltSegments(const glm::vec3& start, const glm::vec3& end,
|
||||
std::vector<glm::vec3>& segments, int depth) {
|
||||
if (depth > 4) { // Max recursion depth
|
||||
return;
|
||||
}
|
||||
|
||||
int numSegments = 8 + static_cast<int>(randomRange(0.0f, 8.0f));
|
||||
glm::vec3 direction = end - start;
|
||||
float length = glm::length(direction);
|
||||
direction = glm::normalize(direction);
|
||||
|
||||
glm::vec3 current = start;
|
||||
segments.push_back(current);
|
||||
|
||||
for (int i = 1; i < numSegments; i++) {
|
||||
float t = static_cast<float>(i) / static_cast<float>(numSegments);
|
||||
glm::vec3 target = start + direction * (length * t);
|
||||
|
||||
// Add random offset perpendicular to direction
|
||||
float offsetAmount = (1.0f - t) * 8.0f; // More offset at start
|
||||
glm::vec3 perpendicular1 = glm::normalize(glm::cross(direction, glm::vec3(0.0f, 1.0f, 0.0f)));
|
||||
glm::vec3 perpendicular2 = glm::normalize(glm::cross(direction, perpendicular1));
|
||||
|
||||
glm::vec3 offset = perpendicular1 * randomRange(-offsetAmount, offsetAmount) +
|
||||
perpendicular2 * randomRange(-offsetAmount, offsetAmount);
|
||||
|
||||
current = target + offset;
|
||||
segments.push_back(current);
|
||||
|
||||
// Random branches
|
||||
if (dist(gen) < BRANCH_PROBABILITY && depth < 3) {
|
||||
glm::vec3 branchEnd = current;
|
||||
branchEnd += glm::vec3(randomRange(-20.0f, 20.0f),
|
||||
randomRange(-30.0f, -10.0f),
|
||||
randomRange(-20.0f, 20.0f));
|
||||
generateBoltSegments(current, branchEnd, segments, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
segments.push_back(end);
|
||||
}
|
||||
|
||||
void Lightning::render([[maybe_unused]] const Camera& camera, const glm::mat4& view, const glm::mat4& projection) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
glm::mat4 viewProj = projection * view;
|
||||
|
||||
renderBolts(viewProj);
|
||||
renderFlash();
|
||||
}
|
||||
|
||||
void Lightning::renderBolts(const glm::mat4& viewProj) {
|
||||
// Enable additive blending for electric glow
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
|
||||
glDisable(GL_DEPTH_TEST); // Always visible
|
||||
|
||||
boltShader->use();
|
||||
boltShader->setUniform("uViewProjection", viewProj);
|
||||
|
||||
glBindVertexArray(boltVAO);
|
||||
glLineWidth(3.0f);
|
||||
|
||||
for (const auto& bolt : bolts) {
|
||||
if (!bolt.active || bolt.segments.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boltShader->setUniform("uBrightness", bolt.brightness);
|
||||
|
||||
// Upload segments
|
||||
glBindBuffer(GL_ARRAY_BUFFER, boltVBO);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0,
|
||||
bolt.segments.size() * sizeof(glm::vec3),
|
||||
bolt.segments.data());
|
||||
|
||||
// Draw as line strip
|
||||
glDrawArrays(GL_LINE_STRIP, 0, static_cast<GLsizei>(bolt.segments.size()));
|
||||
}
|
||||
|
||||
glLineWidth(1.0f);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
|
||||
void Lightning::renderFlash() {
|
||||
if (!flash.active || flash.intensity <= 0.01f) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fullscreen flash overlay
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
flashShader->use();
|
||||
flashShader->setUniform("uIntensity", flash.intensity);
|
||||
|
||||
glBindVertexArray(flashVAO);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
void Lightning::setEnabled(bool enabled) {
|
||||
this->enabled = enabled;
|
||||
|
||||
if (!enabled) {
|
||||
// Clear active effects
|
||||
for (auto& bolt : bolts) {
|
||||
bolt.active = false;
|
||||
}
|
||||
flash.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Lightning::setIntensity(float intensity) {
|
||||
this->intensity = glm::clamp(intensity, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
466
src/rendering/m2_renderer.cpp
Normal file
466
src/rendering/m2_renderer.cpp
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
#include "rendering/m2_renderer.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/frustum.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/blp_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
void M2Instance::updateModelMatrix() {
|
||||
modelMatrix = glm::mat4(1.0f);
|
||||
modelMatrix = glm::translate(modelMatrix, position);
|
||||
|
||||
// Rotation in radians
|
||||
modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
|
||||
modelMatrix = glm::scale(modelMatrix, glm::vec3(scale));
|
||||
}
|
||||
|
||||
M2Renderer::M2Renderer() {
|
||||
}
|
||||
|
||||
M2Renderer::~M2Renderer() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||
assetManager = assets;
|
||||
|
||||
LOG_INFO("Initializing M2 renderer...");
|
||||
|
||||
// Create M2 shader
|
||||
const char* vertexSrc = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in vec3 aNormal;
|
||||
layout (location = 2) in vec2 aTexCoord;
|
||||
|
||||
uniform mat4 uModel;
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProjection;
|
||||
|
||||
out vec3 FragPos;
|
||||
out vec3 Normal;
|
||||
out vec2 TexCoord;
|
||||
|
||||
void main() {
|
||||
vec4 worldPos = uModel * vec4(aPos, 1.0);
|
||||
FragPos = worldPos.xyz;
|
||||
Normal = mat3(transpose(inverse(uModel))) * aNormal;
|
||||
TexCoord = aTexCoord;
|
||||
|
||||
gl_Position = uProjection * uView * worldPos;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* fragmentSrc = R"(
|
||||
#version 330 core
|
||||
in vec3 FragPos;
|
||||
in vec3 Normal;
|
||||
in vec2 TexCoord;
|
||||
|
||||
uniform vec3 uLightDir;
|
||||
uniform vec3 uAmbientColor;
|
||||
uniform sampler2D uTexture;
|
||||
uniform bool uHasTexture;
|
||||
uniform bool uAlphaTest;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
vec4 texColor;
|
||||
if (uHasTexture) {
|
||||
texColor = texture(uTexture, TexCoord);
|
||||
} else {
|
||||
texColor = vec4(0.6, 0.5, 0.4, 1.0); // Fallback brownish
|
||||
}
|
||||
|
||||
// Alpha test for leaves, fences, etc.
|
||||
if (uAlphaTest && texColor.a < 0.5) {
|
||||
discard;
|
||||
}
|
||||
|
||||
vec3 normal = normalize(Normal);
|
||||
vec3 lightDir = normalize(uLightDir);
|
||||
|
||||
// Two-sided lighting for foliage
|
||||
float diff = max(abs(dot(normal, lightDir)), 0.3);
|
||||
|
||||
vec3 ambient = uAmbientColor * texColor.rgb;
|
||||
vec3 diffuse = diff * texColor.rgb;
|
||||
|
||||
vec3 result = ambient + diffuse;
|
||||
FragColor = vec4(result, texColor.a);
|
||||
}
|
||||
)";
|
||||
|
||||
shader = std::make_unique<Shader>();
|
||||
if (!shader->loadFromSource(vertexSrc, fragmentSrc)) {
|
||||
LOG_ERROR("Failed to create M2 shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create white fallback texture
|
||||
uint8_t white[] = {255, 255, 255, 255};
|
||||
glGenTextures(1, &whiteTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, whiteTexture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
LOG_INFO("M2 renderer initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void M2Renderer::shutdown() {
|
||||
LOG_INFO("Shutting down M2 renderer...");
|
||||
|
||||
// Delete GPU resources
|
||||
for (auto& [id, model] : models) {
|
||||
if (model.vao != 0) glDeleteVertexArrays(1, &model.vao);
|
||||
if (model.vbo != 0) glDeleteBuffers(1, &model.vbo);
|
||||
if (model.ebo != 0) glDeleteBuffers(1, &model.ebo);
|
||||
}
|
||||
models.clear();
|
||||
instances.clear();
|
||||
|
||||
// Delete cached textures
|
||||
for (auto& [path, texId] : textureCache) {
|
||||
if (texId != 0 && texId != whiteTexture) {
|
||||
glDeleteTextures(1, &texId);
|
||||
}
|
||||
}
|
||||
textureCache.clear();
|
||||
if (whiteTexture != 0) {
|
||||
glDeleteTextures(1, &whiteTexture);
|
||||
whiteTexture = 0;
|
||||
}
|
||||
|
||||
shader.reset();
|
||||
}
|
||||
|
||||
bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
||||
if (models.find(modelId) != models.end()) {
|
||||
// Already loaded
|
||||
return true;
|
||||
}
|
||||
|
||||
if (model.vertices.empty() || model.indices.empty()) {
|
||||
LOG_WARNING("M2 model has no geometry: ", model.name);
|
||||
return false;
|
||||
}
|
||||
|
||||
M2ModelGPU gpuModel;
|
||||
gpuModel.name = model.name;
|
||||
gpuModel.boundMin = model.boundMin;
|
||||
gpuModel.boundMax = model.boundMax;
|
||||
gpuModel.boundRadius = model.boundRadius;
|
||||
gpuModel.indexCount = static_cast<uint32_t>(model.indices.size());
|
||||
gpuModel.vertexCount = static_cast<uint32_t>(model.vertices.size());
|
||||
|
||||
// Create VAO
|
||||
glGenVertexArrays(1, &gpuModel.vao);
|
||||
glBindVertexArray(gpuModel.vao);
|
||||
|
||||
// Create VBO with interleaved vertex data
|
||||
// Format: position (3), normal (3), texcoord (2)
|
||||
std::vector<float> vertexData;
|
||||
vertexData.reserve(model.vertices.size() * 8);
|
||||
|
||||
for (const auto& v : model.vertices) {
|
||||
vertexData.push_back(v.position.x);
|
||||
vertexData.push_back(v.position.y);
|
||||
vertexData.push_back(v.position.z);
|
||||
vertexData.push_back(v.normal.x);
|
||||
vertexData.push_back(v.normal.y);
|
||||
vertexData.push_back(v.normal.z);
|
||||
vertexData.push_back(v.texCoords[0].x);
|
||||
vertexData.push_back(v.texCoords[0].y);
|
||||
}
|
||||
|
||||
glGenBuffers(1, &gpuModel.vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, gpuModel.vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float),
|
||||
vertexData.data(), GL_STATIC_DRAW);
|
||||
|
||||
// Create EBO
|
||||
glGenBuffers(1, &gpuModel.ebo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuModel.ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, model.indices.size() * sizeof(uint16_t),
|
||||
model.indices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// Set up vertex attributes
|
||||
const size_t stride = 8 * sizeof(float);
|
||||
|
||||
// Position
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0);
|
||||
|
||||
// Normal
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
|
||||
|
||||
// TexCoord
|
||||
glEnableVertexAttribArray(2);
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void*)(6 * sizeof(float)));
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Load ALL textures from the model into a local vector
|
||||
std::vector<GLuint> allTextures;
|
||||
if (assetManager) {
|
||||
for (const auto& tex : model.textures) {
|
||||
if (!tex.filename.empty()) {
|
||||
allTextures.push_back(loadTexture(tex.filename));
|
||||
} else {
|
||||
allTextures.push_back(whiteTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build per-batch GPU entries
|
||||
if (!model.batches.empty()) {
|
||||
for (const auto& batch : model.batches) {
|
||||
M2ModelGPU::BatchGPU bgpu;
|
||||
bgpu.indexStart = batch.indexStart;
|
||||
bgpu.indexCount = batch.indexCount;
|
||||
|
||||
// Resolve texture: batch.textureIndex → textureLookup → allTextures
|
||||
GLuint tex = whiteTexture;
|
||||
if (batch.textureIndex < model.textureLookup.size()) {
|
||||
uint16_t texIdx = model.textureLookup[batch.textureIndex];
|
||||
if (texIdx < allTextures.size()) {
|
||||
tex = allTextures[texIdx];
|
||||
}
|
||||
} else if (!allTextures.empty()) {
|
||||
tex = allTextures[0];
|
||||
}
|
||||
bgpu.texture = tex;
|
||||
bgpu.hasAlpha = (tex != 0 && tex != whiteTexture);
|
||||
gpuModel.batches.push_back(bgpu);
|
||||
}
|
||||
} else {
|
||||
// Fallback: single batch covering all indices with first texture
|
||||
M2ModelGPU::BatchGPU bgpu;
|
||||
bgpu.indexStart = 0;
|
||||
bgpu.indexCount = gpuModel.indexCount;
|
||||
bgpu.texture = allTextures.empty() ? whiteTexture : allTextures[0];
|
||||
bgpu.hasAlpha = (bgpu.texture != 0 && bgpu.texture != whiteTexture);
|
||||
gpuModel.batches.push_back(bgpu);
|
||||
}
|
||||
|
||||
models[modelId] = std::move(gpuModel);
|
||||
|
||||
LOG_DEBUG("Loaded M2 model: ", model.name, " (", models[modelId].vertexCount, " vertices, ",
|
||||
models[modelId].indexCount / 3, " triangles, ", models[modelId].batches.size(), " batches)");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position,
|
||||
const glm::vec3& rotation, float scale) {
|
||||
if (models.find(modelId) == models.end()) {
|
||||
LOG_WARNING("Cannot create instance: model ", modelId, " not loaded");
|
||||
return 0;
|
||||
}
|
||||
|
||||
M2Instance instance;
|
||||
instance.id = nextInstanceId++;
|
||||
instance.modelId = modelId;
|
||||
instance.position = position;
|
||||
instance.rotation = rotation;
|
||||
instance.scale = scale;
|
||||
instance.updateModelMatrix();
|
||||
|
||||
instances.push_back(instance);
|
||||
|
||||
return instance.id;
|
||||
}
|
||||
|
||||
uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& modelMatrix,
|
||||
const glm::vec3& position) {
|
||||
if (models.find(modelId) == models.end()) {
|
||||
LOG_WARNING("Cannot create instance: model ", modelId, " not loaded");
|
||||
return 0;
|
||||
}
|
||||
|
||||
M2Instance instance;
|
||||
instance.id = nextInstanceId++;
|
||||
instance.modelId = modelId;
|
||||
instance.position = position; // Used for frustum culling
|
||||
instance.rotation = glm::vec3(0.0f);
|
||||
instance.scale = 1.0f;
|
||||
instance.modelMatrix = modelMatrix;
|
||||
|
||||
instances.push_back(instance);
|
||||
|
||||
return instance.id;
|
||||
}
|
||||
|
||||
void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection) {
|
||||
(void)camera; // unused for now
|
||||
|
||||
if (instances.empty() || !shader) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug: log once when we start rendering
|
||||
static bool loggedOnce = false;
|
||||
if (!loggedOnce) {
|
||||
loggedOnce = true;
|
||||
LOG_INFO("M2 render: ", instances.size(), " instances, ", models.size(), " models");
|
||||
}
|
||||
|
||||
// Set up GL state for M2 rendering
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glDisable(GL_BLEND); // No blend leaking from prior renderers
|
||||
glDisable(GL_CULL_FACE); // Some M2 geometry is single-sided
|
||||
|
||||
// Make models render with a bright color for debugging
|
||||
// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // Wireframe mode
|
||||
|
||||
// Build frustum for culling
|
||||
Frustum frustum;
|
||||
frustum.extractFromMatrix(projection * view);
|
||||
|
||||
shader->use();
|
||||
shader->setUniform("uView", view);
|
||||
shader->setUniform("uProjection", projection);
|
||||
shader->setUniform("uLightDir", lightDir);
|
||||
shader->setUniform("uAmbientColor", ambientColor);
|
||||
|
||||
lastDrawCallCount = 0;
|
||||
|
||||
for (const auto& instance : instances) {
|
||||
auto it = models.find(instance.modelId);
|
||||
if (it == models.end()) continue;
|
||||
|
||||
const M2ModelGPU& model = it->second;
|
||||
if (!model.isValid()) continue;
|
||||
|
||||
// Frustum cull: test bounding sphere in world space
|
||||
float worldRadius = model.boundRadius * instance.scale;
|
||||
if (worldRadius > 0.0f && !frustum.intersectsSphere(instance.position, worldRadius)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
shader->setUniform("uModel", instance.modelMatrix);
|
||||
|
||||
glBindVertexArray(model.vao);
|
||||
|
||||
for (const auto& batch : model.batches) {
|
||||
if (batch.indexCount == 0) continue;
|
||||
|
||||
bool hasTexture = (batch.texture != 0);
|
||||
shader->setUniform("uHasTexture", hasTexture);
|
||||
shader->setUniform("uAlphaTest", batch.hasAlpha);
|
||||
|
||||
if (hasTexture) {
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, batch.texture);
|
||||
shader->setUniform("uTexture", 0);
|
||||
}
|
||||
|
||||
glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT,
|
||||
(void*)(batch.indexStart * sizeof(uint16_t)));
|
||||
|
||||
lastDrawCallCount++;
|
||||
}
|
||||
|
||||
// Check for GL errors (only first draw)
|
||||
static bool checkedOnce = false;
|
||||
if (!checkedOnce) {
|
||||
checkedOnce = true;
|
||||
GLenum err = glGetError();
|
||||
if (err != GL_NO_ERROR) {
|
||||
LOG_ERROR("GL error after M2 draw: ", err);
|
||||
} else {
|
||||
LOG_INFO("M2 draw successful: ", model.indexCount, " indices");
|
||||
}
|
||||
}
|
||||
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
// Restore cull face state
|
||||
glEnable(GL_CULL_FACE);
|
||||
}
|
||||
|
||||
void M2Renderer::removeInstance(uint32_t instanceId) {
|
||||
for (auto it = instances.begin(); it != instances.end(); ++it) {
|
||||
if (it->id == instanceId) {
|
||||
instances.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void M2Renderer::clear() {
|
||||
for (auto& [id, model] : models) {
|
||||
if (model.vao != 0) glDeleteVertexArrays(1, &model.vao);
|
||||
if (model.vbo != 0) glDeleteBuffers(1, &model.vbo);
|
||||
if (model.ebo != 0) glDeleteBuffers(1, &model.ebo);
|
||||
}
|
||||
models.clear();
|
||||
instances.clear();
|
||||
}
|
||||
|
||||
GLuint M2Renderer::loadTexture(const std::string& path) {
|
||||
// Check cache
|
||||
auto it = textureCache.find(path);
|
||||
if (it != textureCache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Load BLP texture
|
||||
pipeline::BLPImage blp = assetManager->loadTexture(path);
|
||||
if (!blp.isValid()) {
|
||||
LOG_WARNING("M2: Failed to load texture: ", path);
|
||||
textureCache[path] = whiteTexture;
|
||||
return whiteTexture;
|
||||
}
|
||||
|
||||
GLuint textureID;
|
||||
glGenTextures(1, &textureID);
|
||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
|
||||
blp.width, blp.height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data());
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
textureCache[path] = textureID;
|
||||
LOG_DEBUG("M2: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
|
||||
|
||||
return textureID;
|
||||
}
|
||||
|
||||
uint32_t M2Renderer::getTotalTriangleCount() const {
|
||||
uint32_t total = 0;
|
||||
for (const auto& instance : instances) {
|
||||
auto it = models.find(instance.modelId);
|
||||
if (it != models.end()) {
|
||||
total += it->second.indexCount / 3;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
8
src/rendering/material.cpp
Normal file
8
src/rendering/material.cpp
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#include "rendering/material.hpp"
|
||||
|
||||
// All implementations are inline in header
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
// Empty file - all methods are inline
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
56
src/rendering/mesh.cpp
Normal file
56
src/rendering/mesh.cpp
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#include "rendering/mesh.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
Mesh::~Mesh() {
|
||||
destroy();
|
||||
}
|
||||
|
||||
void Mesh::create(const std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices) {
|
||||
indexCount = indices.size();
|
||||
|
||||
glGenVertexArrays(1, &VAO);
|
||||
glGenBuffers(1, &VBO);
|
||||
glGenBuffers(1, &EBO);
|
||||
|
||||
glBindVertexArray(VAO);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW);
|
||||
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// Position
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
|
||||
|
||||
// Normal
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
|
||||
|
||||
// TexCoord
|
||||
glEnableVertexAttribArray(2);
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));
|
||||
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void Mesh::destroy() {
|
||||
if (VAO) glDeleteVertexArrays(1, &VAO);
|
||||
if (VBO) glDeleteBuffers(1, &VBO);
|
||||
if (EBO) glDeleteBuffers(1, &EBO);
|
||||
VAO = VBO = EBO = 0;
|
||||
}
|
||||
|
||||
void Mesh::draw() const {
|
||||
if (VAO && indexCount > 0) {
|
||||
glBindVertexArray(VAO);
|
||||
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
213
src/rendering/minimap.cpp
Normal file
213
src/rendering/minimap.cpp
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
#include "rendering/minimap.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/terrain_renderer.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
Minimap::Minimap() = default;
|
||||
|
||||
Minimap::~Minimap() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool Minimap::initialize(int size) {
|
||||
mapSize = size;
|
||||
|
||||
// Create FBO
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
|
||||
// Color texture
|
||||
glGenTextures(1, &fboTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, fboTexture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mapSize, mapSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fboTexture, 0);
|
||||
|
||||
// Depth renderbuffer
|
||||
glGenRenderbuffers(1, &fboDepth);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, fboDepth);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mapSize, mapSize);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, fboDepth);
|
||||
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
LOG_ERROR("Minimap FBO incomplete");
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return false;
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// Screen quad (NDC fullscreen, we'll position via uniforms)
|
||||
float quadVerts[] = {
|
||||
// pos (x,y), uv (u,v)
|
||||
-1.0f, -1.0f, 0.0f, 0.0f,
|
||||
1.0f, -1.0f, 1.0f, 0.0f,
|
||||
1.0f, 1.0f, 1.0f, 1.0f,
|
||||
-1.0f, -1.0f, 0.0f, 0.0f,
|
||||
1.0f, 1.0f, 1.0f, 1.0f,
|
||||
-1.0f, 1.0f, 0.0f, 1.0f,
|
||||
};
|
||||
|
||||
glGenVertexArrays(1, &quadVAO);
|
||||
glGenBuffers(1, &quadVBO);
|
||||
glBindVertexArray(quadVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Quad shader with circular mask and border
|
||||
const char* vertSrc = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 aPos;
|
||||
layout (location = 1) in vec2 aUV;
|
||||
|
||||
uniform vec4 uRect; // x, y, w, h in NDC
|
||||
|
||||
out vec2 TexCoord;
|
||||
|
||||
void main() {
|
||||
vec2 pos = uRect.xy + aUV * uRect.zw;
|
||||
gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
|
||||
TexCoord = aUV;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* fragSrc = R"(
|
||||
#version 330 core
|
||||
in vec2 TexCoord;
|
||||
|
||||
uniform sampler2D uMapTexture;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
vec2 center = TexCoord - vec2(0.5);
|
||||
float dist = length(center);
|
||||
|
||||
// Circular mask
|
||||
if (dist > 0.5) discard;
|
||||
|
||||
// Gold border ring
|
||||
float borderWidth = 0.02;
|
||||
if (dist > 0.5 - borderWidth) {
|
||||
FragColor = vec4(0.8, 0.65, 0.2, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
vec4 texColor = texture(uMapTexture, TexCoord);
|
||||
|
||||
// Player dot at center
|
||||
if (dist < 0.02) {
|
||||
FragColor = vec4(1.0, 0.3, 0.3, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
FragColor = texColor;
|
||||
}
|
||||
)";
|
||||
|
||||
quadShader = std::make_unique<Shader>();
|
||||
if (!quadShader->loadFromSource(vertSrc, fragSrc)) {
|
||||
LOG_ERROR("Failed to create minimap shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Minimap initialized (", mapSize, "x", mapSize, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Minimap::shutdown() {
|
||||
if (fbo) { glDeleteFramebuffers(1, &fbo); fbo = 0; }
|
||||
if (fboTexture) { glDeleteTextures(1, &fboTexture); fboTexture = 0; }
|
||||
if (fboDepth) { glDeleteRenderbuffers(1, &fboDepth); fboDepth = 0; }
|
||||
if (quadVAO) { glDeleteVertexArrays(1, &quadVAO); quadVAO = 0; }
|
||||
if (quadVBO) { glDeleteBuffers(1, &quadVBO); quadVBO = 0; }
|
||||
quadShader.reset();
|
||||
}
|
||||
|
||||
void Minimap::render(const Camera& playerCamera, int screenWidth, int screenHeight) {
|
||||
if (!enabled || !terrainRenderer || !fbo) return;
|
||||
|
||||
// 1. Render terrain from top-down into FBO
|
||||
renderTerrainToFBO(playerCamera);
|
||||
|
||||
// 2. Draw the minimap quad on screen
|
||||
renderQuad(screenWidth, screenHeight);
|
||||
}
|
||||
|
||||
void Minimap::renderTerrainToFBO(const Camera& playerCamera) {
|
||||
// Save current viewport
|
||||
GLint prevViewport[4];
|
||||
glGetIntegerv(GL_VIEWPORT, prevViewport);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, mapSize, mapSize);
|
||||
glClearColor(0.05f, 0.1f, 0.15f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Create a top-down camera at the player's XY position
|
||||
Camera topDownCamera;
|
||||
glm::vec3 playerPos = playerCamera.getPosition();
|
||||
topDownCamera.setPosition(glm::vec3(playerPos.x, playerPos.y, playerPos.z + 5000.0f));
|
||||
topDownCamera.setRotation(0.0f, -89.9f); // Look straight down
|
||||
topDownCamera.setAspectRatio(1.0f);
|
||||
topDownCamera.setFov(1.0f); // Will be overridden by ortho below
|
||||
|
||||
// We need orthographic projection, but Camera only supports perspective.
|
||||
// Use the terrain renderer's render with a custom view/projection.
|
||||
// For now, render with the top-down camera (perspective, narrow FOV approximates ortho)
|
||||
// The narrow FOV + high altitude gives a near-orthographic result.
|
||||
|
||||
// Calculate FOV that covers viewRadius at the altitude
|
||||
float altitude = 5000.0f;
|
||||
float fovDeg = glm::degrees(2.0f * std::atan(viewRadius / altitude));
|
||||
topDownCamera.setFov(fovDeg);
|
||||
|
||||
terrainRenderer->render(topDownCamera);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// Restore viewport
|
||||
glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]);
|
||||
}
|
||||
|
||||
void Minimap::renderQuad(int screenWidth, int screenHeight) {
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
quadShader->use();
|
||||
|
||||
// Position minimap in top-right corner with margin
|
||||
float margin = 10.0f;
|
||||
float pixelW = static_cast<float>(mapSize) / screenWidth;
|
||||
float pixelH = static_cast<float>(mapSize) / screenHeight;
|
||||
float x = 1.0f - pixelW - margin / screenWidth;
|
||||
float y = 1.0f - pixelH - margin / screenHeight;
|
||||
|
||||
// uRect: x, y, w, h in 0..1 screen space
|
||||
quadShader->setUniform("uRect", glm::vec4(x, y, pixelW, pixelH));
|
||||
quadShader->setUniform("uMapTexture", 0);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, fboTexture);
|
||||
|
||||
glBindVertexArray(quadVAO);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
416
src/rendering/performance_hud.cpp
Normal file
416
src/rendering/performance_hud.cpp
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
#include "rendering/performance_hud.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/terrain_renderer.hpp"
|
||||
#include "rendering/terrain_manager.hpp"
|
||||
#include "rendering/water_renderer.hpp"
|
||||
#include "rendering/skybox.hpp"
|
||||
#include "rendering/celestial.hpp"
|
||||
#include "rendering/starfield.hpp"
|
||||
#include "rendering/clouds.hpp"
|
||||
#include "rendering/lens_flare.hpp"
|
||||
#include "rendering/weather.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
PerformanceHUD::PerformanceHUD() {
|
||||
}
|
||||
|
||||
PerformanceHUD::~PerformanceHUD() {
|
||||
}
|
||||
|
||||
void PerformanceHUD::update(float deltaTime) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store frame time
|
||||
frameTime = deltaTime;
|
||||
frameTimeHistory.push_back(deltaTime);
|
||||
|
||||
// Keep history size limited
|
||||
while (frameTimeHistory.size() > MAX_FRAME_HISTORY) {
|
||||
frameTimeHistory.pop_front();
|
||||
}
|
||||
|
||||
// Update stats periodically
|
||||
updateTimer += deltaTime;
|
||||
if (updateTimer >= UPDATE_INTERVAL) {
|
||||
updateTimer = 0.0f;
|
||||
calculateFPS();
|
||||
}
|
||||
}
|
||||
|
||||
void PerformanceHUD::calculateFPS() {
|
||||
if (frameTimeHistory.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Current FPS (from last frame time)
|
||||
currentFPS = frameTime > 0.0001f ? 1.0f / frameTime : 0.0f;
|
||||
|
||||
// Average FPS
|
||||
float sum = 0.0f;
|
||||
for (float ft : frameTimeHistory) {
|
||||
sum += ft;
|
||||
}
|
||||
float avgFrameTime = sum / frameTimeHistory.size();
|
||||
averageFPS = avgFrameTime > 0.0001f ? 1.0f / avgFrameTime : 0.0f;
|
||||
|
||||
// Min/Max FPS (from last 2 seconds)
|
||||
minFPS = 10000.0f;
|
||||
maxFPS = 0.0f;
|
||||
for (float ft : frameTimeHistory) {
|
||||
if (ft > 0.0001f) {
|
||||
float fps = 1.0f / ft;
|
||||
minFPS = std::min(minFPS, fps);
|
||||
maxFPS = std::max(maxFPS, fps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) {
|
||||
if (!enabled || !renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set window position based on setting
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing |
|
||||
ImGuiWindowFlags_NoNav;
|
||||
|
||||
const float PADDING = 10.0f;
|
||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImVec2 work_pos = viewport->WorkPos;
|
||||
ImVec2 work_size = viewport->WorkSize;
|
||||
ImVec2 window_pos, window_pos_pivot;
|
||||
|
||||
switch (position) {
|
||||
case Position::TOP_LEFT:
|
||||
window_pos.x = work_pos.x + PADDING;
|
||||
window_pos.y = work_pos.y + PADDING;
|
||||
window_pos_pivot.x = 0.0f;
|
||||
window_pos_pivot.y = 0.0f;
|
||||
break;
|
||||
case Position::TOP_RIGHT:
|
||||
window_pos.x = work_pos.x + work_size.x - PADDING;
|
||||
window_pos.y = work_pos.y + PADDING;
|
||||
window_pos_pivot.x = 1.0f;
|
||||
window_pos_pivot.y = 0.0f;
|
||||
break;
|
||||
case Position::BOTTOM_LEFT:
|
||||
window_pos.x = work_pos.x + PADDING;
|
||||
window_pos.y = work_pos.y + work_size.y - PADDING;
|
||||
window_pos_pivot.x = 0.0f;
|
||||
window_pos_pivot.y = 1.0f;
|
||||
break;
|
||||
case Position::BOTTOM_RIGHT:
|
||||
window_pos.x = work_pos.x + work_size.x - PADDING;
|
||||
window_pos.y = work_pos.y + work_size.y - PADDING;
|
||||
window_pos_pivot.x = 1.0f;
|
||||
window_pos_pivot.y = 1.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot);
|
||||
ImGui::SetNextWindowBgAlpha(0.7f); // Transparent background
|
||||
|
||||
if (!ImGui::Begin("Performance", nullptr, flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// FPS section
|
||||
if (showFPS) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "PERFORMANCE");
|
||||
ImGui::Separator();
|
||||
|
||||
// Color-code FPS
|
||||
ImVec4 fpsColor;
|
||||
if (currentFPS >= 60.0f) {
|
||||
fpsColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); // Green
|
||||
} else if (currentFPS >= 30.0f) {
|
||||
fpsColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Yellow
|
||||
} else {
|
||||
fpsColor = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red
|
||||
}
|
||||
|
||||
ImGui::Text("FPS: ");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(fpsColor, "%.1f", currentFPS);
|
||||
|
||||
ImGui::Text("Avg: %.1f", averageFPS);
|
||||
ImGui::Text("Min: %.1f", minFPS);
|
||||
ImGui::Text("Max: %.1f", maxFPS);
|
||||
ImGui::Text("Frame: %.2f ms", frameTime * 1000.0f);
|
||||
|
||||
// Frame time graph
|
||||
if (!frameTimeHistory.empty()) {
|
||||
std::vector<float> frameTimesMs;
|
||||
frameTimesMs.reserve(frameTimeHistory.size());
|
||||
for (float ft : frameTimeHistory) {
|
||||
frameTimesMs.push_back(ft * 1000.0f); // Convert to ms
|
||||
}
|
||||
ImGui::PlotLines("##frametime", frameTimesMs.data(), static_cast<int>(frameTimesMs.size()),
|
||||
0, nullptr, 0.0f, 33.33f, ImVec2(200, 40));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Renderer stats
|
||||
if (showRenderer) {
|
||||
auto* terrainRenderer = renderer->getTerrainRenderer();
|
||||
if (terrainRenderer) {
|
||||
ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "RENDERING");
|
||||
ImGui::Separator();
|
||||
|
||||
int totalChunks = terrainRenderer->getChunkCount();
|
||||
int rendered = terrainRenderer->getRenderedChunkCount();
|
||||
int culled = terrainRenderer->getCulledChunkCount();
|
||||
int triangles = terrainRenderer->getTriangleCount();
|
||||
|
||||
ImGui::Text("Chunks: %d", totalChunks);
|
||||
ImGui::Text("Rendered: %d", rendered);
|
||||
ImGui::Text("Culled: %d", culled);
|
||||
|
||||
if (totalChunks > 0) {
|
||||
float visiblePercent = (rendered * 100.0f) / totalChunks;
|
||||
ImGui::Text("Visible: %.1f%%", visiblePercent);
|
||||
}
|
||||
|
||||
ImGui::Text("Triangles: %s",
|
||||
triangles >= 1000000 ?
|
||||
(std::to_string(triangles / 1000) + "K").c_str() :
|
||||
std::to_string(triangles).c_str());
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
// Terrain streaming info
|
||||
if (showTerrain) {
|
||||
auto* terrainManager = renderer->getTerrainManager();
|
||||
if (terrainManager) {
|
||||
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "TERRAIN");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Text("Loaded tiles: %d", terrainManager->getLoadedTileCount());
|
||||
|
||||
auto currentTile = terrainManager->getCurrentTile();
|
||||
ImGui::Text("Current tile: [%d,%d]", currentTile.x, currentTile.y);
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Water info
|
||||
auto* waterRenderer = renderer->getWaterRenderer();
|
||||
if (waterRenderer) {
|
||||
ImGui::TextColored(ImVec4(0.2f, 0.5f, 1.0f, 1.0f), "WATER");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Text("Surfaces: %d", waterRenderer->getSurfaceCount());
|
||||
ImGui::Text("Enabled: %s", waterRenderer->isEnabled() ? "YES" : "NO");
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
// Skybox info
|
||||
if (showTerrain) {
|
||||
auto* skybox = renderer->getSkybox();
|
||||
if (skybox) {
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "SKY");
|
||||
ImGui::Separator();
|
||||
|
||||
float time = skybox->getTimeOfDay();
|
||||
int hours = static_cast<int>(time);
|
||||
int minutes = static_cast<int>((time - hours) * 60);
|
||||
|
||||
ImGui::Text("Time: %02d:%02d", hours, minutes);
|
||||
ImGui::Text("Auto: %s", skybox->isTimeProgressionEnabled() ? "YES" : "NO");
|
||||
|
||||
// Celestial info
|
||||
auto* celestial = renderer->getCelestial();
|
||||
if (celestial) {
|
||||
ImGui::Text("Sun/Moon: %s", celestial->isEnabled() ? "YES" : "NO");
|
||||
|
||||
// Moon phase info
|
||||
float phase = celestial->getMoonPhase();
|
||||
const char* phaseName = "Unknown";
|
||||
if (phase < 0.0625f || phase >= 0.9375f) phaseName = "New";
|
||||
else if (phase < 0.1875f) phaseName = "Wax Cresc";
|
||||
else if (phase < 0.3125f) phaseName = "1st Qtr";
|
||||
else if (phase < 0.4375f) phaseName = "Wax Gibb";
|
||||
else if (phase < 0.5625f) phaseName = "Full";
|
||||
else if (phase < 0.6875f) phaseName = "Wan Gibb";
|
||||
else if (phase < 0.8125f) phaseName = "Last Qtr";
|
||||
else phaseName = "Wan Cresc";
|
||||
|
||||
ImGui::Text("Moon: %s (%.0f%%)", phaseName, phase * 100.0f);
|
||||
ImGui::Text("Cycling: %s", celestial->isMoonPhaseCycling() ? "YES" : "NO");
|
||||
}
|
||||
|
||||
// Star field info
|
||||
auto* starField = renderer->getStarField();
|
||||
if (starField) {
|
||||
ImGui::Text("Stars: %d (%s)", starField->getStarCount(),
|
||||
starField->isEnabled() ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
// Cloud info
|
||||
auto* clouds = renderer->getClouds();
|
||||
if (clouds) {
|
||||
ImGui::Text("Clouds: %s (%.0f%%)",
|
||||
clouds->isEnabled() ? "ON" : "OFF",
|
||||
clouds->getDensity() * 100.0f);
|
||||
}
|
||||
|
||||
// Lens flare info
|
||||
auto* lensFlare = renderer->getLensFlare();
|
||||
if (lensFlare) {
|
||||
ImGui::Text("Lens Flare: %s (%.0f%%)",
|
||||
lensFlare->isEnabled() ? "ON" : "OFF",
|
||||
lensFlare->getIntensity() * 100.0f);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
// Weather info
|
||||
if (showRenderer) {
|
||||
auto* weather = renderer->getWeather();
|
||||
if (weather) {
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "WEATHER");
|
||||
ImGui::Separator();
|
||||
|
||||
const char* typeName = "None";
|
||||
using WeatherType = rendering::Weather::Type;
|
||||
auto type = weather->getWeatherType();
|
||||
if (type == WeatherType::RAIN) typeName = "Rain";
|
||||
else if (type == WeatherType::SNOW) typeName = "Snow";
|
||||
|
||||
ImGui::Text("Type: %s", typeName);
|
||||
if (weather->isEnabled()) {
|
||||
ImGui::Text("Particles: %d", weather->getParticleCount());
|
||||
ImGui::Text("Intensity: %.0f%%", weather->getIntensity() * 100.0f);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
// Fog info
|
||||
if (showRenderer) {
|
||||
auto* terrainRenderer = renderer->getTerrainRenderer();
|
||||
if (terrainRenderer) {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.8f, 0.9f, 1.0f), "FOG");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Text("Distance fog: %s", terrainRenderer->isFogEnabled() ? "ON" : "OFF");
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
// Character info
|
||||
if (showRenderer) {
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
if (charRenderer) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.4f, 1.0f), "CHARACTERS");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Text("Instances: %zu", charRenderer->getInstanceCount());
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
// WMO building info
|
||||
if (showRenderer) {
|
||||
auto* wmoRenderer = renderer->getWMORenderer();
|
||||
if (wmoRenderer) {
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.7f, 0.6f, 1.0f), "WMO BUILDINGS");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Text("Models: %u", wmoRenderer->getModelCount());
|
||||
ImGui::Text("Instances: %u", wmoRenderer->getInstanceCount());
|
||||
ImGui::Text("Triangles: %u", wmoRenderer->getTotalTriangleCount());
|
||||
ImGui::Text("Draw Calls: %u", wmoRenderer->getDrawCallCount());
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
// Zone info
|
||||
{
|
||||
const std::string& zoneName = renderer->getCurrentZoneName();
|
||||
if (!zoneName.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f), "ZONE");
|
||||
ImGui::Separator();
|
||||
ImGui::Text("%s", zoneName.c_str());
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
// Camera info
|
||||
if (showCamera && camera) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "CAMERA");
|
||||
ImGui::Separator();
|
||||
|
||||
glm::vec3 pos = camera->getPosition();
|
||||
ImGui::Text("Pos: %.1f, %.1f, %.1f", pos.x, pos.y, pos.z);
|
||||
|
||||
glm::vec3 forward = camera->getForward();
|
||||
ImGui::Text("Dir: %.2f, %.2f, %.2f", forward.x, forward.y, forward.z);
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Controls help
|
||||
if (showControls) {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "CONTROLS");
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F1: Toggle HUD");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F2: Wireframe");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F3: Single tile");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F4: Culling");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F5: Stats");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F6: Multi-tile");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F7: Streaming");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F8: Water");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F9: Time");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F10: Sun/Moon");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F11: Stars");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F12: Fog");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "+/-: Change time");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "C: Clouds");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "[/]: Density");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "L: Lens Flare");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ",/.: Intensity");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "M: Moon Cycle");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ";/': Moon Phase");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "W: Weather");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "</>: Wx Intensity");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "K: Spawn Character");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "J: Remove Chars");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "O: Spawn Test WMO");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Shift+O: Real WMO");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "P: Clear WMOs");
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
911
src/rendering/renderer.cpp
Normal file
911
src/rendering/renderer.cpp
Normal file
|
|
@ -0,0 +1,911 @@
|
|||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
#include "rendering/scene.hpp"
|
||||
#include "rendering/terrain_renderer.hpp"
|
||||
#include "rendering/terrain_manager.hpp"
|
||||
#include "rendering/performance_hud.hpp"
|
||||
#include "rendering/water_renderer.hpp"
|
||||
#include "rendering/skybox.hpp"
|
||||
#include "rendering/celestial.hpp"
|
||||
#include "rendering/starfield.hpp"
|
||||
#include "rendering/clouds.hpp"
|
||||
#include "rendering/lens_flare.hpp"
|
||||
#include "rendering/weather.hpp"
|
||||
#include "rendering/swim_effects.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
#include "rendering/minimap.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/m2_loader.hpp"
|
||||
#include "pipeline/wmo_loader.hpp"
|
||||
#include "pipeline/adt_loader.hpp"
|
||||
#include "pipeline/terrain_mesh.hpp"
|
||||
#include "core/window.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "game/world.hpp"
|
||||
#include "game/zone_manager.hpp"
|
||||
#include "audio/music_manager.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtx/euler_angles.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <cctype>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
struct EmoteInfo {
|
||||
uint32_t animId;
|
||||
bool loop;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
// AnimationData.dbc IDs for WotLK HumanMale emotes
|
||||
// Reference: https://wowdev.wiki/M2/AnimationList
|
||||
static const std::unordered_map<std::string, EmoteInfo> EMOTE_TABLE = {
|
||||
{"wave", {67, false, "waves."}},
|
||||
{"bow", {66, false, "bows down graciously."}},
|
||||
{"laugh", {70, false, "laughs."}},
|
||||
{"point", {84, false, "points over there."}},
|
||||
{"cheer", {68, false, "cheers!"}},
|
||||
{"dance", {69, true, "begins to dance."}},
|
||||
{"kneel", {75, false, "kneels down."}},
|
||||
{"applaud", {80, false, "applauds."}},
|
||||
{"shout", {81, false, "shouts."}},
|
||||
{"chicken", {78, false, "clucks like a chicken."}},
|
||||
{"cry", {77, false, "cries."}},
|
||||
{"kiss", {76, false, "blows a kiss."}},
|
||||
{"roar", {74, false, "roars with bestial vigor."}},
|
||||
{"salute", {113, false, "salutes."}},
|
||||
{"rude", {73, false, "makes a rude gesture."}},
|
||||
{"flex", {82, false, "flexes muscles."}},
|
||||
{"shy", {83, false, "acts shy."}},
|
||||
{"beg", {79, false, "begs everyone around."}},
|
||||
{"eat", {61, false, "begins to eat."}},
|
||||
};
|
||||
|
||||
Renderer::Renderer() = default;
|
||||
Renderer::~Renderer() = default;
|
||||
|
||||
bool Renderer::initialize(core::Window* win) {
|
||||
window = win;
|
||||
LOG_INFO("Initializing renderer");
|
||||
|
||||
// Create camera (in front of Stormwind gate, looking north)
|
||||
camera = std::make_unique<Camera>();
|
||||
camera->setPosition(glm::vec3(-8900.0f, -170.0f, 150.0f));
|
||||
camera->setRotation(0.0f, -5.0f);
|
||||
camera->setAspectRatio(window->getAspectRatio());
|
||||
camera->setFov(60.0f);
|
||||
|
||||
// Create camera controller
|
||||
cameraController = std::make_unique<CameraController>(camera.get());
|
||||
cameraController->setMovementSpeed(100.0f); // Fast movement for terrain exploration
|
||||
cameraController->setMouseSensitivity(0.15f);
|
||||
|
||||
// Create scene
|
||||
scene = std::make_unique<Scene>();
|
||||
|
||||
// Create performance HUD
|
||||
performanceHUD = std::make_unique<PerformanceHUD>();
|
||||
performanceHUD->setPosition(PerformanceHUD::Position::TOP_LEFT);
|
||||
|
||||
// Create water renderer
|
||||
waterRenderer = std::make_unique<WaterRenderer>();
|
||||
if (!waterRenderer->initialize()) {
|
||||
LOG_WARNING("Failed to initialize water renderer");
|
||||
waterRenderer.reset();
|
||||
}
|
||||
|
||||
// Create skybox
|
||||
skybox = std::make_unique<Skybox>();
|
||||
if (!skybox->initialize()) {
|
||||
LOG_WARNING("Failed to initialize skybox");
|
||||
skybox.reset();
|
||||
} else {
|
||||
skybox->setTimeOfDay(12.0f); // Start at noon
|
||||
}
|
||||
|
||||
// Create celestial renderer (sun and moon)
|
||||
celestial = std::make_unique<Celestial>();
|
||||
if (!celestial->initialize()) {
|
||||
LOG_WARNING("Failed to initialize celestial renderer");
|
||||
celestial.reset();
|
||||
}
|
||||
|
||||
// Create star field
|
||||
starField = std::make_unique<StarField>();
|
||||
if (!starField->initialize()) {
|
||||
LOG_WARNING("Failed to initialize star field");
|
||||
starField.reset();
|
||||
}
|
||||
|
||||
// Create clouds
|
||||
clouds = std::make_unique<Clouds>();
|
||||
if (!clouds->initialize()) {
|
||||
LOG_WARNING("Failed to initialize clouds");
|
||||
clouds.reset();
|
||||
} else {
|
||||
clouds->setDensity(0.5f); // Medium cloud coverage
|
||||
}
|
||||
|
||||
// Create lens flare
|
||||
lensFlare = std::make_unique<LensFlare>();
|
||||
if (!lensFlare->initialize()) {
|
||||
LOG_WARNING("Failed to initialize lens flare");
|
||||
lensFlare.reset();
|
||||
}
|
||||
|
||||
// Create weather system
|
||||
weather = std::make_unique<Weather>();
|
||||
if (!weather->initialize()) {
|
||||
LOG_WARNING("Failed to initialize weather");
|
||||
weather.reset();
|
||||
}
|
||||
|
||||
// Create swim effects
|
||||
swimEffects = std::make_unique<SwimEffects>();
|
||||
if (!swimEffects->initialize()) {
|
||||
LOG_WARNING("Failed to initialize swim effects");
|
||||
swimEffects.reset();
|
||||
}
|
||||
|
||||
// Create character renderer
|
||||
characterRenderer = std::make_unique<CharacterRenderer>();
|
||||
if (!characterRenderer->initialize()) {
|
||||
LOG_WARNING("Failed to initialize character renderer");
|
||||
characterRenderer.reset();
|
||||
}
|
||||
|
||||
// Create WMO renderer
|
||||
wmoRenderer = std::make_unique<WMORenderer>();
|
||||
if (!wmoRenderer->initialize()) {
|
||||
LOG_WARNING("Failed to initialize WMO renderer");
|
||||
wmoRenderer.reset();
|
||||
}
|
||||
|
||||
// Create minimap
|
||||
minimap = std::make_unique<Minimap>();
|
||||
if (!minimap->initialize(200)) {
|
||||
LOG_WARNING("Failed to initialize minimap");
|
||||
minimap.reset();
|
||||
}
|
||||
|
||||
// Create M2 renderer (for doodads)
|
||||
m2Renderer = std::make_unique<M2Renderer>();
|
||||
// Note: M2 renderer needs asset manager, will be initialized when terrain loads
|
||||
|
||||
// Create zone manager
|
||||
zoneManager = std::make_unique<game::ZoneManager>();
|
||||
zoneManager->initialize();
|
||||
|
||||
// Create music manager (initialized later with asset manager)
|
||||
musicManager = std::make_unique<audio::MusicManager>();
|
||||
|
||||
LOG_INFO("Renderer initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Renderer::shutdown() {
|
||||
if (terrainManager) {
|
||||
terrainManager->unloadAll();
|
||||
terrainManager.reset();
|
||||
}
|
||||
|
||||
if (terrainRenderer) {
|
||||
terrainRenderer->shutdown();
|
||||
terrainRenderer.reset();
|
||||
}
|
||||
|
||||
if (waterRenderer) {
|
||||
waterRenderer->shutdown();
|
||||
waterRenderer.reset();
|
||||
}
|
||||
|
||||
if (skybox) {
|
||||
skybox->shutdown();
|
||||
skybox.reset();
|
||||
}
|
||||
|
||||
if (celestial) {
|
||||
celestial->shutdown();
|
||||
celestial.reset();
|
||||
}
|
||||
|
||||
if (starField) {
|
||||
starField->shutdown();
|
||||
starField.reset();
|
||||
}
|
||||
|
||||
if (clouds) {
|
||||
clouds.reset();
|
||||
}
|
||||
|
||||
if (lensFlare) {
|
||||
lensFlare.reset();
|
||||
}
|
||||
|
||||
if (weather) {
|
||||
weather.reset();
|
||||
}
|
||||
|
||||
if (swimEffects) {
|
||||
swimEffects->shutdown();
|
||||
swimEffects.reset();
|
||||
}
|
||||
|
||||
if (characterRenderer) {
|
||||
characterRenderer->shutdown();
|
||||
characterRenderer.reset();
|
||||
}
|
||||
|
||||
if (wmoRenderer) {
|
||||
wmoRenderer->shutdown();
|
||||
wmoRenderer.reset();
|
||||
}
|
||||
|
||||
if (m2Renderer) {
|
||||
m2Renderer->shutdown();
|
||||
m2Renderer.reset();
|
||||
}
|
||||
|
||||
if (musicManager) {
|
||||
musicManager->shutdown();
|
||||
musicManager.reset();
|
||||
}
|
||||
|
||||
zoneManager.reset();
|
||||
|
||||
performanceHUD.reset();
|
||||
scene.reset();
|
||||
cameraController.reset();
|
||||
camera.reset();
|
||||
|
||||
LOG_INFO("Renderer shutdown");
|
||||
}
|
||||
|
||||
void Renderer::beginFrame() {
|
||||
// Black background (skybox will render over it)
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
void Renderer::endFrame() {
|
||||
// Nothing needed here for now
|
||||
}
|
||||
|
||||
void Renderer::setCharacterFollow(uint32_t instanceId) {
|
||||
characterInstanceId = instanceId;
|
||||
if (cameraController && instanceId > 0) {
|
||||
cameraController->setFollowTarget(&characterPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::updateCharacterAnimation() {
|
||||
// WoW WotLK AnimationData.dbc IDs
|
||||
constexpr uint32_t ANIM_STAND = 0;
|
||||
constexpr uint32_t ANIM_WALK = 4;
|
||||
constexpr uint32_t ANIM_RUN = 5;
|
||||
constexpr uint32_t ANIM_JUMP_START = 37;
|
||||
constexpr uint32_t ANIM_JUMP_MID = 38;
|
||||
constexpr uint32_t ANIM_JUMP_END = 39;
|
||||
constexpr uint32_t ANIM_SIT_DOWN = 97; // SitGround — transition to sitting
|
||||
constexpr uint32_t ANIM_SITTING = 97; // Hold on same animation (no separate idle)
|
||||
constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle)
|
||||
constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim)
|
||||
|
||||
CharAnimState newState = charAnimState;
|
||||
|
||||
bool moving = cameraController->isMoving();
|
||||
bool grounded = cameraController->isGrounded();
|
||||
bool jumping = cameraController->isJumping();
|
||||
bool sprinting = cameraController->isSprinting();
|
||||
bool sitting = cameraController->isSitting();
|
||||
bool swim = cameraController->isSwimming();
|
||||
|
||||
switch (charAnimState) {
|
||||
case CharAnimState::IDLE:
|
||||
if (swim) {
|
||||
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
|
||||
} else if (sitting && grounded) {
|
||||
newState = CharAnimState::SIT_DOWN;
|
||||
} else if (!grounded && jumping) {
|
||||
newState = CharAnimState::JUMP_START;
|
||||
} else if (!grounded) {
|
||||
newState = CharAnimState::JUMP_MID;
|
||||
} else if (moving && sprinting) {
|
||||
newState = CharAnimState::RUN;
|
||||
} else if (moving) {
|
||||
newState = CharAnimState::WALK;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::WALK:
|
||||
if (swim) {
|
||||
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
|
||||
} else if (!grounded && jumping) {
|
||||
newState = CharAnimState::JUMP_START;
|
||||
} else if (!grounded) {
|
||||
newState = CharAnimState::JUMP_MID;
|
||||
} else if (!moving) {
|
||||
newState = CharAnimState::IDLE;
|
||||
} else if (sprinting) {
|
||||
newState = CharAnimState::RUN;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::RUN:
|
||||
if (swim) {
|
||||
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
|
||||
} else if (!grounded && jumping) {
|
||||
newState = CharAnimState::JUMP_START;
|
||||
} else if (!grounded) {
|
||||
newState = CharAnimState::JUMP_MID;
|
||||
} else if (!moving) {
|
||||
newState = CharAnimState::IDLE;
|
||||
} else if (!sprinting) {
|
||||
newState = CharAnimState::WALK;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::JUMP_START:
|
||||
if (swim) {
|
||||
newState = CharAnimState::SWIM_IDLE;
|
||||
} else if (grounded) {
|
||||
newState = CharAnimState::JUMP_END;
|
||||
} else {
|
||||
newState = CharAnimState::JUMP_MID;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::JUMP_MID:
|
||||
if (swim) {
|
||||
newState = CharAnimState::SWIM_IDLE;
|
||||
} else if (grounded) {
|
||||
newState = CharAnimState::JUMP_END;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::JUMP_END:
|
||||
if (swim) {
|
||||
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
|
||||
} else if (moving && sprinting) {
|
||||
newState = CharAnimState::RUN;
|
||||
} else if (moving) {
|
||||
newState = CharAnimState::WALK;
|
||||
} else {
|
||||
newState = CharAnimState::IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::SIT_DOWN:
|
||||
if (swim) {
|
||||
newState = CharAnimState::SWIM_IDLE;
|
||||
} else if (!sitting) {
|
||||
newState = CharAnimState::IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::SITTING:
|
||||
if (swim) {
|
||||
newState = CharAnimState::SWIM_IDLE;
|
||||
} else if (!sitting) {
|
||||
newState = CharAnimState::IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::EMOTE:
|
||||
if (swim) {
|
||||
cancelEmote();
|
||||
newState = CharAnimState::SWIM_IDLE;
|
||||
} else if (jumping || !grounded) {
|
||||
cancelEmote();
|
||||
newState = CharAnimState::JUMP_START;
|
||||
} else if (moving) {
|
||||
cancelEmote();
|
||||
newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK;
|
||||
} else if (sitting) {
|
||||
cancelEmote();
|
||||
newState = CharAnimState::SIT_DOWN;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::SWIM_IDLE:
|
||||
if (!swim) {
|
||||
newState = moving ? CharAnimState::WALK : CharAnimState::IDLE;
|
||||
} else if (moving) {
|
||||
newState = CharAnimState::SWIM;
|
||||
}
|
||||
break;
|
||||
|
||||
case CharAnimState::SWIM:
|
||||
if (!swim) {
|
||||
newState = moving ? CharAnimState::WALK : CharAnimState::IDLE;
|
||||
} else if (!moving) {
|
||||
newState = CharAnimState::SWIM_IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (newState != charAnimState) {
|
||||
charAnimState = newState;
|
||||
|
||||
uint32_t animId = ANIM_STAND;
|
||||
bool loop = true;
|
||||
|
||||
switch (charAnimState) {
|
||||
case CharAnimState::IDLE: animId = ANIM_STAND; loop = true; break;
|
||||
case CharAnimState::WALK: animId = ANIM_WALK; loop = true; break;
|
||||
case CharAnimState::RUN: animId = ANIM_RUN; loop = true; break;
|
||||
case CharAnimState::JUMP_START: animId = ANIM_JUMP_START; loop = false; break;
|
||||
case CharAnimState::JUMP_MID: animId = ANIM_JUMP_MID; loop = false; break;
|
||||
case CharAnimState::JUMP_END: animId = ANIM_JUMP_END; loop = false; break;
|
||||
case CharAnimState::SIT_DOWN: animId = ANIM_SIT_DOWN; loop = false; break;
|
||||
case CharAnimState::SITTING: animId = ANIM_SITTING; loop = true; break;
|
||||
case CharAnimState::EMOTE: animId = emoteAnimId; loop = emoteLoop; break;
|
||||
case CharAnimState::SWIM_IDLE: animId = ANIM_SWIM_IDLE; loop = true; break;
|
||||
case CharAnimState::SWIM: animId = ANIM_SWIM; loop = true; break;
|
||||
}
|
||||
|
||||
characterRenderer->playAnimation(characterInstanceId, animId, loop);
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::playEmote(const std::string& emoteName) {
|
||||
auto it = EMOTE_TABLE.find(emoteName);
|
||||
if (it == EMOTE_TABLE.end()) return;
|
||||
|
||||
const auto& info = it->second;
|
||||
emoteActive = true;
|
||||
emoteAnimId = info.animId;
|
||||
emoteLoop = info.loop;
|
||||
charAnimState = CharAnimState::EMOTE;
|
||||
|
||||
if (characterRenderer && characterInstanceId > 0) {
|
||||
characterRenderer->playAnimation(characterInstanceId, emoteAnimId, emoteLoop);
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::cancelEmote() {
|
||||
emoteActive = false;
|
||||
emoteAnimId = 0;
|
||||
emoteLoop = false;
|
||||
}
|
||||
|
||||
std::string Renderer::getEmoteText(const std::string& emoteName) {
|
||||
auto it = EMOTE_TABLE.find(emoteName);
|
||||
if (it != EMOTE_TABLE.end()) {
|
||||
return it->second.text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void Renderer::setTargetPosition(const glm::vec3* pos) {
|
||||
targetPosition = pos;
|
||||
}
|
||||
|
||||
bool Renderer::isMoving() const {
|
||||
return cameraController && cameraController->isMoving();
|
||||
}
|
||||
|
||||
void Renderer::update(float deltaTime) {
|
||||
if (cameraController) {
|
||||
cameraController->update(deltaTime);
|
||||
}
|
||||
|
||||
// Sync character model position/rotation and animation with follow target
|
||||
if (characterInstanceId > 0 && characterRenderer && cameraController && cameraController->isThirdPerson()) {
|
||||
characterRenderer->setInstancePosition(characterInstanceId, characterPosition);
|
||||
|
||||
// Only rotate character to face camera direction when right-click is held
|
||||
// Left-click orbits camera without turning the character
|
||||
if (cameraController->isRightMouseHeld() || cameraController->isMoving()) {
|
||||
characterYaw = cameraController->getYaw();
|
||||
} else if (targetPosition && !emoteActive && !cameraController->isMoving()) {
|
||||
// Face target when idle
|
||||
glm::vec3 toTarget = *targetPosition - characterPosition;
|
||||
if (glm::length(glm::vec2(toTarget.x, toTarget.y)) > 0.1f) {
|
||||
float targetYaw = glm::degrees(std::atan2(toTarget.y, toTarget.x));
|
||||
// Smooth rotation toward target
|
||||
float diff = targetYaw - characterYaw;
|
||||
while (diff > 180.0f) diff -= 360.0f;
|
||||
while (diff < -180.0f) diff += 360.0f;
|
||||
float rotSpeed = 360.0f * deltaTime;
|
||||
if (std::abs(diff) < rotSpeed) {
|
||||
characterYaw = targetYaw;
|
||||
} else {
|
||||
characterYaw += (diff > 0 ? rotSpeed : -rotSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
float yawRad = glm::radians(characterYaw);
|
||||
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad));
|
||||
|
||||
// Update animation based on movement state
|
||||
updateCharacterAnimation();
|
||||
}
|
||||
|
||||
// Update terrain streaming
|
||||
if (terrainManager && camera) {
|
||||
terrainManager->update(*camera, deltaTime);
|
||||
}
|
||||
|
||||
// Update skybox time progression
|
||||
if (skybox) {
|
||||
skybox->update(deltaTime);
|
||||
}
|
||||
|
||||
// Update star field twinkle
|
||||
if (starField) {
|
||||
starField->update(deltaTime);
|
||||
}
|
||||
|
||||
// Update clouds animation
|
||||
if (clouds) {
|
||||
clouds->update(deltaTime);
|
||||
}
|
||||
|
||||
// Update celestial (moon phase cycling)
|
||||
if (celestial) {
|
||||
celestial->update(deltaTime);
|
||||
}
|
||||
|
||||
// Update weather particles
|
||||
if (weather && camera) {
|
||||
weather->update(*camera, deltaTime);
|
||||
}
|
||||
|
||||
// Update swim effects
|
||||
if (swimEffects && camera && cameraController && waterRenderer) {
|
||||
swimEffects->update(*camera, *cameraController, *waterRenderer, deltaTime);
|
||||
}
|
||||
|
||||
// Update character animations
|
||||
if (characterRenderer) {
|
||||
characterRenderer->update(deltaTime);
|
||||
}
|
||||
|
||||
// Update zone detection and music
|
||||
if (zoneManager && musicManager && terrainManager && camera) {
|
||||
// First check tile-based zone
|
||||
auto tile = terrainManager->getCurrentTile();
|
||||
uint32_t zoneId = zoneManager->getZoneId(tile.x, tile.y);
|
||||
|
||||
|
||||
|
||||
// Override with WMO-based detection (e.g., inside Stormwind)
|
||||
if (wmoRenderer) {
|
||||
glm::vec3 camPos = camera->getPosition();
|
||||
uint32_t wmoModelId = 0;
|
||||
if (wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &wmoModelId)) {
|
||||
// Check if inside Stormwind WMO (model ID 10047)
|
||||
if (wmoModelId == 10047) {
|
||||
zoneId = 1519; // Stormwind City
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (zoneId != currentZoneId && zoneId != 0) {
|
||||
currentZoneId = zoneId;
|
||||
auto* info = zoneManager->getZoneInfo(zoneId);
|
||||
if (info) {
|
||||
currentZoneName = info->name;
|
||||
LOG_INFO("Entered zone: ", info->name);
|
||||
std::string music = zoneManager->getRandomMusic(zoneId);
|
||||
if (!music.empty()) {
|
||||
musicManager->crossfadeTo(music);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
musicManager->update(deltaTime);
|
||||
}
|
||||
|
||||
// Update performance HUD
|
||||
if (performanceHUD) {
|
||||
performanceHUD->update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::renderWorld(game::World* world) {
|
||||
(void)world; // Unused for now
|
||||
|
||||
// Get time of day for sky-related rendering
|
||||
float timeOfDay = skybox ? skybox->getTimeOfDay() : 12.0f;
|
||||
|
||||
// Render skybox first (furthest back)
|
||||
if (skybox && camera) {
|
||||
skybox->render(*camera, timeOfDay);
|
||||
}
|
||||
|
||||
// Render stars after skybox
|
||||
if (starField && camera) {
|
||||
starField->render(*camera, timeOfDay);
|
||||
}
|
||||
|
||||
// Render celestial bodies (sun/moon) after stars
|
||||
if (celestial && camera) {
|
||||
celestial->render(*camera, timeOfDay);
|
||||
}
|
||||
|
||||
// Render clouds after celestial bodies
|
||||
if (clouds && camera) {
|
||||
clouds->render(*camera, timeOfDay);
|
||||
}
|
||||
|
||||
// Render lens flare (screen-space effect, render after celestial bodies)
|
||||
if (lensFlare && camera && celestial) {
|
||||
glm::vec3 sunPosition = celestial->getSunPosition(timeOfDay);
|
||||
lensFlare->render(*camera, sunPosition, timeOfDay);
|
||||
}
|
||||
|
||||
// Render terrain if loaded and enabled
|
||||
if (terrainEnabled && terrainLoaded && terrainRenderer && camera) {
|
||||
// Check if camera is underwater for fog override
|
||||
bool underwater = false;
|
||||
if (waterRenderer && camera) {
|
||||
glm::vec3 camPos = camera->getPosition();
|
||||
auto waterH = waterRenderer->getWaterHeightAt(camPos.x, camPos.y);
|
||||
if (waterH && camPos.z < *waterH) {
|
||||
underwater = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (underwater) {
|
||||
float fogColor[3] = {0.05f, 0.15f, 0.25f};
|
||||
terrainRenderer->setFog(fogColor, 10.0f, 200.0f);
|
||||
glClearColor(0.05f, 0.15f, 0.25f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT); // Re-clear with underwater color
|
||||
} else if (skybox) {
|
||||
// Update terrain fog based on time of day (match sky color)
|
||||
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
|
||||
float fogColorArray[3] = {horizonColor.r, horizonColor.g, horizonColor.b};
|
||||
terrainRenderer->setFog(fogColorArray, 400.0f, 1200.0f);
|
||||
}
|
||||
|
||||
terrainRenderer->render(*camera);
|
||||
|
||||
// Render water after terrain (transparency requires back-to-front rendering)
|
||||
if (waterRenderer) {
|
||||
// Use accumulated time for water animation
|
||||
static float time = 0.0f;
|
||||
time += 0.016f; // Approximate frame time
|
||||
waterRenderer->render(*camera, time);
|
||||
}
|
||||
}
|
||||
|
||||
// Render weather particles (after terrain/water, before characters)
|
||||
if (weather && camera) {
|
||||
weather->render(*camera);
|
||||
}
|
||||
|
||||
// Render swim effects (ripples and bubbles)
|
||||
if (swimEffects && camera) {
|
||||
swimEffects->render(*camera);
|
||||
}
|
||||
|
||||
// Render characters (after weather)
|
||||
if (characterRenderer && camera) {
|
||||
glm::mat4 view = camera->getViewMatrix();
|
||||
glm::mat4 projection = camera->getProjectionMatrix();
|
||||
characterRenderer->render(*camera, view, projection);
|
||||
}
|
||||
|
||||
// Render WMO buildings (after characters, before UI)
|
||||
if (wmoRenderer && camera) {
|
||||
glm::mat4 view = camera->getViewMatrix();
|
||||
glm::mat4 projection = camera->getProjectionMatrix();
|
||||
wmoRenderer->render(*camera, view, projection);
|
||||
}
|
||||
|
||||
// Render M2 doodads (trees, rocks, etc.)
|
||||
if (m2Renderer && camera) {
|
||||
glm::mat4 view = camera->getViewMatrix();
|
||||
glm::mat4 projection = camera->getProjectionMatrix();
|
||||
m2Renderer->render(*camera, view, projection);
|
||||
}
|
||||
|
||||
// Render minimap overlay
|
||||
if (minimap && camera && window) {
|
||||
minimap->render(*camera, window->getWidth(), window->getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::string& adtPath) {
|
||||
if (!assetManager) {
|
||||
LOG_ERROR("Asset manager is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Loading test terrain: ", adtPath);
|
||||
|
||||
// Create terrain renderer if not already created
|
||||
if (!terrainRenderer) {
|
||||
terrainRenderer = std::make_unique<TerrainRenderer>();
|
||||
if (!terrainRenderer->initialize(assetManager)) {
|
||||
LOG_ERROR("Failed to initialize terrain renderer");
|
||||
terrainRenderer.reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create and initialize terrain manager
|
||||
if (!terrainManager) {
|
||||
terrainManager = std::make_unique<TerrainManager>();
|
||||
if (!terrainManager->initialize(assetManager, terrainRenderer.get())) {
|
||||
LOG_ERROR("Failed to initialize terrain manager");
|
||||
terrainManager.reset();
|
||||
return false;
|
||||
}
|
||||
// Set water renderer for terrain streaming
|
||||
if (waterRenderer) {
|
||||
terrainManager->setWaterRenderer(waterRenderer.get());
|
||||
}
|
||||
// Set M2 renderer for doodad loading during streaming
|
||||
if (m2Renderer) {
|
||||
terrainManager->setM2Renderer(m2Renderer.get());
|
||||
}
|
||||
// Set WMO renderer for building loading during streaming
|
||||
if (wmoRenderer) {
|
||||
terrainManager->setWMORenderer(wmoRenderer.get());
|
||||
}
|
||||
// Pass asset manager to character renderer for texture loading
|
||||
if (characterRenderer) {
|
||||
characterRenderer->setAssetManager(assetManager);
|
||||
}
|
||||
// Wire terrain renderer to minimap
|
||||
if (minimap) {
|
||||
minimap->setTerrainRenderer(terrainRenderer.get());
|
||||
}
|
||||
// Wire terrain manager, WMO renderer, and water renderer to camera controller
|
||||
if (cameraController) {
|
||||
cameraController->setTerrainManager(terrainManager.get());
|
||||
if (wmoRenderer) {
|
||||
cameraController->setWMORenderer(wmoRenderer.get());
|
||||
}
|
||||
if (waterRenderer) {
|
||||
cameraController->setWaterRenderer(waterRenderer.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tile coordinates from ADT path
|
||||
// Format: World\Maps\{MapName}\{MapName}_{X}_{Y}.adt
|
||||
int tileX = 32, tileY = 49; // defaults
|
||||
{
|
||||
// Find last path separator
|
||||
size_t lastSep = adtPath.find_last_of("\\/");
|
||||
if (lastSep != std::string::npos) {
|
||||
std::string filename = adtPath.substr(lastSep + 1);
|
||||
// Find first underscore after map name
|
||||
size_t firstUnderscore = filename.find('_');
|
||||
if (firstUnderscore != std::string::npos) {
|
||||
size_t secondUnderscore = filename.find('_', firstUnderscore + 1);
|
||||
if (secondUnderscore != std::string::npos) {
|
||||
size_t dot = filename.find('.', secondUnderscore);
|
||||
if (dot != std::string::npos) {
|
||||
tileX = std::stoi(filename.substr(firstUnderscore + 1, secondUnderscore - firstUnderscore - 1));
|
||||
tileY = std::stoi(filename.substr(secondUnderscore + 1, dot - secondUnderscore - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Extract map name
|
||||
std::string mapName = filename.substr(0, firstUnderscore != std::string::npos ? firstUnderscore : filename.size());
|
||||
terrainManager->setMapName(mapName);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Loading initial tile [", tileX, ",", tileY, "] via terrain manager");
|
||||
|
||||
// Load the initial tile through TerrainManager (properly tracked for streaming)
|
||||
if (!terrainManager->loadTile(tileX, tileY)) {
|
||||
LOG_ERROR("Failed to load initial tile [", tileX, ",", tileY, "]");
|
||||
return false;
|
||||
}
|
||||
|
||||
terrainLoaded = true;
|
||||
|
||||
// Initialize music manager with asset manager
|
||||
if (musicManager && assetManager && !cachedAssetManager) {
|
||||
musicManager->initialize(assetManager);
|
||||
cachedAssetManager = assetManager;
|
||||
}
|
||||
|
||||
// Snap camera to ground now that terrain is loaded
|
||||
if (cameraController) {
|
||||
cameraController->reset();
|
||||
}
|
||||
|
||||
LOG_INFO("Test terrain loaded successfully!");
|
||||
LOG_INFO(" Chunks: ", terrainRenderer->getChunkCount());
|
||||
LOG_INFO(" Triangles: ", terrainRenderer->getTriangleCount());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Renderer::setWireframeMode(bool enabled) {
|
||||
if (terrainRenderer) {
|
||||
terrainRenderer->setWireframe(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int centerY, int radius) {
|
||||
// Create terrain renderer if not already created
|
||||
if (!terrainRenderer) {
|
||||
LOG_ERROR("Terrain renderer not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create terrain manager if not already created
|
||||
if (!terrainManager) {
|
||||
terrainManager = std::make_unique<TerrainManager>();
|
||||
// Wire terrain manager to camera controller for grounding
|
||||
if (cameraController) {
|
||||
cameraController->setTerrainManager(terrainManager.get());
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Loading terrain area: ", mapName, " [", centerX, ",", centerY, "] radius=", radius);
|
||||
|
||||
terrainManager->setMapName(mapName);
|
||||
terrainManager->setLoadRadius(radius);
|
||||
terrainManager->setUnloadRadius(radius + 1);
|
||||
|
||||
// Load tiles in radius
|
||||
for (int dy = -radius; dy <= radius; dy++) {
|
||||
for (int dx = -radius; dx <= radius; dx++) {
|
||||
int tileX = centerX + dx;
|
||||
int tileY = centerY + dy;
|
||||
|
||||
if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) {
|
||||
terrainManager->loadTile(tileX, tileY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
terrainLoaded = true;
|
||||
|
||||
// Initialize music manager with asset manager (if available from loadTestTerrain)
|
||||
if (musicManager && cachedAssetManager) {
|
||||
if (!musicManager->isInitialized()) {
|
||||
musicManager->initialize(cachedAssetManager);
|
||||
}
|
||||
}
|
||||
|
||||
// Wire WMO and water renderer to camera controller
|
||||
if (cameraController && wmoRenderer) {
|
||||
cameraController->setWMORenderer(wmoRenderer.get());
|
||||
}
|
||||
if (cameraController && waterRenderer) {
|
||||
cameraController->setWaterRenderer(waterRenderer.get());
|
||||
}
|
||||
|
||||
// Snap camera to ground now that terrain is loaded
|
||||
if (cameraController) {
|
||||
cameraController->reset();
|
||||
}
|
||||
|
||||
LOG_INFO("Terrain area loaded: ", terrainManager->getLoadedTileCount(), " tiles");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Renderer::setTerrainStreaming(bool enabled) {
|
||||
if (terrainManager) {
|
||||
terrainManager->setStreamingEnabled(enabled);
|
||||
LOG_INFO("Terrain streaming: ", enabled ? "ON" : "OFF");
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::renderHUD() {
|
||||
if (performanceHUD && camera) {
|
||||
performanceHUD->render(this, camera.get());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
24
src/rendering/scene.cpp
Normal file
24
src/rendering/scene.cpp
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#include "rendering/scene.hpp"
|
||||
#include "rendering/mesh.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
void Scene::addMesh(std::shared_ptr<Mesh> mesh) {
|
||||
meshes.push_back(mesh);
|
||||
}
|
||||
|
||||
void Scene::removeMesh(std::shared_ptr<Mesh> mesh) {
|
||||
auto it = std::find(meshes.begin(), meshes.end(), mesh);
|
||||
if (it != meshes.end()) {
|
||||
meshes.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::clear() {
|
||||
meshes.clear();
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
127
src/rendering/shader.cpp
Normal file
127
src/rendering/shader.cpp
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
#include "rendering/shader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
Shader::~Shader() {
|
||||
if (program) glDeleteProgram(program);
|
||||
if (vertexShader) glDeleteShader(vertexShader);
|
||||
if (fragmentShader) glDeleteShader(fragmentShader);
|
||||
}
|
||||
|
||||
bool Shader::loadFromFile(const std::string& vertexPath, const std::string& fragmentPath) {
|
||||
// Load vertex shader
|
||||
std::ifstream vFile(vertexPath);
|
||||
if (!vFile.is_open()) {
|
||||
LOG_ERROR("Failed to open vertex shader: ", vertexPath);
|
||||
return false;
|
||||
}
|
||||
std::stringstream vStream;
|
||||
vStream << vFile.rdbuf();
|
||||
std::string vertexSource = vStream.str();
|
||||
|
||||
// Load fragment shader
|
||||
std::ifstream fFile(fragmentPath);
|
||||
if (!fFile.is_open()) {
|
||||
LOG_ERROR("Failed to open fragment shader: ", fragmentPath);
|
||||
return false;
|
||||
}
|
||||
std::stringstream fStream;
|
||||
fStream << fFile.rdbuf();
|
||||
std::string fragmentSource = fStream.str();
|
||||
|
||||
return compile(vertexSource, fragmentSource);
|
||||
}
|
||||
|
||||
bool Shader::loadFromSource(const std::string& vertexSource, const std::string& fragmentSource) {
|
||||
return compile(vertexSource, fragmentSource);
|
||||
}
|
||||
|
||||
bool Shader::compile(const std::string& vertexSource, const std::string& fragmentSource) {
|
||||
GLint success;
|
||||
GLchar infoLog[512];
|
||||
|
||||
// Compile vertex shader
|
||||
const char* vCode = vertexSource.c_str();
|
||||
vertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vertexShader, 1, &vCode, nullptr);
|
||||
glCompileShader(vertexShader);
|
||||
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
|
||||
if (!success) {
|
||||
glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
|
||||
LOG_ERROR("Vertex shader compilation failed: ", infoLog);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compile fragment shader
|
||||
const char* fCode = fragmentSource.c_str();
|
||||
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fragmentShader, 1, &fCode, nullptr);
|
||||
glCompileShader(fragmentShader);
|
||||
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
|
||||
if (!success) {
|
||||
glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
|
||||
LOG_ERROR("Fragment shader compilation failed: ", infoLog);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Link program
|
||||
program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glAttachShader(program, fragmentShader);
|
||||
glLinkProgram(program);
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &success);
|
||||
if (!success) {
|
||||
glGetProgramInfoLog(program, 512, nullptr, infoLog);
|
||||
LOG_ERROR("Shader program linking failed: ", infoLog);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Shader::use() const {
|
||||
glUseProgram(program);
|
||||
}
|
||||
|
||||
void Shader::unuse() const {
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
GLint Shader::getUniformLocation(const std::string& name) const {
|
||||
return glGetUniformLocation(program, name.c_str());
|
||||
}
|
||||
|
||||
void Shader::setUniform(const std::string& name, int value) {
|
||||
glUniform1i(getUniformLocation(name), value);
|
||||
}
|
||||
|
||||
void Shader::setUniform(const std::string& name, float value) {
|
||||
glUniform1f(getUniformLocation(name), value);
|
||||
}
|
||||
|
||||
void Shader::setUniform(const std::string& name, const glm::vec2& value) {
|
||||
glUniform2fv(getUniformLocation(name), 1, &value[0]);
|
||||
}
|
||||
|
||||
void Shader::setUniform(const std::string& name, const glm::vec3& value) {
|
||||
glUniform3fv(getUniformLocation(name), 1, &value[0]);
|
||||
}
|
||||
|
||||
void Shader::setUniform(const std::string& name, const glm::vec4& value) {
|
||||
glUniform4fv(getUniformLocation(name), 1, &value[0]);
|
||||
}
|
||||
|
||||
void Shader::setUniform(const std::string& name, const glm::mat3& value) {
|
||||
glUniformMatrix3fv(getUniformLocation(name), 1, GL_FALSE, &value[0][0]);
|
||||
}
|
||||
|
||||
void Shader::setUniform(const std::string& name, const glm::mat4& value) {
|
||||
glUniformMatrix4fv(getUniformLocation(name), 1, GL_FALSE, &value[0][0]);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
334
src/rendering/skybox.cpp
Normal file
334
src/rendering/skybox.cpp
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
#include "rendering/skybox.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
Skybox::Skybox() = default;
|
||||
|
||||
Skybox::~Skybox() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool Skybox::initialize() {
|
||||
LOG_INFO("Initializing skybox");
|
||||
|
||||
// Create sky shader
|
||||
skyShader = std::make_unique<Shader>();
|
||||
|
||||
// Vertex shader - position-only skybox
|
||||
const char* vertexShaderSource = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
|
||||
uniform mat4 view;
|
||||
uniform mat4 projection;
|
||||
|
||||
out vec3 WorldPos;
|
||||
out float Altitude;
|
||||
|
||||
void main() {
|
||||
WorldPos = aPos;
|
||||
|
||||
// Calculate altitude (0 at horizon, 1 at zenith)
|
||||
Altitude = normalize(aPos).z;
|
||||
|
||||
// Remove translation from view matrix (keep rotation only)
|
||||
mat4 viewNoTranslation = mat4(mat3(view));
|
||||
|
||||
gl_Position = projection * viewNoTranslation * vec4(aPos, 1.0);
|
||||
|
||||
// Ensure skybox is always at far plane
|
||||
gl_Position = gl_Position.xyww;
|
||||
}
|
||||
)";
|
||||
|
||||
// Fragment shader - gradient sky with time of day
|
||||
const char* fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
in vec3 WorldPos;
|
||||
in float Altitude;
|
||||
|
||||
uniform vec3 horizonColor;
|
||||
uniform vec3 zenithColor;
|
||||
uniform float timeOfDay;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
// Smooth gradient from horizon to zenith
|
||||
float t = pow(max(Altitude, 0.0), 0.5); // Curve for more interesting gradient
|
||||
|
||||
vec3 skyColor = mix(horizonColor, zenithColor, t);
|
||||
|
||||
// Add atmospheric scattering effect (more saturated near horizon)
|
||||
float scattering = 1.0 - t * 0.3;
|
||||
skyColor *= scattering;
|
||||
|
||||
FragColor = vec4(skyColor, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
if (!skyShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
|
||||
LOG_ERROR("Failed to create sky shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create sky dome mesh
|
||||
createSkyDome();
|
||||
|
||||
LOG_INFO("Skybox initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Skybox::shutdown() {
|
||||
destroySkyDome();
|
||||
skyShader.reset();
|
||||
}
|
||||
|
||||
void Skybox::render(const Camera& camera, float time) {
|
||||
if (!renderingEnabled || vao == 0 || !skyShader) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Render skybox first (before terrain), with depth test set to LEQUAL
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
|
||||
skyShader->use();
|
||||
|
||||
// Set uniforms
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 projection = camera.getProjectionMatrix();
|
||||
|
||||
skyShader->setUniform("view", view);
|
||||
skyShader->setUniform("projection", projection);
|
||||
skyShader->setUniform("timeOfDay", time);
|
||||
|
||||
// Get colors based on time of day
|
||||
glm::vec3 horizon = getHorizonColor(time);
|
||||
glm::vec3 zenith = getZenithColor(time);
|
||||
|
||||
skyShader->setUniform("horizonColor", horizon);
|
||||
skyShader->setUniform("zenithColor", zenith);
|
||||
|
||||
// Render dome
|
||||
glBindVertexArray(vao);
|
||||
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, nullptr);
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Restore depth function
|
||||
glDepthFunc(GL_LESS);
|
||||
}
|
||||
|
||||
void Skybox::update(float deltaTime) {
|
||||
if (timeProgressionEnabled) {
|
||||
timeOfDay += deltaTime * timeSpeed;
|
||||
|
||||
// Wrap around 24 hours
|
||||
if (timeOfDay >= 24.0f) {
|
||||
timeOfDay -= 24.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Skybox::setTimeOfDay(float time) {
|
||||
// Clamp to 0-24 range
|
||||
while (time < 0.0f) time += 24.0f;
|
||||
while (time >= 24.0f) time -= 24.0f;
|
||||
|
||||
timeOfDay = time;
|
||||
}
|
||||
|
||||
void Skybox::createSkyDome() {
|
||||
// Create an extended dome that goes below horizon for better coverage
|
||||
const int rings = 16; // Vertical resolution
|
||||
const int sectors = 32; // Horizontal resolution
|
||||
const float radius = 2000.0f; // Large enough to cover view without looking curved
|
||||
|
||||
std::vector<float> vertices;
|
||||
std::vector<uint32_t> indices;
|
||||
|
||||
// Generate vertices - extend slightly below horizon
|
||||
const float minPhi = -M_PI / 12.0f; // Start 15° below horizon
|
||||
const float maxPhi = M_PI / 2.0f; // End at zenith
|
||||
for (int ring = 0; ring <= rings; ring++) {
|
||||
float phi = minPhi + (maxPhi - minPhi) * (static_cast<float>(ring) / rings);
|
||||
float y = radius * std::sin(phi);
|
||||
float ringRadius = radius * std::cos(phi);
|
||||
|
||||
for (int sector = 0; sector <= sectors; sector++) {
|
||||
float theta = (2.0f * M_PI) * (static_cast<float>(sector) / sectors);
|
||||
float x = ringRadius * std::cos(theta);
|
||||
float z = ringRadius * std::sin(theta);
|
||||
|
||||
// Position
|
||||
vertices.push_back(x);
|
||||
vertices.push_back(z); // Z up in WoW coordinates
|
||||
vertices.push_back(y);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate indices
|
||||
for (int ring = 0; ring < rings; ring++) {
|
||||
for (int sector = 0; sector < sectors; sector++) {
|
||||
int current = ring * (sectors + 1) + sector;
|
||||
int next = current + sectors + 1;
|
||||
|
||||
// Two triangles per quad
|
||||
indices.push_back(current);
|
||||
indices.push_back(next);
|
||||
indices.push_back(current + 1);
|
||||
|
||||
indices.push_back(current + 1);
|
||||
indices.push_back(next);
|
||||
indices.push_back(next + 1);
|
||||
}
|
||||
}
|
||||
|
||||
indexCount = static_cast<int>(indices.size());
|
||||
|
||||
// Create OpenGL buffers
|
||||
glGenVertexArrays(1, &vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glGenBuffers(1, &ebo);
|
||||
|
||||
glBindVertexArray(vao);
|
||||
|
||||
// Upload vertex data
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// Upload index data
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// Set vertex attributes (position only)
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
LOG_DEBUG("Sky dome created: ", (rings + 1) * (sectors + 1), " vertices, ", indexCount / 3, " triangles");
|
||||
}
|
||||
|
||||
void Skybox::destroySkyDome() {
|
||||
if (vao != 0) {
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
vao = 0;
|
||||
}
|
||||
if (vbo != 0) {
|
||||
glDeleteBuffers(1, &vbo);
|
||||
vbo = 0;
|
||||
}
|
||||
if (ebo != 0) {
|
||||
glDeleteBuffers(1, &ebo);
|
||||
ebo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 Skybox::getHorizonColor(float time) const {
|
||||
// Time-based horizon colors
|
||||
// 0-6: Night (dark blue)
|
||||
// 6-8: Dawn (orange/pink)
|
||||
// 8-16: Day (light blue)
|
||||
// 16-18: Dusk (orange/red)
|
||||
// 18-24: Night (dark blue)
|
||||
|
||||
if (time < 5.0f || time >= 21.0f) {
|
||||
// Night - dark blue/purple horizon
|
||||
return glm::vec3(0.05f, 0.05f, 0.15f);
|
||||
}
|
||||
else if (time >= 5.0f && time < 7.0f) {
|
||||
// Dawn - blend from night to orange
|
||||
float t = (time - 5.0f) / 2.0f;
|
||||
glm::vec3 night = glm::vec3(0.05f, 0.05f, 0.15f);
|
||||
glm::vec3 dawn = glm::vec3(1.0f, 0.5f, 0.2f);
|
||||
return glm::mix(night, dawn, t);
|
||||
}
|
||||
else if (time >= 7.0f && time < 9.0f) {
|
||||
// Morning - blend from orange to blue
|
||||
float t = (time - 7.0f) / 2.0f;
|
||||
glm::vec3 dawn = glm::vec3(1.0f, 0.5f, 0.2f);
|
||||
glm::vec3 day = glm::vec3(0.6f, 0.7f, 0.9f);
|
||||
return glm::mix(dawn, day, t);
|
||||
}
|
||||
else if (time >= 9.0f && time < 17.0f) {
|
||||
// Day - light blue horizon
|
||||
return glm::vec3(0.6f, 0.7f, 0.9f);
|
||||
}
|
||||
else if (time >= 17.0f && time < 19.0f) {
|
||||
// Dusk - blend from blue to orange/red
|
||||
float t = (time - 17.0f) / 2.0f;
|
||||
glm::vec3 day = glm::vec3(0.6f, 0.7f, 0.9f);
|
||||
glm::vec3 dusk = glm::vec3(1.0f, 0.4f, 0.1f);
|
||||
return glm::mix(day, dusk, t);
|
||||
}
|
||||
else {
|
||||
// Evening - blend from orange to night
|
||||
float t = (time - 19.0f) / 2.0f;
|
||||
glm::vec3 dusk = glm::vec3(1.0f, 0.4f, 0.1f);
|
||||
glm::vec3 night = glm::vec3(0.05f, 0.05f, 0.15f);
|
||||
return glm::mix(dusk, night, t);
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 Skybox::getZenithColor(float time) const {
|
||||
// Zenith (top of sky) colors
|
||||
|
||||
if (time < 5.0f || time >= 21.0f) {
|
||||
// Night - very dark blue, almost black
|
||||
return glm::vec3(0.01f, 0.01f, 0.05f);
|
||||
}
|
||||
else if (time >= 5.0f && time < 7.0f) {
|
||||
// Dawn - blend from night to light blue
|
||||
float t = (time - 5.0f) / 2.0f;
|
||||
glm::vec3 night = glm::vec3(0.01f, 0.01f, 0.05f);
|
||||
glm::vec3 dawn = glm::vec3(0.3f, 0.4f, 0.7f);
|
||||
return glm::mix(night, dawn, t);
|
||||
}
|
||||
else if (time >= 7.0f && time < 9.0f) {
|
||||
// Morning - blend to bright blue
|
||||
float t = (time - 7.0f) / 2.0f;
|
||||
glm::vec3 dawn = glm::vec3(0.3f, 0.4f, 0.7f);
|
||||
glm::vec3 day = glm::vec3(0.2f, 0.5f, 1.0f);
|
||||
return glm::mix(dawn, day, t);
|
||||
}
|
||||
else if (time >= 9.0f && time < 17.0f) {
|
||||
// Day - bright blue zenith
|
||||
return glm::vec3(0.2f, 0.5f, 1.0f);
|
||||
}
|
||||
else if (time >= 17.0f && time < 19.0f) {
|
||||
// Dusk - blend to darker blue
|
||||
float t = (time - 17.0f) / 2.0f;
|
||||
glm::vec3 day = glm::vec3(0.2f, 0.5f, 1.0f);
|
||||
glm::vec3 dusk = glm::vec3(0.1f, 0.2f, 0.4f);
|
||||
return glm::mix(day, dusk, t);
|
||||
}
|
||||
else {
|
||||
// Evening - blend to night
|
||||
float t = (time - 19.0f) / 2.0f;
|
||||
glm::vec3 dusk = glm::vec3(0.1f, 0.2f, 0.4f);
|
||||
glm::vec3 night = glm::vec3(0.01f, 0.01f, 0.05f);
|
||||
return glm::mix(dusk, night, t);
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 Skybox::getSkyColor(float altitude, float time) const {
|
||||
// Blend between horizon and zenith based on altitude
|
||||
glm::vec3 horizon = getHorizonColor(time);
|
||||
glm::vec3 zenith = getZenithColor(time);
|
||||
|
||||
// Use power curve for more natural gradient
|
||||
float t = std::pow(std::max(altitude, 0.0f), 0.5f);
|
||||
|
||||
return glm::mix(horizon, zenith, t);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
259
src/rendering/starfield.cpp
Normal file
259
src/rendering/starfield.cpp
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
#include "rendering/starfield.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
StarField::StarField() = default;
|
||||
|
||||
StarField::~StarField() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool StarField::initialize() {
|
||||
LOG_INFO("Initializing star field");
|
||||
|
||||
// Create star shader
|
||||
starShader = std::make_unique<Shader>();
|
||||
|
||||
// Vertex shader - simple point rendering
|
||||
const char* vertexShaderSource = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in float aBrightness;
|
||||
layout (location = 2) in float aTwinklePhase;
|
||||
|
||||
uniform mat4 view;
|
||||
uniform mat4 projection;
|
||||
uniform float time;
|
||||
uniform float intensity;
|
||||
|
||||
out float Brightness;
|
||||
|
||||
void main() {
|
||||
// Remove translation from view matrix (stars are infinitely far)
|
||||
mat4 viewNoTranslation = mat4(mat3(view));
|
||||
|
||||
gl_Position = projection * viewNoTranslation * vec4(aPos, 1.0);
|
||||
|
||||
// Twinkle effect (subtle brightness variation)
|
||||
float twinkle = sin(time * 2.0 + aTwinklePhase) * 0.2 + 0.8; // 0.6 to 1.0
|
||||
|
||||
Brightness = aBrightness * twinkle * intensity;
|
||||
|
||||
// Point size based on brightness
|
||||
gl_PointSize = 2.0 + aBrightness * 2.0; // 2-4 pixels
|
||||
}
|
||||
)";
|
||||
|
||||
// Fragment shader - star color
|
||||
const char* fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
in float Brightness;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
// Circular point (not square)
|
||||
vec2 coord = gl_PointCoord - vec2(0.5);
|
||||
float dist = length(coord);
|
||||
if (dist > 0.5) {
|
||||
discard;
|
||||
}
|
||||
|
||||
// Soften edges
|
||||
float alpha = smoothstep(0.5, 0.3, dist);
|
||||
|
||||
// Star color (slightly blue-white)
|
||||
vec3 starColor = vec3(0.9, 0.95, 1.0);
|
||||
|
||||
FragColor = vec4(starColor * Brightness, alpha * Brightness);
|
||||
}
|
||||
)";
|
||||
|
||||
if (!starShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
|
||||
LOG_ERROR("Failed to create star shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate random stars
|
||||
generateStars();
|
||||
|
||||
// Create OpenGL buffers
|
||||
createStarBuffers();
|
||||
|
||||
LOG_INFO("Star field initialized: ", starCount, " stars");
|
||||
return true;
|
||||
}
|
||||
|
||||
void StarField::shutdown() {
|
||||
destroyStarBuffers();
|
||||
starShader.reset();
|
||||
stars.clear();
|
||||
}
|
||||
|
||||
void StarField::render(const Camera& camera, float timeOfDay) {
|
||||
if (!renderingEnabled || vao == 0 || !starShader || stars.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get star intensity based on time of day
|
||||
float intensity = getStarIntensity(timeOfDay);
|
||||
|
||||
// Don't render if stars would be invisible
|
||||
if (intensity <= 0.01f) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable blending for star glow
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Enable point sprites
|
||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
|
||||
// Disable depth writing (stars are background)
|
||||
glDepthMask(GL_FALSE);
|
||||
|
||||
starShader->use();
|
||||
|
||||
// Set uniforms
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 projection = camera.getProjectionMatrix();
|
||||
|
||||
starShader->setUniform("view", view);
|
||||
starShader->setUniform("projection", projection);
|
||||
starShader->setUniform("time", twinkleTime);
|
||||
starShader->setUniform("intensity", intensity);
|
||||
|
||||
// Render stars as points
|
||||
glBindVertexArray(vao);
|
||||
glDrawArrays(GL_POINTS, 0, starCount);
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Restore state
|
||||
glDepthMask(GL_TRUE);
|
||||
glDisable(GL_PROGRAM_POINT_SIZE);
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
void StarField::update(float deltaTime) {
|
||||
// Update twinkle animation
|
||||
twinkleTime += deltaTime;
|
||||
}
|
||||
|
||||
void StarField::generateStars() {
|
||||
stars.clear();
|
||||
stars.reserve(starCount);
|
||||
|
||||
// Random number generator
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_real_distribution<float> phiDist(0.0f, M_PI / 2.0f); // 0 to 90 degrees (hemisphere)
|
||||
std::uniform_real_distribution<float> thetaDist(0.0f, 2.0f * M_PI); // 0 to 360 degrees
|
||||
std::uniform_real_distribution<float> brightnessDist(0.3f, 1.0f); // Varying brightness
|
||||
std::uniform_real_distribution<float> twinkleDist(0.0f, 2.0f * M_PI); // Random twinkle phase
|
||||
|
||||
const float radius = 900.0f; // Slightly larger than skybox
|
||||
|
||||
for (int i = 0; i < starCount; i++) {
|
||||
Star star;
|
||||
|
||||
// Spherical coordinates (hemisphere)
|
||||
float phi = phiDist(gen); // Elevation angle
|
||||
float theta = thetaDist(gen); // Azimuth angle
|
||||
|
||||
// Convert to Cartesian coordinates
|
||||
float x = radius * std::sin(phi) * std::cos(theta);
|
||||
float y = radius * std::sin(phi) * std::sin(theta);
|
||||
float z = radius * std::cos(phi);
|
||||
|
||||
star.position = glm::vec3(x, y, z);
|
||||
star.brightness = brightnessDist(gen);
|
||||
star.twinklePhase = twinkleDist(gen);
|
||||
|
||||
stars.push_back(star);
|
||||
}
|
||||
|
||||
LOG_DEBUG("Generated ", stars.size(), " stars");
|
||||
}
|
||||
|
||||
void StarField::createStarBuffers() {
|
||||
// Prepare vertex data (position, brightness, twinkle phase)
|
||||
std::vector<float> vertexData;
|
||||
vertexData.reserve(stars.size() * 5); // 3 pos + 1 brightness + 1 phase
|
||||
|
||||
for (const auto& star : stars) {
|
||||
vertexData.push_back(star.position.x);
|
||||
vertexData.push_back(star.position.y);
|
||||
vertexData.push_back(star.position.z);
|
||||
vertexData.push_back(star.brightness);
|
||||
vertexData.push_back(star.twinklePhase);
|
||||
}
|
||||
|
||||
// Create OpenGL buffers
|
||||
glGenVertexArrays(1, &vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
|
||||
glBindVertexArray(vao);
|
||||
|
||||
// Upload vertex data
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), vertexData.data(), GL_STATIC_DRAW);
|
||||
|
||||
// Set vertex attributes
|
||||
// Position
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
// Brightness
|
||||
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
// Twinkle phase
|
||||
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float)));
|
||||
glEnableVertexAttribArray(2);
|
||||
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void StarField::destroyStarBuffers() {
|
||||
if (vao != 0) {
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
vao = 0;
|
||||
}
|
||||
if (vbo != 0) {
|
||||
glDeleteBuffers(1, &vbo);
|
||||
vbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
float StarField::getStarIntensity(float timeOfDay) const {
|
||||
// Stars visible at night (fade in/out at dusk/dawn)
|
||||
|
||||
// Full night: 20:00-4:00
|
||||
if (timeOfDay >= 20.0f || timeOfDay < 4.0f) {
|
||||
return 1.0f;
|
||||
}
|
||||
// Fade in at dusk: 18:00-20:00
|
||||
else if (timeOfDay >= 18.0f && timeOfDay < 20.0f) {
|
||||
return (timeOfDay - 18.0f) / 2.0f; // 0 to 1 over 2 hours
|
||||
}
|
||||
// Fade out at dawn: 4:00-6:00
|
||||
else if (timeOfDay >= 4.0f && timeOfDay < 6.0f) {
|
||||
return 1.0f - (timeOfDay - 4.0f) / 2.0f; // 1 to 0 over 2 hours
|
||||
}
|
||||
// Daytime: no stars
|
||||
else {
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
380
src/rendering/swim_effects.cpp
Normal file
380
src/rendering/swim_effects.cpp
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
#include "rendering/swim_effects.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
#include "rendering/water_renderer.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
static std::mt19937& rng() {
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
return gen;
|
||||
}
|
||||
|
||||
static float randFloat(float lo, float hi) {
|
||||
std::uniform_real_distribution<float> dist(lo, hi);
|
||||
return dist(rng());
|
||||
}
|
||||
|
||||
SwimEffects::SwimEffects() = default;
|
||||
SwimEffects::~SwimEffects() { shutdown(); }
|
||||
|
||||
bool SwimEffects::initialize() {
|
||||
LOG_INFO("Initializing swim effects");
|
||||
|
||||
// --- Ripple/splash shader (small white spray droplets) ---
|
||||
rippleShader = std::make_unique<Shader>();
|
||||
|
||||
const char* rippleVS = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in float aSize;
|
||||
layout (location = 2) in float aAlpha;
|
||||
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProjection;
|
||||
|
||||
out float vAlpha;
|
||||
|
||||
void main() {
|
||||
gl_Position = uProjection * uView * vec4(aPos, 1.0);
|
||||
gl_PointSize = aSize;
|
||||
vAlpha = aAlpha;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* rippleFS = R"(
|
||||
#version 330 core
|
||||
in float vAlpha;
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
vec2 coord = gl_PointCoord - vec2(0.5);
|
||||
float dist = length(coord);
|
||||
if (dist > 0.5) discard;
|
||||
// Soft circular splash droplet
|
||||
float alpha = smoothstep(0.5, 0.2, dist) * vAlpha;
|
||||
FragColor = vec4(0.85, 0.92, 1.0, alpha);
|
||||
}
|
||||
)";
|
||||
|
||||
if (!rippleShader->loadFromSource(rippleVS, rippleFS)) {
|
||||
LOG_ERROR("Failed to create ripple shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Bubble shader ---
|
||||
bubbleShader = std::make_unique<Shader>();
|
||||
|
||||
const char* bubbleVS = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in float aSize;
|
||||
layout (location = 2) in float aAlpha;
|
||||
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProjection;
|
||||
|
||||
out float vAlpha;
|
||||
|
||||
void main() {
|
||||
gl_Position = uProjection * uView * vec4(aPos, 1.0);
|
||||
gl_PointSize = aSize;
|
||||
vAlpha = aAlpha;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* bubbleFS = R"(
|
||||
#version 330 core
|
||||
in float vAlpha;
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
vec2 coord = gl_PointCoord - vec2(0.5);
|
||||
float dist = length(coord);
|
||||
if (dist > 0.5) discard;
|
||||
// Bubble with highlight
|
||||
float edge = smoothstep(0.5, 0.35, dist);
|
||||
float hollow = smoothstep(0.25, 0.35, dist);
|
||||
float bubble = edge * hollow;
|
||||
// Specular highlight near top-left
|
||||
float highlight = smoothstep(0.3, 0.0, length(coord - vec2(-0.12, -0.12)));
|
||||
float alpha = (bubble * 0.6 + highlight * 0.4) * vAlpha;
|
||||
vec3 color = vec3(0.7, 0.85, 1.0);
|
||||
FragColor = vec4(color, alpha);
|
||||
}
|
||||
)";
|
||||
|
||||
if (!bubbleShader->loadFromSource(bubbleVS, bubbleFS)) {
|
||||
LOG_ERROR("Failed to create bubble shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Ripple VAO/VBO ---
|
||||
glGenVertexArrays(1, &rippleVAO);
|
||||
glGenBuffers(1, &rippleVBO);
|
||||
glBindVertexArray(rippleVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, rippleVBO);
|
||||
// layout: vec3 pos, float size, float alpha (stride = 5 floats)
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float)));
|
||||
glEnableVertexAttribArray(2);
|
||||
glBindVertexArray(0);
|
||||
|
||||
// --- Bubble VAO/VBO ---
|
||||
glGenVertexArrays(1, &bubbleVAO);
|
||||
glGenBuffers(1, &bubbleVBO);
|
||||
glBindVertexArray(bubbleVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bubbleVBO);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float)));
|
||||
glEnableVertexAttribArray(2);
|
||||
glBindVertexArray(0);
|
||||
|
||||
ripples.reserve(MAX_RIPPLE_PARTICLES);
|
||||
bubbles.reserve(MAX_BUBBLE_PARTICLES);
|
||||
rippleVertexData.reserve(MAX_RIPPLE_PARTICLES * 5);
|
||||
bubbleVertexData.reserve(MAX_BUBBLE_PARTICLES * 5);
|
||||
|
||||
LOG_INFO("Swim effects initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void SwimEffects::shutdown() {
|
||||
if (rippleVAO) { glDeleteVertexArrays(1, &rippleVAO); rippleVAO = 0; }
|
||||
if (rippleVBO) { glDeleteBuffers(1, &rippleVBO); rippleVBO = 0; }
|
||||
if (bubbleVAO) { glDeleteVertexArrays(1, &bubbleVAO); bubbleVAO = 0; }
|
||||
if (bubbleVBO) { glDeleteBuffers(1, &bubbleVBO); bubbleVBO = 0; }
|
||||
rippleShader.reset();
|
||||
bubbleShader.reset();
|
||||
ripples.clear();
|
||||
bubbles.clear();
|
||||
}
|
||||
|
||||
void SwimEffects::spawnRipple(const glm::vec3& pos, const glm::vec3& moveDir, float waterH) {
|
||||
if (static_cast<int>(ripples.size()) >= MAX_RIPPLE_PARTICLES) return;
|
||||
|
||||
Particle p;
|
||||
// Scatter splash droplets around the character at the water surface
|
||||
float ox = randFloat(-1.5f, 1.5f);
|
||||
float oy = randFloat(-1.5f, 1.5f);
|
||||
p.position = glm::vec3(pos.x + ox, pos.y + oy, waterH + 0.3f);
|
||||
|
||||
// Spray outward + upward from movement direction
|
||||
float spread = randFloat(-1.0f, 1.0f);
|
||||
glm::vec3 perp(-moveDir.y, moveDir.x, 0.0f);
|
||||
glm::vec3 outDir = -moveDir + perp * spread;
|
||||
float speed = randFloat(1.5f, 4.0f);
|
||||
p.velocity = glm::vec3(outDir.x * speed, outDir.y * speed, randFloat(1.0f, 3.0f));
|
||||
|
||||
p.lifetime = 0.0f;
|
||||
p.maxLifetime = randFloat(0.5f, 1.0f);
|
||||
p.size = randFloat(3.0f, 7.0f);
|
||||
p.alpha = randFloat(0.5f, 0.8f);
|
||||
|
||||
ripples.push_back(p);
|
||||
}
|
||||
|
||||
void SwimEffects::spawnBubble(const glm::vec3& pos, float /*waterH*/) {
|
||||
if (static_cast<int>(bubbles.size()) >= MAX_BUBBLE_PARTICLES) return;
|
||||
|
||||
Particle p;
|
||||
float ox = randFloat(-3.0f, 3.0f);
|
||||
float oy = randFloat(-3.0f, 3.0f);
|
||||
float oz = randFloat(-2.0f, 0.0f);
|
||||
p.position = glm::vec3(pos.x + ox, pos.y + oy, pos.z + oz);
|
||||
|
||||
p.velocity = glm::vec3(randFloat(-0.3f, 0.3f), randFloat(-0.3f, 0.3f), randFloat(4.0f, 8.0f));
|
||||
p.lifetime = 0.0f;
|
||||
p.maxLifetime = randFloat(2.0f, 3.5f);
|
||||
p.size = randFloat(6.0f, 12.0f);
|
||||
p.alpha = 0.6f;
|
||||
|
||||
bubbles.push_back(p);
|
||||
}
|
||||
|
||||
void SwimEffects::update(const Camera& camera, const CameraController& cc,
|
||||
const WaterRenderer& water, float deltaTime) {
|
||||
glm::vec3 camPos = camera.getPosition();
|
||||
|
||||
// Use character position for ripples in third-person mode
|
||||
glm::vec3 charPos = camPos;
|
||||
const glm::vec3* followTarget = cc.getFollowTarget();
|
||||
if (cc.isThirdPerson() && followTarget) {
|
||||
charPos = *followTarget;
|
||||
}
|
||||
|
||||
// Check water at character position (for ripples) and camera position (for bubbles)
|
||||
auto charWaterH = water.getWaterHeightAt(charPos.x, charPos.y);
|
||||
auto camWaterH = water.getWaterHeightAt(camPos.x, camPos.y);
|
||||
|
||||
bool swimming = cc.isSwimming();
|
||||
bool moving = cc.isMoving();
|
||||
|
||||
// --- Ripple/splash spawning ---
|
||||
if (swimming && charWaterH) {
|
||||
float wh = *charWaterH;
|
||||
float spawnRate = moving ? 40.0f : 8.0f;
|
||||
rippleSpawnAccum += spawnRate * deltaTime;
|
||||
|
||||
// Compute movement direction from camera yaw
|
||||
float yawRad = glm::radians(cc.getYaw());
|
||||
glm::vec3 moveDir(std::cos(yawRad), std::sin(yawRad), 0.0f);
|
||||
if (glm::length(glm::vec2(moveDir)) > 0.001f) {
|
||||
moveDir = glm::normalize(moveDir);
|
||||
}
|
||||
|
||||
while (rippleSpawnAccum >= 1.0f) {
|
||||
spawnRipple(charPos, moveDir, wh);
|
||||
rippleSpawnAccum -= 1.0f;
|
||||
}
|
||||
} else {
|
||||
rippleSpawnAccum = 0.0f;
|
||||
ripples.clear();
|
||||
}
|
||||
|
||||
// --- Bubble spawning ---
|
||||
bool underwater = camWaterH && camPos.z < *camWaterH;
|
||||
if (underwater) {
|
||||
float bubbleRate = 20.0f;
|
||||
bubbleSpawnAccum += bubbleRate * deltaTime;
|
||||
while (bubbleSpawnAccum >= 1.0f) {
|
||||
spawnBubble(camPos, *camWaterH);
|
||||
bubbleSpawnAccum -= 1.0f;
|
||||
}
|
||||
} else {
|
||||
bubbleSpawnAccum = 0.0f;
|
||||
bubbles.clear();
|
||||
}
|
||||
|
||||
// --- Update ripples (splash droplets with gravity) ---
|
||||
for (int i = static_cast<int>(ripples.size()) - 1; i >= 0; --i) {
|
||||
auto& p = ripples[i];
|
||||
p.lifetime += deltaTime;
|
||||
if (p.lifetime >= p.maxLifetime) {
|
||||
ripples[i] = ripples.back();
|
||||
ripples.pop_back();
|
||||
continue;
|
||||
}
|
||||
// Apply gravity to splash droplets
|
||||
p.velocity.z -= 9.8f * deltaTime;
|
||||
p.position += p.velocity * deltaTime;
|
||||
|
||||
// Kill if fallen back below water
|
||||
float surfaceZ = charWaterH ? *charWaterH : 0.0f;
|
||||
if (p.position.z < surfaceZ && p.lifetime > 0.1f) {
|
||||
ripples[i] = ripples.back();
|
||||
ripples.pop_back();
|
||||
continue;
|
||||
}
|
||||
|
||||
float t = p.lifetime / p.maxLifetime;
|
||||
p.alpha = glm::mix(0.7f, 0.0f, t);
|
||||
p.size = glm::mix(5.0f, 2.0f, t);
|
||||
}
|
||||
|
||||
// --- Update bubbles ---
|
||||
float bubbleCeilH = camWaterH ? *camWaterH : 0.0f;
|
||||
for (int i = static_cast<int>(bubbles.size()) - 1; i >= 0; --i) {
|
||||
auto& p = bubbles[i];
|
||||
p.lifetime += deltaTime;
|
||||
if (p.lifetime >= p.maxLifetime || p.position.z >= bubbleCeilH) {
|
||||
bubbles[i] = bubbles.back();
|
||||
bubbles.pop_back();
|
||||
continue;
|
||||
}
|
||||
// Wobble
|
||||
float wobbleX = std::sin(p.lifetime * 3.0f) * 0.5f;
|
||||
float wobbleY = std::cos(p.lifetime * 2.5f) * 0.5f;
|
||||
p.position += (p.velocity + glm::vec3(wobbleX, wobbleY, 0.0f)) * deltaTime;
|
||||
|
||||
float t = p.lifetime / p.maxLifetime;
|
||||
if (t > 0.8f) {
|
||||
p.alpha = 0.6f * (1.0f - (t - 0.8f) / 0.2f);
|
||||
} else {
|
||||
p.alpha = 0.6f;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Build vertex data ---
|
||||
rippleVertexData.clear();
|
||||
for (const auto& p : ripples) {
|
||||
rippleVertexData.push_back(p.position.x);
|
||||
rippleVertexData.push_back(p.position.y);
|
||||
rippleVertexData.push_back(p.position.z);
|
||||
rippleVertexData.push_back(p.size);
|
||||
rippleVertexData.push_back(p.alpha);
|
||||
}
|
||||
|
||||
bubbleVertexData.clear();
|
||||
for (const auto& p : bubbles) {
|
||||
bubbleVertexData.push_back(p.position.x);
|
||||
bubbleVertexData.push_back(p.position.y);
|
||||
bubbleVertexData.push_back(p.position.z);
|
||||
bubbleVertexData.push_back(p.size);
|
||||
bubbleVertexData.push_back(p.alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void SwimEffects::render(const Camera& camera) {
|
||||
if (rippleVertexData.empty() && bubbleVertexData.empty()) return;
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glDepthMask(GL_FALSE);
|
||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 projection = camera.getProjectionMatrix();
|
||||
|
||||
// --- Render ripples (splash droplets above water surface) ---
|
||||
if (!rippleVertexData.empty() && rippleShader) {
|
||||
rippleShader->use();
|
||||
rippleShader->setUniform("uView", view);
|
||||
rippleShader->setUniform("uProjection", projection);
|
||||
|
||||
glBindVertexArray(rippleVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, rippleVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER,
|
||||
rippleVertexData.size() * sizeof(float),
|
||||
rippleVertexData.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(rippleVertexData.size() / 5));
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
// --- Render bubbles ---
|
||||
if (!bubbleVertexData.empty() && bubbleShader) {
|
||||
bubbleShader->use();
|
||||
bubbleShader->setUniform("uView", view);
|
||||
bubbleShader->setUniform("uProjection", projection);
|
||||
|
||||
glBindVertexArray(bubbleVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bubbleVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER,
|
||||
bubbleVertexData.size() * sizeof(float),
|
||||
bubbleVertexData.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(bubbleVertexData.size() / 5));
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glDepthMask(GL_TRUE);
|
||||
glDisable(GL_PROGRAM_POINT_SIZE);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
826
src/rendering/terrain_manager.cpp
Normal file
826
src/rendering/terrain_manager.cpp
Normal file
|
|
@ -0,0 +1,826 @@
|
|||
#include "rendering/terrain_manager.hpp"
|
||||
#include "rendering/terrain_renderer.hpp"
|
||||
#include "rendering/water_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/adt_loader.hpp"
|
||||
#include "pipeline/m2_loader.hpp"
|
||||
#include "pipeline/wmo_loader.hpp"
|
||||
#include "pipeline/terrain_mesh.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <glm/gtx/euler_angles.hpp>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <functional>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
TerrainManager::TerrainManager() {
|
||||
}
|
||||
|
||||
TerrainManager::~TerrainManager() {
|
||||
// Stop worker thread before cleanup (containers clean up via destructors)
|
||||
if (workerRunning.load()) {
|
||||
workerRunning.store(false);
|
||||
queueCV.notify_all();
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* renderer) {
|
||||
assetManager = assets;
|
||||
terrainRenderer = renderer;
|
||||
|
||||
if (!assetManager) {
|
||||
LOG_ERROR("Asset manager is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!terrainRenderer) {
|
||||
LOG_ERROR("Terrain renderer is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start background worker thread
|
||||
workerRunning.store(true);
|
||||
workerThread = std::thread(&TerrainManager::workerLoop, this);
|
||||
|
||||
LOG_INFO("Terrain manager initialized (async loading enabled)");
|
||||
LOG_INFO(" Map: ", mapName);
|
||||
LOG_INFO(" Load radius: ", loadRadius, " tiles");
|
||||
LOG_INFO(" Unload radius: ", unloadRadius, " tiles");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TerrainManager::update(const Camera& camera, float deltaTime) {
|
||||
if (!streamingEnabled || !assetManager || !terrainRenderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always process ready tiles each frame (GPU uploads from background thread)
|
||||
processReadyTiles();
|
||||
|
||||
timeSinceLastUpdate += deltaTime;
|
||||
|
||||
// Only update streaming periodically (not every frame)
|
||||
if (timeSinceLastUpdate < updateInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
timeSinceLastUpdate = 0.0f;
|
||||
|
||||
// Get current tile from camera position
|
||||
// GL coordinate mapping: GL Y = -(wowX - ZEROPOINT), GL X = -(wowZ - ZEROPOINT), GL Z = height
|
||||
// worldToTile expects: worldX = -glY (maps to tileX), worldY = glX (maps to tileY)
|
||||
glm::vec3 camPos = camera.getPosition();
|
||||
TileCoord newTile = worldToTile(-camPos.y, camPos.x);
|
||||
|
||||
// Check if we've moved to a different tile
|
||||
if (newTile.x != currentTile.x || newTile.y != currentTile.y) {
|
||||
LOG_DEBUG("Camera moved to tile [", newTile.x, ",", newTile.y, "]");
|
||||
currentTile = newTile;
|
||||
}
|
||||
|
||||
// Stream tiles if we've moved significantly or initial load
|
||||
if (newTile.x != lastStreamTile.x || newTile.y != lastStreamTile.y) {
|
||||
LOG_INFO("Streaming: cam=(", camPos.x, ",", camPos.y, ",", camPos.z,
|
||||
") tile=[", newTile.x, ",", newTile.y,
|
||||
"] loaded=", loadedTiles.size());
|
||||
streamTiles();
|
||||
lastStreamTile = newTile;
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronous fallback for initial tile loading (before worker thread is useful)
|
||||
bool TerrainManager::loadTile(int x, int y) {
|
||||
TileCoord coord = {x, y};
|
||||
|
||||
// Check if already loaded
|
||||
if (loadedTiles.find(coord) != loadedTiles.end()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't retry tiles that already failed
|
||||
if (failedTiles.find(coord) != failedTiles.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Loading terrain tile [", x, ",", y, "] (synchronous)");
|
||||
|
||||
auto pending = prepareTile(x, y);
|
||||
if (!pending) {
|
||||
failedTiles[coord] = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
finalizeTile(std::move(pending));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
||||
TileCoord coord = {x, y};
|
||||
|
||||
LOG_INFO("Preparing tile [", x, ",", y, "] (CPU work)");
|
||||
|
||||
// Load ADT file
|
||||
std::string adtPath = getADTPath(coord);
|
||||
auto adtData = assetManager->readFile(adtPath);
|
||||
|
||||
if (adtData.empty()) {
|
||||
LOG_WARNING("Failed to load ADT file: ", adtPath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Parse ADT
|
||||
pipeline::ADTTerrain terrain = pipeline::ADTLoader::load(adtData);
|
||||
if (!terrain.isLoaded()) {
|
||||
LOG_ERROR("Failed to parse ADT terrain: ", adtPath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Set tile coordinates so mesh knows where to position this tile in world
|
||||
terrain.coord.x = x;
|
||||
terrain.coord.y = y;
|
||||
|
||||
// Generate mesh
|
||||
pipeline::TerrainMesh mesh = pipeline::TerrainMeshGenerator::generate(terrain);
|
||||
if (mesh.validChunkCount == 0) {
|
||||
LOG_ERROR("Failed to generate terrain mesh: ", adtPath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto pending = std::make_unique<PendingTile>();
|
||||
pending->coord = coord;
|
||||
pending->terrain = std::move(terrain);
|
||||
pending->mesh = std::move(mesh);
|
||||
|
||||
// Pre-load M2 doodads (CPU: read files, parse models)
|
||||
if (!pending->terrain.doodadPlacements.empty()) {
|
||||
std::unordered_set<uint32_t> preparedModelIds;
|
||||
|
||||
int skippedNameId = 0, skippedFileNotFound = 0, skippedInvalid = 0, skippedSkinNotFound = 0;
|
||||
|
||||
for (const auto& placement : pending->terrain.doodadPlacements) {
|
||||
if (placement.nameId >= pending->terrain.doodadNames.size()) {
|
||||
skippedNameId++;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string m2Path = pending->terrain.doodadNames[placement.nameId];
|
||||
|
||||
// Convert .mdx to .m2 if needed
|
||||
if (m2Path.size() > 4) {
|
||||
std::string ext = m2Path.substr(m2Path.size() - 4);
|
||||
for (char& c : ext) c = std::tolower(c);
|
||||
if (ext == ".mdx") {
|
||||
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
|
||||
}
|
||||
}
|
||||
|
||||
// Use path hash as globally unique model ID (nameId is per-tile local)
|
||||
uint32_t modelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
|
||||
|
||||
// Parse model if not already done for this tile
|
||||
if (preparedModelIds.find(modelId) == preparedModelIds.end()) {
|
||||
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
|
||||
if (!m2Data.empty()) {
|
||||
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
|
||||
|
||||
// Try to load skin file
|
||||
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||
std::vector<uint8_t> skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, m2Model);
|
||||
} else {
|
||||
skippedSkinNotFound++;
|
||||
LOG_WARNING("M2 skin not found: ", skinPath);
|
||||
}
|
||||
|
||||
if (m2Model.isValid()) {
|
||||
PendingTile::M2Ready ready;
|
||||
ready.modelId = modelId;
|
||||
ready.model = std::move(m2Model);
|
||||
ready.path = m2Path;
|
||||
pending->m2Models.push_back(std::move(ready));
|
||||
preparedModelIds.insert(modelId);
|
||||
} else {
|
||||
skippedInvalid++;
|
||||
LOG_WARNING("M2 model invalid (no verts/indices): ", m2Path);
|
||||
}
|
||||
} else {
|
||||
skippedFileNotFound++;
|
||||
LOG_WARNING("M2 file not found: ", m2Path);
|
||||
}
|
||||
}
|
||||
|
||||
// Store placement data for instance creation on main thread
|
||||
if (preparedModelIds.count(modelId)) {
|
||||
const float ZEROPOINT = 32.0f * 533.33333f;
|
||||
|
||||
float wowX = placement.position[0];
|
||||
float wowY = placement.position[1];
|
||||
float wowZ = placement.position[2];
|
||||
|
||||
PendingTile::M2Placement p;
|
||||
p.modelId = modelId;
|
||||
p.uniqueId = placement.uniqueId;
|
||||
p.position = glm::vec3(
|
||||
-(wowZ - ZEROPOINT),
|
||||
-(wowX - ZEROPOINT),
|
||||
wowY
|
||||
);
|
||||
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
|
||||
);
|
||||
p.scale = placement.scale / 1024.0f;
|
||||
pending->m2Placements.push_back(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (skippedNameId > 0 || skippedFileNotFound > 0 || skippedInvalid > 0) {
|
||||
LOG_WARNING("Tile [", x, ",", y, "] doodad issues: ",
|
||||
skippedNameId, " bad nameId, ",
|
||||
skippedFileNotFound, " file not found, ",
|
||||
skippedInvalid, " invalid model, ",
|
||||
skippedSkinNotFound, " skin not found");
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-load WMOs (CPU: read files, parse models and groups)
|
||||
if (!pending->terrain.wmoPlacements.empty()) {
|
||||
for (const auto& placement : pending->terrain.wmoPlacements) {
|
||||
if (placement.nameId >= pending->terrain.wmoNames.size()) continue;
|
||||
|
||||
const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId];
|
||||
std::vector<uint8_t> wmoData = assetManager->readFile(wmoPath);
|
||||
if (wmoData.empty()) continue;
|
||||
|
||||
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
|
||||
if (wmoModel.nGroups > 0) {
|
||||
std::string basePath = wmoPath;
|
||||
std::string extension;
|
||||
if (basePath.size() > 4) {
|
||||
extension = basePath.substr(basePath.size() - 4);
|
||||
std::string extLower = extension;
|
||||
for (char& c : extLower) c = std::tolower(c);
|
||||
if (extLower == ".wmo") {
|
||||
basePath = basePath.substr(0, basePath.size() - 4);
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
|
||||
char groupSuffix[16];
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
|
||||
std::string groupPath = basePath + groupSuffix;
|
||||
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (!groupData.empty()) {
|
||||
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!wmoModel.groups.empty()) {
|
||||
const float ZEROPOINT = 32.0f * 533.33333f;
|
||||
|
||||
glm::vec3 pos(
|
||||
-(placement.position[2] - ZEROPOINT),
|
||||
-(placement.position[0] - ZEROPOINT),
|
||||
placement.position[1]
|
||||
);
|
||||
|
||||
glm::vec3 rot(
|
||||
-placement.rotation[2] * 3.14159f / 180.0f,
|
||||
-placement.rotation[0] * 3.14159f / 180.0f,
|
||||
(placement.rotation[1] + 180.0f) * 3.14159f / 180.0f
|
||||
);
|
||||
|
||||
// Pre-load WMO doodads (M2 models inside WMO)
|
||||
if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) {
|
||||
glm::mat4 wmoMatrix(1.0f);
|
||||
wmoMatrix = glm::translate(wmoMatrix, pos);
|
||||
wmoMatrix = glm::rotate(wmoMatrix, rot.z, glm::vec3(0, 0, 1));
|
||||
wmoMatrix = glm::rotate(wmoMatrix, rot.y, glm::vec3(0, 1, 0));
|
||||
wmoMatrix = glm::rotate(wmoMatrix, rot.x, glm::vec3(1, 0, 0));
|
||||
|
||||
const auto& doodadSet = wmoModel.doodadSets[0];
|
||||
for (uint32_t di = 0; di < doodadSet.count; di++) {
|
||||
uint32_t doodadIdx = doodadSet.startIndex + di;
|
||||
if (doodadIdx >= wmoModel.doodads.size()) break;
|
||||
|
||||
const auto& doodad = wmoModel.doodads[doodadIdx];
|
||||
auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex);
|
||||
if (nameIt == wmoModel.doodadNames.end()) continue;
|
||||
|
||||
std::string m2Path = nameIt->second;
|
||||
if (m2Path.empty()) continue;
|
||||
|
||||
if (m2Path.size() > 4) {
|
||||
std::string ext = m2Path.substr(m2Path.size() - 4);
|
||||
for (char& c : ext) c = std::tolower(c);
|
||||
if (ext == ".mdx" || ext == ".mdl") {
|
||||
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
|
||||
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
|
||||
if (m2Data.empty()) continue;
|
||||
|
||||
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
|
||||
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||
std::vector<uint8_t> skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, m2Model);
|
||||
}
|
||||
if (!m2Model.isValid()) continue;
|
||||
|
||||
// Build doodad's local transform (WoW coordinates)
|
||||
// WMO doodads use quaternion rotation
|
||||
glm::mat4 doodadLocal(1.0f);
|
||||
doodadLocal = glm::translate(doodadLocal, doodad.position);
|
||||
doodadLocal *= glm::mat4_cast(doodad.rotation);
|
||||
doodadLocal = glm::scale(doodadLocal, glm::vec3(doodad.scale));
|
||||
|
||||
// Full world transform = WMO world transform * doodad local transform
|
||||
glm::mat4 worldMatrix = wmoMatrix * doodadLocal;
|
||||
|
||||
// Extract world position for frustum culling
|
||||
glm::vec3 worldPos = glm::vec3(worldMatrix[3]);
|
||||
|
||||
PendingTile::WMODoodadReady doodadReady;
|
||||
doodadReady.modelId = doodadModelId;
|
||||
doodadReady.model = std::move(m2Model);
|
||||
doodadReady.worldPosition = worldPos;
|
||||
doodadReady.modelMatrix = worldMatrix;
|
||||
pending->wmoDoodads.push_back(std::move(doodadReady));
|
||||
}
|
||||
}
|
||||
|
||||
PendingTile::WMOReady ready;
|
||||
ready.modelId = placement.uniqueId;
|
||||
ready.model = std::move(wmoModel);
|
||||
ready.position = pos;
|
||||
ready.rotation = rot;
|
||||
pending->wmoModels.push_back(std::move(ready));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Prepared tile [", x, ",", y, "]: ",
|
||||
pending->m2Models.size(), " M2 models, ",
|
||||
pending->m2Placements.size(), " M2 placements, ",
|
||||
pending->wmoModels.size(), " WMOs, ",
|
||||
pending->wmoDoodads.size(), " WMO doodads");
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
|
||||
int x = pending->coord.x;
|
||||
int y = pending->coord.y;
|
||||
TileCoord coord = pending->coord;
|
||||
|
||||
LOG_INFO("Finalizing tile [", x, ",", y, "] (GPU upload)");
|
||||
|
||||
// Check if tile was already loaded (race condition guard) or failed
|
||||
if (loadedTiles.find(coord) != loadedTiles.end()) {
|
||||
return;
|
||||
}
|
||||
if (failedTiles.find(coord) != failedTiles.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload terrain to GPU
|
||||
if (!terrainRenderer->loadTerrain(pending->mesh, pending->terrain.textures, x, y)) {
|
||||
LOG_ERROR("Failed to upload terrain to GPU for tile [", x, ",", y, "]");
|
||||
failedTiles[coord] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Load water
|
||||
if (waterRenderer) {
|
||||
waterRenderer->loadFromTerrain(pending->terrain, true, x, y);
|
||||
}
|
||||
|
||||
std::vector<uint32_t> m2InstanceIds;
|
||||
std::vector<uint32_t> wmoInstanceIds;
|
||||
std::vector<uint32_t> tileUniqueIds;
|
||||
|
||||
// Upload M2 models to GPU and create instances
|
||||
if (m2Renderer && assetManager) {
|
||||
if (!m2Renderer->getModelCount()) {
|
||||
m2Renderer->initialize(assetManager);
|
||||
}
|
||||
|
||||
// Upload unique models
|
||||
std::unordered_set<uint32_t> uploadedModelIds;
|
||||
for (auto& m2Ready : pending->m2Models) {
|
||||
if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) {
|
||||
uploadedModelIds.insert(m2Ready.modelId);
|
||||
}
|
||||
}
|
||||
|
||||
// Create instances (deduplicate by uniqueId across tile boundaries)
|
||||
int loadedDoodads = 0;
|
||||
int skippedDedup = 0;
|
||||
for (const auto& p : pending->m2Placements) {
|
||||
// Skip if this doodad was already placed by a neighboring tile
|
||||
if (placedDoodadIds.count(p.uniqueId)) {
|
||||
skippedDedup++;
|
||||
continue;
|
||||
}
|
||||
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
|
||||
if (instId) {
|
||||
m2InstanceIds.push_back(instId);
|
||||
placedDoodadIds.insert(p.uniqueId);
|
||||
tileUniqueIds.push_back(p.uniqueId);
|
||||
loadedDoodads++;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO(" Loaded doodads for tile [", x, ",", y, "]: ",
|
||||
loadedDoodads, " instances (", uploadedModelIds.size(), " new models, ",
|
||||
skippedDedup, " dedup skipped)");
|
||||
}
|
||||
|
||||
// Upload WMO models to GPU and create instances
|
||||
if (wmoRenderer && assetManager) {
|
||||
if (!wmoRenderer->getModelCount()) {
|
||||
wmoRenderer->initialize(assetManager);
|
||||
}
|
||||
|
||||
int loadedWMOs = 0;
|
||||
for (auto& wmoReady : pending->wmoModels) {
|
||||
if (wmoRenderer->loadModel(wmoReady.model, wmoReady.modelId)) {
|
||||
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
|
||||
if (wmoInstId) {
|
||||
wmoInstanceIds.push_back(wmoInstId);
|
||||
loadedWMOs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upload WMO doodad M2 models
|
||||
if (m2Renderer) {
|
||||
for (auto& doodad : pending->wmoDoodads) {
|
||||
m2Renderer->loadModel(doodad.model, doodad.modelId);
|
||||
uint32_t wmoDoodadInstId = m2Renderer->createInstanceWithMatrix(
|
||||
doodad.modelId, doodad.modelMatrix, doodad.worldPosition);
|
||||
if (wmoDoodadInstId) m2InstanceIds.push_back(wmoDoodadInstId);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedWMOs > 0) {
|
||||
LOG_INFO(" Loaded WMOs for tile [", x, ",", y, "]: ", loadedWMOs);
|
||||
}
|
||||
}
|
||||
|
||||
// Create tile entry
|
||||
auto tile = std::make_unique<TerrainTile>();
|
||||
tile->coord = coord;
|
||||
tile->terrain = std::move(pending->terrain);
|
||||
tile->mesh = std::move(pending->mesh);
|
||||
tile->loaded = true;
|
||||
tile->m2InstanceIds = std::move(m2InstanceIds);
|
||||
tile->wmoInstanceIds = std::move(wmoInstanceIds);
|
||||
tile->doodadUniqueIds = std::move(tileUniqueIds);
|
||||
|
||||
// Calculate world bounds
|
||||
getTileBounds(coord, tile->minX, tile->minY, tile->maxX, tile->maxY);
|
||||
|
||||
loadedTiles[coord] = std::move(tile);
|
||||
|
||||
LOG_INFO(" Finalized tile [", x, ",", y, "]");
|
||||
}
|
||||
|
||||
void TerrainManager::workerLoop() {
|
||||
LOG_INFO("Terrain worker thread started");
|
||||
|
||||
while (workerRunning.load()) {
|
||||
TileCoord coord;
|
||||
bool hasWork = false;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queueMutex);
|
||||
queueCV.wait(lock, [this]() {
|
||||
return !loadQueue.empty() || !workerRunning.load();
|
||||
});
|
||||
|
||||
if (!workerRunning.load()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!loadQueue.empty()) {
|
||||
coord = loadQueue.front();
|
||||
loadQueue.pop();
|
||||
hasWork = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWork) {
|
||||
auto pending = prepareTile(coord.x, coord.y);
|
||||
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
if (pending) {
|
||||
readyQueue.push(std::move(pending));
|
||||
} else {
|
||||
// Mark as failed so we don't re-enqueue
|
||||
// We'll set failedTiles on the main thread in processReadyTiles
|
||||
// For now, just remove from pending tracking
|
||||
pendingTiles.erase(coord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Terrain worker thread stopped");
|
||||
}
|
||||
|
||||
void TerrainManager::processReadyTiles() {
|
||||
// Process up to 2 ready tiles per frame to spread GPU work
|
||||
int processed = 0;
|
||||
const int maxPerFrame = 2;
|
||||
|
||||
while (processed < maxPerFrame) {
|
||||
std::unique_ptr<PendingTile> pending;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
if (readyQueue.empty()) {
|
||||
break;
|
||||
}
|
||||
pending = std::move(readyQueue.front());
|
||||
readyQueue.pop();
|
||||
}
|
||||
|
||||
if (pending) {
|
||||
TileCoord coord = pending->coord;
|
||||
finalizeTile(std::move(pending));
|
||||
pendingTiles.erase(coord);
|
||||
processed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainManager::unloadTile(int x, int y) {
|
||||
TileCoord coord = {x, y};
|
||||
|
||||
// Also remove from pending if it was queued but not yet loaded
|
||||
pendingTiles.erase(coord);
|
||||
|
||||
auto it = loadedTiles.find(coord);
|
||||
if (it == loadedTiles.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Unloading terrain tile [", x, ",", y, "]");
|
||||
|
||||
const auto& tile = it->second;
|
||||
|
||||
// Remove doodad unique IDs from dedup set
|
||||
for (uint32_t uid : tile->doodadUniqueIds) {
|
||||
placedDoodadIds.erase(uid);
|
||||
}
|
||||
|
||||
// Remove M2 doodad instances
|
||||
if (m2Renderer) {
|
||||
for (uint32_t id : tile->m2InstanceIds) {
|
||||
m2Renderer->removeInstance(id);
|
||||
}
|
||||
LOG_DEBUG(" Removed ", tile->m2InstanceIds.size(), " M2 instances");
|
||||
}
|
||||
|
||||
// Remove WMO instances
|
||||
if (wmoRenderer) {
|
||||
for (uint32_t id : tile->wmoInstanceIds) {
|
||||
wmoRenderer->removeInstance(id);
|
||||
}
|
||||
LOG_DEBUG(" Removed ", tile->wmoInstanceIds.size(), " WMO instances");
|
||||
}
|
||||
|
||||
// Remove terrain chunks for this tile
|
||||
if (terrainRenderer) {
|
||||
terrainRenderer->removeTile(x, y);
|
||||
}
|
||||
|
||||
// Remove water surfaces for this tile
|
||||
if (waterRenderer) {
|
||||
waterRenderer->removeTile(x, y);
|
||||
}
|
||||
|
||||
loadedTiles.erase(it);
|
||||
}
|
||||
|
||||
void TerrainManager::unloadAll() {
|
||||
// Stop worker thread
|
||||
if (workerRunning.load()) {
|
||||
workerRunning.store(false);
|
||||
queueCV.notify_all();
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear queues
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
while (!loadQueue.empty()) loadQueue.pop();
|
||||
while (!readyQueue.empty()) readyQueue.pop();
|
||||
}
|
||||
pendingTiles.clear();
|
||||
placedDoodadIds.clear();
|
||||
|
||||
LOG_INFO("Unloading all terrain tiles");
|
||||
loadedTiles.clear();
|
||||
failedTiles.clear();
|
||||
|
||||
// Clear terrain renderer
|
||||
if (terrainRenderer) {
|
||||
terrainRenderer->clear();
|
||||
}
|
||||
|
||||
// Clear water
|
||||
if (waterRenderer) {
|
||||
waterRenderer->clear();
|
||||
}
|
||||
}
|
||||
|
||||
TileCoord TerrainManager::worldToTile(float worldX, float worldY) const {
|
||||
// WoW world coordinate system:
|
||||
// - Tiles are 8533.33 units wide (TILE_SIZE)
|
||||
// - Tile (32, 32) is roughly at world origin for continents
|
||||
// - Coordinates increase going east (X) and south (Y)
|
||||
|
||||
int tileX = 32 + static_cast<int>(std::floor(worldX / TILE_SIZE));
|
||||
int tileY = 32 - static_cast<int>(std::floor(worldY / TILE_SIZE));
|
||||
|
||||
// Clamp to valid range (0-63)
|
||||
tileX = std::max(0, std::min(63, tileX));
|
||||
tileY = std::max(0, std::min(63, tileY));
|
||||
|
||||
return {tileX, tileY};
|
||||
}
|
||||
|
||||
void TerrainManager::getTileBounds(const TileCoord& coord, float& minX, float& minY,
|
||||
float& maxX, float& maxY) const {
|
||||
// Calculate world bounds for this tile
|
||||
// Tile (32, 32) is at origin
|
||||
float offsetX = (32 - coord.x) * TILE_SIZE;
|
||||
float offsetY = (32 - coord.y) * TILE_SIZE;
|
||||
|
||||
minX = offsetX - TILE_SIZE;
|
||||
minY = offsetY - TILE_SIZE;
|
||||
maxX = offsetX;
|
||||
maxY = offsetY;
|
||||
}
|
||||
|
||||
std::string TerrainManager::getADTPath(const TileCoord& coord) const {
|
||||
// Format: World\Maps\{MapName}\{MapName}_{X}_{Y}.adt
|
||||
// Example: Azeroth_32_49.adt for tile at coord.x=32, coord.y=49
|
||||
return "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
||||
std::to_string(coord.x) + "_" + std::to_string(coord.y) + ".adt";
|
||||
}
|
||||
|
||||
std::optional<float> TerrainManager::getHeightAt(float glX, float glY) const {
|
||||
// Terrain mesh vertices are positioned as:
|
||||
// vertex.position[0] = chunk.position[0] - (offsetY * unitSize) -> GL X
|
||||
// vertex.position[1] = chunk.position[1] - (offsetX * unitSize) -> GL Y
|
||||
// vertex.position[2] = chunk.position[2] + height -> GL Z (height)
|
||||
//
|
||||
// The 9x9 outer vertex grid has offsetX, offsetY in [0, 8].
|
||||
// So the chunk spans:
|
||||
// X: [chunk.position[0] - 8*unitSize, chunk.position[0]]
|
||||
// Y: [chunk.position[1] - 8*unitSize, chunk.position[1]]
|
||||
|
||||
const float unitSize = CHUNK_SIZE / 8.0f;
|
||||
|
||||
for (const auto& [coord, tile] : loadedTiles) {
|
||||
if (!tile || !tile->loaded) continue;
|
||||
|
||||
for (int cy = 0; cy < 16; cy++) {
|
||||
for (int cx = 0; cx < 16; cx++) {
|
||||
const auto& chunk = tile->terrain.getChunk(cx, cy);
|
||||
if (!chunk.hasHeightMap()) continue;
|
||||
|
||||
float chunkMaxX = chunk.position[0];
|
||||
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
|
||||
float chunkMaxY = chunk.position[1];
|
||||
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
|
||||
|
||||
if (glX < chunkMinX || glX > chunkMaxX ||
|
||||
glY < chunkMinY || glY > chunkMaxY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fractional position within chunk (0-8 range)
|
||||
float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY
|
||||
float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX
|
||||
|
||||
fracX = glm::clamp(fracX, 0.0f, 8.0f);
|
||||
fracY = glm::clamp(fracY, 0.0f, 8.0f);
|
||||
|
||||
// Bilinear interpolation on 9x9 outer grid
|
||||
int gx0 = static_cast<int>(std::floor(fracX));
|
||||
int gy0 = static_cast<int>(std::floor(fracY));
|
||||
int gx1 = std::min(gx0 + 1, 8);
|
||||
int gy1 = std::min(gy0 + 1, 8);
|
||||
|
||||
float tx = fracX - gx0;
|
||||
float ty = fracY - gy0;
|
||||
|
||||
// Outer vertex heights from the 9x17 layout
|
||||
// Outer vertex (gx, gy) is at index: gy * 17 + gx
|
||||
float h00 = chunk.heightMap.heights[gy0 * 17 + gx0];
|
||||
float h10 = chunk.heightMap.heights[gy0 * 17 + gx1];
|
||||
float h01 = chunk.heightMap.heights[gy1 * 17 + gx0];
|
||||
float h11 = chunk.heightMap.heights[gy1 * 17 + gx1];
|
||||
|
||||
float h = h00 * (1 - tx) * (1 - ty) +
|
||||
h10 * tx * (1 - ty) +
|
||||
h01 * (1 - tx) * ty +
|
||||
h11 * tx * ty;
|
||||
|
||||
return chunk.position[2] + h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void TerrainManager::streamTiles() {
|
||||
// Enqueue tiles in radius around current tile for async loading
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
|
||||
for (int dy = -loadRadius; dy <= loadRadius; dy++) {
|
||||
for (int dx = -loadRadius; dx <= loadRadius; dx++) {
|
||||
int tileX = currentTile.x + dx;
|
||||
int tileY = currentTile.y + dy;
|
||||
|
||||
// Check valid range
|
||||
if (tileX < 0 || tileX > 63 || tileY < 0 || tileY > 63) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TileCoord coord = {tileX, tileY};
|
||||
|
||||
// Skip if already loaded, pending, or failed
|
||||
if (loadedTiles.find(coord) != loadedTiles.end()) continue;
|
||||
if (pendingTiles.find(coord) != pendingTiles.end()) continue;
|
||||
if (failedTiles.find(coord) != failedTiles.end()) continue;
|
||||
|
||||
loadQueue.push(coord);
|
||||
pendingTiles[coord] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify worker thread that there's work
|
||||
queueCV.notify_one();
|
||||
|
||||
// Unload tiles beyond unload radius (well past the camera far clip)
|
||||
std::vector<TileCoord> tilesToUnload;
|
||||
|
||||
for (const auto& pair : loadedTiles) {
|
||||
const TileCoord& coord = pair.first;
|
||||
|
||||
int dx = std::abs(coord.x - currentTile.x);
|
||||
int dy = std::abs(coord.y - currentTile.y);
|
||||
|
||||
// Chebyshev distance
|
||||
if (dx > unloadRadius || dy > unloadRadius) {
|
||||
tilesToUnload.push_back(coord);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& coord : tilesToUnload) {
|
||||
unloadTile(coord.x, coord.y);
|
||||
}
|
||||
|
||||
if (!tilesToUnload.empty()) {
|
||||
LOG_INFO("Unloaded ", tilesToUnload.size(), " distant tiles, ",
|
||||
loadedTiles.size(), " remain");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
520
src/rendering/terrain_renderer.cpp
Normal file
520
src/rendering/terrain_renderer.cpp
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
#include "rendering/terrain_renderer.hpp"
|
||||
#include "rendering/frustum.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/blp_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
TerrainRenderer::TerrainRenderer() {
|
||||
}
|
||||
|
||||
TerrainRenderer::~TerrainRenderer() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool TerrainRenderer::initialize(pipeline::AssetManager* assets) {
|
||||
assetManager = assets;
|
||||
|
||||
if (!assetManager) {
|
||||
LOG_ERROR("Asset manager is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Initializing terrain renderer");
|
||||
|
||||
// Load terrain shader
|
||||
shader = std::make_unique<Shader>();
|
||||
if (!shader->loadFromFile("assets/shaders/terrain.vert", "assets/shaders/terrain.frag")) {
|
||||
LOG_ERROR("Failed to load terrain shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create default white texture for fallback
|
||||
uint8_t whitePixel[4] = {255, 255, 255, 255};
|
||||
glGenTextures(1, &whiteTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, whiteTexture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
LOG_INFO("Terrain renderer initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void TerrainRenderer::shutdown() {
|
||||
LOG_INFO("Shutting down terrain renderer");
|
||||
|
||||
clear();
|
||||
|
||||
// Delete white texture
|
||||
if (whiteTexture) {
|
||||
glDeleteTextures(1, &whiteTexture);
|
||||
whiteTexture = 0;
|
||||
}
|
||||
|
||||
// Delete cached textures
|
||||
for (auto& pair : textureCache) {
|
||||
glDeleteTextures(1, &pair.second);
|
||||
}
|
||||
textureCache.clear();
|
||||
|
||||
shader.reset();
|
||||
}
|
||||
|
||||
bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh,
|
||||
const std::vector<std::string>& texturePaths,
|
||||
int tileX, int tileY) {
|
||||
LOG_INFO("Loading terrain mesh: ", mesh.validChunkCount, " chunks");
|
||||
|
||||
// Upload each chunk to GPU
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
const auto& chunk = mesh.getChunk(x, y);
|
||||
|
||||
if (!chunk.isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TerrainChunkGPU gpuChunk = uploadChunk(chunk);
|
||||
|
||||
if (!gpuChunk.isValid()) {
|
||||
LOG_WARNING("Failed to upload chunk [", x, ",", y, "]");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate bounding sphere for frustum culling
|
||||
calculateBoundingSphere(gpuChunk, chunk);
|
||||
|
||||
// Load textures for this chunk
|
||||
if (!chunk.layers.empty()) {
|
||||
// Base layer (always present)
|
||||
uint32_t baseTexId = chunk.layers[0].textureId;
|
||||
if (baseTexId < texturePaths.size()) {
|
||||
gpuChunk.baseTexture = loadTexture(texturePaths[baseTexId]);
|
||||
} else {
|
||||
gpuChunk.baseTexture = whiteTexture;
|
||||
}
|
||||
|
||||
// Additional layers (with alpha blending)
|
||||
for (size_t i = 1; i < chunk.layers.size() && i < 4; i++) {
|
||||
const auto& layer = chunk.layers[i];
|
||||
|
||||
// Load layer texture
|
||||
GLuint layerTex = whiteTexture;
|
||||
if (layer.textureId < texturePaths.size()) {
|
||||
layerTex = loadTexture(texturePaths[layer.textureId]);
|
||||
}
|
||||
gpuChunk.layerTextures.push_back(layerTex);
|
||||
|
||||
// Create alpha texture
|
||||
GLuint alphaTex = 0;
|
||||
if (!layer.alphaData.empty()) {
|
||||
alphaTex = createAlphaTexture(layer.alphaData);
|
||||
}
|
||||
gpuChunk.alphaTextures.push_back(alphaTex);
|
||||
}
|
||||
} else {
|
||||
// No layers, use default white texture
|
||||
gpuChunk.baseTexture = whiteTexture;
|
||||
}
|
||||
|
||||
gpuChunk.tileX = tileX;
|
||||
gpuChunk.tileY = tileY;
|
||||
chunks.push_back(gpuChunk);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded ", chunks.size(), " terrain chunks to GPU");
|
||||
return !chunks.empty();
|
||||
}
|
||||
|
||||
TerrainChunkGPU TerrainRenderer::uploadChunk(const pipeline::ChunkMesh& chunk) {
|
||||
TerrainChunkGPU gpuChunk;
|
||||
|
||||
gpuChunk.worldX = chunk.worldX;
|
||||
gpuChunk.worldY = chunk.worldY;
|
||||
gpuChunk.worldZ = chunk.worldZ;
|
||||
gpuChunk.indexCount = static_cast<uint32_t>(chunk.indices.size());
|
||||
|
||||
// Debug: verify Z values in uploaded vertices
|
||||
static int uploadLogCount = 0;
|
||||
if (uploadLogCount < 3 && !chunk.vertices.empty()) {
|
||||
float minZ = 999999.0f, maxZ = -999999.0f;
|
||||
for (const auto& v : chunk.vertices) {
|
||||
if (v.position[2] < minZ) minZ = v.position[2];
|
||||
if (v.position[2] > maxZ) maxZ = v.position[2];
|
||||
}
|
||||
LOG_DEBUG("GPU upload Z range: [", minZ, ", ", maxZ, "] delta=", maxZ - minZ);
|
||||
uploadLogCount++;
|
||||
}
|
||||
|
||||
// Create VAO
|
||||
glGenVertexArrays(1, &gpuChunk.vao);
|
||||
glBindVertexArray(gpuChunk.vao);
|
||||
|
||||
// Create VBO
|
||||
glGenBuffers(1, &gpuChunk.vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, gpuChunk.vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER,
|
||||
chunk.vertices.size() * sizeof(pipeline::TerrainVertex),
|
||||
chunk.vertices.data(),
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
// Create IBO
|
||||
glGenBuffers(1, &gpuChunk.ibo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuChunk.ibo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
|
||||
chunk.indices.size() * sizeof(pipeline::TerrainIndex),
|
||||
chunk.indices.data(),
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
// Set up vertex attributes
|
||||
// Location 0: Position (vec3)
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
|
||||
sizeof(pipeline::TerrainVertex),
|
||||
(void*)offsetof(pipeline::TerrainVertex, position));
|
||||
|
||||
// Location 1: Normal (vec3)
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,
|
||||
sizeof(pipeline::TerrainVertex),
|
||||
(void*)offsetof(pipeline::TerrainVertex, normal));
|
||||
|
||||
// Location 2: TexCoord (vec2)
|
||||
glEnableVertexAttribArray(2);
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE,
|
||||
sizeof(pipeline::TerrainVertex),
|
||||
(void*)offsetof(pipeline::TerrainVertex, texCoord));
|
||||
|
||||
// Location 3: LayerUV (vec2)
|
||||
glEnableVertexAttribArray(3);
|
||||
glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE,
|
||||
sizeof(pipeline::TerrainVertex),
|
||||
(void*)offsetof(pipeline::TerrainVertex, layerUV));
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
return gpuChunk;
|
||||
}
|
||||
|
||||
GLuint TerrainRenderer::loadTexture(const std::string& path) {
|
||||
// Check cache first
|
||||
auto it = textureCache.find(path);
|
||||
if (it != textureCache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Load BLP texture
|
||||
pipeline::BLPImage blp = assetManager->loadTexture(path);
|
||||
if (!blp.isValid()) {
|
||||
LOG_WARNING("Failed to load texture: ", path);
|
||||
textureCache[path] = whiteTexture;
|
||||
return whiteTexture;
|
||||
}
|
||||
|
||||
// Create OpenGL texture
|
||||
GLuint textureID;
|
||||
glGenTextures(1, &textureID);
|
||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||
|
||||
// Upload texture data (BLP loader outputs RGBA8)
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
|
||||
blp.width, blp.height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data());
|
||||
|
||||
// Set texture parameters
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
|
||||
// Generate mipmaps
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
// Cache texture
|
||||
textureCache[path] = textureID;
|
||||
|
||||
LOG_DEBUG("Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
|
||||
|
||||
return textureID;
|
||||
}
|
||||
|
||||
GLuint TerrainRenderer::createAlphaTexture(const std::vector<uint8_t>& alphaData) {
|
||||
if (alphaData.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
GLuint textureID;
|
||||
glGenTextures(1, &textureID);
|
||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||
|
||||
// Alpha data is always expanded to 4096 bytes (64x64 at 8-bit) by terrain_mesh
|
||||
int width = 64;
|
||||
int height = static_cast<int>(alphaData.size()) / 64;
|
||||
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED,
|
||||
width, height, 0,
|
||||
GL_RED, GL_UNSIGNED_BYTE, alphaData.data());
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
return textureID;
|
||||
}
|
||||
|
||||
void TerrainRenderer::render(const Camera& camera) {
|
||||
if (chunks.empty() || !shader) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable depth testing
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
|
||||
// Disable backface culling temporarily to debug flashing
|
||||
glDisable(GL_CULL_FACE);
|
||||
// glEnable(GL_CULL_FACE);
|
||||
// glCullFace(GL_BACK);
|
||||
|
||||
// Wireframe mode
|
||||
if (wireframe) {
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
|
||||
} else {
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
}
|
||||
|
||||
// Use shader
|
||||
shader->use();
|
||||
|
||||
// Set view/projection matrices
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 projection = camera.getProjectionMatrix();
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
|
||||
shader->setUniform("uModel", model);
|
||||
shader->setUniform("uView", view);
|
||||
shader->setUniform("uProjection", projection);
|
||||
|
||||
// Set lighting
|
||||
shader->setUniform("uLightDir", glm::vec3(lightDir[0], lightDir[1], lightDir[2]));
|
||||
shader->setUniform("uLightColor", glm::vec3(lightColor[0], lightColor[1], lightColor[2]));
|
||||
shader->setUniform("uAmbientColor", glm::vec3(ambientColor[0], ambientColor[1], ambientColor[2]));
|
||||
|
||||
// Set camera position
|
||||
glm::vec3 camPos = camera.getPosition();
|
||||
shader->setUniform("uViewPos", camPos);
|
||||
|
||||
// Set fog (disable by setting very far distances)
|
||||
shader->setUniform("uFogColor", glm::vec3(fogColor[0], fogColor[1], fogColor[2]));
|
||||
if (fogEnabled) {
|
||||
shader->setUniform("uFogStart", fogStart);
|
||||
shader->setUniform("uFogEnd", fogEnd);
|
||||
} else {
|
||||
shader->setUniform("uFogStart", 100000.0f); // Very far
|
||||
shader->setUniform("uFogEnd", 100001.0f); // Effectively disabled
|
||||
}
|
||||
|
||||
// Extract frustum for culling
|
||||
Frustum frustum;
|
||||
if (frustumCullingEnabled) {
|
||||
glm::mat4 viewProj = projection * view;
|
||||
frustum.extractFromMatrix(viewProj);
|
||||
}
|
||||
|
||||
// Render each chunk
|
||||
renderedChunks = 0;
|
||||
culledChunks = 0;
|
||||
for (const auto& chunk : chunks) {
|
||||
if (!chunk.isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Frustum culling
|
||||
if (frustumCullingEnabled && !isChunkVisible(chunk, frustum)) {
|
||||
culledChunks++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bind textures
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, chunk.baseTexture);
|
||||
shader->setUniform("uBaseTexture", 0);
|
||||
|
||||
// Bind layer textures and alphas
|
||||
bool hasLayer1 = chunk.layerTextures.size() > 0;
|
||||
bool hasLayer2 = chunk.layerTextures.size() > 1;
|
||||
bool hasLayer3 = chunk.layerTextures.size() > 2;
|
||||
|
||||
shader->setUniform("uHasLayer1", hasLayer1 ? 1 : 0);
|
||||
shader->setUniform("uHasLayer2", hasLayer2 ? 1 : 0);
|
||||
shader->setUniform("uHasLayer3", hasLayer3 ? 1 : 0);
|
||||
|
||||
if (hasLayer1) {
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[0]);
|
||||
shader->setUniform("uLayer1Texture", 1);
|
||||
|
||||
glActiveTexture(GL_TEXTURE4);
|
||||
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[0]);
|
||||
shader->setUniform("uLayer1Alpha", 4);
|
||||
}
|
||||
|
||||
if (hasLayer2) {
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[1]);
|
||||
shader->setUniform("uLayer2Texture", 2);
|
||||
|
||||
glActiveTexture(GL_TEXTURE5);
|
||||
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[1]);
|
||||
shader->setUniform("uLayer2Alpha", 5);
|
||||
}
|
||||
|
||||
if (hasLayer3) {
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[2]);
|
||||
shader->setUniform("uLayer3Texture", 3);
|
||||
|
||||
glActiveTexture(GL_TEXTURE6);
|
||||
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[2]);
|
||||
shader->setUniform("uLayer3Alpha", 6);
|
||||
}
|
||||
|
||||
// Draw chunk
|
||||
glBindVertexArray(chunk.vao);
|
||||
glDrawElements(GL_TRIANGLES, chunk.indexCount, GL_UNSIGNED_INT, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
renderedChunks++;
|
||||
}
|
||||
|
||||
// Reset wireframe
|
||||
if (wireframe) {
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainRenderer::removeTile(int tileX, int tileY) {
|
||||
int removed = 0;
|
||||
auto it = chunks.begin();
|
||||
while (it != chunks.end()) {
|
||||
if (it->tileX == tileX && it->tileY == tileY) {
|
||||
if (it->vao) glDeleteVertexArrays(1, &it->vao);
|
||||
if (it->vbo) glDeleteBuffers(1, &it->vbo);
|
||||
if (it->ibo) glDeleteBuffers(1, &it->ibo);
|
||||
for (GLuint alpha : it->alphaTextures) {
|
||||
if (alpha) glDeleteTextures(1, &alpha);
|
||||
}
|
||||
it = chunks.erase(it);
|
||||
removed++;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (removed > 0) {
|
||||
LOG_DEBUG("Removed ", removed, " terrain chunks for tile [", tileX, ",", tileY, "]");
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainRenderer::clear() {
|
||||
// Delete all GPU resources
|
||||
for (auto& chunk : chunks) {
|
||||
if (chunk.vao) glDeleteVertexArrays(1, &chunk.vao);
|
||||
if (chunk.vbo) glDeleteBuffers(1, &chunk.vbo);
|
||||
if (chunk.ibo) glDeleteBuffers(1, &chunk.ibo);
|
||||
|
||||
// Delete alpha textures (not cached)
|
||||
for (GLuint alpha : chunk.alphaTextures) {
|
||||
if (alpha) glDeleteTextures(1, &alpha);
|
||||
}
|
||||
}
|
||||
|
||||
chunks.clear();
|
||||
renderedChunks = 0;
|
||||
}
|
||||
|
||||
void TerrainRenderer::setLighting(const float lightDirIn[3], const float lightColorIn[3],
|
||||
const float ambientColorIn[3]) {
|
||||
lightDir[0] = lightDirIn[0];
|
||||
lightDir[1] = lightDirIn[1];
|
||||
lightDir[2] = lightDirIn[2];
|
||||
|
||||
lightColor[0] = lightColorIn[0];
|
||||
lightColor[1] = lightColorIn[1];
|
||||
lightColor[2] = lightColorIn[2];
|
||||
|
||||
ambientColor[0] = ambientColorIn[0];
|
||||
ambientColor[1] = ambientColorIn[1];
|
||||
ambientColor[2] = ambientColorIn[2];
|
||||
}
|
||||
|
||||
void TerrainRenderer::setFog(const float fogColorIn[3], float fogStartIn, float fogEndIn) {
|
||||
fogColor[0] = fogColorIn[0];
|
||||
fogColor[1] = fogColorIn[1];
|
||||
fogColor[2] = fogColorIn[2];
|
||||
fogStart = fogStartIn;
|
||||
fogEnd = fogEndIn;
|
||||
}
|
||||
|
||||
int TerrainRenderer::getTriangleCount() const {
|
||||
int total = 0;
|
||||
for (const auto& chunk : chunks) {
|
||||
total += chunk.indexCount / 3;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
bool TerrainRenderer::isChunkVisible(const TerrainChunkGPU& chunk, const Frustum& frustum) {
|
||||
// Test bounding sphere against frustum
|
||||
return frustum.intersectsSphere(chunk.boundingSphereCenter, chunk.boundingSphereRadius);
|
||||
}
|
||||
|
||||
void TerrainRenderer::calculateBoundingSphere(TerrainChunkGPU& gpuChunk,
|
||||
const pipeline::ChunkMesh& meshChunk) {
|
||||
if (meshChunk.vertices.empty()) {
|
||||
gpuChunk.boundingSphereRadius = 0.0f;
|
||||
gpuChunk.boundingSphereCenter = glm::vec3(0.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate AABB first
|
||||
glm::vec3 min(std::numeric_limits<float>::max());
|
||||
glm::vec3 max(std::numeric_limits<float>::lowest());
|
||||
|
||||
for (const auto& vertex : meshChunk.vertices) {
|
||||
glm::vec3 pos(vertex.position[0], vertex.position[1], vertex.position[2]);
|
||||
min = glm::min(min, pos);
|
||||
max = glm::max(max, pos);
|
||||
}
|
||||
|
||||
// Center is midpoint of AABB
|
||||
gpuChunk.boundingSphereCenter = (min + max) * 0.5f;
|
||||
|
||||
// Radius is distance from center to furthest vertex
|
||||
float maxDistSq = 0.0f;
|
||||
for (const auto& vertex : meshChunk.vertices) {
|
||||
glm::vec3 pos(vertex.position[0], vertex.position[1], vertex.position[2]);
|
||||
glm::vec3 diff = pos - gpuChunk.boundingSphereCenter;
|
||||
float distSq = glm::dot(diff, diff);
|
||||
maxDistSq = std::max(maxDistSq, distSq);
|
||||
}
|
||||
|
||||
gpuChunk.boundingSphereRadius = std::sqrt(maxDistSq);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
51
src/rendering/texture.cpp
Normal file
51
src/rendering/texture.cpp
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#include "rendering/texture.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
// Stub implementation - would use stb_image or similar
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
Texture::~Texture() {
|
||||
if (textureID) {
|
||||
glDeleteTextures(1, &textureID);
|
||||
}
|
||||
}
|
||||
|
||||
bool Texture::loadFromFile(const std::string& path) {
|
||||
// TODO: Implement with stb_image or BLP loader
|
||||
LOG_WARNING("Texture loading not yet implemented: ", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Texture::loadFromMemory(const unsigned char* data, int w, int h, int channels) {
|
||||
width = w;
|
||||
height = h;
|
||||
|
||||
glGenTextures(1, &textureID);
|
||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||
|
||||
GLenum format = (channels == 4) ? GL_RGBA : GL_RGB;
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Texture::bind(GLuint unit) const {
|
||||
glActiveTexture(GL_TEXTURE0 + unit);
|
||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||
}
|
||||
|
||||
void Texture::unbind() const {
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
497
src/rendering/water_renderer.cpp
Normal file
497
src/rendering/water_renderer.cpp
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
#include "rendering/water_renderer.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "pipeline/adt_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
WaterRenderer::WaterRenderer() = default;
|
||||
|
||||
WaterRenderer::~WaterRenderer() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool WaterRenderer::initialize() {
|
||||
LOG_INFO("Initializing water renderer");
|
||||
|
||||
// Create water shader
|
||||
waterShader = std::make_unique<Shader>();
|
||||
|
||||
// Vertex shader
|
||||
const char* vertexShaderSource = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in vec3 aNormal;
|
||||
layout (location = 2) in vec2 aTexCoord;
|
||||
|
||||
uniform mat4 model;
|
||||
uniform mat4 view;
|
||||
uniform mat4 projection;
|
||||
uniform float time;
|
||||
|
||||
out vec3 FragPos;
|
||||
out vec3 Normal;
|
||||
out vec2 TexCoord;
|
||||
out float WaveOffset;
|
||||
|
||||
void main() {
|
||||
// Simple pass-through for debugging (no wave animation)
|
||||
vec3 pos = aPos;
|
||||
|
||||
FragPos = vec3(model * vec4(pos, 1.0));
|
||||
Normal = mat3(transpose(inverse(model))) * aNormal;
|
||||
TexCoord = aTexCoord;
|
||||
WaveOffset = 0.0;
|
||||
|
||||
gl_Position = projection * view * vec4(FragPos, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Fragment shader
|
||||
const char* fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
in vec3 FragPos;
|
||||
in vec3 Normal;
|
||||
in vec2 TexCoord;
|
||||
in float WaveOffset;
|
||||
|
||||
uniform vec3 viewPos;
|
||||
uniform vec4 waterColor;
|
||||
uniform float waterAlpha;
|
||||
uniform float time;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
// Normalize interpolated normal
|
||||
vec3 norm = normalize(Normal);
|
||||
|
||||
// Simple directional light (sun)
|
||||
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3));
|
||||
float diff = max(dot(norm, lightDir), 0.0);
|
||||
|
||||
// Specular highlights (shininess for water)
|
||||
vec3 viewDir = normalize(viewPos - FragPos);
|
||||
vec3 reflectDir = reflect(-lightDir, norm);
|
||||
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 64.0);
|
||||
|
||||
// Animated texture coordinates for flowing effect
|
||||
vec2 uv1 = TexCoord + vec2(time * 0.02, time * 0.01);
|
||||
vec2 uv2 = TexCoord + vec2(-time * 0.01, time * 0.015);
|
||||
|
||||
// Combine lighting
|
||||
vec3 ambient = vec3(0.3) * waterColor.rgb;
|
||||
vec3 diffuse = vec3(0.6) * diff * waterColor.rgb;
|
||||
vec3 specular = vec3(1.0) * spec;
|
||||
|
||||
// Add wave offset to brightness
|
||||
float brightness = 1.0 + WaveOffset * 0.1;
|
||||
|
||||
vec3 result = (ambient + diffuse + specular) * brightness;
|
||||
|
||||
// Apply transparency
|
||||
FragColor = vec4(result, waterAlpha);
|
||||
}
|
||||
)";
|
||||
|
||||
if (!waterShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
|
||||
LOG_ERROR("Failed to create water shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Water renderer initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void WaterRenderer::shutdown() {
|
||||
clear();
|
||||
waterShader.reset();
|
||||
}
|
||||
|
||||
void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append,
|
||||
int tileX, int tileY) {
|
||||
if (!append) {
|
||||
LOG_INFO("Loading water from terrain (replacing)");
|
||||
clear();
|
||||
} else {
|
||||
LOG_INFO("Loading water from terrain (appending)");
|
||||
}
|
||||
|
||||
// Load water surfaces from MH2O data
|
||||
int totalLayers = 0;
|
||||
|
||||
for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) {
|
||||
const auto& chunkWater = terrain.waterData[chunkIdx];
|
||||
|
||||
if (!chunkWater.hasWater()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the terrain chunk for position reference
|
||||
int chunkX = chunkIdx % 16;
|
||||
int chunkY = chunkIdx / 16;
|
||||
const auto& terrainChunk = terrain.getChunk(chunkX, chunkY);
|
||||
|
||||
// Process each water layer in this chunk
|
||||
for (const auto& layer : chunkWater.layers) {
|
||||
WaterSurface surface;
|
||||
|
||||
// Use the chunk base position - layer offsets will be applied in mesh generation
|
||||
// to match terrain's coordinate transformation
|
||||
surface.position = glm::vec3(
|
||||
terrainChunk.position[0],
|
||||
terrainChunk.position[1],
|
||||
layer.minHeight
|
||||
);
|
||||
|
||||
// Debug log first few water surfaces
|
||||
if (totalLayers < 5) {
|
||||
LOG_DEBUG("Water layer ", totalLayers, ": chunk=", chunkIdx,
|
||||
" liquidType=", layer.liquidType,
|
||||
" offset=(", (int)layer.x, ",", (int)layer.y, ")",
|
||||
" size=", (int)layer.width, "x", (int)layer.height,
|
||||
" height range=[", layer.minHeight, ",", layer.maxHeight, "]");
|
||||
}
|
||||
|
||||
surface.minHeight = layer.minHeight;
|
||||
surface.maxHeight = layer.maxHeight;
|
||||
surface.liquidType = layer.liquidType;
|
||||
|
||||
// Store dimensions
|
||||
surface.xOffset = layer.x;
|
||||
surface.yOffset = layer.y;
|
||||
surface.width = layer.width;
|
||||
surface.height = layer.height;
|
||||
|
||||
// Copy height data
|
||||
if (!layer.heights.empty()) {
|
||||
surface.heights = layer.heights;
|
||||
} else {
|
||||
// Flat water at minHeight if no height data
|
||||
size_t numVertices = (layer.width + 1) * (layer.height + 1);
|
||||
surface.heights.resize(numVertices, layer.minHeight);
|
||||
}
|
||||
|
||||
// Copy render mask
|
||||
surface.mask = layer.mask;
|
||||
|
||||
surface.tileX = tileX;
|
||||
surface.tileY = tileY;
|
||||
createWaterMesh(surface);
|
||||
surfaces.push_back(surface);
|
||||
totalLayers++;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded ", totalLayers, " water layers from MH2O data");
|
||||
}
|
||||
|
||||
void WaterRenderer::removeTile(int tileX, int tileY) {
|
||||
int removed = 0;
|
||||
auto it = surfaces.begin();
|
||||
while (it != surfaces.end()) {
|
||||
if (it->tileX == tileX && it->tileY == tileY) {
|
||||
destroyWaterMesh(*it);
|
||||
it = surfaces.erase(it);
|
||||
removed++;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (removed > 0) {
|
||||
LOG_DEBUG("Removed ", removed, " water surfaces for tile [", tileX, ",", tileY, "]");
|
||||
}
|
||||
}
|
||||
|
||||
void WaterRenderer::clear() {
|
||||
for (auto& surface : surfaces) {
|
||||
destroyWaterMesh(surface);
|
||||
}
|
||||
surfaces.clear();
|
||||
}
|
||||
|
||||
void WaterRenderer::render(const Camera& camera, float time) {
|
||||
if (!renderingEnabled || surfaces.empty() || !waterShader) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable alpha blending for transparent water
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Disable depth writing so terrain shows through water
|
||||
glDepthMask(GL_FALSE);
|
||||
|
||||
waterShader->use();
|
||||
|
||||
// Set uniforms
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 projection = camera.getProjectionMatrix();
|
||||
|
||||
waterShader->setUniform("view", view);
|
||||
waterShader->setUniform("projection", projection);
|
||||
waterShader->setUniform("viewPos", camera.getPosition());
|
||||
waterShader->setUniform("time", time);
|
||||
|
||||
// Render each water surface
|
||||
for (const auto& surface : surfaces) {
|
||||
if (surface.vao == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Model matrix (identity, position already in vertices)
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
waterShader->setUniform("model", model);
|
||||
|
||||
// Set liquid-specific color and alpha
|
||||
glm::vec4 color = getLiquidColor(surface.liquidType);
|
||||
float alpha = getLiquidAlpha(surface.liquidType);
|
||||
|
||||
waterShader->setUniform("waterColor", color);
|
||||
waterShader->setUniform("waterAlpha", alpha);
|
||||
|
||||
// Render
|
||||
glBindVertexArray(surface.vao);
|
||||
glDrawElements(GL_TRIANGLES, surface.indexCount, GL_UNSIGNED_INT, nullptr);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
// Restore state
|
||||
glDepthMask(GL_TRUE);
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
void WaterRenderer::createWaterMesh(WaterSurface& surface) {
|
||||
// Variable-size grid based on water layer dimensions
|
||||
const int gridWidth = surface.width + 1; // Vertices = tiles + 1
|
||||
const int gridHeight = surface.height + 1;
|
||||
const float TILE_SIZE = 33.33333f / 8.0f; // Size of one tile (same as terrain unitSize)
|
||||
|
||||
std::vector<float> vertices;
|
||||
std::vector<uint32_t> indices;
|
||||
|
||||
// Generate vertices
|
||||
// Match terrain coordinate transformation: pos[0] = baseX - (y * unitSize), pos[1] = baseY - (x * unitSize)
|
||||
for (int y = 0; y < gridHeight; y++) {
|
||||
for (int x = 0; x < gridWidth; x++) {
|
||||
int index = y * gridWidth + x;
|
||||
|
||||
// Use per-vertex height data if available, otherwise flat at minHeight
|
||||
float height;
|
||||
if (index < static_cast<int>(surface.heights.size())) {
|
||||
height = surface.heights[index];
|
||||
} else {
|
||||
height = surface.minHeight;
|
||||
}
|
||||
|
||||
// Position - match terrain coordinate transformation (swap and negate)
|
||||
// Terrain uses: X = baseX - (offsetY * unitSize), Y = baseY - (offsetX * unitSize)
|
||||
// Also apply layer offset within chunk (xOffset, yOffset)
|
||||
float posX = surface.position.x - ((surface.yOffset + y) * TILE_SIZE);
|
||||
float posY = surface.position.y - ((surface.xOffset + x) * TILE_SIZE);
|
||||
float posZ = height;
|
||||
|
||||
// Debug first surface's corner vertices
|
||||
static int debugCount = 0;
|
||||
if (debugCount < 4 && (x == 0 || x == gridWidth-1) && (y == 0 || y == gridHeight-1)) {
|
||||
LOG_DEBUG("Water vertex: (", posX, ", ", posY, ", ", posZ, ")");
|
||||
debugCount++;
|
||||
}
|
||||
|
||||
vertices.push_back(posX);
|
||||
vertices.push_back(posY);
|
||||
vertices.push_back(posZ);
|
||||
|
||||
// Normal (pointing up for water surface)
|
||||
vertices.push_back(0.0f);
|
||||
vertices.push_back(0.0f);
|
||||
vertices.push_back(1.0f);
|
||||
|
||||
// Texture coordinates
|
||||
vertices.push_back(static_cast<float>(x) / std::max(1, gridWidth - 1));
|
||||
vertices.push_back(static_cast<float>(y) / std::max(1, gridHeight - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate indices (triangles), respecting the render mask
|
||||
for (int y = 0; y < gridHeight - 1; y++) {
|
||||
for (int x = 0; x < gridWidth - 1; x++) {
|
||||
// Check render mask - each bit represents a tile
|
||||
bool renderTile = true;
|
||||
if (!surface.mask.empty()) {
|
||||
int tileIndex = y * surface.width + x;
|
||||
int byteIndex = tileIndex / 8;
|
||||
int bitIndex = tileIndex % 8;
|
||||
if (byteIndex < static_cast<int>(surface.mask.size())) {
|
||||
renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!renderTile) {
|
||||
continue; // Skip this tile
|
||||
}
|
||||
|
||||
int topLeft = y * gridWidth + x;
|
||||
int topRight = topLeft + 1;
|
||||
int bottomLeft = (y + 1) * gridWidth + x;
|
||||
int bottomRight = bottomLeft + 1;
|
||||
|
||||
// First triangle
|
||||
indices.push_back(topLeft);
|
||||
indices.push_back(bottomLeft);
|
||||
indices.push_back(topRight);
|
||||
|
||||
// Second triangle
|
||||
indices.push_back(topRight);
|
||||
indices.push_back(bottomLeft);
|
||||
indices.push_back(bottomRight);
|
||||
}
|
||||
}
|
||||
|
||||
if (indices.empty()) {
|
||||
// No visible tiles
|
||||
return;
|
||||
}
|
||||
|
||||
surface.indexCount = static_cast<int>(indices.size());
|
||||
|
||||
// Create OpenGL buffers
|
||||
glGenVertexArrays(1, &surface.vao);
|
||||
glGenBuffers(1, &surface.vbo);
|
||||
glGenBuffers(1, &surface.ebo);
|
||||
|
||||
glBindVertexArray(surface.vao);
|
||||
|
||||
// Upload vertex data
|
||||
glBindBuffer(GL_ARRAY_BUFFER, surface.vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// Upload index data
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, surface.ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// Set vertex attributes
|
||||
// Position
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
// Normal
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
// Texture coordinates
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
|
||||
glEnableVertexAttribArray(2);
|
||||
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void WaterRenderer::destroyWaterMesh(WaterSurface& surface) {
|
||||
if (surface.vao != 0) {
|
||||
glDeleteVertexArrays(1, &surface.vao);
|
||||
surface.vao = 0;
|
||||
}
|
||||
if (surface.vbo != 0) {
|
||||
glDeleteBuffers(1, &surface.vbo);
|
||||
surface.vbo = 0;
|
||||
}
|
||||
if (surface.ebo != 0) {
|
||||
glDeleteBuffers(1, &surface.ebo);
|
||||
surface.ebo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const {
|
||||
const float TILE_SIZE = 33.33333f / 8.0f;
|
||||
std::optional<float> best;
|
||||
|
||||
for (size_t si = 0; si < surfaces.size(); si++) {
|
||||
const auto& surface = surfaces[si];
|
||||
float gy = (surface.position.x - glX) / TILE_SIZE - static_cast<float>(surface.yOffset);
|
||||
float gx = (surface.position.y - glY) / TILE_SIZE - static_cast<float>(surface.xOffset);
|
||||
|
||||
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
|
||||
gy < 0.0f || gy > static_cast<float>(surface.height)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int gridWidth = surface.width + 1;
|
||||
|
||||
// Bilinear interpolation
|
||||
int ix = static_cast<int>(gx);
|
||||
int iy = static_cast<int>(gy);
|
||||
float fx = gx - ix;
|
||||
float fy = gy - iy;
|
||||
|
||||
// Clamp to valid vertex range
|
||||
if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; }
|
||||
if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; }
|
||||
|
||||
int idx00 = iy * gridWidth + ix;
|
||||
int idx10 = idx00 + 1;
|
||||
int idx01 = idx00 + gridWidth;
|
||||
int idx11 = idx01 + 1;
|
||||
|
||||
int total = static_cast<int>(surface.heights.size());
|
||||
if (idx11 >= total) continue;
|
||||
|
||||
float h00 = surface.heights[idx00];
|
||||
float h10 = surface.heights[idx10];
|
||||
float h01 = surface.heights[idx01];
|
||||
float h11 = surface.heights[idx11];
|
||||
|
||||
float h = h00 * (1-fx) * (1-fy) + h10 * fx * (1-fy) +
|
||||
h01 * (1-fx) * fy + h11 * fx * fy;
|
||||
|
||||
if (!best || h > *best) {
|
||||
best = h;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
glm::vec4 WaterRenderer::getLiquidColor(uint8_t liquidType) const {
|
||||
// WoW 3.3.5a LiquidType.dbc IDs:
|
||||
// 1,5,9,13,17 = Water variants (still, slow, fast)
|
||||
// 2,6,10,14 = Ocean
|
||||
// 3,7,11,15 = Magma
|
||||
// 4,8,12 = Slime
|
||||
// Map to basic type using (id - 1) % 4 for standard IDs, or handle ranges
|
||||
uint8_t basicType;
|
||||
if (liquidType == 0) {
|
||||
basicType = 0; // Water (fallback)
|
||||
} else {
|
||||
basicType = ((liquidType - 1) % 4);
|
||||
}
|
||||
|
||||
switch (basicType) {
|
||||
case 0: // Water
|
||||
return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f);
|
||||
case 1: // Ocean
|
||||
return glm::vec4(0.1f, 0.3f, 0.5f, 1.0f);
|
||||
case 2: // Magma
|
||||
return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f);
|
||||
case 3: // Slime
|
||||
return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f);
|
||||
default:
|
||||
return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); // Water fallback
|
||||
}
|
||||
}
|
||||
|
||||
float WaterRenderer::getLiquidAlpha(uint8_t liquidType) const {
|
||||
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
|
||||
switch (basicType) {
|
||||
case 2: return 0.85f; // Magma - mostly opaque
|
||||
case 3: return 0.75f; // Slime - semi-opaque
|
||||
default: return 0.55f; // Water/Ocean - semi-transparent
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
275
src/rendering/weather.cpp
Normal file
275
src/rendering/weather.cpp
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
#include "rendering/weather.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
Weather::Weather() {
|
||||
}
|
||||
|
||||
Weather::~Weather() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
bool Weather::initialize() {
|
||||
LOG_INFO("Initializing weather system");
|
||||
|
||||
// Create shader
|
||||
shader = std::make_unique<Shader>();
|
||||
|
||||
// Vertex shader - point sprites with instancing
|
||||
const char* vertexShaderSource = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProjection;
|
||||
uniform float uParticleSize;
|
||||
|
||||
void main() {
|
||||
gl_Position = uProjection * uView * vec4(aPos, 1.0);
|
||||
gl_PointSize = uParticleSize;
|
||||
}
|
||||
)";
|
||||
|
||||
// Fragment shader - simple particle with alpha
|
||||
const char* fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
|
||||
uniform vec4 uParticleColor;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
// Circular particle shape
|
||||
vec2 coord = gl_PointCoord - vec2(0.5);
|
||||
float dist = length(coord);
|
||||
|
||||
if (dist > 0.5) {
|
||||
discard;
|
||||
}
|
||||
|
||||
// Soft edges
|
||||
float alpha = smoothstep(0.5, 0.3, dist) * uParticleColor.a;
|
||||
|
||||
FragColor = vec4(uParticleColor.rgb, alpha);
|
||||
}
|
||||
)";
|
||||
|
||||
if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
|
||||
LOG_ERROR("Failed to create weather shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create VAO and VBO for particle positions
|
||||
glGenVertexArrays(1, &vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
|
||||
glBindVertexArray(vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
|
||||
// Position attribute
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Reserve space for particles
|
||||
particles.reserve(MAX_PARTICLES);
|
||||
particlePositions.reserve(MAX_PARTICLES);
|
||||
|
||||
LOG_INFO("Weather system initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Weather::update(const Camera& camera, float deltaTime) {
|
||||
if (!enabled || weatherType == Type::NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize particles if needed
|
||||
if (particles.empty()) {
|
||||
resetParticles(camera);
|
||||
}
|
||||
|
||||
// Calculate active particle count based on intensity
|
||||
int targetParticleCount = static_cast<int>(MAX_PARTICLES * intensity);
|
||||
|
||||
// Adjust particle count
|
||||
while (static_cast<int>(particles.size()) < targetParticleCount) {
|
||||
Particle p;
|
||||
p.position = getRandomPosition(camera.getPosition());
|
||||
p.position.y = camera.getPosition().y + SPAWN_HEIGHT;
|
||||
p.lifetime = 0.0f;
|
||||
|
||||
if (weatherType == Type::RAIN) {
|
||||
p.velocity = glm::vec3(0.0f, -50.0f, 0.0f); // Fast downward
|
||||
p.maxLifetime = 5.0f;
|
||||
} else { // SNOW
|
||||
p.velocity = glm::vec3(0.0f, -5.0f, 0.0f); // Slow downward
|
||||
p.maxLifetime = 10.0f;
|
||||
}
|
||||
|
||||
particles.push_back(p);
|
||||
}
|
||||
|
||||
while (static_cast<int>(particles.size()) > targetParticleCount) {
|
||||
particles.pop_back();
|
||||
}
|
||||
|
||||
// Update each particle
|
||||
for (auto& particle : particles) {
|
||||
updateParticle(particle, camera, deltaTime);
|
||||
}
|
||||
|
||||
// Update position buffer
|
||||
particlePositions.clear();
|
||||
for (const auto& particle : particles) {
|
||||
particlePositions.push_back(particle.position);
|
||||
}
|
||||
}
|
||||
|
||||
void Weather::updateParticle(Particle& particle, const Camera& camera, float deltaTime) {
|
||||
// Update lifetime
|
||||
particle.lifetime += deltaTime;
|
||||
|
||||
// Reset if lifetime exceeded or too far from camera
|
||||
glm::vec3 cameraPos = camera.getPosition();
|
||||
float distance = glm::length(particle.position - cameraPos);
|
||||
|
||||
if (particle.lifetime >= particle.maxLifetime || distance > SPAWN_VOLUME_SIZE ||
|
||||
particle.position.y < cameraPos.y - 20.0f) {
|
||||
// Respawn at top
|
||||
particle.position = getRandomPosition(cameraPos);
|
||||
particle.position.y = cameraPos.y + SPAWN_HEIGHT;
|
||||
particle.lifetime = 0.0f;
|
||||
}
|
||||
|
||||
// Add wind effect for snow
|
||||
if (weatherType == Type::SNOW) {
|
||||
float windX = std::sin(particle.lifetime * 0.5f) * 2.0f;
|
||||
float windZ = std::cos(particle.lifetime * 0.3f) * 2.0f;
|
||||
particle.velocity.x = windX;
|
||||
particle.velocity.z = windZ;
|
||||
}
|
||||
|
||||
// Update position
|
||||
particle.position += particle.velocity * deltaTime;
|
||||
}
|
||||
|
||||
void Weather::render(const Camera& camera) {
|
||||
if (!enabled || weatherType == Type::NONE || particlePositions.empty() || !shader) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable blending
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Disable depth write (particles are transparent)
|
||||
glDepthMask(GL_FALSE);
|
||||
|
||||
// Enable point sprites
|
||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
|
||||
shader->use();
|
||||
|
||||
// Set matrices
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 projection = camera.getProjectionMatrix();
|
||||
|
||||
shader->setUniform("uView", view);
|
||||
shader->setUniform("uProjection", projection);
|
||||
|
||||
// Set particle appearance based on weather type
|
||||
if (weatherType == Type::RAIN) {
|
||||
// Rain: white/blue streaks, small size
|
||||
shader->setUniform("uParticleColor", glm::vec4(0.7f, 0.8f, 0.9f, 0.6f));
|
||||
shader->setUniform("uParticleSize", 3.0f);
|
||||
} else { // SNOW
|
||||
// Snow: white fluffy, larger size
|
||||
shader->setUniform("uParticleColor", glm::vec4(1.0f, 1.0f, 1.0f, 0.9f));
|
||||
shader->setUniform("uParticleSize", 8.0f);
|
||||
}
|
||||
|
||||
// Upload particle positions
|
||||
glBindVertexArray(vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER,
|
||||
particlePositions.size() * sizeof(glm::vec3),
|
||||
particlePositions.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
|
||||
// Render particles as points
|
||||
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(particlePositions.size()));
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Restore state
|
||||
glDisable(GL_BLEND);
|
||||
glDepthMask(GL_TRUE);
|
||||
glDisable(GL_PROGRAM_POINT_SIZE);
|
||||
}
|
||||
|
||||
void Weather::resetParticles(const Camera& camera) {
|
||||
particles.clear();
|
||||
|
||||
int particleCount = static_cast<int>(MAX_PARTICLES * intensity);
|
||||
glm::vec3 cameraPos = camera.getPosition();
|
||||
|
||||
for (int i = 0; i < particleCount; ++i) {
|
||||
Particle p;
|
||||
p.position = getRandomPosition(cameraPos);
|
||||
p.position.y = cameraPos.y + SPAWN_HEIGHT * (static_cast<float>(rand()) / RAND_MAX);
|
||||
p.lifetime = 0.0f;
|
||||
|
||||
if (weatherType == Type::RAIN) {
|
||||
p.velocity = glm::vec3(0.0f, -50.0f, 0.0f);
|
||||
p.maxLifetime = 5.0f;
|
||||
} else { // SNOW
|
||||
p.velocity = glm::vec3(0.0f, -5.0f, 0.0f);
|
||||
p.maxLifetime = 10.0f;
|
||||
}
|
||||
|
||||
particles.push_back(p);
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 Weather::getRandomPosition(const glm::vec3& center) const {
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
|
||||
|
||||
float x = center.x + dist(gen) * SPAWN_VOLUME_SIZE;
|
||||
float z = center.z + dist(gen) * SPAWN_VOLUME_SIZE;
|
||||
float y = center.y;
|
||||
|
||||
return glm::vec3(x, y, z);
|
||||
}
|
||||
|
||||
void Weather::setIntensity(float intensity) {
|
||||
this->intensity = glm::clamp(intensity, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
int Weather::getParticleCount() const {
|
||||
return static_cast<int>(particles.size());
|
||||
}
|
||||
|
||||
void Weather::cleanup() {
|
||||
if (vao) {
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
vao = 0;
|
||||
}
|
||||
if (vbo) {
|
||||
glDeleteBuffers(1, &vbo);
|
||||
vbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
835
src/rendering/wmo_renderer.cpp
Normal file
835
src/rendering/wmo_renderer.cpp
Normal file
|
|
@ -0,0 +1,835 @@
|
|||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "pipeline/wmo_loader.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
WMORenderer::WMORenderer() {
|
||||
}
|
||||
|
||||
WMORenderer::~WMORenderer() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool WMORenderer::initialize(pipeline::AssetManager* assets) {
|
||||
core::Logger::getInstance().info("Initializing WMO renderer...");
|
||||
|
||||
assetManager = assets;
|
||||
|
||||
// Create WMO shader with texture support
|
||||
const char* vertexSrc = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in vec3 aNormal;
|
||||
layout (location = 2) in vec2 aTexCoord;
|
||||
layout (location = 3) in vec4 aColor;
|
||||
|
||||
uniform mat4 uModel;
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProjection;
|
||||
|
||||
out vec3 FragPos;
|
||||
out vec3 Normal;
|
||||
out vec2 TexCoord;
|
||||
out vec4 VertexColor;
|
||||
|
||||
void main() {
|
||||
vec4 worldPos = uModel * vec4(aPos, 1.0);
|
||||
FragPos = worldPos.xyz;
|
||||
Normal = mat3(transpose(inverse(uModel))) * aNormal;
|
||||
TexCoord = aTexCoord;
|
||||
VertexColor = aColor;
|
||||
|
||||
gl_Position = uProjection * uView * worldPos;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* fragmentSrc = R"(
|
||||
#version 330 core
|
||||
in vec3 FragPos;
|
||||
in vec3 Normal;
|
||||
in vec2 TexCoord;
|
||||
in vec4 VertexColor;
|
||||
|
||||
uniform vec3 uLightDir;
|
||||
uniform vec3 uViewPos;
|
||||
uniform vec3 uAmbientColor;
|
||||
uniform sampler2D uTexture;
|
||||
uniform bool uHasTexture;
|
||||
uniform bool uAlphaTest;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
vec3 normal = normalize(Normal);
|
||||
vec3 lightDir = normalize(uLightDir);
|
||||
|
||||
// Diffuse lighting
|
||||
float diff = max(dot(normal, lightDir), 0.0);
|
||||
vec3 diffuse = diff * vec3(1.0);
|
||||
|
||||
// Ambient
|
||||
vec3 ambient = uAmbientColor;
|
||||
|
||||
// Sample texture or use vertex color
|
||||
vec4 texColor;
|
||||
if (uHasTexture) {
|
||||
texColor = texture(uTexture, TexCoord);
|
||||
// Alpha test only for cutout materials (lattice, grating, etc.)
|
||||
if (uAlphaTest && texColor.a < 0.5) discard;
|
||||
} else {
|
||||
// MOCV vertex color alpha is a lighting blend factor, not transparency
|
||||
texColor = vec4(VertexColor.rgb, 1.0);
|
||||
}
|
||||
|
||||
// Combine lighting with texture
|
||||
vec3 result = (ambient + diffuse) * texColor.rgb;
|
||||
FragColor = vec4(result, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
shader = std::make_unique<Shader>();
|
||||
if (!shader->loadFromSource(vertexSrc, fragmentSrc)) {
|
||||
core::Logger::getInstance().error("Failed to create WMO shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create default white texture for fallback
|
||||
uint8_t whitePixel[4] = {255, 255, 255, 255};
|
||||
glGenTextures(1, &whiteTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, whiteTexture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
core::Logger::getInstance().info("WMO renderer initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void WMORenderer::shutdown() {
|
||||
core::Logger::getInstance().info("Shutting down WMO renderer...");
|
||||
|
||||
// Free all GPU resources
|
||||
for (auto& [id, model] : loadedModels) {
|
||||
for (auto& group : model.groups) {
|
||||
if (group.vao != 0) glDeleteVertexArrays(1, &group.vao);
|
||||
if (group.vbo != 0) glDeleteBuffers(1, &group.vbo);
|
||||
if (group.ebo != 0) glDeleteBuffers(1, &group.ebo);
|
||||
}
|
||||
}
|
||||
|
||||
// Free cached textures
|
||||
for (auto& [path, texId] : textureCache) {
|
||||
if (texId != 0 && texId != whiteTexture) {
|
||||
glDeleteTextures(1, &texId);
|
||||
}
|
||||
}
|
||||
textureCache.clear();
|
||||
|
||||
// Free white texture
|
||||
if (whiteTexture != 0) {
|
||||
glDeleteTextures(1, &whiteTexture);
|
||||
whiteTexture = 0;
|
||||
}
|
||||
|
||||
loadedModels.clear();
|
||||
instances.clear();
|
||||
shader.reset();
|
||||
}
|
||||
|
||||
bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||
if (!model.isValid()) {
|
||||
core::Logger::getInstance().error("Cannot load invalid WMO model");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if already loaded
|
||||
if (loadedModels.find(id) != loadedModels.end()) {
|
||||
core::Logger::getInstance().warning("WMO model ", id, " already loaded");
|
||||
return true;
|
||||
}
|
||||
|
||||
core::Logger::getInstance().info("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
|
||||
model.textures.size(), " textures...");
|
||||
|
||||
ModelData modelData;
|
||||
modelData.id = id;
|
||||
modelData.boundingBoxMin = model.boundingBoxMin;
|
||||
modelData.boundingBoxMax = model.boundingBoxMax;
|
||||
|
||||
core::Logger::getInstance().info(" WMO bounds: min=(", model.boundingBoxMin.x, ", ", model.boundingBoxMin.y, ", ", model.boundingBoxMin.z,
|
||||
") max=(", model.boundingBoxMax.x, ", ", model.boundingBoxMax.y, ", ", model.boundingBoxMax.z, ")");
|
||||
|
||||
// Load textures for this model
|
||||
core::Logger::getInstance().info(" WMO has ", model.textures.size(), " texture paths, ", model.materials.size(), " materials");
|
||||
if (assetManager && !model.textures.empty()) {
|
||||
for (size_t i = 0; i < model.textures.size(); i++) {
|
||||
const auto& texPath = model.textures[i];
|
||||
core::Logger::getInstance().debug(" Loading texture ", i, ": ", texPath);
|
||||
GLuint texId = loadTexture(texPath);
|
||||
modelData.textures.push_back(texId);
|
||||
}
|
||||
core::Logger::getInstance().info(" Loaded ", modelData.textures.size(), " textures for WMO");
|
||||
}
|
||||
|
||||
// Store material -> texture index mapping
|
||||
// IMPORTANT: mat.texture1 is a byte offset into MOTX, not an array index!
|
||||
// We need to convert it using the textureOffsetToIndex map
|
||||
core::Logger::getInstance().info(" textureOffsetToIndex map has ", model.textureOffsetToIndex.size(), " entries");
|
||||
static int matLogCount = 0;
|
||||
for (size_t i = 0; i < model.materials.size(); i++) {
|
||||
const auto& mat = model.materials[i];
|
||||
uint32_t texIndex = 0; // Default to first texture
|
||||
|
||||
auto it = model.textureOffsetToIndex.find(mat.texture1);
|
||||
if (it != model.textureOffsetToIndex.end()) {
|
||||
texIndex = it->second;
|
||||
if (matLogCount < 20) {
|
||||
core::Logger::getInstance().info(" Material ", i, ": texture1 offset ", mat.texture1, " -> texture index ", texIndex);
|
||||
matLogCount++;
|
||||
}
|
||||
} else if (mat.texture1 < model.textures.size()) {
|
||||
// Fallback: maybe it IS an index in some files?
|
||||
texIndex = mat.texture1;
|
||||
if (matLogCount < 20) {
|
||||
core::Logger::getInstance().info(" Material ", i, ": using texture1 as direct index: ", texIndex);
|
||||
matLogCount++;
|
||||
}
|
||||
} else {
|
||||
if (matLogCount < 20) {
|
||||
core::Logger::getInstance().info(" Material ", i, ": texture1 offset ", mat.texture1, " NOT FOUND, using default");
|
||||
matLogCount++;
|
||||
}
|
||||
}
|
||||
|
||||
modelData.materialTextureIndices.push_back(texIndex);
|
||||
modelData.materialBlendModes.push_back(mat.blendMode);
|
||||
}
|
||||
|
||||
// Create GPU resources for each group
|
||||
uint32_t loadedGroups = 0;
|
||||
for (const auto& wmoGroup : model.groups) {
|
||||
// Skip empty groups
|
||||
if (wmoGroup.vertices.empty() || wmoGroup.indices.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GroupResources resources;
|
||||
if (createGroupResources(wmoGroup, resources)) {
|
||||
modelData.groups.push_back(resources);
|
||||
loadedGroups++;
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedGroups == 0) {
|
||||
core::Logger::getInstance().warning("No valid groups loaded for WMO ", id);
|
||||
return false;
|
||||
}
|
||||
|
||||
loadedModels[id] = std::move(modelData);
|
||||
core::Logger::getInstance().info("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
|
||||
return true;
|
||||
}
|
||||
|
||||
void WMORenderer::unloadModel(uint32_t id) {
|
||||
auto it = loadedModels.find(id);
|
||||
if (it == loadedModels.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Free GPU resources
|
||||
for (auto& group : it->second.groups) {
|
||||
if (group.vao != 0) glDeleteVertexArrays(1, &group.vao);
|
||||
if (group.vbo != 0) glDeleteBuffers(1, &group.vbo);
|
||||
if (group.ebo != 0) glDeleteBuffers(1, &group.ebo);
|
||||
}
|
||||
|
||||
loadedModels.erase(it);
|
||||
core::Logger::getInstance().info("WMO model ", id, " unloaded");
|
||||
}
|
||||
|
||||
uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position,
|
||||
const glm::vec3& rotation, float scale) {
|
||||
// Check if model is loaded
|
||||
if (loadedModels.find(modelId) == loadedModels.end()) {
|
||||
core::Logger::getInstance().error("Cannot create instance of unloaded WMO model ", modelId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
WMOInstance instance;
|
||||
instance.id = nextInstanceId++;
|
||||
instance.modelId = modelId;
|
||||
instance.position = position;
|
||||
instance.rotation = rotation;
|
||||
instance.scale = scale;
|
||||
instance.updateModelMatrix();
|
||||
|
||||
instances.push_back(instance);
|
||||
core::Logger::getInstance().info("Created WMO instance ", instance.id, " (model ", modelId, ")");
|
||||
return instance.id;
|
||||
}
|
||||
|
||||
void WMORenderer::removeInstance(uint32_t instanceId) {
|
||||
auto it = std::find_if(instances.begin(), instances.end(),
|
||||
[instanceId](const WMOInstance& inst) { return inst.id == instanceId; });
|
||||
if (it != instances.end()) {
|
||||
instances.erase(it);
|
||||
core::Logger::getInstance().info("Removed WMO instance ", instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
void WMORenderer::clearInstances() {
|
||||
instances.clear();
|
||||
core::Logger::getInstance().info("Cleared all WMO instances");
|
||||
}
|
||||
|
||||
void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection) {
|
||||
if (!shader || instances.empty()) {
|
||||
lastDrawCalls = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
lastDrawCalls = 0;
|
||||
|
||||
// Set shader uniforms
|
||||
shader->use();
|
||||
shader->setUniform("uView", view);
|
||||
shader->setUniform("uProjection", projection);
|
||||
shader->setUniform("uViewPos", camera.getPosition());
|
||||
shader->setUniform("uLightDir", glm::vec3(-0.3f, -0.7f, -0.6f)); // Default sun direction
|
||||
shader->setUniform("uAmbientColor", glm::vec3(0.4f, 0.4f, 0.5f));
|
||||
|
||||
// Enable wireframe if requested
|
||||
if (wireframeMode) {
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
|
||||
}
|
||||
|
||||
// WMOs are opaque — ensure blending is off (alpha test via discard in shader)
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
// Disable backface culling for WMOs (some faces may have wrong winding)
|
||||
glDisable(GL_CULL_FACE);
|
||||
|
||||
// Render all instances
|
||||
for (const auto& instance : instances) {
|
||||
auto modelIt = loadedModels.find(instance.modelId);
|
||||
if (modelIt == loadedModels.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ModelData& model = modelIt->second;
|
||||
shader->setUniform("uModel", instance.modelMatrix);
|
||||
|
||||
// Render all groups
|
||||
for (const auto& group : model.groups) {
|
||||
// Frustum culling
|
||||
if (frustumCulling && !isGroupVisible(group, instance.modelMatrix, camera)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
renderGroup(group, model, instance.modelMatrix, view, projection);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore polygon mode
|
||||
if (wireframeMode) {
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
}
|
||||
|
||||
// Re-enable backface culling
|
||||
glEnable(GL_CULL_FACE);
|
||||
}
|
||||
|
||||
uint32_t WMORenderer::getTotalTriangleCount() const {
|
||||
uint32_t total = 0;
|
||||
for (const auto& instance : instances) {
|
||||
auto modelIt = loadedModels.find(instance.modelId);
|
||||
if (modelIt != loadedModels.end()) {
|
||||
total += modelIt->second.getTotalTriangles();
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupResources& resources) {
|
||||
if (group.vertices.empty() || group.indices.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
resources.vertexCount = group.vertices.size();
|
||||
resources.indexCount = group.indices.size();
|
||||
resources.boundingBoxMin = group.boundingBoxMin;
|
||||
resources.boundingBoxMax = group.boundingBoxMax;
|
||||
|
||||
// Create vertex data (position, normal, texcoord, color)
|
||||
struct VertexData {
|
||||
glm::vec3 position;
|
||||
glm::vec3 normal;
|
||||
glm::vec2 texCoord;
|
||||
glm::vec4 color;
|
||||
};
|
||||
|
||||
std::vector<VertexData> vertices;
|
||||
vertices.reserve(group.vertices.size());
|
||||
|
||||
for (const auto& v : group.vertices) {
|
||||
VertexData vd;
|
||||
vd.position = v.position;
|
||||
vd.normal = v.normal;
|
||||
vd.texCoord = v.texCoord;
|
||||
vd.color = v.color;
|
||||
vertices.push_back(vd);
|
||||
}
|
||||
|
||||
// Create VAO/VBO/EBO
|
||||
glGenVertexArrays(1, &resources.vao);
|
||||
glGenBuffers(1, &resources.vbo);
|
||||
glGenBuffers(1, &resources.ebo);
|
||||
|
||||
glBindVertexArray(resources.vao);
|
||||
|
||||
// Upload vertex data
|
||||
glBindBuffer(GL_ARRAY_BUFFER, resources.vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(VertexData),
|
||||
vertices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// Upload index data
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, resources.ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, group.indices.size() * sizeof(uint16_t),
|
||||
group.indices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// Vertex attributes
|
||||
// Position
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData),
|
||||
(void*)offsetof(VertexData, position));
|
||||
|
||||
// Normal
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData),
|
||||
(void*)offsetof(VertexData, normal));
|
||||
|
||||
// TexCoord
|
||||
glEnableVertexAttribArray(2);
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(VertexData),
|
||||
(void*)offsetof(VertexData, texCoord));
|
||||
|
||||
// Color
|
||||
glEnableVertexAttribArray(3);
|
||||
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(VertexData),
|
||||
(void*)offsetof(VertexData, color));
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Store collision geometry for floor raycasting
|
||||
resources.collisionVertices.reserve(group.vertices.size());
|
||||
for (const auto& v : group.vertices) {
|
||||
resources.collisionVertices.push_back(v.position);
|
||||
}
|
||||
resources.collisionIndices = group.indices;
|
||||
|
||||
// Compute actual bounding box from vertices (WMO header bboxes can be unreliable)
|
||||
if (!resources.collisionVertices.empty()) {
|
||||
resources.boundingBoxMin = resources.collisionVertices[0];
|
||||
resources.boundingBoxMax = resources.collisionVertices[0];
|
||||
for (const auto& v : resources.collisionVertices) {
|
||||
resources.boundingBoxMin = glm::min(resources.boundingBoxMin, v);
|
||||
resources.boundingBoxMax = glm::max(resources.boundingBoxMax, v);
|
||||
}
|
||||
}
|
||||
|
||||
// Create batches
|
||||
if (!group.batches.empty()) {
|
||||
for (const auto& batch : group.batches) {
|
||||
GroupResources::Batch resBatch;
|
||||
resBatch.startIndex = batch.startIndex;
|
||||
resBatch.indexCount = batch.indexCount;
|
||||
resBatch.materialId = batch.materialId;
|
||||
resources.batches.push_back(resBatch);
|
||||
}
|
||||
} else {
|
||||
// No batches defined - render entire group as one batch
|
||||
GroupResources::Batch batch;
|
||||
batch.startIndex = 0;
|
||||
batch.indexCount = resources.indexCount;
|
||||
batch.materialId = 0;
|
||||
resources.batches.push_back(batch);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WMORenderer::renderGroup(const GroupResources& group, const ModelData& model,
|
||||
[[maybe_unused]] const glm::mat4& modelMatrix,
|
||||
[[maybe_unused]] const glm::mat4& view,
|
||||
[[maybe_unused]] const glm::mat4& projection) {
|
||||
glBindVertexArray(group.vao);
|
||||
|
||||
static int debugLogCount = 0;
|
||||
|
||||
// Render each batch
|
||||
for (const auto& batch : group.batches) {
|
||||
// Bind texture for this batch's material
|
||||
// materialId -> materialTextureIndices[materialId] -> textures[texIndex]
|
||||
GLuint texId = whiteTexture;
|
||||
bool hasTexture = false;
|
||||
|
||||
if (batch.materialId < model.materialTextureIndices.size()) {
|
||||
uint32_t texIndex = model.materialTextureIndices[batch.materialId];
|
||||
if (texIndex < model.textures.size()) {
|
||||
texId = model.textures[texIndex];
|
||||
hasTexture = (texId != 0 && texId != whiteTexture);
|
||||
|
||||
if (debugLogCount < 10) {
|
||||
core::Logger::getInstance().debug(" Batch: materialId=", (int)batch.materialId,
|
||||
" -> texIndex=", texIndex, " -> texId=", texId, " hasTexture=", hasTexture);
|
||||
debugLogCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if this material uses alpha-test cutout (blendMode 1)
|
||||
bool alphaTest = false;
|
||||
if (batch.materialId < model.materialBlendModes.size()) {
|
||||
alphaTest = (model.materialBlendModes[batch.materialId] == 1);
|
||||
}
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texId);
|
||||
shader->setUniform("uTexture", 0);
|
||||
shader->setUniform("uHasTexture", hasTexture);
|
||||
shader->setUniform("uAlphaTest", alphaTest);
|
||||
|
||||
glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT,
|
||||
(void*)(batch.startIndex * sizeof(uint16_t)));
|
||||
lastDrawCalls++;
|
||||
}
|
||||
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
bool WMORenderer::isGroupVisible(const GroupResources& group, const glm::mat4& modelMatrix,
|
||||
const Camera& camera) const {
|
||||
// Simple frustum culling using bounding box
|
||||
// Transform bounding box corners to world space
|
||||
glm::vec3 corners[8] = {
|
||||
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMin.y, group.boundingBoxMin.z),
|
||||
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMin.y, group.boundingBoxMin.z),
|
||||
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMax.y, group.boundingBoxMin.z),
|
||||
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMax.y, group.boundingBoxMin.z),
|
||||
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMin.y, group.boundingBoxMax.z),
|
||||
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMin.y, group.boundingBoxMax.z),
|
||||
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMax.y, group.boundingBoxMax.z),
|
||||
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMax.y, group.boundingBoxMax.z)
|
||||
};
|
||||
|
||||
// Transform corners to world space
|
||||
for (int i = 0; i < 8; i++) {
|
||||
glm::vec4 worldPos = modelMatrix * glm::vec4(corners[i], 1.0f);
|
||||
corners[i] = glm::vec3(worldPos);
|
||||
}
|
||||
|
||||
// Simple check: if all corners are behind camera, cull
|
||||
// (This is a very basic culling implementation - a full frustum test would be better)
|
||||
glm::vec3 forward = camera.getForward();
|
||||
glm::vec3 camPos = camera.getPosition();
|
||||
|
||||
int behindCount = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
glm::vec3 toCorner = corners[i] - camPos;
|
||||
if (glm::dot(toCorner, forward) < 0.0f) {
|
||||
behindCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If all corners are behind camera, cull
|
||||
return behindCount < 8;
|
||||
}
|
||||
|
||||
void WMORenderer::WMOInstance::updateModelMatrix() {
|
||||
modelMatrix = glm::mat4(1.0f);
|
||||
modelMatrix = glm::translate(modelMatrix, position);
|
||||
|
||||
// Apply MODF placement rotation (WoW-to-GL coordinate transform)
|
||||
// WoW Ry(B)*Rx(A)*Rz(C) becomes GL Rz(B)*Ry(-A)*Rx(-C)
|
||||
// rotation stored as (-C, -A, B) in radians by caller
|
||||
// Apply in Z, Y, X order to get Rz(B) * Ry(-A) * Rx(-C)
|
||||
modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
|
||||
modelMatrix = glm::scale(modelMatrix, glm::vec3(scale));
|
||||
}
|
||||
|
||||
GLuint WMORenderer::loadTexture(const std::string& path) {
|
||||
if (!assetManager) {
|
||||
return whiteTexture;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
auto it = textureCache.find(path);
|
||||
if (it != textureCache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Load BLP texture
|
||||
pipeline::BLPImage blp = assetManager->loadTexture(path);
|
||||
if (!blp.isValid()) {
|
||||
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
|
||||
textureCache[path] = whiteTexture;
|
||||
return whiteTexture;
|
||||
}
|
||||
|
||||
core::Logger::getInstance().debug("WMO texture: ", path, " size=", blp.width, "x", blp.height);
|
||||
|
||||
// Create OpenGL texture
|
||||
GLuint textureID;
|
||||
glGenTextures(1, &textureID);
|
||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||
|
||||
// Upload texture data (BLP loader outputs RGBA8)
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
|
||||
blp.width, blp.height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data());
|
||||
|
||||
// Set texture parameters with mipmaps
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
// Cache it
|
||||
textureCache[path] = textureID;
|
||||
core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
|
||||
|
||||
return textureID;
|
||||
}
|
||||
|
||||
// Ray-AABB intersection (slab method)
|
||||
// Returns true if the ray intersects the axis-aligned bounding box
|
||||
static bool rayIntersectsAABB(const glm::vec3& origin, const glm::vec3& dir,
|
||||
const glm::vec3& bmin, const glm::vec3& bmax) {
|
||||
float tmin = -1e30f, tmax = 1e30f;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (std::abs(dir[i]) < 1e-8f) {
|
||||
// Ray is parallel to this slab — check if origin is inside
|
||||
if (origin[i] < bmin[i] || origin[i] > bmax[i]) return false;
|
||||
} else {
|
||||
float invD = 1.0f / dir[i];
|
||||
float t0 = (bmin[i] - origin[i]) * invD;
|
||||
float t1 = (bmax[i] - origin[i]) * invD;
|
||||
if (t0 > t1) std::swap(t0, t1);
|
||||
tmin = std::max(tmin, t0);
|
||||
tmax = std::min(tmax, t1);
|
||||
if (tmin > tmax) return false;
|
||||
}
|
||||
}
|
||||
return tmax >= 0.0f; // At least part of the ray is forward
|
||||
}
|
||||
|
||||
// Möller–Trumbore ray-triangle intersection
|
||||
// Returns distance along ray if hit, or negative if miss
|
||||
static float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
|
||||
const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) {
|
||||
const float EPSILON = 1e-6f;
|
||||
glm::vec3 e1 = v1 - v0;
|
||||
glm::vec3 e2 = v2 - v0;
|
||||
glm::vec3 h = glm::cross(dir, e2);
|
||||
float a = glm::dot(e1, h);
|
||||
if (a > -EPSILON && a < EPSILON) return -1.0f;
|
||||
|
||||
float f = 1.0f / a;
|
||||
glm::vec3 s = origin - v0;
|
||||
float u = f * glm::dot(s, h);
|
||||
if (u < 0.0f || u > 1.0f) return -1.0f;
|
||||
|
||||
glm::vec3 q = glm::cross(s, e1);
|
||||
float v = f * glm::dot(dir, q);
|
||||
if (v < 0.0f || u + v > 1.0f) return -1.0f;
|
||||
|
||||
float t = f * glm::dot(e2, q);
|
||||
return t > EPSILON ? t : -1.0f;
|
||||
}
|
||||
|
||||
std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ) const {
|
||||
std::optional<float> bestFloor;
|
||||
|
||||
// World-space ray: from high above, pointing straight down
|
||||
glm::vec3 worldOrigin(glX, glY, glZ + 500.0f);
|
||||
glm::vec3 worldDir(0.0f, 0.0f, -1.0f);
|
||||
|
||||
for (const auto& instance : instances) {
|
||||
auto it = loadedModels.find(instance.modelId);
|
||||
if (it == loadedModels.end()) continue;
|
||||
|
||||
const ModelData& model = it->second;
|
||||
|
||||
// Transform ray into model-local space
|
||||
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
|
||||
glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(worldOrigin, 1.0f));
|
||||
glm::vec3 localDir = glm::normalize(glm::vec3(invModel * glm::vec4(worldDir, 0.0f)));
|
||||
|
||||
for (const auto& group : model.groups) {
|
||||
// Quick bounding box check: does the ray intersect this group's AABB?
|
||||
// Use proper ray-AABB intersection (slab method) which handles rotated rays
|
||||
if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Raycast against triangles
|
||||
const auto& verts = group.collisionVertices;
|
||||
const auto& indices = group.collisionIndices;
|
||||
|
||||
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
|
||||
const glm::vec3& v0 = verts[indices[i]];
|
||||
const glm::vec3& v1 = verts[indices[i + 1]];
|
||||
const glm::vec3& v2 = verts[indices[i + 2]];
|
||||
|
||||
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
|
||||
if (t > 0.0f) {
|
||||
// Hit point in local space -> world space
|
||||
glm::vec3 hitLocal = localOrigin + localDir * t;
|
||||
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
||||
|
||||
// Only use floors below or near the query point
|
||||
if (hitWorld.z <= glZ + 2.0f) {
|
||||
if (!bestFloor || hitWorld.z > *bestFloor) {
|
||||
bestFloor = hitWorld.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (debug logging removed)
|
||||
}
|
||||
|
||||
return bestFloor;
|
||||
}
|
||||
|
||||
bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3& adjustedPos) const {
|
||||
adjustedPos = to;
|
||||
bool blocked = false;
|
||||
|
||||
glm::vec3 moveDir = to - from;
|
||||
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;
|
||||
|
||||
for (const auto& instance : instances) {
|
||||
auto it = loadedModels.find(instance.modelId);
|
||||
if (it == loadedModels.end()) continue;
|
||||
|
||||
const ModelData& model = it->second;
|
||||
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
|
||||
|
||||
// Transform positions into local space
|
||||
glm::vec3 localTo = glm::vec3(invModel * glm::vec4(to, 1.0f));
|
||||
|
||||
for (const auto& group : model.groups) {
|
||||
// Quick bounding box check
|
||||
float margin = PLAYER_RADIUS + 5.0f;
|
||||
if (localTo.x < group.boundingBoxMin.x - margin || localTo.x > group.boundingBoxMax.x + margin ||
|
||||
localTo.y < group.boundingBoxMin.y - margin || localTo.y > group.boundingBoxMax.y + margin ||
|
||||
localTo.z < group.boundingBoxMin.z - margin || localTo.z > group.boundingBoxMax.z + margin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& verts = group.collisionVertices;
|
||||
const auto& indices = group.collisionIndices;
|
||||
|
||||
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
|
||||
const glm::vec3& v0 = verts[indices[i]];
|
||||
const glm::vec3& v1 = verts[indices[i + 1]];
|
||||
const glm::vec3& v2 = verts[indices[i + 2]];
|
||||
|
||||
// Get triangle normal
|
||||
glm::vec3 edge1 = v1 - v0;
|
||||
glm::vec3 edge2 = v2 - v0;
|
||||
glm::vec3 normal = glm::cross(edge1, edge2);
|
||||
float normalLen = glm::length(normal);
|
||||
if (normalLen < 0.001f) continue;
|
||||
normal /= normalLen;
|
||||
|
||||
// Skip mostly-horizontal triangles (floors/ceilings)
|
||||
if (std::abs(normal.z) > 0.7f) continue;
|
||||
|
||||
// Signed distance from player to triangle plane
|
||||
float planeDist = glm::dot(localTo - v0, normal);
|
||||
float absPlaneDist = std::abs(planeDist);
|
||||
if (absPlaneDist > PLAYER_RADIUS) continue;
|
||||
|
||||
// Project point onto plane
|
||||
glm::vec3 projected = localTo - normal * planeDist;
|
||||
|
||||
// Check if projected point is inside triangle using same-side test
|
||||
// Use edge cross products and check they all point same direction as normal
|
||||
float d0 = glm::dot(glm::cross(v1 - v0, projected - v0), normal);
|
||||
float d1 = glm::dot(glm::cross(v2 - v1, projected - v1), normal);
|
||||
float d2 = glm::dot(glm::cross(v0 - v2, projected - v2), normal);
|
||||
|
||||
// Also check nearby: if projected point is close to a triangle edge
|
||||
bool insideTriangle = (d0 >= 0.0f && d1 >= 0.0f && d2 >= 0.0f);
|
||||
|
||||
if (insideTriangle) {
|
||||
// Push player away from wall
|
||||
float pushDist = PLAYER_RADIUS - absPlaneDist;
|
||||
if (pushDist > 0.0f) {
|
||||
// Push in the direction the player is on (sign of planeDist)
|
||||
float sign = planeDist > 0.0f ? 1.0f : -1.0f;
|
||||
glm::vec3 pushLocal = normal * sign * pushDist;
|
||||
|
||||
// Transform push vector back to world space (direction, not point)
|
||||
glm::vec3 pushWorld = glm::vec3(instance.modelMatrix * glm::vec4(pushLocal, 0.0f));
|
||||
|
||||
// Only apply horizontal push (don't push vertically)
|
||||
adjustedPos.x += pushWorld.x;
|
||||
adjustedPos.y += pushWorld.y;
|
||||
blocked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blocked;
|
||||
}
|
||||
|
||||
bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outModelId) const {
|
||||
for (const auto& instance : instances) {
|
||||
auto it = loadedModels.find(instance.modelId);
|
||||
if (it == loadedModels.end()) continue;
|
||||
|
||||
const ModelData& model = it->second;
|
||||
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
|
||||
glm::vec3 localPos = glm::vec3(invModel * glm::vec4(glX, glY, glZ, 1.0f));
|
||||
|
||||
// Check if inside any group's bounding box
|
||||
for (const auto& group : model.groups) {
|
||||
if (localPos.x >= group.boundingBoxMin.x && localPos.x <= group.boundingBoxMax.x &&
|
||||
localPos.y >= group.boundingBoxMin.y && localPos.y <= group.boundingBoxMax.y &&
|
||||
localPos.z >= group.boundingBoxMin.z && localPos.z <= group.boundingBoxMax.z) {
|
||||
if (outModelId) *outModelId = instance.modelId;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue