mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
Implement WoW-style 3D billboard quest markers
Replace 2D ImGui text markers with proper 3D billboard sprites using BLP textures. Features: - Billboard rendering using Interface\GossipFrame\ BLP textures (yellow !, yellow ?, grey ?) - WoW-style visual effects: bob animation, distance-based scaling, glow pass, distance fade - Proper NPC height positioning with bounding box detection - Camera-facing quads with depth testing but no depth write - Shader-based alpha modulation for glow and fade effects Technical changes: - Created QuestMarkerRenderer class with billboard sprite system - Integrated into Renderer initialization for both online and offline terrain loading - Rewrote updateQuestMarkers() to use billboard system instead of M2 models - Disabled old 2D ImGui renderQuestMarkers() in game_screen.cpp - Added debug logging for initialization and marker tracking Quest markers now render with proper WoW visual fidelity.
This commit is contained in:
parent
084a79a6bc
commit
71d14b77c9
8 changed files with 407 additions and 63 deletions
|
|
@ -146,6 +146,7 @@ set(WOWEE_SOURCES
|
||||||
src/rendering/character_preview.cpp
|
src/rendering/character_preview.cpp
|
||||||
src/rendering/wmo_renderer.cpp
|
src/rendering/wmo_renderer.cpp
|
||||||
src/rendering/m2_renderer.cpp
|
src/rendering/m2_renderer.cpp
|
||||||
|
src/rendering/quest_marker_renderer.cpp
|
||||||
src/rendering/minimap.cpp
|
src/rendering/minimap.cpp
|
||||||
src/rendering/world_map.cpp
|
src/rendering/world_map.cpp
|
||||||
src/rendering/swim_effects.cpp
|
src/rendering/swim_effects.cpp
|
||||||
|
|
|
||||||
|
|
@ -204,12 +204,9 @@ private:
|
||||||
std::vector<PendingGameObjectSpawn> pendingGameObjectSpawns_;
|
std::vector<PendingGameObjectSpawn> pendingGameObjectSpawns_;
|
||||||
void processGameObjectSpawnQueue();
|
void processGameObjectSpawnQueue();
|
||||||
|
|
||||||
// Quest marker 3D models (billboarded above NPCs)
|
// Quest marker billboard sprites (above NPCs)
|
||||||
uint32_t questExclamationModelId_ = 0;
|
void loadQuestMarkerModels(); // Now loads BLP textures
|
||||||
uint32_t questQuestionMarkModelId_ = 0;
|
void updateQuestMarkers(); // Updates billboard positions
|
||||||
std::unordered_map<uint64_t, uint32_t> questMarkerInstances_; // npcGuid → marker instanceId
|
|
||||||
void loadQuestMarkerModels();
|
|
||||||
void updateQuestMarkers();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace core
|
} // namespace core
|
||||||
|
|
|
||||||
71
include/rendering/quest_marker_renderer.hpp
Normal file
71
include/rendering/quest_marker_renderer.hpp
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace pipeline { class AssetManager; }
|
||||||
|
namespace rendering {
|
||||||
|
|
||||||
|
class Camera;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders quest markers as billboarded sprites above NPCs
|
||||||
|
* Uses BLP textures from Interface\GossipFrame\
|
||||||
|
*/
|
||||||
|
class QuestMarkerRenderer {
|
||||||
|
public:
|
||||||
|
QuestMarkerRenderer();
|
||||||
|
~QuestMarkerRenderer();
|
||||||
|
|
||||||
|
bool initialize(pipeline::AssetManager* assetManager);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or update a quest marker at a position
|
||||||
|
* @param guid NPC GUID
|
||||||
|
* @param position World position (NPC base position)
|
||||||
|
* @param markerType 0=available(!), 1=turnin(?), 2=incomplete(?)
|
||||||
|
* @param boundingHeight NPC bounding height (optional, default 2.0f)
|
||||||
|
*/
|
||||||
|
void setMarker(uint64_t guid, const glm::vec3& position, int markerType, float boundingHeight = 2.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a quest marker
|
||||||
|
*/
|
||||||
|
void removeMarker(uint64_t guid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all markers
|
||||||
|
*/
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render all quest markers (call after world rendering, before UI)
|
||||||
|
*/
|
||||||
|
void render(const Camera& camera);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Marker {
|
||||||
|
glm::vec3 position;
|
||||||
|
int type; // 0=available, 1=turnin, 2=incomplete
|
||||||
|
float boundingHeight = 2.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<uint64_t, Marker> markers_;
|
||||||
|
|
||||||
|
// OpenGL resources
|
||||||
|
uint32_t vao_ = 0;
|
||||||
|
uint32_t vbo_ = 0;
|
||||||
|
uint32_t shaderProgram_ = 0;
|
||||||
|
uint32_t textures_[3] = {0, 0, 0}; // available, turnin, incomplete
|
||||||
|
|
||||||
|
void createQuad();
|
||||||
|
void loadTextures(pipeline::AssetManager* assetManager);
|
||||||
|
void createShader();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -32,6 +32,7 @@ class CharacterRenderer;
|
||||||
class WMORenderer;
|
class WMORenderer;
|
||||||
class M2Renderer;
|
class M2Renderer;
|
||||||
class Minimap;
|
class Minimap;
|
||||||
|
class QuestMarkerRenderer;
|
||||||
class Shader;
|
class Shader;
|
||||||
|
|
||||||
class Renderer {
|
class Renderer {
|
||||||
|
|
@ -105,6 +106,7 @@ public:
|
||||||
WMORenderer* getWMORenderer() const { return wmoRenderer.get(); }
|
WMORenderer* getWMORenderer() const { return wmoRenderer.get(); }
|
||||||
M2Renderer* getM2Renderer() const { return m2Renderer.get(); }
|
M2Renderer* getM2Renderer() const { return m2Renderer.get(); }
|
||||||
Minimap* getMinimap() const { return minimap.get(); }
|
Minimap* getMinimap() const { return minimap.get(); }
|
||||||
|
QuestMarkerRenderer* getQuestMarkerRenderer() const { return questMarkerRenderer.get(); }
|
||||||
const std::string& getCurrentZoneName() const { return currentZoneName; }
|
const std::string& getCurrentZoneName() const { return currentZoneName; }
|
||||||
|
|
||||||
// Third-person character follow
|
// Third-person character follow
|
||||||
|
|
@ -177,6 +179,7 @@ private:
|
||||||
std::unique_ptr<WMORenderer> wmoRenderer;
|
std::unique_ptr<WMORenderer> wmoRenderer;
|
||||||
std::unique_ptr<M2Renderer> m2Renderer;
|
std::unique_ptr<M2Renderer> m2Renderer;
|
||||||
std::unique_ptr<Minimap> minimap;
|
std::unique_ptr<Minimap> minimap;
|
||||||
|
std::unique_ptr<QuestMarkerRenderer> questMarkerRenderer;
|
||||||
std::unique_ptr<audio::MusicManager> musicManager;
|
std::unique_ptr<audio::MusicManager> musicManager;
|
||||||
std::unique_ptr<audio::FootstepManager> footstepManager;
|
std::unique_ptr<audio::FootstepManager> footstepManager;
|
||||||
std::unique_ptr<audio::ActivitySoundManager> activitySoundManager;
|
std::unique_ptr<audio::ActivitySoundManager> activitySoundManager;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
#include "rendering/wmo_renderer.hpp"
|
#include "rendering/wmo_renderer.hpp"
|
||||||
#include "rendering/m2_renderer.hpp"
|
#include "rendering/m2_renderer.hpp"
|
||||||
#include "rendering/minimap.hpp"
|
#include "rendering/minimap.hpp"
|
||||||
|
#include "rendering/quest_marker_renderer.hpp"
|
||||||
#include "rendering/loading_screen.hpp"
|
#include "rendering/loading_screen.hpp"
|
||||||
#include "audio/music_manager.hpp"
|
#include "audio/music_manager.hpp"
|
||||||
#include "audio/footstep_manager.hpp"
|
#include "audio/footstep_manager.hpp"
|
||||||
|
|
@ -3001,67 +3002,56 @@ void Application::loadQuestMarkerModels() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::updateQuestMarkers() {
|
void Application::updateQuestMarkers() {
|
||||||
if (!gameHandler || !renderer || questExclamationModelId_ == 0) {
|
if (!gameHandler || !renderer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* questMarkerRenderer = renderer->getQuestMarkerRenderer();
|
||||||
|
if (!questMarkerRenderer) {
|
||||||
static bool logged = false;
|
static bool logged = false;
|
||||||
if (!logged) {
|
if (!logged) {
|
||||||
LOG_INFO("updateQuestMarkers: skipped - gameHandler=", (gameHandler ? "yes" : "no"),
|
LOG_WARNING("QuestMarkerRenderer not available!");
|
||||||
" renderer=", (renderer ? "yes" : "no"),
|
|
||||||
" questExclamationModelId=", questExclamationModelId_);
|
|
||||||
logged = true;
|
logged = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* m2Renderer = renderer->getM2Renderer();
|
|
||||||
if (!m2Renderer) return;
|
|
||||||
|
|
||||||
const auto& questStatuses = gameHandler->getNpcQuestStatuses();
|
const auto& questStatuses = gameHandler->getNpcQuestStatuses();
|
||||||
|
|
||||||
static int logCounter = 0;
|
static int logCounter = 0;
|
||||||
if (++logCounter % 300 == 0) { // Log every ~10 seconds at 30fps
|
if (++logCounter % 300 == 0) { // Log every ~10 seconds at 30fps
|
||||||
LOG_INFO("Quest markers: ", questStatuses.size(), " NPCs with status, ",
|
LOG_INFO("Quest markers: ", questStatuses.size(), " NPCs with quest status");
|
||||||
questMarkerInstances_.size(), " markers active");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove markers for NPCs that no longer have quest status
|
// Clear all markers (we'll re-add active ones)
|
||||||
std::vector<uint64_t> toRemove;
|
questMarkerRenderer->clear();
|
||||||
for (const auto& [guid, instanceId] : questMarkerInstances_) {
|
|
||||||
if (questStatuses.find(guid) == questStatuses.end()) {
|
|
||||||
m2Renderer->removeInstance(instanceId);
|
|
||||||
toRemove.push_back(guid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (uint64_t guid : toRemove) {
|
|
||||||
questMarkerInstances_.erase(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update or create markers for NPCs with quest status
|
static bool firstRun = true;
|
||||||
|
int markersAdded = 0;
|
||||||
|
|
||||||
|
// Add markers for NPCs with quest status
|
||||||
for (const auto& [guid, status] : questStatuses) {
|
for (const auto& [guid, status] : questStatuses) {
|
||||||
// Determine which marker model to use
|
// Determine marker type
|
||||||
uint32_t markerModelId = 0;
|
int markerType = -1; // -1 = no marker
|
||||||
bool shouldShow = false;
|
|
||||||
|
|
||||||
using game::QuestGiverStatus;
|
using game::QuestGiverStatus;
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case QuestGiverStatus::AVAILABLE:
|
case QuestGiverStatus::AVAILABLE:
|
||||||
case QuestGiverStatus::AVAILABLE_LOW:
|
case QuestGiverStatus::AVAILABLE_LOW:
|
||||||
markerModelId = questExclamationModelId_;
|
markerType = 0; // Available (yellow !)
|
||||||
shouldShow = true;
|
|
||||||
break;
|
break;
|
||||||
case QuestGiverStatus::REWARD:
|
case QuestGiverStatus::REWARD:
|
||||||
markerModelId = questQuestionMarkModelId_;
|
markerType = 1; // Turn-in (yellow ?)
|
||||||
shouldShow = true;
|
|
||||||
break;
|
break;
|
||||||
case QuestGiverStatus::INCOMPLETE:
|
case QuestGiverStatus::INCOMPLETE:
|
||||||
// Gray ? - for now just use regular ? (could load yellow variant later)
|
markerType = 2; // Incomplete (grey ?)
|
||||||
markerModelId = questQuestionMarkModelId_;
|
|
||||||
shouldShow = false; // Don't show incomplete markers
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
shouldShow = false;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (markerType < 0) continue;
|
||||||
|
|
||||||
// Get NPC entity position
|
// Get NPC entity position
|
||||||
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
||||||
if (!entity) continue;
|
if (!entity) continue;
|
||||||
|
|
@ -3069,35 +3059,22 @@ void Application::updateQuestMarkers() {
|
||||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||||
glm::vec3 renderPos = coords::canonicalToRender(canonical);
|
glm::vec3 renderPos = coords::canonicalToRender(canonical);
|
||||||
|
|
||||||
// Offset marker above NPC head
|
// Get NPC bounding height for proper marker positioning
|
||||||
glm::vec3 boundsCenter;
|
glm::vec3 boundsCenter;
|
||||||
float boundsRadius = 0.0f;
|
float boundsRadius = 0.0f;
|
||||||
float heightOffset = 3.0f;
|
float boundingHeight = 2.0f; // Default
|
||||||
if (getRenderBoundsForGuid(guid, boundsCenter, boundsRadius)) {
|
if (getRenderBoundsForGuid(guid, boundsCenter, boundsRadius)) {
|
||||||
heightOffset = boundsRadius * 2.0f + 1.0f;
|
boundingHeight = boundsRadius * 2.0f;
|
||||||
}
|
}
|
||||||
renderPos.z += heightOffset;
|
|
||||||
|
|
||||||
if (shouldShow && markerModelId != 0) {
|
// Set the marker (renderer will handle positioning, bob, glow, etc.)
|
||||||
// Check if marker already exists
|
questMarkerRenderer->setMarker(guid, renderPos, markerType, boundingHeight);
|
||||||
auto it = questMarkerInstances_.find(guid);
|
markersAdded++;
|
||||||
if (it != questMarkerInstances_.end()) {
|
}
|
||||||
// Update existing marker position
|
|
||||||
m2Renderer->setInstancePosition(it->second, renderPos);
|
if (firstRun && markersAdded > 0) {
|
||||||
} else {
|
LOG_INFO("Quest markers: Added ", markersAdded, " markers on first run");
|
||||||
// Create new marker instance (billboarded, no rotation needed)
|
firstRun = false;
|
||||||
uint32_t instanceId = m2Renderer->createInstance(
|
|
||||||
markerModelId, renderPos, glm::vec3(0.0f), 1.0f);
|
|
||||||
questMarkerInstances_[guid] = instanceId;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Remove marker if it exists but shouldn't show
|
|
||||||
auto it = questMarkerInstances_.find(guid);
|
|
||||||
if (it != questMarkerInstances_.end()) {
|
|
||||||
m2Renderer->removeInstance(it->second);
|
|
||||||
questMarkerInstances_.erase(it);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
280
src/rendering/quest_marker_renderer.cpp
Normal file
280
src/rendering/quest_marker_renderer.cpp
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
#include "rendering/quest_marker_renderer.hpp"
|
||||||
|
#include "rendering/camera.hpp"
|
||||||
|
#include "pipeline/asset_manager.hpp"
|
||||||
|
#include "pipeline/blp_loader.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
#include <GL/glew.h>
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace wowee { namespace rendering {
|
||||||
|
|
||||||
|
QuestMarkerRenderer::QuestMarkerRenderer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
QuestMarkerRenderer::~QuestMarkerRenderer() {
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QuestMarkerRenderer::initialize(pipeline::AssetManager* assetManager) {
|
||||||
|
if (!assetManager) {
|
||||||
|
LOG_WARNING("QuestMarkerRenderer: No AssetManager provided");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("QuestMarkerRenderer: Initializing...");
|
||||||
|
createShader();
|
||||||
|
createQuad();
|
||||||
|
loadTextures(assetManager);
|
||||||
|
LOG_INFO("QuestMarkerRenderer: Initialization complete");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuestMarkerRenderer::shutdown() {
|
||||||
|
if (vao_) glDeleteVertexArrays(1, &vao_);
|
||||||
|
if (vbo_) glDeleteBuffers(1, &vbo_);
|
||||||
|
if (shaderProgram_) glDeleteProgram(shaderProgram_);
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
if (textures_[i]) glDeleteTextures(1, &textures_[i]);
|
||||||
|
}
|
||||||
|
markers_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuestMarkerRenderer::createQuad() {
|
||||||
|
// Billboard quad vertices (centered, 1 unit size)
|
||||||
|
float vertices[] = {
|
||||||
|
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // bottom-left
|
||||||
|
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, // bottom-right
|
||||||
|
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // top-right
|
||||||
|
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, // top-left
|
||||||
|
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // bottom-left
|
||||||
|
0.5f, 0.5f, 0.0f, 1.0f, 0.0f // top-right
|
||||||
|
};
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &vao_);
|
||||||
|
glGenBuffers(1, &vbo_);
|
||||||
|
|
||||||
|
glBindVertexArray(vao_);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
// Position attribute
|
||||||
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
|
||||||
|
// Texture coord attribute
|
||||||
|
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuestMarkerRenderer::loadTextures(pipeline::AssetManager* assetManager) {
|
||||||
|
const char* paths[3] = {
|
||||||
|
"Interface\\GossipFrame\\AvailableQuestIcon.blp",
|
||||||
|
"Interface\\GossipFrame\\ActiveQuestIcon.blp",
|
||||||
|
"Interface\\GossipFrame\\IncompleteQuestIcon.blp"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
pipeline::BLPImage blp = assetManager->loadTexture(paths[i]);
|
||||||
|
if (!blp.isValid()) {
|
||||||
|
LOG_WARNING("Failed to load quest marker texture: ", paths[i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &textures_[i]);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textures_[i]);
|
||||||
|
|
||||||
|
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_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
|
||||||
|
LOG_INFO("Loaded quest marker texture: ", paths[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuestMarkerRenderer::createShader() {
|
||||||
|
const char* vertexShaderSource = R"(
|
||||||
|
#version 330 core
|
||||||
|
layout (location = 0) in vec3 aPos;
|
||||||
|
layout (location = 1) in vec2 aTexCoord;
|
||||||
|
|
||||||
|
out vec2 TexCoord;
|
||||||
|
|
||||||
|
uniform mat4 model;
|
||||||
|
uniform mat4 view;
|
||||||
|
uniform mat4 projection;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = projection * view * model * vec4(aPos, 1.0);
|
||||||
|
TexCoord = aTexCoord;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* fragmentShaderSource = R"(
|
||||||
|
#version 330 core
|
||||||
|
in vec2 TexCoord;
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
uniform sampler2D markerTexture;
|
||||||
|
uniform float uAlpha;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 texColor = texture(markerTexture, TexCoord);
|
||||||
|
if (texColor.a < 0.1)
|
||||||
|
discard;
|
||||||
|
FragColor = vec4(texColor.rgb, texColor.a * uAlpha);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Compile vertex shader
|
||||||
|
uint32_t vertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
|
||||||
|
glCompileShader(vertexShader);
|
||||||
|
|
||||||
|
// Compile fragment shader
|
||||||
|
uint32_t fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
|
||||||
|
glCompileShader(fragmentShader);
|
||||||
|
|
||||||
|
// Link shader program
|
||||||
|
shaderProgram_ = glCreateProgram();
|
||||||
|
glAttachShader(shaderProgram_, vertexShader);
|
||||||
|
glAttachShader(shaderProgram_, fragmentShader);
|
||||||
|
glLinkProgram(shaderProgram_);
|
||||||
|
|
||||||
|
glDeleteShader(vertexShader);
|
||||||
|
glDeleteShader(fragmentShader);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuestMarkerRenderer::setMarker(uint64_t guid, const glm::vec3& position, int markerType, float boundingHeight) {
|
||||||
|
markers_[guid] = {position, markerType, boundingHeight};
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuestMarkerRenderer::removeMarker(uint64_t guid) {
|
||||||
|
markers_.erase(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuestMarkerRenderer::clear() {
|
||||||
|
markers_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuestMarkerRenderer::render(const Camera& camera) {
|
||||||
|
if (markers_.empty() || !shaderProgram_ || !vao_) return;
|
||||||
|
|
||||||
|
// WoW-style quest marker tuning parameters
|
||||||
|
constexpr float BASE_SIZE = 0.65f; // Base world-space size
|
||||||
|
constexpr float HEIGHT_OFFSET = 2.1f; // Height above NPC bounds
|
||||||
|
constexpr float BOB_AMPLITUDE = 0.10f; // Bob animation amplitude
|
||||||
|
constexpr float BOB_FREQUENCY = 1.25f; // Bob frequency (Hz)
|
||||||
|
constexpr float MIN_DIST = 4.0f; // Near clamp
|
||||||
|
constexpr float MAX_DIST = 90.0f; // Far fade-out start
|
||||||
|
constexpr float FADE_RANGE = 25.0f; // Fade-out range
|
||||||
|
constexpr float GLOW_ALPHA = 0.35f; // Glow pass alpha
|
||||||
|
|
||||||
|
// Get time for bob animation
|
||||||
|
float timeSeconds = SDL_GetTicks() / 1000.0f;
|
||||||
|
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
glDepthMask(GL_FALSE); // Don't write to depth buffer
|
||||||
|
|
||||||
|
glUseProgram(shaderProgram_);
|
||||||
|
|
||||||
|
glm::mat4 view = camera.getViewMatrix();
|
||||||
|
glm::mat4 projection = camera.getProjectionMatrix();
|
||||||
|
glm::vec3 cameraPos = camera.getPosition();
|
||||||
|
|
||||||
|
int viewLoc = glGetUniformLocation(shaderProgram_, "view");
|
||||||
|
int projLoc = glGetUniformLocation(shaderProgram_, "projection");
|
||||||
|
int modelLoc = glGetUniformLocation(shaderProgram_, "model");
|
||||||
|
int alphaLoc = glGetUniformLocation(shaderProgram_, "uAlpha");
|
||||||
|
|
||||||
|
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
|
||||||
|
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
|
||||||
|
|
||||||
|
glBindVertexArray(vao_);
|
||||||
|
|
||||||
|
// Get camera right and up vectors for billboarding
|
||||||
|
glm::vec3 cameraRight = glm::vec3(view[0][0], view[1][0], view[2][0]);
|
||||||
|
glm::vec3 cameraUp = glm::vec3(view[0][1], view[1][1], view[2][1]);
|
||||||
|
|
||||||
|
for (const auto& [guid, marker] : markers_) {
|
||||||
|
if (marker.type < 0 || marker.type > 2) continue;
|
||||||
|
if (!textures_[marker.type]) continue;
|
||||||
|
|
||||||
|
// Calculate distance for LOD and culling
|
||||||
|
glm::vec3 toCamera = cameraPos - marker.position;
|
||||||
|
float dist = glm::length(toCamera);
|
||||||
|
|
||||||
|
// Calculate fade alpha
|
||||||
|
float fadeAlpha = 1.0f;
|
||||||
|
if (dist > MAX_DIST) {
|
||||||
|
float t = glm::clamp((dist - MAX_DIST) / FADE_RANGE, 0.0f, 1.0f);
|
||||||
|
t = t * t * (3.0f - 2.0f * t); // Smoothstep
|
||||||
|
fadeAlpha = 1.0f - t;
|
||||||
|
}
|
||||||
|
if (fadeAlpha <= 0.001f) continue; // Cull if fully faded
|
||||||
|
|
||||||
|
// Distance-based scaling (mild compensation for readability)
|
||||||
|
float distScale = 1.0f;
|
||||||
|
if (dist > MIN_DIST) {
|
||||||
|
float t = glm::clamp((dist - 5.0f) / 55.0f, 0.0f, 1.0f);
|
||||||
|
distScale = 1.0f + 0.35f * t;
|
||||||
|
}
|
||||||
|
float size = BASE_SIZE * distScale;
|
||||||
|
size = glm::clamp(size, BASE_SIZE * 0.9f, BASE_SIZE * 1.6f);
|
||||||
|
|
||||||
|
// Bob animation
|
||||||
|
float bob = std::sin(timeSeconds * BOB_FREQUENCY * 2.0f * 3.14159f) * BOB_AMPLITUDE;
|
||||||
|
|
||||||
|
// Position marker above NPC with bob
|
||||||
|
glm::vec3 markerPos = marker.position;
|
||||||
|
markerPos.z += marker.boundingHeight + HEIGHT_OFFSET + bob;
|
||||||
|
|
||||||
|
// Build billboard matrix (camera-facing quad)
|
||||||
|
glm::mat4 model = glm::mat4(1.0f);
|
||||||
|
model = glm::translate(model, markerPos);
|
||||||
|
|
||||||
|
// Billboard: align quad to face camera
|
||||||
|
model[0] = glm::vec4(cameraRight * size, 0.0f);
|
||||||
|
model[1] = glm::vec4(cameraUp * size, 0.0f);
|
||||||
|
model[2] = glm::vec4(glm::cross(cameraRight, cameraUp), 0.0f);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textures_[marker.type]);
|
||||||
|
|
||||||
|
// Glow pass (subtle additive glow for available/turnin markers)
|
||||||
|
if (marker.type == 0 || marker.type == 1) { // Available or turnin
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending
|
||||||
|
glUniform1f(alphaLoc, fadeAlpha * GLOW_ALPHA); // Reduced alpha for glow
|
||||||
|
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
|
||||||
|
// Restore standard alpha blending for main pass
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main pass with fade alpha
|
||||||
|
glUniform1f(alphaLoc, fadeAlpha);
|
||||||
|
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace wowee::rendering
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
#include "rendering/wmo_renderer.hpp"
|
#include "rendering/wmo_renderer.hpp"
|
||||||
#include "rendering/m2_renderer.hpp"
|
#include "rendering/m2_renderer.hpp"
|
||||||
#include "rendering/minimap.hpp"
|
#include "rendering/minimap.hpp"
|
||||||
|
#include "rendering/quest_marker_renderer.hpp"
|
||||||
#include "rendering/shader.hpp"
|
#include "rendering/shader.hpp"
|
||||||
#include "pipeline/m2_loader.hpp"
|
#include "pipeline/m2_loader.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -330,6 +331,9 @@ bool Renderer::initialize(core::Window* win) {
|
||||||
minimap.reset();
|
minimap.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create quest marker renderer (initialized later with AssetManager)
|
||||||
|
questMarkerRenderer = std::make_unique<QuestMarkerRenderer>();
|
||||||
|
|
||||||
// Create M2 renderer (for doodads)
|
// Create M2 renderer (for doodads)
|
||||||
m2Renderer = std::make_unique<M2Renderer>();
|
m2Renderer = std::make_unique<M2Renderer>();
|
||||||
// Note: M2 renderer needs asset manager, will be initialized when terrain loads
|
// Note: M2 renderer needs asset manager, will be initialized when terrain loads
|
||||||
|
|
@ -1871,6 +1875,11 @@ void Renderer::renderWorld(game::World* world) {
|
||||||
waterRenderer->render(*camera, time);
|
waterRenderer->render(*camera, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render quest markers (billboards above NPCs)
|
||||||
|
if (questMarkerRenderer && camera) {
|
||||||
|
questMarkerRenderer->render(*camera);
|
||||||
|
}
|
||||||
|
|
||||||
// Full-screen underwater tint so WMO/M2/characters also feel submerged.
|
// Full-screen underwater tint so WMO/M2/characters also feel submerged.
|
||||||
if (false && underwater && underwaterOverlayShader && underwaterOverlayVAO) {
|
if (false && underwater && underwaterOverlayShader && underwaterOverlayVAO) {
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
|
@ -2224,6 +2233,9 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
|
||||||
if (movementSoundManager) {
|
if (movementSoundManager) {
|
||||||
movementSoundManager->initialize(assetManager);
|
movementSoundManager->initialize(assetManager);
|
||||||
}
|
}
|
||||||
|
if (questMarkerRenderer) {
|
||||||
|
questMarkerRenderer->initialize(assetManager);
|
||||||
|
}
|
||||||
cachedAssetManager = assetManager;
|
cachedAssetManager = assetManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2323,6 +2335,9 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent
|
||||||
if (movementSoundManager && cachedAssetManager) {
|
if (movementSoundManager && cachedAssetManager) {
|
||||||
movementSoundManager->initialize(cachedAssetManager);
|
movementSoundManager->initialize(cachedAssetManager);
|
||||||
}
|
}
|
||||||
|
if (questMarkerRenderer && cachedAssetManager) {
|
||||||
|
questMarkerRenderer->initialize(cachedAssetManager);
|
||||||
|
}
|
||||||
|
|
||||||
// Wire ambient sound manager to terrain manager for emitter registration
|
// Wire ambient sound manager to terrain manager for emitter registration
|
||||||
if (terrainManager && ambientSoundManager) {
|
if (terrainManager && ambientSoundManager) {
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
renderVendorWindow(gameHandler);
|
renderVendorWindow(gameHandler);
|
||||||
renderTrainerWindow(gameHandler);
|
renderTrainerWindow(gameHandler);
|
||||||
renderTaxiWindow(gameHandler);
|
renderTaxiWindow(gameHandler);
|
||||||
renderQuestMarkers(gameHandler); // 2D markers (3D M2 files not in MPQ)
|
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
||||||
renderMinimapMarkers(gameHandler);
|
renderMinimapMarkers(gameHandler);
|
||||||
renderDeathScreen(gameHandler);
|
renderDeathScreen(gameHandler);
|
||||||
renderResurrectDialog(gameHandler);
|
renderResurrectDialog(gameHandler);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue