mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +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/wmo_renderer.cpp
|
||||
src/rendering/m2_renderer.cpp
|
||||
src/rendering/quest_marker_renderer.cpp
|
||||
src/rendering/minimap.cpp
|
||||
src/rendering/world_map.cpp
|
||||
src/rendering/swim_effects.cpp
|
||||
|
|
|
|||
|
|
@ -204,12 +204,9 @@ private:
|
|||
std::vector<PendingGameObjectSpawn> pendingGameObjectSpawns_;
|
||||
void processGameObjectSpawnQueue();
|
||||
|
||||
// Quest marker 3D models (billboarded above NPCs)
|
||||
uint32_t questExclamationModelId_ = 0;
|
||||
uint32_t questQuestionMarkModelId_ = 0;
|
||||
std::unordered_map<uint64_t, uint32_t> questMarkerInstances_; // npcGuid → marker instanceId
|
||||
void loadQuestMarkerModels();
|
||||
void updateQuestMarkers();
|
||||
// Quest marker billboard sprites (above NPCs)
|
||||
void loadQuestMarkerModels(); // Now loads BLP textures
|
||||
void updateQuestMarkers(); // Updates billboard positions
|
||||
};
|
||||
|
||||
} // 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 M2Renderer;
|
||||
class Minimap;
|
||||
class QuestMarkerRenderer;
|
||||
class Shader;
|
||||
|
||||
class Renderer {
|
||||
|
|
@ -105,6 +106,7 @@ public:
|
|||
WMORenderer* getWMORenderer() const { return wmoRenderer.get(); }
|
||||
M2Renderer* getM2Renderer() const { return m2Renderer.get(); }
|
||||
Minimap* getMinimap() const { return minimap.get(); }
|
||||
QuestMarkerRenderer* getQuestMarkerRenderer() const { return questMarkerRenderer.get(); }
|
||||
const std::string& getCurrentZoneName() const { return currentZoneName; }
|
||||
|
||||
// Third-person character follow
|
||||
|
|
@ -177,6 +179,7 @@ private:
|
|||
std::unique_ptr<WMORenderer> wmoRenderer;
|
||||
std::unique_ptr<M2Renderer> m2Renderer;
|
||||
std::unique_ptr<Minimap> minimap;
|
||||
std::unique_ptr<QuestMarkerRenderer> questMarkerRenderer;
|
||||
std::unique_ptr<audio::MusicManager> musicManager;
|
||||
std::unique_ptr<audio::FootstepManager> footstepManager;
|
||||
std::unique_ptr<audio::ActivitySoundManager> activitySoundManager;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
#include "rendering/minimap.hpp"
|
||||
#include "rendering/quest_marker_renderer.hpp"
|
||||
#include "rendering/loading_screen.hpp"
|
||||
#include "audio/music_manager.hpp"
|
||||
#include "audio/footstep_manager.hpp"
|
||||
|
|
@ -3001,67 +3002,56 @@ void Application::loadQuestMarkerModels() {
|
|||
}
|
||||
|
||||
void Application::updateQuestMarkers() {
|
||||
if (!gameHandler || !renderer || questExclamationModelId_ == 0) {
|
||||
if (!gameHandler || !renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* questMarkerRenderer = renderer->getQuestMarkerRenderer();
|
||||
if (!questMarkerRenderer) {
|
||||
static bool logged = false;
|
||||
if (!logged) {
|
||||
LOG_INFO("updateQuestMarkers: skipped - gameHandler=", (gameHandler ? "yes" : "no"),
|
||||
" renderer=", (renderer ? "yes" : "no"),
|
||||
" questExclamationModelId=", questExclamationModelId_);
|
||||
LOG_WARNING("QuestMarkerRenderer not available!");
|
||||
logged = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto* m2Renderer = renderer->getM2Renderer();
|
||||
if (!m2Renderer) return;
|
||||
|
||||
const auto& questStatuses = gameHandler->getNpcQuestStatuses();
|
||||
|
||||
static int logCounter = 0;
|
||||
if (++logCounter % 300 == 0) { // Log every ~10 seconds at 30fps
|
||||
LOG_INFO("Quest markers: ", questStatuses.size(), " NPCs with status, ",
|
||||
questMarkerInstances_.size(), " markers active");
|
||||
LOG_INFO("Quest markers: ", questStatuses.size(), " NPCs with quest status");
|
||||
}
|
||||
|
||||
// Remove markers for NPCs that no longer have quest status
|
||||
std::vector<uint64_t> toRemove;
|
||||
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);
|
||||
}
|
||||
// Clear all markers (we'll re-add active ones)
|
||||
questMarkerRenderer->clear();
|
||||
|
||||
// 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) {
|
||||
// Determine which marker model to use
|
||||
uint32_t markerModelId = 0;
|
||||
bool shouldShow = false;
|
||||
// Determine marker type
|
||||
int markerType = -1; // -1 = no marker
|
||||
|
||||
using game::QuestGiverStatus;
|
||||
switch (status) {
|
||||
case QuestGiverStatus::AVAILABLE:
|
||||
case QuestGiverStatus::AVAILABLE_LOW:
|
||||
markerModelId = questExclamationModelId_;
|
||||
shouldShow = true;
|
||||
markerType = 0; // Available (yellow !)
|
||||
break;
|
||||
case QuestGiverStatus::REWARD:
|
||||
markerModelId = questQuestionMarkModelId_;
|
||||
shouldShow = true;
|
||||
markerType = 1; // Turn-in (yellow ?)
|
||||
break;
|
||||
case QuestGiverStatus::INCOMPLETE:
|
||||
// Gray ? - for now just use regular ? (could load yellow variant later)
|
||||
markerModelId = questQuestionMarkModelId_;
|
||||
shouldShow = false; // Don't show incomplete markers
|
||||
markerType = 2; // Incomplete (grey ?)
|
||||
break;
|
||||
default:
|
||||
shouldShow = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (markerType < 0) continue;
|
||||
|
||||
// Get NPC entity position
|
||||
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
||||
if (!entity) continue;
|
||||
|
|
@ -3069,35 +3059,22 @@ void Application::updateQuestMarkers() {
|
|||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||
glm::vec3 renderPos = coords::canonicalToRender(canonical);
|
||||
|
||||
// Offset marker above NPC head
|
||||
// Get NPC bounding height for proper marker positioning
|
||||
glm::vec3 boundsCenter;
|
||||
float boundsRadius = 0.0f;
|
||||
float heightOffset = 3.0f;
|
||||
float boundingHeight = 2.0f; // Default
|
||||
if (getRenderBoundsForGuid(guid, boundsCenter, boundsRadius)) {
|
||||
heightOffset = boundsRadius * 2.0f + 1.0f;
|
||||
boundingHeight = boundsRadius * 2.0f;
|
||||
}
|
||||
renderPos.z += heightOffset;
|
||||
|
||||
if (shouldShow && markerModelId != 0) {
|
||||
// Check if marker already exists
|
||||
auto it = questMarkerInstances_.find(guid);
|
||||
if (it != questMarkerInstances_.end()) {
|
||||
// Update existing marker position
|
||||
m2Renderer->setInstancePosition(it->second, renderPos);
|
||||
} else {
|
||||
// Create new marker instance (billboarded, no rotation needed)
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Set the marker (renderer will handle positioning, bob, glow, etc.)
|
||||
questMarkerRenderer->setMarker(guid, renderPos, markerType, boundingHeight);
|
||||
markersAdded++;
|
||||
}
|
||||
|
||||
if (firstRun && markersAdded > 0) {
|
||||
LOG_INFO("Quest markers: Added ", markersAdded, " markers on first run");
|
||||
firstRun = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
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/m2_renderer.hpp"
|
||||
#include "rendering/minimap.hpp"
|
||||
#include "rendering/quest_marker_renderer.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "pipeline/m2_loader.hpp"
|
||||
#include <algorithm>
|
||||
|
|
@ -330,6 +331,9 @@ bool Renderer::initialize(core::Window* win) {
|
|||
minimap.reset();
|
||||
}
|
||||
|
||||
// Create quest marker renderer (initialized later with AssetManager)
|
||||
questMarkerRenderer = std::make_unique<QuestMarkerRenderer>();
|
||||
|
||||
// Create M2 renderer (for doodads)
|
||||
m2Renderer = std::make_unique<M2Renderer>();
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Render quest markers (billboards above NPCs)
|
||||
if (questMarkerRenderer && camera) {
|
||||
questMarkerRenderer->render(*camera);
|
||||
}
|
||||
|
||||
// Full-screen underwater tint so WMO/M2/characters also feel submerged.
|
||||
if (false && underwater && underwaterOverlayShader && underwaterOverlayVAO) {
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
|
@ -2224,6 +2233,9 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
|
|||
if (movementSoundManager) {
|
||||
movementSoundManager->initialize(assetManager);
|
||||
}
|
||||
if (questMarkerRenderer) {
|
||||
questMarkerRenderer->initialize(assetManager);
|
||||
}
|
||||
cachedAssetManager = assetManager;
|
||||
}
|
||||
|
||||
|
|
@ -2323,6 +2335,9 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent
|
|||
if (movementSoundManager && cachedAssetManager) {
|
||||
movementSoundManager->initialize(cachedAssetManager);
|
||||
}
|
||||
if (questMarkerRenderer && cachedAssetManager) {
|
||||
questMarkerRenderer->initialize(cachedAssetManager);
|
||||
}
|
||||
|
||||
// Wire ambient sound manager to terrain manager for emitter registration
|
||||
if (terrainManager && ambientSoundManager) {
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderVendorWindow(gameHandler);
|
||||
renderTrainerWindow(gameHandler);
|
||||
renderTaxiWindow(gameHandler);
|
||||
renderQuestMarkers(gameHandler); // 2D markers (3D M2 files not in MPQ)
|
||||
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
||||
renderMinimapMarkers(gameHandler);
|
||||
renderDeathScreen(gameHandler);
|
||||
renderResurrectDialog(gameHandler);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue