Files
flightgear/src/Viewer/sview.cxx
T
2022-10-20 20:29:11 +08:00

1908 lines
72 KiB
C++

/*
Implementation of 'step' view system.
The basic idea here is calculate view position and direction by a series
of explicit steps. Steps can move to the origin of the user aircraft or a
multiplayer aircraft, or modify the current position by a fixed vector (e.g.
to move from aircraft origin to the pilot's eyepoint), or rotate the current
direction by a fixed transformation etc. We can also have a step that sets the
direction to point to a previously-calculated target position.
This is similar to what is already done by FlightGear's existing View code, but
making the individual steps explicit gives us more flexibility.
The dynamic nature of step views allows view cloning with composite-viewer.
We also allow views to be defined and created at runtime instead of being
hard-coded in *-set.xml files. For example this makes it possible to define a
view from the user's aircraft's pilot to the centre of a multiplayer aircraft
(or to a multiplayer aircraft's pilot).
*/
/*
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.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "sview.hxx"
#include <Main/fg_props.hxx>
#include <Main/globals.hxx>
#include <Scenery/scenery.hxx>
#include <Viewer/FGEventHandler.hxx>
#include <Viewer/renderer.hxx>
#include <Viewer/WindowBuilder.hxx>
#include <Viewer/WindowSystemAdapter.hxx>
#include <simgear/debug/logstream.hxx>
#include <simgear/math/SGMath.hxx>
#include <simgear/props/props.hxx>
#include <simgear/props/props_io.hxx>
#include <simgear/scene/util/OsgMath.hxx>
#include <simgear/scene/viewer/Compositor.hxx>
#include <osg/CameraView>
#include <osg/GraphicsContext>
#include <osgViewer/CompositeViewer>
static const double pi = 3.141592653589793238463;
static std::ostream& operator << (std::ostream& out, const osg::Vec3f& vec)
{
return out << "Vec3f{"
<< " x=" << vec._v[0]
<< " y=" << vec._v[1]
<< " z=" << vec._v[2]
<< "}";
}
static std::ostream& operator << (std::ostream& out, const osg::Quat& quat)
{
return out << "Quat{"
<< " x=" << quat._v[0]
<< " y=" << quat._v[1]
<< " z= " << quat._v[2]
<< " w=" << quat._v[3]
<< "}";
}
static std::ostream& operator << (std::ostream& out, const osg::Matrixd& matrix)
{
osg::Vec3f translation;
osg::Quat rotation;
osg::Vec3f scale;
osg::Quat so;
matrix.decompose(translation, rotation, scale, so);
return out << "Matrixd {"
<< " translation=" << translation
<< " rotation=" << rotation
<< " scale=" << scale
<< " so=" << so
<< "}";
}
/* A position and direction. Repeatedly modified by SviewStep::evaluate() when
calculating final camera position/orientation. */
struct SviewPosDir
{
SviewPosDir()
:
heading(0),
pitch(0),
roll(0),
target_is_set(false)
{
}
SGGeod position;
double heading;
double pitch;
double roll;
SGGeod target;
bool target_is_set;
/* The final position and direction, in a form suitable for setting an
osg::Camera's view matrix. */
SGVec3d position2;
SGQuatd direction2;
/* If a step sets either/both of these to non-zero, the view will alter
zoom to accomodate the required field of views (in degreea). */
double fov_h = 0;
double fov_v = 0;
friend std::ostream& operator<< (std::ostream& out, const SviewPosDir& posdir)
{
out << "SviewPosDir {"
<< " position=" << posdir.position
<< " heading=" << posdir.heading
<< " pitch=" << posdir.pitch
<< " roll=" << posdir.roll
<< " target=" << posdir.target
<< " position2=" << posdir.position2
<< " direction2=" << posdir.direction2
<< "}"
;
return out;
}
};
/* Generic damping support. Improved version of class in view.hxx and view.cxx.
We model dx/dt = (target-current)/damping_time, so damping_time is time for
value to change by a factor of e. */
struct Damping {
/* If m_wrap_max is non-zero, we wrap to ensure values are always between 0
and m_wrap_max. E.g. use m_wrap_max=360 for an angle in degrees. */
Damping(double damping_time, double wrap_max=0, double current=0)
:
m_damping_time(damping_time),
m_current(current),
m_wrap_max(wrap_max)
{}
/* Updates and returns new smoothed value. */
double update(double dt, double target)
{
const double e = 2.718281828459045;
double delta = target - m_current;
if (m_wrap_max) {
if (delta < -m_wrap_max/2) delta += m_wrap_max;
if (delta >= m_wrap_max/2) delta -= m_wrap_max;
}
m_current = target - delta * pow(e, -dt/m_damping_time);
if (m_wrap_max) {
if (m_current < 0) m_current += m_wrap_max;
if (m_current >= m_wrap_max) m_current -= m_wrap_max;
}
return m_current;
}
/* Forces current value to be <current>. */
double reset(double current)
{
m_current = current;
return m_current;
}
private:
double m_damping_time;
double m_current;
double m_wrap_max;
};
/* Abstract base class for a single view step. A view step modifies a
SviewPosDir, e.g. translating the position and/or rotating the direction. */
struct SviewStep
{
/* Updates <posdir>. */
virtual void evaluate(SviewPosDir& posdir, double dt=0) = 0;
/* Modify view angle. */
virtual void mouse_drag(double delta_x_deg, double delta_y_deg)
{
}
virtual void stream(std::ostream& out) const
{
out << " <SviewStep>";
}
virtual ~SviewStep() {}
std::string m_description;
friend std::ostream& operator<< (std::ostream& out, const SviewStep& step)
{
out << ' ' << typeid(step).name();
step.stream(out);
return out;
}
};
struct Callsign
/* Info about aircraft with a particular callsign. "" means the user
aircraft. We use the user aircraft if specified callsign does not exist.
update() must be called before m_root is dereferenced. */
{
Callsign(const std::string& callsign)
:
m_callsign(callsign)
{
}
bool update()
/* Returns true if we have changed m_root. */
{
if (m_callsign == "") {
if (m_root) return false;
}
else if (m_root && m_root->getStringValue("callsign") == m_callsign) {
return false;
}
m_root = nullptr;
if (m_callsign != "") {
/* Find multiplayer with matching callsign. */
SGPropertyNode* c = globals->get_props()->getNode("ai/models/callsigns")->getNode(m_callsign);
if (c) {
int i = c->getIntValue();
m_root = globals->get_props()->getNode("ai/models/multiplayer", i);
assert(m_root);
}
}
if (!m_root) {
/* Default to user aircraft. */
m_root = globals->get_props();
}
return true;
}
std::string m_callsign;
SGPropertyNode_ptr m_root;
};
/* A step that sets position to aircraft origin and direction to aircraft's
longitudal axis. */
struct SviewStepAircraft : SviewStep
{
SviewStepAircraft(const std::string& callsign)
:
m_callsign(callsign)
{
}
void evaluate(SviewPosDir& posdir, double dt) override
{
if (m_callsign.update()) {
m_longitude = m_callsign.m_root->getNode("position/longitude-deg");
m_latitude = m_callsign.m_root->getNode("position/latitude-deg");
m_altitude = m_callsign.m_root->getNode("position/altitude-ft");
m_heading = m_callsign.m_root->getNode("orientation/true-heading-deg");
m_pitch = m_callsign.m_root->getNode("orientation/pitch-deg");
m_roll = m_callsign.m_root->getNode("orientation/roll-deg");
}
posdir.position = SGGeod::fromDegFt(
m_longitude->getDoubleValue(),
m_latitude->getDoubleValue(),
m_altitude->getDoubleValue()
);
posdir.heading = m_heading->getDoubleValue();
posdir.pitch = m_pitch->getDoubleValue();
posdir.roll = m_roll->getDoubleValue();
}
virtual void stream(std::ostream& out) const
{
out << " <SviewStepAircraft:" + m_callsign.m_callsign + ">";
}
private:
Callsign m_callsign;
SGPropertyNode_ptr m_longitude;
SGPropertyNode_ptr m_latitude;
SGPropertyNode_ptr m_altitude;
SGPropertyNode_ptr m_heading;
SGPropertyNode_ptr m_pitch;
SGPropertyNode_ptr m_roll;
};
/* Moves position by fixed vector, does not change direction.
E.g. for moving from aircraft origin to pilot viewpoint. */
struct SviewStepMove : SviewStep
{
SviewStepMove(double forward, double up, double right)
:
m_offset(-right, -up, -forward)
{
SG_LOG(SG_VIEW, SG_INFO, "forward=" << forward << " up=" << up << " right=" << right);
}
void evaluate(SviewPosDir& posdir, double dt) override
{
/* These calculations are copied from View::recalcLookFrom(). */
/* The rotation rotating from the earth centerd frame to the horizontal
local frame. */
SGQuatd hlOr = SGQuatd::fromLonLat(posdir.position);
/* The rotation from the horizontal local frame to the basic view
orientation. */
SGQuatd hlToBody = SGQuatd::fromYawPitchRollDeg(posdir.heading, posdir.pitch, posdir.roll);
/* Compute the eyepoints orientation and position wrt the earth
centered frame - that is global coorinates. */
SGQuatd ec2body = hlOr * hlToBody;
/* The cartesian position of the basic view coordinate. */
SGVec3d position = SGVec3d::fromGeod(posdir.position);
/* This is rotates the x-forward, y-right, z-down coordinate system the
where simulation runs into the OpenGL camera system with x-right, y-up,
z-back. */
SGQuatd q(-0.5, -0.5, 0.5, 0.5);
position += (ec2body * q).backTransform(m_offset);
posdir.position = SGGeod::fromCart(position);
}
virtual void stream(std::ostream& out) const
{
out << " <SviewStepMove>" << m_offset;
}
private:
SGVec3d m_offset;
};
/* Modifies heading, pitch and roll by fixed amounts; does not change
position.
E.g. can be used to preserve direction (relative to aircraft) of Helicopter
view at the time it was cloned. */
struct SviewStepRotate : SviewStep
{
SviewStepRotate(
double heading,
double pitch,
double roll,
double damping_heading = 0,
double damping_pitch = 0,
double damping_roll = 0
)
:
m_heading(heading),
m_pitch(pitch),
m_roll(roll),
m_damping_heading(damping_heading, 360 /*m_wrap_max*/),
m_damping_pitch(damping_pitch, 360 /*m_wrap_max*/),
m_damping_roll(damping_roll, 360 /*m_wrap_max*/)
{
SG_LOG(SG_VIEW, SG_INFO, "heading=" << heading << " pitch=" << pitch << " roll=" << roll);
}
void evaluate(SviewPosDir& posdir, double dt) override
{
posdir.heading = m_damping_heading.update(dt, posdir.heading + m_heading);
posdir.pitch = m_damping_pitch.update(dt, posdir.pitch + m_pitch);
posdir.roll = m_damping_roll.update(dt, posdir.roll + m_roll);
}
virtual void stream(std::ostream& out) const
{
out << " <SviewStepRotate>"
<< ' ' << m_heading
<< ' ' << m_pitch
<< ' ' << m_roll
;
}
private:
double m_heading;
double m_pitch;
double m_roll;
Damping m_damping_heading;
Damping m_damping_pitch;
Damping m_damping_roll;
};
/* Modify a view's heading and pitch in response to mouse dragging. */
struct SviewStepMouseDrag : SviewStep
{
SviewStepMouseDrag(double heading_scale, double pitch_scale)
:
m_heading_scale(heading_scale),
m_pitch_scale(pitch_scale)
{}
void evaluate(SviewPosDir& posdir, double dt) override
{
posdir.heading += m_heading;
posdir.pitch += m_pitch;
}
void mouse_drag(double delta_x_deg, double delta_y_deg) override
{
m_heading += m_heading_scale * delta_x_deg;
m_pitch += m_pitch_scale * delta_y_deg;
}
double m_heading_scale;
double m_pitch_scale;
double m_heading = 0;
double m_pitch = 0;
};
/* Multiply heading, pitch and roll by constants. */
struct SviewStepDirectionMultiply : SviewStep
{
SviewStepDirectionMultiply(double heading=0, double pitch=0, double roll=0)
:
m_heading(heading),
m_pitch(pitch),
m_roll(roll)
{
SG_LOG(SG_VIEW, SG_INFO, "heading=" << heading << " pitch=" << pitch << " roll=" << roll);
}
void evaluate(SviewPosDir& posdir, double dt) override
{
posdir.heading *= m_heading;
posdir.pitch *= m_pitch;
posdir.roll *= m_roll;
}
virtual void stream(std::ostream& out) const
{
out << " <SviewStepDirectionMultiply>"
<< ' ' << m_heading
<< ' ' << m_pitch
<< ' ' << m_roll
;
}
private:
double m_heading;
double m_pitch;
double m_roll;
};
/* Copies current position to posdir.target. Used by SviewEyeTarget()
to make current position be available as target later on, e.g. by
SviewStepFinal with to_target=true. */
struct SviewStepCopyToTarget : SviewStep
{
void evaluate(SviewPosDir& posdir, double dt) override
{
assert(!posdir.target_is_set);
posdir.target = posdir.position;
posdir.target_is_set = true;
}
virtual void stream(std::ostream& out) const
{
out << " <SviewStepCopyToTarget>";
}
};
/* Move position to nearest tower. */
struct SviewStepNearestTower : SviewStep
{
SviewStepNearestTower(const std::string& callsign)
:
m_callsign(callsign)
{
m_description = "Nearest tower";
}
void evaluate(SviewPosDir& posdir, double dt) override
{
if (m_callsign.update()) {
m_latitude = m_callsign.m_root->getNode("sim/tower/latitude-deg", true /*create*/);
m_longitude = m_callsign.m_root->getNode("sim/tower/longitude-deg", true /*create*/);
m_altitude = m_callsign.m_root->getNode("sim/tower/altitude-ft", true /*create*/);
}
posdir.position = SGGeod::fromDegFt(
m_longitude->getDoubleValue(),
m_latitude->getDoubleValue(),
m_altitude->getDoubleValue()
);
posdir.heading = 0;
posdir.pitch = 0;
posdir.roll = 0;
SG_LOG(SG_VIEW, SG_BULK, "moved posdir.postion to: " << posdir.position);
}
virtual void stream(std::ostream& out) const
{
out << " <SviewStepNearestTower:" + m_callsign.m_callsign + ">";
}
Callsign m_callsign;
SGPropertyNode_ptr m_latitude;
SGPropertyNode_ptr m_longitude;
SGPropertyNode_ptr m_altitude;
};
/*
Converts posdir's eye position/direction to global cartesian
position/quarternion direction in position2 and direction2, which will be used
to set the camera parameters.
If posdir.target_is_set is true (i.e. posdir.target is valid), we also change
the direction to point at the target, using the original direction to determine
the 'up' vector.
*/
struct SviewStepFinal : SviewStep
{
SviewStepFinal()
{
}
void evaluate(SviewPosDir& posdir, double dt) override
{
/* See View::recalcLookFrom(). */
/* The rotation rotating from the earth centerd frame to the horizontal
local frame. */
SGQuatd eye_position_direction = SGQuatd::fromLonLat(posdir.position);
/* The rotation from the horizontal local frame to the basic view
orientation. */
SGQuatd eye_local_direction = SGQuatd::fromYawPitchRollDeg(posdir.heading, posdir.pitch, posdir.roll);
/* The cartesian position of the basic view coordinate. */
SGVec3d eye_position = SGVec3d::fromGeod(posdir.position);
/* Compute the eye direction in global coordinates. */
SGQuatd eye_direction = eye_position_direction * eye_local_direction;
if (posdir.target_is_set)
{
/* Rotate eye direction to point at posdir.target. */
SGVec3d target_position = SGVec3d::fromGeod(posdir.target);
/* add target offsets to at_position...
Compute the eyepoints orientation and position wrt the earth centered
frame - that is global coorinates _absolute_view_pos = eye_position; */
/* the view direction. */
SGVec3d eye_to_target_direction = normalize(target_position - eye_position);
/* the up directon. */
SGVec3d up = eye_direction.backTransform(SGVec3d(0, 0, -1));
/* rotate -dir to the 2-th unit vector
rotate up to 1-th unit vector
Note that this matches the OpenGL camera coordinate system with
x-right, y-up, z-back. */
posdir.direction2 = SGQuatd::fromRotateTo(-eye_to_target_direction, 2, up, 1);
}
else
{
/* This is rotates the x-forward, y-right, z-down coordinate system the
where simulation runs into the OpenGL camera system with x-right, y-up,
z-back. */
SGQuatd q(-0.5, -0.5, 0.5, 0.5);
posdir.direction2 = eye_direction * q;
}
posdir.position2 = eye_position;
}
virtual void stream(std::ostream& out) const
{
out << " <SviewStepFinal>";
}
};
/* Change angle and field of view so that we can see an aircraft and the ground
immediately below it. */
struct SviewStepAGL : SviewStep
{
SviewStepAGL(const std::string& callsign, double damping_time)
:
m_callsign(callsign),
relative_height_ground_damping(damping_time)
{
}
void evaluate(SviewPosDir& posdir, double dt) override
{
if (m_callsign.update()) {
m_chase_distance = -25;
if (m_callsign.m_callsign == "") {
m_chase_distance = globals->get_props()->getDoubleValue("/sim/chase-distance-m", m_chase_distance);
}
else {
m_chase_distance = m_callsign.m_root->getDoubleValue("set/sim/chase-distance-m", m_chase_distance);
}
}
assert(posdir.target_is_set);
double _fov_user_deg = 30;
double _configFOV_deg = 30;
/* Some aircraft appear to have elevation that is slightly below ground
level when on the ground, e.g. SenecaII, which makes get_elevation_m()
fail. So we pass a slightly incremented elevation. */
double ground_altitude = 0;
const simgear::BVHMaterial* material = NULL;
SGGeod target0 = posdir.target;
SGGeod target_plus = posdir.target;
target_plus.setElevationM(target_plus.getElevationM() + 1);
bool ok = globals->get_scenery()->get_elevation_m(target_plus, ground_altitude, &material);
if (ok) {
m_ground_altitude = ground_altitude;
}
else {
/* get_elevation_m() can fail if scenery has been un-cached, which
appears to happen quite often with remote multiplayer aircraft, so
we preserve the previous ground altitude to give some consistency
and avoid confusing zooming when switching between views. */
ground_altitude = m_ground_altitude;
}
double h_distance = SGGeodesy::distanceM(posdir.position, posdir.target);
if (h_distance == 0) {
/* Not sure this should ever happen, but we need to cope with this
here otherwise we'll get divide-by-zero. */
return;
}
/* Find vertical region we want to be able to see. */
double relative_height_target = posdir.target.getElevationM() - posdir.position.getElevationM();
double relative_height_ground = ground_altitude - posdir.position.getElevationM();
/* We expand the field of view so that it hopefully shows the whole
aircraft and a little more of the ground.
We use chase-distance as a crude measure of the aircraft's
size. There doesn't seem to be any more definitive information.
We damp our measure of ground level, to avoid the view jumping around
if an aircraft flies over buildings. */
relative_height_ground -= 2;
double aircraft_size_vertical = fabs(m_chase_distance) * 0.3;
double aircraft_size_horizontal = fabs(m_chase_distance) * 0.9;
double relative_height_target_plus = relative_height_target + aircraft_size_vertical;
double relative_height_ground_ = relative_height_ground;
relative_height_ground = relative_height_ground_damping.update(dt, relative_height_ground);
if (relative_height_ground > relative_height_target) {
/* Damping of relative_height_ground can result in it being
temporarily above the aircraft, so we ensure the aircraft is
visible. */
relative_height_ground = relative_height_ground_damping.reset(relative_height_ground_);
}
/* Not implemented yet: apply scaling from user field of view
setting, altering only relative_height_ground so that the aircraft
is always in view. */
{
double delta = relative_height_target_plus - relative_height_ground;
delta *= (_fov_user_deg / _configFOV_deg);
relative_height_ground = relative_height_target_plus - delta;
}
double angle_v_target = atan(relative_height_target_plus / h_distance);
double angle_v_ground = atan(relative_height_ground / h_distance);
/* The target we want to use is determined by the midpoint of the two
angles we've calculated. */
double angle_v_mid = (angle_v_target + angle_v_ground) / 2;
double posdir_target_old_elevation = posdir.target.getElevationM();
posdir.target.setElevationM(posdir.position.getElevationM() + h_distance * tan(angle_v_mid));
/* Set required vertical field of view. We use fabs to avoid
things being upside down if target is below ground level (e.g. new
multiplayer aircraft are briefly at -9999ft). */
double fov_v = fabs(angle_v_target - angle_v_ground);
/* Set required horizontal field of view so that we can see entire
horizontal extent of the aircraft (assuming worst case where
airplane is horizontal and square-on to viewer). */
double fov_h = 2 * atan(aircraft_size_horizontal / 2 / h_distance);
posdir.fov_v = fov_v * 180 / pi;
posdir.fov_h = fov_h * 180 / pi;
bool verbose = false;
if (0) {
static time_t t0 = 0;
time_t t = time(NULL);
if (0 && t - t0 >= 3) {
t0 = t;
verbose = true;
}
}
if (verbose) SG_LOG(SG_VIEW, SG_ALERT, ""
<< " target0=" << target0
<< " fov_v=" << fov_v * 180/pi
<< " ground_altitude=" << ground_altitude
<< " relative_height_target_plus=" << relative_height_target_plus
<< " h_distance=" << h_distance
<< " relative_height_ground=" << relative_height_ground
<< " m_chase_distance=" << m_chase_distance
<< " angle_v_mid=" << angle_v_mid * 180/pi
<< " posdir_target_old_elevation=" << posdir_target_old_elevation
<< " posdir.target=" << posdir.target
);
}
double m_chase_distance;
Callsign m_callsign;
double m_ground_altitude = 0;
Damping relative_height_ground_damping;
};
/* A step that takes the SviewPosDir's eye and target positions and treats
them as local and remote points respectively. We choose an eye position and
direction such that the local and remote points are both visible, with the
original eye position in the foreground at a fixed distance. */
struct SviewStepDouble : SviewStep
{
SviewStepDouble()
{
m_local_chase_distance = 25;
m_angle_rad = 15 * pi / 180;
}
SviewStepDouble(SGPropertyNode* config)
{
m_local_chase_distance = config->getDoubleValue("chase-distance");
m_angle_rad = config->getDoubleValue("angle") * pi / 180;
}
void evaluate(SviewPosDir& posdir, double dt) override
{
/*
We choose eye position so that we show the local aircraft a fixed
amount below the view midpoint, and the remote aircraft the same fixed
amount above the view midpoint.
L: middle of local aircraft.
R: middle of remote aircraft.
E: desired eye-point
---- R
/ \
E |
| L | .................... H (horizon)
| |
\ /
----
We require that:
EL is local aircraft's chase-distance so that local aircraft is in
perfect view.
Angle LER is fixed to give good view of both aircraft in
window. (Should be related to the vertical angular size of the
window, but at the moment we use a fixed value.)
We need to calculate angle RLE, and add to HLR, in order to find
position of E (eye) relative to L. Then for view pitch we use midpoint
of angle of ER and angle EL, so that local and remote aircraft are
symmetrically below and above the centre of the view.
We find angle RLE by using cosine rule twice in the triangle RLE:
ER^2 = EL^2 + LR^2 - 2*EL*LR*cos(RLE)
LR^2 = ER^2 + EL^2 - 2*ER*EL*cos(LER)
Wen end up with a quadratic for ER with solution:
ER = EL * cos(LER) + sqrt(LR^2 - EL^22*sin(LER)^2)
(We discard the -sqrt because it ends up with ER being negative.)
and:
cos(RLE) = (LR^2 + LE^2 - ER^2) / (2*LE*LR)
So we can find RLE using acos().
*/
bool debug = false;
static time_t t0 = 0;
time_t t = time(NULL);
if (0 && t - t0 > 3) {
t0 = t;
debug = true;
}
assert(posdir.target_is_set);
SviewPosDir posdir_remote = posdir;
SviewPosDir posdir_local = posdir;
posdir_local.target = posdir_local.position;
if (debug) {
SG_LOG(SG_VIEW, SG_ALERT, " posdir =" << posdir);
SG_LOG(SG_VIEW, SG_ALERT, " posdir_local =" << posdir_local);
SG_LOG(SG_VIEW, SG_ALERT, " posdir_remote=" << posdir_remote);
}
/* Create cartesian coordinates so we can calculate distance <lr>. */
SGVec3d local_pos = SGVec3d::fromGeod(posdir_local.target);
SGVec3d remote_pos = SGVec3d::fromGeod(posdir_remote.target);
double lr = sqrt(distSqr(local_pos, remote_pos));
/* Desired angle between local and remote aircraft in final view. */
double ler = m_angle_rad;
/* Distance of eye from local aircraft. */
double le = m_local_chase_distance;
/* Find <er>, the distance of eye from remote aircraft. Have to be
careful to cope when there is no solution if remote is too close, and
choose the +ve sqrt(). */
double er_root_term = lr*lr - le*le*sin(ler)*sin(ler);
if (er_root_term < 0) {
/* This can happen if LR is too small, i.e. remote aircraft too
close to local aircraft. */
er_root_term = 0;
}
double er = le * cos(ler) + sqrt(er_root_term);
/* Now find rle, angle at local aircraft between vector to remote
aircraft and vector to desired eye position. Again we have to cope when
a real solution is not possible. */
double cos_rle = (lr*lr + le*le - er*er) / (2*le*lr);
if (cos_rle > 1) cos_rle = 1;
if (cos_rle < -1) cos_rle = -1;
double rle = acos(cos_rle);
double rle_deg = rle * 180 / pi;
/* Now find the actual eye position. We do this by calculating heading
and pitch from local aircraft L to eye position E, then using a
temporary SviewStepMove. */
double lr_vertical = posdir_remote.target.getElevationM() - posdir_local.target.getElevationM();
double lr_horizontal = SGGeodesy::distanceM(posdir_local.target, posdir_remote.target);
double hlr = atan2(lr_vertical, lr_horizontal);
posdir_local.heading = SGGeodesy::courseDeg(
posdir_local.target,
posdir_remote.target
);
posdir_local.pitch = (hlr + rle) * 180 / pi;
posdir_local.roll = 0;
auto move = SviewStepMove(le, 0, 0);
move.evaluate(posdir_local, 0 /*dt*/);
/* At this point, posdir_local.position is eye position. We make
posdir_local.pitch point from this eye position to halfway between the
remote and local aircraft. */
double er_vertical = posdir_remote.target.getElevationM()
- posdir_local.position.getElevationM();
double her = asin(er_vertical / er);
double hel = (hlr + rle) - pi;
posdir_local.pitch = (her + hel) / 2 * 180 / pi;
posdir = posdir_local;
/* Need to ensure that SviewStepFinal will not rotate the view to point
at posdir.target. */
posdir.target_is_set = false;
if (debug) {
SG_LOG(SG_VIEW, SG_ALERT, ""
<< " lr=" << lr
<< " ler=" << ler
<< " er_root_term=" << er_root_term
<< " er=" << er
<< " cos_rle=" << cos_rle
<< " rle_deg=" << rle_deg
<< " lr_vertical=" << lr_vertical
<< " lr_horizontal=" << lr_horizontal
<< " hlr=" << hlr
<< " posdir_local=" << posdir_local
<< " posdir_remote=" << posdir_remote
);
}
}
double m_local_chase_distance;
double m_angle_rad;
};
/* Contains a list of SviewStep's that are used to evaluate a SviewPosDir
(position and orientation)
*/
struct SviewSteps
{
void add_step(std::shared_ptr<SviewStep> step)
{
m_steps.push_back(step);
}
void add_step(SviewStep* step)
{
return add_step(std::shared_ptr<SviewStep>(step));
}
void evaluate(SviewPosDir& posdir, double dt, bool debug=false)
{
if (debug) SG_LOG(SG_VIEW, SG_ALERT, "evaluating m_name=" << m_name);
for (auto step: m_steps) {
step->evaluate(posdir, dt);
if (debug) SG_LOG(SG_VIEW, SG_ALERT, "posdir=" << posdir);
}
};
std::string m_name;
std::vector<std::shared_ptr<SviewStep>> m_steps;
friend std::ostream& operator << (std::ostream& out, const SviewSteps& viewpos)
{
out << viewpos.m_name << " (" << viewpos.m_steps.size() << ")";
for (auto step: viewpos.m_steps) {
out << " " << *step;
}
return out;
}
};
/* Base class for views.
*/
struct SviewView
{
SviewView(osgViewer::View* osg_view)
:
m_osg_view(osg_view)
{
s_id += 1;
}
/* Description that also includes integer identifier. */
const std::string description2()
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "[%i] ", s_id);
return buffer + description();
}
/* Description of this view, used in window title etc. */
virtual const std::string description() = 0;
virtual ~SviewView()
{
if (!m_osg_view) {
return;
}
osgViewer::ViewerBase* viewer_base = m_osg_view->getViewerBase();
auto composite_viewer = dynamic_cast<osgViewer::CompositeViewer*>(viewer_base);
assert(composite_viewer);
for (unsigned i=0; i<composite_viewer->getNumViews(); ++i) {
osgViewer::View* view = composite_viewer->getView(i);
SG_LOG(SG_VIEW, SG_DEBUG, "composite_viewer view i=" << i << " view=" << view);
}
SG_LOG(SG_VIEW, SG_DEBUG, "removing m_osg_view=" << m_osg_view);
composite_viewer->stopThreading();
composite_viewer->removeView(m_osg_view);
composite_viewer->startThreading();
}
/* Returns false if window has been closed. */
virtual bool update(double dt) = 0;
virtual void mouse_drag(double delta_x_deg, double delta_y_deg) = 0;
/* Sets this view's camera position/orientation from <posdir>. */
void posdir_to_view(SviewPosDir posdir)
{
/* FGViewMgr::update(). */
osg::Vec3d position = toOsg(posdir.position2);
osg::Quat orientation = toOsg(posdir.direction2);
osg::Camera* camera = m_osg_view->getCamera();
osg::Matrix old_m = camera->getViewMatrix();
/* This calculation is copied from CameraGroup::update(). */
const osg::Matrix new_m(
osg::Matrix::translate(-position)
* osg::Matrix::rotate(orientation.inverse())
);
SG_LOG(SG_VIEW, SG_BULK, "old_m: " << old_m);
SG_LOG(SG_VIEW, SG_BULK, "new_m: " << new_m);
camera->setViewMatrix(new_m);
if (posdir.fov_v || posdir.fov_h) {
/* Update zoom to accomodate required vertical/horizontal field of
views. */
double fovy;
double aspect_ratio;
double zNear;
double zFar;
camera->getProjectionMatrixAsPerspective(fovy, aspect_ratio, zNear, zFar);
if (posdir.fov_v) {
camera->setProjectionMatrixAsPerspective(
posdir.fov_v,
aspect_ratio,
zNear,
zFar
);
if (posdir.fov_h) {
/* Increase fov if necessary so that we include both
fov_h and fov_v. */
double aspect_ratio;
camera->getProjectionMatrixAsPerspective(
fovy,
aspect_ratio,
zNear,
zFar
);
if (fovy * aspect_ratio < posdir.fov_h) {
camera->setProjectionMatrixAsPerspective(
posdir.fov_v * posdir.fov_h / (fovy * aspect_ratio),
aspect_ratio,
zNear,
zFar
);
}
}
}
else {
camera->setProjectionMatrixAsPerspective(
posdir.fov_h / aspect_ratio,
aspect_ratio,
zNear,
zFar
);
}
}
}
osgViewer::View* m_osg_view = nullptr;
simgear::compositor::Compositor* m_compositor = nullptr;
bool m_mouse_button2;
double m_mouse_x = 0;
double m_mouse_y = 0;
static int s_id;
};
/* Converts legacy damping values (e.g. at-model-heading-damping) to a damping
time suitable for use by our Damping class. */
static double legacy_damping_time(double damping)
{
if (damping <= 0) return 0;
return 1 / damping;
}
int SviewView::s_id = 0;
/* A view defined by a series of steps that defines an eye and a target. Used
for clones of main window views. */
struct SviewViewEyeTarget : SviewView
{
SviewViewEyeTarget(osgViewer::View* view, SGPropertyNode* config)
:
SviewView(view)
{
if (config->getStringValue("type") == "legacy")
{
/* Legacy view. */
std::string callsign = config->getStringValue("callsign");
std::string callsign_desc = (callsign == "") ? "" : ": " + callsign;
SG_LOG(SG_VIEW, SG_INFO, "callsign=" << callsign);
if (config->getBoolValue("view/config/eye-fixed")) {
SG_LOG(SG_VIEW, SG_INFO, "eye-fixed");
m_steps.m_name = std::string() + "legacy tower" + callsign_desc;
if (config->getStringValue("view/type") == "lookat") {
/* E.g. Tower view or Tower view AGL. */
/* Add a step to move to centre of aircraft. target offsets appear
to have reversed sign compared to what we require. */
m_steps.add_step(new SviewStepAircraft(callsign));
m_steps.add_step(new SviewStepMove(
-config->getDoubleValue("view/config/target-z-offset-m"),
-config->getDoubleValue("view/config/target-y-offset-m"),
-config->getDoubleValue("view/config/target-x-offset-m")
));
/* Add a step to set pitch and roll to zero, otherwise view
from tower (as calculated by SviewStepFinal) rolls/pitches
with aircraft. */
m_steps.add_step(new SviewStepDirectionMultiply(
1 /* heading */,
0 /* pitch */,
0 /* roll */
));
/* Current position is the target, so add a step that copies it to
SviewPosDir.target. */
m_steps.add_step(new SviewStepCopyToTarget);
/* Added steps to set .m_eye up so that it looks from the nearest
tower. */
m_steps.add_step(new SviewStepNearestTower(callsign));
if (config->getBoolValue("view/config/lookat-agl")) {
double damping = config->getDoubleValue("view/config/lookat-agl-damping");
double damping_time = log(10) * legacy_damping_time(damping);
SG_LOG(SG_VIEW, SG_DEBUG, "lookat-agl");
m_steps.add_step(new SviewStepAGL(callsign, damping_time));
}
m_steps.add_step(new SviewStepFinal);
/* Would be nice to add a step that moves towards the
target a little, like we do with Tower view look from. But
simply adding a SviewStepMove doesn't work because the
preceding SviewStepFinal has finalised the view angle etc.
*/
}
else {
/* E.g. Tower view look from. */
m_steps.add_step(new SviewStepNearestTower(callsign));
/* Looks like Tower view look from's heading-offset is reversed. */
m_steps.add_step(new SviewStepRotate(
-globals->get_props()->getDoubleValue("sim/current-view/heading-offset-deg"),
globals->get_props()->getDoubleValue("sim/current-view/pitch-offset-deg"),
globals->get_props()->getDoubleValue("sim/current-view/roll-offset-deg")
));
/* Move forward a little as though one was walking towards
the window inside the tower; this might improve view of
aircraft near the tower on the ground. Ideally each tower
would have a 'diameter' property, but for now we just use
a hard-coded value. Also it would be nice to make this
movement not change the height. */
m_steps.add_step(new SviewStepMove(1, 0, 0));
m_steps.add_step(new SviewStepMouseDrag(
1 /*mouse_heading_scale*/,
1 /*mouse_pitch_scale*/
));
m_steps.add_step(new SviewStepFinal);
}
}
else {
SG_LOG(SG_VIEW, SG_INFO, "not eye-fixed");
/* E.g. Pilot view and Helicopter/Chase views. */
SGPropertyNode* global_sim_view = globals->get_props()
->getNode("sim/view", config->getIntValue("view-number-raw"));
if (global_sim_view->getStringValue("type") == "lookat") {
/* E.g. Helicopter view and Chase views. */
m_steps.m_name = std::string() + "legacy helicopter/chase" + callsign_desc;
m_steps.add_step(new SviewStepAircraft(callsign));
/* Move to centre of aircraft. config/target-z-offset-m
seems to use +ve to indicate movement backwards relative
to the aircraft, so we need to negate the value we pass to
SviewStepMove(). */
m_steps.add_step(new SviewStepMove(
-config->getDoubleValue("view/config/target-z-offset-m"),
-config->getDoubleValue("view/config/target-y-offset-m"),
-config->getDoubleValue("view/config/target-x-offset-m")
));
/* Add a step that crudely preserves or don't preserve
aircraft's heading, pitch and roll; this enables us to mimic
Helicopter and Chase views. In theory we should evaluate the
specified paths, but in practise we only need to multiply
current values by 0 or 1. */
assert(global_sim_view);
m_steps.add_step(new SviewStepDirectionMultiply(
global_sim_view->getStringValue("config/eye-heading-deg-path")[0] ? 1 : 0,
global_sim_view->getStringValue("config/eye-pitch-deg-path")[0] ? 1 : 0,
global_sim_view->getStringValue("config/eye-roll-deg-path")[0] ? 1 : 0
));
/* Add a step that applies the current view rotation. We
use -ve heading and pitch because in the legacy
viewing system's Helicopter view etc, increments to
heading-offset-deg and pitch-offset-deg actually correspond
to a decreasing actual heading and pitch values. */
double damping_heading = legacy_damping_time(config->getDoubleValue("view/config/at-model-heading-damping"));
double damping_pitch = legacy_damping_time(config->getDoubleValue("view/config/at-model-pitch-damping"));
double damping_roll = legacy_damping_time(config->getDoubleValue("view/config/at-model-roll-damping"));
m_steps.add_step(new SviewStepRotate(
-globals->get_props()->getDoubleValue("sim/current-view/heading-offset-deg"),
-globals->get_props()->getDoubleValue("sim/current-view/pitch-offset-deg"),
globals->get_props()->getDoubleValue("sim/current-view/roll-offset-deg"),
damping_heading,
damping_pitch,
damping_roll
));
m_steps.add_step(new SviewStepMouseDrag(
1 /*mouse_heading_scale*/,
-1 /*mouse_pitch_scale*/
));
/* Set current position as target. This isn't actually
necessary for this view because the direction implied by
heading/pitch/roll will still point to the centre of the
aircraft after the following steps. But it allows double
views to work better - they will use the centre of the
aircraft instead of the eye position. */
m_steps.add_step(new SviewStepCopyToTarget);
/* Add step that moves eye away from aircraft.
config/z-offset-m defaults to /sim/chase-distance-m (see
fgdata:defaults.xml) which is -ve, e.g. -25m. */
m_steps.add_step(new SviewStepMove(
config->getDoubleValue("view/config/z-offset-m"),
-config->getDoubleValue("view/config/y-offset-m"),
config->getDoubleValue("view/config/x-offset-m")
));
/* Finally add a step that converts
lat,lon,height,heading,pitch,roll into SGVec3d position and
SGQuatd orientation. */
m_steps.add_step(new SviewStepFinal);
}
else {
/* E.g. pilot view.
Add steps to move to the pilot's eye position.
config/z-offset-m seems to be +ve when moving backwards
relative to the aircraft, so we need to negate the value we
pass to SviewStepMove(). */
m_steps.m_name = std::string() + "legacy pilot" + callsign_desc;
m_steps.add_step(new SviewStepAircraft(callsign));
m_steps.add_step(new SviewStepMove(
-config->getDoubleValue("view/config/z-offset-m"),
-config->getDoubleValue("view/config/y-offset-m"),
-config->getDoubleValue("view/config/x-offset-m")
));
double current_heading_offset = globals->get_props()->getDoubleValue("sim/current-view/heading-offset-deg");
double current_pitch_offset = globals->get_props()->getDoubleValue("sim/current-view/pitch-offset-deg");
double current_roll_offset = globals->get_props()->getDoubleValue("sim/current-view/roll-offset-deg");
/*double default_heading_offset = config->getDoubleValue("view/config/heading-offset-deg");
double default_pitch_offset = config->getDoubleValue("view/config/pitch-offset-deg");
double default_roll_offset = config->getDoubleValue("view/config/roll-offset-deg");*/
/* Apply final rotation. */
m_steps.add_step(new SviewStepRotate(
current_heading_offset,
current_pitch_offset,
current_roll_offset
));
m_steps.add_step(new SviewStepMouseDrag(
1 /*mouse_heading_scale*/,
1 /*mouse_pitch_scale*/
));
m_steps.add_step(new SviewStepFinal);
SG_LOG(SG_VIEW, SG_INFO, "m_steps=" << m_steps);
}
}
}
else
{
/* New-style Sview specification, where each step is specified for
us. */
simgear::PropertyList steps = config->getChildren("step");
if (steps.empty()) {
throw std::runtime_error(std::string() + "No steps specified");
}
for (SGPropertyNode* step: steps) {
std::string type = step->getStringValue("type");
if (0) {}
else if (type == "aircraft") {
std::string callsign = step->getStringValue("callsign");
m_steps.add_step(new SviewStepAircraft(callsign));
if (callsign != "") {
m_steps.m_name += " callsign=" + callsign;
}
}
else if (type == "move") {
m_steps.add_step(new SviewStepMove(
step->getDoubleValue("forward"),
step->getDoubleValue("up"),
step->getDoubleValue("right")
));
}
else if (type == "direction-multiply") {
m_steps.add_step(new SviewStepDirectionMultiply(
step->getDoubleValue("heading"),
step->getDoubleValue("pitch"),
step->getDoubleValue("roll")
));
}
else if (type == "copy-to-target") {
m_steps.add_step(new SviewStepCopyToTarget);
}
else if (type == "nearest-tower") {
std::string callsign = step->getStringValue("callsign");
m_steps.add_step(new SviewStepNearestTower(callsign));
m_steps.m_name += " tower";
if (callsign != "") m_steps.m_name += " callsign=" + callsign;
}
else if (type == "rotate") {
m_steps.add_step(new SviewStepRotate(
step->getDoubleValue("heading"),
step->getDoubleValue("pitch"),
step->getDoubleValue("roll"),
step->getDoubleValue("damping-heading"),
step->getDoubleValue("damping-pitch"),
step->getDoubleValue("damping-roll")
));
}
else if (type == "rotate-current-view") {
m_steps.add_step(new SviewStepRotate(
globals->get_props()->getDoubleValue("heading"),
globals->get_props()->getDoubleValue("pitch"),
globals->get_props()->getDoubleValue("roll")
));
}
else if (type == "mouse-drag")
{
m_steps.add_step(new SviewStepMouseDrag(
step->getDoubleValue("heading-scale", 1),
step->getDoubleValue("pitch-scale", 1)
));
}
else if (type == "double") {
m_steps.add_step(new SviewStepDouble(step));
m_steps.m_name += " double";
}
else if (type == "agl") {
std::string callsign = step->getStringValue("callsign");
double damping_time = step->getDoubleValue("damping-time");
m_steps.add_step(new SviewStepAGL(callsign, damping_time));
}
else {
throw std::runtime_error(std::string() + "Unrecognised step name: '" + type + "'");
}
}
m_steps.add_step(new SviewStepFinal);
}
SG_LOG(SG_VIEW, SG_DEBUG, "m_steps=" << m_steps);
}
/* Construct a double view using two step sequences. */
SviewViewEyeTarget(
osgViewer::View* view,
SGPropertyNode* config,
SviewSteps& a,
SviewSteps& b
)
:
SviewView(view)
{
std::string type = config->getStringValue("type");
if (type.empty()) {
throw std::runtime_error("double-type not specified");
}
if (type == "last_pair" || type == "last_pair_double")
{
/* Copy steps from <b> that will set .target. */
for (auto step: b.m_steps)
{
if (0
|| dynamic_cast<SviewStepCopyToTarget*>(step.get())
|| dynamic_cast<SviewStepFinal*>(step.get())
)
{
break;
}
m_steps.add_step(step);
}
m_steps.add_step(new SviewStepCopyToTarget);
/* Copy steps from <a> that will set .position. */
for (auto step: a.m_steps)
{
if (0
|| dynamic_cast<SviewStepCopyToTarget*>(step.get())
|| dynamic_cast<SviewStepFinal*>(step.get())
)
{
break;
}
m_steps.add_step(step);
}
if (type == "last_pair_double")
{
/* We need a final SviewStepDouble step. */
m_steps.add_step(new SviewStepDouble);
m_steps.m_name = std::string() + " double: " + a.m_name + " - " + b.m_name;
}
else
{
m_steps.m_name = std::string() + " pair: " + a.m_name + " - " + b.m_name;
}
m_steps.add_step(new SviewStepFinal);
SG_LOG(SG_VIEW, SG_INFO, " m_steps:" << m_steps);
}
else {
throw std::runtime_error(std::string("Unrecognised double view: ") + type);
}
/* Disable our mouse_drag() method - doesn't make sense for double views. */
m_mouse_drag = false;
SG_LOG(SG_VIEW, SG_DEBUG, "m_steps=" << m_steps);
}
const std::string description() override
{
return m_steps.m_name;
}
bool update(double dt) override
{
bool valid = m_osg_view->getCamera()->getGraphicsContext()->valid();
SG_LOG(SG_VIEW, SG_BULK, "valid=" << valid);
if (!valid) return false;
SviewPosDir posdir;
bool debug = false;
if (m_debug) {
time_t t = time(NULL) / 10;
if (t != m_debug_time) {
m_debug_time = t;
debug = true;
}
}
if (debug) SG_LOG(SG_VIEW, SG_INFO, "evaluating m_steps:");
m_steps.evaluate(posdir, dt, debug);
posdir_to_view(posdir);
return true;
}
void mouse_drag(double delta_x_deg, double delta_y_deg) override
{
if (!m_mouse_drag) return;
for (auto step: m_steps.m_steps) {
step->mouse_drag(delta_x_deg, delta_y_deg);
}
}
SviewSteps m_steps;
bool m_mouse_drag = true;
bool m_debug = false;
time_t m_debug_time = 0;
};
/* All our view windows. */
static std::vector<std::shared_ptr<SviewView>> s_views;
/* Recent views, for use by SviewAddLastPair(). */
static std::deque<std::shared_ptr<SviewViewEyeTarget>> s_recent_views;
/* Returns an independent property tree that defines an Sview that is a copy of
the current view. */
static SGPropertyNode_ptr SviewConfigForCurrentView()
{
SGPropertyNode_ptr config = new SGPropertyNode;
int view_number_raw = globals->get_props()->getIntValue("/sim/current-view/view-number-raw");
config->setIntValue("view-number-raw", view_number_raw);
SGPropertyNode* global_view = globals->get_props()->getNode("/sim/view", view_number_raw /*index*/, true /*create*/);
std::string root_path = global_view->getStringValue("config/root"); /* "" or /ai/models/multiplayer[]. */
SGPropertyNode* root = globals->get_props()->getNode(root_path);
std::string callsign = root->getStringValue("callsign");
config->setStringValue("type", "legacy");
config->setStringValue("callsign", callsign);
SGPropertyNode* config_view = config->getNode("view", true /*create*/);
if (callsign == "") {
/* User aircraft. */
copyProperties(globals->get_props()->getNode("/sim/view", view_number_raw), config_view);
}
else {
/* Multiplayer aircraft. */
copyProperties(root->getNode("set/sim/view", view_number_raw), config_view);
}
config->setDoubleValue(
"direction-delta/heading",
globals->get_props()->getDoubleValue("sim/current-view/heading-offset-deg")
);
config->setDoubleValue(
"direction-delta/pitch",
globals->get_props()->getDoubleValue("sim/current-view/pitch-offset-deg")
);
config->setDoubleValue(
"direction-delta/roll",
globals->get_props()->getDoubleValue("sim/current-view/roll-offset-deg")
);
config->setDoubleValue("zoom-delta", 1);
SG_LOG(SG_VIEW, SG_INFO, "returning:\n" << writePropertiesInline(config, true /*write_all*/));
return config;
}
void SviewPush()
{
if (s_recent_views.size() >= 2) {
s_recent_views.pop_front();
}
/* Make a dummy view whose eye and target members will copy the current
view. */
SGPropertyNode_ptr config = SviewConfigForCurrentView();
std::shared_ptr<SviewViewEyeTarget> v(new SviewViewEyeTarget(nullptr /*view*/, config));
s_recent_views.push_back(v);
SG_LOG(SG_VIEW, SG_DEBUG, "Have pushed view: " << v);
}
void SviewUpdate(double dt)
{
bool verbose = 0;
for (size_t i=0; i<s_views.size(); /* incremented in loop*/) {
if (verbose) {
SG_LOG(SG_VIEW, SG_INFO, "updating i=" << i
<< ": " << s_views[i]->m_osg_view
<< ' ' << s_views[i]->m_compositor
);
}
bool valid = s_views[i]->update(dt);
if (valid) {
const osg::Matrix& view_matrix = s_views[i]->m_osg_view->getCamera()->getViewMatrix();
const osg::Matrix& projection_matrix = s_views[i]->m_osg_view->getCamera()->getProjectionMatrix();
s_views[i]->m_compositor->update(view_matrix, projection_matrix);
i += 1;
}
else {
/* Window has been closed.
This doesn't work reliably with OpenSceneGraph-3.4. We seem to
sometimes get valid=false when a different window from s_views[i]
is closed. And sometimes OSG seems to end up sending a close-window
even to the main window when the user has closed an sview window.
*/
auto view_it = s_views.begin() + i;
SG_LOG(SG_VIEW, SG_INFO, "deleting SviewView i=" << i << ": " << (*view_it)->description2());
for (size_t j=0; j<s_views.size(); ++j) {
SG_LOG(SG_VIEW, SG_INFO, " " << j
<< ": " << s_views[j]->m_osg_view
<< ' ' << s_views[j]->m_compositor
);
}
s_views.erase(view_it);
for (size_t j=0; j<s_views.size(); ++j) {
SG_LOG(SG_VIEW, SG_INFO, " " << j
<< ": " << s_views[j]->m_osg_view
<< ' ' << s_views[j]->m_compositor
);
}
verbose = true;
}
}
}
void SviewClear()
{
s_views.clear();
}
/* These are set by SViewSetCompositorParams(). */
static osg::ref_ptr<simgear::SGReaderWriterOptions> s_compositor_options;
static std::string s_compositor_path;
struct EventHandler : osgGA::EventHandler
{
bool handle(osgGA::Event* event, osg::Object* object, osg::NodeVisitor* nv) override
{
osgGA::GUIEventAdapter* ea = event->asGUIEventAdapter();
if (ea) {
osgGA::GUIEventAdapter::EventType et = ea->getEventType();
if (et != osgGA::GUIEventAdapter::FRAME) {
SG_LOG(SG_GENERAL, SG_BULK, "sview event handler called. ea->getEventType()=" << ea->getEventType());
}
}
else {
SG_LOG(SG_GENERAL, SG_BULK, "sview event handler called...");
}
return true;
}
};
#include "Viewer/FGEventHandler.hxx"
std::shared_ptr<SviewView> SviewCreate(SGPropertyNode* config)
{
assert(config);
FGRenderer* renderer = globals->get_renderer();
osgViewer::ViewerBase* viewer_base = renderer->getViewerBase();
osgViewer::CompositeViewer* composite_viewer = dynamic_cast<osgViewer::CompositeViewer*>(viewer_base);
if (!composite_viewer) {
SG_LOG(SG_GENERAL, SG_ALERT, "SviewCreate() doing nothing because not composite-viewer mode not enabled.");
return nullptr;
}
osgViewer::View* main_view = renderer->getView();
osg::Node* scene_data = main_view->getSceneData();
SG_LOG(SG_GENERAL, SG_DEBUG, "main_view->getNumSlaves()=" << main_view->getNumSlaves());
osgViewer::View* view = new osgViewer::View();
flightgear::FGEventHandler* event_handler = globals->get_renderer()->getEventHandler();
view->addEventHandler(event_handler);
std::shared_ptr<SviewView> sview_view;
std::string type = config->getStringValue("type");
SG_LOG(SG_VIEW, SG_DEBUG, "type=" << type);
if (0) {
}
else if (type == "current") {
SGPropertyNode_ptr config2 = SviewConfigForCurrentView();
copyProperties(config, config2);
config2->setStringValue("type", "legacy"); /* restore it after copy() sets to "current". */
sview_view.reset(new SviewViewEyeTarget(view, config2));
}
else if (type == "last_pair") {
if (s_recent_views.size() < 2) {
SG_LOG(SG_VIEW, SG_ALERT, "Need two pushed views");
return nullptr;
}
auto it = s_recent_views.end();
std::shared_ptr<SviewViewEyeTarget> target = *(--it);
std::shared_ptr<SviewViewEyeTarget> eye = *(--it);
sview_view.reset(new SviewViewEyeTarget(view, config, eye->m_steps, target->m_steps));
}
else if (type == "last_pair_double") {
if (s_recent_views.size() < 2) {
SG_LOG(SG_VIEW, SG_ALERT, "Need two pushed views");
return nullptr;
}
auto it = s_recent_views.end();
std::shared_ptr<SviewViewEyeTarget> remote = *(--it);
std::shared_ptr<SviewViewEyeTarget> local = *(--it);
sview_view.reset(new SviewViewEyeTarget(view, config, local->m_steps, remote->m_steps));
}
else {
SG_LOG(SG_VIEW, SG_ALERT, "config is:\n" << writePropertiesInline(config, true /*write_all*/));
sview_view.reset(new SviewViewEyeTarget(view, config));
}
osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
osg::ref_ptr<osg::GraphicsContext> gc;
/* When we implement canvas views, we won't create a new window here. */
if (1) {
/* Create a new window. */
osg::GraphicsContext::WindowingSystemInterface* wsi = osg::GraphicsContext::getWindowingSystemInterface();
assert(wsi);
flightgear::WindowBuilder* window_builder = flightgear::WindowBuilder::getWindowBuilder();
flightgear::GraphicsWindow* main_window = window_builder->getDefaultWindow();
osg::ref_ptr<osg::GraphicsContext> main_gc = main_window->gc;
const osg::GraphicsContext::Traits* main_traits = main_gc->getTraits();
/* Arbitrary initial position of new window. */
traits->x = config->getIntValue("window-x", 100);
traits->y = config->getIntValue("window-y", 100);
/* We set new window size as fraction of main window. This keeps the
aspect ratio of new window same as that of the main window which allows
us to use main window's projection matrix directly. Presumably we could
calculate a suitable projection matrix to match an arbitrary aspect
ratio, but i don't know the maths well enough to do this. */
traits->width = config->getIntValue("window-width", main_traits->width / 2);
traits->height = config->getIntValue("window-height", main_traits->height / 2);
traits->windowDecoration = true;
traits->doubleBuffer = true;
traits->sharedContext = 0;
traits->readDISPLAY();
if (traits->displayNum < 0) traits->displayNum = 0;
if (traits->screenNum < 0) traits->screenNum = 0;
int bpp = fgGetInt("/sim/rendering/bits-per-pixel");
int cbits = (bpp <= 16) ? 5 : 8;
int zbits = (bpp <= 16) ? 16 : 24;
traits->red = traits->green = traits->blue = cbits;
traits->depth = zbits;
traits->mipMapGeneration = true;
traits->windowName = "Flightgear " + sview_view->description2();
traits->sampleBuffers = fgGetInt("/sim/rendering/multi-sample-buffers", traits->sampleBuffers);
traits->samples = fgGetInt("/sim/rendering/multi-samples", traits->samples);
traits->vsync = fgGetBool("/sim/rendering/vsync-enable", traits->vsync);
traits->stencil = 8;
gc = osg::GraphicsContext::createGraphicsContext(traits);
assert(gc.valid());
}
/* need to ensure that the window is cleared make sure that the complete
window is set the correct colour rather than just the parts of the window
that are under the camera's viewports. */
//gc->setClearColor(osg::Vec4f(0.2f,0.2f,0.6f,1.0f));
//gc->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
view->setSceneData(scene_data);
view->setDatabasePager(FGScenery::getPagerSingleton());
/* https://www.mail-archive.com/osg-users@lists.openscenegraph.org/msg29820.html
Passing (false, false) here seems to cause a hang on startup. */
view->getDatabasePager()->setUnrefImageDataAfterApplyPolicy(true, false);
osg::GraphicsContext::createNewContextID();
osg::Camera* main_camera = main_view->getCamera();
osg::Camera* camera = view->getCamera();
camera->setGraphicsContext(gc.get());
if (0) {
/* Show the projection matrix. */
double left;
double right;
double bottom;
double top;
double zNear;
double zFar;
auto projection_matrix = main_camera->getProjectionMatrix();
bool ok = projection_matrix.getFrustum(left, right, bottom, top, zNear, zFar);
SG_LOG(SG_GENERAL, SG_ALERT, "projection_matrix:"
<< " ok=" << ok
<< " left=" << left
<< " right=" << right
<< " bottom=" << bottom
<< " top=" << top
<< " zNear=" << zNear
<< " zFar=" << zFar
);
}
camera->setProjectionMatrix(main_camera->getProjectionMatrix());
camera->setViewMatrix(main_camera->getViewMatrix());
camera->setCullMask(0xffffffff);
camera->setCullMaskLeft(0xffffffff);
camera->setCullMaskRight(0xffffffff);
/* This appears to avoid unhelpful culling of nearby objects. Though the
above SG_LOG() says zNear=0.1 zFar=120000, so not sure what's going on. */
camera->setComputeNearFarMode(osgUtil::CullVisitor::DO_NOT_COMPUTE_NEAR_FAR);
/*
from CameraGroup::buildGUICamera():
camera->setInheritanceMask(osg::CullSettings::ALL_VARIABLES
& ~(osg::CullSettings::COMPUTE_NEAR_FAR_MODE
| osg::CullSettings::CULLING_MODE
| osg::CullSettings::CLEAR_MASK
));
camera->setCullingMode(osg::CullSettings::NO_CULLING);
main_viewport seems to be null so this doesn't work.
osg::Viewport* main_viewport = main_view->getCamera()->getViewport();
SG_LOG(SG_GENERAL, SG_ALERT, "main_viewport=" << main_viewport);
osg::Viewport* viewport = new osg::Viewport(*main_viewport);
view->getCamera()->setViewport(viewport);
*/
view->getCamera()->setViewport(0, 0, traits->width, traits->height);
view->setName("Cloned view");
view->setFrameStamp(composite_viewer->getFrameStamp());
simgear::compositor::Compositor* compositor = simgear::compositor::Compositor::create(
view,
gc,
view->getCamera()->getViewport(),
s_compositor_path,
s_compositor_options
);
sview_view->m_compositor = compositor;
s_views.push_back(sview_view);
/* stop/start threading:
https://www.mail-archive.com/osg-users@lists.openscenegraph.org/msg54341.html
*/
composite_viewer->stopThreading();
composite_viewer->addView(view);
composite_viewer->startThreading();
SG_LOG(SG_GENERAL, SG_DEBUG, "main_view->getNumSlaves()=" << main_view->getNumSlaves());
SG_LOG(SG_GENERAL, SG_DEBUG, "view->getNumSlaves()=" << view->getNumSlaves());
SG_LOG(SG_VIEW, SG_DEBUG, "have added extra view. views are now:");
for (unsigned i=0; i<composite_viewer->getNumViews(); ++i) {
osgViewer::View* view = composite_viewer->getView(i);
SG_LOG(SG_VIEW, SG_DEBUG, "composite_viewer view i=" << i << " view=" << view);
}
return sview_view;
}
std::shared_ptr<SviewView> SviewCreate(const SGPropertyNode* config)
{
return SviewCreate(const_cast<SGPropertyNode*>(config));
}
simgear::compositor::Compositor* SviewGetEventViewport(const osgGA::GUIEventAdapter& ea)
{
const osg::GraphicsContext* gc = ea.getGraphicsContext();
for (std::shared_ptr<SviewView> sview_view: s_views) {
if (sview_view->m_compositor->getGraphicsContext() == gc) {
return sview_view->m_compositor;
}
}
return nullptr;
}
void SViewSetCompositorParams(
osg::ref_ptr<simgear::SGReaderWriterOptions> options,
const std::string& compositor_path
)
{
s_compositor_options = options;
s_compositor_path = compositor_path;
}
bool SviewMouseMotion(int x, int y, const osgGA::GUIEventAdapter& ea)
{
const osg::GraphicsContext* gc = ea.getGraphicsContext();
std::shared_ptr<SviewView> sview_view;
for (auto v: s_views) {
if (v->m_compositor->getGraphicsContext() == gc) {
sview_view = v;
break;
}
}
if (!sview_view) {
return false;
}
double xx;
double yy;
if (!flightgear::eventToWindowCoords(&ea, xx, yy)) {
return false;
}
SG_LOG(SG_GENERAL, SG_DEBUG, "sview_view=" << sview_view.get() << " xx=" << xx << " yy=" << yy);
bool button2 = globals->get_props()->getBoolValue("/devices/status/mice/mouse/button[2]");
if (button2 && sview_view->m_mouse_button2) {
/* Button2 drag. */
double delta_x = xx - sview_view->m_mouse_x;
double delta_y = yy - sview_view->m_mouse_y;
if (delta_x || delta_y) {
/* Convert delta (which is mouse movement in pixels) to a change
in viewing angle in degrees. We do this using the fov and window
size so that the result is the image moving as though it was being
panned by the mouse movement. So angle changes are smaller for
high zoom values or small windows, making high zoom views easy to
control. */
osg::Camera* camera = sview_view->m_osg_view->getCamera();
double fov_y;
double aspect_ratio;
double z_near;
double z_far;
camera->getProjectionMatrixAsPerspective(fov_y, aspect_ratio, z_near, z_far);
double fov_x = fov_y * aspect_ratio;
simgear::compositor::Compositor* compositor = sview_view->m_compositor;
osg::Viewport* viewport = compositor->getViewport();
double delta_x_deg = delta_x / viewport->width() * fov_x;
double delta_y_deg = delta_y / viewport->height() * fov_y;
/* Scale movement a little to make things more similar to normal
operation. */
double scale = 5;
delta_x_deg *= scale;
delta_y_deg *= scale;
sview_view->mouse_drag(delta_x_deg, delta_y_deg);
}
}
sview_view->m_mouse_button2 = button2;
sview_view->m_mouse_x = xx;
sview_view->m_mouse_y = yy;
return true;
}