Cum se utilizează OpenGL ES în aplicațiile Android

Aproape toate telefoanele Android disponibile pe piață au astăzi o unitate de procesare grafică sau GPU pe scurt. După cum sugerează și numele, aceasta este o unitate hardware dedicată calculelor de manipulare care sunt de obicei legate de grafica 3D. Ca dezvoltator de aplicații, puteți utiliza GPU-ul pentru a crea grafică complexă și animație care rulează la rate de cadre foarte mari.

În prezent există două API-uri diferite pe care le puteți utiliza pentru a interacționa cu GPU-ul dispozitivului Android: Vulkan și OpenGL ES. În timp ce Vulkan este disponibil numai pe dispozitive care rulează Android 7.0 sau o versiune ulterioară, OpenGL ES este acceptată de toate versiunile Android.

În acest tutorial, vă ajut să începeți utilizarea OpenGL ES 2.0 în aplicațiile Android.

Cerințe preliminare

Pentru a putea urma acest tutorial, veți avea nevoie de:

  • cea mai recentă versiune de Android Studio
  • un dispozitiv Android care acceptă OpenGL ES 2.0 sau o versiune ulterioară
  • o versiune recentă de Blender sau orice alt software de modelare 3D

1. Ce este OpenGL ES??

OpenGL, care este scurt pentru Open Graphics Library, este un API independent de platformă, care vă permite să creați grafică 3D accelerată hardware. OpenGL ES, scurt pentru OpenGL pentru Embedded Systems, este un subset al API-ului.

OpenGL ES este un API foarte scăzut. Cu alte cuvinte, acesta nu oferă metode care să vă permită să creați rapid sau să manipulați obiecte 3D. În schimb, în ​​timp ce lucrați cu acesta, trebuie să gestionați manual sarcini cum ar fi crearea de vârfuri și chipuri individuale ale obiectelor 3D, calculul diferitelor transformări 3D și crearea diferitelor tipuri de shadere.

Este, de asemenea, de menționat faptul că Android SDK și NDK împreună vă permit să scrieți OpenGL ES legate de cod în ambele Java și C.

2. Configurarea proiectului

Deoarece OpenGL ES API-urile fac parte din framework-ul Android, nu trebuie să adăugați dependențe la proiectul dvs. pentru a le putea utiliza. În acest tutorial, cu toate acestea, vom folosi biblioteca Apache Commons IO pentru a citi conținutul câtorva fișiere text. Prin urmare, adăugați-o ca a compila dependență în modulul dvs. de aplicație build.gradle fişier:

compile 'commons-io: commons-io: 2.5'

În plus, pentru a opri utilizatorii Google Play care nu au dispozitive care acceptă versiunea OpenGL ES, trebuie să instalați aplicația, adăugați următoarele tag-ul la fișierul manifest al proiectului:

3. Creați o pânză

Cadrul Android oferă două widget-uri care pot acționa ca o pânză pentru grafica 3D: GLSurfaceView și TextureView. Majoritatea dezvoltatorilor preferă utilizarea GLSurfaceView, și alegeți TextureView numai când intenționează să își suprapună grafica 3D pe alta Vedere widget. Pentru aplicația pe care o vom crea în acest tutorial, GLSurfaceView va fi de ajuns.

Adăugarea unui GLSurfaceView widgetul pentru fișierul de aspect nu este diferit de adăugarea oricărui alt widget.

Rețineți că am făcut ca lățimea widgetului să fie egală cu înălțimea sa. Acest lucru este important deoarece sistemul de coordonate ES OpenGL este un pătrat. Dacă trebuie să utilizați o pânză dreptunghiulară, nu uitați să includeți raportul de aspect în timp ce calculați matricea de proiecție. Veți afla ce este o matrice de proiecție într-un pas mai târziu.

Inițializarea unui a GLSurfaceView widget în interiorul unui Activitate clasa este la fel de simplă ca apelarea findViewById () metodă și transmiterea id-ului la acesta.

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

În plus, trebuie să sunăm setEGLContextClientVersion () pentru a specifica explicit versiunea OpenGL ES pe care o vom folosi pentru a desena în interiorul widget-ului.

mySurfaceView.setEGLContextClientVersion (2);

4. Creați un obiect 3D

Deși este posibilă crearea obiectelor 3D în Java prin codarea manuală a coordonatelor X, Y și Z ale tuturor vârfurilor, acest lucru este foarte greoaie. Utilizarea instrumentelor de modelare 3D este mult mai ușoară. Blender este un astfel de instrument. Este o sursă deschisă, puternică și foarte ușor de învățat.

Activați Blender și apăsați X pentru a șterge cubul implicit. Apoi, apăsați Shift-A și selectați Mesh> Torus. Acum avem un obiect 3D destul de complex, format din 576 de noduri.

Pentru a putea folosi torus în aplicația noastră Android, trebuie să exportim ca fișier OBJ Wavefront. De aceea, du-te la Fișier> Export> Wavefront (.obj). În ecranul următor, dați un nume fișierului OBJ, asigurați-vă că Fețele triangulate și Păstrați Ordinul Vertex sunt selectate opțiunile și apăsați pe Export OBJ buton.

Acum puteți închide Blender și mutați fișierul OBJ în proiectul dvs. Android Studio bunuri pliant.

5. Parsează fișierul OBJ

Dacă nu ați observat deja, fișierul OBJ pe care l-am creat în pasul anterior este un fișier text care poate fi deschis utilizând orice editor de text.

În fișier, fiecare linie care începe cu un "v" reprezintă un singur vertex. În mod similar, fiecare linie care începe cu un "f" reprezintă o singură față triunghiulară. În timp ce fiecare linie de vârfuri conține coordonatele X, Y și Z ale unui vârf, fiecare linie de față conține indicii a trei vârfuri, care formează împreună o față. Asta e tot ce trebuie să știți pentru a analiza un fișier OBJ.

Înainte de a începe, creați o nouă clasă Java numită Torus și adăugați două Listă obiecte, unul pentru vârfuri și unul pentru fețe, ca variabile membre.

clasa publică Torus listă privată verticesList; Lista privată facesList; Torus public (Context context) verticesList = new ArrayList <> (); facesList = noul ArrayList <> (); // Mai multe coduri sunt aici

Cel mai simplu mod de a citi toate liniile individuale ale fișierului OBJ este să utilizați Scanner clasa și ei nextLine () metodă. În timp ce faceți buclă prin linii și populând cele două liste, puteți utiliza Şir clasa lui incepe cu() pentru a verifica dacă linia curentă începe cu un "v" sau un "f".

// Deschideți fișierul OBJ cu un Scanner Scanner Scanner = Scanner nou (context.getAssets () open ("torus.obj")); // Faceți buclă prin toate liniile sale în timp ce (scanner.hasNextLine ()) String line = scanner.nextLine (); dacă (line.startsWith ("v")) // Adăugați linia de vârfuri în lista vertexelor verticesList.add (line);  altceva dacă (line.startsWith ("f")) // Adăugați linia de față în lista de fețe facesList.add (linie);  // Închide scanerul scanner.close (); 

6. Creați obiecte tampon

Nu puteți trece direct listele de vârfuri și fețe la metodele disponibile în OpenGL ES API. Mai întâi trebuie să le convertiți în obiecte tampon. Pentru a stoca datele coordonatelor de vârf, vom avea nevoie de a FloatBuffer obiect. Pentru datele de față, care constau pur și simplu din indici de vârfuri, a ShortBuffer obiect este suficient.

În consecință, adăugați următoarele variabile membre la Torus clasă:

privat FloatBuffer verticesBuffer; private ShortBuffer facesBuffer;

Pentru a inițializa tampoanele, trebuie să creați mai întâi a ByteBuffer obiect folosind allocateDirect () metodă. Pentru tamponul de noduri, alocați patru octeți pentru fiecare coordonată, ceea ce cu coordonatele fiind numere în virgulă mobilă. Odata ce ByteBuffer obiect a fost creat, îl puteți converti în a FloatBuffer prin numirea lui asFloatBuffer () metodă.

// Crearea tampon pentru nodurile ByteBuffer buffer1 = ByteBuffer.allocateDirect (verticesList.size () * 3 * 4); buffer1.order (ByteOrder.nativeOrder ()); verticesBuffer = buffer1.asFloatBuffer ();

În mod similar, creați un altul ByteBuffer obiect pentru tamponul pentru fețe. De data aceasta, aloca doi octeți pentru fiecare index de vârf, deoarece indicii sunt nesemnate scurt literali. De asemenea, asigurați-vă că utilizați asShortBuffer () - metoda de conversie ByteBuffer obiecteaza la a ShortBuffer.

// Crearea tampon pentru fețe ByteBuffer buffer2 = ByteBuffer.allocateDirect (facesList.size () * 3 * 2); buffer2.order (ByteOrder.nativeOrder ()); facesBuffer = buffer2.asShortBuffer ();

Populația tamponului de vârfuri implică looping prin conținutul lui verticesList, extragând coordonatele X, Y și Z din fiecare element și apelând a pune() metoda de a pune datele în tampon. pentru că verticesList conține numai șiruri, trebuie să folosim parseFloat () pentru a converti coordonatele de la șiruri la pluti valorile.

pentru (Vertex șir: verticesList) Coarde șir [] = vertex.split (""); // Split prin spațiu float x = Float.parseFloat (coords [1]); float y = Float.parseFloat (coarde [2]); float z = Float.parseFloat (coarde [3]); verticesBuffer.put (x); verticesBuffer.put (y); verticesBuffer.put (z);  verticesBuffer.position (0);

Rețineți că în codul de mai sus am folosit poziţie() metoda de resetare a poziției tamponului.

Populația tamponului pentru fețe este puțin diferită. Trebuie să utilizați parseShort () metoda de a converti fiecare index de vârf la o valoare scurtă. În plus, deoarece indicii încep de la unul în loc de zero, trebuie să vă amintiți să sculați unul dintre ei înainte de a le pune în interiorul tamponului.

pentru (String face: facesList) String vertexIndices [] = face.split (""); scurt vertex1 = Short.parseShort (vertexIndices [1]); scurt vertex2 = Short.parseShort (vertexIndices [2]); scurtă vertex3 = Short.parseShort (vertexIndices [3]); facesBuffer.put ((scurt) (vertex1 - 1)); facesBuffer.put (scurtă) (vertex2 - 1)); facesBuffer.put (scurtă) (vertex3 - 1));  facesBuffer.position (0);

7. Creați Shaders

Pentru a putea face obiectul nostru 3D, trebuie să creăm un shader de vertex și un shader de fragmente pentru el. Pentru moment, vă puteți gândi la un shader ca pe un program foarte simplu, scris într-un limbaj asemănător C numit OpenGL Shading Language sau GLSL pe scurt.

Un shader de vârf, după cum probabil ați ghicit, este responsabil pentru manipularea vârfurilor unui obiect 3D. Un shader de fragment, denumit și un shader pixel, este responsabil pentru colorarea pixelilor obiectului 3D.

Pasul 1: Creați un Shader de vârf

Creați un fișier nou numit vertex_shader.txt în interiorul proiectului res / raw pliant.

Un shader de vârfuri trebuie să aibă un atribut variabilă globală în interiorul acestuia pentru a primi date despre poziția de vârf din codul dvs. Java. În plus, adăugați a uniformă variabilă globală pentru a primi o matrice de vizualizare-proiecție din codul Java.

În interiorul principal() funcția shader-ului de vârf, trebuie să setați valoarea lui gl_position, o variabilă încorporată GLSL care decide poziția finală a vârfului. Pentru moment, puteți seta pur și simplu valoarea sa la produsul produsului uniformă și atribut variabile globale.

În consecință, adăugați în fișier următorul cod:

atributul vec4 poziție; Matrice matrică uniformă; void principală () gl_Position = matrice * poziție; 

Pasul 2: Creați un shader de fragmente

Creați un fișier nou numit fragment_shader.txt în interiorul proiectului res / raw pliant.

Pentru a păstra scurt acest tutorial, vom crea acum un shader de minimalist fragment care atribuie pur și simplu culoarea portocalie tuturor pixelilor. Pentru a atribui o culoare unui pixel, în interiorul principal() funcția de shader fragment, puteți utiliza gl_FragColor built-in variabilă.

precizie, cu capăt mediu; void principal () gl_FragColor = vec4 (1, 0,5, 0, 1,0); 

În codul de mai sus, prima linie care specifică precizia numerelor în virgulă mobilă este importantă deoarece un fragment de shader nu are o precizie implicită pentru ele.

Pasul 3: Compilați Shaders

Înapoi în Torus , trebuie să adăugați acum codul pentru a compila cele două shadere create. Înainte de a face acest lucru, cu toate acestea, trebuie să le convertiți de la resurse prime la șiruri de caractere. IOUtils clasa, care face parte din biblioteca Apache Commons IO, are un toString () metodă pentru a face acest lucru. Următorul cod vă arată cum să îl utilizați:

// Convertiți vertex_shader.txt la un șir InputStream vertexShaderStream = context.getResources () openRawResource (R.raw.vertex_shader); Stringul vertexShaderCode = IOUtils.toString (vertexShaderStream, Charset.defaultCharset ()); vertexShaderStream.close (); // Convertește fragment_shader.txt într-un șir InputStream fragmentShaderStream = context.getResources () openRawResource (R.raw.fragment_shader); Fragment de șirShaderCode = IOUtils.toString (fragmentShaderStream, Charset.defaultCharset ()); fragmentShaderStream.close ();

Codul shaders trebuie adăugat obiectelor shader OpenGL ES. Pentru a crea un nou obiect shader, utilizați glCreateShader () metodă a GLES20 clasă. În funcție de tipul de obiect de shader pe care doriți să îl creați, puteți trece fie GL_VERTEX_SHADER sau GL_FRAGMENT_SHADER la el. Metoda returnează un întreg care servește drept referință la obiectul shader. Un obiect shader nou creat nu conține niciun cod. Pentru a adăuga codul shader la obiectul shader, trebuie să utilizați glShaderSource () metodă.

Următorul cod creează obiecte shader atât pentru shader-ul de vârf, cât și pentru shader-ul fragmentului:

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

Acum putem trece obiectele shader la glCompileShader () de compilare a codului pe care îl conțin.

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

8. Creați un program

În timp ce realizați un obiect 3D, nu utilizați direct shaderele. În schimb, le atașați la un program și utilizați programul. Prin urmare, adăugați o variabilă membru la Torus pentru a stoca o referință la un program OpenGL ES.

programul privat int;

Pentru a crea un nou program, utilizați glCreateProgram () metodă. Pentru a atașa obiectele de vârf și fragmente de fragmentare, utilizați glAttachShader () metodă.

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

În acest moment, puteți conecta programul și începe să îl utilizați. Pentru a face acest lucru, utilizați glLinkProgram () și glUseProgram () metode.

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

9. Desenați obiectul 3D

Cu umbrele și tampoanele gata, avem tot ce avem nevoie pentru a atrage torusul nostru. Adăugați o nouă metodă la Torus clasa numită a desena:

public void draw () // Codul de desen vine aici

Într-o etapă anterioară, în cadrul shader-ului de vârf, am definit a poziţie variabilă pentru a primi date de poziție de vârf din codul Java. Acum este momentul să trimiteți datele despre poziția vârfului. Pentru a face acest lucru, trebuie mai întâi să luăm mâna poziţie variabilă în codul nostru Java folosind glGetAttribLocation () metodă. În plus, mânerul trebuie să fie activat utilizând glEnableVertexAttribArray () metodă.

