Så här använder du OpenGL ES i Android Apps

Nästan alla Android-telefoner som finns tillgängliga på marknaden idag har en grafikbehandlingsenhet eller GPU för korta. Som namnet antyder är det en maskinvaruenhet som är avsedd för hanteringsberäkningar som vanligtvis är relaterade till 3D-grafik. Som apputvecklare kan du använda GPU: n för att skapa komplexa grafik och animeringar som körs med mycket höga bildhastigheter.

Det finns för närvarande två olika API: er som du kan använda för att interagera med en Android-enhetens GPU: Vulkan och OpenGL ES. Medan Vulkan endast är tillgängligt på enheter som kör Android 7.0 eller senare, stöds OpenGL ES av alla Android-versioner.

I den här handledningen hjälper jag dig att komma igång med att använda OpenGL ES 2.0 i Android-appar.

förutsättningar

För att kunna följa denna handledning behöver du:

  • den senaste versionen av Android Studio
  • en Android-enhet som stöder OpenGL ES 2.0 eller högre
  • en ny version av Blender, eller någon annan 3D-modelleringsprogramvara

1. Vad är OpenGL ES?

OpenGL, som är kort för Open Graphics Library, är ett plattformsoberoende API som gör att du kan skapa hårdvarubaserad 3D-grafik. OpenGL ES, kort för OpenGL för inbyggda system, är en delmängd av API: n.

OpenGL ES är ett mycket lågt nivå API. Med andra ord erbjuder det inte några metoder som gör att du snabbt kan skapa eller manipulera 3D-objekt. Istället, medan du arbetar med det, förväntas man manuellt hantera uppgifter som att skapa de enskilda punkterna och ansikten i 3D-objekt, beräkna olika 3D-omvandlingar och skapa olika typer av shaders.

Det är också värt att nämna att Android SDK och NDK tillsammans ger dig möjlighet att skriva OpenGL ES-relaterad kod i både Java och C.

2. Projektinställningar

Eftersom OpenGL ES APIs är en del av Android-ramen behöver du inte lägga till några beroende av ditt projekt för att kunna använda dem. I denna handledning använder vi dock Apache Commons IO-biblioteket för att läsa innehållet i några textfiler. Lägg därför till det som en sammanställa beroende av din appmodul build.gradle fil:

kompilera "commons-io: commons-io: 2,5"

För att stoppa Google Play-användare som inte har enheter som stöder OpenGL ES-versionen behöver du från att installera din app, lägg till följande tagga till ditt projekts manifestfil:

3. Skapa en Canvas

Android-ramen erbjuder två widgets som kan fungera som en duk för din 3D-grafik: GLSurfaceView och TextureView. De flesta utvecklare föredrar att använda GLSurfaceView, och välj TextureView bara när de tänker överlappa sin 3D-grafik på en annan Se widget. För appen kommer vi att skapa i denna handledning, GLSurfaceView kommer att räcka.

Lägga till en GLSurfaceView Widget till din layoutfil skiljer sig inte från att lägga till någon annan widget.

Observera att vi har gjort bredden på vår widget lika med dess höjd. Att göra det är viktigt eftersom OpenGL ES-koordinatsystemet är en fyrkant. Om du måste använda en rektangulär duk, kom ihåg att inkludera dess bildförhållande när du beräknar din projektionsmatris. Du lär dig vad en projiceringsmatris ligger i ett senare steg.

Initiera a GLSurfaceView widgeten inuti en Aktivitet klassen är så enkel som att ringa findViewById () metod och skickar sin id till den.

mySurfaceView = (GLSurfaceView) findViewById (R.id.my_surface_view);

Dessutom måste vi ringa setEGLContextClientVersion () metod för att explicit ange vilken version av OpenGL ES vi ska använda för att rita inuti widgeten.

mySurfaceView.setEGLContextClientVersion (2);

4. Skapa ett 3D-objekt

Även om det är möjligt att skapa 3D-objekt i Java genom att handkodning X, Y och Z-koordinaterna för alla sina hörn, är det väldigt besvärligt att göra det. Att använda 3D-modelleringsverktyg är istället mycket lättare. Blender är ett sådant verktyg. Det är öppen källkod, kraftfull och mycket lätt att lära.

Slå upp Blender och tryck på X för att ta bort standard kuben. Tryck sedan på Shift-A och välj Mesh> Torus. Vi har nu ett ganska komplext 3D-objekt bestående av 576 vertices.

