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ör att kunna följa denna handledning behöver du:
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.
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:
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);
Ä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.
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 listaverticesList; 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 ();
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);
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.
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;
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.
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);
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);
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);
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.
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+!