Android OpenGl Es 学习(四):增填颜色

2,067 阅读10分钟

概述

这是一个新的系列,学习OpengGl Es,其实是《OpenGl Es 应用开发实践指南 Android卷》的学习笔记,感兴趣的可以直接看这本书,当然这个会记录自己的理解,以下只作为笔记,以防以后忘记

之后会对本书的前九章依次分析记录

Android OpenGl Es 学习(一):创建一个OpenGl es程序

Android OpenGl Es 学习(二):定义顶点和着色器

Android OpenGl Es 学习(三):编译着色器

Android OpenGl Es 学习(四):增填颜色

Android OpenGl Es 学习(五):调整宽高比

Android OpenGl Es 学习(六):进入三维

Android OpenGl Es 学习(七):使用纹理

Android OpenGl Es 学习(八):构建简单物体

Android OpenGl Es 学习(九):增添触摸反馈

最终是要实现一个曲棍球的简单游戏,类似这样的

增添颜色

在现实世界中,物体有各种各样变色的颜色,我也可以在代码给我们的矩形添加颜色

平滑找色

我们上一篇学习了用uniform用单一的颜色绘制物体,那么我们该如何用许多不同的颜色和找色表达一个复杂的场景呢?

opengl有一个方法,可以在同一个三角形中混合不同的颜色,如果一个三角形每个顶点都有一个不同的颜色,并且在这些三角形的表面混合这些颜色,我们最终得到的是一个平滑找色的三角形

平滑找色是在顶点之间完成的

opengl给了我们一个方法,他可以平滑的混合一个直线和一个三角形的表面上每个顶点的颜色,我们接下来要让我们的矩形表现中间明亮而边缘处比较暗淡,所以我们需要知道矩形的中心点,让我们知道在哪里开始去混合颜色,所以我们要修改这个矩形的结构,拿到中心点

三角形扇

上篇文章定义的顶点

 / //逆时针绘制三角形
    float[] tableVertices = {
            //第一个三角
            -0.5f, -0.5f,
            0.5f, 0.5f,
            -0.5f, 0.5f,
            //第二个三角
            -0.5f,-0.5f,
            0.5f, -0.5f,
            0.5f, 0.5f,
            //线
            -0.5f, 0f,
            0.5f, 0f,
            //点
            0f, -0.25f,
            0f, 0.25f
    };

我们需要把这个顶点改了,改成如下的形式

 //逆时针绘制三角形
    float[] tableVertices = {
            //顶点
            0f, 0f,
            -0.5f, -0.5f,
            0.5f, -0.5f,
            0.5f, 0.5f,
            -0.5f, 0.5f,
            -0.5f, -0.5f,
            //线
            -0.5f, 0f,
            0.5f, 0f,
            //点
            0f, -0.25f,
            0f, 0.25f
    };

那这个该如何理解呢?

随着这个矩形中心点(0,0)的加入,我们最终看到了四个三角形,而不是俩个,如图:

每个三角形需要3个顶点,但有时候同一个顶点会用于多个三角形,我们看下上面的图片,每条边上的顶点都被俩个三角形使用,而中心顶点被4个三角形使用,我们为了不重复写这些顶点,我们可以采用上面的方式定义顶点,然后告诉opengl可以重用这些顶点,把这些顶点作为三角形扇绘制

一个三角形扇,以中间顶点作为起始,使用相邻俩个顶点创建第一个三角形,接下来每个顶点都会创建一个三角形,围绕起始的中心点按扇形展开,为了使这个扇形闭合,我们需要最后重复绘制第二个点

同时我们也要更新opengl函数,让他知道我们绘制的是三角形扇

GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);
//更新为
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 6);

然后运行工程,跟上一篇一样,不过绘制的方式变了

增加新的颜色属性

