mario::konrad
programming / C++ / sailing / nerd stuff
OpenGL: Tutorial 8 - ArcBall
© 2004 / Mario Konrad

Beschreibung

Verwendung eines ArcBalls zur intuitiven Drehung der Szene.

Platformen:

Download

Die auf dieser Seite aufgeführten Sourcecodes dürfen uneingeschränkt verwendet, kopiert, verändert und publiziert werden. Jegliche Haftung wird abgelehnt, die Verwendung des hier publizierten Materials geschieht auf eigenes Risiko.

Source:

Paket: tutorial08.tgz

Build und Start

$ tar -xzf tutorial08.tgz
$ cd tutorial08
$ make
$ ./tutorial08

Das Demo

Dieses Demo zeigt die Verwendung des von Ken Shoemake vorgeschlagenen ArcBall, ein virtueller Trackball. Dies ist eine intuitive Möglichkeit 3D-Objekte interaktiv zu drehen.

Das Prinzip

Die Idee hinter dem ArcBall, dem virtuellen Trackball, ist, dass eine Mausbewegung (z.B. mit gedrückter Taste, dragging) eine Kurve beschreibt, genauer einen Bogen auf einer Kugeloberfläche. Stellt man sich nun den Mauszeiger vor wie er an dieser virtuellen Kugel festhält, wäre er dazu in der Lage die Kugel in einer Richtung zu ziehen oder zu schubsen.

Die gleiche Bewegung die nun die virtuell Kugel macht, kann nun verwendet werden um die ganze Szene zu drehen, in die gleiche Richtung und um die gleiche Dreachse wie diese virtuelle Kugel.

Dazu muss die Mausbewegung (beim ziehen) auf diese virtuelle Kugel projeziert werden und mit Hilfe der Quaternionen ist der Bogen (und die damit gegebene Drehung) einfach realisierbar, auch ohne den gimbal lock.

Die Implementation des ArcBall

In diesem Kaptitel wird die Implementation des ArcBalls beschrieben. Alle darin verwendeten Klassen (Vektoren, Matrizen, Quaterionen) werden nicht beschrieben.

class ArcBall
{
    private:

Die Grösse des Fensters:

        float width;
        float height;

Zentrum und Radius des virtuellen Trackballs:

        math::vec3 center;
        double radius;

Flag das anzeigt ob sich der ArcBall am drehen ist (ob mit der Maus gezogen wird):

        bool drag;

Aktuelle Position der Maus:

        math::vec3 v_cur;

Position der Maus als die Maustaste gedrückt wurde:

        math::vec3 v_down;

Quaternion welches den momentanen Bogen auf dem virtuellen Trackball beschreibt.

        math::quaternion q_cur;

Beschreibt den Bogen auf dem virtuellen Trackball nach dem ziehen der Maus.

        math::quaternion q_end;

Rotationsmatrix:

        math::mat4 mat_cur;

Diese Methode projeziert die aktuelle Mausposition auf den virtuellen Trackball, definiert durch Zentrum und Radius. Das Resultat ist eine Position auf diesem virtuellen Trackball.

    private:
        inline math::vec3 map_sphere(const math::vec3 & mouse,
            const math::vec3 & center, double r) const
        {
            math::vec3 bm;

            bm = (1.0 / r) * (mouse - center);
            double mag = bm.length2();
            if (mag > 1.0) {
                bm.normalize();
                bm[2] = 0.0;
            } else {
                bm[2] = sqrt(1.0 - mag);
            }
            return bm;
        }

Diese Methode errechnet ein Quaternion aus den zwei Positionen auf dem virutellen Trackball. Das Quaternion beschreibt den Bogen zwischen den zwei Positionen auf dem Trackball.

    private:
        inline math::quaternion from_ball_points(const math::vec3 & from,
            const math::vec3 & to) const
        {
            return math::quaternion(
                 from[0] * to[0] + from[1] * to[1] + from[2] * to[2]
                ,from[1] * to[2] - from[2] * to[1]
                ,from[2] * to[0] - from[0] * to[2]
                ,from[0] * to[1] - from[1] * to[0]
                );
        }

Wandelt das angegebene Quaternion in eine 4x4 Rotationsmatrix um:

    private:
        inline void to_matrix(math::mat4 & m,
            const math::quaternion & q) const
        {
            double l = q.length2();
            double s = (l > 0.0) ? (2.0 / l) : 0.0;

            double xs = q[1] * s;
            double ys = q[2] * s;
            double zs = q[3] * s;

            double wx = q[0] * xs;
            double wy = q[0] * ys;
            double wz = q[0] * zs;

            double xx = q[1] * xs;
            double xy = q[1] * ys;
            double xz = q[1] * zs;

            double yy = q[2] * ys;
            double yz = q[2] * zs;

            double zz = q[3] * zs;

            m[ 0] = 1.0 - (yy + zz);
            m[ 1] = xy + wz;
            m[ 2] = xz - wy;
            m[ 3] = 0.0;

            m[ 4] = xy - wz;
            m[ 5] = 1.0 - (xx + zz);
            m[ 6] = yz + wx;
            m[ 7] = 0.0;

            m[ 8] = xz + wy;
            m[ 9] = yz - wx;
            m[10] = 1.0 - (xx + yy);
            m[11] = 0.0;

            m[12] = 0.0;
            m[13] = 0.0;
            m[14] = 0.0;
            m[15] = 1.0;
        }

Update der Rotationsmatrix anhand der Mausposition beim Beginn des Ziehens und der aktuellen Mausposition. Beide Positionen werden auf den virtuellen Trackball projeziert und daraus die Rotation ermittelt:

