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 (at) 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 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 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 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 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> 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 150 EulerAngles() {} 151 inline EulerAngles(Scalar a0, Scalar a1, Scalar a2) : m_angles(a0, a1, a2) {} 152 inline EulerAngles(const QuaternionType& q) { *this = q; } 153 154 const Vector3& coeffs() const { return m_angles; } 155 Vector3& coeffs() { return m_angles; } 156 157 EulerAngles& operator=(const QuaternionType& q) 158 { 159 Matrix3 m = q.toRotationMatrix(); 160 return *this = m; 161 } 162 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 174 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 185 operator QuaternionType() { return QuaternionType(toRotationMatrix()); } 186 }; 187 188 // Euler angles slerp 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 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 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 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 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 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 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 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 } 395 void RenderingWidget::mouseReleaseEvent(QMouseEvent*) 396 { 397 mCurrentTrackingMode = TM_NO_TRACK; 398 updateGL(); 399 } 400 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 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 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 490 void RenderingWidget::resizeGL(int width, int height) 491 { 492 mCamera.setViewport(width,height); 493 } 494 495 void RenderingWidget::setNavMode(int m) 496 { 497 mNavMode = NavMode(m); 498 } 499 500 void RenderingWidget::setLerpMode(int m) 501 { 502 mLerpMode = LerpMode(m); 503 } 504 505 void RenderingWidget::setRotationMode(int m) 506 { 507 mRotationMode = RotationMode(m); 508 } 509 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 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 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 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