Cómo utilizar los efectos de Android Media con OpenGL ES
() translation by (you can also view the original English article)
El framework Media Effects de Android permite a los desarrolladores aplicar fácilmente muchos efectos visuales impresionantes a las fotos y videos. Como el framework usa la GPU para realizar todas sus operaciones de procesamiento de imágenes, solo puede aceptar texturas OpenGL como su entrada. En este tutorial, aprenderás cómo usar OpenGL ES 2.0 para convertir un recurso dibujable en una textura y luego usar el framework para aplicarle varios efectos.
Requisitos previos
Para seguir este tutorial, debes tener:
- un IDE que admita el desarrollo de aplicaciones de Android. Si no tienes uno, obtén la última versión de Android Studio del sitio web del desarrollador de Android.
- un dispositivo que ejecute Android 4.0+ y tiene una GPU que admite OpenGL ES 2.0.
- una comprensión básica de OpenGL.
1. Configuración del entorno de OpenGL ES
Paso 1: Crea un GLSurfaceView
Para mostrar los gráficos OpenGL en tu aplicación, debes usar un objeto GLSurfaceView
. Al igual que cualquier otra View
, puedes agregarla a una Activity
o Fragment
definiéndola en un archivo XML de diseño o creando una instancia de este en el código.
En este tutorial, vas a tener un objeto GLSurfaceView
como la única view
en tu activity
. Por lo tanto, crearlo en código es más simple. Una vez creado, pásalo al método setContentView
para que llene toda la pantalla. El método onCreate
de tu Activity
debería verse así:
1 |
protected void onCreate(Bundle savedInstanceState) { |
2 |
super.onCreate(savedInstanceState); |
3 |
|
4 |
GLSurfaceView view = new GLSurfaceView(this); |
5 |
setContentView(view); |
6 |
}
|
Como el framework de efectos de medios solo admite OpenGL ES 2.0 o superior, pasa el valor 2 al método setEGLContextClientVersion
.
1 |
view.setEGLContextClientVersion(2); |
Para asegurarte de que GLSurfaceView
muestra su contenido solo cuando es necesario, pasa el valor RENDERMODE_WHEN_DIRTY
al método setRenderMode
.
1 |
view.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); |
Paso 2: Crea un Renderer
Un GLSurfaceView.Renderer
es responsable de dibujar los contenidos de GLSurfaceView
.
Crea una nueva clase que implemente la interfaz GLSurfaceView.Renderer
. Voy a llamar a esta clase EffectsRenderer
. Después de agregar un constructor y anular todos los métodos de la interfaz, la clase debería verse así:
1 |
public class EffectsRenderer implements GLSurfaceView.Renderer { |
2 |
|
3 |
public EffectsRenderer(Context context){ |
4 |
super(); |
5 |
}
|
6 |
|
7 |
@Override
|
8 |
public void onSurfaceCreated(GL10 gl, EGLConfig config) { |
9 |
}
|
10 |
|
11 |
@Override
|
12 |
public void onSurfaceChanged(GL10 gl, int width, int height) { |
13 |
}
|
14 |
|
15 |
@Override
|
16 |
public void onDrawFrame(GL10 gl) { |
17 |
}
|
18 |
}
|
Regresa a tu Activity
y llama al método setRenderer
para que GLSurfaceView
use el renderizador personalizado.
1 |
view.setRenderer(new EffectsRenderer(this)); |
Paso 3: Editar el Manifiesto
Si planeas publicar tu aplicación en Google Play, agrega lo siguiente a AndroidManifest.xml:
1 |
<uses-feature android:glEsVersion="0x00020000" android:required="true" /> |
Esto asegura que tu aplicación solo pueda instalarse en dispositivos compatibles con OpenGL ES 2.0. El entorno OpenGL ahora está listo.
2. Creando un plano OpenGL
Paso 1: Definir vértices
GLSurfaceView
no puede mostrar una foto directamente. La foto debe convertirse en una textura y aplicarse a una forma OpenGL primero. En este tutorial, crearemos un plano 2D que tiene cuatro vértices. En aras de la simplicidad, hagámoslo un cuadrado. Crea una nueva clase, Cuadrado, para representar el cuadrado.
1 |
public class Square { |
2 |
|
3 |
}
|
El sistema de coordenadas predeterminado de OpenGL tiene su origen en su centro. Como resultado, las coordenadas de las cuatro esquinas de nuestro cuadrado, cuyos lados tienen dos unidades de longitud, serán:
- esquina inferior izquierda a (-1, -1)
- esquina inferior derecha en (1, -1)
- esquina superior derecha en (1, 1)
- esquina superior izquierda a (-1, 1)
Todos los objetos que dibujamos usando OpenGL deben estar formados por triángulos. Para dibujar el cuadrado, necesitamos dos triángulos con un borde común. Esto significa que las coordenadas de los triángulos serán:
triángulo 1: (-1, -1), (1, -1) y (-1, 1)
triángulo 2: (1, -1), (-1, 1) y (1, 1)
Crea una matriz flotante para representar estos vértices.
1 |
private float vertices[] = { |
2 |
-1f, -1f, |
3 |
1f, -1f, |
4 |
-1f, 1f, |
5 |
1f, 1f, |
6 |
};
|
Para asignar la textura al cuadrado, debes especificar las coordenadas de los vértices de la textura. Las texturas siguen un sistema de coordenadas en el que el valor de la coordenada y aumenta a medida que avanzas. Crea otra matriz para representar los vértices de la textura.
1 |
private float textureVertices[] = { |
2 |
0f,1f, |
3 |
1f,1f, |
4 |
0f,0f, |
5 |
1f,0f |
6 |
};
|
Paso 2: Crear objetos de memoria intermedia
Las matrices de coordenadas deben convertirse en búferes de bytes antes de que OpenGL pueda usarlas. Declaremos estos búferes primero.
1 |
private FloatBuffer verticesBuffer; |
2 |
private FloatBuffer textureBuffer; |
Escribe el código para inicializar estos almacenamientos intermedios en un nuevo método llamado initializeBuffers
. Usa el método ByteBuffer.allocateDirect
para crear el búfer. Debido a que un float
usa 4 bytes, necesitas multiplicar el tamaño de las matrices con el valor 4.
A continuación, usa ByteBuffer.nativeOrder
para determinar el orden de bytes de la plataforma nativa subyacente y establece el orden de los almacenamientos intermedios en ese valor. Utiliza el método asFloatBuffer
para convertir la instancia de ByteBuffer
en un FloatBuffer
. Después de crear FloatBuffer
, usa el método put
para cargar la matriz en el búfer. Finalmente, usa el método position
para asegurarte de que el buffer se lee desde el principio.
El contenido del método initializeBuffers
debería verse así:
1 |
private void initializeBuffers(){ |
2 |
ByteBuffer buff = ByteBuffer.allocateDirect(vertices.length * 4); |
3 |
buff.order(ByteOrder.nativeOrder()); |
4 |
verticesBuffer = buff.asFloatBuffer(); |
5 |
verticesBuffer.put(vertices); |
6 |
verticesBuffer.position(0); |
7 |
|
8 |
buff = ByteBuffer.allocateDirect(textureVertices.length * 4); |
9 |
buff.order(ByteOrder.nativeOrder()); |
10 |
textureBuffer = buff.asFloatBuffer(); |
11 |
textureBuffer.put(textureVertices); |
12 |
textureBuffer.position(0); |
13 |
}
|
Paso 3: Crea sombreadores
Es hora de escribir tus propios sombreadores. Los sombreadores no son más que simples programas en C que ejecuta la GPU para procesar cada vértice individual. Para este tutorial, debes crear dos sombreadores, un sombreador de vértices y un sombreador de fragmentos.
El código C para el sombreador de vértices es:
1 |
attribute vec4 aPosition; |
2 |
attribute vec2 aTexPosition; |
3 |
varying vec2 vTexPosition; |
4 |
void main() { |
5 |
gl_Position = aPosition; |
6 |
vTexPosition = aTexPosition; |
7 |
};
|
El código C para el sombreador de fragmentos es:
1 |
precision mediump float; |
2 |
uniform sampler2D uTexture; |
3 |
varying vec2 vTexPosition; |
4 |
void main() { |
5 |
gl_FragColor = texture2D(uTexture, vTexPosition); |
6 |
};
|
Si ya conoces OpenGL, este código te resultará familiar porque es común en todas las plataformas. Si no lo haces, para entender estos programas debes consultar la documentación de OpenGL. Aquí hay una breve explicación para que comiences:
- El sombreador de vértices es responsable de dibujar los vértices individuales.
aPosition
es una variable que estará vinculada alFloatBuffer
que contiene las coordenadas de los vértices. De manera similar,aTexPosition
es una variable que estará vinculada alFloatBuffer
que contiene las coordenadas de la textura.gl_Position
es una variable OpenGL incorporada y representa la posición de cada vértice. LavTexPosition
es una variablevarying
, cuyo valor simplemente se transfiere al fragmento sombreador. - En este tutorial, el sombreador de fragmentos es responsable de colorear el cuadrado. Recoge colores de la textura usando el método
texture2D
y los asigna al fragmento usando una variable incorporada llamadagl_FragColor
.
El código del sombreador debe representarse como objetos String
en la clase.
1 |
private final String vertexShaderCode = |
2 |
"attribute vec4 aPosition;" + |
3 |
"attribute vec2 aTexPosition;" + |
4 |
"varying vec2 vTexPosition;" + |
5 |
"void main() {" + |
6 |
" gl_Position = aPosition;" + |
7 |
" vTexPosition = aTexPosition;" + |
8 |
"}"; |
9 |
|
10 |
private final String fragmentShaderCode = |
11 |
"precision mediump float;" + |
12 |
"uniform sampler2D uTexture;" + |
13 |
"varying vec2 vTexPosition;" + |
14 |
"void main() {" + |
15 |
" gl_FragColor = texture2D(uTexture, vTexPosition);" + |
16 |
"}"; |
Paso 4: Crea un programa
Crea un nuevo método llamado initializeProgram
para crear un programa OpenGL después de compilar y vincular los sombreadores.
Utiliza glCreateShader
para crear un objeto shader y devolverle una referencia en forma de int
. Para crear un sombreador de vértices, pásale el valor GL_VERTEX_SHADER
. Del mismo modo, para crear un sombreador de fragmentos, pásale el valor GL_FRAGMENT_SHADER
. Luego usa glShaderSource
para asociar el código de sombreador apropiado con el sombreador. Utiliza glCompileShader
para compilar el código de sombreado.
Después de compilar ambos sombreadores, crea un nuevo programa usando glCreateProgram
. Al igual que glCreateShader
, esto también devuelve un int
como referencia al programa. Llama a glAttachShader
para adjuntar los sombreadores al programa. Finalmente, llama a glLinkProgram
para vincular el programa.
Tu método y las variables asociadas deberían verse así:
1 |
private int vertexShader; |
2 |
private int fragmentShader; |
3 |
private int program; |
4 |
|
5 |
private void initializeProgram(){ |
6 |
vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER); |
7 |
GLES20.glShaderSource(vertexShader, vertexShaderCode); |
8 |
GLES20.glCompileShader(vertexShader); |
9 |
|
10 |
fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER); |
11 |
GLES20.glShaderSource(fragmentShader, fragmentShaderCode); |
12 |
GLES20.glCompileShader(fragmentShader); |
13 |
|
14 |
program = GLES20.glCreateProgram(); |
15 |
GLES20.glAttachShader(program, vertexShader); |
16 |
GLES20.glAttachShader(program, fragmentShader); |
17 |
|
18 |
GLES20.glLinkProgram(program); |
19 |
}
|
Es posible que hayas notado que los métodos OpenGL (los métodos con prefijos gl
) pertenecen a la clase GLES20
. Esto se debe a que estamos utilizando OpenGL ES 2.0. Si deseas utilizar una versión superior, deberás usar las clases GLES30
o GLES31
.
Paso 5: Dibuja el cuadrado
Crea un nuevo método llamado draw
para dibujar el cuadrado usando los vértices y shaders que definimos anteriormente.
Esto es lo que debes hacer en este método:
- Usa
glBindFramebuffer
para crear un objeto de búfer de cuadro nombrado (a menudo llamado FBO).
- Usa
glUseProgram
para comenzar a usar el programa que acabamos de vincular. - Pasa el valor
GL_BLEND
aglDisable
para deshabilitar la combinación de colores durante la representación.
- Usa
glGetAttribLocation
para obtener un control de las variablesaPosition
yaTexPosition
mencionadas en el código de sombreado de vértices.
- Utiliza
glGetUniformLocation
para obtener un control de la constanteuTexture
mencionada en el código de sombreado de fragmentos.
- Utiliza el
glVertexAttribPointer
para asociar los controlesaPosition
yaTexPosition
converticesBuffer
ytextureBuffer
, respectivamente.
- Usa
glBindTexture
para unir la textura (pasada como argumento al método de dibujo) con el sombreador de fragmentos.
- Borra los contenidos de
GLSurfaceView
usandoglClear
.
- Finalmente, usa el método
glDrawArrays
para dibujar realmente los dos triángulos (y por lo tanto el cuadrado).
El código para el método draw
debería verse así:
1 |
public void draw(int texture){ |
2 |
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); |
3 |
GLES20.glUseProgram(program); |
4 |
GLES20.glDisable(GLES20.GL_BLEND); |
5 |
|
6 |
int positionHandle = GLES20.glGetAttribLocation(program, "aPosition"); |
7 |
int textureHandle = GLES20.glGetUniformLocation(program, "uTexture"); |
8 |
int texturePositionHandle = GLES20.glGetAttribLocation(program, "aTexPosition"); |
9 |
|
10 |
GLES20.glVertexAttribPointer(texturePositionHandle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer); |
11 |
GLES20.glEnableVertexAttribArray(texturePositionHandle); |
12 |
|
13 |
GLES20.glActiveTexture(GLES20.GL_TEXTURE0); |
14 |
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture); |
15 |
GLES20.glUniform1i(textureHandle, 0); |
16 |
|
17 |
GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, verticesBuffer); |
18 |
GLES20.glEnableVertexAttribArray(positionHandle); |
19 |
|
20 |
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); |
21 |
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); |
22 |
}
|
Agrega un constructor a la clase para inicializar los búferes y el programa en el momento de la creación del objeto.
1 |
public Square(){ |
2 |
initializeBuffers(); |
3 |
initializeProgram(); |
4 |
}
|
3. Renderizar el plano y la textura OpenGL
Actualmente, nuestro procesador no hace nada. Necesitamos cambiar eso para que pueda representar el plano que creamos en los pasos anteriores.
Pero primero, creemos un mapa de bits
. Agrega cualquier foto a la carpeta res/drawable de tu proyecto. El archivo que estoy usando se llama forest.jpg. Utiliza la herramienta BitmapFactory
para convertir la foto en un objeto Bitmap
. Además, almacena las dimensiones del objeto Bitmap
en variables separadas.
Cambia el constructor de la clase EffectsRenderer
para que tenga los siguientes contenidos:
1 |
private Bitmap photo; |
2 |
private int photoWidth, photoHeight; |
3 |
public EffectsRenderer(Context context){ |
4 |
super(); |
5 |
photo = BitmapFactory.decodeResource(context.getResources(), R.drawable.forest); |
6 |
photoWidth = photo.getWidth(); |
7 |
photoHeight = photo.getHeight(); |
8 |
}
|
Crea un nuevo método llamado generateSquare
para convertir el mapa de bits en una textura e inicializa un objeto Square
. También necesitarás una matriz de enteros para contener referencias a las texturas OpenGL. Usa glGenTextures
para inicializar la matriz y glBindTexture
para activar la textura en el índice 0.
A continuación, usa glTexParameteri
para establecer varias propiedades que decidan cómo se procesa la textura:
- Establece
GL_TEXTURE_MIN_FILTER
(la función de minificación) yGL_TEXTURE_MAG_FILTER
(la función de ampliación) enGL_LINEAR
para asegurarte de que la textura se ve lisa, incluso cuando está estirada o encogida.
- Establece
GL_TEXTURE_WRAP_S
yGL_TEXTURE_WRAP_T
enGL_CLAMP_TO_EDGE
para que la textura nunca se repita.
Finalmente, usa el método texImage2D
para asignar el mapa de bits a la textura. La implementación del método generateSquare
debería verse así:
1 |
private int textures[] = new int[2]; |
2 |
private Square square; |
3 |
|
4 |
private void generateSquare(){ |
5 |
GLES20.glGenTextures(2, textures, 0); |
6 |
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]); |
7 |
|
8 |
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); |
9 |
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); |
10 |
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); |
11 |
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); |
12 |
|
13 |
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, photo, 0); |
14 |
square = new Square(); |
15 |
}
|
Cada vez que cambian las dimensiones de GLSurfaceView
, se llama al método onSurfaceChanged
del Renderer
. Aquí es donde debes llamar a glViewPort
para especificar las nuevas dimensiones de la ventana gráfica. Además, llama a glClearColor
para pintar el negro GLSurfaceView
. A continuación, llama a generateSquare
para reiniciar las texturas y el plano.
1 |
@Override
|
2 |
public void onSurfaceChanged(GL10 gl, int width, int height) { |
3 |
GLES20.glViewport(0,0,width, height); |
4 |
GLES20.glClearColor(0,0,0,1); |
5 |
generateSquare(); |
6 |
}
|
Finalmente, llama al método draw
del objeto Square
dentro del método onDrawFrame
del Renderer
.
1 |
@Override
|
2 |
public void onDrawFrame(GL10 gl) { |
3 |
square.draw(textures[0]); |
4 |
}
|
Ahora puedes ejecutar tu aplicación y ver la foto que elegiste que se muestra como una textura OpenGL en un avión.



4. Usando el Framework Media Effects
El código complejo que escribimos hasta ahora era solo un prerrequisito para usar el framework Media Effects. Ahora es el momento de comenzar a usar el framework mismo. Agrega los siguientes campos a tu clase Renderer
.
1 |
private EffectContext effectContext; |
2 |
private Effect effect; |
Inicializa el campo effectContext
utilizando EffectContext.createWithCurrentGlContext
. Es responsable de administrar la información sobre los efectos visuales dentro de un contexto OpenGL. Para optimizar el rendimiento, esto se debe llamar solo una vez. Agrega el siguiente código al principio de tu método onDrawFrame
.
1 |
if(effectContext==null) { |
2 |
effectContext = EffectContext.createWithCurrentGlContext(); |
3 |
}
|
Crear un efecto es muy simple. Usa el effectContext
para crear un EffectFactory
y usa EffectFactory
para crear un objeto Effect
. Una vez que un objeto Effect
está disponible, puedes invocar apply
y pasarle una referencia a la textura original, en nuestro caso son textures[0]
, junto con una referencia a un objeto de textura en blanco, en nuestro caso son textures[1]
. Después de invocar el método apply
, textures[1]
contendrán el resultado de Effect
.
Por ejemplo, para crear y aplicar el efecto de escala de grises, aquí está el código que debes escribir:
1 |
private void grayScaleEffect(){ |
2 |
EffectFactory factory = effectContext.getFactory(); |
3 |
effect = factory.createEffect(EffectFactory.EFFECT_GRAYSCALE); |
4 |
effect.apply(textures[0], photoWidth, photoHeight, textures[1]); |
5 |
}
|
Llama a este método en onDrawFrame
y pasa textures[1]
al método draw
del objeto Square
. Tu método onDrawFrame
debe tener el siguiente código:
1 |
@Override
|
2 |
public void onDrawFrame(GL10 gl) { |
3 |
if(effectContext==null) { |
4 |
effectContext = EffectContext.createWithCurrentGlContext(); |
5 |
}
|
6 |
if(effect!=null){ |
7 |
effect.release(); |
8 |
}
|
9 |
grayScaleEffect(); |
10 |
square.draw(textures[1]); |
11 |
}
|
El método release
se usa para liberar todos los recursos que posee un Effect
. Cuando ejecutas la aplicación, deberías ver el siguiente resultado:



Puedes usar el mismo código para aplicar otros efectos. Por ejemplo, aquí está el código para aplicar el efecto documental:
1 |
private void documentaryEffect(){ |
2 |
EffectFactory factory = effectContext.getFactory(); |
3 |
effect = factory.createEffect(EffectFactory.EFFECT_DOCUMENTARY); |
4 |
effect.apply(textures[0], photoWidth, photoHeight, textures[1]); |
5 |
}
|
El resultado es así:



Algunos efectos toman parámetros. Por ejemplo, el efecto de ajuste de brillo tiene un parámetro brightness
que toma un valor flotante
. Puedes usar setParameter
para cambiar el valor de cualquier parámetro. El siguiente código muestra cómo usarlo:
1 |
private void brightnessEffect(){ |
2 |
EffectFactory factory = effectContext.getFactory(); |
3 |
effect = factory.createEffect(EffectFactory.EFFECT_BRIGHTNESS); |
4 |
effect.setParameter("brightness", 2f); |
5 |
effect.apply(textures[0], photoWidth, photoHeight, textures[1]); |
6 |
}
|
El efecto hará que tu aplicación muestre el siguiente resultado:



Conclusión
En este tutorial, has aprendido a usar el Framework Media Effects para aplicar varios efectos a tus fotos. Al hacerlo, también aprendiste a dibujar un plano con OpenGL ES 2.0 y aplicarle diversas texturas.
El framework se puede aplicar tanto a fotos como a videos. En el caso de los videos, simplemente tienes que aplicar el efecto a los fotogramas individuales del video en el método onDrawFrame
.
Ya has visto tres efectos en este tutorial y el framework tiene docenas más para que experimentes. Para saber más sobre ellos, consulta el sitio web del desarrollador de Android.