mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-24 00:00:13 +00:00
Shadow toggle allows disabling shadow pass for performance testing. Distance culling skips WMO groups beyond 200 units. Occlusion queries disabled by default as overhead outweighs benefits in dense scenes.
1919 lines
69 KiB
C++
1919 lines
69 KiB
C++
#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 "rendering/shader.hpp"
|
|
#include "pipeline/m2_loader.hpp"
|
|
#include <algorithm>
|
|
#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 "audio/footstep_manager.hpp"
|
|
#include "audio/activity_sound_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 <cmath>
|
|
#include <chrono>
|
|
#include <optional>
|
|
#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->setUseWoWSpeed(true); // Use realistic WoW movement speed
|
|
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>();
|
|
footstepManager = std::make_unique<audio::FootstepManager>();
|
|
activitySoundManager = std::make_unique<audio::ActivitySoundManager>();
|
|
|
|
// Underwater full-screen tint overlay (applies to all world geometry).
|
|
underwaterOverlayShader = std::make_unique<Shader>();
|
|
const char* overlayVS = R"(
|
|
#version 330 core
|
|
layout (location = 0) in vec2 aPos;
|
|
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
|
)";
|
|
const char* overlayFS = R"(
|
|
#version 330 core
|
|
uniform vec4 uTint;
|
|
out vec4 FragColor;
|
|
void main() { FragColor = uTint; }
|
|
)";
|
|
if (!underwaterOverlayShader->loadFromSource(overlayVS, overlayFS)) {
|
|
LOG_WARNING("Failed to initialize underwater overlay shader");
|
|
underwaterOverlayShader.reset();
|
|
} else {
|
|
const float quadVerts[] = {
|
|
-1.0f, -1.0f, 1.0f, -1.0f,
|
|
-1.0f, 1.0f, 1.0f, 1.0f
|
|
};
|
|
glGenVertexArrays(1, &underwaterOverlayVAO);
|
|
glGenBuffers(1, &underwaterOverlayVBO);
|
|
glBindVertexArray(underwaterOverlayVAO);
|
|
glBindBuffer(GL_ARRAY_BUFFER, underwaterOverlayVBO);
|
|
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW);
|
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
|
|
glEnableVertexAttribArray(0);
|
|
glBindVertexArray(0);
|
|
}
|
|
|
|
// Initialize post-process FBO pipeline
|
|
initPostProcess(window->getWidth(), window->getHeight());
|
|
|
|
// Initialize shadow map
|
|
initShadowMap();
|
|
|
|
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();
|
|
}
|
|
if (footstepManager) {
|
|
footstepManager->shutdown();
|
|
footstepManager.reset();
|
|
}
|
|
if (activitySoundManager) {
|
|
activitySoundManager->shutdown();
|
|
activitySoundManager.reset();
|
|
}
|
|
if (underwaterOverlayVAO) {
|
|
glDeleteVertexArrays(1, &underwaterOverlayVAO);
|
|
underwaterOverlayVAO = 0;
|
|
}
|
|
if (underwaterOverlayVBO) {
|
|
glDeleteBuffers(1, &underwaterOverlayVBO);
|
|
underwaterOverlayVBO = 0;
|
|
}
|
|
underwaterOverlayShader.reset();
|
|
|
|
// Cleanup shadow map resources
|
|
if (shadowFBO) { glDeleteFramebuffers(1, &shadowFBO); shadowFBO = 0; }
|
|
if (shadowDepthTex) { glDeleteTextures(1, &shadowDepthTex); shadowDepthTex = 0; }
|
|
if (shadowShaderProgram) { glDeleteProgram(shadowShaderProgram); shadowShaderProgram = 0; }
|
|
|
|
shutdownPostProcess();
|
|
|
|
zoneManager.reset();
|
|
|
|
performanceHUD.reset();
|
|
scene.reset();
|
|
cameraController.reset();
|
|
camera.reset();
|
|
|
|
LOG_INFO("Renderer shutdown");
|
|
}
|
|
|
|
void Renderer::beginFrame() {
|
|
// Resize post-process FBO if window size changed
|
|
int w = window->getWidth();
|
|
int h = window->getHeight();
|
|
if (w != fbWidth || h != fbHeight) {
|
|
resizePostProcess(w, h);
|
|
}
|
|
|
|
// Clear default framebuffer (login screen renders here directly)
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
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);
|
|
}
|
|
}
|
|
|
|
uint32_t Renderer::resolveMeleeAnimId() {
|
|
if (!characterRenderer || characterInstanceId == 0) {
|
|
meleeAnimId = 0;
|
|
meleeAnimDurationMs = 0.0f;
|
|
return 0;
|
|
}
|
|
|
|
if (meleeAnimId != 0 && characterRenderer->hasAnimation(characterInstanceId, meleeAnimId)) {
|
|
return meleeAnimId;
|
|
}
|
|
|
|
std::vector<pipeline::M2Sequence> sequences;
|
|
if (!characterRenderer->getAnimationSequences(characterInstanceId, sequences)) {
|
|
meleeAnimId = 0;
|
|
meleeAnimDurationMs = 0.0f;
|
|
return 0;
|
|
}
|
|
|
|
auto findDuration = [&](uint32_t id) -> float {
|
|
for (const auto& seq : sequences) {
|
|
if (seq.id == id && seq.duration > 0) {
|
|
return static_cast<float>(seq.duration);
|
|
}
|
|
}
|
|
return 0.0f;
|
|
};
|
|
|
|
const uint32_t attackCandidates[] = {16, 17, 18, 19, 20, 21};
|
|
for (uint32_t id : attackCandidates) {
|
|
if (characterRenderer->hasAnimation(characterInstanceId, id)) {
|
|
meleeAnimId = id;
|
|
meleeAnimDurationMs = findDuration(id);
|
|
return meleeAnimId;
|
|
}
|
|
}
|
|
|
|
const uint32_t avoidIds[] = {0, 1, 4, 5, 11, 12, 13, 37, 38, 39, 41, 42, 97};
|
|
auto isAvoid = [&](uint32_t id) -> bool {
|
|
for (uint32_t avoid : avoidIds) {
|
|
if (id == avoid) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
uint32_t bestId = 0;
|
|
uint32_t bestDuration = 0;
|
|
for (const auto& seq : sequences) {
|
|
if (seq.duration == 0) continue;
|
|
if (isAvoid(seq.id)) continue;
|
|
if (seq.movingSpeed > 0.1f) continue;
|
|
if (seq.duration < 150 || seq.duration > 2000) continue;
|
|
if (bestId == 0 || seq.duration < bestDuration) {
|
|
bestId = seq.id;
|
|
bestDuration = seq.duration;
|
|
}
|
|
}
|
|
|
|
if (bestId == 0) {
|
|
for (const auto& seq : sequences) {
|
|
if (seq.duration == 0) continue;
|
|
if (isAvoid(seq.id)) continue;
|
|
if (bestId == 0 || seq.duration < bestDuration) {
|
|
bestId = seq.id;
|
|
bestDuration = seq.duration;
|
|
}
|
|
}
|
|
}
|
|
|
|
meleeAnimId = bestId;
|
|
meleeAnimDurationMs = static_cast<float>(bestDuration);
|
|
return meleeAnimId;
|
|
}
|
|
|
|
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;
|
|
// Candidate locomotion clips by common WotLK IDs.
|
|
constexpr uint32_t ANIM_STRAFE_RUN_RIGHT = 92;
|
|
constexpr uint32_t ANIM_STRAFE_RUN_LEFT = 93;
|
|
constexpr uint32_t ANIM_STRAFE_WALK_LEFT = 11;
|
|
constexpr uint32_t ANIM_STRAFE_WALK_RIGHT = 12;
|
|
constexpr uint32_t ANIM_BACKPEDAL = 13;
|
|
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 movingBackward = cameraController->isMovingBackward();
|
|
bool strafeLeft = cameraController->isStrafingLeft();
|
|
bool strafeRight = cameraController->isStrafingRight();
|
|
bool anyStrafeLeft = strafeLeft && !strafeRight;
|
|
bool anyStrafeRight = strafeRight && !strafeLeft;
|
|
bool grounded = cameraController->isGrounded();
|
|
bool jumping = cameraController->isJumping();
|
|
bool sprinting = cameraController->isSprinting();
|
|
bool sitting = cameraController->isSitting();
|
|
bool swim = cameraController->isSwimming();
|
|
bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim;
|
|
|
|
if (!forceMelee) 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;
|
|
|
|
case CharAnimState::MELEE_SWING:
|
|
if (swim) {
|
|
newState = CharAnimState::SWIM_IDLE;
|
|
} 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;
|
|
} else if (sitting) {
|
|
newState = CharAnimState::SIT_DOWN;
|
|
} else {
|
|
newState = CharAnimState::IDLE;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (forceMelee) {
|
|
newState = CharAnimState::MELEE_SWING;
|
|
}
|
|
|
|
if (newState != charAnimState) {
|
|
charAnimState = newState;
|
|
}
|
|
|
|
auto pickFirstAvailable = [&](std::initializer_list<uint32_t> candidates, uint32_t fallback) -> uint32_t {
|
|
for (uint32_t id : candidates) {
|
|
if (characterRenderer->hasAnimation(characterInstanceId, id)) {
|
|
return id;
|
|
}
|
|
}
|
|
return fallback;
|
|
};
|
|
|
|
uint32_t animId = ANIM_STAND;
|
|
bool loop = true;
|
|
|
|
switch (charAnimState) {
|
|
case CharAnimState::IDLE: animId = ANIM_STAND; loop = true; break;
|
|
case CharAnimState::WALK:
|
|
if (movingBackward) {
|
|
animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK);
|
|
} else if (anyStrafeLeft) {
|
|
animId = pickFirstAvailable({ANIM_STRAFE_WALK_LEFT, ANIM_STRAFE_RUN_LEFT}, ANIM_WALK);
|
|
} else if (anyStrafeRight) {
|
|
animId = pickFirstAvailable({ANIM_STRAFE_WALK_RIGHT, ANIM_STRAFE_RUN_RIGHT}, ANIM_WALK);
|
|
} else {
|
|
animId = ANIM_WALK;
|
|
}
|
|
loop = true;
|
|
break;
|
|
case CharAnimState::RUN:
|
|
if (movingBackward) {
|
|
animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK);
|
|
} else if (anyStrafeLeft) {
|
|
animId = pickFirstAvailable({ANIM_STRAFE_RUN_LEFT}, ANIM_RUN);
|
|
} else if (anyStrafeRight) {
|
|
animId = pickFirstAvailable({ANIM_STRAFE_RUN_RIGHT}, ANIM_RUN);
|
|
} else {
|
|
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;
|
|
case CharAnimState::MELEE_SWING:
|
|
animId = resolveMeleeAnimId();
|
|
if (animId == 0) {
|
|
animId = ANIM_STAND;
|
|
}
|
|
loop = false;
|
|
break;
|
|
}
|
|
|
|
uint32_t currentAnimId = 0;
|
|
float currentAnimTimeMs = 0.0f;
|
|
float currentAnimDurationMs = 0.0f;
|
|
bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs);
|
|
if (!haveState || currentAnimId != animId) {
|
|
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;
|
|
}
|
|
|
|
void Renderer::triggerMeleeSwing() {
|
|
if (!characterRenderer || characterInstanceId == 0) return;
|
|
if (meleeSwingCooldown > 0.0f) return;
|
|
if (emoteActive) {
|
|
cancelEmote();
|
|
}
|
|
resolveMeleeAnimId();
|
|
meleeSwingCooldown = 0.1f;
|
|
float durationSec = meleeAnimDurationMs > 0.0f ? meleeAnimDurationMs / 1000.0f : 0.6f;
|
|
if (durationSec < 0.25f) durationSec = 0.25f;
|
|
if (durationSec > 1.0f) durationSec = 1.0f;
|
|
meleeSwingTimer = durationSec;
|
|
if (activitySoundManager) {
|
|
activitySoundManager->playMeleeSwing();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
bool Renderer::isFootstepAnimationState() const {
|
|
return charAnimState == CharAnimState::WALK || charAnimState == CharAnimState::RUN;
|
|
}
|
|
|
|
bool Renderer::shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs) {
|
|
if (animationDurationMs <= 1.0f) {
|
|
footstepNormInitialized = false;
|
|
return false;
|
|
}
|
|
|
|
float norm = std::fmod(animationTimeMs, animationDurationMs) / animationDurationMs;
|
|
if (norm < 0.0f) norm += 1.0f;
|
|
|
|
if (animationId != footstepLastAnimationId) {
|
|
footstepLastAnimationId = animationId;
|
|
footstepLastNormTime = norm;
|
|
footstepNormInitialized = true;
|
|
return false;
|
|
}
|
|
|
|
if (!footstepNormInitialized) {
|
|
footstepNormInitialized = true;
|
|
footstepLastNormTime = norm;
|
|
return false;
|
|
}
|
|
|
|
auto crossed = [&](float eventNorm) {
|
|
if (footstepLastNormTime <= norm) {
|
|
return footstepLastNormTime < eventNorm && eventNorm <= norm;
|
|
}
|
|
return footstepLastNormTime < eventNorm || eventNorm <= norm;
|
|
};
|
|
|
|
bool trigger = crossed(0.22f) || crossed(0.72f);
|
|
footstepLastNormTime = norm;
|
|
return trigger;
|
|
}
|
|
|
|
audio::FootstepSurface Renderer::resolveFootstepSurface() const {
|
|
if (!cameraController || !cameraController->isThirdPerson()) {
|
|
return audio::FootstepSurface::STONE;
|
|
}
|
|
|
|
const glm::vec3& p = characterPosition;
|
|
|
|
if (cameraController->isSwimming()) {
|
|
return audio::FootstepSurface::WATER;
|
|
}
|
|
|
|
if (waterRenderer) {
|
|
auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y);
|
|
if (waterH && p.z < (*waterH + 0.25f)) {
|
|
return audio::FootstepSurface::WATER;
|
|
}
|
|
}
|
|
|
|
if (wmoRenderer) {
|
|
auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f);
|
|
auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt;
|
|
if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) {
|
|
return audio::FootstepSurface::STONE;
|
|
}
|
|
}
|
|
|
|
if (terrainManager) {
|
|
auto texture = terrainManager->getDominantTextureAt(p.x, p.y);
|
|
if (texture) {
|
|
std::string t = *texture;
|
|
for (char& c : t) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) return audio::FootstepSurface::SNOW;
|
|
if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) return audio::FootstepSurface::GRASS;
|
|
if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) return audio::FootstepSurface::DIRT;
|
|
if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) return audio::FootstepSurface::WOOD;
|
|
if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) return audio::FootstepSurface::METAL;
|
|
if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) return audio::FootstepSurface::STONE;
|
|
}
|
|
}
|
|
|
|
return audio::FootstepSurface::STONE;
|
|
}
|
|
|
|
void Renderer::update(float deltaTime) {
|
|
auto updateStart = std::chrono::steady_clock::now();
|
|
if (wmoRenderer) wmoRenderer->resetQueryStats();
|
|
if (m2Renderer) m2Renderer->resetQueryStats();
|
|
|
|
if (cameraController) {
|
|
auto cameraStart = std::chrono::steady_clock::now();
|
|
cameraController->update(deltaTime);
|
|
auto cameraEnd = std::chrono::steady_clock::now();
|
|
lastCameraUpdateMs = std::chrono::duration<double, std::milli>(cameraEnd - cameraStart).count();
|
|
} else {
|
|
lastCameraUpdateMs = 0.0;
|
|
}
|
|
|
|
// Sync character model position/rotation and animation with follow target
|
|
if (characterInstanceId > 0 && characterRenderer && cameraController && cameraController->isThirdPerson()) {
|
|
if (meleeSwingCooldown > 0.0f) {
|
|
meleeSwingCooldown = std::max(0.0f, meleeSwingCooldown - deltaTime);
|
|
}
|
|
if (meleeSwingTimer > 0.0f) {
|
|
meleeSwingTimer = std::max(0.0f, meleeSwingTimer - deltaTime);
|
|
}
|
|
|
|
characterRenderer->setInstancePosition(characterInstanceId, characterPosition);
|
|
if (activitySoundManager) {
|
|
std::string modelName;
|
|
if (characterRenderer->getInstanceModelName(characterInstanceId, modelName)) {
|
|
activitySoundManager->setCharacterVoiceProfile(modelName);
|
|
}
|
|
}
|
|
|
|
// Movement-facing comes from camera controller and is decoupled from LMB orbit.
|
|
if (cameraController->isMoving() || cameraController->isRightMouseHeld()) {
|
|
characterYaw = cameraController->getFacingYaw();
|
|
} 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);
|
|
}
|
|
|
|
// Footsteps: animation-event driven + surface query at event time.
|
|
if (footstepManager) {
|
|
footstepManager->update(deltaTime);
|
|
if (characterRenderer && characterInstanceId > 0 &&
|
|
cameraController && cameraController->isThirdPerson() &&
|
|
isFootstepAnimationState() && cameraController->isGrounded() &&
|
|
!cameraController->isSwimming()) {
|
|
uint32_t animId = 0;
|
|
float animTimeMs = 0.0f;
|
|
float animDurationMs = 0.0f;
|
|
if (characterRenderer->getAnimationState(characterInstanceId, animId, animTimeMs, animDurationMs) &&
|
|
shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) {
|
|
footstepManager->playFootstep(resolveFootstepSurface(), cameraController->isSprinting());
|
|
}
|
|
} else {
|
|
footstepNormInitialized = false;
|
|
}
|
|
}
|
|
|
|
// Activity SFX: animation/state-driven jump, landing, and swim loops/splashes.
|
|
if (activitySoundManager) {
|
|
activitySoundManager->update(deltaTime);
|
|
if (cameraController && cameraController->isThirdPerson()) {
|
|
bool grounded = cameraController->isGrounded();
|
|
bool jumping = cameraController->isJumping();
|
|
bool falling = cameraController->isFalling();
|
|
bool swimming = cameraController->isSwimming();
|
|
bool moving = cameraController->isMoving();
|
|
|
|
if (!sfxStateInitialized) {
|
|
sfxPrevGrounded = grounded;
|
|
sfxPrevJumping = jumping;
|
|
sfxPrevFalling = falling;
|
|
sfxPrevSwimming = swimming;
|
|
sfxStateInitialized = true;
|
|
}
|
|
|
|
if (jumping && !sfxPrevJumping && !swimming) {
|
|
activitySoundManager->playJump();
|
|
}
|
|
|
|
if (grounded && !sfxPrevGrounded) {
|
|
bool hardLanding = sfxPrevFalling;
|
|
activitySoundManager->playLanding(resolveFootstepSurface(), hardLanding);
|
|
}
|
|
|
|
if (swimming && !sfxPrevSwimming) {
|
|
activitySoundManager->playWaterEnter();
|
|
} else if (!swimming && sfxPrevSwimming) {
|
|
activitySoundManager->playWaterExit();
|
|
}
|
|
|
|
activitySoundManager->setSwimmingState(swimming, moving);
|
|
|
|
sfxPrevGrounded = grounded;
|
|
sfxPrevJumping = jumping;
|
|
sfxPrevFalling = falling;
|
|
sfxPrevSwimming = swimming;
|
|
} else {
|
|
activitySoundManager->setSwimmingState(false, false);
|
|
sfxStateInitialized = false;
|
|
}
|
|
}
|
|
|
|
// Update M2 doodad animations
|
|
if (m2Renderer) {
|
|
m2Renderer->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);
|
|
}
|
|
|
|
auto updateEnd = std::chrono::steady_clock::now();
|
|
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();
|
|
}
|
|
|
|
void Renderer::renderWorld(game::World* world) {
|
|
auto renderStart = std::chrono::steady_clock::now();
|
|
lastTerrainRenderMs = 0.0;
|
|
lastWMORenderMs = 0.0;
|
|
lastM2RenderMs = 0.0;
|
|
|
|
// Shadow pass (before main scene)
|
|
if (shadowsEnabled && shadowFBO && shadowShaderProgram && terrainLoaded) {
|
|
renderShadowPass();
|
|
} else {
|
|
// Clear shadow maps when disabled
|
|
if (terrainRenderer) terrainRenderer->clearShadowMap();
|
|
if (wmoRenderer) wmoRenderer->clearShadowMap();
|
|
if (m2Renderer) m2Renderer->clearShadowMap();
|
|
if (characterRenderer) characterRenderer->clearShadowMap();
|
|
}
|
|
|
|
// Bind HDR scene framebuffer for world rendering
|
|
glBindFramebuffer(GL_FRAMEBUFFER, sceneFBO);
|
|
glViewport(0, 0, fbWidth, fbHeight);
|
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
|
|
(void)world; // Unused for now
|
|
|
|
// Get time of day for sky-related rendering
|
|
float timeOfDay = skybox ? skybox->getTimeOfDay() : 12.0f;
|
|
bool underwater = false;
|
|
bool canalUnderwater = false;
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Update fog across all renderers based on time of day (match sky color)
|
|
if (skybox) {
|
|
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
|
|
if (wmoRenderer) wmoRenderer->setFog(horizonColor, 100.0f, 600.0f);
|
|
if (m2Renderer) m2Renderer->setFog(horizonColor, 100.0f, 600.0f);
|
|
if (characterRenderer) characterRenderer->setFog(horizonColor, 100.0f, 600.0f);
|
|
}
|
|
|
|
// Render terrain if loaded and enabled
|
|
if (terrainEnabled && terrainLoaded && terrainRenderer && camera) {
|
|
// Check if camera/character is underwater for fog override
|
|
if (cameraController && cameraController->isSwimming() && waterRenderer && camera) {
|
|
glm::vec3 camPos = camera->getPosition();
|
|
auto waterH = waterRenderer->getWaterHeightAt(camPos.x, camPos.y);
|
|
constexpr float MAX_UNDERWATER_DEPTH = 12.0f;
|
|
// Require camera to be meaningfully below the surface before
|
|
// underwater fog/tint kicks in (avoids "wrong plane" near surface).
|
|
constexpr float UNDERWATER_ENTER_EPS = 1.10f;
|
|
if (waterH &&
|
|
camPos.z < (*waterH - UNDERWATER_ENTER_EPS) &&
|
|
(*waterH - camPos.z) <= MAX_UNDERWATER_DEPTH) {
|
|
underwater = true;
|
|
}
|
|
}
|
|
|
|
if (underwater) {
|
|
glm::vec3 camPos = camera->getPosition();
|
|
std::optional<uint16_t> liquidType = waterRenderer ? waterRenderer->getWaterTypeAt(camPos.x, camPos.y) : std::nullopt;
|
|
if (!liquidType && cameraController) {
|
|
const glm::vec3* followTarget = cameraController->getFollowTarget();
|
|
if (followTarget && waterRenderer) {
|
|
liquidType = waterRenderer->getWaterTypeAt(followTarget->x, followTarget->y);
|
|
}
|
|
}
|
|
canalUnderwater = liquidType && (*liquidType == 5 || *liquidType == 13 || *liquidType == 17);
|
|
}
|
|
|
|
if (skybox) {
|
|
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
|
|
float fogColorArray[3] = {horizonColor.r, horizonColor.g, horizonColor.b};
|
|
terrainRenderer->setFog(fogColorArray, 400.0f, 1200.0f);
|
|
}
|
|
|
|
auto terrainStart = std::chrono::steady_clock::now();
|
|
terrainRenderer->render(*camera);
|
|
auto terrainEnd = std::chrono::steady_clock::now();
|
|
lastTerrainRenderMs = std::chrono::duration<double, std::milli>(terrainEnd - terrainStart).count();
|
|
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Compute view/projection once for all sub-renderers
|
|
const glm::mat4& view = camera ? camera->getViewMatrix() : glm::mat4(1.0f);
|
|
const glm::mat4& projection = camera ? camera->getProjectionMatrix() : glm::mat4(1.0f);
|
|
|
|
// Render characters (after weather)
|
|
if (characterRenderer && camera) {
|
|
characterRenderer->render(*camera, view, projection);
|
|
}
|
|
|
|
// Render WMO buildings (after characters, before UI)
|
|
if (wmoRenderer && camera) {
|
|
auto wmoStart = std::chrono::steady_clock::now();
|
|
wmoRenderer->render(*camera, view, projection);
|
|
auto wmoEnd = std::chrono::steady_clock::now();
|
|
lastWMORenderMs = std::chrono::duration<double, std::milli>(wmoEnd - wmoStart).count();
|
|
}
|
|
|
|
// Render M2 doodads (trees, rocks, etc.)
|
|
if (m2Renderer && camera) {
|
|
auto m2Start = std::chrono::steady_clock::now();
|
|
m2Renderer->render(*camera, view, projection);
|
|
m2Renderer->renderSmokeParticles(*camera, view, projection);
|
|
auto m2End = std::chrono::steady_clock::now();
|
|
lastM2RenderMs = std::chrono::duration<double, std::milli>(m2End - m2Start).count();
|
|
}
|
|
|
|
// Render water after opaque terrain/WMO/M2 so transparent surfaces remain visible.
|
|
if (waterRenderer && camera) {
|
|
static float time = 0.0f;
|
|
time += 0.016f; // Approximate frame time
|
|
waterRenderer->render(*camera, time);
|
|
}
|
|
|
|
// Full-screen underwater tint so WMO/M2/characters also feel submerged.
|
|
if (false && underwater && underwaterOverlayShader && underwaterOverlayVAO) {
|
|
glDisable(GL_DEPTH_TEST);
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
underwaterOverlayShader->use();
|
|
if (canalUnderwater) {
|
|
underwaterOverlayShader->setUniform("uTint", glm::vec4(0.01f, 0.05f, 0.11f, 0.50f));
|
|
} else {
|
|
underwaterOverlayShader->setUniform("uTint", glm::vec4(0.02f, 0.08f, 0.15f, 0.30f));
|
|
}
|
|
glBindVertexArray(underwaterOverlayVAO);
|
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
glBindVertexArray(0);
|
|
glDisable(GL_BLEND);
|
|
glEnable(GL_DEPTH_TEST);
|
|
}
|
|
|
|
// --- Resolve MSAA → non-MSAA texture ---
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, sceneFBO);
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolveFBO);
|
|
glBlitFramebuffer(0, 0, fbWidth, fbHeight, 0, 0, fbWidth, fbHeight,
|
|
GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
|
|
|
// --- Post-process: tonemap via fullscreen quad ---
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glViewport(0, 0, window->getWidth(), window->getHeight());
|
|
glDisable(GL_DEPTH_TEST);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
if (postProcessShader && screenQuadVAO) {
|
|
postProcessShader->use();
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, resolveColorTex);
|
|
postProcessShader->setUniform("uScene", 0);
|
|
glBindVertexArray(screenQuadVAO);
|
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
glBindVertexArray(0);
|
|
postProcessShader->unuse();
|
|
}
|
|
|
|
// Render minimap overlay (after post-process so it's not overwritten)
|
|
if (minimap && camera && window) {
|
|
glm::vec3 minimapCenter = camera->getPosition();
|
|
if (cameraController && cameraController->isThirdPerson()) {
|
|
minimapCenter = characterPosition;
|
|
}
|
|
minimap->render(*camera, minimapCenter, window->getWidth(), window->getHeight());
|
|
}
|
|
|
|
glEnable(GL_DEPTH_TEST);
|
|
|
|
auto renderEnd = std::chrono::steady_clock::now();
|
|
lastRenderMs = std::chrono::duration<double, std::milli>(renderEnd - renderStart).count();
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────
|
|
// Post-process FBO helpers
|
|
// ──────────────────────────────────────────────────────
|
|
|
|
void Renderer::initPostProcess(int w, int h) {
|
|
fbWidth = w;
|
|
fbHeight = h;
|
|
constexpr int SAMPLES = 4;
|
|
|
|
// --- MSAA FBO (render target) ---
|
|
glGenRenderbuffers(1, &sceneColorRBO);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, sceneColorRBO);
|
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_RGBA16F, w, h);
|
|
|
|
glGenRenderbuffers(1, &sceneDepthRBO);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, sceneDepthRBO);
|
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_DEPTH_COMPONENT24, w, h);
|
|
|
|
glGenFramebuffers(1, &sceneFBO);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, sceneFBO);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, sceneColorRBO);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, sceneDepthRBO);
|
|
|
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
LOG_ERROR("MSAA scene FBO incomplete!");
|
|
}
|
|
|
|
// --- Resolve FBO (non-MSAA, for post-process sampling) ---
|
|
glGenTextures(1, &resolveColorTex);
|
|
glBindTexture(GL_TEXTURE_2D, resolveColorTex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, 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);
|
|
|
|
glGenTextures(1, &resolveDepthTex);
|
|
glBindTexture(GL_TEXTURE_2D, resolveDepthTex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, w, h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
|
|
glGenFramebuffers(1, &resolveFBO);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, resolveFBO);
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, resolveColorTex, 0);
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, resolveDepthTex, 0);
|
|
|
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
LOG_ERROR("Resolve FBO incomplete!");
|
|
}
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
|
|
// --- Fullscreen quad (triangle strip, pos + UV) ---
|
|
const 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, 0.0f, 1.0f,
|
|
1.0f, 1.0f, 1.0f, 1.0f,
|
|
};
|
|
glGenVertexArrays(1, &screenQuadVAO);
|
|
glGenBuffers(1, &screenQuadVBO);
|
|
glBindVertexArray(screenQuadVAO);
|
|
glBindBuffer(GL_ARRAY_BUFFER, screenQuadVBO);
|
|
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);
|
|
|
|
// --- Post-process shader (Reinhard tonemap + gamma 2.2) ---
|
|
const char* ppVS = R"(
|
|
#version 330 core
|
|
layout (location = 0) in vec2 aPos;
|
|
layout (location = 1) in vec2 aUV;
|
|
out vec2 vUV;
|
|
void main() {
|
|
vUV = aUV;
|
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
|
}
|
|
)";
|
|
const char* ppFS = R"(
|
|
#version 330 core
|
|
in vec2 vUV;
|
|
uniform sampler2D uScene;
|
|
out vec4 FragColor;
|
|
void main() {
|
|
vec3 color = texture(uScene, vUV).rgb;
|
|
// Shoulder tonemap: identity below 0.9, soft rolloff above
|
|
vec3 excess = max(color - 0.9, 0.0);
|
|
vec3 mapped = min(color, vec3(0.9)) + 0.1 * excess / (excess + 0.1);
|
|
FragColor = vec4(mapped, 1.0);
|
|
}
|
|
)";
|
|
postProcessShader = std::make_unique<Shader>();
|
|
if (!postProcessShader->loadFromSource(ppVS, ppFS)) {
|
|
LOG_ERROR("Failed to compile post-process shader");
|
|
postProcessShader.reset();
|
|
}
|
|
|
|
LOG_INFO("Post-process FBO initialized (", w, "x", h, ")");
|
|
}
|
|
|
|
void Renderer::resizePostProcess(int w, int h) {
|
|
if (w <= 0 || h <= 0) return;
|
|
fbWidth = w;
|
|
fbHeight = h;
|
|
constexpr int SAMPLES = 4;
|
|
|
|
// Resize MSAA renderbuffers
|
|
glBindRenderbuffer(GL_RENDERBUFFER, sceneColorRBO);
|
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_RGBA16F, w, h);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, sceneDepthRBO);
|
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_DEPTH_COMPONENT24, w, h);
|
|
|
|
// Resize resolve textures
|
|
glBindTexture(GL_TEXTURE_2D, resolveColorTex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, nullptr);
|
|
glBindTexture(GL_TEXTURE_2D, resolveDepthTex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, w, h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
|
|
|
|
LOG_INFO("Post-process FBO resized (", w, "x", h, ")");
|
|
}
|
|
|
|
void Renderer::shutdownPostProcess() {
|
|
if (sceneFBO) {
|
|
glDeleteFramebuffers(1, &sceneFBO);
|
|
sceneFBO = 0;
|
|
}
|
|
if (sceneColorRBO) {
|
|
glDeleteRenderbuffers(1, &sceneColorRBO);
|
|
sceneColorRBO = 0;
|
|
}
|
|
if (sceneDepthRBO) {
|
|
glDeleteRenderbuffers(1, &sceneDepthRBO);
|
|
sceneDepthRBO = 0;
|
|
}
|
|
if (resolveFBO) {
|
|
glDeleteFramebuffers(1, &resolveFBO);
|
|
resolveFBO = 0;
|
|
}
|
|
if (resolveColorTex) {
|
|
glDeleteTextures(1, &resolveColorTex);
|
|
resolveColorTex = 0;
|
|
}
|
|
if (resolveDepthTex) {
|
|
glDeleteTextures(1, &resolveDepthTex);
|
|
resolveDepthTex = 0;
|
|
}
|
|
if (screenQuadVAO) {
|
|
glDeleteVertexArrays(1, &screenQuadVAO);
|
|
screenQuadVAO = 0;
|
|
}
|
|
if (screenQuadVBO) {
|
|
glDeleteBuffers(1, &screenQuadVBO);
|
|
screenQuadVBO = 0;
|
|
}
|
|
postProcessShader.reset();
|
|
}
|
|
|
|
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 asset manager to minimap for tile texture loading
|
|
if (minimap) {
|
|
minimap->setAssetManager(assetManager);
|
|
}
|
|
// Wire terrain manager, WMO renderer, and water renderer to camera controller
|
|
if (cameraController) {
|
|
cameraController->setTerrainManager(terrainManager.get());
|
|
if (wmoRenderer) {
|
|
cameraController->setWMORenderer(wmoRenderer.get());
|
|
}
|
|
if (m2Renderer) {
|
|
cameraController->setM2Renderer(m2Renderer.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);
|
|
if (minimap) {
|
|
minimap->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);
|
|
if (footstepManager) {
|
|
footstepManager->initialize(assetManager);
|
|
}
|
|
if (activitySoundManager) {
|
|
activitySoundManager->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);
|
|
}
|
|
}
|
|
if (footstepManager && cachedAssetManager) {
|
|
if (!footstepManager->isInitialized()) {
|
|
footstepManager->initialize(cachedAssetManager);
|
|
}
|
|
}
|
|
if (activitySoundManager && cachedAssetManager) {
|
|
if (!activitySoundManager->isInitialized()) {
|
|
activitySoundManager->initialize(cachedAssetManager);
|
|
}
|
|
}
|
|
|
|
// Wire WMO, M2, and water renderer to camera controller
|
|
if (cameraController && wmoRenderer) {
|
|
cameraController->setWMORenderer(wmoRenderer.get());
|
|
}
|
|
if (cameraController && m2Renderer) {
|
|
cameraController->setM2Renderer(m2Renderer.get());
|
|
}
|
|
if (cameraController && waterRenderer) {
|
|
cameraController->setWaterRenderer(waterRenderer.get());
|
|
}
|
|
|
|
// 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());
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────
|
|
// Shadow mapping helpers
|
|
// ──────────────────────────────────────────────────────
|
|
|
|
void Renderer::initShadowMap() {
|
|
// Compile shadow shader
|
|
shadowShaderProgram = compileShadowShader();
|
|
if (!shadowShaderProgram) {
|
|
LOG_ERROR("Failed to compile shadow shader");
|
|
return;
|
|
}
|
|
|
|
// Create depth texture
|
|
glGenTextures(1, &shadowDepthTex);
|
|
glBindTexture(GL_TEXTURE_2D, shadowDepthTex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24,
|
|
SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 0,
|
|
GL_DEPTH_COMPONENT, GL_FLOAT, 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_BORDER);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
|
float borderColor[] = {1.0f, 1.0f, 1.0f, 1.0f};
|
|
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
// Create depth-only FBO
|
|
glGenFramebuffers(1, &shadowFBO);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowDepthTex, 0);
|
|
glDrawBuffer(GL_NONE);
|
|
glReadBuffer(GL_NONE);
|
|
|
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
LOG_ERROR("Shadow FBO incomplete!");
|
|
}
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
|
|
LOG_INFO("Shadow map initialized (", SHADOW_MAP_SIZE, "x", SHADOW_MAP_SIZE, ")");
|
|
}
|
|
|
|
uint32_t Renderer::compileShadowShader() {
|
|
const char* vertSrc = R"(
|
|
#version 330 core
|
|
uniform mat4 uLightSpaceMatrix;
|
|
uniform mat4 uModel;
|
|
layout(location = 0) in vec3 aPos;
|
|
layout(location = 2) in vec2 aTexCoord;
|
|
layout(location = 3) in vec4 aBoneWeights;
|
|
layout(location = 4) in vec4 aBoneIndicesF;
|
|
uniform bool uUseBones;
|
|
uniform mat4 uBones[200];
|
|
out vec2 vTexCoord;
|
|
void main() {
|
|
vec3 pos = aPos;
|
|
if (uUseBones) {
|
|
ivec4 bi = ivec4(aBoneIndicesF);
|
|
mat4 boneTransform = uBones[bi.x] * aBoneWeights.x
|
|
+ uBones[bi.y] * aBoneWeights.y
|
|
+ uBones[bi.z] * aBoneWeights.z
|
|
+ uBones[bi.w] * aBoneWeights.w;
|
|
pos = vec3(boneTransform * vec4(aPos, 1.0));
|
|
}
|
|
vTexCoord = aTexCoord;
|
|
gl_Position = uLightSpaceMatrix * uModel * vec4(pos, 1.0);
|
|
}
|
|
)";
|
|
const char* fragSrc = R"(
|
|
#version 330 core
|
|
in vec2 vTexCoord;
|
|
uniform bool uUseTexture;
|
|
uniform sampler2D uTexture;
|
|
uniform bool uAlphaTest;
|
|
uniform float uShadowOpacity;
|
|
|
|
float hash12(vec2 p) {
|
|
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
p3 += dot(p3, p3.yzx + 33.33);
|
|
return fract((p3.x + p3.y) * p3.z);
|
|
}
|
|
|
|
void main() {
|
|
float opacity = clamp(uShadowOpacity, 0.0, 1.0);
|
|
if (uUseTexture) {
|
|
vec4 tex = texture(uTexture, vTexCoord);
|
|
if (uAlphaTest && tex.a < 0.5) discard;
|
|
opacity *= tex.a;
|
|
}
|
|
|
|
// Stochastic alpha for soft/translucent shadow casters (foliage).
|
|
// Use UV-space hash so pattern stays stable with camera movement.
|
|
if (opacity < 0.999) {
|
|
float d = hash12(floor(vTexCoord * 4096.0));
|
|
if (d > opacity) discard;
|
|
}
|
|
}
|
|
)";
|
|
|
|
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
|
glShaderSource(vs, 1, &vertSrc, nullptr);
|
|
glCompileShader(vs);
|
|
GLint success;
|
|
glGetShaderiv(vs, GL_COMPILE_STATUS, &success);
|
|
if (!success) {
|
|
char log[512];
|
|
glGetShaderInfoLog(vs, 512, nullptr, log);
|
|
LOG_ERROR("Shadow vertex shader error: ", log);
|
|
glDeleteShader(vs);
|
|
return 0;
|
|
}
|
|
|
|
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
|
glShaderSource(fs, 1, &fragSrc, nullptr);
|
|
glCompileShader(fs);
|
|
glGetShaderiv(fs, GL_COMPILE_STATUS, &success);
|
|
if (!success) {
|
|
char log[512];
|
|
glGetShaderInfoLog(fs, 512, nullptr, log);
|
|
LOG_ERROR("Shadow fragment shader error: ", log);
|
|
glDeleteShader(vs);
|
|
glDeleteShader(fs);
|
|
return 0;
|
|
}
|
|
|
|
GLuint program = glCreateProgram();
|
|
glAttachShader(program, vs);
|
|
glAttachShader(program, fs);
|
|
glLinkProgram(program);
|
|
glGetProgramiv(program, GL_LINK_STATUS, &success);
|
|
if (!success) {
|
|
char log[512];
|
|
glGetProgramInfoLog(program, 512, nullptr, log);
|
|
LOG_ERROR("Shadow shader link error: ", log);
|
|
glDeleteProgram(program);
|
|
program = 0;
|
|
}
|
|
|
|
glDeleteShader(vs);
|
|
glDeleteShader(fs);
|
|
return program;
|
|
}
|
|
|
|
glm::mat4 Renderer::computeLightSpaceMatrix() {
|
|
constexpr float kShadowHalfExtent = 180.0f;
|
|
constexpr float kShadowLightDistance = 280.0f;
|
|
constexpr float kShadowNearPlane = 1.0f;
|
|
constexpr float kShadowFarPlane = 600.0f;
|
|
|
|
// Sun direction matching WMO light dir
|
|
glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f));
|
|
|
|
// Keep a stable shadow focus center and only recentre occasionally.
|
|
glm::vec3 desiredCenter = characterPosition;
|
|
if (!shadowCenterInitialized) {
|
|
shadowCenter = desiredCenter;
|
|
shadowCenterInitialized = true;
|
|
} else {
|
|
constexpr float recenterThreshold = 30.0f; // world units
|
|
if (std::abs(desiredCenter.x - shadowCenter.x) > recenterThreshold ||
|
|
std::abs(desiredCenter.y - shadowCenter.y) > recenterThreshold) {
|
|
shadowCenter.x = desiredCenter.x;
|
|
shadowCenter.y = desiredCenter.y;
|
|
}
|
|
// Avoid vertical jitter from tiny terrain/camera height changes.
|
|
if (std::abs(desiredCenter.z - shadowCenter.z) > 4.0f) {
|
|
shadowCenter.z = desiredCenter.z;
|
|
}
|
|
}
|
|
glm::vec3 center = shadowCenter;
|
|
|
|
// Texel snapping: round center to shadow texel boundaries to prevent shimmer
|
|
float halfExtent = kShadowHalfExtent;
|
|
float texelWorld = (2.0f * halfExtent) / static_cast<float>(SHADOW_MAP_SIZE);
|
|
|
|
// Build light view to get stable axes
|
|
glm::vec3 up(0.0f, 0.0f, 1.0f);
|
|
// If sunDir is nearly parallel to up, pick a different up vector
|
|
if (std::abs(glm::dot(sunDir, up)) > 0.99f) {
|
|
up = glm::vec3(0.0f, 1.0f, 0.0f);
|
|
}
|
|
glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
|
|
|
|
// Snap center in light space to texel grid
|
|
glm::vec4 centerLS = lightView * glm::vec4(center, 1.0f);
|
|
centerLS.x = std::round(centerLS.x / texelWorld) * texelWorld;
|
|
centerLS.y = std::round(centerLS.y / texelWorld) * texelWorld;
|
|
glm::vec4 snappedCenter = glm::inverse(lightView) * centerLS;
|
|
center = glm::vec3(snappedCenter);
|
|
shadowCenter = center;
|
|
|
|
// Rebuild with snapped center
|
|
lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
|
|
glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent,
|
|
kShadowNearPlane, kShadowFarPlane);
|
|
|
|
return lightProj * lightView;
|
|
}
|
|
|
|
void Renderer::renderShadowPass() {
|
|
constexpr float kShadowHalfExtent = 180.0f;
|
|
constexpr float kShadowLightDistance = 280.0f;
|
|
constexpr float kShadowNearPlane = 1.0f;
|
|
constexpr float kShadowFarPlane = 600.0f;
|
|
|
|
// Compute light space matrix
|
|
lightSpaceMatrix = computeLightSpaceMatrix();
|
|
|
|
// Bind shadow FBO
|
|
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
|
|
glViewport(0, 0, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE);
|
|
glClear(GL_DEPTH_BUFFER_BIT);
|
|
|
|
// Caster-side bias: front-face culling + polygon offset
|
|
glEnable(GL_POLYGON_OFFSET_FILL);
|
|
glPolygonOffset(2.0f, 4.0f);
|
|
glEnable(GL_CULL_FACE);
|
|
glCullFace(GL_FRONT);
|
|
|
|
// Use shadow shader
|
|
glUseProgram(shadowShaderProgram);
|
|
GLint lsmLoc = glGetUniformLocation(shadowShaderProgram, "uLightSpaceMatrix");
|
|
glUniformMatrix4fv(lsmLoc, 1, GL_FALSE, &lightSpaceMatrix[0][0]);
|
|
GLint useTexLoc = glGetUniformLocation(shadowShaderProgram, "uUseTexture");
|
|
GLint texLoc = glGetUniformLocation(shadowShaderProgram, "uTexture");
|
|
GLint alphaTestLoc = glGetUniformLocation(shadowShaderProgram, "uAlphaTest");
|
|
GLint opacityLoc = glGetUniformLocation(shadowShaderProgram, "uShadowOpacity");
|
|
GLint useBonesLoc = glGetUniformLocation(shadowShaderProgram, "uUseBones");
|
|
if (useTexLoc >= 0) glUniform1i(useTexLoc, 0);
|
|
if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, 0);
|
|
if (opacityLoc >= 0) glUniform1f(opacityLoc, 1.0f);
|
|
if (useBonesLoc >= 0) glUniform1i(useBonesLoc, 0);
|
|
if (texLoc >= 0) glUniform1i(texLoc, 0);
|
|
|
|
// Render terrain into shadow map
|
|
if (terrainRenderer) {
|
|
terrainRenderer->renderShadow(shadowShaderProgram);
|
|
}
|
|
|
|
// Render WMO into shadow map
|
|
if (wmoRenderer) {
|
|
// WMO renderShadow takes separate view/proj matrices and a Shader ref.
|
|
// We need to decompose our lightSpaceMatrix or use the raw shader program.
|
|
// Since WMO::renderShadow sets uModel per instance, we use the shadow shader
|
|
// directly by calling renderShadow with the light view/proj split.
|
|
// For simplicity, compute the split:
|
|
glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f));
|
|
glm::vec3 center = shadowCenterInitialized ? shadowCenter : characterPosition;
|
|
float halfExtent = kShadowHalfExtent;
|
|
glm::vec3 up(0.0f, 0.0f, 1.0f);
|
|
if (std::abs(glm::dot(sunDir, up)) > 0.99f) up = glm::vec3(0.0f, 1.0f, 0.0f);
|
|
glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up);
|
|
glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent,
|
|
kShadowNearPlane, kShadowFarPlane);
|
|
|
|
// WMO renderShadow needs a Shader reference — but it only uses setUniform("uModel", ...)
|
|
// We'll create a thin wrapper. Actually, WMO's renderShadow takes a Shader& and calls
|
|
// shadowShader.setUniform("uModel", ...). We need a Shader object wrapping our program.
|
|
// Instead, let's use the lower-level approach: WMO renderShadow uses the shader passed in.
|
|
// We need to temporarily wrap our GL program in a Shader object.
|
|
Shader shadowShaderWrapper;
|
|
shadowShaderWrapper.setProgram(shadowShaderProgram);
|
|
wmoRenderer->renderShadow(lightView, lightProj, shadowShaderWrapper);
|
|
shadowShaderWrapper.releaseProgram(); // Don't let wrapper delete our program
|
|
}
|
|
|
|
// Render M2 doodads into shadow map
|
|
if (m2Renderer) {
|
|
m2Renderer->renderShadow(shadowShaderProgram);
|
|
}
|
|
|
|
// Render characters into shadow map
|
|
if (characterRenderer) {
|
|
// Character shadows need less caster bias to avoid "floating" away from feet.
|
|
glDisable(GL_POLYGON_OFFSET_FILL);
|
|
glCullFace(GL_BACK);
|
|
characterRenderer->renderShadow(lightSpaceMatrix);
|
|
glCullFace(GL_FRONT);
|
|
glEnable(GL_POLYGON_OFFSET_FILL);
|
|
glPolygonOffset(2.0f, 4.0f);
|
|
}
|
|
|
|
// Restore state
|
|
glDisable(GL_POLYGON_OFFSET_FILL);
|
|
glCullFace(GL_BACK);
|
|
|
|
// Restore main viewport
|
|
glViewport(0, 0, fbWidth, fbHeight);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
|
|
// Distribute shadow map to all receivers
|
|
if (terrainRenderer) terrainRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
|
|
if (wmoRenderer) wmoRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
|
|
if (m2Renderer) m2Renderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
|
|
if (characterRenderer) characterRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix);
|
|
}
|
|
|
|
} // namespace rendering
|
|
} // namespace wowee
|