快乐不是因为得到的多,而是因为计较的少
这篇文章我们来简单介绍下OpenGL着色语言(OpenGL Shading Language)
,我们在前面的文章中都直接使用了顶点着色器和片段着色器代码,但是并没有过多的介绍。一方面是为了文章的流畅性,一方面这块知识需要单独拿出来讲。
着色器
是用来实现图像渲染的,用来替代固定渲染管线的可编程程序。着色器替代了传统的固定渲染管线,可以实现3D图形学计算中的相关计算,由于其可编程性,可以实现各种各样的图像效果而不用受显卡的固定渲染管线限制。这极大的提高了图像的画质。
GLSL(OpenGL Shading Language)是用于编写顶点着色器
和片元着色器
的语言。OpenGL ES的着色器语言GLSL是一种高级的图形化编程语言,其源自应用广泛的C语言。与传统的C语言不同的是,它提供了更加丰富的针对于图像处理的原生类型,诸如向量、矩阵之类。OpenGLES 主要包含以下特性:
在渲染图形时,主程序会将顶点数据发送到 GPU,然后 GPU 会使用图形着色器来计算每个像素的最终颜色。图形着色器的输入是顶点数据,输出是像素颜色。
着色器代码和主程序之间的关系就在于着色器代码是在GPU上执行的,主程序是在CPU上执行的。主程序会把数据传给着色器,例如顶点数据,着色器代码就能够处理这些数据,并将结果返回给主程序。
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虽然是基于C/C++的语言,但是它和C/C++还是有很大的不同的,比如在GLSL中没有double
、long
等类型,没有union
、enum
、unsigned
以及位运算等特性。
GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构体、数组、空类型七种类型:
颜色
、坐标
等数据,针对维数,可分为二维、三维和四维向量。针对存储的标量类型,可以分为bool、int和float。共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。需要注意的是,GLSL中的向量表示竖向量,所以与矩阵相乘进行变换时,矩阵在前,向量在后(与DirectX正好相反)。向量在GPU中由硬件支持运算,比CPU快的多
。
采样器是专门用来对纹理进行采样工作的
,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。变量声明示例:
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);
GLSL中的运算符如下(越靠前,运算优先级越高):
运算符 | 说明 |
---|---|
[] | 索引 |
++,- - | 前缀自加和自减 |
~,! | 一元非和逻辑非 |
+,- | 加法和减法 |
==,!= | 等于和不等于 |
^^ | 逻辑异或 |
: ? : | 三元运算符号 |
. | 成员选择与混合 |
++,- - | 后缀自加和自减 |
*,/ | 乘法和除法 |
>,<,=,>=,<=,<> | 关系运算符 |
&& | 逻辑与 |
|| | 逻辑或 |
=,+=,-=,*=,/= | 赋值预算 |
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。
GLSL中的限定符号主要有:
名称 | 说明 |
---|---|
attribute | 一般用于各个顶点各不相同的量。如顶点颜色、坐标等。 |
uniform | 一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。 |
varying | 表示易变量,一般用于顶点着色器传递到片元着色器的量。 |
const | 常量。 |
限定符与java限定符类似,放在变量类型之前,并且只能用于全局变量。在GLSL中,没有默认限定符一说。
GLSL中的流程控制与C中基本相同,主要有:
GLSL中也可以定义函数,定义函数的方式也与C语言基本相同。函数的返回值可以是GLSL中的除了采样器的任意类型。对于GLSL中函数的参数,可以用参数用途修饰符来进行修饰,常用修饰符如下:
修饰符 | 说明 |
---|---|
in | 输入参数,无修饰符时默认为此修饰符。 |
out | 输入参数 |
inout | 既可以作为输入参数,又可以作为输出参数。 |
与顶点着色器不同的是,在片元着色器中使用浮点型时,必须指定浮点类型的精度
,否则编译会报错。精度有三种,如下:
名称 | 说明 |
---|---|
lowp | 低精度8位 |
mediump | 中精度10位 是 PS 支持的最低要求 |
highp | 高精度16位 是 VS 支持的最低要求 |
精度范围如下:
精度 | 浮点范围 | 浮点量级 | 浮点精度 | 整体范围 |
---|---|---|---|---|
highp | ( − 2 62 -2^{62} −262, 2 62 2^{62} 262) | ( 2 − 62 2^{-62} 2−62, 2 62 2^{62} 262) | 相对: 2 − 16 2^{-16} 2−16 | ( − 2 16 -2^{16} −216, 2 16 2^{16} 216) |
mediump | ( − 2 14 -2^{14} −214, 2 14 2^{14} 214) | ( 2 − 14 2^{-14} 2−14, 2 14 2^{14} 214) | 相对: 2 − 10 2^{-10} 2−10 | ( − 2 10 -2^{10} −210, 2 10 2^{10} 210) |
lowp | (-2, 2) | ( 2 − 8 2^{-8} 2−8, 2 2 2) | 绝对: 2 − 8 2^{-8} 2−8 | ( − 2 8 -2^{8} −28, 2 8 2^{8} 28) |
不仅仅是float可以制定精度,其他(除了bool相关)类型也同样可以,但是int、采样器类型并不一定要求指定精度。加精度的定义如下:
uniform lowp float a=1.0;
varying mediump vec4 c;
当然,也可以在片元着色器中设置默认精度,只需要在片元着色器最上面加上precision <精度> <类型>
即可制定某种类型的默认精度。其他情况相同的话,精度越高,画质越好,使用的资源也越多。
GLSL程序的结构和C语言差不多,main()方法表示入口函数,可以在其上定义函数和变量,在main中可以引用这些变量和函数。定义在函数体以外的叫做全局变量,定义在函数体内的叫做局部变量。与高级语言不同的是,变量和函数在使用前必须声明,不能再使用的后面声明变量或者函数。
着色器代码的开发中会用到很多变量,其中大部分可能是由开发人员根据需求自定义的,但着色器中也提供了一些用来满足特性需求的內建变量。
內建变量根据信息传递方向分为两类
通常输入变量是只读的,我们可以获取着色器中的一些必要信息设置不同的效果。
输出变量需要通过我们从程序传给着色器。
输入变量:
变量名称 | 变量类型 | 说明 |
---|---|---|
gl_VertexID | int | 当使用glDrawElements,存储的是正在绘制顶点的当前索引。 当使用glDrawArrays,储存的是从渲染调用开始的已处理顶点数量。 |
gl_InstanceID | int | 实例化渲染中,当前实例的索引,下标从0开始。 |
输出变量:
变量名称 | 变量类型 | 说明 |
---|---|---|
gl_Position | vet4 | 顶点坐标 |
gl_PointSize | float | 点的宽高(像素) 该功能默认关闭,需调用 glEnable(GL_PROGRAM_POINT_SIZE); |
我们已经在前面的篇章中使用过gl_Position,这个变量就是用来接收我们从程序中传入的顶点坐标数据的变量,然后OpenGL ES会读取该变量的坐标进行绘制
输入变量:
变量名称 | 变量类型 | 说明 |
---|---|---|
gl_FragCoord | vec4 | (x,y)为当前片元的屏幕坐标(相对于左下角原点) z分量等于对应片段的深度值(只读) |
gl_FrontFacing | bool | 片元朝向,片段是正面则true,否则false。 |
输出变量:
变量名称 | 变量类型 | 说明 |
---|---|---|
gl_FragColor | vet4 | 表示当前片元的颜色 |
gl_FragDepth | float | 可修改片段的深度值 |
我们已经在前面的篇章中使用过gl_FragColor,这个变量就是用来设置片元的颜色的,然后OpenGL ES会读取该变量的颜色值进行绘制
上面只是重点介绍了几个常用的内置变量,要了解更详细的内置变量请参考:
函数 | 说明 |
---|---|
radians(x) | 角度转弧度 |
degrees(x) | 弧度转角度 |
sin(x) | 正弦函数,传入值为弧度。相同的还有cos余弦函数、tan正切函数、asin反正弦、acos反余弦、atan反正切 |
pow(x,y) | x y x^y xy |
exp(x) | e x e^x ex |
exp2(x) | 2 x 2^x 2x |
log(x) | log e x \log_{e}x logex |
log2(x) | log 2 x \log_{2}x log2x |
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方向上的偏导数的绝对值之和 |
函数 | 说明 |
---|---|
length(x) | 计算向量x的长度 |
distance(x,y) | 返回向量xy之间的距离 |
dot(x,y) | 返回向量xy的点积 |
cross(x,y) | 返回向量xy的差积 |
normalize(x) | 返回与x向量方向相同,长度为1的向量 |
函数 | 说明 |
---|---|
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所有分量执行逻辑非运算 |
纹理采样函数有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;
};
其实该章节真的非常枯燥,如果不结合实际案例纯讲语言本身我相信没有人可以看完。但是基础的知识我们还是要罗列下,方便后续用到查阅。就好比字典,他可不是让我们来从首页阅读的,他是为了我们阅读别的文章遇到没见过的字来查询用的。那么本篇文章的意义可能就是这样了。
参考: