Advertisement
  1. Code
  2. Android SDK

Cómo utilizar los efectos de Android Media con OpenGL ES

Scroll to top
Read Time: 13 min

() 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 al FloatBuffer que contiene las coordenadas de los vértices. De manera similar, aTexPosition es una variable que estará vinculada al FloatBuffer que contiene las coordenadas de la textura. gl_Position es una variable OpenGL incorporada y representa la posición de cada vértice. La vTexPosition es una variable varying, 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 llamada gl_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:

  1. Usa glBindFramebuffer para crear un objeto de búfer de cuadro nombrado (a menudo llamado FBO).
  2. Usa glUseProgram para comenzar a usar el programa que acabamos de vincular.
  3. Pasa el valor GL_BLEND a glDisable para deshabilitar la combinación de colores durante la representación.
  4. Usa glGetAttribLocation para obtener un control de las variables aPosition y aTexPosition mencionadas en el código de sombreado de vértices.
  5. Utiliza glGetUniformLocation para obtener un control de la constante uTexture mencionada en el código de sombreado de fragmentos.
  6. Utiliza el glVertexAttribPointer para asociar los controles aPosition y aTexPosition con verticesBuffer y textureBuffer, respectivamente.
  7. Usa glBindTexture para unir la textura (pasada como argumento al método de dibujo) con el sombreador de fragmentos.
  8. Borra los contenidos de GLSurfaceView usando glClear.
  9. 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) y GL_TEXTURE_MAG_FILTER (la función de ampliación) en GL_LINEAR para asegurarte de que la textura se ve lisa, incluso cuando está estirada o encogida.
  • Establece GL_TEXTURE_WRAP_S y GL_TEXTURE_WRAP_T en GL_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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.