Merge remote-tracking branch 'origin/master' into thesis/2021/skybrowser

# Conflicts:
#	data/assets/util/webgui.asset
This commit is contained in:
sylvass
2022-03-02 16:41:11 -05:00
63 changed files with 1005 additions and 623 deletions

View File

@@ -1180,6 +1180,17 @@ void OpenSpaceEngine::preSynchronization() {
setCameraFromProfile(*global::profile);
setAdditionalScriptsFromProfile(*global::profile);
}
// Handle callback(s) for change in engine mode
if (_modeLastFrame != _currentMode) {
using K = CallbackHandle;
using V = ModeChangeCallback;
for (const std::pair<K, V>& it : _modeChangeCallbacks) {
it.second();
}
}
_modeLastFrame = _currentMode;
LTRACE("OpenSpaceEngine::preSynchronization(end)");
}
@@ -1617,6 +1628,31 @@ void OpenSpaceEngine::resetMode() {
LDEBUG(fmt::format("Reset engine mode to {}", stringify(_currentMode)));
}
OpenSpaceEngine::CallbackHandle OpenSpaceEngine::addModeChangeCallback(
ModeChangeCallback cb)
{
CallbackHandle handle = _nextCallbackHandle++;
_modeChangeCallbacks.emplace_back(handle, std::move(cb));
return handle;
}
void OpenSpaceEngine::removeModeChangeCallback(CallbackHandle handle) {
const auto it = std::find_if(
_modeChangeCallbacks.begin(),
_modeChangeCallbacks.end(),
[handle](const std::pair<CallbackHandle, ModeChangeCallback>& cb) {
return cb.first == handle;
}
);
ghoul_assert(
it != _modeChangeCallbacks.end(),
"handle must be a valid callback handle"
);
_modeChangeCallbacks.erase(it);
}
void setCameraFromProfile(const Profile& p) {
if (!p.camera.has_value()) {
throw ghoul::RuntimeError("No 'camera' entry exists in the startup profile");

View File

@@ -158,34 +158,6 @@ namespace {
"meters above the surface."
};
constexpr openspace::properties::Property::PropertyInfo VelocityZoomControlInfo = {
"VelocityZoomControl",
"Velocity Zoom Control",
"Controls the velocity of the camera motion when zooming in to the focus node "
"on a linear flight. The higher the value the faster the camera will move "
"towards the focus."
};
constexpr openspace::properties::Property::PropertyInfo ApplyLinearFlightInfo = {
"ApplyLinearFlight",
"Apply Linear Flight",
"This property makes the camera move to the specified distance "
"'DestinationDistance' while facing the anchor"
};
constexpr openspace::properties::Property::PropertyInfo FlightDestinationDistInfo = {
"FlightDestinationDistance",
"Flight Destination Distance",
"The final distance we want to fly to, with regards to the anchor node."
};
constexpr openspace::properties::Property::PropertyInfo FlightDestinationFactorInfo =
{
"FlightDestinationFactor",
"Flight Destination Factor",
"The minimal distance factor that we need to reach to end linear flight."
};
constexpr openspace::properties::Property::PropertyInfo
StereoInterpolationTimeInfo = {
"StereoInterpolationTime",
@@ -299,20 +271,6 @@ OrbitalNavigator::Friction::Friction()
addProperty(friction);
}
OrbitalNavigator::LinearFlight::LinearFlight()
: properties::PropertyOwner({ "LinearFlight" })
, apply(ApplyLinearFlightInfo, false)
, destinationDistance(FlightDestinationDistInfo, 2e8f, 10.f, 1e10f)
, destinationFactor(FlightDestinationFactorInfo, 1E-4, 1E-6, 0.5, 1E-3)
, velocitySensitivity(VelocityZoomControlInfo, 3.5f, 0.001f, 20.f)
{
addProperty(apply);
addProperty(velocitySensitivity);
destinationDistance.setExponent(5.f);
addProperty(destinationDistance);
addProperty(destinationFactor);
}
OrbitalNavigator::IdleBehavior::IdleBehavior()
: properties::PropertyOwner({ "IdleBehavior" })
, apply(ApplyIdleBehaviorInfo, false)
@@ -472,7 +430,6 @@ OrbitalNavigator::OrbitalNavigator()
});
addPropertySubOwner(_friction);
addPropertySubOwner(_linearFlight);
addPropertySubOwner(_idleBehavior);
_idleBehaviorDampenInterpolator.setTransferFunction(
@@ -593,37 +550,6 @@ void OrbitalNavigator::updateCameraStateFromStates(double deltaTime) {
_camera->rotationQuaternion()
};
if (_linearFlight.apply) {
// Calculate a position handle based on the camera position in world space
glm::dvec3 camPosToAnchorPosDiff = prevCameraPosition - anchorPos;
// Use the interaction sphere to get an approximate distance to the node surface
double nodeRadius = _anchorNode->interactionSphere();
double distFromCameraToFocus =
glm::distance(prevCameraPosition, anchorPos) - nodeRadius;
// Make the approximation delta size depending on the flight distance
double arrivalThreshold =
_linearFlight.destinationDistance * _linearFlight.destinationFactor;
const double distToDestination =
std::fabs(distFromCameraToFocus - _linearFlight.destinationDistance);
// Fly towards the flight destination distance. When getting closer than
// arrivalThreshold terminate the flight
if (distToDestination > arrivalThreshold) {
pose.position = moveCameraAlongVector(
pose.position,
distFromCameraToFocus,
camPosToAnchorPosDiff,
_linearFlight.destinationDistance,
deltaTime
);
}
else {
_linearFlight.apply = false;
}
}
const bool hasPreviousPositions =
_previousAnchorNodePosition.has_value() &&
_previousAimNodePosition.has_value();
@@ -1416,28 +1342,6 @@ glm::dvec3 OrbitalNavigator::translateHorizontally(double deltaTime,
return cameraPosition + rotationDiffVec3;
}
glm::dvec3 OrbitalNavigator::moveCameraAlongVector(const glm::dvec3& camPos,
double distFromCameraToFocus,
const glm::dvec3& camPosToAnchorPosDiff,
double destination,
double deltaTime) const
{
// This factor adapts the velocity so it slows down when getting closer
// to our final destination
double velocity = 0.0;
if (destination < distFromCameraToFocus) { // When flying towards anchor
velocity = 1.0 - destination / distFromCameraToFocus;
}
else { // When flying away from anchor
velocity = distFromCameraToFocus / destination - 1.0;
}
velocity *= _linearFlight.velocitySensitivity * deltaTime;
// Return the updated camera position
return camPos - velocity * camPosToAnchorPosDiff;
}
glm::dvec3 OrbitalNavigator::followAnchorNodeRotation(const glm::dvec3& cameraPosition,
const glm::dvec3& objectPosition,
const glm::dquat& focusNodeRotationDiff) const

View File

@@ -42,7 +42,7 @@
namespace {
constexpr const char _loggerCat[] = "Path";
constexpr const float Epsilon = 1e-5f;
constexpr const float LengthEpsilon = 1e-5f;
constexpr const char SunIdentifier[] = "Sun";
@@ -82,6 +82,15 @@ namespace {
// 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
@@ -108,6 +117,8 @@ Path::Path(Waypoint start, Waypoint end, Type type,
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;
@@ -119,11 +130,9 @@ Path::Path(Waypoint start, Waypoint end, Type type,
// We now know how long it took to traverse the path. Use that
_speedFactorFromDuration = _progressedTime / *duration;
}
// Reset playback variables
_traveledDistance = 0.0;
_progressedTime = 0.0;
resetPlaybackVariables();
}
}
Waypoint Path::startPoint() const { return _start; }
@@ -139,12 +148,49 @@ std::vector<glm::dvec3> Path::controlPoints() const {
CameraPose Path::traversePath(double dt, float speedScale) {
double speed = speedAlongPath(_traveledDistance);
speed *= static_cast<double>(speedScale);
const double displacement = dt * speed;
double displacement = dt * speed;
const double prevDistance = _traveledDistance;
_progressedTime += dt;
_traveledDistance += displacement;
return interpolatedPose(_traveledDistance);
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
const glm::dvec3 prevPosToEnd = _prevPose.position - _end.position();
const double remainingDistance = glm::length(prevPosToEnd);
// Actual displacement may not be bigger than remaining distance
if (displacement > remainingDistance) {
displacement = remainingDistance;
_traveledDistance = pathLength();
_shouldQuit = true;
return _end.pose();
}
// Just move along the line from the current position to the target
newPose.position = _prevPose.position -
displacement * glm::normalize(prevPosToEnd);
const double relativeDistance = _traveledDistance / pathLength();
newPose.rotation = interpolateRotation(relativeDistance);
}
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 {
@@ -153,9 +199,20 @@ std::string Path::currentAnchor() const {
}
bool Path::hasReachedEnd() const {
if (_shouldQuit) {
return true;
}
return (_traveledDistance / pathLength()) >= 1.0;
}
void Path::resetPlaybackVariables() {
_prevPose = _start.pose();
_traveledDistance = 0.0;
_progressedTime = 0.0;
_shouldQuit = false;
}
CameraPose Path::interpolatedPose(double distance) const {
const double relativeDistance = distance / pathLength();
CameraPose cs;
@@ -167,8 +224,9 @@ CameraPose Path::interpolatedPose(double distance) const {
glm::dquat Path::interpolateRotation(double t) const {
switch (_type) {
case Type::AvoidCollision:
case Type::Linear:
return easedSlerpRotation(t);
case Type::Linear:
return linearPathRotation(t);
case Type::ZoomOutOverview:
case Type::AvoidCollisionWithLookAt:
return lookAtTargetsRotation(t);
@@ -183,6 +241,48 @@ glm::dquat Path::easedSlerpRotation(double t) const {
return glm::slerp(_start.rotation(), _end.rotation(), tScaled);
}
glm::dquat Path::linearPathRotation(double t) const {
const double tHalf = 0.5;
const glm::dvec3 endNodePos = _end.node()->worldPosition();
const glm::dvec3 endUp = _end.rotation() * glm::dvec3(0.0, 1.0, 0.0);
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;
@@ -234,51 +334,55 @@ double Path::speedAlongPath(double traveledDistance) const {
const glm::dvec3 endNodePos = _end.node()->worldPosition();
const glm::dvec3 startNodePos = _start.node()->worldPosition();
const CameraPose prevPose = interpolatedPose(traveledDistance);
const double distanceToEndNode = glm::distance(prevPose.position, endNodePos);
const double distanceToStartNode = glm::distance(prevPose.position, startNodePos);
// 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;
// Decide which is the closest node
SceneGraphNode* closestNode = _start.node();
glm::dvec3 closestPos = startNodePos;
const glm::dvec3 closestPos = isCloserToEnd ? endNodePos : startNodePos;
const double distanceToClosestNode = glm::distance(closestPos, _prevPose.position);
if (distanceToEndNode < distanceToStartNode) {
closestPos = endNodePos;
closestNode = _end.node();
}
const double distanceToClosestNode = glm::distance(closestPos, prevPose.position);
double speed = distanceToClosestNode;
// Dampen speed in beginning of path
double startUpDistance = 2.0 * _start.node()->boundingSphere();
if (startUpDistance < Epsilon) { // zero bounding sphere
startUpDistance = glm::distance(_start.position(), startNodePos);
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();
if (pathLength() < startUpDistance + closeUpDistance) {
startUpDistance = 0.49 * pathLength(); // a little less than half
closeUpDistance = startUpDistance;
}
double dampeningFactor = 1.0;
if (traveledDistance < startUpDistance) {
speed *= traveledDistance / startUpDistance + 0.01;
dampeningFactor = traveledDistance / startUpDistance;
}
// Dampen speed in end of path
// Note: this leads to problems when the full length of the path is really big
double closeUpDistance = 2.0 * _end.node()->boundingSphere();
if (closeUpDistance < Epsilon) { // zero bounding sphere
closeUpDistance = glm::distance(_end.position(), endNodePos);
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;
}
}
if (traveledDistance > (pathLength() - closeUpDistance)) {
else if (traveledDistance > (pathLength() - closeUpDistance)) {
const double remainingDistance = pathLength() - traveledDistance;
speed *= remainingDistance / closeUpDistance + 0.01;
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
// TODO: check for when path is shorter than the starUpDistance or closeUpDistance
// variables
return _speedFactorFromDuration * speed;
return _speedFactorFromDuration * speed * dampeningFactor;
}
Waypoint waypointFromCamera() {
@@ -297,7 +401,7 @@ SceneGraphNode* findNodeNearTarget(const SceneGraphNode* node) {
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()) < Epsilon;
isSame |= glm::distance(n->worldPosition(), node->worldPosition()) < LengthEpsilon;
if (isSame) {
continue;
@@ -376,7 +480,9 @@ struct NodeInfo {
bool useTargetUpDirection;
};
Waypoint computeWaypointFromNodeInfo(const NodeInfo& info, const Waypoint& startPoint) {
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));
@@ -392,12 +498,23 @@ Waypoint computeWaypointFromNodeInfo(const NodeInfo& info, const Waypoint& start
);
}
else {
const double radius = Waypoint::findValidBoundingSphere(targetNode);
const double defaultHeight = 2.0 * radius;
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;
const glm::dvec3 stepDir = computeGoodStepDirection(targetNode, startPoint);
glm::dvec3 stepDir = glm::dvec3(0.0);
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;
}
@@ -416,18 +533,29 @@ Waypoint computeWaypointFromNodeInfo(const NodeInfo& info, const Waypoint& start
return Waypoint(targetPos, targetRot, info.identifier);
}
Path createPathFromDictionary(const ghoul::Dictionary& dictionary, Path::Type type) {
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();
// TODO: also handle curve type here
std::vector<Waypoint> waypoints;
switch (p.targetType) {
case Parameters::TargetType::NavigationState: {
@@ -462,7 +590,7 @@ Path createPathFromDictionary(const ghoul::Dictionary& dictionary, Path::Type ty
p.useTargetUpDirection.value_or(false)
};
waypoints = { computeWaypointFromNodeInfo(info, startPoint) };
waypoints = { computeWaypointFromNodeInfo(info, startPoint, type) };
break;
}
default: {
@@ -473,7 +601,36 @@ Path createPathFromDictionary(const ghoul::Dictionary& dictionary, Path::Type ty
// @TODO (emmbr) Allow for an instruction to represent a list of multiple waypoints
const Waypoint waypointToAdd = waypoints[0];
return Path(startPoint, waypointToAdd, type, duration);
if (glm::distance(startPoint.position(), waypointToAdd.position()) < LengthEpsilon) {
LINFO("Already at the requested target");
throw PathCurve::TooShortPathError("Path too short!");
}
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 e;
}
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

View File

@@ -35,11 +35,23 @@
#include <vector>
namespace {
constexpr const int NrSamplesPerSegment = 100;
constexpr const char* _loggerCat = "PathCurve";
constexpr const double LengthEpsilon =
100.0 * std::numeric_limits<double>::epsilon();
} // namespace
namespace openspace::interaction {
PathCurve::InsufficientPrecisionError::InsufficientPrecisionError(std::string error)
: ghoul::RuntimeError(std::move(error), "PathCurve")
{}
PathCurve::TooShortPathError::TooShortPathError(std::string error)
: ghoul::RuntimeError(std::move(error), "PathCurve")
{}
PathCurve::~PathCurve() {}
double PathCurve::length() const {
@@ -57,21 +69,19 @@ std::vector<glm::dvec3> PathCurve::points() const {
void PathCurve::initializeParameterData() {
_nSegments = static_cast<int>(_points.size() - 3);
ghoul_assert(_nSegments > 0, "Cannot have a curve with zero segments!");
_curveParameterSteps.clear();
_lengthSums.clear();
_parameterSamples.clear();
const double max = static_cast<double>(_nSegments);
// Evenly space out parameter intervals
_curveParameterSteps.reserve(_nSegments + 1);
const double dt = 1.0 / _nSegments;
_curveParameterSteps.push_back(0.0);
for (unsigned int i = 1; i < _nSegments; i++) {
_curveParameterSteps.push_back(dt * i);
for (int i = 0; i <= _nSegments; i++) {
_curveParameterSteps.push_back(static_cast<double>(i));
}
_curveParameterSteps.push_back(1.0);
// Arc lengths
_lengthSums.reserve(_nSegments + 1);
@@ -84,49 +94,82 @@ void PathCurve::initializeParameterData() {
}
_totalLength = _lengthSums.back();
if (_totalLength < LengthEpsilon) {
throw TooShortPathError("Path too short!");
}
// Compute a map of arc lengths s and curve parameters u, for reparameterization
_parameterSamples.reserve(NrSamplesPerSegment * _nSegments + 1);
const double uStep = 1.0 / (_nSegments * NrSamplesPerSegment);
constexpr const int Steps = 100;
const double uStep = 1.0 / static_cast<double>(Steps);
_parameterSamples.reserve(Steps * _nSegments + 1);
for (unsigned int i = 0; i < _nSegments; i++) {
double uStart = _curveParameterSteps[i];
double sStart = _lengthSums[i];
for (int j = 0; j < NrSamplesPerSegment; ++j) {
_parameterSamples.push_back({ uStart, sStart });
// Intermediate sampels
for (int j = 1; j < Steps; ++j) {
double u = uStart + j * uStep;
double s = sStart + arcLength(uStart, u);
// Identify samples that are indistinguishable due to precision limitations
if (std::abs(s - _parameterSamples.back().s) < LengthEpsilon) {
throw InsufficientPrecisionError("Insufficient precision due to path length");
}
_parameterSamples.push_back({ u, s });
}
}
_parameterSamples.push_back({ 1.0, _totalLength });
// Remove the very last sample if indistinguishable from the final one
const double diff = std::abs(_totalLength - _parameterSamples.back().s);
if (diff < LengthEpsilon) {
_parameterSamples.pop_back();
}
_parameterSamples.push_back({ max, _totalLength });
_parameterSamples.shrink_to_fit();
}
// Compute the curve parameter from an arc length value, using a combination of
// Newton's method and bisection. Source:
// https://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf
// Input s is a length value, in the range [0, _totalLength]
// Returns curve parameter in range [0, 1]
// Returns curve parameter in range [0, _nSegments]
double PathCurve::curveParameter(double s) const {
if (s <= 0.0) return 0.0;
if (s >= _totalLength) return 1.0;
if (s >= (_totalLength - LengthEpsilon)) return _curveParameterSteps.back();
unsigned int segmentIndex = 1;
while (s > _lengthSums[segmentIndex]) {
segmentIndex++;
}
const int startIndex = (segmentIndex - 1) * NrSamplesPerSegment;
const int endIndex = segmentIndex * NrSamplesPerSegment + 1;
const double segmentS = s - _lengthSums[segmentIndex - 1];
const double uMin = _curveParameterSteps[segmentIndex - 1];
const double uMax = _curveParameterSteps[segmentIndex];
// Find sample indices corresponding to the selected sample
auto findIndexOfFirstEqualOrLarger_u = [&samples = _parameterSamples](double value) {
auto it = std::lower_bound(
samples.begin(),
samples.end(),
ParameterPair{ value, 0.0 }, // 0.0 is a dummy value for s
[](const ParameterPair& lhs, const ParameterPair& rhs) {
return lhs.u < rhs.u;
}
);
return std::distance(samples.begin(), it);
};
size_t startIndex = findIndexOfFirstEqualOrLarger_u(uMin);
size_t endIndex = findIndexOfFirstEqualOrLarger_u(uMax);
// Use samples to find an initial guess for Newton's method
// Find first sample with s larger than input s
auto sampleIterator = std::upper_bound(
_parameterSamples.begin() + startIndex,
_parameterSamples.begin() + endIndex,
ParameterPair{ 0.0 , s }, // 0.0 is a dummy value for u
[](const ParameterPair& lhs,const ParameterPair& rhs) {
[](const ParameterPair& lhs, const ParameterPair& rhs) {
return lhs.s < rhs.s;
}
);
@@ -148,7 +191,7 @@ double PathCurve::curveParameter(double s) const {
double F = arcLength(uMin, u) - segmentS;
// The error we tolerate, in meters. Note that distances are very large
constexpr const double tolerance = 0.005;
constexpr const double tolerance = 0.5;
if (std::abs(F) <= tolerance) {
return u;
}
@@ -174,11 +217,12 @@ double PathCurve::curveParameter(double s) const {
}
double PathCurve::approximatedDerivative(double u, double h) const {
const double max = _curveParameterSteps.back();
if (u <= h) {
return (1.0 / h) * glm::length(interpolate(0.0 + h) - interpolate(0.0));
}
if (u >= 1.0 - h) {
return (1.0 / h) * glm::length(interpolate(1.0) - interpolate(1.0 - h));
if (u >= max - h) {
return (1.0 / h) * glm::length(interpolate(max) - interpolate(max - h));
}
return (0.5 / h) * glm::length(interpolate(u + h) - interpolate(u - h));
}
@@ -196,12 +240,13 @@ double PathCurve::arcLength(double lowerLimit, double upperLimit) const {
}
glm::dvec3 PathCurve::interpolate(double u) const {
ghoul_assert(u >= 0 && u <= 1.0, "Interpolation variable must be in range [0,1]");
const double max = _curveParameterSteps.back();
ghoul_assert(u >= 0 && u <= max, "Interpolation variable must be in range [0,_nSegments]");
if (u < 0.0) {
if (u <= 0.0) {
return _points[1];
}
if (u > 1.0) {
if (u >= max) {
return *(_points.end() - 2);
}
@@ -226,11 +271,26 @@ glm::dvec3 PathCurve::interpolate(double u) const {
}
LinearCurve::LinearCurve(const Waypoint& start, const Waypoint& end) {
_points.push_back(start.position());
_points.push_back(start.position());
_points.push_back(end.position());
_points.push_back(end.position());
initializeParameterData();
_totalLength = glm::distance(start.position(), end.position());
_nSegments = 1;
}
glm::dvec3 LinearCurve::positionAt(double relativeDistance) const {
return interpolate(relativeDistance);
}
glm::dvec3 LinearCurve::interpolate(double u) const {
if (u <= 0.0) {
return _points.front();
}
if (u >= 1.0) {
return _points.back();
}
return ghoul::interpolateLinear(u, _points.front(), _points.back());
}
} // namespace openspace::interaction

View File

@@ -70,10 +70,18 @@ AvoidCollisionCurve::AvoidCollisionCurve(const Waypoint& start, const Waypoint&
// Add an extra point to first go backwards if starting close to planet
glm::dvec3 nodeToStart = start.position() - startNodeCenter;
double distanceToStartNode = glm::length(nodeToStart);
const double distanceToStartNode = glm::length(nodeToStart);
if (distanceToStartNode < CloseToNodeThresholdRadiusMultiplier * startNodeRadius) {
double distance = startNodeRadius;
// Note that the factor 2.0 is arbitrarily chosen to look ok.
// @TODO: (2022-02-27, emmbr) Should be unified to a "getting close to object sphere"
// that can be used in multiple cases when creating paths more cleverly later on
const double closeToNodeThresholdFactor = glm::max(
CloseToNodeThresholdRadiusMultiplier,
2.0 * global::navigationHandler->pathNavigator().arrivalDistanceFactor()
);
if (distanceToStartNode < closeToNodeThresholdFactor * startNodeRadius) {
const double distance = startNodeRadius;
glm::dvec3 newPos = start.position() + distance * glm::normalize(nodeToStart);
_points.push_back(newPos);
}
@@ -104,8 +112,8 @@ AvoidCollisionCurve::AvoidCollisionCurve(const Waypoint& start, const Waypoint&
const glm::dvec3 nodeToEnd = end.position() - endNodeCenter;
const double distanceToEndNode = glm::length(nodeToEnd);
if (distanceToEndNode < CloseToNodeThresholdRadiusMultiplier * endNodeRadius) {
double distance = endNodeRadius;
if (distanceToEndNode < closeToNodeThresholdFactor * endNodeRadius) {
const double distance = endNodeRadius;
glm::dvec3 newPos = end.position() + distance * glm::normalize(nodeToEnd);
_points.push_back(newPos);
}

View File

@@ -45,10 +45,9 @@ namespace {
constexpr openspace::properties::Property::PropertyInfo DefaultCurveOptionInfo = {
"DefaultPathType",
"Default Path Type",
"The defualt path type chosen when generating a path. See wiki for alternatives."
" The shape of the generated path will be different depending on the path type."
// TODO (2021-08-15, emmbr) right now there is no way to specify a type for a
// single path instance, only for any created paths
"The default path type chosen when generating a path or flying to a target. "
"See wiki for alternatives. The shape of the generated path will be different "
"depending on the path type."
};
constexpr openspace::properties::Property::PropertyInfo IncludeRollInfo = {
@@ -72,6 +71,15 @@ namespace {
"triggered once the path has reached its target."
};
constexpr openspace::properties::Property::PropertyInfo ArrivalDistanceFactorInfo = {
"ArrivalDistanceFactor",
"Arrival Distance Factor",
"A factor used to compute the default distance from a target scene graph node "
"when creating a camera path. The factor will be multipled with the node's "
"bounding sphere to compute the target height from the bounding sphere of the "
"object."
};
constexpr const openspace::properties::Property::PropertyInfo MinBoundingSphereInfo =
{
"MinimalValidBoundingSphere",
@@ -102,13 +110,14 @@ PathNavigator::PathNavigator()
, _includeRoll(IncludeRollInfo, false)
, _speedScale(SpeedScaleInfo, 1.f, 0.01f, 2.f)
, _applyIdleBehaviorOnFinish(IdleBehaviorOnFinishInfo, false)
, _arrivalDistanceFactor(ArrivalDistanceFactorInfo, 2.0, 0.1, 20.0)
, _minValidBoundingSphere(MinBoundingSphereInfo, 10.0, 1.0, 3e10)
, _relevantNodeTags(RelevantNodeTagsInfo)
{
_defaultPathType.addOptions({
{ Path::Type::AvoidCollision, "AvoidCollision" },
{ Path::Type::Linear, "Linear" },
{ Path::Type::ZoomOutOverview, "ZoomOutOverview"},
{ Path::Type::Linear, "Linear" },
{ Path::Type::AvoidCollisionWithLookAt, "AvoidCollisionWithLookAt"}
});
addProperty(_defaultPathType);
@@ -116,6 +125,7 @@ PathNavigator::PathNavigator()
addProperty(_includeRoll);
addProperty(_speedScale);
addProperty(_applyIdleBehaviorOnFinish);
addProperty(_arrivalDistanceFactor);
addProperty(_minValidBoundingSphere);
_relevantNodeTags = std::vector<std::string>{
@@ -144,6 +154,10 @@ double PathNavigator::speedScale() const {
return _speedScale;
}
double PathNavigator::arrivalDistanceFactor() const {
return _arrivalDistanceFactor;
}
bool PathNavigator::hasCurrentPath() const {
return _currentPath != nullptr;
}
@@ -191,7 +205,7 @@ void PathNavigator::updateCamera(double deltaTime) {
camera()->setPose(newPose);
if (_currentPath->hasReachedEnd()) {
LINFO("Reached end of path");
LINFO("Reached target");
handlePathEnd();
if (_applyIdleBehaviorOnFinish) {
@@ -211,10 +225,6 @@ void PathNavigator::updateCamera(double deltaTime) {
}
void PathNavigator::createPath(const ghoul::Dictionary& dictionary) {
// @TODO (2021-08.16, emmbr): Improve how curve types are handled.
// We want the user to be able to choose easily
const int pathType = _defaultPathType;
OpenSpaceEngine::Mode m = global::openSpaceEngine->currentMode();
if (m == OpenSpaceEngine::Mode::SessionRecordingPlayback) {
// Silently ignore any paths that are being created during a session recording
@@ -225,9 +235,7 @@ void PathNavigator::createPath(const ghoul::Dictionary& dictionary) {
clearPath();
try {
_currentPath = std::make_unique<Path>(
createPathFromDictionary(dictionary, Path::Type(pathType))
);
_currentPath = std::make_unique<Path>(createPathFromDictionary(dictionary));
}
catch (const documentation::SpecificationError& e) {
LERROR("Could not create camera path");
@@ -238,16 +246,21 @@ void PathNavigator::createPath(const ghoul::Dictionary& dictionary) {
LWARNINGC(w.offender, ghoul::to_string(w.reason));
}
}
catch (const PathCurve::TooShortPathError&) {
// Do nothing
}
catch (const ghoul::RuntimeError& e) {
LERROR(fmt::format("Could not create path. Reason: ", e.message));
return;
}
LINFO("Successfully generated camera path");
LDEBUG("Successfully generated camera path");
}
void PathNavigator::clearPath() {
LDEBUG("Clearing path");
if (_currentPath) {
LDEBUG("Clearing path");
}
_currentPath = nullptr;
}
@@ -272,16 +285,14 @@ void PathNavigator::startPath() {
);
_startSimulationTimeOnFinish = true;
LINFO(
"Pausing time simulation during path traversal. "
"Will unpause once the camera path is finished"
);
LINFO("Pausing time simulation during path traversal");
}
LINFO("Starting path");
_isPlaying = true;
global::navigationHandler->orbitalNavigator().updateOnCameraInteraction();
global::navigationHandler->orbitalNavigator().resetVelocities();
}
void PathNavigator::abortPath() {
@@ -323,10 +334,58 @@ void PathNavigator::continuePath() {
_isPlaying = true;
}
Path::Type PathNavigator::defaultPathType() const {
const int pathType = _defaultPathType;
return Path::Type(pathType);
}
double PathNavigator::minValidBoundingSphere() const {
return _minValidBoundingSphere;
}
double PathNavigator::findValidBoundingSphere(const SceneGraphNode* node) const {
ghoul_assert(node != nullptr, "Node must not be nulltpr");
auto sphere = [](const SceneGraphNode* n) {
// Use the biggest of the bounding sphere and interaction sphere,
// so we don't accidentally choose a bounding sphere that is much smaller
// than the interaction sphere of the node
double bs = n->boundingSphere();
double is = n->interactionSphere();
return is > bs ? is : bs;
};
double result = sphere(node);
if (result < _minValidBoundingSphere) {
// If the bs of the target is too small, try to find a good value in a child node.
// Only check the closest children, to avoid deep traversal in the scene graph.
// Alsp. the chance to find a bounding sphere that represents the visual size of
// the target well is higher for these nodes
for (SceneGraphNode* child : node->children()) {
result = sphere(child);
if (result > _minValidBoundingSphere) {
LDEBUG(fmt::format(
"The scene graph node '{}' has no, or a very small, bounding sphere. "
"Using bounding sphere of child node '{}' in computations.",
node->identifier(),
child->identifier()
));
return result;
}
}
LDEBUG(fmt::format(
"The scene graph node '{}' has no, or a very small, bounding sphere. Using "
"minimal value. This might lead to unexpected results.",
node->identifier())
);
result = _minValidBoundingSphere;
}
return result;
}
const std::vector<SceneGraphNode*>& PathNavigator::relevantNodes() {
if (!_hasInitializedRelevantNodes) {
findRelevantNodes();
@@ -346,7 +405,6 @@ void PathNavigator::handlePathEnd() {
);
}
_startSimulationTimeOnFinish = false;
clearPath();
global::openSpaceEngine->resetMode();
}
@@ -438,9 +496,9 @@ scripting::LuaLibrary PathNavigator::luaLibrary() {
&luascriptfunctions::flyTo,
"string [, bool, double]",
"Move the camera to the node with the specified identifier. The optional "
"double specifies the duration of the motion. If the optional bool is "
"set to true the target up vector for camera is set based on the target "
"node. Either of the optional parameters can be left out."
"double specifies the duration of the motion, in seconds. If the optional "
"bool is set to true the target up vector for camera is set based on the "
"target node. Either of the optional parameters can be left out."
},
{
"flyToHeight",
@@ -449,18 +507,48 @@ scripting::LuaLibrary PathNavigator::luaLibrary() {
"Move the camera to the node with the specified identifier. The second "
"argument is the desired target height above the target node's bounding "
"sphere, in meters. The optional double specifies the duration of the "
"motion. If the optional bool is set to true, the target up vector for "
"camera is set based on the target node. Either of the optional "
"parameters can be left out."
"motion, in seconds. If the optional bool is set to true, the target "
"up vector for camera is set based on the target node. Either of the "
"optional parameters can be left out."
},
{
{
"flyToNavigationState",
&luascriptfunctions::flyToNavigationState,
"table, [double]",
"Create a path to the navigation state described by the input table. "
"The optional double specifies the target duration of the motion. Note "
"that roll must be included for the target up direction to be taken "
"into account."
"The optional double specifies the target duration of the motion, "
"in seconds. Note that roll must be included for the target up "
"direction to be taken into account."
},
{
"zoomToFocus",
&luascriptfunctions::zoomToFocus,
"[duration]",
"Zoom linearly to the current focus node, using the default distance."
"The optional input parameter specifies the duration for the motion, "
"in seconds."
},
{
"zoomToDistance",
&luascriptfunctions::zoomToDistance,
"distance, [duration]",
"Fly linearly to a specific distance in relation to the focus node. "
"The distance is given in meters above the bounding sphere of the "
"current focus node."
"The optional input parameter specifies the duration for the motion, "
"in seconds."
},
{
"zoomToDistanceRelative",
&luascriptfunctions::zoomToDistanceRelative,
"distance, [duration]",
"Fly linearly to a specific distance in relation to the focus node. "
"The distance is given as a multiple of the bounding sphere of the "
"current focus node. That is, a value of 1 will result in a position "
"at a distance of one times the size of the bounding sphere away from "
"the object."
"The optional input parameter specifies the duration for the motion, "
"in seconds."
},
{
"createPath",

View File

@@ -37,10 +37,6 @@
#include <ghoul/logging/logmanager.h>
#include <glm/gtx/vector_angle.hpp>
namespace {
constexpr const double Epsilon = 1e-5;
} // namespace
namespace openspace::luascriptfunctions {
int isFlying(lua_State* L) {
@@ -81,7 +77,6 @@ int flyTo(lua_State* L) {
return ghoul::lua::luaError(L, "Duration cannot be specified twice");
}
if (!sceneGraphNode(nodeIdentifier)) {
return ghoul::lua::luaError(L, "Unknown node name: " + nodeIdentifier);
}
@@ -98,7 +93,7 @@ int flyTo(lua_State* L) {
}
else {
double d = std::get<double>(*useUpFromTargetOrDuration);
if (d <= Epsilon) {
if (d <= 0.0) {
return ghoul::lua::luaError(L, "Duration must be larger than zero");
}
insDict.setValue("Duration", d);
@@ -106,7 +101,7 @@ int flyTo(lua_State* L) {
}
if (duration.has_value()) {
double d = *duration;
if (d <= Epsilon) {
if (d <= 0.0) {
return ghoul::lua::luaError(L, "Duration must be larger than zero");
}
insDict.setValue("Duration", d);
@@ -146,7 +141,7 @@ int flyToHeight(lua_State* L) {
}
else {
double d = std::get<double>(*useUpFromTargetOrDuration);
if (d <= Epsilon) {
if (d <= 0.0) {
return ghoul::lua::luaError(L, "Duration must be larger than zero");
}
insDict.setValue("Duration", d);
@@ -154,7 +149,7 @@ int flyToHeight(lua_State* L) {
}
if (duration.has_value()) {
double d = *duration;
if (d <= Epsilon) {
if (d <= 0.0) {
return ghoul::lua::luaError(L, "Duration must be larger than zero");
}
insDict.setValue("Duration", d);
@@ -194,7 +189,7 @@ int flyToNavigationState(lua_State* L) {
if (duration.has_value()) {
double d = *duration;
if (d <= Epsilon) {
if (d <= 0.0) {
return ghoul::lua::luaError(L, "Duration must be larger than zero");
}
instruction.setValue("Duration", d);
@@ -208,6 +203,113 @@ int flyToNavigationState(lua_State* L) {
return 0;
}
int zoomToFocus(lua_State* L) {
ghoul::lua::checkArgumentsAndThrow(L, { 0, 1 }, "lua::zoomToFocus");
std::optional<double> duration = ghoul::lua::value<std::optional<double>>(L);
const SceneGraphNode* node = global::navigationHandler->anchorNode();
if (!node) {
return ghoul::lua::luaError(L, "Could not determine current focus node");
}
ghoul::Dictionary insDict;
insDict.setValue("TargetType", std::string("Node"));
insDict.setValue("Target", node->identifier());
insDict.setValue("PathType", std::string("Linear"));
if (duration.has_value()) {
double d = *duration;
if (d <= 0.0) {
return ghoul::lua::luaError(L, "Duration must be larger than zero");
}
insDict.setValue("Duration", d);
}
global::navigationHandler->pathNavigator().createPath(insDict);
if (global::navigationHandler->pathNavigator().hasCurrentPath()) {
global::navigationHandler->pathNavigator().startPath();
}
return 0;
}
int zoomToDistance(lua_State* L) {
ghoul::lua::checkArgumentsAndThrow(L, { 1, 2 }, "lua::zoomToDistance");
auto [distance, duration] =
ghoul::lua::values<double, std::optional<double>>(L);
if (distance <= 0.0) {
return ghoul::lua::luaError(L, "The distance must be larger than zero");
}
const SceneGraphNode* node = global::navigationHandler->anchorNode();
if (!node) {
return ghoul::lua::luaError(L, "Could not determine current focus node");
}
ghoul::Dictionary insDict;
insDict.setValue("TargetType", std::string("Node"));
insDict.setValue("Target", node->identifier());
insDict.setValue("Height", distance);
insDict.setValue("PathType", std::string("Linear"));
if (duration.has_value()) {
double d = *duration;
if (d <= 0.0) {
return ghoul::lua::luaError(L, "Duration must be larger than zero");
}
insDict.setValue("Duration", d);
}
global::navigationHandler->pathNavigator().createPath(insDict);
if (global::navigationHandler->pathNavigator().hasCurrentPath()) {
global::navigationHandler->pathNavigator().startPath();
}
return 0;
}
int zoomToDistanceRelative(lua_State* L) {
ghoul::lua::checkArgumentsAndThrow(L, { 1, 2 }, "lua::zoomToDistanceRelative");
auto [distance, duration] =
ghoul::lua::values<double, std::optional<double>>(L);
if (distance <= 0.0) {
return ghoul::lua::luaError(L, "The distance must be larger than zero");
}
const SceneGraphNode* node = global::navigationHandler->anchorNode();
if (!node) {
return ghoul::lua::luaError(L, "Could not determine current focus node");
}
distance *= node->boundingSphere();
ghoul::Dictionary insDict;
insDict.setValue("TargetType", std::string("Node"));
insDict.setValue("Target", node->identifier());
insDict.setValue("Height", distance);
insDict.setValue("PathType", std::string("Linear"));
if (duration.has_value()) {
double d = *duration;
if (d <= 0.0) {
return ghoul::lua::luaError(L, "Duration must be larger than zero");
}
insDict.setValue("Duration", d);
}
global::navigationHandler->pathNavigator().createPath(insDict);
if (global::navigationHandler->pathNavigator().hasCurrentPath()) {
global::navigationHandler->pathNavigator().startPath();
}
return 0;
}
int createPath(lua_State* L) {
ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::createPath");
ghoul::Dictionary dictionary = ghoul::lua::value<ghoul::Dictionary>(L);

View File

@@ -49,7 +49,9 @@ Waypoint::Waypoint(const glm::dvec3& pos, const glm::dquat& rot, const std::stri
LERROR(fmt::format("Could not find node '{}'", _nodeIdentifier));
return;
}
_validBoundingSphere = findValidBoundingSphere(node);
const PathNavigator& navigator = global::navigationHandler->pathNavigator();
_validBoundingSphere = navigator.findValidBoundingSphere(node);
}
Waypoint::Waypoint(const NavigationState& ns) {
@@ -61,7 +63,8 @@ Waypoint::Waypoint(const NavigationState& ns) {
}
_nodeIdentifier = ns.anchor;
_validBoundingSphere = findValidBoundingSphere(anchorNode);
const PathNavigator& navigator = global::navigationHandler->pathNavigator();
_validBoundingSphere = navigator.findValidBoundingSphere(anchorNode);
_pose = ns.cameraPose();
}
@@ -89,41 +92,4 @@ double Waypoint::validBoundingSphere() const {
return _validBoundingSphere;
}
double Waypoint::findValidBoundingSphere(const SceneGraphNode* node) {
double bs = node->boundingSphere();
const double minValidBoundingSphere =
global::navigationHandler->pathNavigator().minValidBoundingSphere();
if (bs < minValidBoundingSphere) {
// If the bs of the target is too small, try to find a good value in a child node.
// Only check the closest children, to avoid deep traversal in the scene graph.
// Alsp. the chance to find a bounding sphere that represents the visual size of
// the target well is higher for these nodes
for (SceneGraphNode* child : node->children()) {
bs = child->boundingSphere();
if (bs > minValidBoundingSphere) {
LWARNING(fmt::format(
"The scene graph node '{}' has no, or a very small, bounding sphere. "
"Using bounding sphere of child node '{}' in computations.",
node->identifier(),
child->identifier()
));
return bs;
}
}
LWARNING(fmt::format(
"The scene graph node '{}' has no, or a very small, bounding sphere. Using "
"minimal value. This might lead to unexpected results.",
node->identifier())
);
bs = minValidBoundingSphere;
}
return bs;
}
} // namespace openspace::interaction

View File

@@ -100,6 +100,13 @@ void MultiThreadedSceneInitializer::initializeNode(SceneGraphNode* node) {
}
std::vector<SceneGraphNode*> MultiThreadedSceneInitializer::takeInitializedNodes() {
// Some of the scene graph nodes might still be in the initialization queue and we
// should wait for those to finish or we end up in some half-initialized state since
// other parts of the application already know about their existence
while (_threadPool.hasOutstandingTasks()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
std::lock_guard g(_mutex);
std::vector<SceneGraphNode*> nodes = std::move(_initializedNodes);
return nodes;

View File

@@ -99,4 +99,8 @@ void ThreadPool::clearTasks() {
} // release lock
}
bool ThreadPool::hasOutstandingTasks() const {
return !tasks.empty();
}
} // namespace openspace