OpenGL-Tutorial für Android: Erstellen eines Mandelbrot-Set-Generators

Veröffentlicht: 2022-03-11

OpenGL ist eine leistungsstarke plattformübergreifende API, die einen sehr engen Zugriff auf die Hardware des Systems in einer Vielzahl von Programmierumgebungen ermöglicht.

Also, warum sollten Sie es verwenden?

Es bietet eine Verarbeitung auf sehr niedrigem Niveau für Grafiken in 2D und 3D. Im Allgemeinen wird dies jegliches Klopfen vermeiden, das wir aufgrund von interpretierten oder höheren Programmiersprachen haben. Noch wichtiger ist jedoch, dass es auch Zugriff auf Hardwareebene auf eine Schlüsselfunktion bietet: GPU.

GPU kann viele Anwendungen erheblich beschleunigen, aber sie hat eine sehr spezifische Rolle in einem Computer. GPU-Kerne sind tatsächlich langsamer als CPU-Kerne. Wenn wir ein Programm ausführen, das insbesondere seriell ohne gleichzeitige Aktivität ausgeführt wird, ist es auf einem GPU-Kern fast immer langsamer als auf einem CPU-Kern. Der Hauptunterschied besteht darin, dass die GPU massive Parallelverarbeitung unterstützt. Wir können kleine Programme namens Shader erstellen, die effektiv auf Hunderten von Kernen gleichzeitig laufen. Das bedeutet, dass wir Aufgaben übernehmen können, die ansonsten unglaublich repetitiv sind, und sie gleichzeitig ausführen können.

OpenGL- und Mandelbrot-Set-Generator

In diesem Artikel werden wir eine einfache Android-Anwendung erstellen, die OpenGL verwendet, um ihren Inhalt auf dem Bildschirm darzustellen. Bevor wir beginnen, ist es wichtig, dass Sie bereits mit dem Schreiben von Android-Anwendungen und der Syntax einer C-ähnlichen Programmiersprache vertraut sind. Der gesamte Quellcode dieses Tutorials ist auf GitHub verfügbar.

OpenGL-Tutorial und Android

Um die Leistungsfähigkeit von OpenGL zu demonstrieren, schreiben wir eine relativ einfache Anwendung für ein Android-Gerät. Jetzt wird OpenGL auf Android unter einer Untergruppe namens OpenGL for Embedded Systems (OpenGL ES) vertrieben. Wir können uns dies im Wesentlichen nur als eine abgespeckte Version von OpenGL vorstellen, obwohl die erforderliche Kernfunktionalität weiterhin verfügbar sein wird.

Anstatt ein einfaches „Hallo Welt“ zu schreiben, schreiben wir eine täuschend einfache Anwendung: einen Mandelbrot-Mengen-Generator. Das Mandelbrot-Set basiert auf dem Gebiet der komplexen Zahlen. Die komplexe Analyse ist ein wunderbar weites Feld, daher konzentrieren wir uns mehr auf das visuelle Ergebnis als auf die eigentliche Mathematik dahinter.

Mit OpenGL ist das Erstellen eines Mandelbrot-Set-Generators einfacher als Sie denken!
Twittern

Versionsunterstützung

OpenGL-Versionsunterstützung

Wenn wir die Anwendung erstellen, möchten wir sicherstellen, dass sie nur an diejenigen mit angemessener OpenGL-Unterstützung verteilt wird. Beginnen Sie damit, die Verwendung von OpenGL 2.0 in der Manifest-Datei zwischen der Manifest-Deklaration und der Anwendung zu deklarieren:

 <uses-feature android:glEsVersion="0x00020000" android:required="true" />

An diesem Punkt ist die Unterstützung für OpenGL 2.0 allgegenwärtig. OpenGL 3.0 und 3.1 gewinnen an Kompatibilität, aber das Schreiben für beides wird ungefähr 65 % der Geräte auslassen, also treffen Sie die Entscheidung nur, wenn Sie sicher sind, dass Sie zusätzliche Funktionalität benötigen. Sie können implementiert werden, indem die Version auf „0x000300000“ bzw. „0x000300001“ gesetzt wird.

Anwendungsarchitektur

Opengl-Anwendungsarchitektur

