Quasi tutti i telefoni Android disponibili sul mercato oggi dispongono di un'unità di elaborazione grafica o di una GPU in breve. Come suggerisce il nome, questa è un'unità hardware dedicata alla gestione dei calcoli che sono solitamente correlati alla grafica 3D. Come sviluppatore di app, puoi utilizzare la GPU per creare grafica e animazioni complesse che funzionano a frame rate molto alti.
Al momento sono disponibili due diverse API che puoi utilizzare per interagire con la GPU di un dispositivo Android: Vulkan e OpenGL ES. Mentre Vulkan è disponibile solo su dispositivi con Android 7.0 o versioni successive, OpenGL ES è supportato da tutte le versioni di Android.
In questo tutorial, ti aiuterò a iniziare a utilizzare OpenGL ES 2.0 nelle app Android.
Per poter seguire questo tutorial, avrai bisogno di:
OpenGL, che è l'abbreviazione di Open Graphics Library, è un'API indipendente dalla piattaforma che consente di creare grafica 3D con accelerazione hardware. OpenGL ES, abbreviazione di OpenGL per Embedded Systems, è un sottoinsieme dell'API.
OpenGL ES è un'API di livello molto basso. In altre parole, non offre alcun metodo che ti permetta di creare o manipolare rapidamente oggetti 3D. Invece, mentre si lavora con esso, ci si aspetta che tu gestisca manualmente compiti come la creazione dei singoli vertici e facce di oggetti 3D, il calcolo di varie trasformazioni 3D e la creazione di diversi tipi di shader.
Vale anche la pena ricordare che Android SDK e NDK insieme consentono di scrivere codice relativo a OpenGL ES in Java e C.
Poiché le API OpenGL ES fanno parte del framework Android, non è necessario aggiungere alcuna dipendenza al progetto per poterle utilizzare. In questo tutorial, tuttavia, utilizzeremo la libreria IO di Apache Commons per leggere il contenuto di alcuni file di testo. Pertanto, aggiungilo come a compilare
dipendenza dal modulo dell'app build.gradle file:
compilare 'commons-io: commons-io: 2.5'
Inoltre, per impedire agli utenti di Google Play che non dispongono di dispositivi che supportano la versione OpenGL ES, è necessario installare l'app, aggiungere quanto segue
tag al file manifest del tuo progetto:
Il framework Android offre due widget che possono fungere da canvas per la tua grafica 3D: GLSurfaceView
e TextureView
. La maggior parte degli sviluppatori preferisce l'utilizzo GLSurfaceView
, e scegliere TextureView
solo quando intendono sovrapporre la loro grafica 3D a un'altra vista
widget di. Per l'app che creeremo in questo tutorial, GLSurfaceView
sarà sufficiente.
Aggiungere un GLSurfaceView
il widget per il file di layout non è diverso dall'aggiunta di altri widget.
Nota che abbiamo reso la larghezza del nostro widget uguale alla sua altezza. Ciò è importante perché il sistema di coordinate OpenGL ES è un quadrato. Se è necessario utilizzare una tela rettangolare, ricordarsi di includere le proporzioni durante il calcolo della matrice di proiezione. Imparerai cosa è una matrice di proiezione in un secondo momento.
Inizializzazione a GLSurfaceView
widget all'interno di Attività
la classe è semplice come chiamare il findViewById ()
metodo e passando il suo id ad esso.
mySurfaceView = (GLSurfaceView) findViewById (R.id.my_surface_view);
Inoltre, dobbiamo chiamare il setEGLContextClientVersion ()
metodo per specificare esplicitamente la versione di OpenGL ES che useremo per disegnare all'interno del widget.
mySurfaceView.setEGLContextClientVersion (2);
Sebbene sia possibile creare oggetti 3D in Java codificando manualmente le coordinate X, Y e Z di tutti i loro vertici, farlo è molto macchinoso. L'utilizzo di strumenti di modellazione 3D è invece molto più semplice. Blender è uno di questi strumenti. È open source, potente e molto facile da imparare.
Accendi Blender e premi X per eliminare il cubo predefinito. Quindi, premere Shift-A e selezionare Maglia> Torus. Ora abbiamo un oggetto 3D abbastanza complesso composto da 576 vertici.
Per poter utilizzare il toro nella nostra app per Android, dobbiamo esportarlo come file OBJ di Wavefront. Pertanto, andare a File> Esporta> Wavefront (.obj). Nella schermata successiva, dai un nome al file OBJ, assicurati che il file Triangulate Faces e Mantieni ordine dei vertici le opzioni sono selezionate e premere il tasto Esporta OBJ pulsante.
Ora puoi chiudere Blender e spostare il file OBJ nei tuoi progetti di Android Studio risorse cartella.
Se non l'hai già notato, il file OBJ che abbiamo creato nel passaggio precedente è un file di testo, che può essere aperto utilizzando qualsiasi editor di testo.
Nel file, ogni riga che inizia con una "v" rappresenta un singolo vertice. Allo stesso modo, ogni riga che inizia con una "f" rappresenta una singola faccia triangolare. Mentre ogni linea di vertici contiene le coordinate X, Y e Z di un vertice, ciascuna linea di fronte contiene gli indici di tre vertici, che insieme formano una faccia. Questo è tutto ciò che devi sapere per analizzare un file OBJ.
Prima di iniziare, creare una nuova classe Java chiamata Torus e aggiungi due Elenco
oggetti, uno per i vertici e uno per i volti, come le sue variabili membro.
Torus di classe pubblica Lista privataverticesList; Elenco privato facesList; public Torus (Context context) verticesList = new ArrayList <> (); facesList = new ArrayList <> (); // Più codice va qui
Il modo più semplice per leggere tutte le singole righe del file OBJ è utilizzare il comando Scanner
classe e il suo nextLine ()
metodo. Durante il looping delle linee e il popolamento dei due elenchi, è possibile utilizzare il comando Stringa
La classe di inizia con()
metodo per verificare se la linea corrente inizia con una "v" o una "f".
// Apre il file OBJ con Scanner Scanner scanner = new Scanner (context.getAssets (). Open ("torus.obj")); // Passa in rassegna tutte le sue linee mentre (scanner.hasNextLine ()) String line = scanner.nextLine (); if (line.startsWith ("v")) // Aggiungi la linea vertice alla lista dei vertici verticesList.add (linea); else if (line.startsWith ("f")) // Aggiungi la linea del viso alla lista delle facce facesList.add (linea); // Chiudi lo scanner scanner.close ();
Non è possibile passare direttamente gli elenchi di vertici e facce ai metodi disponibili nell'API OpenGL ES. È necessario prima convertirli in oggetti buffer. Per memorizzare i dati delle coordinate del vertice, avremo bisogno di a FloatBuffer
oggetto. Per i dati del volto, che consiste semplicemente di indici dei vertici, a ShortBuffer
l'oggetto sarà sufficiente.
Di conseguenza, aggiungere le seguenti variabili membro a Torus
classe:
private FloatBuffer verticesBuffer; ShortBuffer privato facesBuffer;
Per inizializzare i buffer, dobbiamo prima creare a ByteBuffer
oggetto usando il allocateDirect ()
metodo. Per il buffer dei vertici, allocare quattro byte per ogni coordinata, con le coordinate come numeri in virgola mobile. Una volta il ByteBuffer
oggetto è stato creato, puoi convertirlo in a FloatBuffer
chiamando il suo asFloatBuffer ()
metodo.
// Crea buffer per vertici ByteBuffer buffer1 = ByteBuffer.allocateDirect (verticesList.size () * 3 * 4); buffer1.order (ByteOrder.nativeOrder ()); verticesBuffer = buffer1.asFloatBuffer ();
Allo stesso modo, creane un altro ByteBuffer
oggetto per il buffer delle facce. Questa volta, allocare due byte per ogni indice dei vertici perché gli indici sono corto senza firma
letterali. Inoltre, assicurati di utilizzare il asShortBuffer ()
metodo per convertire il ByteBuffer
oggetto a ShortBuffer
.
// Crea buffer per facce ByteBuffer buffer2 = ByteBuffer.allocateDirect (facesList.size () * 3 * 2); buffer2.order (ByteOrder.nativeOrder ()); facesBuffer = buffer2.asShortBuffer ();
Il popolamento del buffer dei vertici implica il looping dei contenuti di verticesList
, estraendo le coordinate X, Y e Z da ciascun elemento e chiamando il tasto mettere()
metodo per mettere i dati all'interno del buffer. Perché verticesList
contiene solo stringhe, dobbiamo usare il parseFloat ()
per convertire le coordinate da stringhe a galleggiante
valori.
for (String vertice: verticesList) String coords [] = vertex.split (""); // Dividi per spazio float x = Float.parseFloat (coords [1]); float y = Float.parseFloat (coords [2]); float z = Float.parseFloat (coords [3]); verticesBuffer.put (x); verticesBuffer.put (y); verticesBuffer.put (z); verticesBuffer.position (0);
Nota che nel codice sopra abbiamo usato il posizione()
metodo per ripristinare la posizione del buffer.
Il popolamento del buffer delle facce è leggermente diverso. Devi usare il parseShort ()
metodo per convertire ciascun indice dei vertici in un valore breve. Inoltre, poiché gli indici partono da uno anziché da zero, è necessario ricordare di sottrarre uno da essi prima di inserirli nel buffer.
for (String face: facesList) String vertexIndices [] = face.split (""); short vertex1 = Short.parseShort (vertexIndices [1]); short vertex2 = Short.parseShort (vertexIndices [2]); short vertex3 = Short.parseShort (vertexIndices [3]); facesBuffer.put ((short) (vertex1 - 1)); facesBuffer.put ((short) (vertex2 - 1)); facesBuffer.put ((short) (vertex3 - 1)); facesBuffer.position (0);
Per essere in grado di rendere il nostro oggetto 3D, dobbiamo creare uno shader di vertici e uno shader di frammenti per esso. Per ora, puoi pensare a uno shader come un programma molto semplice scritto in un linguaggio simile a C chiamato OpenGL Shading Language o GLSL in breve.
Un vertex shader, come avrai intuito, è responsabile della gestione dei vertici di un oggetto 3D. Un framment shader, chiamato anche pixel shader, è responsabile della colorazione dei pixel dell'oggetto 3D.
Crea un nuovo file chiamato vertex_shader.txt all'interno del tuo progetto res / raw cartella.
Un vertex shader deve avere un attributo
variabile globale al suo interno per ricevere i dati di posizione dei vertici dal proprio codice Java. Inoltre, aggiungere un uniforme
variabile globale per ricevere una matrice di proiezione della vista dal codice Java.
Dentro il principale()
funzione del vertex shader, è necessario impostare il valore di GL_POSITION
, una variabile integrata GLSL che decide la posizione finale del vertice. Per ora, puoi semplicemente impostare il suo valore sul prodotto di uniforme
e attributo
variabili globali.
Di conseguenza, aggiungi il seguente codice al file:
attributo posizione vec4; matrice mat4 uniforme; void main () gl_Position = matrix * position;
Crea un nuovo file chiamato fragment_shader.txt all'interno del tuo progetto res / raw cartella.
Per mantenere questo tutorial breve, ora creeremo uno shader di frammenti molto minimalista che assegna semplicemente il colore arancione a tutti i pixel. Per assegnare un colore a un pixel, all'interno di principale()
funzione di un framment shader, puoi usare il gl_FragColor
variabile integrata.
galleggiante mediocre di precisione; void main () gl_FragColor = vec4 (1, 0.5, 0, 1.0);
Nel codice precedente, la prima riga che specifica la precisione dei numeri in virgola mobile è importante perché uno shader di frammenti non ha alcuna precisione predefinita per loro.
Indietro nel Torus
classe, ora devi aggiungere codice per compilare i due shader che hai creato. Prima di farlo, tuttavia, è necessario convertirli da risorse non elaborate a stringhe. Il IOUtils
la classe, che fa parte della libreria IO di Apache Commons, ha a accordare()
metodo per fare proprio questo. Il seguente codice mostra come usarlo:
// Converti vertex_shader.txt in una stringa InputStream vertexShaderStream = context.getResources (). OpenRawResource (R.raw.vertex_shader); String vertexShaderCode = IOUtils.toString (vertexShaderStream, Charset.defaultCharset ()); vertexShaderStream.close (); // Converti fragment_shader.txt in una stringa InputStream fragmentShaderStream = context.getResources (). OpenRawResource (R.raw.fragment_shader); String fragmentShaderCode = IOUtils.toString (fragmentShaderStream, Charset.defaultCharset ()); fragmentShaderStream.close ();
Il codice degli shader deve essere aggiunto agli oggetti shader di OpenGL ES. Per creare un nuovo oggetto shader, usa il glCreateShader ()
metodo del GLES20
classe. A seconda del tipo di oggetto shader che vuoi creare, puoi passare GL_VERTEX_SHADER
o GL_FRAGMENT_SHADER
ad esso. Il metodo restituisce un numero intero che funge da riferimento all'oggetto shader. Un oggetto shader appena creato non contiene alcun codice. Per aggiungere il codice shader all'oggetto shader, è necessario utilizzare glShaderSource ()
metodo.
Il codice seguente crea oggetti shader sia per il vertex shader che per il framment 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);
Ora possiamo passare gli oggetti shader al glCompileShader ()
metodo per compilare il codice che contengono.
GLES20.glCompileShader (Vertexshader); GLES20.glCompileShader (fragmentShader);
Durante il rendering di un oggetto 3D, non si usano direttamente gli shader. Invece, li si collega a un programma e si utilizza il programma. Pertanto, aggiungere una variabile membro al Torus
classe per memorizzare un riferimento a un programma OpenGL ES.
programma privato int;
Per creare un nuovo programma, usa il glCreateProgram ()
metodo. Per attaccare gli oggetti shader di vertice e di frammento, usa il glAttachShader ()
metodo.
programma = GLES20.glCreateProgram (); GLES20.glAttachShader (programma, vertexShader); GLES20.glAttachShader (programma, fragmentShader);
A questo punto, puoi collegare il programma e iniziare a usarlo. Per fare ciò, utilizzare il glLinkProgram ()
e glUseProgram ()
metodi.
GLES20.glLinkProgram (programma); GLES20.glUseProgram (programma);
Con gli shader ei buffer pronti, abbiamo tutto ciò che serve per disegnare il nostro toro. Aggiungi un nuovo metodo al Torus
classe chiamata disegnare:
public void draw () // Il codice dell'illustrazione va qui
In una fase precedente, all'interno del vertex shader, abbiamo definito a posizione
variabile per ricevere i dati di posizione dei vertici dal codice Java. È giunto il momento di inviare i dati di posizione dei vertici ad esso. Per fare ciò, dobbiamo prima ottenere un handle per il posizione
variabile nel nostro codice Java usando il glGetAttribLocation ()
metodo. Inoltre, l'handle deve essere abilitato utilizzando il glEnableVertexAttribArray ()
metodo.
Di conseguenza, aggiungi il seguente codice all'interno di disegnare()
metodo:
int position = GLES20.glGetAttribLocation (programma, "posizione"); GLES20.glEnableVertexAttribArray (posizione);
Puntare il posizione
gestire il nostro buffer di vertici, dobbiamo usare il glVertexAttribPointer ()
metodo. Oltre al buffer dei vertici stesso, il metodo prevede il numero di coordinate per vertice, il tipo di coordinate e l'offset di byte per ciascun vertice. Perché abbiamo tre coordinate per vertice e ogni coordinata è a galleggiante
, l'offset del byte deve essere 3 * 4
.
GLES20.glVertexAttribPointer (posizione, 3, GLES20.GL_FLOAT, false, 3 * 4, verticiBuffer);
Il nostro vertex shader si aspetta anche una matrice di proiezione della vista. Sebbene tale matrice non sia sempre necessaria, l'utilizzo di una ti consente di avere un controllo migliore su come viene eseguito il rendering del tuo oggetto 3D.
Una matrice di proiezione della vista è semplicemente il prodotto delle matrici di visualizzazione e di proiezione. Una matrice di visualizzazione consente di specificare le posizioni della fotocamera e il punto che sta guardando. Una matrice di proiezione, d'altra parte, ti consente non solo di mappare il sistema di coordinate quadrate di OpenGL ES sullo schermo rettangolare di un dispositivo Android, ma anche di specificare i piani vicini e lontani del frustum di visualizzazione.
Per creare le matrici, puoi semplicemente crearne tre galleggiante
matrici di dimensioni 16
:
float [] projectionMatrix = new float [16]; float [] viewMatrix = new float [16]; float [] productMatrix = new float [16];
Per inizializzare la matrice di proiezione, è possibile utilizzare il frustumM ()
metodo del Matrice
classe. Si aspetta la posizione dei piani sinistro, destro, inferiore, superiore, vicino e lontano. Poiché la nostra area di disegno è già un quadrato, puoi utilizzare i valori -1
e 1
per i piani sinistro e destro e inferiore e superiore. Per i piani di clip vicini e lontani, sentiti libero di sperimentare con valori diversi.
Matrix.frustumM (projectionMatrix, 0, -1, 1, -1, 1, 2, 9);
Per inizializzare la matrice di visualizzazione, utilizzare il setLookAtM ()
metodo. Si aspetta le posizioni della telecamera e il punto che sta guardando. Sei di nuovo libero di sperimentare con valori diversi.
Matrix.setLookAtM (viewMatrix, 0, 0, 3, -4, 0, 0, 0, 0, 1, 0);
Infine, per calcolare la matrice del prodotto, utilizzare il multiplyMM ()
metodo.
Matrix.multiplyMM (productMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
Per passare la matrice del prodotto al vertex shader, devi ottenere un handle per il suo matrice
variabile usando il glGetUniformLocation ()
metodo. Una volta ottenuto l'handle, è possibile puntarlo sulla matrice del prodotto utilizzando il glUniformMatrix ()
metodo.
int matrix = GLES20.glGetUniformLocation (programma, "matrice"); GLES20.glUniformMatrix4fv (matrice, 1, false, productMatrix, 0);
Devi aver notato che non abbiamo ancora usato il buffer delle facce. Ciò significa che non abbiamo ancora detto a OpenGL ES come collegare i vertici per formare triangoli, che serviranno come facce del nostro oggetto 3D.
Il glDrawElements ()
metodo consente di utilizzare il buffer facce per creare triangoli. Come argomenti, si aspetta il numero totale di indici dei vertici, il tipo di ciascun indice e il buffer delle facce.
GLES20.glDrawElements (GLES20.GL_TRIANGLES, facesList.size () * 3, GLES20.GL_UNSIGNED_SHORT, facesBuffer);
Infine, ricorda di disabilitare il attributo
gestore abilitato in precedenza per passare i dati del vertice al vertex shader.
GLES20.glDisableVertexAttribArray (posizione);
Nostro GLSurfaceView
il widget ha bisogno di a GLSurfaceView.Renderer
oggetto di essere in grado di rendere grafica 3D. Puoi usare il setRenderer ()
associare un renderer con esso.
mySurfaceView.setRenderer (new GLSurfaceView.Renderer () // Più codice va qui);
Dentro il onSurfaceCreated ()
metodo del renderer, è necessario specificare la frequenza di rendering della grafica 3D. Per ora, rendiamo solo quando l'immagine 3D cambia. Per fare ciò, passa il RENDERMODE_WHEN_DIRTY
costante al setRenderMode ()
metodo. Inoltre, inizializza una nuova istanza di Torus
oggetto.
@Override public void onSurfaceCreated (GL10 gl10, EGLConfig eglConfig) mySurfaceView.setRenderMode (GLSurfaceView.RENDERMODE_WHEN_DIRTY); torus = new Torus (getApplicationContext ());
Dentro il onSurfaceChanged ()
metodo del renderer, puoi definire la larghezza e l'altezza della tua vista usando il glViewport ()
metodo.
@Override public void onSurfaceChanged (GL10 gl10, int width, int height) GLES20.glViewport (0,0, larghezza, altezza);
Dentro il onDrawFrame ()
metodo del renderer, aggiungere una chiamata al disegnare()
metodo del Torus
classe per disegnare effettivamente il toro.
@Override public void onDrawFrame (GL10 gl10) torus.draw ();
A questo punto, puoi eseguire la tua app per vedere il toro arancione.
Ora sai come usare OpenGL ES nelle app Android. In questo tutorial, hai anche imparato come analizzare un file OBJ di Wavefront ed estrarre i dati di vertice e di faccia da esso. Ti suggerisco di generare altri oggetti 3D usando Blender e provare a renderli nell'app.
Sebbene ci concentriamo solo su OpenGL ES 2.0, comprendiamo che OpenGL ES 3.x è retrocompatibile con OpenGL ES 2.0. Ciò significa che se preferisci utilizzare OpenGL ES 3.x nella tua app, puoi semplicemente sostituire il file GLES20
classe con il GLES30
o GLES31
classi.
Per saperne di più su OpenGL ES, è possibile fare riferimento alle sue pagine di riferimento. E per saperne di più sullo sviluppo di app per Android, assicurati di dare un'occhiata ad alcuni dei nostri tutorial qui su Envato Tuts+!