上面我们已经更新了矩形的结构,现在我们为每一个顶点加入颜色属性,我们把数组更新如下

  float[] tableVertices = {
            //顶点
            0f, 0f,
            //顶点颜色值
            1f, 1f, 1f,

            -0.5f, -0.5f,
            0.7f, 0.7f, 0.7f,

            0.5f, -0.5f,
            0.7f, 0.7f, 0.7f,

            0.5f, 0.5f,
            0.7f, 0.7f, 0.7f,

            -0.5f, 0.5f,
            0.7f, 0.7f, 0.7f,

            -0.5f, -0.5f,
            0.7f, 0.7f, 0.7f,

            //线
            -0.5f, 0f,
            1f, 0f, 0f,

            0.5f, 0f,
            1f, 0f, 0f,

            //点
            0f, -0.25f,
            1f, 0f, 0f,

            0f, 0.25f,
            0f, 0f, 1f
    };

我们为每个顶点增加了三个额外数字,这些数字分别代表红,绿,蓝,他们一起构成对应顶点的颜色

给着色器增加颜色属性

我们先更新顶点着色器vertex_shader.glsl

 attribute vec4 a_Position;
 attribute vec4 a_Color;
 varying vec4 v_Color;
 
  void main() {
      v_Color=a_Color;
      gl_Position =  a_Position;
      gl_PointSize=10.0;
   }

这次加入了一个属性a_Color,和一个varyingv_Color

当opengl构建一条直线和三角形时,会根据顶点构建对应的图形,然后把图像分解为片段,然后每一个片段都会被片段着色器执行一次

varying是一个特殊的变量类型,他把给他的那些值进行混合,并把这些值发给片段找色器,假如说一个顶点0为红色,顶点1为绿色,然后通过a_Color赋值给v_Color,来告诉opengl接近顶点0的混合后颜色更红,接近顶点1的颜色变得更绿

下面更新片段找色器fragment_shader1.glsl

 precision mediump float;
 varying vec4 v_Color;
 
   void main() {
        gl_FragColor = v_Color;
    }

我们用varying变量v_Color替换原来代码中的uniform,如果这个顶点属于直线那么opengl会用直线的俩个顶点颜色计算混合后的颜色,如果属于三角形,会利用三角形的三个顶点颜色计算混合后的颜色

varying如何生成每个片段混合后的颜色

现在我们了解到,直线或三角形每个片段的混合后颜色可以用一个varying生成,我们不仅可以混合颜色,可以给varying传递任何值,opengl都会选择直线顶点的俩个值或三角形三个顶点的三个值,然后平滑的混合这些值,每个片段都会有不同的值,这种混合使用线性差值实现的

一条直线的线性差值

假设我们有一条直线,他有一个红色的顶点,和一个绿色的顶点

直线从左到右:最左边100%红色 0%绿色,然后向右红色逐渐变淡,直到中间50%红色50%绿色,继续向右绿色逐渐增强,直到最右端0%红色 100%绿色

一旦我们把俩个颜色叠加在一起,最终就得到一个混合后的直线,这个就是线性差值的基本解释,每种颜色的强度依赖每个片段与包含那个颜色顶点的距离

我们可以用顶点0和顶点1的值,计算出当前片段对应的距离比,距离比是0到100之间的百分比,0%是左边顶点,100%是右边顶点,我们向左向右移动,距离比会0%到100%增加

要用线性差值计算混合值,可以用一下公式

blended_value=(vertex_0_value*(100%-diatance_ratio))+(vertex_1_value*diatance_ratio)

红色(1,0,0),绿色(010),下图验证这个公式

在三角形表面混合

其实三角形颜色混合,和直线差不多,只不过这次计算方式不太一样

如图所示,对于三角形给的任意点,从这个点向三个顶点画一条直线,就可以生成一个内部三角形,这三个三角形的面积比例,就决定了这个点上每种颜色的权重

这些权重之和也是100%,可以用下面公式计算每个点的颜色分量

blended_value=(vertex_0_value*vertex_0_weight) + (vertex_1_value*vertex_1_weight)+
(vertex_2_value*(100%-vertex_0_weight-vertex_1_weight))

原理是一样的,只不过这次要处理三个点,而不是俩个

使用新的颜色属性渲染

我们上方已经更新了着色器,增加了颜色属性,去掉了uniform,那么我的java代码中也要相应的更新一下

首先我们增加俩个常量

  private final int BYTES_PER_FLOAT = 4;
  private int POSITION_COMPONENT_COUNT = 2;
  //新增
  private final int COLOR_COMPONENT_COUNT = 3;
  private final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT;

第一个常量COLOR_COMPONENT_COUNT表示颜色有三个分量rgb表示

第二常量STRIDE:我们现在同一个数据数组既有位置属性又有颜色属性,opengl不能假定下一个位置紧跟上一个位置,如果OpenGL读入了顶点数据,那么想要读入下一个顶点数据,他需要跳过颜色数据,所以我们需要跨距(STRIDE)告诉opengl每个位置之间有多少个字节,这样他就知道要跳过多少

然后更新属性顶点

  		//获取shader属性
        a_position = GLES20.glGetAttribLocation(program, "a_Position");
        a_color = GLES20.glGetAttribLocation(program, "a_Color");

        //绑定a_position和verticeData顶点位置
        /**
         * 第一个参数,这个就是shader属性
         * 第二个参数,每个顶点有多少分量,我们这个只有来个分量
         * 第三个参数,数据类型
         * 第四个参数,只有整形才有意义,忽略
         * 第5个参数,一个数组有多个属性才有意义,我们只有一个属性,传0
         * 第六个参数,opengl从哪里读取数据
         */
        verticeData.position(0);
        GLES20.glVertexAttribPointer(a_position, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT,
                false, STRIDE, verticeData);
        //开启顶点
        GLES20.glEnableVertexAttribArray(a_position);

		//注释1
        verticeData.position(POSITION_COMPONENT_COUNT);
        GLES20.glVertexAttribPointer(a_color, COLOR_COMPONENT_COUNT, GLES20.GL_FLOAT,
                false, STRIDE, verticeData);
        //开启顶点
        GLES20.glEnableVertexAttribArray(a_color);

我们从注释1 开始看

verticeData.position(POSITION_COMPONENT_COUNT);:opengl开始读入颜色属性时,我们要从第一个颜色属性开始,而不是第一个位置属性,所以我们要跳过第一个位置

接下来调用GLES20.glVertexAttribPointer把颜色数据和找色器中a_color关联起来,跨距告诉opengl俩个颜色之间的距离

运行一下,可以了自己改下颜色试试

使用Android只Color类转换颜色

当我们使用浮点属性时,我们需要0-1之间每个颜色分量的值,如果我们有一个颜色值,怎么才能拿到对应颜色分量的值呢?

Android中提供了Color类,可以轻易的获取颜色每个分量的值,比如:

     float red = Color.red(Color.GREEN) / 255f;
     float green = Color.green(Color.GREEN) / 255f;
     float blue = Color.blue(Color.GREEN) / 255f;

     //网络颜色定义

     int parseColor = Color.parseColor("#FFFFFF");
     float red = Color.red(parseColor) / 255f;
     float green = Color.green(parseColor) / 255f;
     float blue = Color.blue(parseColor) / 255f;

Color.red(),Color.green(),Color.blue(),返回0-255,为了转换为opengl的颜色范围,在除以255就行了

完整代码

public class AirHockKeyRender1 implements GLSurfaceView.Renderer {

    private final FloatBuffer verticeData;
    private final int BYTES_PER_FLOAT = 4;
    private int POSITION_COMPONENT_COUNT = 2;
    private final int COLOR_COMPONENT_COUNT = 3;
    private final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT;
    private final Context mContext;
    //逆时针绘制三角形
    //逆时针绘制三角形
    float[] tableVertices = {
            //顶点
            0f, 0f,
            //顶点颜色值
            1f, 1f, 1f,

            -0.5f, -0.5f,
            0.7f, 0.7f, 0.7f,

            0.5f, -0.5f,
            0.7f, 0.7f, 0.7f,

            0.5f, 0.5f,
            0.7f, 0.7f, 0.7f,

            -0.5f, 0.5f,
            0.7f, 0.7f, 0.7f,

            -0.5f, -0.5f,
            0.7f, 0.7f, 0.7f,

            //线
            -0.5f, 0f,
            1f, 0f, 0f,

            0.5f, 0f,
            1f, 0f, 0f,

            //点
            0f, -0.25f,
            1f, 0f, 0f,

            0f, 0.25f,
            0f, 0f, 1f
    };
    private int a_position;
    private int a_color;



    public AirHockKeyRender1(Context context) {
        this.mContext=context;
        //把float加载到本地内存
        verticeData = ByteBuffer.allocateDirect(tableVertices.length * BYTES_PER_FLOAT)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(tableVertices);
        verticeData.position(0);
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //当surface被创建时,GlsurfaceView会调用这个方法,这个发生在应用程序
        // 第一次运行的时候或者从其他Activity回来的时候也会调用

        //清空屏幕
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        //读取着色器源码
        String fragment_shader_source = ReadResouceText.readResoucetText(mContext, R.raw.fragment_shader1);
        String vertex_shader_source = ReadResouceText.readResoucetText(mContext, R.raw.vertex_shader1);

        //编译着色器源码
        int mVertexshader = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertex_shader_source);
        int mFragmentshader = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragment_shader_source);
        //链接程序
        int program = ShaderHelper.linkProgram(mVertexshader, mFragmentshader);

        //验证opengl对象
        ShaderHelper.volidateProgram(program);
        //使用程序
        GLES20.glUseProgram(program);

        //获取shader属性
        a_position = GLES20.glGetAttribLocation(program, "a_Position");
        a_color = GLES20.glGetAttribLocation(program, "a_Color");

        //绑定a_position和verticeData顶点位置
        /**
         * 第一个参数,这个就是shader属性
         * 第二个参数,每个顶点有多少分量,我们这个只有来个分量
         * 第三个参数,数据类型
         * 第四个参数,只有整形才有意义,忽略
         * 第5个参数,一个数组有多个属性才有意义,我们只有一个属性,传0
         * 第六个参数,opengl从哪里读取数据
         */
        verticeData.position(0);
        GLES20.glVertexAttribPointer(a_position, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT,
                false, STRIDE, verticeData);
        //开启顶点
        GLES20.glEnableVertexAttribArray(a_position);

        verticeData.position(POSITION_COMPONENT_COUNT);
        GLES20.glVertexAttribPointer(a_color, COLOR_COMPONENT_COUNT, GLES20.GL_FLOAT,
                false, STRIDE, verticeData);
        //开启顶点
        GLES20.glEnableVertexAttribArray(a_color);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        //在Surface创建以后,每次surface尺寸大小发生变化,这个方法会被调用到,比如横竖屏切换

        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        //当绘制每一帧数据的时候,会调用这个放方法,这个方法一定要绘制一些东西,即使只是清空屏幕
        //因为这个方法返回后,渲染区的数据会被交换并显示在屏幕上,如果什么都没有话,会看到闪烁效果

        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        //绘制长方形
        //指定着色器u_color的颜色为白色
        /**
         * 第一个参数:绘制绘制三角形
         * 第二个参数:从顶点数组0索引开始读
         * 第三个参数:读入6个顶点
         *
         * 最终绘制俩个三角形,组成矩形
         */
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 6);

        //绘制分割线

        GLES20.glDrawArrays(GLES20.GL_LINES, 6, 2);

        //绘制点
        GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1);

        GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1);
    }
}