// observer.cpp // // Copyright (C) 2001-2008, Chris Laurel // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. #include #include #include "observer.h" #include "simulation.h" #include "frametree.h" static const double maximumSimTime = 730486721060.00073; // 2000000000 Jan 01 12:00:00 UTC static const double minimumSimTime = -730498278941.99951; // -2000000000 Jan 01 12:00:00 UTC using namespace std; #define VELOCITY_CHANGE_TIME 0.25f static Vec3d slerp(double t, const Vec3d& v0, const Vec3d& v1) { double r0 = v0.length(); double r1 = v1.length(); Vec3d u = v0 / r0; Vec3d n = u ^ (v1 / r1); n.normalize(); Vec3d v = n ^ u; if (v * v1 < 0.0) v = -v; double cosTheta = u * (v1 / r1); double theta = acos(cosTheta); return (cos(theta * t) * u + sin(theta * t) * v) * Mathd::lerp(t, r0, r1); } /*! Notes on the Observer class * The values position and orientation are in observer's reference frame. positionUniv * and orientationUniv are the equivalent values in the universal coordinate system. * They must be kept in sync. Generally, it's position and orientation that are modified; * after they're changed, the method updateUniversal is called. However, when the observer * frame is changed, positionUniv and orientationUniv are not changed, but the position * and orientation within the frame /do/ change. Thus, a 'reverse' update is necessary. * * There are two types of 'automatic' updates to position and orientation that may * occur when the observer's update method is called: updates from free travel, and * updates due to an active goto operation. */ Observer::Observer() : simTime(0.0), position(0.0, 0.0, 0.0), orientation(1.0), velocity(0.0, 0.0, 0.0), angularVelocity(0.0, 0.0, 0.0), frame(NULL), realTime(0.0), targetSpeed(0.0), targetVelocity(0.0, 0.0, 0.0), initialVelocity(0.0, 0.0, 0.0), beginAccelTime(0.0), observerMode(Free), trackingOrientation(1.0f, 0.0f, 0.0f, 0.0f), fov((float) (PI / 4.0)), reverseFlag(false), locationFilter(~0u) { frame = new ObserverFrame(); updateUniversal(); } /*! Copy constructor. */ Observer::Observer(const Observer& o) : simTime(o.simTime), position(o.position), orientation(o.orientation), velocity(o.velocity), angularVelocity(o.angularVelocity), frame(NULL), realTime(o.realTime), targetSpeed(o.targetSpeed), targetVelocity(o.targetVelocity), beginAccelTime(o.beginAccelTime), observerMode(o.observerMode), journey(o.journey), trackObject(o.trackObject), trackingOrientation(o.trackingOrientation), fov(o.fov), reverseFlag(o.reverseFlag), locationFilter(o.locationFilter), displayedSurface(o.displayedSurface) { frame = new ObserverFrame(*o.frame); updateUniversal(); } Observer& Observer::operator=(const Observer& o) { simTime = o.simTime; position = o.position; orientation = o.orientation; velocity = o.velocity; angularVelocity = o.angularVelocity; frame = NULL; realTime = o.realTime; targetSpeed = o.targetSpeed; targetVelocity = o.targetVelocity; beginAccelTime = o.beginAccelTime; observerMode = o.observerMode; journey = o.journey; trackObject = o.trackObject; trackingOrientation = o.trackingOrientation; fov = o.fov; reverseFlag = o.reverseFlag; locationFilter = o.locationFilter; displayedSurface = o.displayedSurface; setFrame(*o.frame); updateUniversal(); return *this; } /*! Get the current simulation time. The time returned is a Julian date, * and the time standard is TDB. */ double Observer::getTime() const { return simTime; }; /*! Get the current real time. The time returned is a Julian date, * and the time standard is TDB. */ double Observer::getRealTime() const { return realTime; }; /*! Set the simulation time (Julian date, TDB time standard) */ void Observer::setTime(double jd) { simTime = jd; updateUniversal(); } /*! Return the position of the observer in universal coordinates. The origin * The origin of this coordinate system is the Solar System Barycenter, and * axes are defined by the J2000 ecliptic and equinox. */ UniversalCoord Observer::getPosition() const { return positionUniv; } // TODO: Low-precision set position that should be removed void Observer::setPosition(const Point3d& p) { setPosition(UniversalCoord(p)); } /*! Set the position of the observer; position is specified in the universal * coordinate system. */ void Observer::setPosition(const UniversalCoord& p) { positionUniv = p; position = frame->convertFromUniversal(p, getTime()); } /*! Return the orientation of the observer in the universal coordinate * system. */ Quatd Observer::getOrientation() const { return orientationUniv; } /*! Reduced precision version of getOrientation() */ Quatf Observer::getOrientationf() const { Quatd q = getOrientation(); return Quatf((float) q.w, (float) q.x, (float) q.y, (float) q.z); } /* Set the orientation of the observer. The orientation is specified in * the universal coordinate system. */ void Observer::setOrientation(const Quatf& q) { /* RigidTransform rt = frame.toUniversal(situation, getTime()); rt.rotation = Quatd(q.w, q.x, q.y, q.z); situation = frame.fromUniversal(rt, getTime()); */ setOrientation(Quatd(q.w, q.x, q.y, q.z)); } /*! Set the orientation of the observer. The orientation is specified in * the universal coordinate system. */ void Observer::setOrientation(const Quatd& q) { orientationUniv = q; orientation = frame->convertFromUniversal(q, getTime()); } /*! Get the velocity of the observer within the observer's reference frame. */ Vec3d Observer::getVelocity() const { return velocity; } /*! Set the velocity of the observer within the observer's reference frame. */ void Observer::setVelocity(const Vec3d& v) { velocity = v; } Vec3d Observer::getAngularVelocity() const { return angularVelocity; } void Observer::setAngularVelocity(const Vec3d& v) { angularVelocity = v; } /*! Determine an orientation that will make the negative z-axis point from * from the observer to the target, with the y-axis pointing in direction * of the component of 'up' that is orthogonal to the z-axis. */ // TODO: This is a generally useful function that should be moved to // the celmath package. template static Quaternion lookAt(Point3 from, Point3 to, Vector3 up) { Vector3 n = to - from; n.normalize(); Vector3 v = n ^ up; v.normalize(); Vector3 u = v ^ n; return Quaternion::matrixToQuaternion(Matrix3(v, u, -n)); } double Observer::getArrivalTime() const { if (observerMode != Travelling) return realTime; else return journey.startTime + journey.duration; } /*! Tick the simulation by dt seconds. Update the observer position * and orientation due to an active goto command or non-zero velocity * or angular velocity. */ void Observer::update(double dt, double timeScale) { realTime += dt; simTime += (dt / 86400.0) * timeScale; if (simTime >= maximumSimTime) simTime = maximumSimTime; if (simTime <= minimumSimTime) simTime = minimumSimTime; if (observerMode == Travelling) { // Compute the fraction of the trip that has elapsed; handle zero // durations correctly by skipping directly to the destination. float t = 1.0; if (journey.duration > 0) t = (float) clamp((realTime - journey.startTime) / journey.duration); Vec3d jv = journey.to - journey.from; UniversalCoord p; // Another interpolation method . . . accelerate exponentially, // maintain a constant velocity for a period of time, then // decelerate. The portion of the trip spent accelerating is // controlled by the parameter journey.accelTime; a value of 1 means // that the entire first half of the trip will be spent accelerating // and there will be no coasting at constant velocity. { double u = t < 0.5 ? t * 2 : (1 - t) * 2; double x; if (u < journey.accelTime) { x = exp(journey.expFactor * u) - 1.0; } else { x = exp(journey.expFactor * journey.accelTime) * (journey.expFactor * (u - journey.accelTime) + 1) - 1; } if (journey.traj == Linear) { Vec3d v = jv; if (v.length() == 0.0) { p = journey.from; } else { v.normalize(); if (t < 0.5) p = journey.from + v * astro::kilometersToMicroLightYears(x); else p = journey.to - v * astro::kilometersToMicroLightYears(x); } } else if (journey.traj == GreatCircle) { Selection centerObj = frame->getRefObject(); if (centerObj.body() != NULL) { Body* body = centerObj.body(); if (body->getSystem()) { if (body->getSystem()->getPrimaryBody() != NULL) centerObj = Selection(body->getSystem()->getPrimaryBody()); else centerObj = Selection(body->getSystem()->getStar()); } } UniversalCoord ufrom = frame->convertToUniversal(journey.from, simTime); UniversalCoord uto = frame->convertToUniversal(journey.to, simTime); UniversalCoord origin = centerObj.getPosition(simTime); Vec3d v0 = ufrom - origin; Vec3d v1 = uto - origin; if (jv.length() == 0.0) { p = journey.from; } else { x = astro::kilometersToMicroLightYears(x / jv.length()); Vec3d v; if (t < 0.5) v = slerp(x, v0, v1); else v = slerp(x, v1, v0); p = frame->convertFromUniversal(origin + v, simTime); } } else if (journey.traj == CircularOrbit) { Selection centerObj = frame->getRefObject(); UniversalCoord ufrom = frame->convertToUniversal(journey.from, simTime); UniversalCoord uto = frame->convertToUniversal(journey.to, simTime); UniversalCoord origin = centerObj.getPosition(simTime); Vec3d v0 = ufrom - origin; Vec3d v1 = uto - origin; if (jv.length() == 0.0) { p = journey.from; } else { //x = astro::kilometersToMicroLightYears(x / jv.length()); Quatd q0(1.0); Quatd q1(journey.rotation1.w, journey.rotation1.x, journey.rotation1.y, journey.rotation1.z); p = origin + v0 * Quatd::slerp(q0, q1, t).toMatrix3(); p = frame->convertFromUniversal(p, simTime); } } } // Spherically interpolate the orientation over the first half // of the journey. Quatd q; if (t >= journey.startInterpolation && t < journey.endInterpolation ) { // Smooth out the interpolation to avoid jarring changes in // orientation double v; if (journey.traj == CircularOrbit) { // In circular orbit mode, interpolation of orientation must // match the interpolation of position. v = t; } else { v = pow(sin((t - journey.startInterpolation) / (journey.endInterpolation - journey.startInterpolation) * PI / 2), 2); } // Be careful to choose the shortest path when interpolating if (norm(journey.initialOrientation - journey.finalOrientation) < norm(journey.initialOrientation + journey.finalOrientation)) { q = Quatd::slerp(journey.initialOrientation, journey.finalOrientation, v); } else { q = Quatd::slerp(journey.initialOrientation, -journey.finalOrientation, v); } } else if (t < journey.startInterpolation) { q = journey.initialOrientation; } else // t >= endInterpolation { q = journey.finalOrientation; } position = p; orientation = q; // If the journey's complete, reset to manual control if (t == 1.0f) { if (journey.traj != CircularOrbit) { //situation = RigidTransform(journey.to, journey.finalOrientation); position = journey.to; orientation = journey.finalOrientation; } observerMode = Free; setVelocity(Vec3d(0, 0, 0)); // targetVelocity = Vec3d(0, 0, 0); } } if (getVelocity() != targetVelocity) { double t = clamp((realTime - beginAccelTime) / VELOCITY_CHANGE_TIME); Vec3d v = getVelocity() * (1.0 - t) + targetVelocity * t; // At some threshold, we just set the velocity to zero; otherwise, // we'll end up with ridiculous velocities like 10^-40 m/s. if (v.length() < 1.0e-12) v = Vec3d(0.0, 0.0, 0.0); setVelocity(v); } // Update the position position = position + getVelocity() * dt; if (observerMode == Free) { // Update the observer's orientation Vec3d AV = getAngularVelocity(); Quatd dr = 0.5 * (AV * orientation); orientation += dt * dr; orientation.normalize(); } updateUniversal(); // Update orientation for tracking--must occur after updateUniversal(), as it // relies on the universal position and orientation of the observer. if (!trackObject.empty()) { Vec3d up = Vec3d(0, 1, 0) * getOrientation().toMatrix3(); Vec3d viewDir = trackObject.getPosition(getTime()) - getPosition(); setOrientation(lookAt(Point3d(0, 0, 0), Point3d(viewDir.x, viewDir.y, viewDir.z), up)); } } Selection Observer::getTrackedObject() const { return trackObject; } void Observer::setTrackedObject(const Selection& sel) { trackObject = sel; } const string& Observer::getDisplayedSurface() const { return displayedSurface; } void Observer::setDisplayedSurface(const string& surf) { displayedSurface = surf; } uint32 Observer::getLocationFilter() const { return locationFilter; } void Observer::setLocationFilter(uint32 _locationFilter) { locationFilter = _locationFilter; } void Observer::reverseOrientation() { Quatd q = getOrientation(); q.yrotate(PI); setOrientation(q); reverseFlag = !reverseFlag; } struct TravelExpFunc : public unary_function { double dist, s; TravelExpFunc(double d, double _s) : dist(d), s(_s) {}; double operator()(double x) const { // return (1.0 / x) * (exp(x / 2.0) - 1.0) - 0.5 - dist / 2.0; return exp(x * s) * (x * (1 - s) + 1) - 1 - dist; } }; void Observer::computeGotoParameters(const Selection& destination, JourneyParams& jparams, double gotoTime, double startInter, double endInter, Vec3d offset, ObserverFrame::CoordinateSystem offsetCoordSys, Vec3f up, ObserverFrame::CoordinateSystem upCoordSys) { if (frame->getCoordinateSystem() == ObserverFrame::PhaseLock) { //setFrame(FrameOfReference(astro::Ecliptical, destination)); setFrame(ObserverFrame::Ecliptical, destination); } else { setFrame(frame->getCoordinateSystem(), destination); } UniversalCoord targetPosition = destination.getPosition(getTime()); Vec3d v = targetPosition - getPosition(); v.normalize(); jparams.traj = Linear; jparams.duration = gotoTime; jparams.startTime = realTime; // Right where we are now . . . jparams.from = getPosition(); if (offsetCoordSys == ObserverFrame::ObserverLocal) { offset = offset * orientationUniv.toMatrix3(); } else { ObserverFrame offsetFrame(offsetCoordSys, destination); offset = offset * offsetFrame.getFrame()->getOrientation(getTime()).toMatrix3(); } jparams.to = targetPosition + offset; Vec3d upd(up.x, up.y, up.z); if (upCoordSys == ObserverFrame::ObserverLocal) { upd = upd * orientationUniv.toMatrix3(); } else { ObserverFrame upFrame(upCoordSys, destination); upd = upd * upFrame.getFrame()->getOrientation(getTime()).toMatrix3(); } jparams.initialOrientation = getOrientation(); Vec3d vn = targetPosition - jparams.to; Point3d focus(vn.x, vn.y, vn.z); jparams.finalOrientation = lookAt(Point3d(0, 0, 0), focus, upd); jparams.startInterpolation = min(startInter, endInter); jparams.endInterpolation = max(startInter, endInter); jparams.accelTime = 0.5; double distance = astro::microLightYearsToKilometers(jparams.from.distanceTo(jparams.to)) / 2.0; pair sol = solve_bisection(TravelExpFunc(distance, jparams.accelTime), 0.0001, 100.0, 1e-10); jparams.expFactor = sol.first; // Convert to frame coordinates jparams.from = frame->convertFromUniversal(jparams.from, getTime()); jparams.initialOrientation = frame->convertFromUniversal(jparams.initialOrientation, getTime()); jparams.to = frame->convertFromUniversal(jparams.to, getTime()); jparams.finalOrientation = frame->convertFromUniversal(jparams.finalOrientation, getTime()); } void Observer::computeGotoParametersGC(const Selection& destination, JourneyParams& jparams, double gotoTime, double startInter, double endInter, Vec3d offset, ObserverFrame::CoordinateSystem offsetCoordSys, Vec3f up, ObserverFrame::CoordinateSystem upCoordSys, const Selection& centerObj) { setFrame(frame->getCoordinateSystem(), destination); UniversalCoord targetPosition = destination.getPosition(getTime()); Vec3d v = targetPosition - getPosition(); v.normalize(); jparams.traj = GreatCircle; jparams.duration = gotoTime; jparams.startTime = realTime; jparams.centerObject = centerObj; // Right where we are now . . . jparams.from = getPosition(); ObserverFrame offsetFrame(offsetCoordSys, destination); offset = offset * offsetFrame.getFrame()->getOrientation(getTime()).toMatrix3(); jparams.to = targetPosition + offset; Vec3d upd(up.x, up.y, up.z); if (upCoordSys == ObserverFrame::ObserverLocal) { upd = upd * orientationUniv.toMatrix3(); } else { ObserverFrame upFrame(upCoordSys, destination); upd = upd * upFrame.getFrame()->getOrientation(getTime()).toMatrix3(); } jparams.initialOrientation = getOrientation(); Vec3d vn = targetPosition - jparams.to; Point3d focus(vn.x, vn.y, vn.z); jparams.finalOrientation = lookAt(Point3d(0, 0, 0), focus, upd); jparams.startInterpolation = min(startInter, endInter); jparams.endInterpolation = max(startInter, endInter); jparams.accelTime = 0.5; double distance = astro::microLightYearsToKilometers(jparams.from.distanceTo(jparams.to)) / 2.0; pair sol = solve_bisection(TravelExpFunc(distance, jparams.accelTime), 0.0001, 100.0, 1e-10); jparams.expFactor = sol.first; // Convert to frame coordinates jparams.from = frame->convertFromUniversal(jparams.from, getTime()); jparams.initialOrientation = frame->convertFromUniversal(jparams.initialOrientation, getTime()); jparams.to = frame->convertFromUniversal(jparams.to, getTime()); jparams.finalOrientation = frame->convertFromUniversal(jparams.finalOrientation, getTime()); } void Observer::computeCenterParameters(const Selection& destination, JourneyParams& jparams, double centerTime) { UniversalCoord targetPosition = destination.getPosition(getTime()); jparams.duration = centerTime; jparams.startTime = realTime; jparams.traj = Linear; // Don't move through space, just rotate the camera jparams.from = getPosition(); jparams.to = jparams.from; Vec3d up = Vec3d(0, 1, 0) * getOrientation().toMatrix3(); jparams.initialOrientation = getOrientation(); Vec3d vn = targetPosition - jparams.to; Point3d focus(vn.x, vn.y, vn.z); jparams.finalOrientation = lookAt(Point3d(0, 0, 0), focus, up); jparams.startInterpolation = 0; jparams.endInterpolation = 1; jparams.accelTime = 0.5; jparams.expFactor = 0; // Convert to frame coordinates jparams.from = frame->convertFromUniversal(jparams.from, getTime()); jparams.initialOrientation = frame->convertFromUniversal(jparams.initialOrientation, getTime()); jparams.to = frame->convertFromUniversal(jparams.to, getTime()); jparams.finalOrientation = frame->convertFromUniversal(jparams.finalOrientation, getTime()); } void Observer::computeCenterCOParameters(const Selection& destination, JourneyParams& jparams, double centerTime) { jparams.duration = centerTime; jparams.startTime = realTime; jparams.traj = CircularOrbit; jparams.centerObject = frame->getRefObject(); jparams.expFactor = 0.5; Vec3d v = destination.getPosition(getTime()) - getPosition(); Vec3d w = Vec3d(0.0, 0.0, -1.0) * getOrientation().toMatrix3(); v.normalize(); Selection centerObj = frame->getRefObject(); UniversalCoord centerPos = centerObj.getPosition(getTime()); UniversalCoord targetPosition = destination.getPosition(getTime()); Quatd q = Quatd::vecToVecRotation(v, w); jparams.from = getPosition(); jparams.to = centerPos + ((getPosition() - centerPos) * q.toMatrix3()); jparams.initialOrientation = getOrientation(); jparams.finalOrientation = getOrientation() * q; jparams.startInterpolation = 0.0; jparams.endInterpolation = 1.0; jparams.rotation1 = q; // Convert to frame coordinates jparams.from = frame->convertFromUniversal(jparams.from, getTime()); jparams.initialOrientation = frame->convertFromUniversal(jparams.initialOrientation, getTime()); jparams.to = frame->convertFromUniversal(jparams.to, getTime()); jparams.finalOrientation = frame->convertFromUniversal(jparams.finalOrientation, getTime()); } /*! Center the selection by moving on a circular orbit arround * the primary body (refObject). */ void Observer::centerSelectionCO(const Selection& selection, double centerTime) { if (!selection.empty() && !frame->getRefObject().empty()) { computeCenterCOParameters(selection, journey, centerTime); observerMode = Travelling; } } Observer::ObserverMode Observer::getMode() const { return observerMode; } void Observer::setMode(Observer::ObserverMode mode) { observerMode = mode; } // Private method to convert coordinates when a new observer frame is set. // Universal coordinates remain the same. All frame coordinates get updated, including // the goto parameters. void Observer::convertFrameCoordinates(const ObserverFrame* newFrame) { double now = getTime(); // Universal coordinates don't change. // Convert frame coordinates to the new frame. position = newFrame->convertFromUniversal(positionUniv, now); orientation = newFrame->convertFromUniversal(orientationUniv, now); // Convert goto parameters to the new frame journey.from = ObserverFrame::convert(frame, newFrame, journey.from, now); journey.initialOrientation = ObserverFrame::convert(frame, newFrame, journey.initialOrientation, now); journey.to = ObserverFrame::convert(frame, newFrame, journey.to, now); journey.finalOrientation = ObserverFrame::convert(frame, newFrame, journey.finalOrientation, now); } /*! Set the observer's reference frame. The position of the observer in * universal coordinates will not change. */ void Observer::setFrame(ObserverFrame::CoordinateSystem cs, const Selection& refObj, const Selection& targetObj) { ObserverFrame* newFrame = new ObserverFrame(cs, refObj, targetObj); if (newFrame != NULL) { convertFrameCoordinates(newFrame); delete frame; frame = newFrame; } } /*! Set the observer's reference frame. The position of the observer in * universal coordinates will not change. */ void Observer::setFrame(ObserverFrame::CoordinateSystem cs, const Selection& refObj) { setFrame(cs, refObj, Selection()); } /*! Set the observer's reference frame. The position of the observer in * universal coordinates will not change. */ void Observer::setFrame(const ObserverFrame& f) { if (frame != &f) { ObserverFrame* newFrame = new ObserverFrame(f); if (newFrame != NULL) { if (frame != NULL) { convertFrameCoordinates(newFrame); delete frame; } frame = newFrame; } } } /*! Get the current reference frame for the observer. */ const ObserverFrame* Observer::getFrame() const { return frame; } /*! Rotate the observer about its center. */ void Observer::rotate(Quatf q) { Quatd qd(q.w, q.x, q.y, q.z); orientation = qd * orientation; updateUniversal(); } /*! Orbit around the reference object (if there is one.) This involves changing * both the observer's position and orientation. If there is no current center * object, the specified selection will be used as the center of rotation, and * the observer reference frame will be modified. */ void Observer::orbit(const Selection& selection, Quatf q) { Selection center = frame->getRefObject(); if (center.empty() && !selection.empty()) { // Automatically set the center of the reference frame center = selection; setFrame(frame->getCoordinateSystem(), center); } if (!center.empty()) { // Get the focus position (center of rotation) in frame // coordinates; in order to make this function work in all // frames of reference, it's important to work in frame // coordinates. UniversalCoord focusPosition = center.getPosition(getTime()); //focusPosition = frame.fromUniversal(RigidTransform(focusPosition), getTime()).translation; focusPosition = frame->convertFromUniversal(focusPosition, getTime()); // v = the vector from the observer's position to the focus //Vec3d v = situation.translation - focusPosition; Vec3d v = position - focusPosition; // Get a double precision version of the rotation Quatd qd(q.w, q.x, q.y, q.z); // To give the right feel for rotation, we want to premultiply // the current orientation by q. However, because of the order in // which we apply transformations later on, we can't pre-multiply. // To get around this, we compute a rotation q2 such // that q1 * r = r * q2. Quatd qd2 = ~orientation * qd * orientation; qd2.normalize(); // Roundoff errors will accumulate and cause the distance between // viewer and focus to drift unless we take steps to keep the // length of v constant. double distance = v.length(); v = v * qd2.toMatrix3(); v.normalize(); v *= distance; //situation.rotation = situation.rotation * qd2; //situation.translation = focusPosition + v; orientation = orientation * qd2; position = focusPosition + v; updateUniversal(); } } /*! Exponential camera dolly--move toward or away from the selected object * at a rate dependent on the observer's distance from the object. */ void Observer::changeOrbitDistance(const Selection& selection, float d) { Selection center = frame->getRefObject(); if (center.empty() && !selection.empty()) { center = selection; setFrame(frame->getCoordinateSystem(), center); } if (!center.empty()) { UniversalCoord focusPosition = center.getPosition(getTime()); double size = center.radius(); // Somewhat arbitrary parameters to chosen to give the camera movement // a nice feel. They should probably be function parameters. double minOrbitDistance = astro::kilometersToMicroLightYears(size); double naturalOrbitDistance = astro::kilometersToMicroLightYears(4.0 * size); // Determine distance and direction to the selected object Vec3d v = getPosition() - focusPosition; double currentDistance = v.length(); if (currentDistance < minOrbitDistance) minOrbitDistance = currentDistance * 0.5; if (currentDistance >= minOrbitDistance && naturalOrbitDistance != 0) { double r = (currentDistance - minOrbitDistance) / naturalOrbitDistance; double newDistance = minOrbitDistance + naturalOrbitDistance * exp(log(r) + d); v = v * (newDistance / currentDistance); position = frame->convertFromUniversal(focusPosition + v, getTime()); updateUniversal(); } } } void Observer::setTargetSpeed(float s) { targetSpeed = s; Vec3d v; if (reverseFlag) s = -s; if (trackObject.empty()) { trackingOrientation = getOrientation(); // Generate vector for velocity using current orientation // and specified speed. v = Vec3d(0, 0, -s) * getOrientation().toMatrix4(); } else { // Use tracking orientation vector to generate target velocity v = Vec3d(0, 0, -s) * trackingOrientation.toMatrix4(); } targetVelocity = v; initialVelocity = getVelocity(); beginAccelTime = realTime; } float Observer::getTargetSpeed() { return (float) targetSpeed; } void Observer::gotoJourney(const JourneyParams& params) { journey = params; double distance = astro::microLightYearsToKilometers(journey.from.distanceTo(journey.to)) / 2.0; pair sol = solve_bisection(TravelExpFunc(distance, journey.accelTime), 0.0001, 100.0, 1e-10); journey.expFactor = sol.first; journey.startTime = realTime; observerMode = Travelling; } void Observer::gotoSelection(const Selection& selection, double gotoTime, Vec3f up, ObserverFrame::CoordinateSystem upFrame) { gotoSelection(selection, gotoTime, 0.0, 0.5, up, upFrame); } // Return the preferred distance for viewing an object static double getPreferredDistance(const Selection& selection) { switch (selection.getType()) { case Selection::Type_Body: // Handle reference points (i.e. invisible objects) specially, since the // actual radius of the point is meaningless. Instead, use the size of // bounding sphere of all child objects. This is useful for system // barycenters--the normal goto command will place the observer at // a viewpoint in which the entire system can be seen. if (selection.body()->getClassification() == Body::Invisible) { double r = selection.body()->getRadius(); if (selection.body()->getFrameTree() != NULL) r = selection.body()->getFrameTree()->boundingSphereRadius(); return min(astro::lightYearsToKilometers(0.1), r * 5.0); } else { return 5.0 * selection.radius(); } case Selection::Type_DeepSky: return 5.0 * selection.radius(); case Selection::Type_Star: if (selection.star()->getVisibility()) { return 100.0 * selection.radius(); } else { // Handle star system barycenters specially, using the same approach as // for reference points in solar systems. double maxOrbitRadius = 0.0; const vector* orbitingStars = selection.star()->getOrbitingStars(); if (orbitingStars != NULL) { for (vector::const_iterator iter = orbitingStars->begin(); iter != orbitingStars->end(); iter++) { Orbit* orbit = (*iter)->getOrbit(); if (orbit != NULL) maxOrbitRadius = max(orbit->getBoundingRadius(), maxOrbitRadius); } } if (maxOrbitRadius == 0.0) return astro::AUtoKilometers(1.0); else return maxOrbitRadius * 5.0; } case Selection::Type_Location: { double maxDist = getPreferredDistance(selection.location()->getParentBody()); return max(min(selection.location()->getSize() * 50.0, maxDist), 1.0); } default: return 1.0; } } // Given an object and its current distance from the camera, determine how // close we should go on the next goto. static double getOrbitDistance(const Selection& selection, double currentDistance) { // If further than 10 times the preferrred distance, goto the // preferred distance. If closer, zoom in 10 times closer or to the // minimum distance. double maxDist = astro::kilometersToMicroLightYears(getPreferredDistance(selection)); double minDist = astro::kilometersToMicroLightYears(1.01 * selection.radius()); double dist = (currentDistance > maxDist * 10.0) ? maxDist : currentDistance * 0.1; return max(dist, minDist); } void Observer::gotoSelection(const Selection& selection, double gotoTime, double startInter, double endInter, Vec3f up, ObserverFrame::CoordinateSystem upFrame) { if (!selection.empty()) { UniversalCoord pos = selection.getPosition(getTime()); Vec3d v = pos - getPosition(); double distance = v.length(); double orbitDistance = getOrbitDistance(selection, distance); computeGotoParameters(selection, journey, gotoTime, startInter, endInter, v * -(orbitDistance / distance), ObserverFrame::Universal, up, upFrame); observerMode = Travelling; } } /*! Like normal goto, except we'll follow a great circle trajectory. Useful * for travelling between surface locations, where we'd rather not go straight * through the middle of a planet. */ void Observer::gotoSelectionGC(const Selection& selection, double gotoTime, double /*startInter*/, //TODO: remove parameter?? double /*endInter*/, //TODO: remove parameter?? Vec3f up, ObserverFrame::CoordinateSystem upFrame) { if (!selection.empty()) { Selection centerObj = selection.parent(); UniversalCoord pos = selection.getPosition(getTime()); Vec3d v = pos - centerObj.getPosition(getTime()); double distanceToCenter = v.length(); Vec3d viewVec = pos - getPosition(); double orbitDistance = getOrbitDistance(selection, viewVec.length()); if (selection.location() != NULL) { Selection parent = selection.parent(); double maintainDist = astro::kilometersToMicroLightYears(getPreferredDistance(parent)); Vec3d parentPos = parent.getPosition(getTime()) - getPosition(); double parentDist = parentPos.length() - astro::kilometersToMicroLightYears(parent.radius()); if (parentDist <= maintainDist && parentDist > orbitDistance) { orbitDistance = parentDist; } } computeGotoParametersGC(selection, journey, gotoTime, //startInter, endInter, 0.25, 0.75, v * (orbitDistance / distanceToCenter), ObserverFrame::Universal, up, upFrame, centerObj); observerMode = Travelling; } } void Observer::gotoSelection(const Selection& selection, double gotoTime, double distance, Vec3f up, ObserverFrame::CoordinateSystem upFrame) { if (!selection.empty()) { UniversalCoord pos = selection.getPosition(getTime()); // The destination position lies along the line between the current // position and the star Vec3d v = pos - getPosition(); v.normalize(); computeGotoParameters(selection, journey, gotoTime, 0.25, 0.75, v * -distance * 1e6, ObserverFrame::Universal, up, upFrame); observerMode = Travelling; } } void Observer::gotoSelectionGC(const Selection& selection, double gotoTime, double distance, Vec3f up, ObserverFrame::CoordinateSystem upFrame) { if (!selection.empty()) { Selection centerObj = selection.parent(); UniversalCoord pos = selection.getPosition(getTime()); Vec3d v = pos - centerObj.getPosition(getTime()); v.normalize(); // The destination position lies along a line extended from the center // object to the target object computeGotoParametersGC(selection, journey, gotoTime, 0.25, 0.75, v * -distance * 1e6, ObserverFrame::Universal, up, upFrame, centerObj); observerMode = Travelling; } } void Observer::gotoSelectionLongLat(const Selection& selection, double gotoTime, double distance, float longitude, float latitude, Vec3f up) { if (!selection.empty()) { double phi = -latitude + PI / 2; double theta = longitude; double x = cos(theta) * sin(phi); double y = cos(phi); double z = -sin(theta) * sin(phi); computeGotoParameters(selection, journey, gotoTime, 0.25, 0.75, Vec3d(x, y, z) * distance * 1e6, ObserverFrame::BodyFixed, up, ObserverFrame::BodyFixed); observerMode = Travelling; } } void Observer::gotoLocation(const UniversalCoord& toPosition, const Quatd& toOrientation, double duration) { journey.startTime = realTime; journey.duration = duration; journey.from = position; journey.initialOrientation = orientation; journey.to = toPosition; journey.finalOrientation = toOrientation; journey.startInterpolation = 0.25f; journey.endInterpolation = 0.75f; journey.accelTime = 0.5; double distance = astro::microLightYearsToKilometers(journey.from.distanceTo(journey.to)) / 2.0; pair sol = solve_bisection(TravelExpFunc(distance, journey.accelTime), 0.0001, 100.0, 1e-10); journey.expFactor = sol.first; observerMode = Travelling; } void Observer::getSelectionLongLat(const Selection& selection, double& distance, double& longitude, double& latitude) { // Compute distance (km) and lat/long (degrees) of observer with // respect to currently selected object. if (!selection.empty()) { ObserverFrame frame(ObserverFrame::BodyFixed, selection); Point3d bfPos = (Point3d) frame.convertFromUniversal(positionUniv, getTime()); distance = bfPos.distanceFromOrigin(); // Convert from Celestia's coordinate system double x = bfPos.x; double y = -bfPos.z; double z = bfPos.y; longitude = radToDeg(atan2(y, x)); latitude = radToDeg(PI/2 - acos(z / distance)); // Convert distance from light years to kilometers. distance = astro::microLightYearsToKilometers(distance); } } void Observer::gotoSurface(const Selection& sel, double duration) { Vec3d v = getPosition() - sel.getPosition(getTime()); v.normalize(); Vec3d viewDir = Vec3d(0, 0, -1) * orientationUniv.toMatrix3(); Vec3d up = Vec3d(0, 1, 0) * orientationUniv.toMatrix3(); Quatd q = orientationUniv; if (v * viewDir < 0.0) { q = lookAt(Point3d(0.0, 0.0, 0.0), Point3d(0.0, 0.0, 0.0) + up, v); } ObserverFrame frame(ObserverFrame::BodyFixed, sel); UniversalCoord bfPos = frame.convertFromUniversal(positionUniv, getTime()); q = frame.convertFromUniversal(q, getTime()); double height = 1.0001 * astro::kilometersToMicroLightYears(sel.radius()); Vec3d dir = bfPos - Point3d(0.0, 0.0, 0.0); dir.normalize(); dir *= height; UniversalCoord nearSurfacePoint(dir.x, dir.y, dir.z); gotoLocation(nearSurfacePoint, q, duration); }; void Observer::cancelMotion() { observerMode = Free; } void Observer::centerSelection(const Selection& selection, double centerTime) { if (!selection.empty()) { computeCenterParameters(selection, journey, centerTime); observerMode = Travelling; } } void Observer::follow(const Selection& selection) { setFrame(ObserverFrame::Ecliptical, selection); } void Observer::geosynchronousFollow(const Selection& selection) { if (selection.body() != NULL || selection.location() != NULL || selection.star() != NULL) { setFrame(ObserverFrame::BodyFixed, selection); } } void Observer::phaseLock(const Selection& selection) { Selection refObject = frame->getRefObject(); if (selection != refObject) { if (refObject.body() != NULL || refObject.star() != NULL) { setFrame(ObserverFrame::PhaseLock, refObject, selection); } } else { // Selection and reference object are identical, so the frame is undefined. // We'll instead use the object's star as the target object. if (selection.body() != NULL) { setFrame(ObserverFrame::PhaseLock, selection, Selection(selection.body()->getSystem()->getStar())); } } } void Observer::chase(const Selection& selection) { if (selection.body() != NULL || selection.star() != NULL) { setFrame(ObserverFrame::Chase, selection); } } float Observer::getFOV() const { return fov; } void Observer::setFOV(float _fov) { fov = _fov; } Vec3f Observer::getPickRay(float x, float y) const { float s = 2 * (float) tan(fov / 2.0); Vec3f pickDirection = Vec3f(x * s, y * s, -1.0f); pickDirection.normalize(); return pickDirection; } // Internal method to update the position and orientation of the observer in // universal coordinates. void Observer::updateUniversal() { positionUniv = frame->convertToUniversal(position, simTime); orientationUniv = frame->convertToUniversal(orientation, simTime); } /*! Create the default 'universal' observer frame, with a center at the * Solar System barycenter and coordinate axes of the J200Ecliptic * reference frame. */ ObserverFrame::ObserverFrame() : coordSys(Universal), frame(NULL) { frame = createFrame(Universal, Selection(), Selection()); frame->addRef(); } /*! Create a new frame with the specified coordinate system and * reference object. The targetObject is only needed for phase * lock frames; the argument is ignored for other frames. */ ObserverFrame::ObserverFrame(CoordinateSystem _coordSys, const Selection& _refObject, const Selection& _targetObject) : coordSys(_coordSys), frame(NULL), targetObject(_targetObject) { frame = createFrame(_coordSys, _refObject, _targetObject); frame->addRef(); } /*! Create a new ObserverFrame with the specified reference frame. * The coordinate system of this frame will be marked as unknown. */ ObserverFrame::ObserverFrame(const ReferenceFrame &f) : coordSys(Unknown), frame(&f) { frame->addRef(); } /*! Copy constructor. */ ObserverFrame::ObserverFrame(const ObserverFrame& f) : coordSys(f.coordSys), frame(f.frame), targetObject(f.targetObject) { frame->addRef(); } ObserverFrame& ObserverFrame::operator=(const ObserverFrame& f) { coordSys = f.coordSys; targetObject = f.targetObject; // In case frames are the same, make sure we addref before releasing f.frame->addRef(); frame->release(); frame = f.frame; return *this; } ObserverFrame::~ObserverFrame() { if (frame != NULL) frame->release(); } ObserverFrame::CoordinateSystem ObserverFrame::getCoordinateSystem() const { return coordSys; } Selection ObserverFrame::getRefObject() const { return frame->getCenter(); } Selection ObserverFrame::getTargetObject() const { return targetObject; } const ReferenceFrame* ObserverFrame::getFrame() const { return frame; } UniversalCoord ObserverFrame::convertFromUniversal(const UniversalCoord& uc, double tjd) const { return frame->convertFromUniversal(uc, tjd); } UniversalCoord ObserverFrame::convertToUniversal(const UniversalCoord& uc, double tjd) const { return frame->convertToUniversal(uc, tjd); } Quatd ObserverFrame::convertFromUniversal(const Quatd& q, double tjd) const { return frame->convertFromUniversal(q, tjd); } Quatd ObserverFrame::convertToUniversal(const Quatd& q, double tjd) const { return frame->convertToUniversal(q, tjd); } /*! Convert a position from one frame to another. */ UniversalCoord ObserverFrame::convert(const ObserverFrame* fromFrame, const ObserverFrame* toFrame, const UniversalCoord& uc, double t) { // Perform the conversion fromFrame -> universal -> toFrame return toFrame->convertFromUniversal(fromFrame->convertToUniversal(uc, t), t); } /*! Convert an orientation from one frame to another. */ Quatd ObserverFrame::convert(const ObserverFrame* fromFrame, const ObserverFrame* toFrame, const Quatd& q, double t) { // Perform the conversion fromFrame -> universal -> toFrame return toFrame->convertFromUniversal(fromFrame->convertToUniversal(q, t), t); } // Create the ReferenceFrame for the specified observer frame parameters. ReferenceFrame* ObserverFrame::createFrame(CoordinateSystem _coordSys, const Selection& _refObject, const Selection& _targetObject) { switch (_coordSys) { case Universal: return new J2000EclipticFrame(Selection()); case Ecliptical: return new J2000EclipticFrame(_refObject); case Equatorial: return new BodyMeanEquatorFrame(_refObject, _refObject); case BodyFixed: return new BodyFixedFrame(_refObject, _refObject); case PhaseLock: { return new TwoVectorFrame(_refObject, FrameVector::createRelativePositionVector(_refObject, _targetObject), 1, FrameVector::createRelativeVelocityVector(_refObject, _targetObject), 2); } case Chase: { return new TwoVectorFrame(_refObject, FrameVector::createRelativeVelocityVector(_refObject, _refObject.parent()), 1, FrameVector::createRelativePositionVector(_refObject, _refObject.parent()), 2); } case PhaseLock_Old: { FrameVector rotAxis(FrameVector::createConstantVector(Vec3d(0, 1, 0), new BodyMeanEquatorFrame(_refObject, _refObject))); return new TwoVectorFrame(_refObject, FrameVector::createRelativePositionVector(_refObject, _targetObject), 3, rotAxis, 2); } case Chase_Old: { FrameVector rotAxis(FrameVector::createConstantVector(Vec3d(0, 1, 0), new BodyMeanEquatorFrame(_refObject, _refObject))); return new TwoVectorFrame(_refObject, FrameVector::createRelativeVelocityVector(_refObject.parent(), _refObject), 3, rotAxis, 2); } case ObserverLocal: // TODO: This is only used for computing up vectors for orientation; it does // define a proper frame for the observer position orientation. return new J2000EclipticFrame(Selection()); default: return new J2000EclipticFrame(_refObject); } }