首先简单介绍一下Shader是什么,Shader意为着色器,只是整个图形渲染管线流程中的某几个单元,也可以理解为图形渲染管线中的几处会被执行的代码,Shader主要包括:顶点Shader,片段Shader以及几何Shader这3种。
那么顶点,片段和几何分别是什么呢?顶点很好理解,如要绘制一张图片,一般是一个由四个顶点组成的图形。就是我们读书那会几何学里的顶点一个意思。
片段简单点理解就是要绘制这张图片的每一个像素。(这种叫法一般指在渲染管线的着色阶段的像素,片段和像素是有区别的,如在重叠的情况下,一个像素上可以渲染多个片段)
几何指的是要绘制的形状,同样的4个点,可以组成两个三角形、一个四边形或者两条线。
我们通常说的图形渲染其实就是一个固定流水线流程,即一张图片从无到有怎么显示到显示器上的一个过程。Shader就是这个过程中我们程序员唯一能动态操作改变的部分。操作的桥梁就是语言了,DirectX中是HLSL,OpenGL中使用的是GLSL语言来编写Shader脚本,可以写出各种炫酷的效果~
下面我们来大致讲讲图形渲染管线流程的具体的几个步骤:
1.准备好顶点数据和顶点连通性数据。顶点数据包括顶点的位置、颜色、纹理坐标、法线等数据,而顶点连通性数据则是描述顶点之间如何连接组合的数据,如这边两个顶点是连成一条线,这边三个顶点组成一个三角形。
2.将第一步获取的顶点数据进行处理,包括顶点位置变换,顶点光照计算,生成纹理坐标等操作,处理完后输出到下一步。(顶点着色器负责阶段)
3.根据上面提供的顶点数据和顶点连通性数据,将点连成线,线连成面。把零零散散的顶点组装成一个一个的图元,并且将图元进行栅格化,即处理成一个一个的像素格子,并且决定了每个像素格子的位置,这就是我们说的片段。
被确定的除了片段的位置之外,还可能包含片段的颜色,OpenGL会根据图元各个顶点的颜色,纹理坐标进行插值,得出每个片段的颜色插值。这个阶段还会对不在视口范围中的内容进行裁剪。背面剔除操作也会在这个阶段执行。(几何着色器负责阶段,这个我们一般不会用到,因为一般不需要我们再去实现一套新的图元组装规则)
4.对第三步输出的片段进行处理,包括对颜色处理,或者丢弃片段,雾化也是在这个阶段执行的。(片段着色器负责阶段)
5.为即将呈现到屏幕上的内容进行最后的处理,即对每个片段进行一系列的测试:
最后,如果测试都通过了的话,会根据当前的混合模式将片段更新到帧缓冲区里对应的像素中,当屏幕刷新时,帧缓冲区的内容会被渲染到屏幕上。这就是我们看到一个图像如何显示到屏幕上的整个过程。
下面我们看看怎么编写Shader脚本,GLSL语法类似C语言,顶点着色器和片段着色器都是使用GLSL编写的。
一:数据类型和变量
1.基础类型
GLSL支持以下三种基本数据类型:float、int、bool
需要特别注意的是,假设有一个float变量,在着色器中将其赋值为1,在一些显卡上可能会崩溃!因为1是整数不是浮点数,所以需要将1.0赋给浮点型变量。可以这样使用基本类型变量:
int a, b;
float c = 3.0;
bool d = true;
2.向量类型
上面3种基本数据类型分别对应3种向量:
vec2、vec3、vec4:浮点型向量,可以存储2~4个浮点数。
ivec2、ivec3、ivec4:整数向量。
bvec2、bvec3、bvec4:布尔型向量。
向量的操作是很灵活的,可以使用下标来访问向量的分量,也可以使用x、y、z、w字段来访问向量的成员,使用r、g、b、a字段可以访问颜色向量,使用s、t、p、q字段可以访问纹理坐标向量。如果要访问向量的x、y、z3个字段,还可以连续使用字段。下面来代码演示下:
vec2 a = vec2(1.0, 2.0);
vec3 b = vec3(3.0, 4.0, 5.0);
vec3 color = b.rgb; //使用rgb来操作b的三个分量
float c = a[0]; //使用下标的方式来访问a的第一个变量
vec3 d = vec3(a, c); //使用一个vec2和一个float初始化vec3
float e = float(d); //d的x分量被赋值给了e
3.矩阵类型
GLSL提供了2✖️2,3✖️3,4✖️4共3种矩阵类型,分别使用mat2,mat3,mat4表示。此外还支持2~4✖️2~4的任意矩阵,如mat3✖️2、mat4✖️3等。用法和向量类似。
4.采样器
GLSL提供了一组采样器,这是一种用于访问纹理的特殊类型,在读取纹理值时会用到,主要有下面几种采样器。
5.数组和结构体
在GLSL中,可以像C语言一样声明和访问数组,但不能在声明时直接初始化数组,数组下标从0开始,如:
int a[3];
a[0] = 1;
GLSL还可以定义结构体,定义和初始化如下:
struct xxx
{
vec3 pos;
vec3 color;
};
xxx st1;
xxx st2 = xxx(vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 0.0));
st2.pos = vec3(0.0, 1.0, 0.0);
二:操作符
=<+-*/&&||.等等这些基本和c语言操作符一致,不作过多介绍。
三:变量修饰符
变量修饰符放在变量类型之前,用于修饰变量,GLSL常用的有以下变量修饰符。
属性变量和统一变量都是只读的全局变量(不能定义在函数内),都是由OpenGL应用程序设置的变量,即值由底层操作,我们只需定义使用。
属性变量是针对每个顶点的数据,所以只能在顶点着色器中定义,一般会将顶点的颜色、位置、法线等顶点数据作为属性变量传递给顶点着色器。
统一变量是针对整个图元的数据,既可以在顶点着色器中使用,也可以在片段着色器中使用。我们把统一变量作为中间媒介,通过统一变量传递应用程序中的变量给着色器使用。
易变变量是着色器中最神奇的变量,也最不容易理解。主要用于存储插值数据,必须同时在顶点着色器和片段着色器中定义同一个易变变量,在顶点着色器中写入易变变量的值,然后在片段着色器中获取易变变量的值(片段着色器中,易变变量是只读的)。
在顶点着色器中写入顶点的插值,然后到了插值计算阶段会根据这些顶点的插值会进行插值运算,计算出顶点之间片段的值。简单的讲就是对数据进行插值计算。
四:语句与函数
GLSL语句和函数基本和c语言一致,支持if-else,for,while,do-while语句,同时支持break和continue关键字。在片段着色器中,还可以使用discard语句来丢弃当前片段。
着色器程序由函数组成,每个着色器都需要有一个main()函数,和c一样这是程序的入口。
函数返回值可以是任意类型,但不能是数组。函数参数有3种修饰符,分别是in,out,inout,分别表示输入、输出、既输入又输出。没有指定默认为in。函数还允许重载。
bool areyouhappy(in float money)
{
if(money < 1000000000.0)
return false;
else
return true;
}
五:Shader简单示例
简单的颜色Shader脚本:
//顶点Shader
//输入顶点和颜色数据
attribute vec4 vVertex;
attribute vec4 vColor;
//输出颜色插值到vVaryingColor中
varying vec4 vVaryingColor;
void main()
{
vVaryingColor = vColor;
gl_Position = vVertex;
}
//片段Shader
//拿到经过插值后的vVaryingColor,将它设置到最终输出的gl_FragColor变量中
varying vec4 vVaryingColor;
void main()
{
gl_FragColor = vVaryingColor;
}
顶点着色器最主要的职责就是计算位置,所以顶点着色器至少需要写入一个变量gl_Position,这个是GLSL内置的位置变量,需要将变换后的顶点坐标赋予该变量。
片段着色器主要职责是计算片段颜色,所以需要将片段最终颜色写入内置的gl_FragColor变量中(或者将该片段抛弃)。