Cómo dibujar formas

Después de definir formas que se dibujarán con OpenGL, es probable que quieras dibujarlas. Dibujar formas con OpenGL ES 2.0 requiere un poco más de código de lo que pensarías, porque la API proporciona mucho control sobre la canalización de procesamiento gráfico.

En esta lección, se explica cómo dibujar las formas que definiste en la lección anterior utilizando la API de OpenGL ES 2.0.

Inicializa formas

Antes de empezar a dibujar, debes inicializar y cargar las formas que planeas hacer. A menos que la estructura (las coordenadas originales) de las formas que usas en tu programa cambien durante el curso de la ejecución, debes inicializarlas en el método onSurfaceCreated() de tu procesador para la una mejor memoria y eficiencia de procesamiento.

Kotlin

    class MyGLRenderer : GLSurfaceView.Renderer {
        ...
        private lateinit var mTriangle: Triangle
        private lateinit var mSquare: Square

        override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
            ...
            // initialize a triangle
            mTriangle = Triangle()
            // initialize a square
            mSquare = Square()
        }
        ...
    }
    

Java

    public class MyGLRenderer implements GLSurfaceView.Renderer {

        ...
        private Triangle mTriangle;
        private Square   mSquare;

        public void onSurfaceCreated(GL10 unused, EGLConfig config) {
            ...
            // initialize a triangle
            mTriangle = new Triangle();
            // initialize a square
            mSquare = new Square();
        }
        ...
    }
    

Traza una forma

Dibujar una forma definida con OpenGL ES 2.0 requiere una cantidad considerable de código, ya que debes proporcionar muchos detalles a la canalización de procesamiento gráfico. Específicamente, debes definir lo siguiente:

  • Sombreador de vértices: Código de gráficos de OpenGL ES para procesar los vértices de una forma.
  • Sombreador de fragmentos: Código de OpenGL ES para dibujar la cara de una forma con colores o texturas.
  • Programa: Un objeto de OpenGL ES que contiene los sombreadores que deseas usar para dibujar una o más formas.

Necesitas al menos un sombreador de vértices para dibujar una forma y un sombreador de fragmentos para colorear esa forma. Estos sombreadores deben compilarse y luego agregarse a un programa de OpenGL ES, que después se usa para dibujar la forma. Aquí se muestra un ejemplo de cómo definir sombreadores básicos que puedes usar para dibujar una forma en la clase Triangle:

Kotlin

    class Triangle {

        private val vertexShaderCode =
                "attribute vec4 vPosition;" +
                "void main() {" +
                "  gl_Position = vPosition;" +
                "}"

        private val fragmentShaderCode =
                "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                "  gl_FragColor = vColor;" +
                "}"

        ...
    }
    

Java

    public class Triangle {

        private final String vertexShaderCode =
            "attribute vec4 vPosition;" +
            "void main() {" +
            "  gl_Position = vPosition;" +
            "}";

        private final String fragmentShaderCode =
            "precision mediump float;" +
            "uniform vec4 vColor;" +
            "void main() {" +
            "  gl_FragColor = vColor;" +
            "}";

        ...
    }
    

Los sombreadores contienen código OpenGL Shading Language (GLSL) que debe compilarse antes de su uso en el entorno OpenGL ES. Para ello, crea un método de utilidades en tu clase de procesador:

Kotlin

    fun loadShader(type: Int, shaderCode: String): Int {

        // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
        // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
        return GLES20.glCreateShader(type).also { shader ->

            // add the source code to the shader and compile it
            GLES20.glShaderSource(shader, shaderCode)
            GLES20.glCompileShader(shader)
        }
    }
    

Java

    public static int loadShader(int type, String shaderCode){

        // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
        // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
        int shader = GLES20.glCreateShader(type);

        // add the source code to the shader and compile it
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);

        return shader;
    }
    

Para dibujar tu forma, debes compilar el código del sombreador, agregarlo a un objeto del programa OpenGL ES y luego vincular el programa. Hazlo en el constructor del objeto dibujado para que solo se realice una vez.

Nota: Compilar sombreadores de OpenGL ES y vincular programas es costoso en términos de ciclos de CPU y tiempo de procesamiento, por lo que debes evitar hacerlo más de una vez. Si no conoces el contenido de tus sombreadores en el tiempo de ejecución, debes compilar el código de modo tal que solo se cree una vez y luego se almacene en caché para su uso posterior.

