Come usare Android Media Effects con OpenGL ES

Il framework di effetti multimediali di Android consente agli sviluppatori di applicare facilmente molti effetti visivi impressionanti a foto e video. Poiché il framework utilizza la GPU per eseguire tutte le operazioni di elaborazione delle immagini, può accettare solo trame OpenGL come input. In questo tutorial, imparerai come usare OpenGL ES 2.0 per convertire una risorsa estraibile in una texture e poi usare la struttura per applicare vari effetti ad essa.

Prerequisiti

Per seguire questo tutorial, devi avere:

  • un IDE che supporta lo sviluppo di applicazioni Android. Se non ne hai uno, scarica l'ultima versione di Android Studio dal sito web degli sviluppatori Android.
  • un dispositivo che esegue Android 4.0+ e ha una GPU che supporta OpenGL ES 2.0.
  • una conoscenza di base di OpenGL.

1. Impostazione dell'ambiente OpenGL ES

Passaggio 1: creare a GLSurfaceView

Per visualizzare la grafica OpenGL nella tua app, devi usare a GLSurfaceView oggetto. Come qualsiasi altro vista, puoi aggiungerlo a un Attività o Frammento definendolo in un file XML di layout o creando un'istanza di esso nel codice.

In questo tutorial, avrai a GLSurfaceView oggetto come unico vista nel tuo Attività. Pertanto, la sua creazione nel codice è più semplice. Una volta creato, passalo al setContentView metodo in modo che riempia l'intero schermo. Il tuo Attività'S onCreate il metodo dovrebbe assomigliare a questo:

protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); GLSurfaceView view = new GLSurfaceView (questo); setContentView (vista); 

Poiché il framework degli effetti multimediali supporta solo OpenGL ES 2.0 o versioni successive, passa il valore 2 al setEGLContextClientVersion metodo.

view.setEGLContextClientVersion (2);

Per assicurarsi che il GLSurfaceView restituisce il suo contenuto solo quando necessario, passa il valore RENDERMODE_WHEN_DIRTY al setRenderMode metodo.

view.setRenderMode (GLSurfaceView.RENDERMODE_WHEN_DIRTY);

Passaggio 2: crea un renderer

UN GLSurfaceView.Renderer è responsabile per il disegno dei contenuti del GLSurfaceView.

Crea una nuova classe che implementa il GLSurfaceView.Renderer interfaccia. Ho intenzione di chiamare questa classe EffectsRenderer. Dopo aver aggiunto un costruttore e aver annullato tutti i metodi dell'interfaccia, la classe dovrebbe apparire così:

public class EffectsRenderer implementa GLSurfaceView.Renderer public EffectsRenderer (Context context) super ();  @Override public void onSurfaceCreated (GL10 gl, EGLConfig config)  @Override public void onSurfaceChanged (GL10 gl, int width, int height)  @Override public void onDrawFrame (GL10 gl) 

Torna al tuo Attività e chiama il setRenderer metodo in modo che il GLSurfaceView usa il renderizzatore personalizzato.

view.setRenderer (new EffectsRenderer (this));

Passaggio 3: modifica il manifest

Se hai intenzione di pubblicare la tua app su Google Play, aggiungi quanto segue a AndroidManifest.xml:

Questo assicura che la tua app possa essere installata solo su dispositivi che supportano OpenGL ES 2.0. L'ambiente OpenGL è ora pronto.

2. Creazione di un piano OpenGL

Passaggio 1: definire i vertici

Il GLSurfaceView non è possibile visualizzare una foto direttamente. La foto deve essere convertita in una texture e applicata prima a una forma OpenGL. In questo tutorial, creeremo un piano 2D con quattro vertici. Per semplicità, facciamo un quadrato. Crea una nuova classe, Piazza, rappresentare il quadrato.

public class Square 

Il sistema di coordinate OpenGL predefinito ha la sua origine al centro. Di conseguenza, le coordinate dei quattro angoli della nostra piazza, di cui sono i lati due unità lungo, sarà:

  • angolo in basso a sinistra in (-1, -1)
  • angolo in basso a destra in (1, -1)
  • in alto a destra in (1, 1)
  • angolo in alto a sinistra in (-1, 1)

Tutti gli oggetti che disegniamo usando OpenGL dovrebbero essere fatti di triangoli. Per disegnare il quadrato, abbiamo bisogno di due triangoli con un bordo comune. Ciò significa che le coordinate dei triangoli saranno:

triangolo 1: (-1, -1), (1, -1) e (-1, 1)
triangle 2: (1, -1), (-1, 1) e (1, 1)

Creare un galleggiante array per rappresentare questi vertici.

vertici float privati ​​[] = -1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f,;

Per mappare la trama sul quadrato, è necessario specificare le coordinate dei vertici della trama. Le trame seguono un sistema di coordinate in cui il valore della coordinata y aumenta man mano che si sale. Crea un altro array per rappresentare i vertici della trama.

private float textureVertices [] = 0f, 1f, 1f, 1f, 0f, 0f, 1f, 0f;

Passaggio 2: creare oggetti buffer

Le matrici di coordinate devono essere convertite in buffer di byte prima che OpenGL possa usarle. Dichiariamo prima questi buffer.

private FloatBuffer verticesBuffer; private FloatBuffer textureBuffer;

Scrivi il codice per inizializzare questi buffer in un nuovo metodo chiamato initializeBuffers. Utilizzare il ByteBuffer.allocateDirect metodo per creare il buffer. Perché a galleggiante usi 4 byte, è necessario moltiplicare la dimensione degli array con il valore 4.

Quindi, usa ByteBuffer.nativeOrder per determinare l'ordine dei byte della piattaforma nativa sottostante e impostare l'ordine dei buffer su quel valore. Utilizzare il asFloatBuffer metodo per convertire il ByteBuffer istanza in a FloatBuffer. Dopo il FloatBuffer viene creato, usa il mettere metodo per caricare la matrice nel buffer. Infine, usa il posizione metodo per assicurarsi che il buffer sia letto dall'inizio.

Il contenuto del initializeBuffers il metodo dovrebbe assomigliare a questo:

private void initializeBuffers () ByteBuffer buff = ByteBuffer.allocateDirect (vertices.length * 4); buff.order (ByteOrder.nativeOrder ()); verticesBuffer = buff.asFloatBuffer (); verticesBuffer.put (vertici); verticesBuffer.position (0); buff = ByteBuffer.allocateDirect (textureVertices.length * 4); buff.order (ByteOrder.nativeOrder ()); textureBuffer = buff.asFloatBuffer (); textureBuffer.put (textureVertices); textureBuffer.position (0); 

Passaggio 3: crea shaders

È tempo di scrivere i tuoi stessi shader. Gli shader non sono altro che semplici programmi C gestiti dalla GPU per elaborare ogni singolo vertice. Per questo tutorial, devi creare due ombreggiatori, uno shader di vertici e uno shader di frammenti.

Il codice C per vertex shader è:

attributo vec4 aPosition; attributo vec2 aTexPosition; variando vec2 vTexPosition; void main () gl_Position = aPosition; vTexPosition = aTexPosition; ;

Il codice C per lo shader di frammenti è:

galleggiante mediocre di precisione; uniforme sampler2D uTexture; variando vec2 vTexPosition; void main () gl_FragColor = texture2D (uTexture, vTexPosition); ;

Se conosci già OpenGL, questo codice dovrebbe esserti familiare perché è comune su tutte le piattaforme. In caso contrario, per comprendere questi programmi è necessario fare riferimento alla documentazione OpenGL. Ecco una breve spiegazione per iniziare:

  • Il vertex shader è responsabile del disegno dei singoli vertici. una posizione è una variabile che sarà associata al FloatBuffer che contiene le coordinate dei vertici. allo stesso modo, aTexPosition è una variabile che sarà associata al FloatBuffer che contiene le coordinate della trama. GL_POSITION è una variabile OpenGL incorporata e rappresenta la posizione di ciascun vertice. Il vTexPosition è un variando variabile, il cui valore viene semplicemente passato allo shader del frammento.
  • In questo tutorial, il framment shader è responsabile della colorazione del quadrato. Raccoglie i colori dalla trama usando il Texture2D metodo e li assegna al frammento utilizzando una variabile incorporata denominata gl_FragColor.

Il codice shader deve essere rappresentato come Stringa oggetti nella classe.

private final String vertexShaderCode = "attributo vec4 aPosition;" + "attributo vec2 aTexPosition;" + "variando vec2 vTexPosition;" + "void main () " + "gl_Position = aPosition;" + "vTexPosition = aTexPosition;" + ""; private final String fragmentShaderCode = "precision mediump float;" + "uniforme sampler2D uTexture;" + "variando vec2 vTexPosition;" + "void main () " + "gl_FragColor = texture2D (uTexture, vTexPosition);" + "";

Passaggio 4: creare un programma

Crea un nuovo metodo chiamato initializeProgram per creare un programma OpenGL dopo aver compilato e collegato gli shader.

Uso glCreateShader per creare un oggetto shader e restituire un riferimento ad esso sotto forma di un int. Per creare un vertex shader, passa il valore GL_VERTEX_SHADER ad esso. Allo stesso modo, per creare uno shader di frammenti, passare il valore GL_FRAGMENT_SHADER ad esso. Prossimo uso glShaderSource associare il codice shader appropriato allo shader. Uso glCompileShader compilare il codice dello shader.

Dopo aver compilato entrambi gli shader, crea un nuovo programma usando glCreateProgram. Proprio come  glCreateShader, anche questo restituisce un int come riferimento al programma. Chiamata glAttachShader per allegare gli shader al programma. Infine, chiama glLinkProgram collegare il programma.

Il tuo metodo e le variabili associate dovrebbero assomigliare a questo:

private int vertexShader; private int fragmentShader; programma privato int; private void initializeProgram () vertexShader = GLES20.glCreateShader (GLES20.GL_VERTEX_SHADER); GLES20.glShaderSource (vertexShader, vertexShaderCode); GLES20.glCompileShader (Vertexshader); fragmentShader = GLES20.glCreateShader (GLES20.GL_FRAGMENT_SHADER); GLES20.glShaderSource (fragmentShader, fragmentShaderCode); GLES20.glCompileShader (fragmentShader); programma = GLES20.glCreateProgram (); GLES20.glAttachShader (programma, vertexShader); GLES20.glAttachShader (programma, fragmentShader); GLES20.glLinkProgram (programma);  

Potresti aver notato che i metodi OpenGL (i metodi con il prefisso gl) appartengono alla classe GLES20. Questo perché stiamo usando OpenGL ES 2.0. Se desideri utilizzare una versione più alta, dovrai utilizzare le classi GLES30 o GLES31.

Passaggio 5: Disegna il quadrato

Crea un nuovo metodo chiamato disegnare disegnare effettivamente il quadrato usando i vertici e gli shader definiti in precedenza.

Ecco cosa devi fare in questo metodo:

  1. Uso glBindFramebuffer creare un oggetto buffer frame con nome (spesso chiamato FBO).
  2. Uso glUseProgram per iniziare a utilizzare il programma che abbiamo appena collegato.
  3. Passa il valore GL_BLEND a glDisable per disabilitare la fusione dei colori durante il rendering.
  4. Uso glGetAttribLocation per ottenere un controllo sulle variabili una posizione e aTexPosition menzionato nel codice vertex shader.
  5. Uso glGetUniformLocation per ottenere una maniglia per la costante uTexture menzionato nel codice shader del frammento.
  6. Utilizzare il glVertexAttribPointer associare il una posizione e aTexPosition maniglie con il verticesBuffer e il textureBuffer rispettivamente.
  7. Uso glBindTexture legare la trama (passata come argomento al file disegnare metodo) allo shader del frammento.
  8. Cancella il contenuto del GLSurfaceView utilizzando glClear.
  9. Infine, usa il glDrawArrays metodo per disegnare effettivamente i due triangoli (e quindi il quadrato).

Il codice per il disegnare il metodo dovrebbe assomigliare a questo:

