From 0240721d4633ba5dd6656cf13afe714a8cb75ea0 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Tue, 27 Oct 2026 20:04:18 +0600 Subject: [PATCH] Working on path visualization --- proj-android/app/jni/src/CMakeLists.txt | 2 + proj-linux/CMakeLists.txt | 2 + proj-web/CMakeLists.txt | 2 + proj-windows/CMakeLists.txt | 2 + resources/shaders/path_visualization.vertex | 8 + .../path_visualization_desktop.fragment | 6 + .../shaders/path_visualization_web.fragment | 8 + src/Game.cpp | 8 + src/Location.cpp | 28 ++- src/Location.h | 7 + src/render/PathVisualization.cpp | 224 ++++++++++++++++++ src/render/PathVisualization.h | 41 ++++ 12 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 resources/shaders/path_visualization.vertex create mode 100644 resources/shaders/path_visualization_desktop.fragment create mode 100644 resources/shaders/path_visualization_web.fragment create mode 100644 src/render/PathVisualization.cpp create mode 100644 src/render/PathVisualization.h diff --git a/proj-android/app/jni/src/CMakeLists.txt b/proj-android/app/jni/src/CMakeLists.txt index 5e757b3..9626416 100644 --- a/proj-android/app/jni/src/CMakeLists.txt +++ b/proj-android/app/jni/src/CMakeLists.txt @@ -60,6 +60,8 @@ add_library(main SHARED ../../../../src/Localization.h ../../../../src/render/Renderer.cpp ../../../../src/render/Renderer.h + ../../../../src/render/PathVisualization.cpp + ../../../../src/render/PathVisualization.h ../../../../src/render/ShaderManager.cpp ../../../../src/render/ShaderManager.h ../../../../src/render/TextureManager.cpp diff --git a/proj-linux/CMakeLists.txt b/proj-linux/CMakeLists.txt index ab7902c..d040f81 100644 --- a/proj-linux/CMakeLists.txt +++ b/proj-linux/CMakeLists.txt @@ -85,6 +85,8 @@ add_executable(ShadowOverBishkekDemo ../src/Localization.h ../src/render/Renderer.cpp ../src/render/Renderer.h + ../src/render/PathVisualization.cpp + ../src/render/PathVisualization.h ../src/render/ShaderManager.cpp ../src/render/ShaderManager.h ../src/render/TextureManager.cpp diff --git a/proj-web/CMakeLists.txt b/proj-web/CMakeLists.txt index fa92489..e68703c 100644 --- a/proj-web/CMakeLists.txt +++ b/proj-web/CMakeLists.txt @@ -79,6 +79,8 @@ set(SOURCES ../src/Localization.h ../src/render/Renderer.cpp ../src/render/Renderer.h + ../src/render/PathVisualization.cpp + ../src/render/PathVisualization.h ../src/render/ShaderManager.cpp ../src/render/ShaderManager.h ../src/render/TextureManager.cpp diff --git a/proj-windows/CMakeLists.txt b/proj-windows/CMakeLists.txt index 40ae8bb..256230d 100644 --- a/proj-windows/CMakeLists.txt +++ b/proj-windows/CMakeLists.txt @@ -27,6 +27,8 @@ add_executable(ShadowOverBishkekDemo WIN32 ../src/Localization.h ../src/render/Renderer.cpp ../src/render/Renderer.h + ../src/render/PathVisualization.cpp + ../src/render/PathVisualization.h ../src/render/ShaderManager.cpp ../src/render/ShaderManager.h ../src/render/TextureManager.cpp diff --git a/resources/shaders/path_visualization.vertex b/resources/shaders/path_visualization.vertex new file mode 100644 index 0000000..7a8fb84 --- /dev/null +++ b/resources/shaders/path_visualization.vertex @@ -0,0 +1,8 @@ +attribute vec3 vPosition; + +uniform mat4 ProjectionModelViewMatrix; + +void main() +{ + gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0); +} diff --git a/resources/shaders/path_visualization_desktop.fragment b/resources/shaders/path_visualization_desktop.fragment new file mode 100644 index 0000000..9b34737 --- /dev/null +++ b/resources/shaders/path_visualization_desktop.fragment @@ -0,0 +1,6 @@ +uniform vec4 uColor; + +void main() +{ + gl_FragColor = uColor; +} diff --git a/resources/shaders/path_visualization_web.fragment b/resources/shaders/path_visualization_web.fragment new file mode 100644 index 0000000..b27e974 --- /dev/null +++ b/resources/shaders/path_visualization_web.fragment @@ -0,0 +1,8 @@ +precision mediump float; + +uniform vec4 uColor; + +void main() +{ + gl_FragColor = uColor; +} diff --git a/src/Game.cpp b/src/Game.cpp index f7496b1..012d301 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -251,6 +251,10 @@ namespace FRG renderer.shaderManager.AddShaderFromFiles("fog_shadow_inst", "resources/shaders/fog_shadow_web.vertex", "resources/shaders/fog_shadow_web.fragment", CONST_ZIP_FILE, "#define INSTANCED\n"); renderer.shaderManager.AddShaderFromFiles("night_fog_inst", "resources/shaders/night_fog_web.vertex", "resources/shaders/night_fog_web.fragment", CONST_ZIP_FILE, "#define INSTANCED\n"); renderer.shaderManager.AddShaderFromFiles("night_fog_shadow_inst", "resources/shaders/night_fog_shadow_web.vertex", "resources/shaders/night_fog_shadow_web.fragment", CONST_ZIP_FILE, "#define INSTANCED\n"); + renderer.shaderManager.AddShaderFromFiles("path_visualization", + "resources/shaders/path_visualization.vertex", + "resources/shaders/path_visualization_web.fragment", + CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("text2d", "resources/shaders/text2d.vertex", @@ -318,6 +322,10 @@ namespace FRG renderer.shaderManager.AddShaderFromFiles("fog_shadow_inst", "resources/shaders/fog_shadow.vertex", "resources/shaders/fog_shadow_desktop.fragment", CONST_ZIP_FILE, "#define INSTANCED\n"); renderer.shaderManager.AddShaderFromFiles("night_fog_inst", "resources/shaders/night_fog.vertex", "resources/shaders/night_fog_desktop.fragment", CONST_ZIP_FILE, "#define INSTANCED\n"); renderer.shaderManager.AddShaderFromFiles("night_fog_shadow_inst", "resources/shaders/night_fog_shadow.vertex", "resources/shaders/night_fog_shadow_desktop.fragment", CONST_ZIP_FILE, "#define INSTANCED\n"); + renderer.shaderManager.AddShaderFromFiles("path_visualization", + "resources/shaders/path_visualization.vertex", + "resources/shaders/path_visualization_desktop.fragment", + CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("text2d", "resources/shaders/text2d.vertex", diff --git a/src/Location.cpp b/src/Location.cpp index 22ebd4d..0783de2 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -104,6 +104,7 @@ namespace FRG void Location::setup(const LocationSetup& params, Quest::QuestJournal* journal, AudioPlayerAsync* audioPlayer) { + pathVisualization.initialize(); // Load static game objects { @@ -708,6 +709,11 @@ namespace FRG renderer.shaderManager.PopShader(); } + void Location::setPathVisualizationEnabled(bool enabled) + { + pathVisualization.setEnabled(enabled); + } + void Location::drawGame() { @@ -766,6 +772,7 @@ namespace FRG drawNpcCar(); for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); + pathVisualization.draw(renderer); if (editorMode == EditorMode::Navigation) { editor.drawNavigation(); @@ -986,6 +993,7 @@ namespace FRG for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); #endif + pathVisualization.draw(renderer); if (editorMode == EditorMode::Navigation) { editor.drawNavigation(); @@ -1068,6 +1076,7 @@ namespace FRG //drawNpcCar(); for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); + pathVisualization.draw(renderer); if (editorMode == EditorMode::Navigation) { editor.drawNavigation(); @@ -1375,6 +1384,7 @@ namespace FRG drawNpcCar(); for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); + pathVisualization.draw(renderer); if (editorMode == EditorMode::Navigation) { editor.drawNavigation(); @@ -1669,6 +1679,8 @@ namespace FRG void Location::update(int64_t delta) { + pathVisualization.update(delta); + for (auto& intObj : interactiveObjects) { intObj.update(delta); } @@ -1986,6 +1998,10 @@ namespace FRG return; } + // Any new gameplay click invalidates the previous click feedback. Ground + // clicks and interactive-object clicks that require walking start a replacement. + pathVisualization.cancel(); + //logger() << "[CLICK] Camera position: (" << camPos.x() << ", " << camPos.y() << ", " << camPos.z() << ")" << std::endl; //logger() << "[CLICK] Ray direction: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl; @@ -2001,9 +2017,13 @@ namespace FRG state.targetInteractiveObjectIndex = findInteractiveObjectIndex(clickedObject); state.targetInteractNpcIndex = -1; state.targetTeleportZoneIndex = -1; - player->setTarget(clickedObject->state.hasInteractionPosition + const Eigen::Vector3f interactionTarget = clickedObject->state.hasInteractionPosition ? clickedObject->state.interactionPosition - : clickedObject->state.position); + : clickedObject->state.position; + player->setTarget(interactionTarget); + if (!player->state.pathWaypoints.empty()) { + pathVisualization.start(*player, interactionTarget); + } player->state.attackTargetIndex = CharacterState::kNoTarget; //logger() << "[CLICK] Player moving to object..." << std::endl; } @@ -2087,7 +2107,9 @@ namespace FRG player->setTarget(clickedTeleport->position); player->state.attackTargetIndex = CharacterState::kNoTarget; } else { - player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z())); + const Eigen::Vector3f clickedDestination(hit.x(), 0.f, hit.z()); + player->setTarget(clickedDestination); + pathVisualization.start(*player, clickedDestination); player->state.attackTargetIndex = CharacterState::kNoTarget; state.targetInteractNpcIndex = -1; state.targetTeleportZoneIndex = -1; diff --git a/src/Location.h b/src/Location.h index eed23c6..532524b 100644 --- a/src/Location.h +++ b/src/Location.h @@ -8,6 +8,7 @@ #include "items/InteractiveObject.h" #include "navigation/PathFinder.h" #include "render/ShadowMap.h" +#include "render/PathVisualization.h" #include "ScriptEngine.h" #include "dialogue/DialogueSystem.h" @@ -178,6 +179,10 @@ namespace FRG void update(int64_t deltaMs); void updateNpcCar(int64_t deltaMs); + // Purely visual feedback for click-triggered player routes. Disabling it + // also cancels an animation that is currently playing. + void setPathVisualizationEnabled(bool enabled); + bool isPathVisualizationEnabled() const { return pathVisualization.isEnabled(); } void handleDown(int64_t fingerId, int eventX, int eventY, int mx, int my); void handleUp(int64_t fingerId, int mx, int my); @@ -228,6 +233,8 @@ namespace FRG int findNpcIndex(const Character* npc) const; void fireBumpCallbacks(Character* mover, Character* standing); + PathVisualization pathVisualization; + std::unordered_map lastCharacterPositions; std::unordered_map replanCooldownRemainingMs; std::unordered_map stuckFrameCounts; diff --git a/src/render/PathVisualization.cpp b/src/render/PathVisualization.cpp new file mode 100644 index 0000000..6fbeff3 --- /dev/null +++ b/src/render/PathVisualization.cpp @@ -0,0 +1,224 @@ +#include "render/PathVisualization.h" + +#include "Character.h" + +#include +#include + +namespace FRG { + + namespace { + constexpr float kPi = 3.14159265358979323846f; + } + + void PathVisualization::initialize() + { + if (!dotMesh.data.PositionData.empty()) return; + + constexpr int kSphereSegments = 8; + constexpr int kSphereRings = 3; + constexpr float kSphereRadius = 0.06f; + VertexDataStruct dotData; + dotData.PositionData.reserve(kSphereSegments * (2 + (kSphereRings - 1) * 2) * 3); + const auto spherePoint = [=](float phi, float angle) { + const float ringRadius = std::sin(phi) * kSphereRadius; + return Eigen::Vector3f( + std::cos(angle) * ringRadius, + kSphereRadius + std::cos(phi) * kSphereRadius, + std::sin(angle) * ringRadius); + }; + const Eigen::Vector3f sphereTop(0.0f, kSphereRadius * 2.0f, 0.0f); + const Eigen::Vector3f sphereBottom(0.0f, 0.0f, 0.0f); + for (int i = 0; i < kSphereSegments; ++i) { + const float a0 = static_cast(i) * 2.0f * kPi / kSphereSegments; + const float a1 = static_cast(i + 1) * 2.0f * kPi / kSphereSegments; + const Eigen::Vector3f first0 = spherePoint(kPi / 4.0f, a0); + const Eigen::Vector3f first1 = spherePoint(kPi / 4.0f, a1); + dotData.PositionData.insert(dotData.PositionData.end(), { sphereTop, first1, first0 }); + + Eigen::Vector3f previous0 = first0; + Eigen::Vector3f previous1 = first1; + for (int ring = 2; ring <= kSphereRings; ++ring) { + const float phi = static_cast(ring) * kPi / 4.0f; + const Eigen::Vector3f current0 = spherePoint(phi, a0); + const Eigen::Vector3f current1 = spherePoint(phi, a1); + dotData.PositionData.insert(dotData.PositionData.end(), { + previous0, previous1, current1, + previous0, current1, current0 + }); + previous0 = current0; + previous1 = current1; + } + dotData.PositionData.insert(dotData.PositionData.end(), { sphereBottom, previous0, previous1 }); + } + dotMesh.AssignFrom(dotData); + + constexpr float kHalfWidth = 0.125f; + constexpr float kHeight = 0.40f; + const Eigen::Vector3f tip(0.0f, 0.0f, 0.0f); + const Eigen::Vector3f c0(-kHalfWidth, kHeight, -kHalfWidth); + const Eigen::Vector3f c1( kHalfWidth, kHeight, -kHalfWidth); + const Eigen::Vector3f c2( kHalfWidth, kHeight, kHalfWidth); + const Eigen::Vector3f c3(-kHalfWidth, kHeight, kHalfWidth); + VertexDataStruct destinationData; + destinationData.PositionData = { + tip, c1, c0, tip, c2, c1, tip, c3, c2, tip, c0, c3, + c0, c1, c2, c0, c2, c3 + }; + destinationMesh.AssignFrom(destinationData); + } + + void PathVisualization::setEnabled(bool value) + { + enabled = value; + if (!enabled) cancel(); + } + + void PathVisualization::cancel() + { + active = false; + elapsedMs = 0.0f; + durationMs = 0.0f; + dots.clear(); + } + + void PathVisualization::start(const Character& player, const Eigen::Vector3f& clickedDestination) + { + cancel(); + if (!enabled) return; + + std::vector route; + route.reserve(player.state.pathWaypoints.size() + 1); + route.push_back(Eigen::Vector3f(player.state.position.x(), 0.0f, player.state.position.z())); + const size_t firstWaypoint = (std::min)( + player.state.currentWaypointIndex, player.state.pathWaypoints.size()); + for (size_t i = firstWaypoint; i < player.state.pathWaypoints.size(); ++i) { + const Eigen::Vector3f& waypoint = player.state.pathWaypoints[i]; + route.push_back(Eigen::Vector3f(waypoint.x(), 0.0f, waypoint.z())); + } + + destination = route.size() > 1 + ? route.back() + : Eigen::Vector3f(clickedDestination.x(), 0.0f, clickedDestination.z()); + + float totalLength = 0.0f; + for (size_t i = 1; i < route.size(); ++i) { + totalLength += (route[i] - route[i - 1]).norm(); + } + + if (totalLength > 0.001f) { + constexpr size_t kMaxDots = 360; + const float spacing = (std::max)(0.24f, totalLength / static_cast(kMaxDots)); + constexpr float kMaximumRevealMs = 300.0f; + constexpr float kMaximumDotLifetimeMs = 580.0f; + constexpr float kPlayerClearanceMs = 20.0f; + constexpr float kPlayerMaximumSpeedMultiplier = 1.5f; + const float playerSpeed = (std::max)( + player.state.walkSpeed * kPlayerMaximumSpeedMultiplier, 0.01f); + const float playerRouteTimeMs = totalLength / playerSpeed * 1000.0f; + const float revealDurationMs = (std::min)(kMaximumRevealMs, playerRouteTimeMs / 3.0f); + const float waveSpeed = totalLength / (revealDurationMs / 1000.0f); + float traversed = 0.0f; + float nextDotDistance = spacing * 0.65f; + + for (size_t i = 1; i < route.size() && dots.size() < kMaxDots; ++i) { + const Eigen::Vector3f segment = route[i] - route[i - 1]; + const float segmentLength = segment.norm(); + if (segmentLength <= 0.001f) continue; + + while (nextDotDistance < totalLength - spacing * 0.25f && + nextDotDistance <= traversed + segmentLength && + dots.size() < kMaxDots) { + const float t = (nextDotDistance - traversed) / segmentLength; + Dot dot; + dot.position = route[i - 1] + segment * t; + dot.delayMs = nextDotDistance / waveSpeed * 1000.0f; + const float playerArrivalMs = nextDotDistance / playerSpeed * 1000.0f; + dot.lifetimeMs = (std::max)(0.0f, (std::min)( + kMaximumDotLifetimeMs, + playerArrivalMs - dot.delayMs - kPlayerClearanceMs)); + dots.push_back(dot); + nextDotDistance += spacing; + } + traversed += segmentLength; + } + } + + durationMs = 900.0f; + elapsedMs = 0.0f; + active = true; + } + + void PathVisualization::update(int64_t deltaMs) + { + if (!active) return; + elapsedMs += static_cast(deltaMs); + if (elapsedMs >= durationMs) cancel(); + } + + void PathVisualization::draw(Renderer& renderer) + { + if (!enabled || !active) return; + + const auto smoothStep = [](float t) { + t = std::clamp(t, 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); + }; + const auto dotAlpha = [&](float localAgeMs, float lifetimeMs) { + if (lifetimeMs <= 0.0f || localAgeMs < 0.0f || localAgeMs >= lifetimeMs) return 0.0f; + const float fadeInMs = (std::min)(55.0f, lifetimeMs * 0.35f); + if (localAgeMs < fadeInMs) return smoothStep(localAgeMs / fadeInMs); + return 1.0f - smoothStep((localAgeMs - fadeInMs) / (lifetimeMs - fadeInMs)); + }; + + GLboolean depthWriteWasEnabled = GL_TRUE; + glGetBooleanv(GL_DEPTH_WRITEMASK, &depthWriteWasEnabled); + const GLboolean blendWasEnabled = glIsEnabled(GL_BLEND); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); + + renderer.shaderManager.PushShader("path_visualization"); + renderer.SetMatrix(); + constexpr float kMaxOpacity = 0.45f; + const float green[3] = { 0.08f, 1.0f, 0.25f }; + + for (const Dot& dot : dots) { + const float localAge = elapsedMs - dot.delayMs; + const float alpha = dotAlpha(localAge, dot.lifetimeMs) * kMaxOpacity; + if (alpha <= 0.001f) continue; + + const float scale = 0.65f + 0.35f * smoothStep(localAge / 55.0f); + const float color[4] = { green[0], green[1], green[2], alpha }; + renderer.RenderUniform4fv("uColor", color); + renderer.PushMatrix(); + renderer.TranslateMatrix(dot.position + Eigen::Vector3f(0.0f, 0.025f, 0.0f)); + renderer.ScaleMatrix(scale); + renderer.DrawVertexRenderStruct(dotMesh); + renderer.PopMatrix(); + } + + constexpr float kMarkerFadeInMs = 70.0f; + constexpr float kMarkerFadeOutMs = 300.0f; + float markerAlpha = smoothStep(elapsedMs / kMarkerFadeInMs); + if (elapsedMs > durationMs - kMarkerFadeOutMs) { + markerAlpha *= 1.0f - smoothStep( + (elapsedMs - (durationMs - kMarkerFadeOutMs)) / kMarkerFadeOutMs); + } + if (markerAlpha > 0.001f) { + const float markerScale = 0.72f + 0.28f * smoothStep(elapsedMs / kMarkerFadeInMs); + const float color[4] = { green[0], green[1], green[2], markerAlpha * kMaxOpacity }; + renderer.RenderUniform4fv("uColor", color); + renderer.PushMatrix(); + renderer.TranslateMatrix(destination + Eigen::Vector3f(0.0f, 0.05f, 0.0f)); + renderer.ScaleMatrix(markerScale); + renderer.DrawVertexRenderStruct(destinationMesh); + renderer.PopMatrix(); + } + + renderer.shaderManager.PopShader(); + glDepthMask(depthWriteWasEnabled); + if (!blendWasEnabled) glDisable(GL_BLEND); + } + +} // namespace FRG diff --git a/src/render/PathVisualization.h b/src/render/PathVisualization.h new file mode 100644 index 0000000..77b753a --- /dev/null +++ b/src/render/PathVisualization.h @@ -0,0 +1,41 @@ +#pragma once + +#include "render/Renderer.h" + +#include +#include + +namespace FRG { + + class Character; + + class PathVisualization + { + public: + void initialize(); + void setEnabled(bool enabled); + bool isEnabled() const { return enabled; } + + void start(const Character& player, const Eigen::Vector3f& clickedDestination); + void cancel(); + void update(int64_t deltaMs); + void draw(Renderer& renderer); + + private: + struct Dot { + Eigen::Vector3f position = Eigen::Vector3f::Zero(); + float delayMs = 0.0f; + float lifetimeMs = 0.0f; + }; + + bool enabled = true; + bool active = false; + float elapsedMs = 0.0f; + float durationMs = 0.0f; + Eigen::Vector3f destination = Eigen::Vector3f::Zero(); + std::vector dots; + VertexRenderStruct dotMesh; + VertexRenderStruct destinationMesh; + }; + +} // namespace FRG