Android OpenGL ES 5.渐变色

433 阅读5分钟
原文链接: www.jianshu.com

前言

之前我们绘制的图形都是只有一个颜色,而本章节我们绘制一个正方形,并且给图形上渐变色,让简单的图形变得绚丽些。

原理

在基础概念的课程中,我们讲解了渲染管道的流程,现在我们再回顾一下。

OpenGL ES开发重点

之前的章节我们都了解使用过了uniform、attribute这两个限定符,现在我们再了解下varying这个限定符。

  • attritude:一般用于各个顶点各不相同的量。如顶点位置、纹理坐标、法向量、颜色等等。
  • uniform:一般用于对于物体中所有顶点或者所有的片段都相同的量。比如光源位置、统一变换矩阵、颜色等。
  • varying:表示易变量,一般用于顶点着色器传递到片段着色器的量。

从上图我们了解到,varying是从顶点着色器传递给片段着色器的变量数据,但这不够严谨准确,接下来仔细分析下这个流程。

我们举例的场景是这样的:有一条线段,有2个顶点,顶点A是红色,顶点B是蓝色,他们做渐变色处理。相关代码如下:

private static final String VERTEX_SHADER = "" +
        "uniform mat4 u_Matrix;\n" +
        "attribute vec4 a_Position;\n" +
        // a_Color:从外部传递进来的每个顶点的颜色值
        "attribute vec4 a_Color;\n" +
        // v_Color:将每个顶点的颜色值传递给片段着色器
        "varying vec4 v_Color;\n" +
        "void main()\n" +
        "{\n" +
        "    v_Color = a_Color;\n" +
        "    gl_Position = u_Matrix * a_Position;\n" +
        "}";
private static final String FRAGMENT_SHADER = "" +
        "precision mediump float;\n" +
        // v_Color:从顶点着色器传递过来的颜色值
        "varying vec4 v_Color;\n" +
        "void main()\n" +
        "{\n" +
        "    gl_FragColor = v_Color;\n" +
        "}";
  1. 顶点着色器 : 每个顶点都执行一次,比如我们绘制一个线段,包含了2个顶点,那么就是执行2次顶点着色器。而我们这里传递给顶点着色器的数据包含了每个顶点的位置、颜色。
  2. 组装图元:将顶点连接,根据需求绘制顶点、线段、三角形,本次案例是线段。
  3. 光栅化图元:关键! 在光栅化图元的时候,将两个顶点之间的线段分解成大量的小片段,varying数据在这个过程中计算生成,记录在每个片段中(而不是从顶点着色器直接传递给片段着色器)。
  4. 片段着色器:每个片段都计算一次,假如是线段中间的片段,那么传递过来的varying值是紫色的。

所以,梳理下上面的流程:顶点的位置(attribute)、颜色(attribute) → 顶点着色器 → 光栅化:计算出每个片段的具体颜色值 → 片段着色器

代码实现

了解了原理、GLSL代码后,其实Java层的部分和之前差不多,应该部分读者可以自己写出来了。 这次课程的案例代码是一个彩色的正方形,GLSL代码如上,Java代码如下。

private static final float[] POINT_DATA = {
        -0.5f, -0.5f,
        0.5f, -0.5f,
        -0.5f, 0.5f,
        0.5f, 0.5f,
};
private static final float[] COLOR_DATA = {
        // 一个顶点有3个向量数据:r、g、b
        1f, 0.5f, 0.5f,
        1f, 0f, 1f,
        0f, 1f, 1f,
        1f, 1f, 0f,
};


/**
 * 坐标占用的向量个数
 */
private static final int POSITION_COMPONENT_COUNT = 2;
/**
 * 颜色占用的向量个数
 */
private static final int COLOR_COMPONENT_COUNT = 3;