    private:
        inline void update(void)
        {
            math::vec3 v_from = map_sphere(v_down, center, radius);
            math::vec3 v_to   = map_sphere(v_cur, center, radius);
            if (drag) q_cur = from_ball_points(v_from, v_to) * q_end;
            to_matrix(mat_cur, q_cur);
        }

Der Konstruktor initialisiert das Objekt mit vernünftigen Werten:

    public:
        ArcBall(void)
            : center(0.0, 0.0, 0.0)
            , radius(1.0)
            , drag(false)
            , v_cur(0.0, 0.0, 0.0)
            , v_down(0.0, 0.0, 0.0)
            , q_cur(1.0, 0.0, 0.0, 0.0)
            , q_end(1.0, 0.0, 0.0, 0.0)
        {}

Das Setzen der Fenstergrösse ist wichtig, um beim ziehen der Maus auch richtig reagieren zu können.

    public:
        inline void set_win_size(float width, float height)
        { this->width = width; this->height = height; }

Die folgenden zwei Methoden sind zur Konfiguration des ArcBalls, sein Zentrum und der Radius. Die Grösse des Radius hat Einfluss auf die Rotationsgeschwindigkeit.

    public:
        inline void place(const math::vec3 & center)
        { this->center = center; }
    public:
        inline void set_r(double r)
        { this->radius = r; }

Das Setzen der aktuellen Position der Maus:

    public:
        inline void set_cur(int x, int y)
        {
            v_cur[0] = 2.0 * ((double)x / width) - 1.0;
            v_cur[1] = 2.0 * ((double)(height - y) / height) - 1.0;
            v_cur[2] = 0.0;
            update();
        }

Start und Ende des Ziehens mit der Maus wird mit den folgenden zwei Methoden dem ArcBall mitgeteilt:

    public:
        inline void begin_drag(void)
        {
            drag   = true;
            v_down = v_cur;
        }
    public:
        inline void end_drag(void)
        {
            drag  = false;
            q_end = q_cur;
        }

Zum Schluss die Metode um die Rotationsmatrix auszulesen:

    public:
        inline const float * get(void) const
        { return mat_cur.get(); }
};

Die Verwenung des ArcBall

In diesem Kapitel werden nur die ArcBall relevanten Teile beschrieben. Der Rest ist im Source einzusehen und ist genügend beschrieben in den vorangegangenen Tutorials.

Die Anpassungen um den ArcBall verwenden zu können sind minimal wie dieses Tutorial beweist.

Als erstes braucht es eine Instanz des ArcBalls:

static gl::ArcBall arcball;

Die Funktion die zuständig ist f¨r das Verändern der Grösse des Fensters. Hier muss dem ArcBall die neue Fenstergrösse mitgeteilt werden:

static void reshape(int w, int h)
{
    arcball.set_win_size(w, h);
    float aspect = (double)w / (double)h;
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0, aspect, 0.1, 150.0);
    glMatrixMode(GL_MODELVIEW);
}

Nach der Positionierung der Kamera (hier vereinfacht mit gluLookAt), wird die Szene rotiert mit Hilfe der Rotationsmatrix des ArcBalls. Danach werden Achsen und Objekte gezeichnet. In diesem Fall eine Kugel und ein Würfel. Die Achsen sind jeweils eingefärbt um sie unterscheiden zu können (rot: X-Achse, grün: Y-Achse, blau: Z-Achse).

static void display(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glLoadIdentity();
    gluLookAt(0.0, 0.0, 40.0, 0.0, 0.0,  0.0, 0.0, 1.0,  0.0);
    glMultMatrixf(arcball.get());

    // draw axis
    glBegin(GL_LINES);
        glColor4f(1.0, 0.0, 0.0, 1.0);
        glVertex3f( 0.0,  0.0,  0.0);
        glVertex3f(10.0,  0.0,  0.0);
        glColor4f(0.0, 1.0, 0.0, 1.0);
        glVertex3f( 0.0,  0.0,  0.0);
        glVertex3f( 0.0, 10.0,  0.0);
        glColor4f(0.0, 0.0, 1.0, 1.0);
        glVertex3f( 0.0,  0.0,  0.0);
        glVertex3f( 0.0,  0.0, 10.0);
    glEnd();

    // draw sphere
    glColor4f(0.8, 0.8, 0.8, 1.0);
    glutWireSphere(5.0, 16, 16);
    glutWireCube(11.0);

    glFlush();
    glutSwapBuffers();
}

Sobald die linke Maustaste gedrückt wird beginnt das ziehen der Maus, bzw. das drehen des Szene. Beim loslassen der linken Maustaste wird das Drehen beendet. Die Szene wird jeweils neu gezeichnet.

static void mouse(int b, int s, int x, int y)
{
    if (b == GLUT_LEFT_BUTTON) {
        arcball.set_cur(x, y);
        if (s == GLUT_DOWN) {
            arcball.begin_drag();
        } else {
            arcball.end_drag();
        }
        glutPostRedisplay();
    }
}

Während des Ziehens der Maus wird jeweils die neue Position dem ArcBall mitgeteilt, die Rotation wird neu berechnet und beim zeichnen der Szene berücksichtigt.

static void motion(int x, int y)
{
    arcball.set_cur(x, y);
    glutPostRedisplay();
}