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

3,207 阅读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 学习(九):增添触摸反馈

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

定义顶点

我们看到上面是最终实现的效果,首先我们先简单实现,一个矩形,一根线,俩个点

我们看到其实一个矩形是由4个顶点组成,然后把四个顶点连成线

点,直线,三角形

在OpenGl中只能绘制点,子线,三角形,也就是所我们不能直接绘制矩形,我们需要把矩形分解成若干三角形,然后在组成成矩形

如图所示,就把一个矩形划分为俩个三角形

在代码中定义顶点

在代码中这些顶点会用浮点数数组来表示,因为是二维坐标,所以每个顶点要用俩个浮点数来记录,一个标记x轴位置,一个标记y轴位置,这个数组通常被称为属性(attribute)数组

  float[] tableVertices = {
                //第一个三角
                0f, 0f,
                9f, 14f,
                0,14f,
                //第二个三角
                0f,0f,
                9f, 0f,
                9f, 14f,
        };

这个数组表示俩个三角形,每个三角形都以逆时针表示,一共四个顶点,俩个三角形共用俩个顶点

添加线和点

   float[] tableVertices = {
            //第一个三角
            0f, 0f,
            9f, 14f,
            0, 14f,
            //第二个三角
            0f, 0f,
            9f, 0f,
            9f, 14f,

            //线
            0f, 7f,
            9f, 7f,

            //点
            4.5f, 2f,
            4.5f, 12f
    };

让数据可以被opengl使用

上面我们已经定义好顶点了,但是我们的java代码是运行在虚拟机上,而opengl是运行在本地的硬件上的,那么如何才能把java数据可以让opengl使用呢?

  • 第一种就是JNI,如果需要了解可以看我之前的博客
  • 第二种就是改变内存的分配方式,java有一个特殊的类集合,可以分配本地的内存块,并且把java数据复制到本地内存

我们看下代码

private final int BYTES_PER_FLOAT = 4;
				
FloatBuffer verticeData = ByteBuffer.allocateDirect(tableVertices.length * BYTES_PER_FLOAT)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(tableVertices);
        verticeData.position(0);

每个浮点数有32位精度,而每个byte有8位精度,所以每个浮点数都占4个字节

方法描述
ByteBuffer.allocateDirect分配一块本地内存,分配大小由外部传入
order(ByteOrder.nativeOrder())告诉缓冲区,按照本地字节序组织内容
asFloatBuffer()我们希望操作Float,调用这个方法会返回FloatBuffer
put()填充数据
position()把数据下标移动到指定位置

opengl管道

现在opengl已经拥有了数据,在把矩形画到屏幕之前,他们还需要在opengl的管道(pipeline)中传递,这一步就需要使用着色器(shader),这些着色器会告诉图形处理单元(CPU)如何绘制数据,有俩种着色器我们需要定义

  • 顶点着色器(vertex shader):生成每个顶点的最终位置,针对每个顶点他都会执行一次,一旦位置确定,opengl就可以把这些顶点组装成点,线和三角形
  • 片段着色器(fragment shader):为组成点,线,三角形的每个片段生成最终的颜色,针对每个片段他都会执行一次,一个片段是一个小的,单一颜色的长方形区域,类似计算机屏幕上的一个像素

一旦最终的颜色生成后,opengl会把他们写到一块称为帧缓冲区(frame buffer)的内存块中,然后Android会把这个帧缓冲区显示到屏幕上

光栅化技术

移动设备的显示屏由成千上百万个独立小组件组成,他们被称为像素,这些像素中每一个都有能力显示几百万颜色中的一种,其实每个像素都是由三个独立的子组件组成,他们发出红色,蓝色,绿色的光,因为每个像素很小,人的眼睛就会把,红绿蓝管混合,从而创造出巨量的颜色,只需要局狗多的像素就可以显示出你想要的画面

opengl通过光栅化的过程,把每个点,线,三角形,分解成大量的小片段,映射到移动设备的像素上,从而生成图像,这些小片段类似显示屏上的像素,每一个都包含单一的纯色,为了表示颜色,每个片段都有4个分量,红色,蓝色,绿色,来表示颜色,阿尔发(alpha)分量用来表示透明度

GLSL

要创建顶点着色器,首先要了解opengl的着色语言OpenGl Shader Language(GLSL),想要详学习换这个语言,可以参考这个OpenGL shader GLSL 中文手册 ,下面记录一些基本信息

基本数据类型

类型说明
void空类型,不返回任何值
bool布尔类型true,false
int带符号的整数 signed integer
float带符号的浮点数
vec2, vec3, vec4n维浮点数向量,包括2,3,4个元素的浮点型向量
bvec2, bvec3, bvec4n维布尔向量,包含2,3,4个元素的布尔向量
ivec2, ivec3, ivec4n维整形向量,包含2,3,4个元素的整形向量
mat2, mat3, mat42✖️2,3✖️3,4✖️4,浮点数矩阵
sampler2D2D纹理
samplerCube立方体纹理

基本结构和数组

类型描述
结构struct type-name{} 类似c语言中的 结构体
数组float foo[3] glsl只支持1维数组,数组可以是结构体的成员

变量限定符

修饰符描述
none(默认可省略)本地变量可读可写,函数的入参就是这种变量
const声明变量或函数的参数为只读类型
attribute只能存在vertex shader(顶点着色器),一般用于保存顶点或法线数据,可以在缓冲区中读取数据
uniform在运行时shader无法改变uniform变量,一版用来放置程序传递给shader的变换矩阵,材质,光照参数
varying主要负责vertex和fragment之间传递数据

参数限定符

函数的参数默认是以拷贝的形式传递的,也就是值传递,任何传递给函数的变量,其值都会拷贝一份,然后再交给函数内部进行处理,我们也可以添加限定符,来达到引用传递的效果

类型描述
默认默认使用in限定符
in真正传入函数的是参数的一份拷贝,在函数内修改参数值,不会影响参数变量本身
out参数的值不会传递给函数,但是在函数内部修改值,会在函数结束后参数的值会改变
inout传入函数的参数是引用,函数内部修改值,参数也会改变

函数

GLSL允许在程序的最外部声明函数,函数不能嵌套,不能递归调用,且必须声明返回值类型(无返回值时返回void),在其他方面与c函数一样

vec4 getPosition(){ 
    vec4 v4 = vec4(0.,0.,0.,1.);
    return v4;
}
 
void doubleSize(inout float size){
    size= size*2.0  ;
}
void main() {
    float psize= 10.0;
    doubleSize(psize);
    gl_Position = getPosition();
    gl_PointSize = psize;
}

类型转换

GLSL可以使用构造函数进行显示类型转换

bool t= true;
bool f = false;
 
int a = int(t); //true转换为1或1.0
int a1 = int(f);//false转换为0或0.0
 
float b = float(t);
float b1 = float(f);
 
bool c = bool(0);//0或0.0转换为false
bool c1 = bool(1);//非0转换为true
 
bool d = bool(0.0);
bool d1 = bool(1.0);

精度限制

GLSL在进行光栅化的时候,会进行大量的浮点运算,这些运算可能是设备不能承受的,所以GLSL提供了三种浮点精度,可以根据不同的设备选择不同的精度

在变量前加highp mediump lowp,即可对精度的声明

lowp float color;
varying mediump vec2 Coord;
lowp ivec2 foo(lowp mat3);
highp mat4 m;

除了精度限定符,我们还可以指定默认使用的精度,如果某个变量没有使用精度限定符,则会使用默认精度,默认精度限定符放在着色器代码初始位置,如:

precision highp float;//默认高精度float
precision mediump int;//默认中精度int

invariant关键字:

由于shader在编译的时候内部会做一些优化,可能导致同样的运算在不同的shader中结果不一致的问题,这会引起一些问题,尤其是在vertex shader向fragment shader传值的时候,所以我们需要使用invariant关键字要求计算结果必须一致,除了这个,我们也可以用#pragma STDGL invariant(all)命令来保证所有的输出一致,这样会限制编译器的优化程度,降低性能

#pragma STDGL invariant(all) //所有输出变量为 invariant
invariant varying texCoord; //varying在传递数据的时候声明为invariant

内置的特殊变量

GLSL使用一些特殊的内置变量与硬件进行沟通,他们大致分为俩种,一种是input类型,他们负责向硬件发送数据,另一种是output类型,负责向程序回传数据,方便编程需要

在vertex shader中

output的内置变量

变量描述
highp vec4 gl_Position;gl_Position 放置顶点坐标信息
mediump float gl_PointSize;gl_PointSize 绘制点的大小

在fragment shader中

input类型的内置变量

变量描述
mediump vec4 gl_FragCoord;片元在framebuffer画面的相对位置
bool gl_FrontFacing;标志当前图元是不是正面图元的一部分
mediump vec2 gl_PointCoord;经过插值计算后的纹理坐标,点的范围是0.0到1.0

output类型内置变量

变量描述
mediump vec4 gl_FragColor;片元在framebuffer画面的相对位置
mediump vec4 gl_FragData[n]设置当前片点的颜色,使用glDrawBuffers数据数组

内置函数

参考上面链接吧,这里就不继续写了

创建顶点着色器

首先我们在res新建一个raw文件夹,然后新建一个文件vertex_shader.glsl,然后用GLSL书写代码

 attribute vec4 a_Position;

  void main() {
      gl_Position =  a_Position;
      gl_PointSize=10.0;
   }

我们定义的每一个顶点,顶点着色器都会被调用一次,当他被调用时,他会在gl_Position属性接受当前顶点的位置

main函数是着色器的入口,他需要做的就是把前面定义的位置,赋值到指定输出变量gl_Position,这个着色器给gl_Position赋值,opengl会把gl_Position储存的值当做顶点的最终位置,然后把这些顶点组装成点,线,三角形

创建片段着色器

我们新建一个文件fragment_shader.glsl

 precision mediump float;
 uniform vec4 u_Color;
   void main() {
        gl_FragColor = u_Color;
    }

第一行设置精度

uniform 不像属性每个顶点都要设置一个,一个uniform会让每个顶点都使用同一个值,除非我们改变他们

main函数是着色器的入口,然后把uniform定义的颜色赋值到特殊的输出变量gl_FragColor