@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
    // 省略部分代码
    int aPositionLocation = getAttrib("a_Position");
    int aColorLocation = getAttrib("a_Color");
    mProjectionMatrixHelper = new ProjectionMatrixHelper(mProgram, "u_Matrix");

    mVertexData.position(0);
    GLES20.glVertexAttribPointer(aPositionLocation,
            POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT, false, 0, mVertexData);
    GLES20.glEnableVertexAttribArray(aPositionLocation);

    mColorData.position(0);
    GLES20.glVertexAttribPointer(aColorLocation,
            COLOR_COMPONENT_COUNT, GLES20.GL_FLOAT, false, 0, mColorData);
    GLES20.glEnableVertexAttribArray(aColorLocation);
}

@Override
public void onDrawFrame(GL10 glUnused) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT);
}

效果如下:


渐变色正方形.png

数据传递格式优化

上面我们在描述顶点的信息时候,用了2个数组去分别描述位置、颜色两个信息。那么我们就得去确保两个数组中顶点的位置和颜色是否一一对应,一定错乱就会得不到想要的效果。除了2个数组存两种信息的方式外,我们还可以选择一个数组存两种信息的方式。可以采用“顶点1位置+顶点1颜色+顶点2位置+顶点2颜色......”这样的方式去存储。代码如下:

private static final float[] POINT_DATA = {
    // 一个顶点有5个向量数据:x、y、r、g、b
    -0.5f, -0.5f, 1f, 0.5f, 0.5f,
    0.5f, -0.5f, 1f, 0f, 1f,
    -0.5f, 0.5f, 0f, 1f, 1f,
    0.5f, 0.5f, 1f, 1f, 0f,
};

这样我们每个顶点的颜色和位置从代码层面上的清晰度就比较明确了。

而在将这些数据传递给GLSL中,则会稍微复杂点,我们需要引入跨距“Stride”这个概念。

/**
 * 坐标占用的向量个数
 */
private static final int POSITION_COMPONENT_COUNT = 2;
/**
 * 颜色占用的向量个数
 */
private static final int COLOR_COMPONENT_COUNT = 3;
/**
 * 数据数组中每个顶点起始数据的间距:数组中每个顶点相关属性占的Byte值
 */
private static final int STRIDE =
        (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT;
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
    // 省略部分代码......
    mVertexData.position(0);
    GLES20.glVertexAttribPointer(aPositionLocation,
            POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT, false, STRIDE, mVertexData);
    GLES20.glEnableVertexAttribArray(aPositionLocation);

    // 将数组的初始读取位置右移2位,所以数组读取的顺序是r1, g1, b1, x2, y2, r2, g2, b2...
    mVertexData.position(POSITION_COMPONENT_COUNT);
    // COLOR_COMPONENT_COUNT:从数组中每次读取3个向量
    // STRIDE:每次读取间隔是 (2个位置 + 3个颜色值) * Float占的Byte位
    GLES20.glVertexAttribPointer(aColorLocation,
            COLOR_COMPONENT_COUNT, GLES20.GL_FLOAT, false, STRIDE, mVertexData);
    GLES20.glEnableVertexAttribArray(aColorLocation);
}

现在我们结合上面方法内第二段代码中,实现颜色数据传递的内容,重新再讲解下glVertexAttribPointer这个方法的参数要求。

  1. 顶点信息索引,位置、纹理坐标、法向量、颜色等等 - aColorLocation,顶点颜色索引
  2. 每个顶点属性需要关联的分量个数(必须为1、2、3或者4。初始值为4。) - |COLOR_COMPONENT_COUNT,颜色RGB需要3个向量
  3. 数据类型 - GLES20.GL_FLOAT,浮点类型
  4. 指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)(只有使用整数数据时)
  5. 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。- STRIDE:每次读取间隔是 (2个位置 + 3个颜色值) * Float占的Byte位 = 5 * 4 = 20.
  6. 数据缓冲区 - mVertexData,顶点数据,包含了位置和颜色

场景应用

在顶点数据较多,且属性数据不变的情况下,使用单个数组来存储数据是个比较好的方案,而在部分数据有所变动,如顶点位置不变,颜色变化的情况下,用多个数组存储数据会是比较好的方案。

参考

Android OpenGL ES学习资料所列举的博客、资料。

GitHub代码工程

本系列课程所有相关代码请参考我的GitHub项目GLStudio

课程目录

本系列课程目录详见 简书 - Android OpenGL ES教程规划