1 // This file is part of Eigen, a lightweight C++ template library
2 // for linear algebra.
3 //
4 // Copyright (C) 2008 Gael Guennebaud <gael.guennebaud@inria.fr>
5 //
6 // This Source Code Form is subject to the terms of the Mozilla
7 // Public License v. 2.0. If a copy of the MPL was not distributed
8 // with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
9
10 #include "quaternion_demo.h"
11 #include "icosphere.h"
12
13 #include <Eigen/Geometry>
14 #include <Eigen/QR>
15 #include <Eigen/LU>
16
17 #include <iostream>
18 #include <QEvent>
19 #include <QMouseEvent>
20 #include <QInputDialog>
21 #include <QGridLayout>
22 #include <QButtonGroup>
23 #include <QRadioButton>
24 #include <QDockWidget>
25 #include <QPushButton>
26 #include <QGroupBox>
27
28 using namespace Eigen;
29
30 class FancySpheres
31 {
32 public:
33 EIGEN_MAKE_ALIGNED_OPERATOR_NEW
34
FancySpheres()35 FancySpheres()
36 {
37 const int levels = 4;
38 const float scale = 0.33;
39 float radius = 100;
40 std::vector<int> parents;
41
42 // leval 0
43 mCenters.push_back(Vector3f::Zero());
44 parents.push_back(-1);
45 mRadii.push_back(radius);
46
47 // generate level 1 using icosphere vertices
48 radius *= 0.45;
49 {
50 float dist = mRadii[0]*0.9;
51 for (int i=0; i<12; ++i)
52 {
53 mCenters.push_back(mIcoSphere.vertices()[i] * dist);
54 mRadii.push_back(radius);
55 parents.push_back(0);
56 }
57 }
58
59 static const float angles [10] = {
60 0, 0,
61 M_PI, 0.*M_PI,
62 M_PI, 0.5*M_PI,
63 M_PI, 1.*M_PI,
64 M_PI, 1.5*M_PI
65 };
66
67 // generate other levels
68 int start = 1;
69 for (int l=1; l<levels; l++)
70 {
71 radius *= scale;
72 int end = mCenters.size();
73 for (int i=start; i<end; ++i)
74 {
75 Vector3f c = mCenters[i];
76 Vector3f ax0 = (c - mCenters[parents[i]]).normalized();
77 Vector3f ax1 = ax0.unitOrthogonal();
78 Quaternionf q;
79 q.setFromTwoVectors(Vector3f::UnitZ(), ax0);
80 Affine3f t = Translation3f(c) * q * Scaling(mRadii[i]+radius);
81 for (int j=0; j<5; ++j)
82 {
83 Vector3f newC = c + ( (AngleAxisf(angles[j*2+1], ax0)
84 * AngleAxisf(angles[j*2+0] * (l==1 ? 0.35 : 0.5), ax1)) * ax0)
85 * (mRadii[i] + radius*0.8);
86 mCenters.push_back(newC);
87 mRadii.push_back(radius);
88 parents.push_back(i);
89 }
90 }
91 start = end;
92 }
93 }
94
draw()95 void draw()
96 {
97 int end = mCenters.size();
98 glEnable(GL_NORMALIZE);
99 for (int i=0; i<end; ++i)
100 {
101 Affine3f t = Translation3f(mCenters[i]) * Scaling(mRadii[i]);
102 gpu.pushMatrix(GL_MODELVIEW);
103 gpu.multMatrix(t.matrix(),GL_MODELVIEW);
104 mIcoSphere.draw(2);
105 gpu.popMatrix(GL_MODELVIEW);
106 }
107 glDisable(GL_NORMALIZE);
108 }
109 protected:
110 std::vector<Vector3f> mCenters;
111 std::vector<float> mRadii;
112 IcoSphere mIcoSphere;
113 };
114
115
116 // generic linear interpolation method
lerp(float t,const T & a,const T & b)117 template<typename T> T lerp(float t, const T& a, const T& b)
118 {
119 return a*(1-t) + b*t;
120 }
121
122 // quaternion slerp
lerp(float t,const Quaternionf & a,const Quaternionf & b)123 template<> Quaternionf lerp(float t, const Quaternionf& a, const Quaternionf& b)
124 { return a.slerp(t,b); }
125
126 // linear interpolation of a frame using the type OrientationType
127 // to perform the interpolation of the orientations
128 template<typename OrientationType>
lerpFrame(float alpha,const Frame & a,const Frame & b)129 inline static Frame lerpFrame(float alpha, const Frame& a, const Frame& b)
130 {
131 return Frame(lerp(alpha,a.position,b.position),
132 Quaternionf(lerp(alpha,OrientationType(a.orientation),OrientationType(b.orientation))));
133 }
134
135 template<typename _Scalar> class EulerAngles
136 {
137 public:
138 enum { Dim = 3 };
139 typedef _Scalar Scalar;
140 typedef Matrix<Scalar,3,3> Matrix3;
141 typedef Matrix<Scalar,3,1> Vector3;
142 typedef Quaternion<Scalar> QuaternionType;
143
144 protected:
145
146 Vector3 m_angles;
147
148 public:
149
EulerAngles()150 EulerAngles() {}
EulerAngles(Scalar a0,Scalar a1,Scalar a2)151 inline EulerAngles(Scalar a0, Scalar a1, Scalar a2) : m_angles(a0, a1, a2) {}
EulerAngles(const QuaternionType & q)152 inline EulerAngles(const QuaternionType& q) { *this = q; }
153
coeffs() const154 const Vector3& coeffs() const { return m_angles; }
coeffs()155 Vector3& coeffs() { return m_angles; }
156
operator =(const QuaternionType & q)157 EulerAngles& operator=(const QuaternionType& q)
158 {
159 Matrix3 m = q.toRotationMatrix();
160 return *this = m;
161 }
162
operator =(const Matrix3 & m)163 EulerAngles& operator=(const Matrix3& m)
164 {
165 // mat = cy*cz -cy*sz sy
166 // cz*sx*sy+cx*sz cx*cz-sx*sy*sz -cy*sx
167 // -cx*cz*sy+sx*sz cz*sx+cx*sy*sz cx*cy
168 m_angles.coeffRef(1) = std::asin(m.coeff(0,2));
169 m_angles.coeffRef(0) = std::atan2(-m.coeff(1,2),m.coeff(2,2));
170 m_angles.coeffRef(2) = std::atan2(-m.coeff(0,1),m.coeff(0,0));
171 return *this;
172 }
173
toRotationMatrix(void) const174 Matrix3 toRotationMatrix(void) const
175 {
176 Vector3 c = m_angles.array().cos();
177 Vector3 s = m_angles.array().sin();
178 Matrix3 res;
179 res << c.y()*c.z(), -c.y()*s.z(), s.y(),
180 c.z()*s.x()*s.y()+c.x()*s.z(), c.x()*c.z()-s.x()*s.y()*s.z(), -c.y()*s.x(),
181 -c.x()*c.z()*s.y()+s.x()*s.z(), c.z()*s.x()+c.x()*s.y()*s.z(), c.x()*c.y();
182 return res;
183 }
184
operator QuaternionType()185 operator QuaternionType() { return QuaternionType(toRotationMatrix()); }
186 };
187
188 // Euler angles slerp
lerp(float t,const EulerAngles<float> & a,const EulerAngles<float> & b)189 template<> EulerAngles<float> lerp(float t, const EulerAngles<float>& a, const EulerAngles<float>& b)
190 {
191 EulerAngles<float> res;
192 res.coeffs() = lerp(t, a.coeffs(), b.coeffs());
193 return res;
194 }
195
196
RenderingWidget()197 RenderingWidget::RenderingWidget()
198 {
199 mAnimate = false;
200 mCurrentTrackingMode = TM_NO_TRACK;
201 mNavMode = NavTurnAround;
202 mLerpMode = LerpQuaternion;
203 mRotationMode = RotationStable;
204 mTrackball.setCamera(&mCamera);
205
206 // required to capture key press events
207 setFocusPolicy(Qt::ClickFocus);
208 }
209
grabFrame(void)210 void RenderingWidget::grabFrame(void)
211 {
212 // ask user for a time
213 bool ok = false;
214 double t = 0;
215 if (!m_timeline.empty())
216 t = (--m_timeline.end())->first + 1.;
217 t = QInputDialog::getDouble(this, "Eigen's RenderingWidget", "time value: ",
218 t, 0, 1e3, 1, &ok);
219 if (ok)
220 {
221 Frame aux;
222 aux.orientation = mCamera.viewMatrix().linear();
223 aux.position = mCamera.viewMatrix().translation();
224 m_timeline[t] = aux;
225 }
226 }
227
drawScene()228 void RenderingWidget::drawScene()
229 {
230 static FancySpheres sFancySpheres;
231 float length = 50;
232 gpu.drawVector(Vector3f::Zero(), length*Vector3f::UnitX(), Color(1,0,0,1));
233 gpu.drawVector(Vector3f::Zero(), length*Vector3f::UnitY(), Color(0,1,0,1));
234 gpu.drawVector(Vector3f::Zero(), length*Vector3f::UnitZ(), Color(0,0,1,1));
235
236 // draw the fractal object
237 float sqrt3 = internal::sqrt(3.);
238 glLightfv(GL_LIGHT0, GL_AMBIENT, Vector4f(0.5,0.5,0.5,1).data());
239 glLightfv(GL_LIGHT0, GL_DIFFUSE, Vector4f(0.5,1,0.5,1).data());
240 glLightfv(GL_LIGHT0, GL_SPECULAR, Vector4f(1,1,1,1).data());
241 glLightfv(GL_LIGHT0, GL_POSITION, Vector4f(-sqrt3,-sqrt3,sqrt3,0).data());
242
243 glLightfv(GL_LIGHT1, GL_AMBIENT, Vector4f(0,0,0,1).data());
244 glLightfv(GL_LIGHT1, GL_DIFFUSE, Vector4f(1,0.5,0.5,1).data());
245 glLightfv(GL_LIGHT1, GL_SPECULAR, Vector4f(1,1,1,1).data());
246 glLightfv(GL_LIGHT1, GL_POSITION, Vector4f(-sqrt3,sqrt3,-sqrt3,0).data());
247
248 glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, Vector4f(0.7, 0.7, 0.7, 1).data());
249 glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, Vector4f(0.8, 0.75, 0.6, 1).data());
250 glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, Vector4f(1, 1, 1, 1).data());
251 glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 64);
252
253 glEnable(GL_LIGHTING);
254 glEnable(GL_LIGHT0);
255 glEnable(GL_LIGHT1);
256
257 sFancySpheres.draw();
258 glVertexPointer(3, GL_FLOAT, 0, mVertices[0].data());
259 glNormalPointer(GL_FLOAT, 0, mNormals[0].data());
260 glEnableClientState(GL_VERTEX_ARRAY);
261 glEnableClientState(GL_NORMAL_ARRAY);
262 glDrawArrays(GL_TRIANGLES, 0, mVertices.size());
263 glDisableClientState(GL_VERTEX_ARRAY);
264 glDisableClientState(GL_NORMAL_ARRAY);
265
266 glDisable(GL_LIGHTING);
267 }
268
animate()269 void RenderingWidget::animate()
270 {
271 m_alpha += double(m_timer.interval()) * 1e-3;
272
273 TimeLine::const_iterator hi = m_timeline.upper_bound(m_alpha);
274 TimeLine::const_iterator lo = hi;
275 --lo;
276
277 Frame currentFrame;
278
279 if(hi==m_timeline.end())
280 {
281 // end
282 currentFrame = lo->second;
283 stopAnimation();
284 }
285 else if(hi==m_timeline.begin())
286 {
287 // start
288 currentFrame = hi->second;
289 }
290 else
291 {
292 float s = (m_alpha - lo->first)/(hi->first - lo->first);
293 if (mLerpMode==LerpEulerAngles)
294 currentFrame = ::lerpFrame<EulerAngles<float> >(s, lo->second, hi->second);
295 else if (mLerpMode==LerpQuaternion)
296 currentFrame = ::lerpFrame<Eigen::Quaternionf>(s, lo->second, hi->second);
297 else
298 {
299 std::cerr << "Invalid rotation interpolation mode (abort)\n";
300 exit(2);
301 }
302 currentFrame.orientation.coeffs().normalize();
303 }
304
305 currentFrame.orientation = currentFrame.orientation.inverse();
306 currentFrame.position = - (currentFrame.orientation * currentFrame.position);
307 mCamera.setFrame(currentFrame);
308
309 updateGL();
310 }
311
keyPressEvent(QKeyEvent * e)312 void RenderingWidget::keyPressEvent(QKeyEvent * e)
313 {
314 switch(e->key())
315 {
316 case Qt::Key_Up:
317 mCamera.zoom(2);
318 break;
319 case Qt::Key_Down:
320 mCamera.zoom(-2);
321 break;
322 // add a frame
323 case Qt::Key_G:
324 grabFrame();
325 break;
326 // clear the time line
327 case Qt::Key_C:
328 m_timeline.clear();
329 break;
330 // move the camera to initial pos
331 case Qt::Key_R:
332 resetCamera();
333 break;
334 // start/stop the animation
335 case Qt::Key_A:
336 if (mAnimate)
337 {
338 stopAnimation();
339 }
340 else
341 {
342 m_alpha = 0;
343 connect(&m_timer, SIGNAL(timeout()), this, SLOT(animate()));
344 m_timer.start(1000/30);
345 mAnimate = true;
346 }
347 break;
348 default:
349 break;
350 }
351
352 updateGL();
353 }
354
stopAnimation()355 void RenderingWidget::stopAnimation()
356 {
357 disconnect(&m_timer, SIGNAL(timeout()), this, SLOT(animate()));
358 m_timer.stop();
359 mAnimate = false;
360 m_alpha = 0;
361 }
362
mousePressEvent(QMouseEvent * e)363 void RenderingWidget::mousePressEvent(QMouseEvent* e)
364 {
365 mMouseCoords = Vector2i(e->pos().x(), e->pos().y());
366 bool fly = (mNavMode==NavFly) || (e->modifiers()&Qt::ControlModifier);
367 switch(e->button())
368 {
369 case Qt::LeftButton:
370 if(fly)
371 {
372 mCurrentTrackingMode = TM_LOCAL_ROTATE;
373 mTrackball.start(Trackball::Local);
374 }
375 else
376 {
377 mCurrentTrackingMode = TM_ROTATE_AROUND;
378 mTrackball.start(Trackball::Around);
379 }
380 mTrackball.track(mMouseCoords);
381 break;
382 case Qt::MidButton:
383 if(fly)
384 mCurrentTrackingMode = TM_FLY_Z;
385 else
386 mCurrentTrackingMode = TM_ZOOM;
387 break;
388 case Qt::RightButton:
389 mCurrentTrackingMode = TM_FLY_PAN;
390 break;
391 default:
392 break;
393 }
394 }
mouseReleaseEvent(QMouseEvent *)395 void RenderingWidget::mouseReleaseEvent(QMouseEvent*)
396 {
397 mCurrentTrackingMode = TM_NO_TRACK;
398 updateGL();
399 }
400
mouseMoveEvent(QMouseEvent * e)401 void RenderingWidget::mouseMoveEvent(QMouseEvent* e)
402 {
403 // tracking
404 if(mCurrentTrackingMode != TM_NO_TRACK)
405 {
406 float dx = float(e->x() - mMouseCoords.x()) / float(mCamera.vpWidth());
407 float dy = - float(e->y() - mMouseCoords.y()) / float(mCamera.vpHeight());
408
409 // speedup the transformations
410 if(e->modifiers() & Qt::ShiftModifier)
411 {
412 dx *= 10.;
413 dy *= 10.;
414 }
415
416 switch(mCurrentTrackingMode)
417 {
418 case TM_ROTATE_AROUND:
419 case TM_LOCAL_ROTATE:
420 if (mRotationMode==RotationStable)
421 {
422 // use the stable trackball implementation mapping
423 // the 2D coordinates to 3D points on a sphere.
424 mTrackball.track(Vector2i(e->pos().x(), e->pos().y()));
425 }
426 else
427 {
428 // standard approach mapping the x and y displacements as rotations
429 // around the camera's X and Y axes.
430 Quaternionf q = AngleAxisf( dx*M_PI, Vector3f::UnitY())
431 * AngleAxisf(-dy*M_PI, Vector3f::UnitX());
432 if (mCurrentTrackingMode==TM_LOCAL_ROTATE)
433 mCamera.localRotate(q);
434 else
435 mCamera.rotateAroundTarget(q);
436 }
437 break;
438 case TM_ZOOM :
439 mCamera.zoom(dy*100);
440 break;
441 case TM_FLY_Z :
442 mCamera.localTranslate(Vector3f(0, 0, -dy*200));
443 break;
444 case TM_FLY_PAN :
445 mCamera.localTranslate(Vector3f(dx*200, dy*200, 0));
446 break;
447 default:
448 break;
449 }
450
451 updateGL();
452 }
453
454 mMouseCoords = Vector2i(e->pos().x(), e->pos().y());
455 }
456
paintGL()457 void RenderingWidget::paintGL()
458 {
459 glEnable(GL_DEPTH_TEST);
460 glDisable(GL_CULL_FACE);
461 glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
462 glDisable(GL_COLOR_MATERIAL);
463 glDisable(GL_BLEND);
464 glDisable(GL_ALPHA_TEST);
465 glDisable(GL_TEXTURE_1D);
466 glDisable(GL_TEXTURE_2D);
467 glDisable(GL_TEXTURE_3D);
468
469 // Clear buffers
470 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
471
472 mCamera.activateGL();
473
474 drawScene();
475 }
476
initializeGL()477 void RenderingWidget::initializeGL()
478 {
479 glClearColor(1., 1., 1., 0.);
480 glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, 1);
481 glDepthMask(GL_TRUE);
482 glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
483
484 mCamera.setPosition(Vector3f(-200, -200, -200));
485 mCamera.setTarget(Vector3f(0, 0, 0));
486 mInitFrame.orientation = mCamera.orientation().inverse();
487 mInitFrame.position = mCamera.viewMatrix().translation();
488 }
489
resizeGL(int width,int height)490 void RenderingWidget::resizeGL(int width, int height)
491 {
492 mCamera.setViewport(width,height);
493 }
494
setNavMode(int m)495 void RenderingWidget::setNavMode(int m)
496 {
497 mNavMode = NavMode(m);
498 }
499
setLerpMode(int m)500 void RenderingWidget::setLerpMode(int m)
501 {
502 mLerpMode = LerpMode(m);
503 }
504
setRotationMode(int m)505 void RenderingWidget::setRotationMode(int m)
506 {
507 mRotationMode = RotationMode(m);
508 }
509
resetCamera()510 void RenderingWidget::resetCamera()
511 {
512 if (mAnimate)
513 stopAnimation();
514 m_timeline.clear();
515 Frame aux0 = mCamera.frame();
516 aux0.orientation = aux0.orientation.inverse();
517 aux0.position = mCamera.viewMatrix().translation();
518 m_timeline[0] = aux0;
519
520 Vector3f currentTarget = mCamera.target();
521 mCamera.setTarget(Vector3f::Zero());
522
523 // compute the rotation duration to move the camera to the target
524 Frame aux1 = mCamera.frame();
525 aux1.orientation = aux1.orientation.inverse();
526 aux1.position = mCamera.viewMatrix().translation();
527 float duration = aux0.orientation.angularDistance(aux1.orientation) * 0.9;
528 if (duration<0.1) duration = 0.1;
529
530 // put the camera at that time step:
531 aux1 = aux0.lerp(duration/2,mInitFrame);
532 // and make it look at the target again
533 aux1.orientation = aux1.orientation.inverse();
534 aux1.position = - (aux1.orientation * aux1.position);
535 mCamera.setFrame(aux1);
536 mCamera.setTarget(Vector3f::Zero());
537
538 // add this camera keyframe
539 aux1.orientation = aux1.orientation.inverse();
540 aux1.position = mCamera.viewMatrix().translation();
541 m_timeline[duration] = aux1;
542
543 m_timeline[2] = mInitFrame;
544 m_alpha = 0;
545 animate();
546 connect(&m_timer, SIGNAL(timeout()), this, SLOT(animate()));
547 m_timer.start(1000/30);
548 mAnimate = true;
549 }
550
createNavigationControlWidget()551 QWidget* RenderingWidget::createNavigationControlWidget()
552 {
553 QWidget* panel = new QWidget();
554 QVBoxLayout* layout = new QVBoxLayout();
555
556 {
557 QPushButton* but = new QPushButton("reset");
558 but->setToolTip("move the camera to initial position (with animation)");
559 layout->addWidget(but);
560 connect(but, SIGNAL(clicked()), this, SLOT(resetCamera()));
561 }
562 {
563 // navigation mode
564 QGroupBox* box = new QGroupBox("navigation mode");
565 QVBoxLayout* boxLayout = new QVBoxLayout;
566 QButtonGroup* group = new QButtonGroup(panel);
567 QRadioButton* but;
568 but = new QRadioButton("turn around");
569 but->setToolTip("look around an object");
570 group->addButton(but, NavTurnAround);
571 boxLayout->addWidget(but);
572 but = new QRadioButton("fly");
573 but->setToolTip("free navigation like a spaceship\n(this mode can also be enabled pressing the \"shift\" key)");
574 group->addButton(but, NavFly);
575 boxLayout->addWidget(but);
576 group->button(mNavMode)->setChecked(true);
577 connect(group, SIGNAL(buttonClicked(int)), this, SLOT(setNavMode(int)));
578 box->setLayout(boxLayout);
579 layout->addWidget(box);
580 }
581 {
582 // track ball, rotation mode
583 QGroupBox* box = new QGroupBox("rotation mode");
584 QVBoxLayout* boxLayout = new QVBoxLayout;
585 QButtonGroup* group = new QButtonGroup(panel);
586 QRadioButton* but;
587 but = new QRadioButton("stable trackball");
588 group->addButton(but, RotationStable);
589 boxLayout->addWidget(but);
590 but->setToolTip("use the stable trackball implementation mapping\nthe 2D coordinates to 3D points on a sphere");
591 but = new QRadioButton("standard rotation");
592 group->addButton(but, RotationStandard);
593 boxLayout->addWidget(but);
594 but->setToolTip("standard approach mapping the x and y displacements\nas rotations around the camera's X and Y axes");
595 group->button(mRotationMode)->setChecked(true);
596 connect(group, SIGNAL(buttonClicked(int)), this, SLOT(setRotationMode(int)));
597 box->setLayout(boxLayout);
598 layout->addWidget(box);
599 }
600 {
601 // interpolation mode
602 QGroupBox* box = new QGroupBox("spherical interpolation");
603 QVBoxLayout* boxLayout = new QVBoxLayout;
604 QButtonGroup* group = new QButtonGroup(panel);
605 QRadioButton* but;
606 but = new QRadioButton("quaternion slerp");
607 group->addButton(but, LerpQuaternion);
608 boxLayout->addWidget(but);
609 but->setToolTip("use quaternion spherical interpolation\nto interpolate orientations");
610 but = new QRadioButton("euler angles");
611 group->addButton(but, LerpEulerAngles);
612 boxLayout->addWidget(but);
613 but->setToolTip("use Euler angles to interpolate orientations");
614 group->button(mNavMode)->setChecked(true);
615 connect(group, SIGNAL(buttonClicked(int)), this, SLOT(setLerpMode(int)));
616 box->setLayout(boxLayout);
617 layout->addWidget(box);
618 }
619 layout->addItem(new QSpacerItem(0,0,QSizePolicy::Minimum,QSizePolicy::Expanding));
620 panel->setLayout(layout);
621 return panel;
622 }
623
QuaternionDemo()624 QuaternionDemo::QuaternionDemo()
625 {
626 mRenderingWidget = new RenderingWidget();
627 setCentralWidget(mRenderingWidget);
628
629 QDockWidget* panel = new QDockWidget("navigation", this);
630 panel->setAllowedAreas((QFlags<Qt::DockWidgetArea>)(Qt::RightDockWidgetArea | Qt::LeftDockWidgetArea));
631 addDockWidget(Qt::RightDockWidgetArea, panel);
632 panel->setWidget(mRenderingWidget->createNavigationControlWidget());
633 }
634
main(int argc,char * argv[])635 int main(int argc, char *argv[])
636 {
637 std::cout << "Navigation:\n";
638 std::cout << " left button: rotate around the target\n";
639 std::cout << " middle button: zoom\n";
640 std::cout << " left button + ctrl quake rotate (rotate around camera position)\n";
641 std::cout << " middle button + ctrl walk (progress along camera's z direction)\n";
642 std::cout << " left button: pan (translate in the XY camera's plane)\n\n";
643 std::cout << "R : move the camera to initial position\n";
644 std::cout << "A : start/stop animation\n";
645 std::cout << "C : clear the animation\n";
646 std::cout << "G : add a key frame\n";
647
648 QApplication app(argc, argv);
649 QuaternionDemo demo;
650 demo.resize(600,500);
651 demo.show();
652 return app.exec();
653 }
654
655 #include "quaternion_demo.moc"
656
657