]> git.mxchange.org Git - flightgear.git/blob - src/Main/viewer.cxx
7e243c05d8e452a376f083b8055d1c59fcaf45e5
[flightgear.git] / src / Main / viewer.cxx
1 // viewer.cxx -- class for managing a viewer in the flightgear world.
2 //
3 // Written by Curtis Olson, started August 1997.
4 //                          overhaul started October 2000.
5 //   partially rewritten by Jim Wilson jim@kelcomaine.com using interface
6 //                          by David Megginson March 2002
7 //
8 // Copyright (C) 1997 - 2000  Curtis L. Olson  - http://www.flightgear.org/~curt
9 //
10 // This program is free software; you can redistribute it and/or
11 // modify it under the terms of the GNU General Public License as
12 // published by the Free Software Foundation; either version 2 of the
13 // License, or (at your option) any later version.
14 //
15 // This program is distributed in the hope that it will be useful, but
16 // WITHOUT ANY WARRANTY; without even the implied warranty of
17 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 // General Public License for more details.
19 //
20 // You should have received a copy of the GNU General Public License
21 // along with this program; if not, write to the Free Software
22 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
23 //
24 // $Id$
25
26 #ifdef HAVE_CONFIG_H
27 #  include "config.h"
28 #endif
29
30 #include <simgear/compiler.h>
31
32 #include "fg_props.hxx"
33
34 #ifdef HAVE_CONFIG_H
35 #  include <config.h>
36 #endif
37
38 #include <simgear/debug/logstream.hxx>
39 #include <simgear/constants.h>
40 #include <simgear/math/polar3d.hxx>
41 #include <simgear/math/sg_geodesy.hxx>
42 #include <simgear/scene/model/location.hxx>
43 #include <simgear/scene/model/placement.hxx>
44 #include <simgear/math/vector.hxx>
45
46 #include <Main/globals.hxx>
47 #include <Scenery/scenery.hxx>
48 #include <Model/acmodel.hxx>
49
50 #include "viewer.hxx"
51
52 #include "CameraGroup.hxx"
53
54 using namespace flightgear;
55 \f
56 ////////////////////////////////////////////////////////////////////////
57 // Implementation of FGViewer.
58 ////////////////////////////////////////////////////////////////////////
59
60 // Constructor...
61 FGViewer::FGViewer( fgViewType Type, bool from_model, int from_model_index,
62                     bool at_model, int at_model_index,
63                     double damp_roll, double damp_pitch, double damp_heading,
64                     double x_offset_m, double y_offset_m, double z_offset_m,
65                     double heading_offset_deg, double pitch_offset_deg,
66                     double roll_offset_deg,
67                     double fov_deg, double aspect_ratio_multiplier,
68                     double target_x_offset_m, double target_y_offset_m,
69                     double target_z_offset_m, double near_m, bool internal ):
70     _dirty(true),
71     _lon_deg(0),
72     _lat_deg(0),
73     _alt_ft(0),
74     _target_lon_deg(0),
75     _target_lat_deg(0),
76     _target_alt_ft(0),
77     _roll_deg(0),
78     _pitch_deg(0),
79     _heading_deg(0),
80     _damp_sync(0),
81     _damp_roll(0),
82     _damp_pitch(0),
83     _damp_heading(0),
84     _scaling_type(FG_SCALING_MAX),
85     _location(0),
86     _target_location(0),
87     _cameraGroup(CameraGroup::getDefault())
88 {
89     _absolute_view_pos = SGVec3d(0, 0, 0);
90     _type = Type;
91     _from_model = from_model;
92     _from_model_index = from_model_index;
93     _at_model = at_model;
94     _at_model_index = at_model_index;
95
96     _internal = internal;
97
98     if (damp_roll > 0.0)
99       _damp_roll = 1.0 / pow(10.0, fabs(damp_roll));
100     if (damp_pitch > 0.0)
101       _damp_pitch = 1.0 / pow(10.0, fabs(damp_pitch));
102     if (damp_heading > 0.0)
103       _damp_heading = 1.0 / pow(10.0, fabs(damp_heading));
104
105     _offset_m.x() = x_offset_m;
106     _offset_m.y() = y_offset_m;
107     _offset_m.z() = z_offset_m;
108     _heading_offset_deg = heading_offset_deg;
109     _pitch_offset_deg = pitch_offset_deg;
110     _roll_offset_deg = roll_offset_deg;
111     _goal_heading_offset_deg = heading_offset_deg;
112     _goal_pitch_offset_deg = pitch_offset_deg;
113     _goal_roll_offset_deg = roll_offset_deg;
114     if (fov_deg > 0) {
115       _fov_deg = fov_deg;
116     } else {
117       _fov_deg = 55;
118     }
119     _aspect_ratio_multiplier = aspect_ratio_multiplier;
120     _target_offset_m.x() = target_x_offset_m;
121     _target_offset_m.y() = target_y_offset_m;
122     _target_offset_m.z() = target_z_offset_m;
123     _ground_level_nearplane_m = near_m;
124     // a reasonable guess for init, so that the math doesn't blow up
125 }
126
127
128 // Destructor
129 FGViewer::~FGViewer( void ) {
130 }
131
132 void
133 FGViewer::init ()
134 {
135   if ( _from_model )
136     _location = (SGLocation *) globals->get_aircraft_model()->get3DModel()->getSGLocation();
137   else
138     _location = new SGLocation;
139
140   if ( _type == FG_LOOKAT ) {
141     if ( _at_model )
142       _target_location = (SGLocation *) globals->get_aircraft_model()->get3DModel()->getSGLocation();
143     else
144       _target_location = (SGLocation *) new SGLocation;
145   }
146 }
147
148 void
149 FGViewer::bind ()
150 {
151 }
152
153 void
154 FGViewer::unbind ()
155 {
156 }
157
158 void
159 FGViewer::setType ( int type )
160 {
161   if (type == 0)
162     _type = FG_LOOKFROM;
163   if (type == 1)
164     _type = FG_LOOKAT;
165 }
166
167 void
168 FGViewer::setInternal ( bool internal )
169 {
170   _internal = internal;
171 }
172
173 void
174 FGViewer::setLongitude_deg (double lon_deg)
175 {
176   _dirty = true;
177   _lon_deg = lon_deg;
178 }
179
180 void
181 FGViewer::setLatitude_deg (double lat_deg)
182 {
183   _dirty = true;
184   _lat_deg = lat_deg;
185 }
186
187 void
188 FGViewer::setAltitude_ft (double alt_ft)
189 {
190   _dirty = true;
191   _alt_ft = alt_ft;
192 }
193
194 void
195 FGViewer::setPosition (double lon_deg, double lat_deg, double alt_ft)
196 {
197   _dirty = true;
198   _lon_deg = lon_deg;
199   _lat_deg = lat_deg;
200   _alt_ft = alt_ft;
201 }
202
203 void
204 FGViewer::setTargetLongitude_deg (double lon_deg)
205 {
206   _dirty = true;
207   _target_lon_deg = lon_deg;
208 }
209
210 void
211 FGViewer::setTargetLatitude_deg (double lat_deg)
212 {
213   _dirty = true;
214   _target_lat_deg = lat_deg;
215 }
216
217 void
218 FGViewer::setTargetAltitude_ft (double alt_ft)
219 {
220   _dirty = true;
221   _target_alt_ft = alt_ft;
222 }
223
224 void
225 FGViewer::setTargetPosition (double lon_deg, double lat_deg, double alt_ft)
226 {
227   _dirty = true;
228   _target_lon_deg = lon_deg;
229   _target_lat_deg = lat_deg;
230   _target_alt_ft = alt_ft;
231 }
232
233 void
234 FGViewer::setRoll_deg (double roll_deg)
235 {
236   _dirty = true;
237   _roll_deg = roll_deg;
238 }
239
240 void
241 FGViewer::setPitch_deg (double pitch_deg)
242 {
243   _dirty = true;
244   _pitch_deg = pitch_deg;
245 }
246
247 void
248 FGViewer::setHeading_deg (double heading_deg)
249 {
250   _dirty = true;
251   _heading_deg = heading_deg;
252 }
253
254 void
255 FGViewer::setOrientation (double roll_deg, double pitch_deg, double heading_deg)
256 {
257   _dirty = true;
258   _roll_deg = roll_deg;
259   _pitch_deg = pitch_deg;
260   _heading_deg = heading_deg;
261 }
262
263 void
264 FGViewer::setTargetRoll_deg (double target_roll_deg)
265 {
266   _dirty = true;
267   _target_roll_deg = target_roll_deg;
268 }
269
270 void
271 FGViewer::setTargetPitch_deg (double target_pitch_deg)
272 {
273   _dirty = true;
274   _target_pitch_deg = target_pitch_deg;
275 }
276
277 void
278 FGViewer::setTargetHeading_deg (double target_heading_deg)
279 {
280   _dirty = true;
281   _target_heading_deg = target_heading_deg;
282 }
283
284 void
285 FGViewer::setTargetOrientation (double target_roll_deg, double target_pitch_deg, double target_heading_deg)
286 {
287   _dirty = true;
288   _target_roll_deg = target_roll_deg;
289   _target_pitch_deg = target_pitch_deg;
290   _target_heading_deg = target_heading_deg;
291 }
292
293 void
294 FGViewer::setXOffset_m (double x_offset_m)
295 {
296   _dirty = true;
297   _offset_m.x() = x_offset_m;
298 }
299
300 void
301 FGViewer::setYOffset_m (double y_offset_m)
302 {
303   _dirty = true;
304   _offset_m.y() = y_offset_m;
305 }
306
307 void
308 FGViewer::setZOffset_m (double z_offset_m)
309 {
310   _dirty = true;
311   _offset_m.z() = z_offset_m;
312 }
313
314 void
315 FGViewer::setTargetXOffset_m (double target_x_offset_m)
316 {
317   _dirty = true;
318   _target_offset_m.x() = target_x_offset_m;
319 }
320
321 void
322 FGViewer::setTargetYOffset_m (double target_y_offset_m)
323 {
324   _dirty = true;
325   _target_offset_m.y() = target_y_offset_m;
326 }
327
328 void
329 FGViewer::setTargetZOffset_m (double target_z_offset_m)
330 {
331   _dirty = true;
332   _target_offset_m.z() = target_z_offset_m;
333 }
334
335 void
336 FGViewer::setPositionOffsets (double x_offset_m, double y_offset_m, double z_offset_m)
337 {
338   _dirty = true;
339   _offset_m.x() = x_offset_m;
340   _offset_m.y() = y_offset_m;
341   _offset_m.z() = z_offset_m;
342 }
343
344 void
345 FGViewer::setRollOffset_deg (double roll_offset_deg)
346 {
347   _dirty = true;
348   _roll_offset_deg = roll_offset_deg;
349 }
350
351 void
352 FGViewer::setPitchOffset_deg (double pitch_offset_deg)
353 {
354   _dirty = true;
355   _pitch_offset_deg = pitch_offset_deg;
356 }
357
358 void
359 FGViewer::setHeadingOffset_deg (double heading_offset_deg)
360 {
361   _dirty = true;
362   _heading_offset_deg = heading_offset_deg;
363 }
364
365 void
366 FGViewer::setGoalRollOffset_deg (double goal_roll_offset_deg)
367 {
368   _dirty = true;
369   _goal_roll_offset_deg = goal_roll_offset_deg;
370 }
371
372 void
373 FGViewer::setGoalPitchOffset_deg (double goal_pitch_offset_deg)
374 {
375   _dirty = true;
376   _goal_pitch_offset_deg = goal_pitch_offset_deg;
377   if ( _goal_pitch_offset_deg < -90 ) {
378     _goal_pitch_offset_deg = -90.0;
379   }
380   if ( _goal_pitch_offset_deg > 90.0 ) {
381     _goal_pitch_offset_deg = 90.0;
382   }
383
384 }
385
386 void
387 FGViewer::setGoalHeadingOffset_deg (double goal_heading_offset_deg)
388 {
389   _dirty = true;
390   _goal_heading_offset_deg = goal_heading_offset_deg;
391   while ( _goal_heading_offset_deg < 0.0 ) {
392     _goal_heading_offset_deg += 360;
393   }
394   while ( _goal_heading_offset_deg > 360 ) {
395     _goal_heading_offset_deg -= 360;
396   }
397 }
398
399 void
400 FGViewer::setOrientationOffsets (double roll_offset_deg, double pitch_offset_deg, double heading_offset_deg)
401 {
402   _dirty = true;
403   _roll_offset_deg = roll_offset_deg;
404   _pitch_offset_deg = pitch_offset_deg;
405   _heading_offset_deg = heading_offset_deg;
406 }
407
408 // recalc() is done every time one of the setters is called (making the 
409 // cached data "dirty") on the next "get".  It calculates all the outputs 
410 // for viewer.
411 void
412 FGViewer::recalc ()
413 {
414   if (_type == FG_LOOKFROM) {
415     recalcLookFrom();
416   } else {
417     recalcLookAt();
418   }
419
420   // Update viewer's postion data for the eye location...
421   _lon_deg = _location->getLongitude_deg();
422   _lat_deg = _location->getLatitude_deg();
423   _alt_ft = _location->getAltitudeASL_ft();
424   _roll_deg = _location->getRoll_deg();
425   _pitch_deg = _location->getPitch_deg();
426   _heading_deg = _location->getHeading_deg();
427
428   // Update viewer's postion data for the target (at object) location
429   if (_type == FG_LOOKAT) {
430     _target_lon_deg = _target_location->getLongitude_deg();
431     _target_lat_deg = _target_location->getLatitude_deg();
432     _target_alt_ft = _target_location->getAltitudeASL_ft();
433     _target_roll_deg = _target_location->getRoll_deg();
434     _target_pitch_deg = _target_location->getPitch_deg();
435     _target_heading_deg = _target_location->getHeading_deg();
436   }
437
438   set_clean();
439 }
440
441 // recalculate for LookFrom view type...
442 void
443 FGViewer::recalcLookFrom ()
444 {
445   // Update location data ...
446   if ( !_from_model ) {
447     _location->setPosition( _lon_deg, _lat_deg, _alt_ft );
448     _location->setOrientation( _roll_deg, _pitch_deg, _heading_deg );
449     _location->getTransformMatrix();
450   }
451   double lat = _location->getLatitude_deg();
452   double lon = _location->getLongitude_deg();
453   double alt = _location->getAltitudeASL_ft();
454   double head = _location->getHeading_deg();
455   double pitch = _location->getPitch_deg();
456   double roll = _location->getRoll_deg();
457   if ( !_from_model ) {
458     // update from our own data...
459     dampEyeData(roll, pitch, head);
460   }
461
462   // The geodetic position of our base view position
463   SGGeod geodPos = SGGeod::fromDegFt(lon, lat, alt);
464   // The rotation rotating from the earth centerd frame to
465   // the horizontal local OpenGL frame
466   SGQuatd hlOr = SGQuatd::viewHL(geodPos);
467
468   // the rotation from the horizontal local frame to the basic view orientation
469   SGQuatd hlToBody = SGQuatd::fromYawPitchRollDeg(head, pitch, roll);
470   hlToBody = SGQuatd::simToView(hlToBody);
471
472   // The cartesian position of the basic view coordinate
473   SGVec3d position = SGVec3d::fromGeod(geodPos);
474   // the rotation offset, don't know why heading is negative here ...
475   SGQuatd viewOffsetOr = SGQuatd::simToView(
476     SGQuatd::fromYawPitchRollDeg(-_heading_offset_deg, _pitch_offset_deg,
477                                  _roll_offset_deg));
478
479   // Compute the eyepoints orientation and position
480   // wrt the earth centered frame - that is global coorinates
481   SGQuatd ec2body = hlOr*hlToBody;
482   _absolute_view_pos = position + ec2body.backTransform(_offset_m);
483   mViewOrientation = ec2body*viewOffsetOr;
484 }
485
486 void
487 FGViewer::recalcLookAt ()
488 {
489   // The geodetic position of our target to look at
490   SGGeod geodTargetPos;
491   SGQuatd geodTargetOr;
492   if ( _at_model ) {
493     geodTargetPos = SGGeod::fromDegFt(_target_location->getLongitude_deg(),
494                                       _target_location->getLatitude_deg(),
495                                       _target_location->getAltitudeASL_ft());
496     double head = _target_location->getHeading_deg();
497     double pitch = _target_location->getPitch_deg();
498     double roll = _target_location->getRoll_deg();
499     geodTargetOr = SGQuatd::fromYawPitchRollDeg(head, pitch, roll);
500   } else {
501     dampEyeData(_target_roll_deg, _target_pitch_deg, _target_heading_deg);
502     _target_location->setPosition( _target_lon_deg, _target_lat_deg, _target_alt_ft );
503     _target_location->setOrientation( _target_roll_deg, _target_pitch_deg, _target_heading_deg );
504     _target_location->getTransformMatrix();
505
506     // if not model then calculate our own target position...
507     geodTargetPos = SGGeod::fromDegFt(_target_lon_deg,
508                                       _target_lat_deg,
509                                       _target_alt_ft);
510     geodTargetOr = SGQuatd::fromYawPitchRollDeg(_target_heading_deg,
511                                                 _target_pitch_deg,
512                                                 _target_roll_deg);
513   }
514   SGQuatd geodTargetHlOr = SGQuatd::fromLonLat(geodTargetPos);
515
516
517   SGGeod geodEyePos;
518   SGQuatd geodEyeOr;
519   if ( _from_model ) {
520     geodEyePos = SGGeod::fromDegFt(_location->getLongitude_deg(),
521                                    _location->getLatitude_deg(),
522                                    _location->getAltitudeASL_ft());
523     double head = _location->getHeading_deg();
524     double pitch = _location->getPitch_deg();
525     double roll = _location->getRoll_deg();
526     geodEyeOr = SGQuatd::fromYawPitchRollDeg(head, pitch, roll);
527   } else {
528     dampEyeData(_roll_deg, _pitch_deg, _heading_deg);
529     _location->setPosition( _lon_deg, _lat_deg, _alt_ft );
530     _location->setOrientation( _roll_deg, _pitch_deg, _heading_deg );
531     _location->getTransformMatrix();
532
533     // update from our own data, just the rotation here...
534     geodEyePos = SGGeod::fromDegFt(_lon_deg, _lat_deg, _alt_ft);
535     geodEyeOr = SGQuatd::fromYawPitchRollDeg(_heading_deg,
536                                              _pitch_deg,
537                                              _roll_deg);
538   }
539   SGQuatd geodEyeHlOr = SGQuatd::fromLonLat(geodEyePos);
540
541   // the rotation offset, don't know why heading is negative here ...
542   SGQuatd eyeOffsetOr =
543     SGQuatd::fromYawPitchRollDeg(-_heading_offset_deg + 180, _pitch_offset_deg,
544                                  _roll_offset_deg);
545
546   // Offsets to the eye position
547   SGVec3d eyeOff(-_offset_m.z(), _offset_m.x(), -_offset_m.y());
548   SGQuatd ec2eye = geodEyeHlOr*geodEyeOr;
549   SGVec3d eyeCart = SGVec3d::fromGeod(geodEyePos);
550   eyeCart += (ec2eye*eyeOffsetOr).backTransform(eyeOff);
551
552   SGVec3d atCart = SGVec3d::fromGeod(geodTargetPos);
553
554   // add target offsets to at_position...
555   SGVec3d target_pos_off(-_target_offset_m.z(), _target_offset_m.x(),
556                          -_target_offset_m.y());
557   target_pos_off = (geodTargetHlOr*geodTargetOr).backTransform(target_pos_off);
558   atCart += target_pos_off;
559   eyeCart += target_pos_off;
560
561   // Compute the eyepoints orientation and position
562   // wrt the earth centered frame - that is global coorinates
563   _absolute_view_pos = eyeCart;
564
565   // the view direction
566   SGVec3d dir = normalize(atCart - eyeCart);
567   // the up directon
568   SGVec3d up = ec2eye.backTransform(SGVec3d(0, 0, -1));
569   // rotate dir to the 0-th unit vector
570   // rotate up to 2-th unit vector
571   mViewOrientation = SGQuatd::fromRotateTo(-dir, 2, up, 1);
572 }
573
574 void
575 FGViewer::dampEyeData(double &roll_deg, double &pitch_deg, double &heading_deg)
576 {
577   const double interval = 0.01;
578
579   static FGViewer *last_view = 0;
580   if (last_view != this) {
581     _damp_sync = 0.0;
582     _damped_roll_deg = roll_deg;
583     _damped_pitch_deg = pitch_deg;
584     _damped_heading_deg = heading_deg;
585     last_view = this;
586     return;
587   }
588
589   if (_damp_sync < interval) {
590     if (_damp_roll > 0.0)
591       roll_deg = _damped_roll_deg;
592     if (_damp_pitch > 0.0)
593       pitch_deg = _damped_pitch_deg;
594     if (_damp_heading > 0.0)
595       heading_deg = _damped_heading_deg;
596     return;
597   }
598
599   while (_damp_sync >= interval) {
600     _damp_sync -= interval;
601
602     double d;
603     if (_damp_roll > 0.0) {
604       d = _damped_roll_deg - roll_deg;
605       if (d >= 180.0)
606         _damped_roll_deg -= 360.0;
607       else if (d < -180.0)
608         _damped_roll_deg += 360.0;
609       roll_deg = _damped_roll_deg = roll_deg * _damp_roll + _damped_roll_deg * (1 - _damp_roll);
610     }
611
612     if (_damp_pitch > 0.0) {
613       d = _damped_pitch_deg - pitch_deg;
614       if (d >= 180.0)
615         _damped_pitch_deg -= 360.0;
616       else if (d < -180.0)
617         _damped_pitch_deg += 360.0;
618       pitch_deg = _damped_pitch_deg = pitch_deg * _damp_pitch + _damped_pitch_deg * (1 - _damp_pitch);
619     }
620
621     if (_damp_heading > 0.0) {
622       d = _damped_heading_deg - heading_deg;
623       if (d >= 180.0)
624         _damped_heading_deg -= 360.0;
625       else if (d < -180.0)
626         _damped_heading_deg += 360.0;
627       heading_deg = _damped_heading_deg = heading_deg * _damp_heading + _damped_heading_deg * (1 - _damp_heading);
628     }
629   }
630 }
631
632 double
633 FGViewer::get_h_fov()
634 {
635     switch (_scaling_type) {
636     case FG_SCALING_WIDTH:  // h_fov == fov
637         return _fov_deg;
638     case FG_SCALING_MAX:
639         if (_aspect_ratio < 1.0) {
640             // h_fov == fov
641             return _fov_deg;
642         } else {
643             // v_fov == fov
644             return
645                 atan(tan(_fov_deg/2 * SG_DEGREES_TO_RADIANS)
646                      / (_aspect_ratio*_aspect_ratio_multiplier))
647                 * SG_RADIANS_TO_DEGREES * 2;
648         }
649     default:
650         assert(false);
651     }
652     return 0.0;
653 }
654
655
656
657 double
658 FGViewer::get_v_fov()
659 {
660     switch (_scaling_type) {
661     case FG_SCALING_WIDTH:  // h_fov == fov
662         return 
663             atan(tan(_fov_deg/2 * SG_DEGREES_TO_RADIANS)
664                  * (_aspect_ratio*_aspect_ratio_multiplier))
665             * SG_RADIANS_TO_DEGREES * 2;
666     case FG_SCALING_MAX:
667         if (_aspect_ratio < 1.0) {
668             // h_fov == fov
669             return
670                 atan(tan(_fov_deg/2 * SG_DEGREES_TO_RADIANS)
671                      * (_aspect_ratio*_aspect_ratio_multiplier))
672                 * SG_RADIANS_TO_DEGREES * 2;
673         } else {
674             // v_fov == fov
675             return _fov_deg;
676         }
677     default:
678         assert(false);
679     }
680     return 0.0;
681 }
682
683 void
684 FGViewer::update (double dt)
685 {
686   _damp_sync += dt;
687
688   int i;
689   int dt_ms = int(dt * 1000);
690   for ( i = 0; i < dt_ms; i++ ) {
691     if ( fabs( _goal_heading_offset_deg - _heading_offset_deg) < 1 ) {
692       setHeadingOffset_deg( _goal_heading_offset_deg );
693       break;
694     } else {
695       // move current_view.headingoffset towards
696       // current_view.goal_view_offset
697       if ( _goal_heading_offset_deg > _heading_offset_deg )
698         {
699           if ( _goal_heading_offset_deg - _heading_offset_deg < 180 ){
700             incHeadingOffset_deg( 0.5 );
701           } else {
702             incHeadingOffset_deg( -0.5 );
703           }
704         } else {
705           if ( _heading_offset_deg - _goal_heading_offset_deg < 180 ){
706             incHeadingOffset_deg( -0.5 );
707           } else {
708             incHeadingOffset_deg( 0.5 );
709           }
710         }
711       if ( _heading_offset_deg > 360 ) {
712         incHeadingOffset_deg( -360 );
713       } else if ( _heading_offset_deg < 0 ) {
714         incHeadingOffset_deg( 360 );
715       }
716     }
717   }
718
719   for ( i = 0; i < dt_ms; i++ ) {
720     if ( fabs( _goal_pitch_offset_deg - _pitch_offset_deg ) < 1 ) {
721       setPitchOffset_deg( _goal_pitch_offset_deg );
722       break;
723     } else {
724       // move current_view.pitch_offset_deg towards
725       // current_view.goal_pitch_offset
726       if ( _goal_pitch_offset_deg > _pitch_offset_deg )
727         {
728           incPitchOffset_deg( 1.0 );
729         } else {
730             incPitchOffset_deg( -1.0 );
731         }
732       if ( _pitch_offset_deg > 90 ) {
733         setPitchOffset_deg(90);
734       } else if ( _pitch_offset_deg < -90 ) {
735         setPitchOffset_deg( -90 );
736       }
737     }
738   }
739
740
741   for ( i = 0; i < dt_ms; i++ ) {
742     if ( fabs( _goal_roll_offset_deg - _roll_offset_deg ) < 1 ) {
743       setRollOffset_deg( _goal_roll_offset_deg );
744       break;
745     } else {
746       // move current_view.roll_offset_deg towards
747       // current_view.goal_roll_offset
748       if ( _goal_roll_offset_deg > _roll_offset_deg )
749         {
750           incRollOffset_deg( 1.0 );
751         } else {
752             incRollOffset_deg( -1.0 );
753         }
754       if ( _roll_offset_deg > 90 ) {
755         setRollOffset_deg(90);
756       } else if ( _roll_offset_deg < -90 ) {
757         setRollOffset_deg( -90 );
758       }
759     }
760   }
761   recalc();
762   _cameraGroup->update(_absolute_view_pos.osg(), mViewOrientation.osg());
763   _cameraGroup->setCameraParameters(get_v_fov(), get_aspect_ratio());
764 }