Anmelden Registrieren

Badges

Follow Peter Bouda on Google Plus

Feeds

Neueste BlogeinträgeBlog

Daten des Accelerometer-Sensors mit einem OpenGL-Objekt verknüpfen

Bearbeitet am Montag, 05. April 2010, 20:26 Uhr von pbouda

Dieses Projekt zeigt, wie ihr mit der Qt-Mobility-API Sensordaten auslesen und diese zur Steuerung eurer Anwendung verwenden könnt. In diesem Beispiel werden die Sensordaten des Accelerometer mit den Bewegungen eines dreidimensionalen OpenGL-Objekts verknüpft. Wenn ihr das Programm auf dem N900 ausführt, dann werden alle Bewegungen eures Telefons durch das Objekt auf dem Dislay nachvollzogen. Hier ein kleines Video, dass euch das Programm auf dem Gerät demonstriert:

YouTube-Video zur glsensordemo-Beispielanwendung

Die Anwendung basiert auf dem OpenGL-Overpainting-Beispiel von Nokia. Ich habe das Logo und die Steuerung mit der Maus wie im Original belassen, ansonsten alles andere rausgeschmissen.

Voraussetzungen

Um die Anwendung kompilieren und starten zu können müsst ihr die aktuelle Beta von Qt Mobility auf eurem Entwicklungsrechner installieren. Um die Anwendung auf dem N900 in voller Pracht erleben zu können braucht ihr die kompilierten Maemo-Pakete (libqtm-*.deb). Dazu auf dem N900 als root einfach apt-get install libqtm-* ausführen.

Quellcode der Anwendung

Download der komletten Anwendung

Zum Download steht ein Paket mit den oben aufgeführten Quellcode-Dateien sowie einer Qt-Creator-Projektdatei samt “debian”-Ordner bereit. Das Projekt kann mit den aktuellen Schnappschüssen des Qt Creators bearbeitet werden und auf eurem Entwicklungsrechner und in Scratchbox kompiliert werden. Auf dem Desktop kann das 3D-Objekt auch mit der Maus gesteuert werden. Haltet dazu die linke Maustaste gedrückt und bewegt die Maus. Die Anwendung wird beendet, indem ihr die Taste “q” drückt oder mit der Maus in die rechte obere Ecke klickt. Hier ist das das gesamte Projekt zum Download:

Paket mit allen Projektdateien der glsensordemo-Beispielanwendung

Beschreibung des Quellcodes

Die Projektdatei qtprog.pro

Das Projektgerüst habe ich mit MADDE erstellt. MADDE erstellt eine Datei “qtprog.pro”, die zentrale Qt-Creator-Projektdatei. In diese Datei müssen folgende Zeilen zusätzlich eingetragen werden, damit man Zugriff auf die OpenGL- und die Mobility-Sensors-Klassen von Qt erhält:

QT += opengl
CONFIG += mobility
MOBILITY += sensors

Diese Datei liegt im Hauptordner des Projekts. Die anderen Quellcode-Dateien liegen im Unterordner “src”.

Die Hauptanwendungsdatei src/qtmain.cpp

#include <QApplication>
#include <QtOpenGL>
#include "glwidget.h"

int main(int argc, char *argv[])
{
	QApplication app(argc, argv);
    
  if (!QGLFormat::hasOpenGL() || !QGLFramebufferObject::hasOpenGLFramebufferObjects()) {
    QMessageBox::information(0, "OpenGL framebuffer objects",
                             "This system does not support OpenGL/framebuffer objects.");
    return -1;
  }
    
  GLWidget widget(0);
  widget.show();
  app.setQuitOnLastWindowClosed(true);
  return app.exec();
}

Zunächst werden in der Hauptdatei unserer Anwendung die Header für eine Qt-Anwendung (QApplication), OpenGL und unsere selbst geschriebene GLWidget-Klasse eingebunden. In der main-Funktion wird das Objekt QApplication erstellt und danach überprüft, ob OpenGL und Framebuffer unterstützt werden. Anschließend wird das GLWidget als Hauptwidget gesetzt und die Event-Loop mit app.exec() gestartet. Die gesamte Funktionalität der Anwendung befindet sich also in der Klasse GLWdiget, die wir uns jetzt näher anschauen wollen.

