Fast jedes auf dem Markt verfügbare Android-Telefon verfügt über eine Grafikverarbeitungseinheit (GPU). Wie der Name vermuten lässt, handelt es sich hierbei um eine Hardwareeinheit, die sich mit Berechnungen befasst, die normalerweise mit 3D-Grafiken zusammenhängen. Als App-Entwickler können Sie mit der GPU komplexe Grafiken und Animationen erstellen, die mit sehr hohen Bildraten laufen.
Derzeit können Sie mit zwei verschiedenen APIs mit der GPU eines Android-Geräts interagieren: 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 Lernprogramm unterstütze ich Sie beim Einstieg in OpenGL ES 2.0 in Android-Apps.
Um diesem Tutorial folgen zu können, benötigen Sie:
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 für eingebettete Systeme, ist eine Teilmenge der API.
OpenGL ES ist eine sehr einfache API. Mit anderen Worten, es bietet keine Methoden, mit denen Sie 3D-Objekte schnell erstellen oder bearbeiten können. Stattdessen müssen Sie während der Arbeit Aufgaben manuell verwalten, z. B. das Erstellen der einzelnen Scheitelpunkte und Flächen von 3D-Objekten, das Berechnen verschiedener 3D-Transformationen und das Erstellen verschiedener Arten von Shader.
Erwähnenswert ist auch, dass Sie mit dem Android-SDK und -NDK OpenGL ES-Code in Java und C schreiben können.
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 Lernprogramm verwenden wir jedoch die Apache Commons IO-Bibliothek, um den Inhalt einiger Textdateien zu lesen. Fügen Sie es daher als ein kompilieren
Abhängigkeit in Ihrem App-Modul build.gradle Datei:
'commons-io: commons-io: 2.5' kompilieren
Um Google Play-Nutzer, die über keine Geräte verfügen, die die von Ihnen benötigte OpenGL ES-Version unterstützen, von der Installation Ihrer App abzuhalten, fügen Sie außerdem Folgendes hinzu
kennzeichnen Sie die Manifest-Datei Ihres Projekts:
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 GLSurfaceView
, und wähle TextureView
Nur wenn sie beabsichtigen, ihre 3D-Grafiken auf eine andere zu legen Aussicht
Widget. Für die App erstellen wir in diesem Tutorial, GLSurfaceView
wird genügen.
Hinzufügen eines GLSurfaceView
Das Widget in Ihrer Layoutdatei unterscheidet sich nicht vom Hinzufügen eines anderen Widget.
Beachten Sie, dass wir die Breite unseres Widgets der Höhe angepasst 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 anzugeben. In einer späteren Phase erfahren Sie, was eine Projektionsmatrix ist.
A initialisieren GLSurfaceView
Widget in einem Aktivität
Klasse ist so einfach wie das Aufrufen der findViewById ()
Methode und Übergabe der ID an sie.
mySurfaceView = (GLSurfaceView) findViewById (R.id.my_surface_view);
Zusätzlich müssen wir das anrufen setEGLContextClientVersion ()
Methode, um explizit die Version von OpenGL ES anzugeben, die zum Zeichnen in das Widget verwendet wird.
mySurfaceView.setEGLContextClientVersion (2);
Obwohl es möglich ist, 3D-Objekte in Java zu erstellen, indem die X-, Y- und Z-Koordinaten aller Scheitelpunkte manuell codiert werden, ist dies sehr umständlich. Die Verwendung von 3D-Modellierungswerkzeugen ist jedoch viel einfacher. Mixer ist ein solches Werkzeug. Es ist Open Source, leistungsstark und sehr einfach zu erlernen.
Starten Sie den Mixer und drücken Sie X um den Standardwürfel zu löschen. Drücken Sie anschließend Shift-A und wählen Sie Mesh> Torus. Wir haben jetzt ein ziemlich komplexes 3D-Objekt, das aus 576 Scheitelpunkten besteht.
Um den Torus in unserer Android-App verwenden zu können, müssen Sie ihn als Wavefront-OBJ-Datei exportieren. Deshalb gehe zu Datei> Exportieren> Wellenfront (.obj). Geben Sie im nächsten Bildschirm der OBJ-Datei einen Namen. Stellen Sie sicher, dass die Dreieckige Gesichter und Vertex-Reihenfolge beibehalten Optionen werden ausgewählt und drücken Sie die Taste OBJ exportieren Taste.
Sie können jetzt Blender schließen und die OBJ-Datei in Ihr Android Studio-Projekt verschieben Vermögenswerte Mappe.
Wenn Sie es noch nicht bemerkt haben, handelt es sich bei der OBJ-Datei, die wir im vorherigen Schritt erstellt haben, um eine Textdatei, die mit einem beliebigen Texteditor geöffnet werden kann.
In der Datei steht jede Zeile, die mit einem "v" beginnt, für 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 Gesichtslinie die Indizes von drei Scheitelpunkten, die zusammen eine Fläche bilden. Das ist alles, was Sie wissen müssen, um eine OBJ-Datei zu analysieren.
Bevor Sie beginnen, erstellen Sie eine neue Java-Klasse namens Torus und zwei hinzufügen Liste
Objekte, eines für die Scheitelpunkte und eines für die Flächen, als seine Mitgliedsvariablen.
öffentliche Klasse Torus private ListeverticesList; private Liste FacesList; public Torus (Kontextkontext) verticesList = new ArrayList <> (); facesList = neue ArrayList <> (); // mehr Code geht hier
Der einfachste Weg, um alle einzelnen Zeilen der OBJ-Datei zu lesen, ist die Verwendung der Scanner
Klasse und seine nächste Zeile()
Methode. Während Sie die Zeilen durchlaufen und die beiden Listen auffüllen, können Sie die String
Klasse beginnt mit()
Methode, um zu prüfen, ob die aktuelle Zeile mit einem "v" oder einem "f" beginnt.
// Öffne die OBJ-Datei mit einem Scanner Scanner Scanner = new Scanner (context.getAssets (). Open ("torus.obj")); // alle Zeilen durchlaufen, während (scanner.hasNextLine ()) String line = scanner.nextLine (); if (line.startsWith ("v")) // Scheitelpunktlinie zur Liste der Scheitelpunkte hinzufügen verticesList.add (line); else if (line.startsWith ("f")) // Gesichtslinie zur Gesichtsliste hinzufügen facesList.add (line); // Schließen Sie den Scanner. Scanner.close ();
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 konvertieren. Um die Vertex-Koordinatendaten zu speichern, benötigen wir a FloatBuffer
Objekt. Für die Gesichtsdaten, die einfach aus Scheitelpunktindizes bestehen, a ShortBuffer
Objekt wird ausreichen.
Fügen Sie dem folgenden Element die folgenden Elementvariablen hinzu Torus
Klasse:
private FloatBuffer verticesBuffer; privater ShortBuffer facesBuffer;
Um die Puffer zu initialisieren, müssen wir zuerst eine ByteBuffer
Objekt mit der allocateDirect ()
Methode. Weisen Sie für den Vertices-Puffer vier Bytes für jede Koordinate zu, wobei die Koordinaten Fließkommazahlen sind. Einmal die ByteBuffer
Objekt erstellt wurde, können Sie es in ein konvertieren FloatBuffer
indem man es nennt asFloatBuffer ()
Methode.
// Puffer für Vertices erstellen ByteBuffer buffer1 = ByteBuffer.allocateDirect (verticesList.size () * 3 * 4); buffer1.order (ByteOrder.nativeOrder ()); verticesBuffer = buffer1.asFloatBuffer ();
Erstellen Sie auf ähnliche Weise eine andere ByteBuffer
Objekt für den Gesichtspuffer. Weisen Sie diesmal zwei Bytes für jeden Scheitelindex zu, da dies die Indizes sind unsigniert kurz
Literale. Stellen Sie außerdem sicher, dass Sie das verwenden asShortBuffer ()
Methode zur Konvertierung der ByteBuffer
Einwände gegen a ShortBuffer
.
// Puffer für Gesichter erstellen ByteBuffer buffer2 = ByteBuffer.allocateDirect (facesList.size () * 3 * 2); buffer2.order (ByteOrder.nativeOrder ()); facesBuffer = buffer2.asShortBuffer ();
Beim Auffüllen des Vertices-Puffers werden die Inhalte von durchlaufen verticesList
, Extrahieren der X-, Y- und Z-Koordinaten von jedem Element und Aufrufen der stellen()
Methode, um Daten in den Puffer einzufügen. weil verticesList
enthält nur Zeichenfolgen, die wir verwenden müssen parseFloat ()
die Koordinaten von Strings in konvertieren schweben
Werte.
for (String vertex: verticesList) Zeichenfolge [] = vertex.split (""); // Durch Leerzeichen aufgeteilt float x = Float.parseFloat (coords [1]); float y = Float.parseFloat (Koordinaten [2]); float z = Float.parseFloat (Koordinaten [3]); verticesBuffer.put (x); verticesBuffer.put (y); verticesBuffer.put (z); verticesBuffer.position (0);
Beachten Sie, dass wir im obigen Code die Position()
Methode, um die Position des Puffers zurückzusetzen.
Das Auffüllen des Gesichtspuffers ist etwas anders. Sie müssen das verwenden parseShort ()
Methode, um jeden Vertex-Index in einen kurzen Wert zu konvertieren. Da die Indizes von eins statt von null beginnen, müssen Sie außerdem einen Wert von ihnen abziehen, bevor Sie sie in den Puffer legen.
for (String face: facesList) String vertexIndices [] = face.split (""); short vertex1 = Short.parseShort (vertexIndices [1]); short vertex2 = Short.parseShort (vertexIndices [2]); short vertex3 = Short.parseShort (vertexIndices [3]); facesBuffer.put ((kurz) (vertex1 - 1)); facesBuffer.put ((kurz) (vertex2 - 1)); facesBuffer.put ((kurz) (vertex3 - 1)); facesBuffer.position (0);
Um unser 3D-Objekt rendern zu können, müssen wir einen Vertex-Shader und einen Fragment-Shader dafür 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 oder kurz GLSL geschrieben ist.
Wie Sie vielleicht schon vermutet haben, ist ein Vertex-Shader 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.
Erstellen Sie eine neue Datei mit dem Namen vertex_shader.txt in Ihrem Projekt res / raw Mappe.
Ein Vertex-Shader muss ein Attribut
globale Variable, um Vertex-Positionsdaten von Ihrem Java-Code zu erhalten. Fügen Sie zusätzlich eine Uniform
globale Variable, um eine Ansichtsprojektionsmatrix aus dem Java-Code zu erhalten.
In der Main()
Funktion des Vertex-Shaders müssen Sie den Wert von festlegen gl_position
, eine eingebaute GLSL-Variable, die die endgültige Position des Scheitelpunkts bestimmt. Für den Moment können Sie den Wert einfach auf das Produkt der setzen Uniform
und Attribut
globale Variablen.
Fügen Sie der Datei daher folgenden Code hinzu:
Attribut vec4 Position; einheitliche mat4-Matrix; void main () gl_Position = Matrix * Position;
Erstellen Sie eine neue Datei mit dem Namen fragment_shader.txt in Ihrem Projekt res / raw Mappe.
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, klicken Sie im Main()
Funktion eines Fragment-Shaders können Sie den gl_FragColor
eingebaute Variable.
Präzisions-Mediumschwimmer; void main () gl_FragColor = vec4 (1, 0.5, 0, 1.0);
In dem obigen Code ist die erste Zeile, die die Genauigkeit von Gleitkommazahlen angibt, wichtig, da ein Fragment-Shader keine Standardgenauigkeit für sie hat.
Zurück in Torus
Klasse, müssen Sie jetzt Code hinzufügen, um die beiden von Ihnen erstellten Shader zu kompilieren. Bevor Sie dies tun, müssen Sie sie jedoch von rohen Ressourcen in Strings konvertieren. Das IOUtils
Klasse, die Teil der IO-Bibliothek von Apache Commons ist, verfügt über eine toString ()
Methode, um genau das zu tun. Der folgende Code zeigt Ihnen, wie Sie es verwenden:
// Konvertiere vertex_shader.txt in einen String InputStream vertexShaderStream = context.getResources (). OpenRawResource (R.raw.vertex_shader); String vertexShaderCode = IOUtils.toString (vertexShaderStream, Charset.defaultCharset ()); vertexShaderStream.close (); // Konvertiere fragment_shader.txt in einen String InputStream fragmentShaderStream = context.getResources (). OpenRawResource (R.raw.fragment_shader); String fragmentShaderCode = IOUtils.toString (fragmentShaderStream, Charset.defaultCharset ()); fragmentShaderStream.close ();
Der Shader-Code muss zu OpenGL ES-Shader-Objekten hinzugefügt werden. Um ein neues Shader-Objekt zu erstellen, verwenden Sie die glCreateShader ()
Methode der GLES20
Klasse. Abhängig von dem Typ des Shader-Objekts, das Sie erstellen möchten, können Sie entweder übergeben GL_VERTEX_SHADER
oder GL_FRAGMENT_SHADER
dazu Die Methode gibt eine Ganzzahl 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 das verwenden glShaderSource ()
Methode.
Mit dem folgenden Code werden Shaderobjekte für den Vertex-Shader und den Fragment-Shader erstellt:
int vertexShader = GLES20.glCreateShader (GLES20.GL_VERTEX_SHADER); GLES20.glShaderSource (vertexShader, vertexShaderCode); int fragmentShader = GLES20.glCreateShader (GLES20.GL_FRAGMENT_SHADER); GLES20.glShaderSource (fragmentShader, fragmentShaderCode);
Wir können jetzt die Shader-Objekte an den übergeben glCompileShader ()
Methode, um den darin enthaltenen Code zu kompilieren.
GLES20.glCompileShader (vertexShader); GLES20.glCompileShader (fragmentShader);
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 eine Membervariable hinzu Torus
Klasse, um einen Verweis auf ein OpenGL ES-Programm zu speichern.
privates int-Programm;
Um ein neues Programm zu erstellen, verwenden Sie die glCreateProgram ()
Methode. Um die Vertex- und Fragment-Shader-Objekte daran anzubringen, verwenden Sie die glAttachShader ()
Methode.
program = GLES20.glCreateProgram (); GLES20.glAttachShader (Programm, vertexShader); GLES20.glAttachShader (Programm, fragmentShader);
An dieser Stelle können Sie das Programm verknüpfen und es verwenden. Verwenden Sie dazu die glLinkProgram ()
und glUseProgram ()
Methoden.
GLES20.glLinkProgram (Programm); GLES20.glUseProgram (Programm);
Mit den Shadern und Puffern haben wir alles, was wir brauchen, um unseren Torus zu zeichnen. Fügen Sie dem eine neue Methode hinzu Torus
Klasse aufgerufen zeichnen:
public void draw () // Zeichnungscode geht hier hin
In einem früheren Schritt haben wir im Vertex-Shader a definiert Position
Variable, um Vertex-Positionsdaten aus Java-Code zu erhalten. Es ist jetzt Zeit, die Vertex-Positionsdaten an sie zu senden. Dazu müssen wir uns zunächst mit der Position
Variable in unserem Java-Code mit der glGetAttribLocation ()
Methode. Darüber hinaus muss das Handle mithilfe von aktiviert werden glEnableVertexAttribArray ()
Methode.
Fügen Sie daher den folgenden Code in der zeichnen()
Methode:
int position = GLES20.glGetAttribLocation (Programm, "Position"); GLES20.glEnableVertexAttribArray (Position);
Um das zu zeigen Position
Handle zu unserem Vertices-Puffer, müssen wir die glVertexAttribPointer ()
Methode. Zusätzlich zum Vertices-Puffer selbst erwartet die Methode die Anzahl der Koordinaten pro Vertex, den Typ der Koordinaten und den Byte-Versatz für jeden Vertex. Weil wir drei Koordinaten pro Scheitelpunkt haben und jede Koordinate a ist schweben
, Der Byte-Offset muss sein 3 * 4
.
GLES20.glVertexAttribPointer (position, 3, GLES20.GL_FLOAT, false, 3 * 4, verticesBuffer);
Unser Vertex-Shader erwartet auch eine Ansichtsprojektionsmatrix. Obwohl eine solche Matrix nicht immer erforderlich ist, können Sie mit der Matrix besser steuern, wie Ihr 3D-Objekt gerendert wird.
Eine Sichtprojektionsmatrix ist einfach das Produkt der Sicht- und Projektionsmatrizen. In einer Ansichtsmatrix können Sie die Positionen Ihrer Kamera und den von Ihnen betrachteten Punkt angeben. Eine Projektionsmatrix hingegen ermöglicht es Ihnen nicht nur, das quadratische Koordinatensystem von OpenGL ES auf dem rechteckigen Bildschirm eines Android-Geräts abzubilden, sondern auch die Nah- und die Fernebene des Betrachtungsstumpfs anzugeben.
Um die Matrizen zu erstellen, können Sie einfach drei erstellen schweben
Arrays von Größe 16
:
float [] projectionMatrix = neuer float [16]; float [] viewMatrix = neuer Float [16]; Float [] productMatrix = neuer Float [16];
Um die Projektionsmatrix zu initialisieren, können Sie die frustumM ()
Methode der Matrix
Klasse. Es erwartet die Positionen der linken, rechten, unteren, oberen, nahen und fernen Clip-Ebenen. Da unsere Leinwand bereits ein Quadrat ist, können Sie die Werte verwenden -1
und 1
für die linke und rechte sowie die untere und obere Clipebene. Experimentieren Sie für die nahen und fernen Clip-Ebenen mit verschiedenen Werten.
Matrix.frustumM (Projektionsmatrix, 0, -1, 1, -1, 1, 2, 9);
Um die Ansichtsmatrix zu initialisieren, verwenden Sie die setLookAtM ()
Methode. Sie erwartet die Position der Kamera und den Punkt, auf den sie schaut. Sie können wieder mit verschiedenen Werten experimentieren.
Matrix.setLookAtM (viewMatrix, 0, 0, 3, -4, 0, 0, 0, 0, 1, 0);
Verwenden Sie zum Berechnen der Produktmatrix schließlich die multiplyMM ()
Methode.
Matrix.multiplyMM (productMatrix, 0, Projektionsmatrix, 0, Ansichtsmatrix, 0);
Um die Produktmatrix an den Vertex-Shader zu übergeben, müssen Sie einen Handle für dessen erhalten Matrix
Variable mit der glGetUniformLocation ()
Methode. Sobald Sie den Griff haben, können Sie ihn mit der auf die Produktmatrix zeigen glUniformMatrix ()
Methode.
int matrix = GLES20.glGetUniformLocation (Programm, "Matrix"); GLES20.glUniformMatrix4fv (Matrix, 1, falsch, ProduktMatrix, 0);
Sie müssen bemerkt haben, dass wir den Gesichtspuffer immer noch nicht verwendet haben. Das bedeutet, dass wir OpenGL ES immer noch nicht erklärt haben, wie die Scheitelpunkte zu Dreiecken verbunden werden sollen, die als Flächen unseres 3D-Objekts dienen.
Das glDrawElements ()
Mit dieser Methode können Sie den Flächenpuffer verwenden, um Dreiecke zu erstellen. Als Argument erwartet sie die Gesamtanzahl der Vertex-Indizes, den Typ jedes Index und den Flächenpuffer.
GLES20.glDrawElements (GLES20.GL_TRIANGLES, facesList.size () * 3, GLES20.GL_UNSIGNED_SHORT, facesBuffer);
Vergessen Sie nicht, das zu deaktivieren Attribut
Handler, den Sie zuvor aktiviert haben, um die Vertex-Daten an den Vertex-Shader zu übergeben.
GLES20.glDisableVertexAttribArray (Position);
Unsere GLSurfaceView
Widget benötigt ein GLSurfaceView.Renderer
Objekt, um 3D-Grafiken darstellen zu können. Du kannst den ... benutzen setRenderer ()
um einen Renderer damit zu verknüpfen.
mySurfaceView.setRenderer (new GLSurfaceView.Renderer () // Mehr Code geht hier);
In der onSurfaceCreated ()
Als Renderer-Methode müssen Sie angeben, wie oft die 3D-Grafik gerendert werden muss. Lassen Sie uns erst jetzt rendern, wenn sich die 3D-Grafik ändert. Dazu übergeben Sie die RENDERMODE_WHEN_DIRTY
konstant zum setRenderMode ()
Methode. Initialisieren Sie zusätzlich eine neue Instanz von Torus
Objekt.
@Override public void onSurfaceCreated (GL10 gl10, EGLConfig eglConfig) mySurfaceView.setRenderMode (GLSurfaceView.RENDERMODE_WHEN_DIRTY); Torus = neuer Torus (getApplicationContext ());
In der onSurfaceChanged ()
Mit dieser Methode des Renderers können Sie die Breite und Höhe Ihres Ansichtsfensters mithilfe von festlegen glViewport ()
Methode.
@Override public void onSurfaceChanged (GL10 gl10, int width, int height) GLES20.glViewport (0,0, width, height);
In der onDrawFrame ()
Methode des Renderers, fügen Sie einen Aufruf zum hinzu zeichnen()
Methode der Torus
Klasse, um tatsächlich den Torus zu zeichnen.
@Override public void onDrawFrame (GL10 gl10) torus.draw ();
An diesem Punkt können Sie Ihre App ausführen, um den orangefarbenen Torus zu sehen.
Sie wissen jetzt, wie Sie OpenGL ES in Android-Apps verwenden. In diesem Lernprogramm haben Sie außerdem gelernt, wie Sie eine Wavefront-OBJ-Datei analysieren und Scheitel- und Gesichtsdaten daraus extrahieren. Ich schlage vor, Sie generieren einige weitere 3D-Objekte mit Blender und versuchen, sie in der App zu rendern.
Obwohl wir uns nur auf OpenGL ES 2.0 konzentriert haben, ist OpenGL ES 3.x abwärtskompatibel mit OpenGL ES 2.0. Das heißt, wenn Sie OpenGL ES 3.x in Ihrer App bevorzugen, können Sie das einfach ersetzen GLES20
Klasse mit der GLES30
oder GLES31
Klassen.
Weitere Informationen zu OpenGL ES finden Sie auf den entsprechenden Referenzseiten. Wenn Sie mehr über die Entwicklung von Android-Apps erfahren möchten, lesen Sie einige der anderen Tutorials hier bei Envato Tuts+!