public void draw (int texture) GLES20.glBindFramebuffer (GLES20.GL_FRAMEBUFFER, 0); GLES20.glUseProgram (programma); GLES20.glDisable (GLES20.GL_BLEND); int positionHandle = GLES20.glGetAttribLocation (programma, "aPosition"); int textureHandle = GLES20.glGetUniformLocation (programma, "uTexture"); int texturePositionHandle = GLES20.glGetAttribLocation (programma, "aTexPosition"); GLES20.glVertexAttribPointer (texturePositionHandle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer); GLES20.glEnableVertexAttribArray (texturePositionHandle); GLES20.glActiveTexture (GLES20.GL_TEXTURE0); GLES20.glBindTexture (GLES20.GL_TEXTURE_2D, texture); GLES20.glUniform1i (textureHandle, 0); GLES20.glVertexAttribPointer (positionHandle, 2, GLES20.GL_FLOAT, false, 0, verticesBuffer); GLES20.glEnableVertexAttribArray (positionHandle); GLES20.glClear (GLES20.GL_COLOR_BUFFER_BIT); GLES20.glDrawArrays (GLES20.GL_TRIANGLE_STRIP, 0, 4);  

Aggiungi un costruttore alla classe per inizializzare i buffer e il programma al momento della creazione dell'oggetto.

public Square () initializeBuffers (); initializeProgram (); 

3. Rendering del piano e della trama OpenGL

Al momento, il nostro renderer non fa nulla. Abbiamo bisogno di cambiarlo in modo che possa rendere il piano che abbiamo creato nei passaggi precedenti.

Ma prima, creiamo un Bitmap. Aggiungi una foto al tuo progetto res / drawable cartella. Viene chiamato il file che sto usando forest.jpg. Utilizzare il BitmapFactory convertire la foto in a Bitmap oggetto. Inoltre, memorizzare le dimensioni del Bitmap oggetto in variabili separate.

Cambia il costruttore del EffectsRenderer classe in modo che abbia il seguente contenuto:

foto bitmap privata; private int photoWidth, photoHeight; public EffectsRenderer (Context context) super (); photo = BitmapFactory.decodeResource (context.getResources (), R.drawable.forest); photoWidth = photo.getWidth (); photoHeight = photo.getHeight (); 

Crea un nuovo metodo chiamato generateSquare per convertire la bitmap in una texture e inizializzare a Piazza oggetto. Avrai anche bisogno di una serie di numeri interi per conservare i riferimenti alle trame OpenGL. Uso glGenTextures inizializzare la matrice e glBindTexture attivare la texture all'indice 0.

Quindi, usa glTexParameteri per impostare varie proprietà che decidono come viene resa la texture:

  • Impostato GL_TEXTURE_MIN_FILTER (la funzione minifying) e il GL_TEXTURE_MAG_FILTER (la funzione di ingrandimento) a GL_LINEAR per assicurarti che la texture sia liscia, anche quando è allungata o ridotta.
  • Impostato GL_TEXTURE_WRAP_S e GL_TEXTURE_WRAP_T a GL_CLAMP_TO_EDGE in modo che la trama non venga mai ripetuta.

Infine, usa il texImage2D metodo per mappare il Bitmap alla trama L'implementazione del generateSquare il metodo dovrebbe assomigliare a questo:

int texture private [] = new int [2]; piazza privata; private void generateSquare () GLES20.glGenTextures (2, texture, 0); GLES20.glBindTexture (GLES20.GL_TEXTURE_2D, textures [0]); GLES20.glTexParameteri (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLUtils.texImage2D (GLES20.GL_TEXTURE_2D, 0, foto, 0); square = new Square (); 

Ogni volta che le dimensioni del GLSurfaceView cambiare il onSurfaceChanged metodo del Renderer è chiamato. Ecco dove devi chiamare glViewport per specificare le nuove dimensioni della vista. Inoltre, chiama glClearColor dipingere il GLSurfaceView nero. Quindi, chiama generateSquare per reinizializzare le trame e il piano.

@Override public void onSurfaceChanged (GL10 gl, int width, int height) GLES20.glViewport (0,0, larghezza, altezza); GLES20.glClearColor (0,0,0,1); generateSquare (); 

Infine, chiama il Piazza oggetto di disegnare metodo all'interno del onDrawFrame metodo del Renderer.

@Override public void onDrawFrame (GL10 gl) square.draw (textures [0]); 

Ora puoi eseguire la tua app e vedere la foto che hai scelto di essere resa come una texture OpenGL su un piano.

4. Utilizzo di Media Effects Framework

Il codice complesso che abbiamo scritto fino ad ora era solo un prerequisito per utilizzare il framework degli effetti multimediali. È giunto il momento di iniziare a utilizzare il framework stesso. Aggiungi i seguenti campi al tuo Renderer classe.

effetto EffectContext privatoContext; effetto effetto privato;

Inizializza il effectContext campo usando il EffectContext.createWithCurrentGlContext. È responsabile della gestione delle informazioni sugli effetti visivi all'interno di un contesto OpenGL. Per ottimizzare le prestazioni, questo dovrebbe essere chiamato una sola volta. Aggiungi il seguente codice all'inizio del tuo onDrawFrame metodo.

if (effectContext == null) effectContext = EffectContext.createWithCurrentGlContext (); 

Creare un effetto è molto semplice. Utilizzare il effectContext creare un EffectFactory e usa il EffectFactory creare un Effetto oggetto. Una volta un Effetto l'oggetto è disponibile, puoi chiamare applicare e passare un riferimento alla trama originale ad esso, nel nostro caso lo è texture [0], insieme a un riferimento a un oggetto texture vuoto, nel nostro caso lo è texture [1]. Dopo il applicare il metodo è chiamato, texture [1] conterrà il risultato del Effetto.

Ad esempio, per creare e applicare il in scala di grigi effetto, ecco il codice che devi scrivere:

private void grayScaleEffect () EffectFactory factory = effectContext.getFactory (); effect = factory.createEffect (EffectFactory.EFFECT_GRAYSCALE); effect.apply (textures [0], photoWidth, photoHeight, texture [1]); 

Chiama questo metodo onDrawFrame e passare texture [1] al Piazza oggetto di disegnare metodo. Il tuo onDrawFrame il metodo dovrebbe avere il seguente codice:

@Override public void onDrawFrame (GL10 gl) if (effectContext == null) effectContext = EffectContext.createWithCurrentGlContext ();  if (effect! = null) effect.release ();  grayScaleEffect (); square.draw (texture [1]); 

Il pubblicazione il metodo è utilizzato per liberare tutte le risorse detenute da un Effetto. Quando esegui l'app, dovresti vedere il seguente risultato:

Puoi usare lo stesso codice per applicare altri effetti. Ad esempio, ecco il codice per applicare il documentario effetto:

private void documentaryEffect () EffectFactory factory = effectContext.getFactory (); effect = factory.createEffect (EffectFactory.EFFECT_DOCUMENTARY); effect.apply (textures [0], photoWidth, photoHeight, texture [1]); 

Il risultato è simile a questo:

Alcuni effetti prendono parametri. Ad esempio, l'effetto di regolazione della luminosità ha a luminosità parametro che accetta a galleggiante valore. Puoi usare setParameter per cambiare il valore di qualsiasi parametro. Il seguente codice mostra come usarlo:

private void brightnessEffect () EffectFactory factory = effectContext.getFactory (); effect = factory.createEffect (EffectFactory.EFFECT_BRIGHTNESS); effect.setParameter ("brightness", 2f); effect.apply (textures [0], photoWidth, photoHeight, texture [1]); 

L'effetto renderà la tua app rendere il seguente risultato:

Conclusione

In questo tutorial, hai appreso come utilizzare Media Effects Framework per applicare vari effetti alle tue foto. Mentre lo facevi, hai anche imparato a disegnare un piano usando OpenGL ES 2.0 e ad applicare varie trame.

Il framework può essere applicato sia a foto che a video. In caso di video, devi semplicemente applicare l'effetto ai singoli fotogrammi del video in onDrawFrame metodo.

Hai già visto tre effetti in questo tutorial e il framework ha dozzine di cose in più per farti sperimentare. Per saperne di più su di loro, fare riferimento al sito Web dello sviluppatore Android.