Die Klasse GLWidget

Ich werde hier nicht jede Funktion der Klasse GLWidget erklären, sondern nur die für das Demo Wichtigsten. Die Klasse erbt von QGLWidget und hat folgenden Konstruktor:

GLWidget::GLWidget(QWidget *parent) :
    QGLWidget(parent)

{
    setWindowTitle(tr("Sensor-GL-Demo"));
    makeCurrent();

    setAttribute(Qt::WA_PaintOnScreen);
    setAttribute(Qt::WA_NoSystemBackground);
    setAutoBufferSwap(false);

    xRot = 0;
    yRot = 0;
    zRot = 0;

    _rotationSensorAvailable = false;
    _rotationSensor = new QtMobility::QAccelerometer(this);
    _rotationSensor->connect();
    if (!_rotationSensor->isAvailable()) {
        qWarning("Kein Beschleunigungssensor verfügbar!");
    } else {
        _rotationSensorAvailable = true;
        _rotationSensor->setSignalEnabled(false); // wir holen uns die Werte selbst ab
        _rotationSensor->setUpdateInterval(100); // so schnell wie möglich
        _rotationSensor->start();
    }

    QTimer *timer = new QTimer(this);
    timer->setInterval(10);
    QObject::connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
    timer->start();
    showFullScreen();
}

Für uns interessant sind vor allem die Zeilen 16-26: hier wird der Accelerometer initialisiert. Damit das funktioniert, muss im Header unserer Klasse (glsensordemo.h) der Header für diesen Sensor per #include <QAccelerometer> eingebunden werden. Die Member-Variable _rotationSensor ist vom Typ QtMobility::QAccelerometer, per connect() erhalten wir Zugriff auf den Sensor. Bei QAccelerometer handelt es sich um eine Hilfsklasse der Sensor-API, die uns einfachen Zugriff auf die Werte des Accelerometers gewährt. Anschließend ist mit dem Aufruf isAvailable() zu checken, ob es überhaupt einen entsprechenden Sensor gibt. Der Aufruf von setSignalEnabled(false) unterbindet das Senden von Signalen durch das Objekt. Wir wollen nicht bei jeder Änderung der Accelerometerwerte benachrichtigt werden, sondern wir holen uns später bei jedem Zeichnen des Objekts die aktuellen Werte ab. Das Update-Interval setzen wir mit setUpdateInterval(100) auf 100 Millisekunden, wir erhalten damit immerhin um die 10 Werte pro Sekunde. Die API garantiert uns zwar dieses Update-Intervall nicht zu 100%, für unsere Zwecke ist das aber ausreichend. Per start() wird der Accelerometer gestartet. Zum Schluss des Konstrutors starten wir noch einen QTimer, der alle 10 Millisekunden das Signal updateGL() emitiert. Die Klasse zeichnet so maximal 100 Frames pro Sekunde. Am Ende des Konstruktors machen wir das Widget mit showFullScreen() bildschirmfüllend.

QGLWidgets vollführen ihre Aufgabe über das Signal updateGL() und die beiden Funktionen initializeGL() und paintGL(). Das Signal startet das Zeichnen des derzeitigen Frames per glDraw(), letzten Endes wird dabei paintGL() ausgeführt. Diese Methode sowie die Methode initializeGL() müssen wir in unserer Klasse implementieren, damit das Widget vollständig ist.

initializeGL() kümmert sich um die Initialisierung aller für das Zeichnen wichtigen Daten, hier vor allem um das Shader-Programm:

void GLWidget::initializeGL ()
{
    glClearColor(0.1f, 0.1f, 0.2f, 1.0f);

    QGLShader *vshader1 = new QGLShader(QGLShader::Vertex, this);
    const char *vsrc1 =
        "attribute highp vec4 vertex;\n"
        "attribute mediump vec3 normal;\n"
        "uniform mediump mat4 matrix;\n"
        "varying mediump vec4 color;\n"
        "void main(void)\n"
        "{\n"
        "    vec3 toLight = normalize(vec3(0.0, 0.3, 1.0));\n"
        "    float angle = max(dot(normal, toLight), 0.0);\n"
        "    vec3 col = vec3(0.40, 1.0, 0.0);\n"
        "    color = vec4(col * 0.2 + col * 0.8 * angle, 1.0);\n"
        "    color = clamp(color, 0.0, 1.0);\n"
        "    gl_Position = matrix * vertex;\n"
        "}\n";
    vshader1->compileSourceCode(vsrc1);

    QGLShader *fshader1 = new QGLShader(QGLShader::Fragment, this);
    const char *fsrc1 =
        "varying mediump vec4 color;\n"
        "void main(void)\n"
        "{\n"
        "    gl_FragColor = color;\n"
        "}\n";
    fshader1->compileSourceCode(fsrc1);

    program1.addShader(vshader1);
    program1.addShader(fshader1);
    program1.link();

    vertexAttr1 = program1.attributeLocation("vertex");
    normalAttr1 = program1.attributeLocation("normal");
    matrixUniform1 = program1.uniformLocation("matrix");

    m_fAngle = 0;
    m_fScale = 1;
    createGeometry();
}

In unserem Fall generieren wir in den Variablen vshader1 und fshader1 einen Vertex- und einen Fragment-Shader. Beide zusammen bilden in der Member-Variablen program1 ein QGLShaderProgram. Über die Aufrufe attributeLocation() und uniformLocation() holen wir uns die Adressen der entsprechenden Variablen im Shader-Programm. In diese Adressen schreiben wir dann später bei jedem Zeichnen die entsprechenden Daten des zu zeichnenden Objekts. Der Aufruf von createGeometry() am Ende generiert das zu zeichnende 3D-Objekt, in unserem Fall ein dreidimensionales “Q” als Qt-Logo. Die Funktion createGeometry() führe ich hier nicht extra auf, selbstverständlich ist sie aber in der Datei glwidet.cpp enthalten, die über den oben aufgeführten Link oder im Download-Paket verfügbar ist.

Im Prinzip fehlt nun nur noch die Zeichenfunktion updateGL(). Zum Zeichnen des Qt-Logos greift sie dabei auf eine Hilfsfunktion paintQtLogo() zurück, die das eben definierte Shader-Programm mit den Daten des Qt-Logos füllt und ausführt:

void GLWidget::paintQtLogo()
{
    program1.enableAttributeArray(normalAttr1);
    program1.enableAttributeArray(vertexAttr1);
    program1.setAttributeArray(vertexAttr1, vertices.constData());
    program1.setAttributeArray(normalAttr1, normals.constData());
    glDrawArrays(GL_TRIANGLES, 0, vertices.size());
    program1.disableAttributeArray(normalAttr1);
    program1.disableAttributeArray(vertexAttr1);
}

Die gl*-Funktionen wie glDrawArrays und glClearColor (am Anfang der Funktion initializeGL()) sind übrigens keine Qt-Funktionen, sondern stammen direkt aus der OpenGL-Bibliothek des Systems (eine Beschreibung von glDrawArrays findet ihr z.B. hier auf den Webseiten von OpenGL).

Die Funktion paintGL() implementieren wir nun folgendermaßen:

void GLWidget::paintGL()
{
    QPainter painter;
    painter.begin(this);

    painter.beginNativePainting();

    glClearColor(0.1f, 0.1f, 0.2f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glFrontFace(GL_CW);
    glCullFace(GL_FRONT);
    glEnable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);

    QMatrix4x4 modelview;
    if (_rotationSensorAvailable) {
        modelview.rotate((xRot / 16) + (_rotationSensor->reading()->x()*10), 0.0f, 1.0f, 0.0f);
        modelview.rotate((yRot / 16) - (_rotationSensor->reading()->y()*10), 1.0f, 0.0f, 0.0f);
    } else {
        modelview.rotate(xRot / 16, 0.0f, 1.0f, 0.0f);
        modelview.rotate(yRot / 16, 1.0f, 0.0f, 0.0f);
    }
    modelview.rotate(zRot / 16, 0.0f, 0.0f, 1.0f);
    modelview.scale(m_fScale);
    modelview.translate(0.0f, -0.2f, 0.0f);

    program1.bind();
    program1.setUniformValue(matrixUniform1, modelview);
    paintQtLogo();
    program1.release();

    glDisable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);

    painter.endNativePainting();

    painter.end();

    swapBuffers();
}

