Slik bruker du OpenGL ES i Android Apps

Nesten hver Android-telefon som er tilgjengelig i markedet i dag, har en grafikkbehandlingsenhet, eller GPU for kort. Som navnet antyder, er dette en maskinvareenhet dedikert til håndteringsberegninger som vanligvis er relatert til 3D-grafikk. Som en apputvikler kan du bruke GPU til å lage kompliserte grafikk og animasjoner som kjører med svært høye rammepriser.

Det finnes for øyeblikket to forskjellige APIer du kan bruke til å samhandle med en Android-enhetens GPU: Vulkan og OpenGL ES. Mens Vulkan bare er tilgjengelig på enheter som kjører Android 7.0 eller nyere, støttes OpenGL ES av alle Android-versjoner.

I denne veiledningen hjelper jeg deg med å komme i gang med å bruke OpenGL ES 2.0 i Android-apper.

Forutsetninger

For å kunne følge denne opplæringen må du:

  • den nyeste versjonen av Android Studio
  • en Android-enhet som støtter OpenGL ES 2.0 eller høyere
  • en nylig versjon av Blender, eller annen 3D-modelleringsprogramvare

1. Hva er OpenGL ES?

OpenGL, som er kort for Open Graphics Library, er en plattformuavhengig API som lar deg lage hardware-akselerert 3D-grafikk. OpenGL ES, kort for OpenGL for Embedded Systems, er en del av API.

OpenGL ES er et svært lavt nivå API. Med andre ord tilbyr det ikke noen metoder som lar deg raskt lage eller manipulere 3D-objekter. I stedet, mens du jobber med det, forventes det å manuelt administrere oppgaver som å skape de individuelle hjørnene og ansiktene til 3D-objekter, beregne ulike 3D-transformasjoner og skape forskjellige typer shaders.

Det er også verdt å nevne at Android SDK og NDK sammen tillater deg å skrive OpenGL ES-relatert kode i både Java og C.

2. Prosjektoppsett

Fordi OpenGL ES APIs er en del av Android-rammene, trenger du ikke legge til noen avhengigheter i prosjektet ditt for å kunne bruke dem. I denne opplæringen bruker vi imidlertid Apache Commons IO-biblioteket til å lese innholdet i noen få tekstfiler. Legg derfor til det som en kompilere avhengighet i appmodulets build.gradle fil:

kompilere 'commons-io: commons-io: 2,5'

I tillegg, for å stoppe Google Play-brukere som ikke har enheter som støtter OpenGL ES-versjonen du trenger fra å installere appen din, legger du til følgende tag til prosjektets manifestfil:

3. Lag et lærred

Android-rammeverket tilbyr to widgets som kan fungere som et lerret for 3D-grafikken din: GLSurfaceView og TextureView. De fleste utviklere foretrekker å bruke GLSurfaceView, og velg TextureView bare når de har tenkt å legge over 3D-grafikken sin på en annen Utsikt widget. For appen vil vi lage i denne opplæringen, GLSurfaceView vil være tilstrekkelig.

Legge til en GLSurfaceView Widget til layoutfilen din er ikke annerledes enn å legge til en annen widget.

Legg merke til at vi har gjort bredden på vår widget lik den høyde han har. Å gjøre det er viktig fordi OpenGL ES-koordinatsystemet er en firkant. Hvis du må bruke et rektangulært lerret, husk å inkludere aspektforholdet mens du beregner projeksjonsmatrisen din. Du lærer hva en projeksjonsmatrise er i et senere trinn.

Initialisere a GLSurfaceView widget inne i en Aktivitet Klassen er så enkel som å ringe findViewById () metode og passerer sin id til den.

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

I tillegg må vi ringe setEGLContextClientVersion () Metode for å spesifisere spesifikt versjonen av OpenGL ES vi skal bruke til å trekke inn i widgeten.

mySurfaceView.setEGLContextClientVersion (2);

4. Opprett et 3D-objekt

Selv om det er mulig å lage 3D-objekter i Java ved å håndkoding X-, Y- og Z-koordinatene for alle sine hjørner, er det veldig tungvint å gjøre det. Bruk av 3D-modelleringsverktøy er i stedet langt lettere. Blender er et slikt verktøy. Det er åpen kildekode, kraftig og veldig lett å lære.

Brann opp Blender og trykk X for å slette standard kuben. Deretter trykker du på Shift-A og velg Mesh> Torus. Vi har nå et ganske komplisert 3D-objekt bestående av 576 hjørner.

For å kunne bruke torusen i vår Android-app, må vi eksportere den som en Wavefront OBJ-fil. Derfor gå til Fil> Eksporter> Wavefront (.obj). I det neste skjermbildet oppgir du OBJ-filen, kontroller at Triangulerte ansikter og Hold Vertex Order Alternativer er valgt, og trykk på Eksporter OBJ knapp.

Du kan nå lukke Blender og flytte OBJ-filen til Android Studio-prosjektet ditt eiendeler mappe.

5. Parser OBJ-filen

Hvis du ikke har lagt merke til det, er OBJ-filen vi opprettet i forrige trinn, en tekstfil, som kan åpnes ved hjelp av en hvilken som helst tekstredigerer.

I filen, representerer hver linje som starter med en "v" et enkelt toppunkt. På samme måte representerer hver linje som starter med en "f" en enkelt trekantet flate. Mens hver topplinje inneholder X-, Y- og Z-koordinatene til et toppunkt, inneholder hver ansiktslinje indeksene av tre hjørner, som sammen danner et ansikt. Det er alt du trenger å vite for å analysere en OBJ-fil.

Før du begynner, opprett en ny Java-klasse som heter torus og legg til to Liste objekter, en for toppene og en for ansiktene, som dens medlemsvariabler.

offentlig klasse Torus privat liste verticesList; privat liste facesList; offentlig Torus (kontekst kontekst) verticesList = new ArrayList <> (); facesList = new ArrayList <> (); // Mer kode går her

Den enkleste måten å lese alle de enkelte linjene i OBJ-filen er å bruke Scanner klasse og dens nextLine () metode. Mens du løper gjennom linjene og fyller de to lister, kan du bruke string klassens begynner med() metode for å kontrollere om den nåværende linjen starter med en "v" eller en "f".

// Åpne OBJ-filen med en Scanner Scanner-skanner = Ny skanner (context.getAssets (). Åpne ("torus.obj")); // Loop gjennom alle sine linjer mens (scanner.hasNextLine ()) String line = scanner.nextLine (); if (line.startsWith ("v")) // Legg vertex linje til listen over vertices verticesList.add (linje);  annet hvis (line.startsWith ("f")) // Legg til ansiktslinje til ansiktsliste facesList.add (linje);  // Lukk skanneren scanner.close (); 

6. Opprett bufferobjekter

Du kan ikke sende lister over hjørner og ansikter til metodene som er tilgjengelige i OpenGL ES API direkte. Du må først konvertere dem til bufferobjekter. For å lagre vertex-koordinatdataene, trenger vi en FloatBuffer gjenstand. For ansiktsdata, som ganske enkelt består av verteksindekser, a ShortBuffer objektet vil være tilstrekkelig.

Følg deretter følgende medlemsvariabler til torus klasse:

private FloatBuffer verticesBuffer; private ShortBuffer facesBuffer;

For å initialisere buffere må vi først opprette en ByteBuffer objekt ved hjelp av allocateDirect () metode. For vertices buffer, tilordne fire byte for hver koordinat, hva med koordinatene som flytende punkt tall. Først når ByteBuffer objektet er opprettet, du kan konvertere det til en FloatBuffer ved å ringe det asFloatBuffer () metode.

// Opprett buffer for vertices ByteBuffer buffer1 = ByteBuffer.allocateDirect (verticesList.size () * 3 * 4); buffer1.order (ByteOrder.nativeOrder ()); verticesBuffer = buffer1.asFloatBuffer ();

Likeledes opprett en annen ByteBuffer objekt for ansiktsbufferen. Denne gangen allokere to byte for hver toppunktindeks fordi indeksene er usignert kort litteraler. Sørg også for at du bruker asShortBuffer () metode for å konvertere ByteBuffer protester mot a ShortBuffer.

// Lag buffer for ansikter ByteBuffer buffer2 = ByteBuffer.allocateDirect (facesList.size () * 3 * 2); buffer2.order (ByteOrder.nativeOrder ()); facesBuffer = buffer2.asShortBuffer ();

Fylling av vertices buffer innebærer looping gjennom innholdet av verticesList, utvinning av X-, Y- og Z-koordinatene fra hvert element, og ringer til sette() Metode for å sette data inne i bufferen. Fordi verticesList inneholder bare strenge, vi må bruke parseFloat () å konvertere koordinatene fra strengene til flyte verdier.

for (String vertex: verticesList) String koords [] = vertex.split (""); // Splitt etter plass flyte 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);

Legg merke til at i ovennevnte kode har vi brukt stilling() metode for å tilbakestille posisjonen til bufferen.

Det er litt annerledes å populere ansiktsbufferen. Du må bruke parseShort () Metode for å konvertere hver toppunktsindeks til en kort verdi. I tillegg, fordi indeksene starter fra en i stedet for null, må du huske å trekke en fra dem før de plasseres i bufferen.

for (String ansikt: ansiktsliste) 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. Lag Shaders

For å kunne gjengi vårt 3D-objekt må vi opprette en topptekstskygge og et fragmentskader for det. For nå kan du tenke på en skygge som et veldig enkelt program skrevet i et C-lignende språk som heter OpenGL Shading Language, eller GLSL for kort.

En vertex shader, som du kanskje har gjettet, er ansvarlig for å håndtere et 3D-objektets hjørner. En fragment shader, også kalt en pixel shader, er ansvarlig for å fargelegge 3D-objektets piksler.

Trinn 1: Opprett en Vertex Shader

Opprett en ny fil som heter vertex_shader.txt inne i prosjektet ditt res / rå mappe.

En vertex shader må ha en Egenskap global variabel inne i den for å motta toppunktsdata fra din Java-kode. I tillegg legger du til en uniform global variabel for å motta en visningsfremvisningsmatrise fra Java-koden.

Inne i hoved() funksjonen av vertex shader, må du sette verdien av gl_position, en GLSL innebygd variabel som bestemmer toppunktets sluttposisjon. For nå kan du bare sette verdien til produktet av uniform og Egenskap globale variabler.

Følg derfor følgende kode til filen:

Tilordne vec4 posisjon uniform mat4 matrise; void main () gl_Position = matrise * posisjon; 

Trinn 2: Lag en Fragment Shader

Opprett en ny fil som heter fragment_shader.txt inne i prosjektet ditt res / rå mappe.

For å holde denne opplæringen kort, oppretter vi nå en svært minimalistisk fragmentarbeider som bare tildeler fargen oransje til alle piksler. For å tilordne en farge til en piksel, inne i hoved() funksjon av en fragment shader, kan du bruke gl_FragColor innebygd variabel.

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

I den ovennevnte koden er den første linjen som angir nøyaktigheten av flytpunktstall, viktig fordi en fragmentskader ikke har noen standardpresisjon for dem.

Trinn 3: Kompilere Shaders

Tilbake i torus klasse, må du legge til kode for å kompilere de to shaders du opprettet. Før du gjør det, må du imidlertid konvertere dem fra råressurser til strenger. De IOUtils klassen, som er en del av Apache Commons IO biblioteket, har a toString () metode for å gjøre nettopp det. Følgende kode viser deg hvordan du bruker den:

// Konverter vertex_shader.txt til en streng InputStream vertexShaderStream = context.getResources (). OpenRawResource (R.raw.vertex_shader); String vertexShaderCode = IOUtils.toString (vertexShaderStream, Charset.defaultCharset ()); vertexShaderStream.close (); // Konverter fragment_shader.txt til en streng InputStream fragmentShaderStream = context.getResources (). OpenRawResource (R.raw.fragment_shader); String fragmentShaderCode = IOUtils.toString (fragmentShaderStream, Charset.defaultCharset ()); fragmentShaderStream.close ();

