[Android] OpenGL ES 中的顶点数据 Vertex 以及着色器 Shader 介绍并绘制三角形

在 OpenGL ES 中,拥有顶点 Vertex 和 着色器 Shader 这2个很重要的知识点,这篇文章将会介绍这两个知识点并且完成绘制一个三角形。

顶点数据 Vertex

OpenGL 需要将数据传给 GPU,这些数据就是顶点数据,称为 Vertex,其可表示位置、颜色等我们想要的任何信息。比如,我们要定义一个三角形,那么需要定义三个顶点,一个顶点有三个坐标:X、Y、Z。其坐标是一个右手坐标系。

右手坐标系在我们以前初中高中学几何的时候也经常用到。在三维坐标系中,Z轴的正轴方向是根据右手定则确定的。右手定则也决定三维空间中任一坐标轴的正旋转方向。要标注X、Y和Z轴的正轴方向,就将右手背对着屏幕放置,拇指即指向X轴的正方向。伸出食指和中指,如右图所示,食指指向Y轴的正方向,中指所指示的方向即是Z轴的正方向。

我们要绘制三角形,首先我们先来定义它的三个顶点。需要注意的是, OpenGL 需要在三个轴都在 -1 到 1 的范围内才会处理,在此之外的都不会处理。定义顶点代码如下:

    // 定义三角形的坐标
    public static float triangleCoords[] = {
            0.5f, 0.5f, 0.0f, // top
            -0.5f, -0.5f, 0.0f, // bottom left
            0.5f, -0.5f, 0.0f  // bottom right
    };

同时我们再定义它的颜色:

    // 定义三角形的颜色——白色
    public static float color[] = {1.0f, 1.0f, 1.0f, 1.0f};

其中颜色的四个值分别指代的是 RGBA。

定义好之后,由于我们的应用并不能直接跟 OpenGL 通信,此时我们需要借助 Java 的 NIO 去直接在 Native 申请内存,达到应用于 OpenGL 通信。代码如下:

        ByteBuffer bb = ByteBuffer.allocateDirect(Triangle.triangleCoords.length * 4); // 1个float占用4个字节
        bb.order(ByteOrder.nativeOrder()); // 设计大小端模式,采用和底层一致的模式

        vertexBuffer = bb.asFloatBuffer(); //我们不希望按字节操作,将坐标数据转换为FloatBuffer
        vertexBuffer.put(Triangle.triangleCoords); // 将数据从Android应用的虚拟机内存中放到Native的内存中
        vertexBuffer.position(0);

着色器 Shader

在了解着色器之前我们需要了解一下 OpenGL 是如何将 3D 空间的坐标转变为屏幕里的 2D 坐标,其是通过图形渲染管线管理的,其分为两部分:将 3D 转 2D 和将坐标转换到有实际颜色的像素。图形渲染管线有几个阶段,每个阶段都将前一个阶段的输出作为输入,每个阶段都有特定函数,并可以并行运行。显卡中有很多小核心,在 GPU 中为每一个阶段运行各自的着色器。

以上就是渲染管线的整个流程,顶点数据传入之后经过管线的各个阶段后即可导出最终的图像。我在网上找到一张图(下图),更让我们容易理解,其中蓝色部分是可以我们注入自定义着色器的部分,大多数时候我们只需要配置顶点着色器、片段(像素)着色器就好了。着色器之间不能相互通信,是独立的程序,唯一的交流就是输入和输出。

即使是相互独立,但是他们也必须组合成一个整体。我们通过 GLSL 对其进行处理,GLSL 有 in 和 out 分别代表输入和输出。uniform 是一种将数据从 CPU 发送到 GPU 的着色器的一种方式,是全局的,具体的 GLSL 语言我们可见此链接

我们定义了两个 shader,代码如下:

    // 简单的顶点着色器
    public static final String vertexShaderCode =
            "attribute vec4 vPosition;\n" +
                    " void main() {\n" +
                    "     gl_Position   = vPosition;\n" +
                    " }";

    // 简单的片段着色器
    public static final String fragmentShaderCode =
            " precision mediump float;\n" +
                    " uniform vec4 vColor;\n" +
                    " void main() {\n" +
                    "     gl_FragColor = vColor;\n" +
                    " }";

然后我们将其加载到内存中:

        int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
        GLES20.glShaderSource(vertexShader,vertexShaderCode);
        GLES20.glCompileShader(vertexShader);
        int fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
        GLES20.glShaderSource(fragmentShader, fragmentShaderCode);
        GLES20.glCompileShader(fragmentShader);

创建 OpenGL Program:

        int program = GLES20.glCreateProgram();
        if (program != 0) {
            GLES20.glAttachShader(program, vertexShader);
            GLES20.glAttachShader(program, fragmentShader);
            GLES20.glLinkProgram(program);
        }

以上步骤在 GLSurfaceView 的 onSurfaceCreated 中初始化之后,在 onDrawFrame 执行以下代码:

        //将程序加入到OpenGLES2.0环境
        GLES20.glUseProgram(mProgram);
        //获取顶点着色器的vPosition成员句柄
        int mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        //启用三角形顶点的句柄
        GLES20.glEnableVertexAttribArray(mPositionHandle);
        //准备三角形的坐标数据
        GLES20.glVertexAttribPointer(mPositionHandle, Triangle.COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, Triangle.vertexStride, vertexBuffer);
        //获取片元着色器的vColor成员的句柄
        int mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
        //设置绘制三角形的颜色
        GLES20.glUniform4fv(mColorHandle, 1, color, 0);
        //绘制三角形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, Triangle.vertexCount);
        //禁止顶点数组的句柄
        GLES20.glDisableVertexAttribArray(mPositionHandle);

至此,一个白色三角形就绘制出来了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注