Ein QPainter zeichnet uns alles auf den Bildschirm. Und an dieser Stelle greifen wir nun auch auf die Daten des Accelerometers zu: die Funktion _rotationSensor->reading() liefert ein QAccelerometerReading-Objekt, über dessen x()-, y()- und z()-Funktionen wir die aktuellen Werte auslesen können. In diesem Fall interessieren wir uns nur für die x- und y-Werte und drehen den gesamten View (also nicht das Objekt!) um die entsprechenden Werte. Zur Transformation benutzen wir eine 4×4-Matrix, die mit setUniformValue(matrixUniform1, modelview) auf das Shader-Programm angewandt wird (im Shader-Programm entsprach diese Matrix der Definition von uniform mediump mat4 matrix;).

xRot, yRot und zRot sind Member-Variablen mit den Rotationswerten aus der Mausbewegung. Dazu wurden in GLWidget die Funktionen mousePressEvent(QMouseEvent *event) und mouseMoveEvent(QMouseEvent *event) überschrieben, so dass wir die entsprechenden Events abfangen und in der Funktion @ mouseMoveEvent() die Rotationswerte entsprechend ändern. Auch diese Funktionen sind hier nicht weiter aufgeführt.

Kompilierung und Paketerstellung

Wer sich das oben verlinkte Paket herunterlädt, kann die Datei qtprog.pro als Projekt im Qt Creator öffnen und die Anwendung mit einem Klick auf den “Play”-Button starten.

Unter Scratchbox kann außerdem ein Debianpaket erstellt werden. Dazu startet ihr Sratchbox, setzt das ARMEL-Target und baut das Paket mit “dpkg-buildpackage”:

maemo@maemo-desktop:~$ /scratchbox/login 
Welcome to Scratchbox, the cross-compilation toolkit!
Use 'sb-menu' to change your compilation target.
See /scratchbox/doc/ for documentation.
[sbox-FREMANTLE_X86: ~] > sb-conf select FREMANTLE_ARMEL
Shell restarting...
[sbox-FREMANTLE_ARMEL: ~] > cd glsensordemo-0.1/
[sbox-FREMANTLE_ARMEL: ~/glsensordemo-0.1] > dpkg-buildpackage -rfakeroot  
dpkg-buildpackage: source package is glsensordemo
dpkg-buildpackage: source version is 0.1
dpkg-buildpackage: source changed by Peter Bouda <admin@mobileqt.de>
dpkg-buildpackage: host architecture armel
dpkg-buildpackage: source version without epoch 0.1
: Using Scratchbox tools to satisfy builddeps
 . . . hier kommen viele Zeilen . . .
dpkg-deb: ignoring 1 warnings about the control file(s)
 dpkg-genchanges
dpkg-genchanges: warning: unknown information field `Xb-Maemo-Icon-26' in input data in package's section of control info file
dpkg-genchanges: including full source code in upload
dpkg-buildpackage: full upload; Debian-native package (full source is included)

Näheres zur Projekterstellung unter Scratchbox findet ihr in Tutorial zur Projekterstellung mit Scratchbox.

Hinweis: Das Projekt kann derzeit nicht mit MADDE kompiliert werden, da MADDE die Qt-Mobility-API (noch) nicht unterstützt.

Fazit

Die Qt-Mobility-Schnittstellen schaffen einen einfachen Zugriff auf die Funktionen der unterstützten Mobiltelefone. In unserem Beispiel haben wir gesehen, wie sich die Sensordaten des Accelerometers auf einfach Art und Weise zur Anwendungssteuerung nutzen lassen. Neben dem Accelerometer gibt es Zugriff auf Lichtsensor, Kompass, Annäherungssensor, usw. Für uns interessant wäre noch der Rotationssensor, über den sich Drehungen des Mobiltelefons noch direkter auf den OpenGL-View übertragen ließen. Im Moment ist die Klasse QRotationSensor aber noch für kein Gerät implementiert.

Falls ihr tiefer in OpenGL einsteigen wollt, dann habe ich auch noch folgenden Buchtipp für euch:

OpenGL ES 2.0 Programming Guide