Shaders-koden må legges til OpenGL ES shader-objekter. For å opprette et nytt shaderobjekt, bruk glCreateShader () metode av GLES20 klasse. Avhengig av hvilken type shader-objekt du vil opprette, kan du enten passere GL_VERTEX_SHADER eller GL_FRAGMENT_SHADER til det. Metoden returnerer et heltall som tjener som en referanse til shader-objektet. Et nylig opprettet shader-objekt inneholder ingen kode. For å legge til skyggekoden til shader-objektet, må du bruke glShaderSource () metode.

Følgende kode oppretter skyggerobjekter for både vertex shader og 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 nå sende shaderobjektene til glCompileShader () metode for å kompilere koden de inneholder.

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

8. Lag et program

Mens du gjør et 3D-objekt, bruker du ikke shaders direkte. I stedet legger du dem til et program og bruker programmet. Derfor legger du til en variabel for medlemmen til torus klasse for å lagre en referanse til et OpenGL ES-program.

privat int program;

For å opprette et nytt program, bruk glCreateProgram () metode. For å feste vertex- og fragment-shaderobjektene til den, bruk glAttachShader () metode.

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

På dette tidspunktet kan du koble programmet og begynne å bruke det. For å gjøre det, bruk glLinkProgram () og glUseProgram () fremgangsmåter.

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

9. Tegn 3D-objektet

Med shaders og buffere klar, har vi alt vi trenger for å tegne vår torus. Legg til en ny metode for torus klassen kalles tegne:

offentlig ugyldig tegning () // Tegningskode går her

I et tidligere trinn, inne i vertex shader, definerte vi a stilling variabel for å motta toppunktsdata fra Java-kode. Det er nå på tide å sende toppunktsdataene til den. For å gjøre det må vi først få et håndtak til stilling variabel i vår Java-kode ved hjelp av glGetAttribLocation () metode. I tillegg må håndtaket aktiveres ved hjelp av glEnableVertexAttribArray () metode.

Følgelig legger du til følgende kode inne i tegne() metode:

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

Å peke på stilling håndtere til våre vertices buffer, må vi bruke glVertexAttribPointer () metode. I tillegg til selve vertices-bufferen, forventer metoden antall koordinater per toppunkt, typen av koordinatene og byteforskyvningen for hvert toppunkt. Fordi vi har tre koordinater per vertex og hver koordinat er a flyte, Byteforskyvningen må være 3 * 4.

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

Våre toppskygger forventer også en visningsfremvisningsmatrise. Selv om en slik matrise ikke alltid er nødvendig, kan du bruke en til å få bedre kontroll over hvordan 3D-objektet ditt blir gjengitt.

En visning-projeksjonsmatrise er ganske enkelt produktet av visning og projeksjonsmatriser. En visningsmatrise lar deg spesifisere plasseringene til kameraet ditt og punktet det ser på. En projeksjonsmatrise, derimot, lar deg ikke bare kartlegge firkantkoordinatsystemet for OpenGL ES til den rektangulære skjermen på en Android-enhet, men angir også de nærliggende og fjerne flyene i visningskretsen.

For å lage matriser kan du bare lage tre flyte arrays av størrelse 16:

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

For å initialisere projeksjonsmatrisen, kan du bruke frustumM () metode av Matrise klasse. Den forventer plasseringen av venstre, høyre, bunn, topp, nær og langt klippeplan. Fordi lerretet vårt allerede er et kvadrat, kan du bruke verdiene -1 og 1 til venstre og høyre, og bunn- og toppklippplanene. For de nærliggende og fjerne klippeplanene, vær så snill å eksperimentere med forskjellige verdier.

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

For å initialisere visningsmatrisen, bruk setLookAtM () metode. Det forventer kameraets posisjoner og punktet det ser på. Du er igjen fri til å eksperimentere med forskjellige verdier.

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

Til slutt, for å beregne produktmatrisen, bruk multiplyMM () metode.

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

