OpenGL Shading Language
流水线概览
来源:GLSL教程-图形流水线,计算机图形学基础知识-渲染流水线
![](https://img.haomeiwen.com/i1956008/44e3229624f5d637.png)
图片描述了一个简化的图形处理流水线,虽然简略但仍然可以展示着色器编程(shader programming)的一些重要概念。
一个固定流水线包括如下功能:
一, 顶点变换 (Vertex Transformation):
这里的顶点是一个信息集合,包括空间中的位置、顶点的颜色、法线、纹理坐标等。这一阶段的输入是独立的顶点信息,固定功能流水线在这一阶段通常进行如下工作:
1,顶点位置变换
2,为每个顶点计算光照
3,纹理坐标的生成与变换
二,图元组合和光栅化(光栅化插值) (Primitive Assembly and Rasterization)
此阶段的输入是变换后的顶点和连接信息(connectivity information)。连接信息告诉流水线顶点如何组成图元(三角形、四边形等)。此阶段还负责视景体(view frustum)裁剪和背面剔除。
光栅化决定了片段(fragment),以及图元的像素位置。这里的片段是指一块数据,用来更新帧缓存(frame buffer)中特定位置的一个像素。一个片段除了包含颜色,还有法线和纹理坐标等属性,这些信息用来计算新的像素颜色值。本阶段的输出包括:
帧缓存中片断的位置
在顶点变换阶段计算出的信息对每个片断的插值。
在这个阶段利用在顶点变换阶段算出的数据,结合连接信息计算出片断的数据。例如,每个顶点包含一个变换后的位置,当它们组成图元时,就可以用来计算图元的片断位置。另一个例子是使用颜色,如果多边形的每个顶点都有自己的颜色值,那么多边形内部片断的颜色值就是各个顶点颜色插值得到的。
三,片断纹理化和色彩化 (Fragment Texturing and Coloring)
此阶段的输入是经过插值的片断信息。在前一阶段已经通过插值计算了纹理坐标和一个颜色值,这个颜色在本阶段可以用来和纹理元素进行组合。此外,这一阶段还可以进行雾化处理。通常最后的输出是片断的颜色值以及深度信息。
四,光栅操作(Raster Operations)
此阶段的输入:1,像素的位置 2,片断深度和颜色值
如果测试成功,则根据当前的混合模式(blend mode)用片断信息来更新像素值。注意混合智能在此阶段进行,因为片断纹理化和颜色化阶段不能访问帧缓存。帧缓存只能在此阶段访问。
下图直观地总结了上述流水线的各个阶段:
![](https://img.haomeiwen.com/i1956008/3c06e53172bfa3e7.png)
取代固定管线的功能(Replacing Fixed Functionality)
现在的显卡允许程序员自己编写实现上述流水线中的两个阶段:
1,顶点变换(Vertex Transformation):顶点shader实现顶点变换阶段的功能
2,片断纹理化和色彩化(Fragment Texturing and Coloring): 该阶段使用片断shader实现
顶点处理器
顶点处理器用来运行顶点shader(着色程序)。顶点shader的输入是顶点数据,即位置、颜色、法线等。下面的OpenGL程序发送数据到顶点处理器,每个顶点中包含一个颜色信息和一个位置信息。
glBegin(…);
glColor3f(0.2,0.4,0.6);
glVertex3f(-1.0,1.0,2.0);
glColor3f(0.2,0.4,0.8);
glVertex3f(1.0.-1.0,2.0);
glEnd();
一个顶点shader可以编写代码实现如下功能:
- 使用模型视图矩阵以及投影矩阵进行顶点变换
- 法线变换及归一化
纹理坐标生成和变换 - 逐顶点或逐像素光照计算
- 颜色计算
不一定要完成上面的所有操作,例如你的程序可能不使用光照。但是,一旦你使用了顶点shader,顶点处理器的所有固定功能都被替代。所以你不能只编写法线变换的shader而指望固定功能帮你完成纹理坐标生成。
从上一节已经知道,顶点处理器并不知道连接信息,因此这里不能执行拓扑信息有关的操作。比如顶点处理器不能进行背面剔除,它知识操作顶点而不是面。
顶点shader至少需要一个变量gl_Position,通常要用模型视图矩阵以及投影矩阵进行变换。顶点处理器可以访问OpenGL状态,所以可以用来处理材质和光照。最新的设备还可以访问纹理。
![](https://img.haomeiwen.com/i1956008/1a57d5735f077e81.png)
片断处理器
片断处理器可以运行片断shader,这个单元可以进行如下操作:
- 逐像素计算颜色和纹理坐标
- 应用纹理
- 雾化计算
- 如果需要逐像素照,可以用来计算法线
片断处理器的输入是顶点坐标、颜色、法线等插值得到的结果。在顶点shader中对每个顶点的属性值进行了计算,现在将对图元中的每个片断进行处理,因此需要插值的结果。
如同顶点处理器一样,当你编写片断shader后,所有固定功能将被取代,所以不能使用片断shader对片断材质化,同时用固定功能进行雾化。程序员必须编写程序实现需要的所有效果。
片断处理器只对每个片断独立进行操作,并不知道相邻片断的内容。类似顶点shader,我们必须访问OpenGL状态,才可能知道应用程序中设置的雾颜色等内容。
一个片段shader有两种输出:
- 抛弃片段内容,什么也不输出
- 计算片段的最终颜色gl_FragColor,当要渲染多个目标时计算gl_FragData。
还可以写入深度信息,但上一阶段已经算过了,所以没有必要。
需要强调的是片段shader不能访问帧缓存,所以混合(blend)这样的操作只能发生在这之后。
![](https://img.haomeiwen.com/i1956008/33c18a58298374ca.png)
着色器语言
1,着色器的基本结构
一个着色器程序和一个C程序类似,都是从main()函数开始执行的。同样支持单行注释//以及多行注释/**/
void main(){
// add test code
}
2,着色器的数据类型
GLSL是一种强类型的语言,所有变量使用前必须声明。可用字母、数字、以及下划线字符来组成变量名字。但是数字或者下划线字符不能作为变量名的第一个字符,也不能使用连续下划线。
所有的变量都必须在声明的同时进行初始化。
- 类型透明:基本数据类型(float、double、int、uint、bool)以及聚合类型(所谓聚合类型就是基本类型的合并)
- 类型不透明:采样器(sampler)、图像(Image)、原子计数器(atomic counter)。
前面提到的聚合类型,每个基本类型都有相对应的聚合类型,以int为例,有2D向量(vec2)、3D向量(vec3)、4D向量(vec4)以及矩阵(mat2、mat4)类型。
需要注意的是对于矩阵类型比如mat4*3,其中第一个值表示列数,第二个值表示行数。此外,矩阵的指定需要遵循主序的原则。也就是说,传入的数据将先填充列,然后填充行。
向量与矩阵中的元素是可以单独访问和设置的。
下面主要说三种比较特殊的分量
(x, y, z, w) 与位置相关的分量
(r, g, b, a) 与颜色相关的分量
(s, t, p, q) 与纹理坐标相关的分量 - 此外还有数组类型,一个大小为n的数组的元素范围是0到n-1:如float coeff[3]; float[3] coeff; int indices[]; 类似其他语言比如C++/Java。数组拥有构造函数float coeff[3] = float[3](2.38, 3.14, 42.0)
GLSL的数组可以获取数组的长度length()该函数返回元素的个数。因为长度值在编译时就是已知的,所以length()方法会返回一个编译时常亮。
3,着色器的存储限制符 (Storage Qualifiers)
数据类型可以通过一些修饰符来改变自己的行为。GLSL中一共定义了4种全局范围内的修饰符。
- const将一个变量定义为只读形式,可以理解为常量的意思
- in 设置为着色器阶段的输入变量
- out 设置为着色器阶段的输出变量
- uniform 表示唯一,对所有的几何图元的值都是一致的,除非应用程序对它执行了更新,否则着色器是并不会影响它的值的变化的。
- buffer 设置应用程序共享的可读写内存
- shared 只能用于计算着色器当中,它可以建立本地工作组内共享的内存。
4,精度限定符
精度限定符可以指明shader中变量的精度,变量可以声明为低、中、高三种精度。这些限定度提示编译器允许在较低的范围和精度上执行计算,在较低的精度上,有些实现可以运行的更有效率。当然,这种效率的提升是以精度为代价的,不合理的使用精度限定符有可能导致出现伪像。
精度限定符用来声明任何基于浮点数或整数的变量的精度,分别用lowp、mediump、highp来声明低、中、高三种精度。
5,逻辑语句
与其他语言类似GLSL提供大量的操作符以及控制语句执行流程的逻辑操作包含算术运算符、操作符、ifels、while、for、break、continue、return等,这里有个特殊的关键字 discard 它只能用于片元着色器中,用于丢弃当前的片元、终止着色器的运行,不过这也取决于具体的硬件实现。
另外函数的声明以及定义和C/C++类似,并且使用函数之前必须先声明,否则会产生错误。但是,需要特别注意的是,GLSL中没有指针或者引用的概念,因此GLSL提供了参数限制符,来表明其参数时候可以修改或者拷贝到函数等等,类似如下:
- in 将数据拷贝到函数中(默认)
- const in 将只读数据拷贝到函数中
- out 从函数中获取值
- inout 将数据拷贝到函数中,并且返回函数中修改的数据
网友评论