Working on path visualization

This commit is contained in:
Vladislav Khorev committed 2026-10-27 20:04:18 +06:00
1 parent 96e748715b
commit 0240721d46
12 files changed
+335 -3

No files matched your search

+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
@@ -0,0 +1,8 @@
attribute vec3 vPosition;
uniform mat4 ProjectionModelViewMatrix;
void main()
{
gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0);
}
@@ -0,0 +1,6 @@
uniform vec4 uColor;
void main()
{
gl_FragColor = uColor;
}
@@ -0,0 +1,8 @@
precision mediump float;
uniform vec4 uColor;
void main()
{
gl_FragColor = uColor;
}
+8
View File
@@ -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",
+25 -3
View File
@@ -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;
+7
View File
@@ -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<Character*, Eigen::Vector3f> lastCharacterPositions;
std::unordered_map<Character*, int64_t> replanCooldownRemainingMs;
std::unordered_map<Character*, int> stuckFrameCounts;
+224
View File
@@ -0,0 +1,224 @@
#include "render/PathVisualization.h"
#include "Character.h"
#include <algorithm>
#include <cmath>
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<float>(i) * 2.0f * kPi / kSphereSegments;
const float a1 = static_cast<float>(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<float>(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<Eigen::Vector3f> 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<float>(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<float>(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
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include "render/Renderer.h"
#include <cstdint>
#include <vector>
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<Dot> dots;
VertexRenderStruct dotMesh;
VertexRenderStruct destinationMesh;
};
} // namespace FRG