From f7372a282df698c71dce933b9bea896d7fce1f4f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 20 Feb 2026 16:02:34 -0800 Subject: [PATCH] Improve selected-NPC ring visuals, anchoring, and occlusion behavior Selection ring rendering has been upgraded to better match WoW-like target feedback and remain readable across complex geometry. Visual updates: - Reworked ring mesh/shader from a simple two-band strip to a unit-disc + radial fragment shaping. - Implemented a thinner outer ring profile. - Added an inward color/alpha gradient that fades from the ring toward the center. Placement/anchoring updates: - Added CharacterRenderer::getInstanceFootZ() to query model foot plane from instance bounds. - Added Application::getRenderFootZForGuid() to resolve per-GUID foot height via live instance mapping. - Updated GameScreen target selection placement to anchor the effect at target foot Z. Ground/surface stability: - In renderSelectionCircle(), added floor clamping against terrain, WMO, and M2 floor probes at target XY. - Raised final placement offset to reduce residual clipping on uneven surfaces. Depth/visibility behavior: - Added polygon offset during ring draw to reduce z-fighting. - Disabled depth testing for the selection effect draw pass (with state restore) so the ring remains visible through terrain/WMO occluders, per requested behavior. State safety: - Restored modified GL state after selection pass (depth test / polygon offset / depth mask / cull). Build validation: - Verified with cmake build target "wowee" after each stage; final build succeeds. --- include/core/application.hpp | 1 + include/rendering/character_renderer.hpp | 1 + src/core/application.cpp | 20 +++++++ src/rendering/character_renderer.cpp | 13 +++++ src/rendering/renderer.cpp | 68 ++++++++++++++++++------ src/ui/game_screen.cpp | 4 ++ 6 files changed, 90 insertions(+), 17 deletions(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index 6b55c621..2f961b1e 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -70,6 +70,7 @@ public: // Render bounds lookup (for click targeting / selection) bool getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const; + bool getRenderFootZForGuid(uint64_t guid, float& outFootZ) const; // Character skin composite state (saved at spawn for re-compositing on equipment change) const std::string& getBodySkinPath() const { return bodySkinPath_; } diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index d3f2d19b..b5b4f518 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -75,6 +75,7 @@ public: bool getAnimationSequences(uint32_t instanceId, std::vector& out) const; bool getInstanceModelName(uint32_t instanceId, std::string& modelName) const; bool getInstanceBounds(uint32_t instanceId, glm::vec3& outCenter, float& outRadius) const; + bool getInstanceFootZ(uint32_t instanceId, float& outFootZ) const; /** Debug: Log all available animations for an instance */ void dumpAnimations(uint32_t instanceId) const; diff --git a/src/core/application.cpp b/src/core/application.cpp index 411f819f..d7d4750f 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -3288,6 +3288,26 @@ bool Application::getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, fl return renderer->getCharacterRenderer()->getInstanceBounds(instanceId, outCenter, outRadius); } +bool Application::getRenderFootZForGuid(uint64_t guid, float& outFootZ) const { + if (!renderer || !renderer->getCharacterRenderer()) return false; + uint32_t instanceId = 0; + + if (gameHandler && guid == gameHandler->getPlayerGuid()) { + instanceId = renderer->getCharacterInstanceId(); + } + if (instanceId == 0) { + auto pit = playerInstances_.find(guid); + if (pit != playerInstances_.end()) instanceId = pit->second; + } + if (instanceId == 0) { + auto it = creatureInstances_.find(guid); + if (it != creatureInstances_.end()) instanceId = it->second; + } + if (instanceId == 0) return false; + + return renderer->getCharacterRenderer()->getInstanceFootZ(instanceId, outFootZ); +} + void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return; diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 61b68526..ba902273 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -2166,6 +2166,19 @@ bool CharacterRenderer::getInstanceBounds(uint32_t instanceId, glm::vec3& outCen return true; } +bool CharacterRenderer::getInstanceFootZ(uint32_t instanceId, float& outFootZ) const { + auto it = instances.find(instanceId); + if (it == instances.end()) return false; + auto mIt = models.find(it->second.modelId); + if (mIt == models.end()) return false; + + const auto& inst = it->second; + const auto& model = mIt->second.data; + float scale = std::max(0.001f, inst.scale); + outFootZ = inst.position.z + model.boundMin.z * scale; + return true; +} + void CharacterRenderer::detachWeapon(uint32_t charInstanceId, uint32_t attachmentId) { auto charIt = instances.find(charInstanceId); if (charIt == instances.end()) return; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 9b9de2fc..5d394b60 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -2462,21 +2462,35 @@ void Renderer::update(float deltaTime) { void Renderer::initSelectionCircle() { if (selCircleVAO) return; - // Minimal shader: position + uniform MVP + color + // Selection effect shader: thin outer ring + inward fade toward center. const char* vsSrc = R"( #version 330 core layout(location = 0) in vec3 aPos; uniform mat4 uMVP; + out vec2 vLocalPos; void main() { + vLocalPos = aPos.xy; gl_Position = uMVP * vec4(aPos, 1.0); } )"; const char* fsSrc = R"( #version 330 core uniform vec3 uColor; + in vec2 vLocalPos; out vec4 FragColor; void main() { - FragColor = vec4(uColor, 0.6); + float r = clamp(length(vLocalPos), 0.0, 1.0); + + float ringInner = 0.93; + float ringOuter = 1.00; + float ring = smoothstep(ringInner - 0.01, ringInner + 0.01, r) * + (1.0 - smoothstep(ringOuter - 0.008, ringOuter + 0.004, r)); + + float inward = smoothstep(0.0, ringInner, r); + inward = pow(inward, 1.9) * (1.0 - smoothstep(ringInner - 0.015, ringInner + 0.01, r)); + + float alpha = max(ring * 0.9, inward * 0.45); + FragColor = vec4(uColor, alpha); } )"; @@ -2496,24 +2510,23 @@ void Renderer::initSelectionCircle() { glDeleteShader(vs); glDeleteShader(fs); - // Build ring vertices (two concentric circles forming a strip) + // Build a unit disc; fragment shader shapes ring+gradient by radius. constexpr int SEGMENTS = 48; - constexpr float INNER = 0.85f; - constexpr float OUTER = 1.0f; std::vector verts; - for (int i = 0; i <= SEGMENTS; i++) { + verts.reserve((SEGMENTS + 2) * 3); + + verts.push_back(0.0f); + verts.push_back(0.0f); + verts.push_back(0.0f); + + for (int i = 0; i <= SEGMENTS; ++i) { float angle = 2.0f * 3.14159265f * static_cast(i) / static_cast(SEGMENTS); float c = std::cos(angle), s = std::sin(angle); - // Outer vertex - verts.push_back(c * OUTER); - verts.push_back(s * OUTER); - verts.push_back(0.0f); - // Inner vertex - verts.push_back(c * INNER); - verts.push_back(s * INNER); + verts.push_back(c); + verts.push_back(s); verts.push_back(0.0f); } - selCircleVertCount = static_cast((SEGMENTS + 1) * 2); + selCircleVertCount = static_cast(SEGMENTS + 2); glGenVertexArrays(1, &selCircleVAO); glGenBuffers(1, &selCircleVBO); @@ -2540,9 +2553,24 @@ void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& pro if (!selCircleVisible) return; initSelectionCircle(); - // Small Z offset to prevent clipping under terrain + // Clamp the circle to the best floor estimate at target XY to avoid clipping into + // terrain/WMO/M2 surfaces, then keep a small visual lift above that plane. + float floorZ = selCirclePos.z; + if (terrainManager) { + auto terrainH = terrainManager->getHeightAt(selCirclePos.x, selCirclePos.y); + if (terrainH) floorZ = std::max(floorZ, *terrainH); + } + if (wmoRenderer) { + auto wmoH = wmoRenderer->getFloorHeight(selCirclePos.x, selCirclePos.y, selCirclePos.z + 3.0f); + if (wmoH) floorZ = std::max(floorZ, *wmoH); + } + if (m2Renderer) { + auto m2H = m2Renderer->getFloorHeight(selCirclePos.x, selCirclePos.y, selCirclePos.z + 2.0f); + if (m2H) floorZ = std::max(floorZ, *m2H); + } + glm::vec3 raisedPos = selCirclePos; - raisedPos.z += 0.15f; + raisedPos.z = floorZ + 0.17f; glm::mat4 model = glm::translate(glm::mat4(1.0f), raisedPos); model = glm::scale(model, glm::vec3(selCircleRadius)); @@ -2552,15 +2580,21 @@ void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& pro glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDisable(GL_CULL_FACE); glDepthMask(GL_FALSE); + GLboolean depthTestWasEnabled = glIsEnabled(GL_DEPTH_TEST); + glDisable(GL_DEPTH_TEST); + glEnable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(-2.0f, -2.0f); glUseProgram(selCircleShader); glUniformMatrix4fv(glGetUniformLocation(selCircleShader, "uMVP"), 1, GL_FALSE, &mvp[0][0]); glUniform3fv(glGetUniformLocation(selCircleShader, "uColor"), 1, &selCircleColor[0]); glBindVertexArray(selCircleVAO); - glDrawArrays(GL_TRIANGLE_STRIP, 0, selCircleVertCount); + glDrawArrays(GL_TRIANGLE_FAN, 0, selCircleVertCount); glBindVertexArray(0); + glDisable(GL_POLYGON_OFFSET_FILL); + if (depthTestWasEnabled) glEnable(GL_DEPTH_TEST); glDepthMask(GL_TRUE); glEnable(GL_CULL_FACE); } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index d734b04d..fb0e3138 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -404,6 +404,10 @@ void GameScreen::render(game::GameHandler& gameHandler) { if (target) { targetGLPos = core::coords::canonicalToRender( glm::vec3(target->getX(), target->getY(), target->getZ())); + float footZ = 0.0f; + if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) { + targetGLPos.z = footZ; + } renderer->setTargetPosition(&targetGLPos); // Selection circle color: WoW-canonical level-based colors