Wenn Sie diese OpenGL-Anwendung auf Android erstellen, haben Sie im Allgemeinen drei Hauptklassen, die zum Zeichnen der Oberfläche verwendet werden: Ihre MainActivity , eine Erweiterung von GLSurfaceView und eine Implementierung von GLSurfaceView.Renderer . Von dort aus werden wir verschiedene Modelle erstellen, die Zeichnungen einkapseln.

MainActivity , in diesem Beispiel FractalGenerator genannt, wird im Wesentlichen nur Ihre GLSurfaceView instanziieren und alle globalen Änderungen nach unten weiterleiten. Hier ist ein Beispiel, das im Wesentlichen Ihr Boilerplate-Code sein wird:

 public class FractalGenerator extends Activity { private GLSurfaceView mGLView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Create and set GLSurfaceView mGLView = new FractalSurfaceView(this); setContentView(mGLView); } //[...] @Override protected void onPause() { super.onPause(); mGLView.onPause(); } @Override protected void onResume() { super.onResume(); mGLView.onResume(); } }

Dies wird auch die Klasse sein, in der Sie alle anderen Modifikatoren für das Aktivitätsniveau (z. B. immersiver Vollbildmodus) einfügen möchten.

Eine Klasse tiefer haben wir eine Erweiterung von GLSurfaceView , die als unsere primäre Ansicht fungiert. In dieser Klasse legen wir die Version fest, richten einen Renderer ein und steuern Berührungsereignisse. In unserem Konstruktor müssen wir nur die OpenGL-Version mit setEGLContextClientVersion(int version) setzen und auch unseren Renderer erstellen und setzen:

 public FractalSurfaceView(Context context){ super(context); setEGLContextClientVersion(2); mRenderer = new FractalRenderer(); setRenderer(mRenderer); }

Zusätzlich können wir Attribute wie den Rendermodus mit setRenderMode(int renderMode) . Da das Generieren einer Mandelbrot-Menge sehr kostspielig sein kann, verwenden wir RENDERMODE_WHEN_DIRTY , das die Szene nur bei der Initialisierung und bei expliziten Aufrufen von requestRender() . Weitere Einstellungsmöglichkeiten finden Sie in der GLSurfaceView API.

Nachdem wir den Konstruktor haben, möchten wir wahrscheinlich mindestens eine andere Methode überschreiben: onTouchEvent(MotionEvent event) , die für allgemeine berührungsbasierte Benutzereingaben verwendet werden kann. Ich werde hier nicht zu sehr ins Detail gehen, da dies nicht der Hauptfokus der Lektion ist.

Schließlich kommen wir zu unserem Renderer, wo die meiste Arbeit für die Beleuchtung oder vielleicht Änderungen in der Szene stattfindet. Zunächst müssen wir uns ein wenig damit befassen, wie Matrizen in der Grafikwelt funktionieren und funktionieren.

Schnelle Lektion in linearer Algebra

OpenGL ist stark auf die Verwendung von Matrizen angewiesen. Matrizen sind eine wunderbar kompakte Möglichkeit, Folgen verallgemeinerter Koordinatenänderungen darzustellen. Normalerweise erlauben sie uns beliebige Rotationen, Dehnungen/Kontraktionen und Reflexionen, aber mit ein wenig Geschick können wir auch Übersetzungen machen. Im Wesentlichen bedeutet dies alles, dass Sie problemlos jede gewünschte Änderung vornehmen können, einschließlich des Bewegens einer Kamera oder des Vergrößerns eines Objekts. Indem wir unsere Matrizen mit einem Vektor multiplizieren, der unsere Koordinate darstellt, können wir effektiv das neue Koordinatensystem erzeugen.

Die von OpenGL bereitgestellte Matrix-Klasse bietet eine Reihe vorgefertigter Methoden zum Berechnen von Matrizen, die wir benötigen, aber zu verstehen, wie sie funktionieren, ist eine kluge Idee, selbst wenn Sie mit einfachen Transformationen arbeiten.