För att kunna använda torusen i vår Android-app måste vi exportera den som en Wavefront OBJ-fil. Gå därför till Arkiv> Exportera> Wavefront (.obj). På nästa skärm, ange OBJ-filen, se till att Triangulerade ansikten och Håll Vertex Order alternativ är valda och trycker på Exportera OBJ knapp.

Du kan nu stänga Blender och flytta OBJ-filen till ditt Android Studio-projekt tillgångar mapp.

5. Parsa OBJ-filen

Om du inte har lagt märke till det är den OBJ-fil som vi skapade i föregående steg en textfil, som kan öppnas med någon textredigerare.

I filen är varje rad som börjar med en "v" en enda toppunkt. På samma sätt representerar varje linje som börjar med ett "f" en enda triangulär yta. Medan varje vertexlinje innehåller X, Y och Z-koordinaterna för ett vertex innehåller varje ansiktslinje indexen av tre vertikaler, vilka tillsammans bildar ett ansikte. Det är allt du behöver veta för att analysera en OBJ-fil.

Innan du börjar skapar du en ny Java-klass som heter Torus och lägg till två Lista objekt, en för spetsarna och en för ansikten, som dess medlemsvariabler.

offentlig klass Torus privat lista verticesList; privat lista facesList; offentliga Torus (kontexttext) verticesList = new ArrayList <> (); facesList = new ArrayList <> (); // Mer kod går här

Det enklaste sättet att läsa alla enskilda linjer i OBJ-filen är att använda Scanner klass och dess nextLine () metod. Medan du går igenom linjerna och fyller i de två listorna kan du använda Sträng klassens börjar med() metod för att kontrollera om den aktuella raden börjar med en "v" eller en "f".

// Öppna OBJ-filen med en Scanner Scanner Scanner = Ny Scanner (context.getAssets (). Öppna ("torus.obj")); // Loop genom alla dess rader medan (scanner.hasNextLine ()) String line = scanner.nextLine (); om (line.startsWith ("v")) // Lägg till vertex linje till lista över vertices verticesList.add (linje);  annars om (line.startsWith ("f")) // Lägg till ansiktslinje till ansiktslista facesList.add (linje);  // Stäng skannerns scanner.close (); 

6. Skapa buffertobjekt

Du kan inte skicka listorna över hörn och ansikten direkt till metoderna i OpenGL ES API. Du måste först konvertera dem till buffertobjekt. För att lagra vertexkoordinatdatan behöver vi en FloatBuffer objekt. För ansiktsdata, som helt enkelt består av vertex-index, a ShortBuffer objektet är tillräckligt.

Följ således följande medlemsvariabler till Torus klass:

privat FloatBuffer verticesBuffer; privat ShortBuffer facesBuffer;

För att initiera buffertarna måste vi först skapa en ByteBuffer objekt med hjälp av allocateDirect () metod. För verticesbufferten allokera fyra byte för varje koordinat, vad med koordinaterna som flytande punktnummer. När ByteBuffer objekt har skapats, du kan konvertera det till en FloatBuffer genom att ringa dess asFloatBuffer () metod.

// Skapa buffert för vertices ByteBuffer buffer1 = ByteBuffer.allocateDirect (verticesList.size () * 3 * 4); buffer1.order (ByteOrder.nativeOrder ()); verticesBuffer = buffer1.asFloatBuffer ();

På samma sätt, skapa en annan ByteBuffer objekt mot ansiktsbufferten. Den här gången tilldelas två byte för varje vertexindex eftersom indexen är unsigned short litteraler. Se också till att du använder asShortBuffer () Metod för att konvertera ByteBuffer motsätta sig a ShortBuffer.

// Skapa buffert för ansikten ByteBuffer buffer2 = ByteBuffer.allocateDirect (facesList.size () * 3 * 2); buffer2.order (ByteOrder.nativeOrder ()); facesBuffer = buffer2.asShortBuffer ();

Att fylla i verticesbufferten innebär att looping genom innehållet i verticesList, extrahera X-, Y- och Z-koordinaterna från varje objekt och ringa sätta() Metod för att sätta data inuti bufferten. Därför att verticesList innehåller bara strängar, vi måste använda parseFloat () att konvertera koordinaterna från strängar till flyta värden.

för (String vertex: verticesList) String coords [] = vertex.split (""); // Dela med utrymme float x = Float.parseFloat (koordinat [1]); float y = Float.parseFloat (koordinat [2]); float z = Float.parseFloat (koordinat [3]); verticesBuffer.put (x); verticesBuffer.put (y); verticesBuffer.put (z);  verticesBuffer.position (0);