Kotlin

    class Triangle {
        ...

        private var mProgram: Int

        init {
            ...

            val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
            val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)

            // create empty OpenGL ES Program
            mProgram = GLES20.glCreateProgram().also {

                // add the vertex shader to program
                GLES20.glAttachShader(it, vertexShader)

                // add the fragment shader to program
                GLES20.glAttachShader(it, fragmentShader)

                // creates OpenGL ES program executables
                GLES20.glLinkProgram(it)
            }
        }
    }
    

Java

    public class Triangle() {
        ...

        private final int mProgram;

        public Triangle() {
            ...

            int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
                                            vertexShaderCode);
            int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
                                            fragmentShaderCode);

            // create empty OpenGL ES Program
            mProgram = GLES20.glCreateProgram();

            // add the vertex shader to program
            GLES20.glAttachShader(mProgram, vertexShader);

            // add the fragment shader to program
            GLES20.glAttachShader(mProgram, fragmentShader);

            // creates OpenGL ES program executables
            GLES20.glLinkProgram(mProgram);
        }
    }
    

Ya estás listo para agregar las llamadas reales que dibujan la forma. Dibujar formas con OpenGL ES requiere que especifiques varios parámetros para indicar a la canalización de procesamiento lo que deseas dibujar y cómo hacerlo. Debido a que las opciones de dibujo pueden variar según la forma, es una buena idea que las clases de formas contengan su propia lógica de dibujo.

Para dibujar la forma, crea un método draw(). Con este código, se establecen los valores de posición y color en el sombreador de vértices y el sombreador de fragmentos de la forma, y luego se ejecuta la función de dibujo.

Kotlin

    private var positionHandle: Int = 0
    private var mColorHandle: Int = 0

    private val vertexCount: Int = triangleCoords.size / COORDS_PER_VERTEX
    private val vertexStride: Int = COORDS_PER_VERTEX * 4 // 4 bytes per vertex

    fun draw() {
        // Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram)

        // get handle to vertex shader's vPosition member
        positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition").also {

            // Enable a handle to the triangle vertices
            GLES20.glEnableVertexAttribArray(it)

            // Prepare the triangle coordinate data
            GLES20.glVertexAttribPointer(
                    it,
                    COORDS_PER_VERTEX,
                    GLES20.GL_FLOAT,
                    false,
                    vertexStride,
                    vertexBuffer
            )

            // get handle to fragment shader's vColor member
            mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor").also { colorHandle ->

                // Set color for drawing the triangle
                GLES20.glUniform4fv(colorHandle, 1, color, 0)
            }

            // Draw the triangle
            GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)

            // Disable vertex array
            GLES20.glDisableVertexAttribArray(it)
        }
    }
    

Java

    private int positionHandle;
    private int colorHandle;

    private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
    private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

    public void draw() {
        // Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram);

        // get handle to vertex shader's vPosition member
        positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(positionHandle);

        // Prepare the triangle coordinate data
        GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
                                     GLES20.GL_FLOAT, false,
                                     vertexStride, vertexBuffer);

        // get handle to fragment shader's vColor member
        colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

        // Set color for drawing the triangle
        GLES20.glUniform4fv(colorHandle, 1, color, 0);

        // Draw the triangle
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

        // Disable vertex array
        GLES20.glDisableVertexAttribArray(positionHandle);
    }
    

Una vez que tienes todo este código implementado, dibujar este objeto solamente requiere llamar al método draw() desde el método onDrawFrame() de tu procesador:

Kotlin

    override fun onDrawFrame(unused: GL10) {
        ...

        triangle.draw()
    }
    

Java

    public void onDrawFrame(GL10 unused) {
        ...

        triangle.draw();
    }
    

Cuando ejecutes la aplicación, debería verse de la siguiente manera:

Figura 1: Triángulo dibujado sin una proyección ni una vista de cámara

Hay algunos problemas en este código de ejemplo. En primer lugar, no vas a impresionar a tus amigos. En segundo lugar, el triángulo está un poco aplastado y cambia de forma cuando modificas la orientación de la pantalla del dispositivo. La razón por la cual la forma está sesgada se debe a que no se corrigieron los vértices del objeto para las proporciones del área de la pantalla donde se muestra la GLSurfaceView. Para solucionar ese problema, puedes usar una vista de cámara y proyección en la próxima lección.

Por último, el triángulo es estacionario, lo cual es un poco aburrido. En la lección Cómo agregar movimiento, harás que esta forma rote y usarás de manera más interesante la canalización de gráficos de OpenGL ES.