7 days of WordPress plugins, themes & templates - for free!* Unlimited asset downloads! Start 7-Day Free Trial
Advertisement
  1. Code
  2. Android SDK

So verwenden Sie OpenGL ES in Android-Apps

Scroll to top
Read Time: 14 mins

German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)

Fast jedes heute auf dem Markt erhältliche Android-Handy verfügt über eine Grafikprozessoreinheit, kurz GPU. Wie der Name vermuten lässt, handelt es sich um eine Hardwareeinheit, die sich der Bearbeitung von Berechnungen widmet, die normalerweise mit 3D-Grafiken zusammenhängen. Als App-Entwickler können Sie die GPU nutzen, um komplexe Grafiken und Animationen zu erstellen, die mit sehr hohen Bildraten ausgeführt werden.

Derzeit gibt es zwei verschiedene APIs, mit denen Sie mit der GPU eines Android-Geräts interagieren können: Vulkan und OpenGL ES. Während Vulkan nur auf Geräten mit Android 7.0 oder höher verfügbar ist, wird OpenGL ES von allen Android-Versionen unterstützt.

In diesem Tutorial helfe ich Ihnen bei den ersten Schritten mit der Verwendung von OpenGL ES 2.0 in Android-Apps.

Voraussetzungen

Um diesem Tutorial folgen zu können, benötigen Sie:

  • die neueste Version von Android Studio
  • ein Android-Gerät, das OpenGL ES 2.0 oder höher unterstützt
  • eine aktuelle Version von Blender oder einer anderen 3D-Modellierungssoftware

1. Was ist OpenGL ES?

OpenGL, die Abkürzung für Open Graphics Library, ist eine plattformunabhängige API, mit der Sie hardwarebeschleunigte 3D-Grafiken erstellen können. OpenGL ES, kurz für OpenGL for Embedded Systems, ist eine Teilmenge der API.

OpenGL ES ist eine sehr Low-Level-API. Mit anderen Worten, es bietet keine Methoden, mit denen Sie 3D-Objekte schnell erstellen oder bearbeiten können. Stattdessen wird von Ihnen erwartet, dass Sie während der Arbeit damit manuell Aufgaben wie das Erstellen der einzelnen Scheitelpunkte und Flächen von 3D-Objekten, das Berechnen verschiedener 3D-Transformationen und das Erstellen verschiedener Shader-Typen verwalten.

Es ist auch erwähnenswert, dass Sie mit dem Android SDK und NDK zusammen OpenGL ES-bezogenen Code sowohl in Java als auch in C schreiben können.

2. Projekteinrichtung

Da die OpenGL ES-APIs Teil des Android-Frameworks sind, müssen Sie Ihrem Projekt keine Abhängigkeiten hinzufügen, um sie verwenden zu können. In diesem Tutorial verwenden wir jedoch die Apache Commons IO-Bibliothek, um den Inhalt einiger Textdateien zu lesen. Fügen Sie es daher als compile-Abhängigkeit in die Datei build.gradle Ihres App-Moduls ein:

Um Google Play-Nutzer, die keine Geräte haben, die die von Ihnen benötigte OpenGL ES-Version unterstützen, an der Installation Ihrer App zu hindern, fügen Sie außerdem das folgende <uses-feature>-Tag zur Manifestdatei Ihres Projekts hinzu:

3. Erstellen Sie eine Leinwand

Das Android-Framework bietet zwei Widgets, die als Leinwand für Ihre 3D-Grafiken dienen können: GLSurfaceView und TextureView. Die meisten Entwickler bevorzugen die Verwendung von GLSurfaceView und wählen TextureView nur dann, wenn sie beabsichtigen, ihre 3D-Grafiken auf einem anderen View-Widget zu überlagern. Für die App, die wir in diesem Tutorial erstellen, reicht GLSurfaceView aus.

Das Hinzufügen eines GLSurfaceView-Widgets zu Ihrer Layoutdatei unterscheidet sich nicht vom Hinzufügen anderer Widgets.

Beachten Sie, dass wir die Breite unseres Widgets gleich seiner Höhe gemacht haben. Dies ist wichtig, da das OpenGL ES-Koordinatensystem ein Quadrat ist. Wenn Sie eine rechteckige Leinwand verwenden müssen, denken Sie daran, das Seitenverhältnis bei der Berechnung Ihrer Projektionsmatrix einzubeziehen. In einem späteren Schritt erfahren Sie, was eine Projektionsmatrix ist.

Das Initialisieren eines GLSurfaceView-Widgets innerhalb einer Activity-Klasse ist so einfach wie das Aufrufen der findViewById()-Methode und das Übergeben ihrer ID an sie.

Außerdem müssen wir die Methode setEGLContextClientVersion() aufrufen, um explizit die Version von OpenGL ES anzugeben, die wir zum Zeichnen im Widget verwenden.

4. Erstellen Sie ein 3D-Objekt

Obwohl es möglich ist, 3D-Objekte in Java zu erstellen, indem man die X-, Y- und Z-Koordinaten aller ihrer Scheitelpunkte von Hand codiert, ist dies sehr umständlich. Die Verwendung von 3D-Modellierungswerkzeugen stattdessen ist viel einfacher. Blender ist ein solches Werkzeug. Es ist Open Source, leistungsstark und sehr einfach zu erlernen.

Starten Sie Blender und drücken Sie X, um den Standardwürfel zu löschen. Drücken Sie als Nächstes Umschalt-A und wählen Sie Mesh > Torus. Wir haben jetzt ein ziemlich komplexes 3D-Objekt bestehend aus 576 Scheitelpunkten.

Torus in BlenderTorus in BlenderTorus in Blender

Um den Torus in unserer Android-App verwenden zu können, müssen wir ihn als Wavefront OBJ-Datei exportieren. Gehen Sie daher zu Datei > Exportieren > Wavefront (.obj). Geben Sie im nächsten Bildschirm der OBJ-Datei einen Namen, stellen Sie sicher, dass die Optionen Triangulate Faces und Keep Vertex Order ausgewählt sind, und klicken Sie auf die Schaltfläche OBJ exportieren.

Exporting 3D object as Wavefront OBJ fileExporting 3D object as Wavefront OBJ fileExporting 3D object as Wavefront OBJ file

Sie können Blender jetzt schließen und die OBJ-Datei in den assets-Ordner Ihres Android Studio-Projekts verschieben.

5. Analysieren Sie die OBJ-Datei

Wenn Sie es noch nicht bemerkt haben, ist die OBJ-Datei, die wir im vorherigen Schritt erstellt haben, eine Textdatei, die mit jedem Texteditor geöffnet werden kann.

OBJ file opened with a text editorOBJ file opened with a text editorOBJ file opened with a text editor

In der Datei repräsentiert jede Zeile, die mit einem "v" beginnt, einen einzelnen Scheitelpunkt. In ähnlicher Weise repräsentiert jede mit einem "f" beginnende Linie eine einzelne dreieckige Fläche. Während jede Scheitelpunktlinie die X-, Y- und Z-Koordinaten eines Scheitelpunkts enthält, enthält jede Facettenlinie die Indizes von drei Scheitelpunkten, die zusammen eine Fläche bilden. Das ist alles, was Sie wissen müssen, um eine OBJ-Datei zu parsen.

Bevor Sie beginnen, erstellen Sie eine neue Java-Klasse namens Torus und fügen Sie zwei List-Objekte, eines für die Scheitelpunkte und eines für die Flächen, als Mitgliedsvariablen hinzu.

Der einfachste Weg, alle einzelnen Zeilen der OBJ-Datei zu lesen, ist die Verwendung der Scanner-Klasse und ihrer nextLine()-Methode. Beim Durchlaufen der Zeilen und Auffüllen der beiden Listen können Sie die Methode startsWith() der String-Klasse verwenden, um zu überprüfen, ob die aktuelle Zeile mit einem "v" oder einem "f" beginnt.

6. Pufferobjekte erstellen

Sie können die Listen der Scheitelpunkte und Flächen nicht direkt an die in der OpenGL ES-API verfügbaren Methoden übergeben. Sie müssen sie zuerst in Pufferobjekte umwandeln. Um die Eckpunktkoordinatendaten zu speichern, benötigen wir ein FloatBuffer-Objekt. Für die Flächendaten, die einfach aus Scheitelindizes bestehen, reicht ein ShortBuffer-Objekt aus.

Fügen Sie der Torus-Klasse dementsprechend die folgenden Member-Variablen hinzu:

Um die Puffer zu initialisieren, müssen wir zunächst ein ByteBuffer-Objekt mit der Methode allocateDirect() erstellen. Weisen Sie für den Vertices-Puffer vier Bytes für jede Koordinate zu, wobei die Koordinaten Gleitkommazahlen sind. Nachdem das ByteBuffer-Objekt erstellt wurde, können Sie es in einen FloatBuffer konvertieren, indem Sie seine Methode asFloatBuffer() aufrufen.

Erstellen Sie auf ähnliche Weise ein weiteres ByteBuffer-Objekt für den Face-Puffer. Weisen Sie dieses Mal zwei Bytes für jeden Scheitelpunktindex zu, da die Indizes unsigned short Literale sind. Stellen Sie außerdem sicher, dass Sie die Methode asShortBuffer() verwenden, um das ByteBuffer-Objekt in einen ShortBuffer zu konvertieren.

Das Auffüllen des vertices-Puffers beinhaltet das Durchlaufen des Inhalts von verticesList, das Extrahieren der X-, Y- und Z-Koordinaten von jedem Element und das Aufrufen der put()-Methode, um Daten in den Puffer zu schreiben. Da verticesList nur Strings enthält, müssen wir parseFloat() verwenden, um die Koordinaten von Strings in float-Werte umzuwandeln.

Beachten Sie, dass wir im obigen Code die Methode position() verwendet haben, um die Position des Puffers zurückzusetzen.

Das Auffüllen des Gesichtspuffers ist etwas anders. Sie müssen die Methode parseShort() verwenden, um jeden Scheitelpunktindex in einen kurzen Wert umzuwandeln. Da die Indizes bei eins statt bei null beginnen, müssen Sie außerdem daran denken, eins davon zu subtrahieren, bevor Sie sie in den Puffer legen.

7. Erstellen Sie Shader

Um unser 3D-Objekt rendern zu können, müssen wir dafür einen Vertex-Shader und einen Fragment-Shader erstellen. Im Moment können Sie sich einen Shader als ein sehr einfaches Programm vorstellen, das in einer C-ähnlichen Sprache namens OpenGL Shading Language, kurz GLSL, geschrieben wurde.

Ein Vertex-Shader ist, wie Sie vielleicht vermutet haben, für die Handhabung der Vertices eines 3D-Objekts verantwortlich. Ein Fragment-Shader, auch Pixel-Shader genannt, ist für das Einfärben der Pixel des 3D-Objekts verantwortlich.

Schritt 1: Erstellen Sie einen Vertex-Shader

Erstellen Sie eine neue Datei namens vertex_shader.txt im res/raw-Ordner Ihres Projekts.

Ein Vertex-Shader muss eine globale attribute-Variable enthalten, um Vertex-Positionsdaten von Ihrem Java-Code zu empfangen. Fügen Sie außerdem eine uniform globale Variable hinzu, um eine Ansichts-Projektions-Matrix aus dem Java-Code zu erhalten.

Innerhalb der main()-Funktion des Vertex-Shaders müssen Sie den Wert von gl_position setzen, einer integrierten GLSL-Variablen, die die endgültige Position des Vertex bestimmt. Im Moment können Sie seinen Wert einfach auf das Produkt der uniform und attribute-globalen Variablen setzen.

Fügen Sie der Datei dementsprechend den folgenden Code hinzu:

Schritt 2: Erstellen Sie einen Fragment-Shader

Erstellen Sie eine neue Datei namens fragment_shader.txt im res/raw-Ordner Ihres Projekts.

Um dieses Tutorial kurz zu halten, erstellen wir jetzt einen sehr minimalistischen Fragment-Shader, der einfach allen Pixeln die Farbe Orange zuweist. Um einem Pixel eine Farbe zuzuweisen, können Sie innerhalb der main()-Funktion eines Fragment-Shaders die integrierte Variable gl_FragColor verwenden.

Im obigen Code ist die erste Zeile, die die Genauigkeit von Gleitkommazahlen angibt, wichtig, da ein Fragment-Shader keine Standardgenauigkeit dafür hat.

Schritt 3: Kompilieren Sie die Shader

Zurück in der Torus-Klasse müssen Sie nun Code hinzufügen, um die beiden von Ihnen erstellten Shader zu kompilieren. Zuvor müssen Sie sie jedoch von Rohressourcen in Zeichenfolgen konvertieren. Die IOUtils-Klasse, die Teil der Apache Commons IO-Bibliothek ist, hat dafür eine toString()-Methode. Der folgende Code zeigt Ihnen, wie Sie ihn verwenden:

Der Code der Shader muss zu OpenGL ES-Shader-Objekten hinzugefügt werden. Um ein neues Shader-Objekt zu erstellen, verwenden Sie die Methode glCreateShader() der GLES20-Klasse. Abhängig vom Typ des Shader-Objekts, das Sie erstellen möchten, können Sie entweder GL_VERTEX_SHADER oder GL_FRAGMENT_SHADER daran übergeben. Die Methode gibt eine ganze Zahl zurück, die als Referenz auf das Shader-Objekt dient. Ein neu erstelltes Shader-Objekt enthält keinen Code. Um den Shader-Code zum Shader-Objekt hinzuzufügen, müssen Sie die Methode glShaderSource() verwenden.

Der folgende Code erstellt Shader-Objekte sowohl für den Vertex-Shader als auch für den Fragment-Shader:

Wir können die Shader-Objekte jetzt an die Methode glCompileShader() übergeben, um den darin enthaltenen Code zu kompilieren.

8. Erstellen Sie ein Programm

Beim Rendern eines 3D-Objekts verwenden Sie die Shader nicht direkt. Stattdessen hängen Sie sie an ein Programm an und verwenden das Programm. Fügen Sie daher der Torus-Klasse eine Membervariable hinzu, um eine Referenz auf ein OpenGL ES-Programm zu speichern.

Um ein neues Programm zu erstellen, verwenden Sie die Methode glCreateProgram(). Um die Vertex- und Fragment-Shader-Objekte daran anzuhängen, verwenden Sie die Methode glAttachShader().

An dieser Stelle können Sie das Programm verknüpfen und es verwenden. Verwenden Sie dazu die Methoden glLinkProgram() und glUseProgram().

9. Zeichnen Sie das 3D-Objekt

Wenn die Shader und Puffer fertig sind, haben wir alles, was wir brauchen, um unseren Torus zu zeichnen. Fügen Sie der Torus-Klasse eine neue Methode namens draw hinzu:

In einem früheren Schritt haben wir im Vertex-Shader eine position-Variable definiert, um Vertex-Positionsdaten aus Java-Code zu erhalten. Es ist jetzt an der Zeit, die Eckpunktpositionsdaten an ihn zu senden. Dazu müssen wir zunächst mit der Methode glGetAttribLocation() ein Handle für die position-Variable in unserem Java-Code abrufen. Außerdem muss das Handle mit der Methode glEnableVertexAttribArray() aktiviert werden.

Fügen Sie dementsprechend den folgenden Code in die draw()-Methode ein:

Um das position-Handle auf unseren Vertices-Puffer zu verweisen, müssen wir die Methode glVertexAttribPointer() verwenden. Zusätzlich zum Scheitelpunktpuffer selbst erwartet die Methode die Anzahl der Koordinaten pro Scheitelpunkt, den Koordinatentyp und den Byte-Offset für jeden Scheitelpunkt. Da wir drei Koordinaten pro Scheitelpunkt haben und jede Koordinate ein float ist, muss der Byte-Offset 3 * 4 betragen.

Unser Vertex-Shader erwartet auch eine Ansichts-Projektions-Matrix. Obwohl eine solche Matrix nicht immer erforderlich ist, können Sie mit einer solchen Matrix besser steuern, wie Ihr 3D-Objekt gerendert wird.

Eine Ansichts-Projektions-Matrix ist einfach das Produkt der Ansichts- und Projektionsmatrizen. Mit einer Ansichtsmatrix können Sie die Standorte Ihrer Kamera und den von ihr betrachteten Punkt angeben. Mit einer Projektionsmatrix hingegen können Sie nicht nur das quadratische Koordinatensystem von OpenGL ES auf den rechteckigen Bildschirm eines Android-Geräts abbilden, sondern auch die nahe und ferne Ebene des Betrachtungskegels festlegen.

Um die Matrizen zu erstellen, können Sie einfach drei float-Arrays der Größe 16 erstellen:

Um die Projektionsmatrix zu initialisieren, können Sie die Methode frustumM() der Matrix-Klasse verwenden. Es erwartet die Positionen der linken, rechten, unteren, oberen, nahen und fernen Clipebene. Da unsere Leinwand bereits ein Quadrat ist, können Sie die Werte -1 und 1 für die linke und rechte sowie die untere und obere Schnittebene verwenden. Experimentieren Sie für die nahen und fernen Clip-Ebenen mit unterschiedlichen Werten.

Verwenden Sie zum Initialisieren der Ansichtsmatrix die Methode setLookAtM(). Es erwartet die Positionen der Kamera und den Blickpunkt. Es steht Ihnen wieder frei, mit verschiedenen Werten zu experimentieren.

Um schließlich die Produktmatrix zu berechnen, verwenden Sie die Methode multiplyMM().

Um die Produktmatrix an den Vertex-Shader zu übergeben, müssen Sie mit der Methode glGetUniformLocation() ein Handle für seine matrix-Variable abrufen. Sobald Sie das Handle haben, können Sie es mit der Methode glUniformMatrix() auf die Produktmatrix zeigen.

Sie müssen bemerkt haben, dass wir den Gesichtspuffer immer noch nicht verwendet haben. Das bedeutet, dass wir OpenGL ES immer noch nicht gesagt haben, wie die Scheitelpunkte verbunden werden, um Dreiecke zu bilden, die als Gesichter unseres 3D-Objekts dienen.

Mit der Methode glDrawElements() können Sie den Face-Puffer verwenden, um Dreiecke zu erstellen. Als Argumente erwartet es die Gesamtzahl der Vertex-Indizes, den Typ jedes Index und den Face-Puffer.

Denken Sie schließlich daran, den zuvor aktivierten attribute-Handler zu deaktivieren, um die Vertex-Daten an den Vertex-Shader zu übergeben.

10. Erstellen Sie einen Renderer

Unser GLSurfaceView-Widget benötigt ein GLSurfaceView.Renderer-Objekt, um 3D-Grafiken rendern zu können. Sie können setRenderer() verwenden, um ihm einen Renderer zuzuordnen.

Innerhalb der Methode onSurfaceCreated() des Renderers müssen Sie angeben, wie oft die 3D-Grafik gerendert werden soll. Lassen Sie uns vorerst nur rendern, wenn sich die 3D-Grafik ändert. Übergeben Sie dazu die Konstante RENDERMODE_WHEN_DIRTY an die Methode setRenderMode(). Initialisieren Sie außerdem eine neue Instanz des Torus-Objekts.

Innerhalb der Methode onSurfaceChanged() des Renderers können Sie die Breite und Höhe Ihres Viewports mit der Methode glViewport() definieren.

Fügen Sie innerhalb der onDrawFrame()-Methode des Renderers einen Aufruf der draw()-Methode der Torus-Klasse hinzu, um den Torus tatsächlich zu zeichnen.

An diesem Punkt können Sie Ihre App ausführen, um den orangefarbenen Torus zu sehen.

App displaying torusApp displaying torusApp displaying torus

Abschluss

Sie wissen jetzt, wie Sie OpenGL ES in Android-Apps verwenden. In diesem Lernprogramm haben Sie auch gelernt, wie Sie eine Wavefront-OBJ-Datei analysieren und Vertex- und Flächendaten daraus extrahieren. Ich schlage vor, dass Sie mit Blender ein paar weitere 3D-Objekte generieren und versuchen, sie in der App zu rendern.

Obwohl wir uns nur auf OpenGL ES 2.0 konzentriert haben, verstehen Sie, dass OpenGL ES 3.x abwärtskompatibel mit OpenGL ES 2.0 ist. Das heißt, wenn Sie OpenGL ES 3.x in Ihrer App bevorzugen, können Sie die GLES20-Klasse einfach durch die GLES30- oder GLES31-Klassen ersetzen.

Weitere Informationen zu OpenGL ES finden Sie auf den Referenzseiten. Und um mehr über die Entwicklung von Android-Apps zu erfahren, sollten Sie sich einige unserer anderen Tutorials hier bei Envato Tuts+ ansehen!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.