În consecință, adăugați următorul cod în interiorul a desena() metodă:

int position = GLES20.glGetAttribLocație (program, "poziție"); GLES20.glEnableVertexAttribArray (poziție);

Pentru a indica punctul poziţie mâner la tamponul nostru de vârfuri, trebuie să folosim glVertexAttribPointer () metodă. În plus față de tamponul de niveluri în sine, metoda așteaptă numărul de coordonate pe vârf, tipul de coordonate și offsetul octet pentru fiecare vârf. Pentru că avem trei coordonate pe fiecare vertex și fiecare coordonată este a pluti, offsetul byte trebuie să fie 3 * 4.

GLES20.glVertexAttribPointer (poziția 3, GLES20.GL_FLOAT, false, 3 * 4, verticesBuffer);

Shader-ul nostru de vârfuri așteaptă de asemenea o matrice de vizualizare-proiecție. Deși o astfel de matrice nu este întotdeauna necesară, folosirea uneia vă permite să aveți un control mai bun asupra modului în care obiectul 3D este redat.

O matrice de vizualizare-proiecție este pur și simplu produsul matricelor de vizualizare și de proiecție. O matrice de vizualizare vă permite să specificați locațiile aparatului foto și punctul pe care îl privește. O matrice de proiecție, pe de altă parte, vă permite nu numai să cartografiați sistemul de coordonate pătrat al OpenGL ES pe ecranul dreptunghiular al unui dispozitiv Android, ci și să specificați avioanele apropiate și depărtate ale frustumului vizual.

Pentru a crea matricele, puteți crea doar trei pluti matrice de dimensiuni 16:

float [] proiecțieMatrix = float nou [16]; float [] vizualizareMatrix = float nou [16]; float [] productMatrix = float nou [16];

Pentru a inițializa matricea de proiecție, puteți utiliza frustumM () metodă a Matrice clasă. Se așteaptă amplasarea coloanelor din stânga, din dreapta, de jos, de sus, de lângă și de la distanță. Deoarece panza noastră este deja un pătrat, puteți utiliza valorile -1 și 1 pentru stânga și dreapta și pentru planurile inferioare și superioare ale clipului. Pentru planurile de clipuri apropiate și de departe, nu ezitați să experimentați diferite valori.

Matrix.frustumM (proiecțieMatrix, 0, -1, 1, -1, 1, 2, 9);

Pentru a inițializa matricea de vizualizare, utilizați setLookAtM () metodă. Se așteaptă pozițiile camerei și punctul la care se uită. Sunteți din nou liberi să experimentați diferite valori.

Matrix.setLookAtM (vizualizațiMatrix, 0, 0, 3, -4, 0, 0, 0, 0, 1, 0);

În cele din urmă, pentru a calcula matricea produsului, utilizați multiplyMM () metodă.

Matrix.multiplyMM (productMatrix, 0, proiecțieMatrix, 0, viewMatrix, 0);

Pentru a trece matricea produsului la shaderul de vârfuri, trebuie să-i dați un mâner matrice variabilă folosind glGetUniformLocation () metodă. Odată ce aveți mânerul, îl puteți îndrepta spre matricea produsului folosind glUniformMatrix () metodă.

int matrix = GLES20.glGetUniformLocation (program, "matrice"); GLES20.glUniformMatrix4fv (matrice, 1, fals, productMatrix, 0);

Probabil că ați observat că nu am folosit tamponul pentru fețe. Aceasta înseamnă că încă nu i-am spus OpenGL ES cum să conectăm vârfurile pentru a forma triunghiuri, care vor servi ca fețe ale obiectului nostru 3D.

glDrawElements () vă permite să utilizați tamponul pentru fețe pentru a crea triunghiuri. Ca argumentele sale, se așteaptă numărul total de indici de vârfuri, tipul fiecărui index și tamponul pentru fețe.

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

În fine, nu uitați să dezactivați atribut handler pe care l-ați activat mai devreme pentru a transmite datele de vârf la shaderul de vârfuri.

GLES20.glDisableVertexAttribArray (poziție);

10. Creați un Renderer

Al nostru GLSurfaceView widgetul are nevoie de a GLSurfaceView.Renderer obiect pentru a putea face grafica 3D. Puteți utiliza funcția setRenderer () pentru a asocia un renderer cu acesta.

mySurfaceView.setRenderer (noul GLSurfaceView.Renderer () // Mai mult cod merge aici);

În interiorul onSurfaceCreated () metoda de redare, trebuie să specificați cât de des trebuie să fie redată grafica 3D. Pentru moment, să redăm doar când graficul 3D se schimbă. Pentru a face acest lucru, treceți RENDERMODE_WHEN_DIRTY constanta la setRenderMode () metodă. În plus, inițializați o nouă instanță a Torus obiect.

@Override publice void peSurfaceCreated (GL10 gl10, EGLConfig eglConfig) mySurfaceView.setRenderMode (GLSurfaceView.RENDERMODE_WHEN_DIRTY); torus = Torus nou (getApplicationContext ()); 

În interiorul onSurfaceChanged () metoda de redare, puteți defini lățimea și înălțimea portului de vizualizare folosind glViewport () metodă.

@Override publice void onSurfaceChanged (GL10 gl10, lățime int, int înălțime) GLES20.glViewport (0,0, lățime, înălțime); 

În interiorul onDrawFrame () metoda de redare, adăugați un apel la a desena() metodă a Torus clasa pentru a trage de fapt torus.

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

În acest moment, puteți rula aplicația dvs. pentru a vedea torusul portocaliu.

Concluzie

Acum știți cum să utilizați OpenGL ES în aplicațiile Android. În acest tutorial, ați învățat de asemenea cum să analizați un fișier OBJ Wavefront și să extrageți vertex și să vă confruntați cu date. Vă sugerez să generați mai multe obiecte 3D folosind Blender și să încercați să le redați în aplicație.

Deși ne-am concentrat doar pe OpenGL ES 2.0, înțelegem că OpenGL ES 3.x este compatibil cu OpenGL ES 2.0. Aceasta înseamnă că, dacă preferați să utilizați OpenGL ES 3.x în aplicația dvs., puteți înlocui pur și simplu GLES20 clasa cu GLES30 sau GLES31 clase.

Pentru a afla mai multe despre OpenGL ES, vă puteți referi la paginile sale de referință. Și pentru a afla mai multe despre dezvoltarea aplicațiilor Android, asigurați-vă că ați verificat câteva dintre celelalte tutoriale de la Envato Tuts+!

  • Cum să începeți cu Kitul de dezvoltare nativă Android

    Odată cu lansarea aplicației Android Studio 2.2, dezvoltarea aplicațiilor Android care conțin codul C ++ a devenit mai ușoară ca niciodată. În acest tutorial, vă voi arăta cum ...
    Ashraff Hathibelagal
    Android
  • Lucruri Android: intrări / ieșiri periferice

    Android Things are o capacitate unică de a se conecta cu ușurință la componentele electronice externe, cu API-ul Peripheral și suportul pentru dispozitive încorporate. În acest articol…
    Paul Trebilcox-Ruiz
    Android SDK
  • Cum se asigură o aplicație Android

    În acest articol, vom examina câteva dintre cele mai bune practici pe care le puteți urma pentru a construi o aplicație sigură pentru Android. Aceasta înseamnă o aplicație care nu scapă ...
    Ashraff Hathibelagal
    Android
  • Codarea unei aplicații Android cu Flutter și Dart

    Flutter-ul Google este un cadru de dezvoltare a aplicațiilor care utilizează limbajul de programare Dart. În acest tutorial, vă voi prezenta la elementele de bază ale ...
    Ashraff Hathibelagal
    Android

Cod