可能看到这很多人就会认为跑偏了,我们明明是来学AR/VR的为啥要搞 GLSL ?这是神马鬼?
其实在图形学中,我们要学的东西很多,就比如说一些专业术语,还有线性代数。。。
所以今天就带大家先来认识一下这些专业术语以及基本的OpenGL着色语言(GLSL也叫着色语言)。
1. GLSL是什么(GLSL详细简介)?
GLSL是一门专门为图形开发设计的编程语言。
2.名词解释
图元 :是图形软件包中用来描述各种图形元素的函数,简单来说,图元就是组成图像的基本单元。
渲染管线 :渲染管线也称为渲染流水线,是显示芯片内部处理图形信号相互独立的并行 处理单元。
简单的来说:就是一系列有序的处理 阶段的序列,用于把我们应用中的数据转化到OpenGL生成一个最终的图像的一个过程。
------------------------------------------------------------------------------------------------------------
渲染流程 :
流程:OpenGL首先接收用户提供的几何数据,并且将他输入的数据经过一系列着色阶
段中进行处理,包括顶点着色 ,细分着色,以及几何着色器,然后被送入光栅化单元,光栅化负责剪切区域内的图元并生成片元数据,然后对每一个片元都执行一个片元着色器,然后产生我们最终想要的效果。
在OpenGL中,我们可以使用四种shader阶段。
**顶点着色器(必要) :**接收顶点缓存对象的顶点数据,单独处理每个顶点
**细分着色器(可选) :**接收到来自顶点着色阶段的输出数据,并对收到的数据进一步处理 (用Patch描述一个物体的形状,在OpenGL线管内部生成新的几何体)
**几何着色器(可选):**在管线内部会对所有的图元进行修改,改变几何图元的类型,或者放弃所有的几何体。如果启用,那么输入可能会来自顶点着色阶段完成的几何图元,也可能来自细分阶段的图元数据
片段着色器(必要):作色管线最后一部分,用来处理OpenGL 光栅化之后生成的单独片元,并且这个阶段也必须绑定一个着色器。(在这个阶段里面,计算一个偏远的颜色和深度值,然后传递到管线的片元测试和混合的模块。)
----------------------------------(以下为了解内容,可以先略过)---------------------------------
那在这些步骤 里面都做了什么呢?
那前面我们也讲了,OpenGL需要将所有的数据都保存到缓存对象中,这也就相当于在OpenGL的服务端维护一块内存区域。当缓存初始化完毕后,我们通过调用OpenGL的绘制命令来渲染几何图元(例:glDrawArray命令,就是一个常用的绘制命令)
1、那我们把数据传过来之后OpenGL首先是顶点着色,
顶点着色器(必要):接收顶点缓存对象的顶点数据,单独处理每个顶点
2、当顶点处理完后,会激活细分着色器,
细分着色器(可选):接收到来自顶点着色阶段的输出数据,并对收到的数据进一步处理
(用Patch描述一个物体的形状,在OpenGL线管内部生成新的几何体,并且模型的外观也会变得更加平顺,细分作色器会用到两个阶段来分别管理patch数据,并生成最终状态)
3、几何着色器(可选):在管线内部会对所有的图元进行修改,改变几何图元的类型,或者放弃所有的几何体。如果启用,那么输入可能会来自顶点着色阶段完成的几何图元,也可能来自细分阶段的图元数据
4、那以上的阶段都是顶点数据的处理,顶点着色决定了一个图元应该位于屏幕的什么位置,那接下来我们会用片元着色决定片元的颜色。
接下来这些顶点数据和顶点之间如何让构成几何图元的所有信息会被传入到OpenGL当中。然后将这些顶点与相关的几何图元之间组织起来,准备光栅化(图元装配)
5、在光栅化之前,有些顶点会落在视口之外,那此时与顶点相关的图元会做出改动,来保证相关像素不会在视口之外绘制。(这就是剪切)(视口:我们进行绘制的窗口区域),
6、剪切完之后马上就会进入光栅化,
光栅化:将更新后的图元传递到光栅化单元,生成对应的片元。
7、然后接下来就是片元操作
在这个阶段里面,会计算每一个片元的颜色和深度值,然后传递到管线的片元测试和混合的模块(混合我们的效果),
接下来就是最后的独立片元处理过程
这个阶段里面会使用深度测试和模版测试的方式来决定一个片元是否可见。
如果一个片元通过了所有激活测试,那他就会保存到帧缓存里面,它的对应的的像素的颜色值会被更新。如果开启了融合模式,那么片元的颜色会与该像素当前颜色相叠加形成新的颜色值并写入帧缓存中!
片段着色器(必要):作色管线最后一部分,用来处理OpenGL 光栅化之后生成的单独片元,并且这个阶段也必须绑定一个着色器。(在这个阶段里面,计算一个偏远的颜色和深度值,然后传递到管线的片元测试和混合的模块。)
----------------------------------(以上为了解内容,可以先略过)---------------------------------
说了这麽多 大家先来看一下着色器长什么样子吧!
这里面有好多大家不认识的 类型修饰符 和 数据类型 那这里我们来看一下这里面都用到了那些数据类型 那些类型修饰符 以及简单的语法。
像 uniform 、attribute、varying这些叫类型修饰符!
像 mat4 、vec3 、vec2、这些是数据类型!
像 void positionShift(){ } 、void main(){} 这些是方法!
gl_Position 是内置变量(GLSL中有很多内置变量和内置方法) !
基本语法
(1)注释:支持单行注释符 “//” 和 多行注释符 "/……/ "
(2)main函数没有返回值
(3)每行结尾都必须有一个分号
(4)变量名、方法名、不允许用 gl 或者 gl_ 开头。
总体来说,GLSL数据类型可以分为标量、向量、矩阵、采样器以及数组等几类。 在学习之前,我们要先认识一下这些名词 :
标量 :标量 也被称为 “ 无向量 ” 其值只有大小,并不具有方向。
OpenGL ES着色语言支持的标量类型有布尔型(bool)、整形(int)和浮点型(float)。
向量 :OpenGL ES着色语言中,向量可以看做是用同样类型的标量组成
其基本类型也分为bool、int和float三种。 每个向量可以由2个、3个、4个相同的标量组成。
聚合类型 :是指 矩阵 和 向量。
采样器 :专门用来进行纹理采样的相关操作(后面详细讲)。
数组:有限个类型相同的变量的集合就是数组。
结构体:是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫做结构。
类型 | 描述 |
---|---|
float | IEEE 32位浮点值 |
int | 有符号二进制补码的32位整数 |
bool | 布尔值 |
Android上OpenGL ES2.0中基本数据类型:
浮点型(float)、布尔型(bool)、整型(int)、矩阵型(matrix)以及向量型(vec2、vec3等)等。
2、聚合类型——向量
向量在着色器代码的开发中的作用 : 可以很方便的存储以及操作颜色、位置、纹理坐标等。
| 基本类型| 2D向量| 3D向量|4D向量 |
| - | :-: | -: |
| float | vec2 (包含了2个32位浮点数的向量)值 | vec3 | vec4|
| double
(集成的显卡好像不行,只能独立卡)
| dvce2(包含了2个64位浮点数的向量) | dvce3 | dvce4 |
| int | ivec2 (包含了2个整数的向量)值 | ivec3 | ivec4 |
| uint | uvec2 (包含了2个无符号整数的向量) | uvec3 | uvec4 |
| bool | bvec2 (包含了2个布尔数的向量) | bvec3 | bvec4 |
//向量的使用 :
// 1、使用这些类型声明的变量的初始化过程 与 标量部分是类似的
//例如:(声明一个包含了3个32位浮点数的向量)
vec3 velocity=vec3( 0.0,2.0,3.0 );
// 2、类型之间也可以进行等价转换
//例如:(将一个包含了3个32位浮点数的向量 等价转换为 一个包含3个整数的向量)
vec3 velocity=vec3( 0.0,2.0,3.0 );
ivec3 steps =ivec3 (velocity);
// 3、向量的构造函数还可以用来截短或者加长一个向量。
//如果我们将一个较长的向量传递给一个较短向量的构造函数,那么
//向量将会被自动取短到对应的长度
例如:
截短:
//将一个包含了4个32位浮点数的向量 截短成 一个包含3个32位浮点数的向量
vec4 color;//RGB A
vec3 RGB=vec3 (color);//现在RGB只有前三个分量了
加长:
//将一个包含了3个32位浮点数的向量加长为一个包含4个32位浮点数的向量
vec3 white=vec3(1.0); //white=(1.0,1.0,1.0)
vec4 translucent=vec4 (white,0);
有一些基础的开发人员都知道,3D场景中的移位、旋转、缩放等变换都是由矩阵的运算来实现的。因此3D场景的开发中会非常多的使用矩阵 。
| 基本类型| 2x2的矩阵| 3x3的矩阵| 4x4的矩阵 |
| - | :-: | -: |
| float | mat2(2x2的浮点数矩阵)| mat3 | mat4 |
| double| dmat2| dmat3| dmat4|
矩阵的使用:
1、矩阵类型需要给出两个维度的信息
例如:
mat44,其中第一个值表示列数,第二个值表示行数。
2、矩阵的构建方式
矩阵的构建方式和我们的向量类似,并且可以将它初始化为一个对角矩阵 或者 完全填充的矩阵。对于对角矩阵 ,只需要向构造函数传递一个值,矩阵的对角线元素就设置为这个值,其他元素全部设置为0;
例如:对于矩阵,OpenGL中矩阵是列主顺序的。如果只传了一个值,则会构造成对角矩阵,其余的元素为0。(看下图)
当然矩阵也可以通过在构造函数中指定每一个元素的值来构建。传入元素可以是标量和向量的集合,只要给定足够的数据就行,每一列的设置方式也遵循这样的原则,也就是说,传入的数据将首先填充列,然后填充行。
例如:
1、初始化耦合33的矩阵
mat3 M=mat3(1.0,2.0,3.0, 4.0,5.0,6.0, 7.0,8.0,9.0);
-----------------------------------------------------------------------------------------------------------
vec3 column1=vec3(1.0,2.0,3.0);
vec3 column2=vec3(4.0,5.0,6.0);
vec3 column3=vec3(7.0,8.0,9.0);
mat3 M=mat3(column1,column2,column3);
-----------------------------------------------------------------------------------------------------------
vec2 column1=vec2(1.0,2.0);
vec2 column2=vec2(4.0,5.0);
vec2 column3=vec2(7.0,8.0);
mat3 M=mat3(column1,3.0,column2,6.0, column3,9.0);
这些写法得到的效果都是(看下图):(GLSL的矩阵 首先填充列,然后填充行)
1. 什么是分量?
分量就是聚合类型中的某一个元素
2.分量名称三种形式的集合:
分量访问符 符号描述
(x,y,z,w) 与位置相关的分量
(r,g,b,a) 与颜色相关的分量
(s,t,p,q) 与纹理坐标相关的分量注意:
1.他们实现的工作是一样的。不同的名称集和只是为了在使用的时候便于区分不同的操作
2.w 是 齐次坐标
分量访问符的作用 :
例如: 基于颜色的红色分量来设置一个亮度。
vec3 luminance=color.rrr;
改变向量中分量各自的位置
color =color.abgr;//反转color的每个分量(RGBA——>ABGR)
3.向量与矩阵中的元素访问方式
向量与矩阵中的元素是可以单独访问和设置的。向量支持两种类型的元素访问方式:
1、使用分量的名称,或者数组访问的形式。
(1)使用分量的名称进行访问
例如:
float red=color.r;
float v_Y=velocity.y;
(2)数组的形式访问。(通过索引)
例如:
float red=color [0];
float Y=velocity [1];
2、矩阵元素的访问可以使用数组标记方式,或者从矩阵中直接得到一个标量。
例如:
mat4 m=mat4(2.0);
vec4 zvec=m[2];//获取矩阵第二列
float yScale= m[1] [1] ;//也可以用m[1].y
注意:
1.唯一限制:在一条语句的一个变量中,只能使用一种类型的访问符。
正确写法:vec4 color=otherColor.rgba;
错误写法:vec4 color=otherColor.rgz;// 错误原因: ” Z “来自不同的访问符集和。
2.访问元素不能超出访问类型的范围
错误写法: vec2 pos;
float zPos=pos.z;//错误原因:2D向量不存在“z”分量
采样器作用:专门用来进行纹理采样的相关操作。一般情况下,一个采样器变量代表一副或者一套纹理贴图
采样器类型 | 说明 |
---|---|
sampler2D | 用于访问二维纹理 |
sampler3D | 用于访问三位纹理 |
samplerCube | 用于访问立方贴图纹理 |
注意:
1、采样器变量不能再着色器中初始化。
2、一般情况下,采样器变量都用uniform限定符来修饰,从宿主语言(JAVA语言)接收传递进着色器的值。
3、sampler3D并不是在所有的OpenGL ES实现中都支持,因此,使用时必须要首先在着色器代码中设置,打开相应拓展。
什么是结构体?
结构体:是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫做结构。
简单解释:就是从逻辑上将不同的类型的数据组合到一个数据集合当中
结构体的作用:
结构体和其他类型基础数据类型一样,例如int类型,char类型 只不过结构体可以做成我们想要的数据类型。以方便日后的使用。
在实际项目中,结构体是大量存在的。研发人员常使用结构体来封装一些属性来组成新的类型。研发人员通常使用结构体创造新的“属性”,其目的是 简化运算。
结构体在函数中的作用不是简便,其最主要的作用就是封装。(封装的好处是什么?)封装的好处就是可以再次利用。让使用者不必关心这个是什么,只要根据定义使用就可以了。
结构体在GLSL里面的作用:结构体可以简化多组数据传入函数的过程
// 使用:
// 怎么定义一个结构体?
struct info { //声明一个结构体info
vec3 color; //颜色成员
vec3 position; //位置成员
vec2 velocitCoor; //纹理坐标成员
}
// 怎么调用结构体元素做为输入参数呢?
//如果定义了一个结构体,那么它会自动创建一个新的类型,并且会隐式定义一个构造函数.
// 那我们只需要这样操作:
Particle p=Particle(10.0,pos,vel);
float lifetime=p .lifetime;
//就可以将各种类型的数据的结构体元素作为输入参数。
数组这东西很熟悉吧!但是你知道什么是数组吗?如果不懂就简单看一下吧!
1、什么是数组? 答:有限个类型相同的变量的集合就是数组
2、 GLSL数组特性 和 注意事项:
1.GLSL支持任意类型的数组,包括结构体数组。
2.数组索引从零开始
3.负数形式的数组索引,或者超出范围的索引值都是不允许的!
4.在GLSL中数组的元素也可以是另一个数组,因此可以处理多维度的数据(GLSL4.3以前不行)
5.数组可以定义为有大小的,或者没有大小的。
例如: float coeff[ 3 ];//有三个float元素的数组
float[ 3 ] coeff;//有三个float元素的数组
int indices[ ];//未定义维数,稍后可以重新声明
6.数组属于GLSL中的第一等类型。
1.GLSL数组有构造函数,并且可以用作函数的参数和返回类型。
2.可以静态初始化一个数组的值
例如: //静态初始化一个数组的值(这里构造函数)
float coeff [3]=float[3](1.11,52.0,52.1);
7.GLSL有一个隐式的方法可返回元素个数
(也就是我们所说的,得到数组长度)
例如:
for (int i=0;i
类型修饰符的作用:
数据类型也可以通过一些修饰符来改变自己的行为。(关于限制符这里大家肯定会有很多问题,因为限制符需要结合实际场景操作,这里我会多举一些例子,让大家能更详细的了解 存储限制符,同时我也会在项目中大量用到 存储限制符)
类型修饰符 | 描述 |
---|---|
attribute | 一般用于每个顶点都各不相同的量,如:顶点位置,颜色等 |
uniform | uniform为一致变量限定符 |
varying | 用于从顶点着色器传递到片元的量 |
const | 用const修饰的变量的值是不可变的,也就是我们说的常量,又叫编译时常量 |
in | 设置这个变量为着色器阶段的输入变量。 |
out | 设置这个变量为着色器阶段的输出变量。 |
inout | 用来修饰的参数为输出输入参数,具有输入输出两种参数功能 |
buffer | 设置应用程序共享的一块可读写内存。这块内存也作为作色器总的存储缓存使用。 |
shared | 设置变量是本地工作组中共享的。他只能用于计算着色器中。 |
点击此处查看 具体使用 案例
(1)GLSL操作符 与 优先级
(2)操作符重载
GLSL中大部分操作符都是经过重载的,也就是说他们可以用于多种类型的数据操作。特别是,矩阵和向量的算数操作符,在GLSL中都是经过严格定义的。
例如:如果我们需要进行向量和矩阵之间的乘法
vec3 v;
mat3 m;
vec3 r=v*m;
基本的限制条件:要求矩阵和向量的维度必须是匹配的!
-
但是这里有一个例外 简单说一下
两个向量相乘得到的时一个逐分量相乘的新向量,
但是两个矩阵相乘得到的是通常矩阵相乘的结果。
例如:
vec2 a,b,c;
c=a*b; //c=( a.x*b.x ,a.y*b.y )
mat2 m,u,v;
m= u*v; //m=( u00*v00+u01*v10 u00*v01+u01*v11
// u01*v00+u11*v10 u10*v01+u11+v11 );
//----------------------------------------------------------------
(3)控制流
GLSL的逻辑控制方式也是用的if和switch,
if(条件){
}else{
}
=====================
switch(条件){
case:
brake;
case:
brake;
default :
brake;
}
注意:
1.如果没有使用brake结尾,拿语句会继续执行case的内容。
(4)循环语句
GLSL的循环语句是for 、while、do{ }while
1.for循环可以在循环初始化条件中声明循环迭代变量,迭代变量的作用于直线于循环内
for(int i=0;i<10;++i){}
2. while(n<10){….}
3. do{…...}while(n<10)
(5)流控制语句
语句 | 描述 |
---|---|
brake | 终止循环体的运行,并且继续执行循环体外的内容 |
continue | 种植循环体内当前迭代过程的执行,跳转到代码块开始的部分并继续执行下一次迭代的内容 |
return | 从当前例程返回,可以带有一个函数的返回值(返回值必须与函数声明的返回类型相符合) |
discard | 丢弃当前片元,终止片元着色器执行(只能在片元着色器中使用,运行到该语句位置上时 片元着色器会立即终止) |
(6)函数声明
函数的作用:我们可以使用函数调用来取代可能反复执行的通用代码。
声明:
returnType functionName([accessmodifier] type,…..){
….
//函数体
return returnValue;//如果returnType为void ,则不需要return语句;
}
注意:
1.函数声明,变量名需要添加访问修饰符
2.GLSL支持用户自定义函数,同时它定义了一些内置函数
3.函数名称可以是任何字符、数字、下划线,但是不能使用数字,
连续下划线或者gl_作为函数的开始
4.返回值可以是任何内置的GLSL类型,或者用户定义的结构体和数组类型。
5.返回值是数组时,必须现实的指定大小。函数返回类型是void则没有返回值
6.函数的参数也可以是任何类型的函数,包括数组(这里数组必须设置大小)
7.在使用一个函数前必须声明他的原型或者直接给出函数体。
8.GLSL的编译器在使用函数前必须找到函数的声明,否则会产生错误
9.函数原型只是给出了函数的形式,但是并没有给出具体的实现内容
例如:
float HornerEvalPolynomial(float coeeff[10] ,float x);
一个着色器程序一般3大部分组成:全局变量声明,自定义函数,main函数。
代码解释
【1】前四行为全局变量声明
那大家根据前四行代码判断一下这是一个什么着色器?
判断依据:
1、attribute属性限定符只能用于顶点着色器中。
2、verying用于从顶点着色器传递到片元的量
uniform为一致变量限定符
(一致变量指的是 对同一组顶点组成的单个3D物体中所有顶点都相同的量。)
1.uniform变量可以用在顶点着色器 或者 片元着色器中。
2.可以修饰所有的基本数据类型
【2】自定义函数部分 void positionShift()
这里这个gl_Position是顶点着色器的内置变量(后面会介绍)
【3】主函数 void main()
大家要注意一点 :
着色器程序中要求被调用的函数必须再调用之前声明!!!
【4】拓展知识
gl_Position是vec4类型的?不是应该是vec3吗,多出来的那个是什么呀?
在3d图形用到了 4x4的矩阵(4行4列),
矩阵乘法要求 nxm * mxp(n行m列 乘 m行p列)才能相乘
注意:m是相同的,所以 1x4 * 4x4 才能相乘(nxm * mxp)。
这部分知识我们会在投影变换的时候继续讲
着色器代码的开发中会用到很多变量,其中大部分可能是由开发人员根据需求自定义的,但着色器中也提供了一些用来满足特性需求的内建变量。
特点:
1.内建变量不需要声明就可以使用。
2.一般用来实现 管线渲染固定功能部分与自定义顶点 或者片元着色器之间的信息交互。
分类:
内建变量根据信息传递方向分为两类,
1、输入变量: 输入变量负责将渲染管线中固定部分产生的信息传递进着色器。
2、输出变量: 输出变量负责将着色器产生的信息传递给渲染管线中固定功能
定点着色器的内置变量
名称 | 类型 | 描述 |
---|---|---|
gl_Color | vec4 | 输入属性-表示顶点的主颜色 |
gl_SecondaryColor | vec4 | 输入属性-表示顶点的辅助颜色 |
gl_Normal | vec3 | 输入属性-表示顶点的法线值 |
gl_Vertex | vec4 | 输入属性-表示物体空间的顶点位置 |
gl_MultiTexCoordn | vec4 | 输入属性-表示顶点的第n个纹理的坐标 |
gl_FogCoord | float | 输入属性-表示顶点的雾坐标 |
gl_Position | vec4 | 输出属性-变换后的顶点的位置,用于后面的固定的裁剪等操作。所有的顶点着色器都必须写这个值。 |
gl_ClipVertex | vec4 | 输出坐标,用于用户裁剪平面的裁剪 |
gl_PointSize | float | 点的大小 |
gl_FrontColor | vec4 | 正面的主颜色的varying输出 |
gl_BackColor | vec4 | 背面主颜色的varying输出 |
gl_FrontSecondaryColor | vec4 | 正面的辅助颜色的varying输出 |
gl_BackSecondaryColor | vec4 | 背面的辅助颜色的varying输出 |
gl_TexCoord[] | vec4 | 纹理坐标的数组varying输出 |
gl_FogFragCoord | float | 雾坐标的varying输出 |
片段着色器的内置变量
| 名称 | 类型 | 描述| | - | :-: | -: | | gl_Color | vec4 | 包含主颜色的插值只读输入| | gl_SecondaryColor | vec4 | 包含辅助颜色的插值只读输入| | gl_TexCoord [ ] | vec4 | 包含纹理坐标数组的插值只读输入| | gl_FogFragCoord | float | 包含雾坐标的插值只读输入| | gl_FragCoord | vec4 | 只读输入,窗口的x,y,z和1/w| | gl_FrontFacing | bool| 只读输入,如果是窗口正面图元的一部分,则这个值为true| | gl_PointCoord | vec2| 点精灵的二维空间坐标范围在(0.0, 0.0)到(1.0, 1.0)之间,仅用| 于点图元和点精灵开启的情况下。 | gl_FragData[ ] | vec4 | 使用glDrawBuffers输出的数据数组。不能与gl_FragColor结合使用。| | gl_FragColor | vec4 | 输出的颜色用于随后的像素操作| | gl_FragDepth | float | 输出的深度用于随后的像素操作,如果这个值没有被写,则使用固定功能管线的深度值代替|内置函数
与其他高级语言类似,为了方便开发,OpenGL ES着色语言也提供了很多的内置函数。这些函数大都已经被重载,一般具有4种变体,分别用来接收和返回float、vec2、vec3以及vec4类型的值。
1、内置函数都是以最优的方式实现的,有部分函数甚至由硬件直接支持,大大提高了执行效率!
2、大部分内置函数同时适用于顶点作色器与片元着色器,但也有部分内置函数只适合顶点着色器或者片元着色器的。
内置函数按照设计目的分为3个类型:
类型 | Academy |
---|---|
提供独特硬件功能访问的接口 | 像纹理采样系列的函数,这些函数用户是无法自己开发的。着色语言去年通过提供特定内置函数对这些硬件功能进行封装我们要使用就直接调用就成 |
简单的数学函数 | 像:abs (求摸)、foolr(取整)等。 (自己可以写,但是如果对底层不了解,实现方式机会效率低) |
一些复杂的函数 | 像:三角函数等 高等数学知识的函数 |