Observera att i ovanstående kod har vi använt placera() metod för att återställa buffertens position.

Att betala ansiktsbufferten är något annorlunda. Du måste använda parseShort () Metod för att konvertera varje vertexindex till ett kort värde. Dessutom, eftersom indexen börjar från en istället för noll, måste du komma ihåg att subtrahera en från dem innan de placeras inuti bufferten.

för (String face: facesList) String vertexIndices [] = face.split (""); kort vertex1 = Short.parseShort (vertexIndices [1]); kort vertex2 = Short.parseShort (vertexIndices [2]); kort vertex3 = Short.parseShort (vertexIndices [3]); facesBuffer.put ((short) (vertex1 - 1)); facesBuffer.put ((short) (vertex2 - 1)); facesBuffer.put ((short) (vertex3 - 1));  facesBuffer.position (0);

7. Skapa Shaders

För att kunna göra vårt 3D-objekt måste vi skapa en toppskärm och en fragmentskärare för den. För tillfället kan du tänka på en skuggning som ett mycket enkelt program skrivet i ett C-liknande språk som heter OpenGL Shading Language, eller GLSL för kort.

En vertex-skuggare, som du kanske har gissat, ansvarar för att hantera 3D-objektets hörn. En fragmentskärare, även kallad en pixelskärare, ansvarar för att färga 3D-objektets pixlar.

Steg 1: Skapa en Vertex Shader

Skapa en ny fil som heter vertex_shader.txt inuti ditt projekt res / rå mapp.

En toppskärmshuggare måste ha en attribut global variabel inuti den för att ta emot vertexpositionsdata från din Java-kod. Dessutom lägg till en enhetlig global variabel för att ta emot en visningsprojektionsmatris från Java-koden.

Inuti main () funktionen av vertex shader, måste du ställa in värdet på gl_position, en GLSL inbyggd variabel som bestämmer toppunktets slutliga position. För nu kan du helt enkelt sätta sitt värde på produkten av enhetlig och attribut globala variabler.

Följ således följande kod till filen:

attribut vec4 position enhetlig mat4 matris; void main () gl_Position = matris * position; 

Steg 2: Skapa en Fragment Shader

Skapa en ny fil som heter fragment_shader.txt inuti ditt projekt res / rå mapp.

För att hålla denna handledning kort kommer vi nu att skapa en mycket minimalistisk fragmentskärm som helt enkelt tilldelar färgen orange till alla pixlar. Att tilldela en färg till en pixel, inuti main () funktionen av en fragmentskärare, kan du använda gl_FragColor inbyggd variabel.

precision mediump float; void main () gl_FragColor = vec4 (1, 0,5, 0, 1,0); 

I ovanstående kod är den första raden som anger precisionen av flytpunkten viktig eftersom en fragmentskärare inte har någon standard precision för dem.

Steg 3: Kompilera Shadersna

Tillbaka i Torus klass måste du lägga till kod för att kompilera de två shadersna du skapade. Innan du gör det måste du dock konvertera dem från råa resurser till strängar. De IOUtils klass, som är en del av Apache Commons IO-biblioteket, har a att stränga() metod för att göra just det. Följande kod visar hur du använder den:

// Konvertera vertex_shader.txt till en sträng InputStream vertexShaderStream = context.getResources (). OpenRawResource (R.raw.vertex_shader); String vertexShaderCode = IOUtils.toString (vertexShaderStream, Charset.defaultCharset ()); vertexShaderStream.close (); // Konvertera fragment_shader.txt till en sträng InputStream fragmentShaderStream = context.getResources (). OpenRawResource (R.raw.fragment_shader); String fragmentShaderCode = IOUtils.toString (fragmentShaderStream, Charset.defaultCharset ()); fragmentShaderStream.close ();

Shaders-koden måste läggas till i OpenGL ES-skuggobjekt. För att skapa ett nytt shaderobjekt, använd glCreateShader () metod för GLES20 klass. Beroende på vilken typ av shaderobjekt du vill skapa kan du antingen passera GL_VERTEX_SHADER eller GL_FRAGMENT_SHADER till det. Metoden returnerar ett heltal som fungerar som en referens till shaderobjektet. Ett nyskapat shaderobjekt innehåller inte någon kod. För att lägga till shader-koden i shader-objektet måste du använda glShaderSource () metod.

Följande kod skapar skuggobjekt för både vertex shader och fragment shader:

int vertexShader = GLES20.glCreateShader (GLES20.GL_VERTEX_SHADER); GLES20.glShaderSource (vertexShader, vertexShaderCode); int fragmentShader = GLES20.glCreateShader (GLES20.GL_FRAGMENT_SHADER); GLES20.glShaderSource (fragmentShader, fragmentShaderCode);

Vi kan nu skicka shaderobjekten till glCompileShader () metod för att kompilera koden de innehåller.

GLES20.glCompileShader (vertexShader); GLES20.glCompileShader (fragmentShader);

8. Skapa ett program

När du gör ett 3D-objekt använder du inte shaders direkt. I stället bifogar du dem till ett program och använder programmet. Lägg därför till en medlemsvariabel till Torus klass för att lagra en referens till ett OpenGL ES-program.

privata intprogram

För att skapa ett nytt program, använd glCreateProgram () metod. Om du vill bifoga toppunkts- och fragmentskärmobjekten till det, använder du glAttachShader () metod.

program = GLES20.glCreateProgram (); GLES20.glAttachShader (program, vertexShader); GLES20.glAttachShader (program, fragmentShader);

Vid denna tidpunkt kan du länka programmet och börja använda det. För att göra det, använd glLinkProgram () och glUseProgram () metoder.

GLES20.glLinkProgram (program); GLES20.glUseProgram (program);

9. Rita 3D-objektet

Med shaders och buffers redo har vi allt vi behöver för att rita vår torus. Lägg till en ny metod för Torus klass kallas dra:

public void draw () // Teckningskod går här

I ett tidigare steg, inuti vertex shader definierade vi a placera variabel för att ta emot vertexpositionsdata från Java-kod. Nu är det dags att skicka vertexpositionsdata till den. För att göra det måste vi först hämta ett handtag till placera variabel i vår Java-kod med hjälp av glGetAttribLocation () metod. Dessutom måste handtaget vara aktiverat med hjälp av glEnableVertexAttribArray () metod.

Följ därmed följande kod inuti dra() metod:

int position = GLES20.glGetAttribLocation (program, "position"); GLES20.glEnableVertexAttribArray (positionen);

Att peka på placera hantera till vår vertices buffert, vi måste använda glVertexAttribPointer () metod. Förutom själva verticesbufferten förväntar metoden antalet koordinater per vertex, typen av koordinaterna och byteförskjutningen för varje toppunkt. Eftersom vi har tre koordinater per vertex och varje koordinat är a flyta, byte offset måste vara 3 * 4.

GLES20.glVertexAttribPointer (position, 3, GLES20.GL_FLOAT, false, 3 * 4, verticesBuffer);

Vår toppskärmshuggare förväntar oss också en utsiktsprojektionsmatris. Även om en sådan matris inte alltid är nödvändig, med hjälp av en kan du få bättre kontroll över hur ditt 3D-objekt görs.

En utsiktsprojektionsmatris är helt enkelt produkten av utsikts- och projektionsmatriserna. En visningsmatris låter dig ange platserna för din kamera och den punkt som den tittar på. En projiceringsmatris låter dig dock inte bara kartlägga det öppna kvadratkoordinatsystemet OpenGL ES till den rektangulära skärmen på en Android-enhet, utan också ange de närmaste och fjärre planerna i betraktningsskärmen.

För att skapa matriserna kan du helt enkelt skapa tre flyta arrays av storlek 16:

float [] projectionMatrix = new float [16]; float [] viewMatrix = new float [16]; float [] productMatrix = new float [16];

För att initiera projektionsmatrisen kan du använda frustumM () metod för Matris klass. Den förväntar sig placeringen av vänster, höger, botten, topp, nära och långt klippplan. Eftersom vår duk redan är en fyrkant kan du använda värdena -1 och 1 till vänster och höger, och botten- och toppklippplanen. För nära och långt klippplanen, var god att experimentera med olika värden.

Matrix.frustumM (projectionMatrix, 0, -1, 1, -1, 1, 2, 9);

För att initiera visningsmatrisen, använd setLookAtM () metod. Den förväntar sig kamerans positioner och den punkt som den tittar på. Du är återigen fri att experimentera med olika värden.

Matrix.setLookAtM (viewMatrix, 0, 0, 3, -4, 0, 0, 0, 0, 1, 0);

Slutligen, för att beräkna produktmatrisen, använd multiplyMM () metod.

Matrix.multiplyMM (productMatrix, 0, projectionMatrix, 0, viewMatrix, 0);