Zuerst können wir darüber sprechen, warum wir vierdimensionale Vektoren und Matrizen verwenden werden, um mit Koordinaten umzugehen. Dies geht eigentlich auf die Idee zurück, unsere Verwendung von Koordinaten zu verfeinern, um Übersetzungen durchführen zu können: Während eine Übersetzung im 3D-Raum mit nur drei Dimensionen unmöglich ist, ermöglicht das Hinzufügen einer vierten Dimension die Fähigkeit.

Um dies zu veranschaulichen, können wir eine sehr einfache allgemeine Skalierungs-/Übersetzungsmatrix verwenden:

opengl-Skala und Übersetzungsmatrix

Als wichtiger Hinweis, OpenGL-Matrizen sind spaltenweise, also würde diese Matrix geschrieben werden als {a, 0, 0, 0, 0, b, 0, 0, 0, 0, c, 0, v_a, v_b, v_c, 1} , was senkrecht dazu steht, wie es normalerweise gelesen wird. Dies kann rationalisiert werden, indem sichergestellt wird, dass Vektoren, die bei der Multiplikation als Spalte erscheinen, das gleiche Format wie Matrizen haben.

Zurück zum Kodex

Mit diesem Wissen über Matrizen bewaffnet, können wir zum Entwerfen unseres Renderers zurückkehren. Normalerweise erstellen wir in dieser Klasse eine Matrix, die aus dem Produkt von drei Matrizen gebildet wird: Modell, Ansicht und Projektion. Dies würde passenderweise eine MVPMatrix genannt werden. Sie können hier mehr über die Besonderheiten erfahren, da wir einen einfacheren Satz von Transformationen verwenden werden – das Mandelbrot-Set ist ein zweidimensionales Vollbildmodell und erfordert nicht wirklich die Idee einer Kamera.

Zuerst richten wir die Klasse ein. Wir müssen die erforderlichen Methoden für die Renderer-Schnittstelle implementieren: onSurfaceCreated(GL10 gl, EGLConfig config) , onSurfaceChanged(GL10 gl, int width, int height) und onDrawFrame(GL10 gl) . Die gesamte Klasse sieht am Ende ungefähr so ​​​​aus:

 public class FractalRenderer implements GLSurfaceView.Renderer { //Provide a tag for logging errors private static final String TAG = "FractalRenderer"; //Create all models private Fractal mFractal; //Transformation matrices private final float[] mMVPMatrix = new float[16]; //Any other private variables needed for transformations @Override public void onSurfaceCreated(GL10 unused, EGLConfig config) { // Set the background frame color GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); //Instantiate all models mFractal = new Fractal(); } @Override public void onDrawFrame(GL10 unused) { //Clear the frame of any color information or depth information GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); //Create a basic scale/translate matrix float[] mMVPMatrix = new float[]{ -1.0f/mZoom, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f/(mZoom*mRatio), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, -mX, -mY, 0.0f, 1.0f}; //Pass the draw command down the line to all models, giving access to the transformation matrix mFractal.draw(mMVPMatrix); } @Override public void onSurfaceChanged(GL10 unused, int width, int height) { //Create the viewport as being fullscreen GLES20.glViewport(0, 0, width, height); //Change any projection matrices to reflect changes in screen orientation } //Other public access methods for transformations }

Es gibt auch zwei Dienstprogrammmethoden, die im bereitgestellten Code verwendet werden, checkGLError und loadShaders , um beim Debuggen und der Verwendung von Shadern zu helfen.

Bei all dem geben wir die Befehlskette weiter, um die verschiedenen Teile des Programms zu kapseln. Wir sind endlich an dem Punkt angelangt, an dem wir schreiben können, was unser Programm tatsächlich tut , anstatt wie wir theoretische Änderungen daran vornehmen können. Dabei müssen wir eine Modellklasse erstellen, die die Informationen enthält, die für ein bestimmtes Objekt in der Szene angezeigt werden müssen. In komplexen 3D-Szenen könnte dies ein Tier oder ein Teekessel sein, aber wir werden ein Fraktal als weitaus einfacheres 2D-Beispiel erstellen.

In Modellklassen schreiben wir die gesamte Klasse – es gibt keine Oberklassen, die verwendet werden müssen. Wir brauchen nur einen Konstruktor und eine Art Zeichenmethode, die alle Parameter aufnimmt.

Abgesehen davon gibt es noch eine Reihe von Variablen, die wir brauchen werden, die im Wesentlichen Boilerplates sind. Werfen wir einen Blick auf den genauen Konstruktor, der in der Fractal-Klasse verwendet wird:

 public Fractal() { // initialize vertex byte buffer for shape coordinates ByteBuffer bb = ByteBuffer.allocateDirect( // (# of coordinate values * 4 bytes per float) squareCoords.length * 4); bb.order(ByteOrder.nativeOrder()); vertexBuffer = bb.asFloatBuffer(); vertexBuffer.put(squareCoords); vertexBuffer.position(0); // initialize byte buffer for the draw list ByteBuffer dlb = ByteBuffer.allocateDirect( // (# of coordinate values * 2 bytes per short) drawOrder.length * 2); dlb.order(ByteOrder.nativeOrder()); drawListBuffer = dlb.asShortBuffer(); drawListBuffer.put(drawOrder); drawListBuffer.position(0); // Prepare shaders int vertexShader = FractalRenderer.loadShader( GLES20.GL_VERTEX_SHADER, vertexShaderCode); int fragmentShader = FractalRenderer.loadShader( GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); // create empty OpenGL Program mProgram = GLES20.glCreateProgram(); // add the vertex shader to program GLES20.glAttachShader(mProgram, vertexShader); // add the fragment shader to program GLES20.glAttachShader(mProgram, fragmentShader); // create OpenGL program executables GLES20.glLinkProgram(mProgram); }

Ein ziemlicher Schluck, nicht wahr? Glücklicherweise ist dies ein Teil des Programms, den Sie überhaupt nicht ändern müssen, außer dem Namen des Modells. Vorausgesetzt, Sie ändern die Klassenvariablen entsprechend, sollte dies für grundlegende Formen gut funktionieren.

Um Teile davon zu diskutieren, schauen wir uns einige Variablendeklarationen an:

 static float squareCoords[] = { -1.0f, 1.0f, 0.0f, // top left -1.0f, -1.0f, 0.0f, // bottom left 1.0f, -1.0f, 0.0f, // bottom right 1.0f, 1.0f, 0.0f }; // top right private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices

In squareCoords geben wir alle Koordinaten des Quadrats an. Beachten Sie, dass alle Koordinaten auf dem Bildschirm als Gitter mit (-1,-1) unten links und (1,1) oben rechts dargestellt werden.

In drawOrder geben wir die Reihenfolge der Koordinaten basierend auf Dreiecken an, aus denen das Quadrat bestehen würde. Insbesondere aus Gründen der Konsistenz und Geschwindigkeit verwendet OpenGL Dreiecke, um alle Oberflächen darzustellen. Um ein Quadrat zu erstellen, schneiden Sie einfach eine Diagonale (in diesem Fall 0 bis 2 ) ab, um zwei Dreiecke zu erhalten.

Um beide dem Programm hinzuzufügen, müssen Sie sie zuerst in einen rohen Byte-Puffer konvertieren, um den Inhalt des Arrays direkt mit der OpenGL-Schnittstelle zu verbinden. Java speichert Arrays als Objekte, die zusätzliche Informationen enthalten, die nicht direkt mit den zeigerbasierten C-Arrays kompatibel sind, die die OpenGL-Implementierung verwendet. Um hier Abhilfe zu schaffen, werden ByteBuffers verwendet, die den Zugriff auf den Rohspeicher des Arrays speichern.

Nachdem wir die Daten für die Scheitelpunkte und die Zeichnungsreihenfolge eingegeben haben, müssen wir unsere Shader erstellen.

Shader

Beim Erstellen eines Modells müssen zwei Shader erstellt werden: ein Vertex-Shader und ein Fragment-(Pixel-)Shader. Alle Shader sind in GL Shading Language (GLSL) geschrieben, einer C-basierten Sprache mit einer Reihe von eingebauten Funktionen, variablen Modifikatoren, Primitiven und Standard-Eingabe/Ausgabe. Unter Android werden diese als endgültige Strings durch loadShader(int type, String shaderCode) , eine der beiden Ressourcenmethoden im Renderer, übergeben. Lassen Sie uns zunächst die verschiedenen Arten von Qualifikationsmerkmalen durchgehen:

  • const : Jede finale Variable kann als Konstante deklariert werden, damit ihr Wert für einfachen Zugriff gespeichert werden kann. Zahlen wie π können als Konstanten deklariert werden, wenn sie im gesamten Shader häufig verwendet werden. Es ist wahrscheinlich, dass der Compiler abhängig von der Implementierung automatisch unveränderte Werte als Konstanten deklariert.
  • uniform : Uniform-Variablen sind solche, die für jedes einzelne Rendering als konstant deklariert werden. Sie werden im Wesentlichen als statische Argumente für Ihre Shader verwendet.
  • varying : Wenn eine Variable als variabel deklariert und in einem Vertex-Shader gesetzt ist, dann wird sie im Fragment-Shader linear interpoliert. Dies ist nützlich, um jede Art von Farbverlauf zu erstellen, und ist für Tiefenänderungen implizit.
  • attribute : Attribute können als nicht-statische Argumente für einen Shader betrachtet werden. Sie bezeichnen den Satz von Eingaben, die vertexspezifisch sind und nur in Vertex-Shadern erscheinen.

Darüber hinaus sollten wir zwei weitere Arten von Primitives diskutieren, die hinzugefügt wurden:

  • vec2 , vec3 , vec4 : Fließkommavektoren gegebener Dimension.
  • mat2 , mat3 , mat4 : Gleitkommamatrizen gegebener Dimension.

Auf Vektoren kann über ihre Komponenten x , y , z und w oder r , g , b und a zugegriffen werden. Sie können auch Vektoren beliebiger Größe mit mehreren Indizes generieren: Für vec3 a gibt a.xxyz ein vec4 mit den entsprechenden Werten von a zurück.

Matrizen und Vektoren können auch als Arrays indiziert werden, und Matrizen geben einen Vektor mit nur einer Komponente zurück. Das bedeutet, dass für mat2 matrix matrix[0].a gültig ist und matrix[0][0] 0] zurückgibt. Wenn Sie mit diesen arbeiten, denken Sie daran, dass sie als Primitive und nicht als Objekte fungieren. Betrachten Sie beispielsweise den folgenden Code:

 vec2 a = vec2(1.0,1.0); vec2 b = a; bx=2.0;

Dies lässt a=vec2(1.0,1.0) und b=vec2(2.0,1.0) , was nicht das ist, was man vom Objektverhalten erwarten würde, wo die zweite Zeile b einen Zeiger auf a geben würde.

Im Mandelbrot-Set befindet sich der Großteil des Codes im Fragment-Shader, dem Shader, der auf jedem Pixel ausgeführt wird. Nominell arbeiten Vertex-Shader an jedem Vertex, einschließlich Attributen, die pro Vertex gelten, wie Änderungen an Farbe oder Tiefe. Werfen wir einen Blick auf den extrem einfachen Vertex-Shader für ein Fraktal:

 private final String vertexShaderCode = "attribute vec4 vPosition;" + "void main() {" + " gl_Position = vPosition;" + "}";

Dabei ist gl_Position eine von OpenGL definierte Ausgangsvariable zur Aufzeichnung der Koordinaten eines Scheitelpunkts. In diesem Fall übergeben wir eine Position für jeden Scheitelpunkt, auf den wir gl_Position setzen. In den meisten Anwendungen würden wir vPosition mit einer MVPMatrix multiplizieren und unsere Scheitelpunkte transformieren, aber wir möchten, dass das Fraktal immer im Vollbildmodus angezeigt wird. Alle Transformationen werden mit einem lokalen Koordinatensystem durchgeführt.

Der Fragment-Shader wird der Ort sein, an dem die meiste Arbeit geleistet wird, um das Set zu generieren. Wir setzen fragmentShaderCode auf Folgendes:

 precision highp float; uniform mat4 uMVPMatrix; void main() { //Scale point by input transformation matrix vec2 p = (uMVPMatrix * vec4(gl_PointCoord,0,1)).xy; vec2 c = p; //Set default color to HSV value for black vec3 color=vec3(0.0,0.0,0.0); //Max number of iterations will arbitrarily be defined as 100. Finer detail with more computation will be found for larger values. for(int i=0;i<100;i++){ //Perform complex number arithmetic p= vec2(px*px-py*py,2.0*px*py)+c; if (dot(p,p)>4.0){ //The point, c, is not part of the set, so smoothly color it. colorRegulator increases linearly by 1 for every extra step it takes to break free. float colorRegulator = float(i-1)-log(((log(dot(p,p)))/log(2.0)))/log(2.0); //This is a coloring algorithm I found to be appealing. Written in HSV, many functions will work. color = vec3(0.95 + .012*colorRegulator , 1.0, .2+.4*(1.0+sin(.3*colorRegulator))); break; } } //Change color from HSV to RGB. Algorithm from https://gist.github.com/patriciogonzalezvivo/114c1653de9e3da6e1e3 vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 m = abs(fract(color.xxx + K.xyz) * 6.0 - K.www); gl_FragColor.rgb = color.z * mix(K.xxx, clamp(m - K.xxx, 0.0, 1.0), color.y); gl_FragColor.a=1.0; }

Ein Großteil des Codes ist lediglich die Mathematik und der Algorithmus für die Funktionsweise des Sets. Beachten Sie die Verwendung mehrerer eingebauter Funktionen: fract , abs , mix , sin und clamp , die alle mit Vektoren oder Skalaren arbeiten und Vektoren oder Skalare zurückgeben. Zusätzlich wird dot verwendet, der Vektorargumente akzeptiert und einen Skalar zurückgibt.

Nachdem wir unsere Shader für die Verwendung eingerichtet haben, haben wir einen letzten Schritt, nämlich die Implementierung der draw Funktion in unserem Modell:

 public void draw(float[] mvpMatrix) { // Add program to OpenGL environment GLES20.glUseProgram(mProgram); // get handle to vertex shader's vPosition member mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); //Pass uniform transformation matrix to shader GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0); //Add attribute array of vertices GLES20.glEnableVertexAttribArray(mPositionHandle); GLES20.glVertexAttribPointer( mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer); // Draw the square GLES20.glDrawElements( GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer); // Disable vertex array GLES20.glDisableVertexAttribArray(mPositionHandle); FractalRenderer.checkGlError("Test"); }

Die Funktion übergibt alle Argumente an die Shader, einschließlich der uniform Transformationsmatrix und der attribute .

Nachdem wir alle Teile des Programms zusammengebaut haben, können wir es endlich ausführen. Vorausgesetzt, dass die richtige Touch-Unterstützung gehandhabt wird, werden absolut faszinierende Szenen gemalt:

opengl-Tutorial

Opengl Mandelbrot-Tutorial

Mandelbrot-Generator-Tutorial

Fließkommagenauigkeit

Wenn wir ein bisschen mehr hineinzoomen, bemerken wir einen Zusammenbruch im Bild:

Das hat absolut nichts mit der Mathematik dahinter zu tun, sondern alles mit der Art und Weise, wie Zahlen in OpenGL gespeichert und verarbeitet werden. Während neuere Unterstützung für double Genauigkeit eingeführt wurde, unterstützt OpenGL 2.0 nativ nichts anderes als float s. Wir haben sie ausdrücklich als die Floats mit der höchsten Präzision bezeichnet, die in unserem Shader mit precision highp float verfügbar sind, aber selbst das ist nicht gut genug.

Um dieses Problem zu umgehen, besteht die einzige Möglichkeit darin, double s mit zwei float s zu emulieren. Diese Methode liegt tatsächlich in einer Größenordnung der tatsächlichen Genauigkeit einer nativ implementierten Methode, obwohl die Geschwindigkeit ziemlich hohe Kosten verursacht. Dies wird dem Leser als Übung überlassen, falls er eine höhere Genauigkeit wünscht.

Fazit

Mit ein paar Unterstützungsklassen kann OpenGL das Echtzeit-Rendering komplexer Szenen schnell aufrechterhalten. Das Erstellen eines Layouts, das aus einem GLSurfaceView , dem Festlegen seines Renderer und dem Erstellen eines Modells mit Shadern besteht, gipfelte in der Visualisierung einer schönen mathematischen Struktur. Ich hoffe, dass Sie genauso viel Interesse an der Entwicklung einer OpenGL ES-Anwendung finden werden!