For å sende produktmatrisen til vertex shader må du få et håndtak til det matrise variabel ved bruk av glGetUniformLocation () metode. Når du har håndtaket, kan du peke det til produktmatrisen ved hjelp av glUniformMatrix () metode.

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

Du må ha lagt merke til at vi fortsatt ikke har brukt ansiktsbufferen. Det betyr at vi fortsatt ikke har fortalt OpenGL ES hvordan du kobler knivene til å danne trekanter, som vil fungere som ansiktene til vårt 3D-objekt.

De glDrawElements () Metoden lar deg bruke ansiktsbufferen til å lage trekant. Som argumenter forventer det totalt antall verteksindekser, typen av hver indeks og ansiktsbufferen.

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

Til slutt, husk å deaktivere Egenskap Handler du aktivert tidligere for å passere vertex data til vertex shader.

GLES20.glDisableVertexAttribArray (posisjon);

10. Opprett en Renderer

Våre GLSurfaceView Widget trenger a GLSurfaceView.Renderer protestere mot å kunne gjengi 3D-grafikk. Du kan bruke setRenderer () å knytte en gjengivelse med den.

mySurfaceView.setRenderer (ny GLSurfaceView.Renderer () // Mer kode går her);

Inne i onSurfaceCreated () Metoden til rendereren, må du angi hvor ofte 3D-grafikken må gjengis. For nå, la oss gjengi bare når 3D-grafikken endres. For å gjøre det, passere RENDERMODE_WHEN_DIRTY konstant til setRenderMode () metode. I tillegg initierer en ny forekomst av torus gjenstand.

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

Inne i onSurfaceChanged () Metoden til rendereren, du kan definere bredden og høyden på visningsporten din ved hjelp av glViewport () metode.

@Override offentlig tomrum påSurfaceChanged (GL10 gl10, int bredde, int høyde) GLES20.glViewport (0,0, bredde, høyde); 

Inne i onDrawFrame () Metoden til rendereren, legg til et anrop til tegne() metode av torus klasse å faktisk trekke torus.

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

På dette tidspunktet kan du kjøre appen din for å se den oransje torusen.

Konklusjon

Du vet nå hvordan du bruker OpenGL ES i Android-apper. I denne opplæringen lærte du også å analysere en Wavefront OBJ-fil og trekke ut topptekst og ansiktsdata fra den. Jeg foreslår at du genererer noen flere 3D-objekter ved hjelp av Blender, og prøv å gjengi dem i appen.

Selv om vi bare fokuserte på OpenGL ES 2.0, forstår vi at OpenGL ES 3.x er bakoverkompatibel med OpenGL ES 2.0. Det betyr at hvis du foretrekker å bruke OpenGL ES 3.x i appen din, kan du bare erstatte GLES20 klasse med GLES30 eller GLES31 klasser.

For å lære mer om OpenGL ES, kan du referere til referansesidene. Og for å lære mer om Android app utvikling, sørg for å sjekke ut noen av våre andre opplæringsprogrammer her på Envato Tuts+!

  • Slik kommer du i gang med Android's Native Development Kit

    Med lanseringen av Android Studio 2.2, har utviklingen av Android-applikasjoner som inneholder C ++-kode, blitt enklere enn noensinne. I denne opplæringen vil jeg vise deg hvordan ...
    Ashraff Hathibelagal
    Android
  • Android ting: Perifer inngang / utgang

    Android Things har en unik evne til enkelt å koble til eksterne elektronikkomponenter med Peripheral API og innebygd enhetsstøtte. I denne artikkelen…
    Paul Trebilcox-Ruiz
    Android SDK
  • Slik sikrer du en Android-app

    I denne artikkelen skal vi se på noen av de beste metodene du kan følge for å bygge en sikker Android-app. Dette betyr en app som ikke lekker ...
    Ashraff Hathibelagal
    Android
  • Koding av en Android-app med fladd og dart

    Googles Flutter er et grensesnitt for app-utviklingsrammer som bruker Dart-programmeringsspråket. I denne opplæringen vil jeg introdusere deg til grunnleggende om ...
    Ashraff Hathibelagal
    Android