繪製形狀

定義要使用 OpenGL 繪製的形狀後,可能會想繪製形狀。使用 OpenGL ES 2.0 繪製形狀所需的程式碼比您想像的還多,因為 API 可以對圖形轉譯管道提供完善的控制。

本課程說明如何使用 OpenGL ES 2.0 API 繪製您在上一堂課中定義的形狀。

初始化形狀

在開始繪圖之前,您必須先初始化並載入您要繪製的形狀。除非您在程式執行期間使用的形狀結構 (原始座標) 會在執行期間變更,否則應在轉譯器的 onSurfaceCreated() 方法中初始化這些形狀,以提升記憶體和處理效率。

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();
    }
    ...
}

繪製形狀

使用 OpenGL ES 2.0 繪製已定義形狀時,您需要大量的程式碼,因為您必須提供大量詳細資料給圖形轉譯管道。具體而言,您必須定義下列項目:

  • Vertex 著色器 - OpenGL ES 圖形程式碼來算繪形狀的頂點。
  • 片段著色器 - OpenGL ES 程式碼,用於算繪形狀有顏色或紋理的形狀。
  • 程式 - OpenGL ES 物件,其中包含您想用於繪製一或多個形狀的著色器。

您至少需要一個頂點著色器,才能繪製形狀和一個片段著色器來為該形狀加上顏色。這些著色器必須先編譯,然後新增至 OpenGL ES 程式,再用它來繪製形狀。以下範例說明如何定義可在 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;" +
        "}";

    ...
}

著色器包含 OpenGL 著色語言 (GLSL) 程式碼,這類程式碼必須在 OpenGL ES 環境中使用。如要編譯這個程式碼,請在轉譯器類別中建立公用程式方法:

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;
}

如要繪製形狀,您必須編譯著色器程式碼,將其新增至 OpenGL ES 程式物件,然後連結程式。在所繪製物件的建構函式中執行此操作,因此僅執行一次。

注意:編譯 OpenGL ES 著色器和連結程式在 CPU 週期和處理時間方面所花費的成本相當高,因此請避免重複執行此操作。如果您在執行階段中不知道著色器的內容,則應建構程式碼,使其只建立一次,並快取以供日後使用。

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);
    }
}

現在,您已經準備好新增實際繪製形狀的呼叫。使用 OpenGL ES 繪製形狀時,您必須指定多個參數,才能告知轉譯管道您要繪製的內容及繪製方式。由於繪圖選項可能因形狀而異,建議您讓形狀類別包含自己的繪圖邏輯。

建立 draw() 方法以繪製形狀。此程式碼會將位置和顏色值設為形狀的頂點著色器和片段著色器,然後執行繪圖函式。

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);
}

上述所有程式碼都設定完畢後,只需從轉譯器的 onDrawFrame() 方法呼叫 draw() 方法,即可繪製此物件:

Kotlin

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

    mTriangle.draw()
}

Java

public void onDrawFrame(GL10 unused) {
    ...

    mTriangle.draw();
}

執行應用程式時,畫面應如下所示:

圖 1 沒有投影或相機檢視畫面所繪製的三角形。

這個程式碼範例有幾個問題。首先,這對你的朋友來說並不令人驚豔其次,三角形是稍微傾斜,且會在你變更裝置的螢幕方向時改變形狀。形狀傾斜的原因,是因為顯示 GLSurfaceView 的畫面區域比例尚未更正物件的端點。您可以在下一節課程中使用投影和相機檢視畫面來修正這個問題。

最後,三角形是靜止不動,看起來有點無聊。在「Add 動態」課程中,您會讓這個形狀旋轉,並使 OpenGL ES 圖形管道使用更有趣的方式。