För att skicka produktmatrisen till vertex shader måste du få ett handtag till dess matris variabel med hjälp av glGetUniformLocation () metod. När du har handtaget kan du peka det på produktmatrisen med hjälp av glUniformMatrix () metod.

int-matris = GLES20.glGetUniformLocation (program, "matrix"); GLES20.glUniformMatrix4fv (matris, 1, falsk, productMatrix, 0);

Du måste ha märkt att vi fortfarande inte har använt ansiktsbufferten. Det betyder att vi fortfarande inte har sagt till OpenGL ES hur man kopplar samman hörnen för att bilda trianglar, vilket kommer att fungera som ansikten på vårt 3D-objekt.

De glDrawElements () Metoden låter dig använda ansiktsbufferten för att skapa trianglar. Som sina argument förväntas det totala antalet vertex-index, typen av varje index och ansiktsbufferten.

GLES20.glDrawElements (GLES20.GL_TRIANGLES, facesList.size () * 3, GLES20.GL_UNSIGNED_SHORT, facesBuffer);

Slutligen, kom ihåg att avaktivera attribut handler du aktiverat tidigare för att skicka vertexdata till vertex shader.

GLES20.glDisableVertexAttribArray (positionen);

10. Skapa en Renderer

Vår GLSurfaceView widgeten behöver a GLSurfaceView.Renderer föremål för att kunna göra 3D-grafik. Du kan använda setRenderer () att associera en renderer med den.

mySurfaceView.setRenderer (ny GLSurfaceView.Renderer () // Mer kod går här);

Inuti onSurfaceCreated () Metoden för renderaren måste du ange hur ofta 3D-grafiken måste göras. För nu, låt oss bara göra när 3D-grafiken ändras. För att göra det, skicka RENDERMODE_WHEN_DIRTY konstant till setRenderMode () metod. Dessutom initiera en ny instans av Torus objekt.

@Override public void onSurfaceCreated (GL10 gl10, EGLConfig eglConfig) mySurfaceView.setRenderMode (GLSurfaceView.RENDERMODE_WHEN_DIRTY); torus = ny Torus (getApplicationContext ()); 

Inuti onSurfaceChanged () Metoden för renderaren kan du definiera bredden och höjden på ditt visningsport med glViewport () metod.

@Override public void onSurfaceChanged (GL10 gl10, int bredd, int höjd) GLES20.glViewport (0,0, bredd, höjd); 

Inuti onDrawFrame () Metoden för renderaren, lägg till ett samtal till dra() metod för Torus klass att faktiskt rita torusen.

@Override public void onDrawFrame (GL10 gl10) torus.draw (); 

Vid denna tidpunkt kan du köra din app för att se orange torusen.

Slutsats

Nu vet du hur du använder OpenGL ES i Android-appar. I denna handledning lärde du dig också att analysera en Wavefront OBJ-fil och extrahera vertex- och ansiktsdata från den. Jag föreslår att du genererar några fler 3D-objekt med Blender och försök att göra dem i appen.

Även om vi endast fokuserade på OpenGL ES 2.0, förstår vi att OpenGL ES 3.x är bakåtkompatibel med OpenGL ES 2.0. Det innebär att om du föredrar att använda OpenGL ES 3.x i din app, kan du helt enkelt byta ut GLES20 klass med GLES30 eller GLES31 klasser.

För att läsa mer om OpenGL ES kan du referera till referenssidorna. Och för att lära dig mer om Android app utveckling, se till att kolla in några av våra andra handledning här på Envato Tuts+!

  • Så här börjar du med Androids Native Development Kit

    Med lanseringen av Android Studio 2.2 har det blivit enklare än någonsin att utveckla Android-program som innehåller C ++-kod. I denna handledning visar jag dig hur ...
    Ashraff Hathibelagal
    Android
  • Android Saker: Perifer Input / Output

    Android Things har en unik förmåga att enkelt ansluta till externa elektronikkomponenter med Peripheral API och inbyggt support. I den här artikeln…
    Paul Trebilcox-Ruiz
    Android SDK
  • Så här säkrar du en Android-app

    I den här artikeln ska vi titta på några av de bästa metoderna du kan följa för att bygga en säker Android-app. Det här betyder en app som inte läcker ut ...
    Ashraff Hathibelagal
    Android
  • Kodning av en Android App med Fladd och Dart

    Googles Flutter är en plattformsutvecklingsram för applikationer som använder Dart-programmeringsspråket. I denna handledning introducerar jag dig till grunderna för ...
    Ashraff Hathibelagal
    Android