mirror of
https://github.com/OpenSpace/OpenSpace.git
synced 2026-01-09 21:21:19 -06:00
It still happened every now and then that the rotation didn't quite finish at the end of the path
735 lines
29 KiB
C++
735 lines
29 KiB
C++
/*****************************************************************************************
|
|
* *
|
|
* OpenSpace *
|
|
* *
|
|
* Copyright (c) 2014-2022 *
|
|
* *
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
|
|
* software and associated documentation files (the "Software"), to deal in the Software *
|
|
* without restriction, including without limitation the rights to use, copy, modify, *
|
|
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
|
|
* permit persons to whom the Software is furnished to do so, subject to the following *
|
|
* conditions: *
|
|
* *
|
|
* The above copyright notice and this permission notice shall be included in all copies *
|
|
* or substantial portions of the Software. *
|
|
* *
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
|
|
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
|
|
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
|
|
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
|
|
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
|
|
****************************************************************************************/
|
|
|
|
#include <openspace/navigation/path.h>
|
|
|
|
#include <openspace/camera/camera.h>
|
|
#include <openspace/camera/camerapose.h>
|
|
#include <openspace/engine/globals.h>
|
|
#include <openspace/engine/moduleengine.h>
|
|
#include <openspace/navigation/navigationhandler.h>
|
|
#include <openspace/navigation/pathcurve.h>
|
|
#include <openspace/navigation/pathcurves/avoidcollisioncurve.h>
|
|
#include <openspace/navigation/pathcurves/zoomoutoverviewcurve.h>
|
|
#include <openspace/navigation/pathnavigator.h>
|
|
#include <openspace/rendering/renderable.h>
|
|
#include <openspace/scene/scenegraphnode.h>
|
|
#include <openspace/query/query.h>
|
|
#include <openspace/util/collisionhelper.h>
|
|
#include <openspace/util/universalhelpers.h>
|
|
#include <ghoul/logging/logmanager.h>
|
|
#include <ghoul/misc/interpolator.h>
|
|
#include <glm/ext/quaternion_relational.hpp>
|
|
|
|
namespace {
|
|
constexpr const char _loggerCat[] = "Path";
|
|
constexpr const float LengthEpsilon = 1e-5f;
|
|
|
|
constexpr const char SunIdentifier[] = "Sun";
|
|
|
|
// TODO: where should this documentation be?
|
|
// It's nice to have these to interpret the dictionary when creating the path, but
|
|
// maybe it's not really necessary
|
|
struct [[codegen::Dictionary(PathInstruction)]] Parameters {
|
|
enum class TargetType {
|
|
Node,
|
|
NavigationState
|
|
};
|
|
TargetType targetType;
|
|
|
|
// The desired duration traversing the specified path segment should take
|
|
std::optional<float> duration;
|
|
|
|
// (Node): The target node of the camera path. Not optional for 'Node'
|
|
// instructions
|
|
std::optional<std::string> target;
|
|
|
|
// (Node): An optional position in relation to the target node, in model
|
|
// coordinates (meters)
|
|
std::optional<glm::dvec3> position;
|
|
|
|
// (Node): An optional height in relation to the target node, in meters
|
|
std::optional<double> height;
|
|
|
|
// (Node): If true, the up direction of the node is taken into account when
|
|
// computing the wayopoint for this instruction
|
|
std::optional<bool> useTargetUpDirection;
|
|
|
|
// (NavigationState): A navigation state that will be the target
|
|
// of this path segment
|
|
std::optional<ghoul::Dictionary> navigationState
|
|
[[codegen::reference("core_navigation_state")]];
|
|
|
|
// A navigation state that determines the start state for the camera path
|
|
std::optional<ghoul::Dictionary> startState
|
|
[[codegen::reference("core_navigation_state")]];
|
|
|
|
enum class [[codegen::map(openspace::interaction::Path::Type)]] PathType {
|
|
AvoidCollision,
|
|
ZoomOutOverview,
|
|
Linear,
|
|
AvoidCollisionWithLookAt
|
|
};
|
|
// The type of the created path. Affects the shape of the resulting path
|
|
std::optional<PathType> pathType;
|
|
};
|
|
#include "path_codegen.cpp"
|
|
} // namespace
|
|
|
|
namespace openspace::interaction {
|
|
|
|
Path::Path(Waypoint start, Waypoint end, Type type,
|
|
std::optional<double> duration)
|
|
: _start(start), _end(end), _type(type)
|
|
{
|
|
switch (_type) {
|
|
case Type::AvoidCollision:
|
|
case Type::AvoidCollisionWithLookAt:
|
|
_curve = std::make_unique<AvoidCollisionCurve>(_start, _end);
|
|
break;
|
|
case Type::Linear:
|
|
_curve = std::make_unique<LinearCurve>(_start, _end);
|
|
break;
|
|
case Type::ZoomOutOverview:
|
|
_curve = std::make_unique<ZoomOutOverviewCurve>(_start, _end);
|
|
break;
|
|
default:
|
|
LERROR("Could not create curve. Type does not exist!");
|
|
throw ghoul::MissingCaseException();
|
|
}
|
|
|
|
_prevPose = _start.pose();
|
|
|
|
// Compute speed factor to match any given duration, by traversing the path and
|
|
// computing how much faster/slower it should be
|
|
_speedFactorFromDuration = 1.0;
|
|
if (duration.has_value()) {
|
|
constexpr const double dt = 0.05; // 20 fps
|
|
while (!hasReachedEnd()) {
|
|
traversePath(dt);
|
|
}
|
|
|
|
// We now know how long it took to traverse the path. Use that
|
|
_speedFactorFromDuration = _progressedTime / *duration;
|
|
resetPlaybackVariables();
|
|
}
|
|
}
|
|
|
|
Waypoint Path::startPoint() const { return _start; }
|
|
|
|
Waypoint Path::endPoint() const { return _end; }
|
|
|
|
double Path::pathLength() const { return _curve->length(); }
|
|
|
|
std::vector<glm::dvec3> Path::controlPoints() const {
|
|
return _curve->points();
|
|
}
|
|
|
|
CameraPose Path::traversePath(double dt, float speedScale) {
|
|
double speed = speedAlongPath(_traveledDistance);
|
|
speed *= static_cast<double>(speedScale);
|
|
double displacement = dt * speed;
|
|
|
|
const double prevDistance = _traveledDistance;
|
|
|
|
_progressedTime += dt;
|
|
_traveledDistance += displacement;
|
|
|
|
CameraPose newPose;
|
|
|
|
if (_type == Type::Linear) {
|
|
// Special handling of linear paths, so that it can be used when we are
|
|
// traversing very large distances without introducing precision problems
|
|
newPose = linearInterpolatedPose(_traveledDistance, displacement);
|
|
}
|
|
else {
|
|
if (std::abs(prevDistance - _traveledDistance) < LengthEpsilon) {
|
|
// The distaces are too large, so we are not making progress because of
|
|
// insufficient precision
|
|
_shouldQuit = true;
|
|
LWARNING("Quit camera path prematurely due to insufficient precision");
|
|
}
|
|
|
|
newPose = interpolatedPose(_traveledDistance);
|
|
}
|
|
|
|
_prevPose = newPose;
|
|
return newPose;
|
|
}
|
|
|
|
std::string Path::currentAnchor() const {
|
|
bool pastHalfway = (_traveledDistance / pathLength()) > 0.5;
|
|
return (pastHalfway) ? _end.nodeIdentifier() : _start.nodeIdentifier();
|
|
}
|
|
|
|
bool Path::hasReachedEnd() const {
|
|
if (_shouldQuit) {
|
|
return true;
|
|
}
|
|
|
|
bool isPositionFinished = (_traveledDistance / pathLength()) >= 1.0;
|
|
|
|
constexpr const double RotationEpsilon = 0.0001;
|
|
bool isRotationFinished = ghoul::isSameOrientation(
|
|
_prevPose.rotation,
|
|
_end.rotation(),
|
|
RotationEpsilon
|
|
);
|
|
|
|
return isPositionFinished && isRotationFinished;
|
|
}
|
|
|
|
void Path::resetPlaybackVariables() {
|
|
_prevPose = _start.pose();
|
|
_traveledDistance = 0.0;
|
|
_progressedTime = 0.0;
|
|
_shouldQuit = false;
|
|
}
|
|
|
|
CameraPose Path::linearInterpolatedPose(double distance, double displacement) {
|
|
ghoul_assert(_type == Type::Linear, "Path type must be linear");
|
|
const double relativeDistance = distance / pathLength();
|
|
const glm::dvec3 prevPosToEnd = _prevPose.position - _end.position();
|
|
const double remainingDistance = glm::length(prevPosToEnd);
|
|
CameraPose pose;
|
|
|
|
// Actual displacement may not be bigger than remaining distance
|
|
if (displacement > remainingDistance) {
|
|
_traveledDistance = pathLength();
|
|
pose.position = _end.position();
|
|
}
|
|
else {
|
|
// Just move along line from the current position to the target
|
|
const glm::dvec3 lineDir = glm::normalize(prevPosToEnd);
|
|
pose.position = _prevPose.position - displacement * lineDir;
|
|
}
|
|
|
|
pose.rotation = linearPathRotation(relativeDistance);
|
|
return pose;
|
|
}
|
|
|
|
CameraPose Path::interpolatedPose(double distance) const {
|
|
const double relativeDistance = distance / pathLength();
|
|
CameraPose cs;
|
|
cs.position = _curve->positionAt(relativeDistance);
|
|
cs.rotation = interpolateRotation(relativeDistance);
|
|
return cs;
|
|
}
|
|
|
|
glm::dquat Path::interpolateRotation(double t) const {
|
|
switch (_type) {
|
|
case Type::AvoidCollision:
|
|
return easedSlerpRotation(t);
|
|
case Type::Linear:
|
|
// @TODO (2022-03-29, emmbr) Fix so that rendering the rotation of linear
|
|
// paths works again. I.e. openspace.debugging.renderCameraPath
|
|
return linearPathRotation(t);
|
|
case Type::ZoomOutOverview:
|
|
case Type::AvoidCollisionWithLookAt:
|
|
return lookAtTargetsRotation(t);
|
|
default:
|
|
throw ghoul::MissingCaseException();
|
|
}
|
|
}
|
|
|
|
glm::dquat Path::easedSlerpRotation(double t) const {
|
|
double tScaled = helpers::shiftAndScale(t, 0.1, 0.9);
|
|
tScaled = ghoul::sineEaseInOut(tScaled);
|
|
return glm::slerp(_start.rotation(), _end.rotation(), tScaled);
|
|
}
|
|
|
|
glm::dquat Path::linearPathRotation(double t) const {
|
|
const glm::dvec3 a = ghoul::viewDirection(_start.rotation());
|
|
const glm::dvec3 b = ghoul::viewDirection(_end.rotation());
|
|
const double angle = std::acos(glm::dot(a, b)); // assumes length 1.0 for a & b
|
|
|
|
// Seconds per pi angles. Per default, it takes 5 seconds to turn 90 degrees
|
|
double factor = 5.0 / glm::half_pi<double>();
|
|
factor *= global::navigationHandler->pathNavigator().linearRotationSpeedFactor();
|
|
|
|
double turnDuration = std::max(angle * factor, 1.0); // Always at least 1 second
|
|
const double time = glm::clamp(_progressedTime / turnDuration, 0.0, 1.0);
|
|
return easedSlerpRotation(time);
|
|
|
|
// @TODO (2022-03-18, emmbr) Leaving this for now, as something similar might have to
|
|
// be implemented for navigation states. But should be removed/reimplemented
|
|
|
|
//const glm::dvec3 endNodePos = _end.node()->worldPosition();
|
|
//const glm::dvec3 endUp = _end.rotation() * glm::dvec3(0.0, 1.0, 0.0);
|
|
|
|
//const double tHalf = 0.5;
|
|
//if (t < tHalf) {
|
|
// // Interpolate to look at target
|
|
// const glm::dvec3 halfWayPosition = _curve->positionAt(tHalf);
|
|
// const glm::dquat q = ghoul::lookAtQuaternion(halfWayPosition, endNodePos, endUp);
|
|
|
|
// const double tScaled = ghoul::sineEaseInOut(t / tHalf);
|
|
// return glm::slerp(_start.rotation(), q, tScaled);
|
|
//}
|
|
|
|
//// This distance is guaranteed to be strictly decreasing for linear paths
|
|
//const double distanceToEnd = glm::distance(_prevPose.position, _end.position());
|
|
|
|
//// Determine the distance at which to start interpolating to the target rotation.
|
|
//// The magic numbers here are just randomly picked constants, set to make the
|
|
//// resulting rotation look ok-ish
|
|
//double closingUpDistance = 10.0 * _end.validBoundingSphere();
|
|
//if (pathLength() < 2.0 * closingUpDistance) {
|
|
// closingUpDistance = 0.2 * pathLength();
|
|
//}
|
|
|
|
//if (distanceToEnd < closingUpDistance) {
|
|
// // Interpolate to target rotation
|
|
// const double tScaled = ghoul::sineEaseInOut(1.0 - distanceToEnd / closingUpDistance);
|
|
|
|
// // Compute a position in front of the camera at the end orientation
|
|
// const double inFrontDistance = glm::distance(_end.position(), endNodePos);
|
|
// const glm::dvec3 viewDir = ghoul::viewDirection(_end.rotation());
|
|
// const glm::dvec3 inFrontOfEnd = _end.position() + inFrontDistance * viewDir;
|
|
// const glm::dvec3 lookAtPos = ghoul::interpolateLinear(tScaled, endNodePos, inFrontOfEnd);
|
|
// return ghoul::lookAtQuaternion(_prevPose.position, lookAtPos, endUp);
|
|
//}
|
|
|
|
//// Keep looking at the end node
|
|
//return ghoul::lookAtQuaternion(_prevPose.position, endNodePos, endUp);
|
|
}
|
|
|
|
glm::dquat Path::lookAtTargetsRotation(double t) const {
|
|
const double t1 = 0.2;
|
|
const double t2 = 0.8;
|
|
|
|
const glm::dvec3 startPos = _curve->positionAt(0.0);
|
|
const glm::dvec3 endPos = _curve->positionAt(1.0);
|
|
const glm::dvec3 startNodePos = _start.node()->worldPosition();
|
|
const glm::dvec3 endNodePos = _end.node()->worldPosition();
|
|
|
|
glm::dvec3 lookAtPos;
|
|
if (t < t1) {
|
|
// Compute a position in front of the camera at the start orientation
|
|
const double inFrontDistance = glm::distance(startPos, startNodePos);
|
|
const glm::dvec3 viewDir = ghoul::viewDirection(_start.rotation());
|
|
const glm::dvec3 inFrontOfStart = startPos + inFrontDistance * viewDir;
|
|
|
|
const double tScaled = ghoul::cubicEaseInOut(t / t1);
|
|
lookAtPos =
|
|
ghoul::interpolateLinear(tScaled, inFrontOfStart, startNodePos);
|
|
}
|
|
else if (t <= t2) {
|
|
const double tScaled = ghoul::cubicEaseInOut((t - t1) / (t2 - t1));
|
|
lookAtPos = ghoul::interpolateLinear(tScaled, startNodePos, endNodePos);
|
|
}
|
|
else {
|
|
// (t > t2)
|
|
// Compute a position in front of the camera at the end orientation
|
|
const double inFrontDistance = glm::distance(endPos, endNodePos);
|
|
const glm::dvec3 viewDir = ghoul::viewDirection(_end.rotation());
|
|
const glm::dvec3 inFrontOfEnd = endPos + inFrontDistance * viewDir;
|
|
|
|
const double tScaled = ghoul::cubicEaseInOut((t - t2) / (1.0 - t2));
|
|
lookAtPos = ghoul::interpolateLinear(tScaled, endNodePos, inFrontOfEnd);
|
|
}
|
|
|
|
// Handle up vector separately
|
|
// @TODO (2021-09-06 emmbr) This actually does not interpolate the up vector of the
|
|
// camera, but just the "hint" up vector for the lookAt. This leads to fast rolling
|
|
// when the up vector gets close to the camera's forward vector. Should be improved
|
|
// so any rolling is spread out over the entire motion instead
|
|
double tUp = ghoul::sineEaseInOut(t);
|
|
glm::dvec3 startUp = _start.rotation() * glm::dvec3(0.0, 1.0, 0.0);
|
|
glm::dvec3 endUp = _end.rotation() * glm::dvec3(0.0, 1.0, 0.0);
|
|
glm::dvec3 up = ghoul::interpolateLinear(tUp, startUp, endUp);
|
|
|
|
return ghoul::lookAtQuaternion(_curve->positionAt(t), lookAtPos, up);
|
|
}
|
|
|
|
double Path::speedAlongPath(double traveledDistance) const {
|
|
const glm::dvec3 endNodePos = _end.node()->worldPosition();
|
|
const glm::dvec3 startNodePos = _start.node()->worldPosition();
|
|
|
|
// Set speed based on distance to closest node
|
|
const double distanceToEndNode = glm::distance(_prevPose.position, endNodePos);
|
|
const double distanceToStartNode = glm::distance(_prevPose.position, startNodePos);
|
|
bool isCloserToEnd = distanceToEndNode < distanceToStartNode;
|
|
|
|
const glm::dvec3 closestPos = isCloserToEnd ? endNodePos : startNodePos;
|
|
const double distanceToClosestNode = glm::distance(closestPos, _prevPose.position);
|
|
|
|
const double speed = distanceToClosestNode;
|
|
|
|
// Dampen at the start and end
|
|
constexpr const double DampenDistanceFactor = 3.0;
|
|
double startUpDistance = DampenDistanceFactor * _start.validBoundingSphere();
|
|
double closeUpDistance = DampenDistanceFactor * _end.validBoundingSphere();
|
|
|
|
// Kind of an ugly workaround to make damping behave over very long paths, and/or
|
|
// where the target nodes might have large bounding spheres. The current max is set
|
|
// based on the order of magnitude of the solar system, which ofc is very specific to
|
|
// our space content...
|
|
// @TODO (2022-03-22, emmbr) Come up with a better more general solution
|
|
constexpr const double MaxDistance = 1E12;
|
|
startUpDistance = glm::min(MaxDistance, startUpDistance);
|
|
closeUpDistance = glm::min(MaxDistance, closeUpDistance);
|
|
|
|
if (pathLength() < startUpDistance + closeUpDistance) {
|
|
startUpDistance = 0.4 * pathLength(); // a little less than half
|
|
closeUpDistance = startUpDistance;
|
|
}
|
|
|
|
double dampeningFactor = 1.0;
|
|
if (traveledDistance < startUpDistance) {
|
|
dampeningFactor = traveledDistance / startUpDistance;
|
|
}
|
|
else if (_type == Type::Linear) {
|
|
// Dampen at end of linear path is handled separately, as we can use the
|
|
// current position to scompute the remaining distance rather than the
|
|
// path length minus travels distance. This is more suitable for long paths
|
|
const double remainingDistance = glm::distance(
|
|
_prevPose.position,
|
|
_end.position()
|
|
);
|
|
if (remainingDistance < closeUpDistance) {
|
|
dampeningFactor = remainingDistance / closeUpDistance;
|
|
}
|
|
}
|
|
else if (traveledDistance > (pathLength() - closeUpDistance)) {
|
|
const double remainingDistance = pathLength() - traveledDistance;
|
|
dampeningFactor = remainingDistance / closeUpDistance;
|
|
}
|
|
dampeningFactor = ghoul::sineEaseOut(dampeningFactor);
|
|
|
|
// Prevent multiplying with 0 (and hence a speed of 0.0 => no movement)
|
|
dampeningFactor += 0.01;
|
|
|
|
// TODO: also dampen speed based on curvature, or make sure the curve has a rounder
|
|
// shape
|
|
|
|
return _speedFactorFromDuration * speed * dampeningFactor;
|
|
}
|
|
|
|
Waypoint waypointFromCamera() {
|
|
Camera* camera = global::navigationHandler->camera();
|
|
const glm::dvec3 pos = camera->positionVec3();
|
|
const glm::dquat rot = camera->rotationQuaternion();
|
|
const std::string node = global::navigationHandler->anchorNode()->identifier();
|
|
return Waypoint{ pos, rot, node };
|
|
}
|
|
|
|
SceneGraphNode* findNodeNearTarget(const SceneGraphNode* node) {
|
|
const std::vector<SceneGraphNode*>& relevantNodes =
|
|
global::navigationHandler->pathNavigator().relevantNodes();
|
|
|
|
for (SceneGraphNode* n : relevantNodes) {
|
|
bool isSame = (n->identifier() == node->identifier());
|
|
// If the nodes are in the very same position, they are probably representing
|
|
// the same object
|
|
isSame |=
|
|
glm::distance(n->worldPosition(), node->worldPosition()) < LengthEpsilon;
|
|
|
|
if (isSame) {
|
|
continue;
|
|
}
|
|
|
|
constexpr const float proximityRadiusFactor = 3.f;
|
|
|
|
const float bs = static_cast<float>(n->boundingSphere());
|
|
const float proximityRadius = proximityRadiusFactor * bs;
|
|
const glm::dvec3 posInModelCoords =
|
|
glm::inverse(n->modelTransform()) * glm::dvec4(node->worldPosition(), 1.0);
|
|
|
|
bool isClose = collision::isPointInsideSphere(
|
|
posInModelCoords,
|
|
glm::dvec3(0.0, 0.0, 0.0),
|
|
proximityRadius
|
|
);
|
|
|
|
if (isClose) {
|
|
return n;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// Compute a target position close to the specified target node, using knowledge of
|
|
// the start point and a desired distance from the node's center
|
|
glm::dvec3 computeGoodStepDirection(const SceneGraphNode* targetNode,
|
|
const Waypoint& startPoint)
|
|
{
|
|
const glm::dvec3 nodePos = targetNode->worldPosition();
|
|
const SceneGraphNode* closeNode = findNodeNearTarget(targetNode);
|
|
const SceneGraphNode* sun = sceneGraphNode(SunIdentifier);
|
|
|
|
// @TODO (2021-07-09, emmbr): Not nice to depend on a specific scene graph node,
|
|
// as it might not exist. Ideally, each SGN could know about their preferred
|
|
// direction to be viewed from (their "good side"), and then that could be queried
|
|
// and used instead.
|
|
if (closeNode) {
|
|
// If the node is close to another node in the scene, set the direction in a way
|
|
// that minimizes risk of collision
|
|
return glm::normalize(nodePos - closeNode->worldPosition());
|
|
}
|
|
else if (!sun) {
|
|
// Can't compute position from Sun position, so just use any direction. Z will do
|
|
return glm::dvec3(0.0, 0.0, 1.0);
|
|
}
|
|
else if (targetNode->identifier() == SunIdentifier) {
|
|
// Special case for when the target is the Sun, in which we want to avoid a zero
|
|
// vector. The Z axis is chosen to provide an overview of the solar system, and
|
|
// not stay in the orbital plane
|
|
return glm::dvec3(0.0, 0.0, 1.0);
|
|
}
|
|
else {
|
|
// Go to a point that is lit up by the sun, slightly offsetted from sun direction
|
|
const glm::dvec3 sunPos = sun->worldPosition();
|
|
|
|
const glm::dvec3 prevPos = startPoint.position();
|
|
const glm::dvec3 targetToPrev = prevPos - nodePos;
|
|
const glm::dvec3 targetToSun = sunPos - nodePos;
|
|
|
|
// Check against zero vectors, as this will lead to nan-values from cross product
|
|
if (glm::length(targetToSun) < LengthEpsilon ||
|
|
glm::length(targetToPrev) < LengthEpsilon)
|
|
{
|
|
// Same situation as if sun does not exist. Any direction will do
|
|
return glm::dvec3(0.0, 0.0, 1.0);
|
|
}
|
|
|
|
constexpr const float defaultPositionOffsetAngle = -30.f; // degrees
|
|
constexpr const float angle = glm::radians(defaultPositionOffsetAngle);
|
|
const glm::dvec3 axis = glm::normalize(glm::cross(targetToPrev, targetToSun));
|
|
const glm::dquat offsetRotation = angleAxis(static_cast<double>(angle), axis);
|
|
|
|
return glm::normalize(offsetRotation * targetToSun);
|
|
}
|
|
}
|
|
|
|
struct NodeInfo {
|
|
std::string identifier;
|
|
std::optional<glm::dvec3> position;
|
|
std::optional<double> height;
|
|
bool useTargetUpDirection;
|
|
};
|
|
|
|
Waypoint computeWaypointFromNodeInfo(const NodeInfo& info, const Waypoint& startPoint,
|
|
Path::Type type)
|
|
{
|
|
const SceneGraphNode* targetNode = sceneGraphNode(info.identifier);
|
|
if (!targetNode) {
|
|
LERROR(fmt::format("Could not find target node '{}'", info.identifier));
|
|
return Waypoint();
|
|
}
|
|
|
|
glm::dvec3 targetPos;
|
|
if (info.position.has_value()) {
|
|
// The position in instruction is given in the targetNode's local coordinates.
|
|
// Convert to world coordinates
|
|
targetPos = glm::dvec3(
|
|
targetNode->modelTransform() * glm::dvec4(*info.position, 1.0)
|
|
);
|
|
}
|
|
else {
|
|
const PathNavigator& navigator = global::navigationHandler->pathNavigator();
|
|
|
|
const double radius = navigator.findValidBoundingSphere(targetNode);
|
|
const double defaultHeight = radius * navigator.arrivalDistanceFactor();
|
|
const double height = info.height.value_or(defaultHeight);
|
|
const double distanceFromNodeCenter = radius + height;
|
|
|
|
glm::dvec3 stepDir;
|
|
if (type == Path::Type::Linear) {
|
|
// If linear path, compute position along line form start to end point
|
|
glm::dvec3 endNodePos = targetNode->worldPosition();
|
|
stepDir = glm::normalize(startPoint.position() - endNodePos);
|
|
}
|
|
else {
|
|
stepDir = computeGoodStepDirection(targetNode, startPoint);
|
|
}
|
|
|
|
targetPos = targetNode->worldPosition() + stepDir * distanceFromNodeCenter;
|
|
}
|
|
|
|
glm::dvec3 up = global::navigationHandler->camera()->lookUpVectorWorldSpace();
|
|
if (info.useTargetUpDirection) {
|
|
// @TODO (emmbr 2020-11-17) For now, this is hardcoded to look good for Earth,
|
|
// which is where it matters the most. A better solution would be to make each
|
|
// sgn aware of its own 'up' and query
|
|
up = targetNode->worldRotationMatrix() * glm::dvec3(0.0, 0.0, 1.0);
|
|
}
|
|
|
|
// Compute rotation so the camera is looking at the targetted node
|
|
const glm::dvec3 lookAtPos = targetNode->worldPosition();
|
|
const glm::dquat targetRot = ghoul::lookAtQuaternion(targetPos, lookAtPos, up);
|
|
|
|
return Waypoint(targetPos, targetRot, info.identifier);
|
|
}
|
|
|
|
void checkVisibilityAndShowMessage(const SceneGraphNode* node) {
|
|
auto isEnabled = [](const Renderable* r) {
|
|
std::any propertyValueAny = r->property("Enabled")->get();
|
|
return std::any_cast<bool>(propertyValueAny);
|
|
};
|
|
|
|
// Show some info related to the visiblity of the object
|
|
const Renderable* renderable = node->renderable();
|
|
if (!renderable) {
|
|
// Check if any of the children are visible, if it has children
|
|
bool foundVisible = false;
|
|
for (const SceneGraphNode* child : node->children()) {
|
|
const Renderable* cr = child->renderable();
|
|
if (cr && isEnabled(cr)) {
|
|
foundVisible = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundVisible) {
|
|
LINFO("Creating path to a node without a renderable or visible child nodes");
|
|
}
|
|
}
|
|
else if (!isEnabled(renderable)) {
|
|
LINFO("Creating path to disabled object");
|
|
}
|
|
}
|
|
|
|
Path createPathFromDictionary(const ghoul::Dictionary& dictionary,
|
|
std::optional<Path::Type> forceType)
|
|
{
|
|
const Parameters p = codegen::bake<Parameters>(dictionary);
|
|
|
|
const std::optional<float> duration = p.duration;
|
|
|
|
Path::Type type;
|
|
if (forceType.has_value()) {
|
|
type = *forceType;
|
|
}
|
|
else if (p.pathType.has_value()) {
|
|
type = codegen::map<Path::Type>(*p.pathType);
|
|
}
|
|
else {
|
|
type = global::navigationHandler->pathNavigator().defaultPathType();
|
|
}
|
|
|
|
bool hasStart = p.startState.has_value();
|
|
const Waypoint startPoint = hasStart ?
|
|
Waypoint(NavigationState(p.startState.value())) :
|
|
waypointFromCamera();
|
|
|
|
std::vector<Waypoint> waypoints;
|
|
switch (p.targetType) {
|
|
case Parameters::TargetType::NavigationState: {
|
|
if (!p.navigationState.has_value()) {
|
|
throw ghoul::RuntimeError("A navigation state is required");
|
|
}
|
|
|
|
const NavigationState navigationState =
|
|
NavigationState(p.navigationState.value());
|
|
|
|
waypoints = { Waypoint(navigationState) };
|
|
break;
|
|
}
|
|
case Parameters::TargetType::Node: {
|
|
if (!p.target.has_value()) {
|
|
throw ghoul::RuntimeError("A target node is required");
|
|
}
|
|
|
|
const std::string nodeIdentifier = p.target.value();
|
|
const SceneGraphNode* targetNode = sceneGraphNode(nodeIdentifier);
|
|
|
|
if (!targetNode) {
|
|
throw ghoul::RuntimeError(fmt::format(
|
|
"Could not find target node '{}'", nodeIdentifier
|
|
));
|
|
}
|
|
|
|
NodeInfo info {
|
|
nodeIdentifier,
|
|
p.position,
|
|
p.height,
|
|
p.useTargetUpDirection.value_or(false)
|
|
};
|
|
|
|
double startToTargetCenterDistance = glm::distance(
|
|
startPoint.position(),
|
|
targetNode->worldPosition()
|
|
);
|
|
|
|
// Use a linear path if camera start is within the bounding sphere
|
|
const PathNavigator& navigator = global::navigationHandler->pathNavigator();
|
|
const double bs = navigator.findValidBoundingSphere(targetNode);
|
|
bool withinTargetBoundingSphere = startToTargetCenterDistance < bs;
|
|
if (withinTargetBoundingSphere) {
|
|
LINFO(
|
|
"Camera is within the bounding sphere of the target node. "
|
|
"Using linear path"
|
|
);
|
|
type = Path::Type::Linear;
|
|
}
|
|
|
|
waypoints = { computeWaypointFromNodeInfo(info, startPoint, type) };
|
|
break;
|
|
}
|
|
default: {
|
|
throw ghoul::MissingCaseException();
|
|
}
|
|
}
|
|
|
|
// @TODO (emmbr) Allow for an instruction to represent a list of multiple waypoints
|
|
const Waypoint waypointToAdd = waypoints[0];
|
|
|
|
if (glm::distance(startPoint.position(), waypointToAdd.position()) < LengthEpsilon) {
|
|
LINFO("Already at the requested target");
|
|
throw PathCurve::TooShortPathError("Path too short!");
|
|
}
|
|
|
|
checkVisibilityAndShowMessage(waypointToAdd.node());
|
|
|
|
try {
|
|
return Path(startPoint, waypointToAdd, type, duration);
|
|
}
|
|
catch (const PathCurve::TooShortPathError& e) {
|
|
LINFO("Already at the requested target");
|
|
// Rethrow e, so the pathnavigator can handle it as well
|
|
throw;
|
|
}
|
|
catch (const PathCurve::InsufficientPrecisionError&) {
|
|
// There wasn't enough precision to represent the full curve in world
|
|
// coordinates. For now, use a linear path instead. It uses another
|
|
// method of interpolation that isn't as sensitive to these kinds of problems
|
|
|
|
LINFO(
|
|
"Switching to a linear path, to avoid problems with precision due to "
|
|
"immense path length."
|
|
);
|
|
|
|
return createPathFromDictionary(dictionary, Path::Type::Linear);
|
|
|
|
// @TODO (emmbr26, 2022-02-15): later on we want to improve this case, so that
|
|
// interpolation is not a problem. One suggestion to solve this would be using
|
|
// two identical paths and switch the direction of interpolation halfway through.
|
|
// That should give us sufficient precision at both ends of the path
|
|
}
|
|
}
|
|
|
|
} // namespace openspace::interaction
|