快乐不是因为得到的多,而是因为计较的少
- Android OpenGLES开发:EGL环境搭建
- Android OpenGLES2.0开发(一):艰难的开始
- Android OpenGLES2.0开发(二):环境搭建
- Android OpenGLES2.0开发(三):绘制一个三角形
- Android OpenGLES2.0开发(四):矩阵变换和相机投影
- Android OpenGLES2.0开发(五):绘制正方形和圆形
- Android OpenGLES2.0开发(六):着色器语言GLSL
- Android OpenGLES2.0开发(七):纹理贴图之显示图片
- Android OpenGLES2.0开发(八):Camera预览
- Android OpenGLES2.0开发(九):图片滤镜
- Android OpenGLES2.0开发(十):FBO离屏渲染
- Android OpenGLES2.0开发(十一):渲染YUV
这篇文章我们来简单介绍下OpenGL着色语言(OpenGL Shading Language)
,我们在前面的文章中都直接使用了顶点着色器和片段着色器代码,但是并没有过多的介绍。一方面是为了文章的流畅性,一方面这块知识需要单独拿出来讲。
一. 概述
着色器
是用来实现图像渲染的,用来替代固定渲染管线的可编程程序。着色器替代了传统的固定渲染管线,可以实现3D图形学计算中的相关计算,由于其可编程性,可以实现各种各样的图像效果而不用受显卡的固定渲染管线限制。这极大的提高了图像的画质。
二. GLSL简介
GLSL(OpenGL Shading Language)是用于编写顶点着色器
和片元着色器
的语言。OpenGL ES的着色器语言GLSL是一种高级的图形化编程语言,其源自应用广泛的C语言。与传统的C语言不同的是,它提供了更加丰富的针对于图像处理的原生类型,诸如向量、矩阵之类。OpenGLES 主要包含以下特性:
- GLSL是一种面向过程的语言,和Java的面向对象是不同的。
- GLSL的基本语法与C/C++基本相同。
- 它完美的支持向量和矩阵操作。
- 它是通过限定符操作来管理输入输出类型的。
- GLSL提供了大量的内置函数来提供丰富的扩展功能。
在渲染图形时,主程序会将顶点数据发送到 GPU,然后 GPU 会使用图形着色器来计算每个像素的最终颜色。图形着色器的输入是顶点数据,输出是像素颜色。
着色器代码和主程序之间的关系就在于着色器代码是在GPU上执行的,主程序是在CPU上执行的。主程序会把数据传给着色器,例如顶点数据,着色器代码就能够处理这些数据,并将结果返回给主程序。
三. Shader简介
OpenGL ES 2.0 对应的 Shader 有两种: 顶点着色器(vertex shader):处理图形中每个顶点的位置。 片元着色器(fragment shader):处理每个像素的颜色和透明度。
Vertex shader
的输入为顶点相关的信息数据,输出分为两部分,必须输出的部分是该顶点最终显示在屏幕上的坐标信息,可选输出的是该顶点对应的其他信息(比如颜色、纹理坐标等)。Vertex shader 一次只能操作一个顶点。
Fragment shader
的输入为屏幕上像素点的信息(坐标以及坐标对应像素从 Vertex Shader 传过来的颜色、纹理坐标等信息)。Fragment shader 不能修改一个像素的位置,在操作一个像素点的时候,也不能访问旁边的像素点。Fragment Shader 通过对顶点信息进行操作,输出每个像素点的颜色。输出值用于更新绘制 buffer 中的 color buffer 或者其他目标 buffer。
Shader 文件看上去其实和一个普通的.c 或者.cpp 文件很像。有预处理,会定义一些变量,有 main 函数,会根据变量进行一些运算,并得到一些结果。
- 在 Vertex shader 中 main 函数中最终得到的结果是顶点坐标 gl_Position
- Fragment shader 结构和 VS (pixel shader)基本相同。在 main 函数中计算得到的最终结果是像素点的颜色值 gl_FragColor
四. GLSL基础
GLSL虽然是基于C/C++的语言,但是它和C/C++还是有很大的不同的,比如在GLSL中没有double
、long
等类型,没有union
、enum
、unsigned
以及位运算等特性。
1.数据类型
GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构体、数组、空类型七种类型:
- 标量:bool、int、float,标量表示的是只有大小没有方向的量。对于int和C一样,可以写为十进制(16)、八进制(020)或者十六进制(0x10)。对于标量的运算,我们最需要注意的是精度,防止溢出问题。
- 向量:向量我们可以看做是数组,在GLSL通常用于储存
颜色
、坐标
等数据,针对维数,可分为二维、三维和四维向量。针对存储的标量类型,可以分为bool、int和float。共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。需要注意的是,GLSL中的向量表示竖向量,所以与矩阵相乘进行变换时,矩阵在前,向量在后(与DirectX正好相反)。向量在GPU中由硬件支持运算,比CPU快的多
。- 作为颜色向量时,用rgba表示分量,就如同取数组的中具体数据的索引值。三维颜色向量就用rgb表示分量。比如对于颜色向量vec4 color,color[0]和color.r都表示color向量的第一个值,也就是红色的分量。其他相同。
- 作为位置向量时,用xyzw表示分量,xyz分别表示xyz坐标,w表示向量的模。三维坐标向量为xyz表示分量,二维向量为xy表示分量。
- 作为纹理向量时,用stpq表示分量,三维用stp表示分量,二维用st表示分量。
- 矩阵:在GLSL中矩阵拥有2*2、3*3、4*4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。
- 采样器:sample2D和sampleCube,在 OpenGL ES 中有一个名词叫做 texture,中文名是纹理贴图。
采样器是专门用来对纹理进行采样工作的
,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。 - 结构体:struct,和C语言中的结构体相同,用struct来定义结构体,关于结构体参考C语言中的结构体。
- 数组:数组知识也和C中相同,不同的是数组声明时可以不指定大小,但是建议在不必要的情况下,还是指定大小的好。
- 空类型:空类型用void表示,仅用来声明不返回任何值得函数。
变量声明示例:
float a=1.0;
int b=1;
bool c=true;
vec2 d=vec2(1.0,2.0);
vec3 e=vec3(1.0,2.0,3.0)
vec4 f=vec4(vec3,1.2);
vec4 g=vec4(0.2); //相当于vec(0.2,0.2,0.2,0.2)
vec4 h=vec4(a,a,1.3,a);
mat2 i=mat2(0.1,0.5,1.2,2.4);
mat2 j=mat2(0.8); //相当于mat2(0.8,0.8,0.8,0.8)
mat3 k=mat3(e,e,1.2,1.6,1.8);
2.运算符
GLSL中的运算符如下(越靠前,运算优先级越高):
运算符 | 说明 |
---|---|
[] | 索引 |
++,- - | 前缀自加和自减 |
~,! | 一元非和逻辑非 |
+,- | 加法和减法 |
==,!= | 等于和不等于 |
^^ | 逻辑异或 |
: ? : | 三元运算符号 |
. | 成员选择与混合 |
++,- - | 后缀自加和自减 |
*,/ | 乘法和除法 |
>,<,=,>=,<=,<> | 关系运算符 |
&& | 逻辑与 |
|| | 逻辑或 |
=,+=,-=,*=,/= | 赋值预算 |
3.类型转换
GLSL的类型转换与C不同,在GLSL中类型不可以自动提升。比如float a=1;
就是一种错误的写法,必须严格的写成float a=1.0
,也不可以强制转换,即float a=(float)1;
也是错误的写法,但是可以用内置函数来进行转换,如float a=float(1);
还有float a=float(true);
(true为1.0,false为0.0)等,值得注意的是,低精度的int不能转换为低精度的float。
4. 限定符
GLSL中的限定符号主要有:
名称 | 说明 |
---|---|
attribute | 一般用于各个顶点各不相同的量。如顶点颜色、坐标等。 |
uniform | 一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。 |
varying | 表示易变量,一般用于顶点着色器传递到片元着色器的量。 |
const | 常量。 |
限定符与java限定符类似,放在变量类型之前,并且只能用于全局变量。在GLSL中,没有默认限定符一说。
5. 流程控制
GLSL中的流程控制与C中基本相同,主要有:
- if(){}、if(){}else{}、if(){}else if(){}else{}
- while(){}和do{}while()
- for(;; ){}
- break和continue
6. 函数
GLSL中也可以定义函数,定义函数的方式也与C语言基本相同。函数的返回值可以是GLSL中的除了采样器的任意类型。对于GLSL中函数的参数,可以用参数用途修饰符来进行修饰,常用修饰符如下:
修饰符 | 说明 |
---|---|
in | 输入参数,无修饰符时默认为此修饰符。 |
out | 输入参数 |
inout | 既可以作为输入参数,又可以作为输出参数。 |
7. 浮点精度
与顶点着色器不同的是,在片元着色器中使用浮点型时,必须指定浮点类型的精度
,否则编译会报错。精度有三种,如下:
名称 | 说明 |
---|---|
lowp | 低精度8位 |
mediump | 中精度10位 是 PS 支持的最低要求 |
highp | 高精度16位 是 VS 支持的最低要求 |
精度范围如下:
精度 | 浮点范围 | 浮点量级 | 浮点精度 | 整体范围 |
---|---|---|---|---|
highp | (, ) | (, ) | 相对: | (, ) |
mediump | (, ) | (, ) | 相对: | (, ) |
lowp | (-2, 2) | (, ) | 绝对: | (, ) |
不仅仅是float可以制定精度,其他(除了bool相关)类型也同样可以,但是int、采样器类型并不一定要求指定精度。加精度的定义如下:
uniform lowp float a=1.0;
varying mediump vec4 c;
当然,也可以在片元着色器中设置默认精度,只需要在片元着色器最上面加上precision <精度> <类型>
即可制定某种类型的默认精度。其他情况相同的话,精度越高,画质越好,使用的资源也越多。
8. 程序结构
GLSL程序的结构和C语言差不多,main()方法表示入口函数,可以在其上定义函数和变量,在main中可以引用这些变量和函数。定义在函数体以外的叫做全局变量,定义在函数体内的叫做局部变量。与高级语言不同的是,变量和函数在使用前必须声明,不能再使用的后面声明变量或者函数。
五. GLSL内建变量
内建变量
着色器代码的开发中会用到很多变量,其中大部分可能是由开发人员根据需求自定义的,但着色器中也提供了一些用来满足特性需求的內建变量。
特点
- 內建变量不需要声明就可以使用。
- 一般用来实现管线渲染固定功能部分与自定义顶点或者片元着色器之间的信息交互。
分类
內建变量根据信息传递方向分为两类
- 输入变量: 输入变量负责将渲染管线中固定部分产生的信息传递进着色器。
通常输入变量是只读的,我们可以获取着色器中的一些必要信息设置不同的效果。
- 输出变量: 输出变量负责将着色器产生的信息传递给渲染管线中固定部分。
输出变量需要通过我们从程序传给着色器。
1. 顶点着色器变量
输入变量:
变量名称 | 变量类型 | 说明 |
---|---|---|
gl_VertexID | int | 当使用glDrawElements,存储的是正在绘制顶点的当前索引。 当使用glDrawArrays,储存的是从渲染调用开始的已处理顶点数量。 |
gl_InstanceID | int | 实例化渲染中,当前实例的索引,下标从0开始。 |
输出变量:
变量名称 | 变量类型 | 说明 |
---|---|---|
gl_Position | vet4 | 顶点坐标 |
gl_PointSize | float | 点的宽高(像素) 该功能默认关闭,需调用 glEnable(GL_PROGRAM_POINT_SIZE); |
我们已经在前面的篇章中使用过gl_Position,这个变量就是用来接收我们从程序中传入的顶点坐标数据的变量,然后OpenGL ES会读取该变量的坐标进行绘制
2. 片段着色器变量
输入变量:
变量名称 | 变量类型 | 说明 |
---|---|---|
gl_FragCoord | vec4 | (x,y)为当前片元的屏幕坐标(相对于左下角原点) z分量等于对应片段的深度值(只读) |
gl_FrontFacing | bool | 片元朝向,片段是正面则true,否则false。 |
输出变量:
变量名称 | 变量类型 | 说明 |
---|---|---|
gl_FragColor | vet4 | 表示当前片元的颜色 |
gl_FragDepth | float | 可修改片段的深度值 |
我们已经在前面的篇章中使用过gl_FragColor,这个变量就是用来设置片元的颜色的,然后OpenGL ES会读取该变量的颜色值进行绘制
着色器中的内建变量有很多,在此,我们只列出最常用的集中内建变量。
六. GLSL常用内置函数
1. 常见函数
函数 | 说明 |
---|---|
radians(x) | 角度转弧度 |
degrees(x) | 弧度转角度 |
sin(x) | 正弦函数,传入值为弧度。相同的还有cos余弦函数、tan正切函数、asin反正弦、acos反余弦、atan反正切 |
pow(x,y) | |
exp(x) | |
exp2(x) | |
log(x) | |
log2(x) | |
sqrt(x) | √x |
inversesqr(x) | 1/√x |
abs(x) | 取x的绝对值 |
sign(x) | x>0返回1.0,x<0返回-1.0,否则返回0.0 |
ceil(x) | 返回大于或者等于x的整数 |
floor(x) | 返回小于或者等于x的整数 |
fract(x) | 返回x-floor(x)的值 |
mod(x,y) | 取模(求余) |
min(x,y) | 获取xy中小的那个 |
max(x,y) | 获取xy中大的那个 |
mix(x,y,a) | 返回x∗(1−a)+y∗a |
step(x,a) | x< a返回0.0,否则返回1.0 |
smoothstep(x,y,a) | a < x返回0.0,a>y返回1.0,否则返回0.0-1.0之间平滑的Hermite插值。 |
dFdx(p) | p在x方向上的偏导数 |
dFdy(p) | p在y方向上的偏导数 |
fwidth(p) | p在x和y方向上的偏导数的绝对值之和 |
2. 几何函数
函数 | 说明 |
---|---|
length(x) | 计算向量x的长度 |
distance(x,y) | 返回向量xy之间的距离 |
dot(x,y) | 返回向量xy的点积 |
cross(x,y) | 返回向量xy的差积 |
normalize(x) | 返回与x向量方向相同,长度为1的向量 |
3. 矩阵函数
函数 | 说明 |
---|---|
matrixCompMult(x,y) | 将矩阵相乘 |
lessThan(x,y) | 返回向量xy的各个分量执行x< y的结果,类似的有greaterThan,equal,notEqual |
lessThanEqual(x,y) | 返回向量xy的各个分量执行x<= y的结果,类似的有类似的有greaterThanEqual |
any(bvec x) | x有一个元素为true,则为true |
all(bvec x) | x所有元素为true,则返回true,否则返回false |
not(bvec x) | x所有分量执行逻辑非运算 |
4. 纹理采样函数
纹理采样函数有texture2D、texture2DProj、texture2DLod、texture2DProjLod、textureCube、textureCubeLod及texture3D、texture3DProj、texture3DLod、texture3DProjLod等。
texture表示纹理采样,2D表示对2D纹理采样,3D表示对3D纹理采样 Lod后缀,只适用于顶点着色器采样 Proj表示纹理坐标st会除以q
纹理采样函数中,3D在OpenGLES2.0并不是绝对支持
。我们再次暂时不管3D纹理采样函数。重点只对texture2D函数进行说明。texture2D拥有三个参数,第一个参数表示纹理采样器。第二个参数表示纹理坐标,可以是二维、三维、或者四维。第三个参数加入后只能在片元着色器中调用,且只对采样器为mipmap类型纹理时有效。
小结
我们再来回顾下我们前面用到的着色器程序,是不是发现编写shader程序好像没那么难了。
顶点着色器:
// 一个不变的4维矩阵变量,这里的含义是模型变换矩阵,由外部程序传入
uniform mat4 uMVPMatrix;
// 不同的顶点坐标变量,由外部程序传入
attribute vec4 vPosition;
// 每个顶点会执行一次main函数
void main() {
// 将变换后的顶点坐标赋值给内置顶点坐标变量
gl_Position = uMVPMatrix * vPosition;
};
片段着色器:
// 指定浮点数为中精度10位
precision mediump float;
// 一个不变的颜色向量,由外部程序传入
uniform vec4 vColor;
// 每个片元(像素)会执行一次main函数
void main() {
// 将颜色值赋值给内置的片元色值变量
gl_FragColor = vColor;
};
最后
其实该章节真的非常枯燥,如果不结合实际案例纯讲语言本身我相信没有人可以看完。但是基础的知识我们还是要罗列下,方便后续用到查阅。就好比字典,他可不是让我们来从首页阅读的,他是为了我们阅读别的文章遇到没见过的字来查询用的。那么本篇文章的意义可能就是这样了。
参考: