WebGL rendering pipeline
WebGL rendering pipeline
This article explains the pipeline in three parts: (1) what the pipeline stages are and what they do, (2) a step-by-step example, (3) details such as primitive clipping, screen mapping, perspective divide, rasterization, and coordinate systems.
1. What is the rendering pipeline?
The rendering pipeline is the ordered sequence of processing steps (programs) the GPU runs to turn data into the image on screen.
2. Overview
The pipeline has many stages; each stage’s output is the next stage’s input. Blue = input/output. Gray = fixed. Green = programmable.
Stages in brief:
- 0. Vertex buffer — Before rendering, put all model data (positions, colors, etc.) into buffer memory.
- 1. Vertex shader — Reads from the buffer per vertex. You define behavior in a shader (e.g. transform, rotate, change coordinate system).
OpenGL使用的着色器语言是GLSL,WebGL是OpenGL派生出来的,WebGL的着色器语言也是GLSL(OpenGL Shading Language)。
- 2. Primitive assembly — Primitives are points, lines, or triangles. Assembly uses the vertex shader output and the chosen primitive type and mode to build primitives. (Points have one mode; lines and triangles have several.)
- 3. Rasterization — Converts primitives into fragments (pixels). Each fragment stores 2D screen coords, 3D coords, and interpolated varyings.
- 4. Fragment shader — Colors each fragment; you control the color in the fragment shader.
- 5. Depth/alpha test and blending — When fragments overlap, depth and alpha decide visibility and blended color. Result goes to the color buffer.
- 6. Color buffer — Holds the final frame. When the frame is complete, the browser displays it.
- 7. Screen — The on-screen draw area.
3. WebGL clip space
WebGL draws only coordinates inside the clip cube: x,y,z in [-1, 1], in a coordinate system with z into the screen, y up, x right. That cube is WebGL’s clip space. WebGL uses a left-handed system.
Vertex shader output is homogeneous (x,y,z,w); the range check uses (x/w, y/w, z/w).
The on-screen image is the projection of that cube onto the z=-1 plane. (Left- vs right-handed coordinates are explained later in the article.)
4. Rendering a triangle step by step
What does the pipeline do to draw one triangle?
Step 0 (before calling the pipeline)
To draw a triangle:
- Have data for three vertices.
- Put the data in a buffer the pipeline can read.
- Tell the pipeline how to read from the buffer: offset, stride, count.
- Specify primitive type and draw mode.
Primitives are points, lines, or triangles. Points have one draw mode; lines and triangles each have three.
Suppose V0–V5 are six consecutive vertices in the buffer.
- Line primitive modes:
- Triangle primitive modes:
最后
调用渲染管线程序,执行渲染。
具体操作
(通过WebGL、或封装WebGL的库,编码实现)
- 初始化三个顶点的数据,(0, 0.5), (-0.5, 0.5), (0.5, -0.5)。
- 创建缓冲区对象,存入这三个点的数据。
- 指定顶点着色器,从缓冲区拿3次数据、每次拿2个数据、间隔0个读、刚开始时从第0个数据开始拿。
- 指定渲染的图元类型为“TRIANGLES”,也就是绘制三角形,每三个点装配一个三角图元。
- 调用渲染管线程序,执行渲染。
第一步
开始执行渲染管线程序。上面提到,顶点着色器是逐顶点从缓冲区拿取数据;所以首先,第一步是从缓冲区对象中拿出第一个点(0.0, 0.5),处理一下,传入图形装配程序。 这里的顶点着色器非常简单,没有对拿到的顶点做任何处理,直接拿到就输出,如下
js复制代码attribute vec2 a_Position;
void main() {
gl_Position = vec4(a_Position, 0.0, 1.0);
}
- a_Position 是顶点着色器的入参,用来接受从缓冲对象中读到的顶点坐标。
- vec2表示声明一个2二维向量,同理vec3表示声明一个3维向量,vec4表示声明一个4维向量。
- attribute是顶点着色器程序的一种入参类型,常见的入参类型还有uniform。attribute类型的参数只能做为顶点着色器的入参,而uniform类型的参数既能做为顶点着色器的入参数,又能做为片元着色器的入参。
- gl_Position 是顶点着色器的输出,是一个用四个分量(x,y,z,w)表示的顶点坐标。
- 解释一下这行代码“gl_Position = vec4(a_Position, 0.0, 1.0);”
- 这里gl_Position是vec4类型,而传进来的点是vec2类型,所以需要去构建一个vec4类型的坐标;将z轴坐标设置为了0.0,这样渲染出来的三角形,它就在z=0.0这个平面上。
四个分量(x,y,z,w)表示的坐标也叫齐次坐标,齐次坐标表示的真正3D坐标是(x/w,y/w,z/w)。所以,上面我将gl_Position的w分量设置为了1.0的目的,是为了让渲染出来的坐标就是a_Position表示的坐标。
由于指定的绘制方式是“TRIANGLES”;而目前图元装配程序只拿到了一个点,所以还不能完成一个三角形的装配,必须等待拿到三个顶点后,才可以。
第二步
执行顶点着色器,拿取缓冲区对象中的第二个点(-0.5,-0.5)。
第三步
执行顶点着色器,拿取缓冲区对象中的第三个点,(0.5, -0.5)。
第四步
这个时候图形装配程序已经读入了三个顶点,那么图形装配程序就可以开始装配三角形了。
第五步
将装配好的三角形光栅化成片元。片元是对屏幕上将要绘制像素的抽象表示,每一个片元中都存储着绘制这个像素所需要的一些信息,比如位置信息、颜色信息、深度信息等。
这里为了示意,只显示了10个片元。实际上,片元数目就是这个三角形最终在屏幕上所覆盖的像素数量。如果这里,修改绘制类型为“LINES”,那么渲染管线就会使用前两个点装配一条线出来,第三个点会被舍弃掉。如果修改绘制类型为“LINE_LOOP”,那么渲染管线就会将这三个点装配成首位相连的三条线段,在光栅化阶段就会被光栅化成一个空心的三角形线框。
第六步
片元着色器执行。片元着色器,开始逐片元读取,给其着色,之后会存入颜色缓冲区中。 这里将所有的片元都着为红色。如下,片元着色器。
GLSL复制代码void main() {
// gl_FragColor 是片元着色器的输出值。四个分量分别表示,红、绿、蓝、不透明度。
// 需要注意的时,平常我们所见的颜色通道取值范围一般都是0-255,但是在GLSL中,颜色通道的取值范围是0 - 1。
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // rgba
}
...
...
...
第七步
当缓冲区对象中的所有顶点数据,都走完渲染管线,存入颜色缓冲区之后。webgl就会通知浏览器将颜色缓冲区的像素绘制到屏幕上的绘制区域。
OpenGL中有一个技术,叫双缓存技术。OpenGL会准备两个缓冲区,前缓冲区和后缓冲区,显示器不断的读取前缓冲区的像素画面显示到屏幕上,后缓冲区用于准备下一帧要渲染的像素画面,等后缓冲区准备好一帧画面后,前后缓冲区指针交换,这样准备好的帧画面就会被渲染出来了。
这样做的目的是为了防止闪屏。想象一下,假设GPU处理图像的速度特别慢,渲染一帧画面可能就需要1s,而显示器会每秒钟从GPU像素缓存中读取60次(也就是60HZ),那么你肯定就会看到一帧画面从开始渲染到渲染结束的整个过程,也就是说屏幕上会出现渲染了一半的画面,这就是闪屏。使用双缓存技术可以保证每次从像素缓存中读取的都是完整的一帧图像,很好的解决了闪屏问题。
WebGL没有双缓存的概念,浏览器抹平了这个概念。浏览器大致是这样的干的:对于一帧图像,当所有数据都走完渲染管线变成像素进入颜色缓存区之后,会通知浏览器,浏览器读取颜色缓存区的图像显示到对应的渲染区域。
5 渲染管线中的细节
看到这里,相信你对渲染管线是怎么工作的、有哪些具体的部分,已经有了一个简单、清晰的理解。下面我会结合上面三角形的例子,再补充一些渲染管线执行过程中的细节,让你对渲染管线有更加深入的理解。
5-1 图元裁剪
为了提升渲染管线的执行效率,在光栅化阶段,只会对那些完全 或 部分 处于可视化空间内的图元进行光栅化。那些部分 或 完全 位于可视化空间外的图元会被提前裁剪掉。具体来说,完全处于可视化空间外的图元,会被直接剔除掉。部分处于可视化空间外的图元经裁剪后会生成新的图元。
对于部分在可视化空间外的线图元,会取线图元与可视化空间的交点 与 在可视空间内的点 生成新的线图元。
对于部分在可视化空间外的三角图元,会将存在于可视化空间内的部分三角面,拆分为多个三角面,使其刚好在WebGL的可视空间内。
为了方便讲述,下面以二维空间内的线图元和三角图元的裁剪进行举例。同样的道理,也适用于三维空间的裁剪。
线图元裁剪举例: 点A在可视空间内,点B位于可视空间外,取线段AB与可视空间的交点C,连接AC。AC就是裁剪后新生成的线图元。
三角面图元举例:
如图,三角形ABC处于可视化空间内的部分是四边形 AEFD。可能会被拆分为这样的两个三角形,分别是三角形AED 和 三角形EFD。具体实现细节上,不同图形API、不同的硬件会有不同实现。
图元裁剪时机: 图元装配好之后,紧接着的下一个阶段。如下是添加了图元裁剪阶段后的渲染流水线图示。
5-2 透视除
上面提到,顶点着色器输出的是一个四分量(x,y,z,w)的齐次坐标,在进行完图元装配和裁剪处理之后,保留下来的顶点它仍然是使用四个分量表示的齐次坐标。在进行光栅化之前,需要将它的x、y、z分量都除以w,将它转换为三维坐标形式。这个操作就叫做透视除。
5-3 屏幕映射
屏幕上的WebGL-2维绘制区域,它的坐标系原点在左下角,y轴朝上,x轴朝右,默认范围由对应canvas元素的width和height属性确定。
WebGL可视化空间(上面也提到了),它最终会将所有图元到z=-1.0这个截面上的投影显示到屏幕上(就是屏幕上的WebGL-2维绘制区域)。 对于z=-1.0这个截面确定的二维坐标系来说,它的数据范围是-1.0-1.0、x轴朝右、y轴朝上,与屏幕上的WebGL-2维绘制区域的坐标系不一样;因此将z=-1.0这个截面上的图片输出到屏幕的过程中,就必然需要做一个映射。这个映射操作会在图元装配之后,光栅化之前去执行。
比如: 将canvas的width 和 height分别初始化为400和200,也就是说最终绘制的像素点是400x200个。那么z=-1.0的这个截面中的点(-1.0, -1.0)映射完之后就是屏幕上WebGL-2维绘制区域中最左下角那个像素点,用坐标(0,0)表示;在WebGL可视空间的z=-1.0的这个截面中的点(1.0,1.0)映射完之后对应的就是屏幕上WebGL-2维绘制区域中最右上角那个像素点,用坐标(400,200)表示。
因为屏幕上的WebGL绘制区域是二维的,所以这里只需要将投影至WebGL可视空间的z=-1.0的这个截面上的点的x,y分量对应进行映射就行,z分量不会改变。
5-4 光栅化中的细节
上面提到 “最终显示到屏幕上的是片元(像素),所以还需要将图元转成一个个的片元。如下,是三角图元光栅化的过程。右图每一个小方格代表着一个片元,每一个片元代表最终要显示到屏幕上的一个像素点,每一个片元中存储着对应像素点的一些信息。这些信息包括:屏幕上WebGL-2维绘制区域的二维坐标、WebGL可视化空间范围内的三维坐标、所有varying变量的插值等等。 ”
varying变量是顶点着色器的输出,顶点着色器输出的varying变量最终会被片元着色器中同名的varying变量所接收。
假设,现在我要绘制一条从红色渐变到蓝色的线段:
初始化两个顶点数据 和 两个对应的颜色数据(红色和蓝色)存入缓存中,编写顶点着色器和片元着色器、设置装配图元的方式为LINES、 设置webgl上下文对应的canvas属性的width和height分别是400和400。
- 取两个顶点数据为(0.0, 0.0, 0.0)、(0.0,1.0,0.0)。
- 取两个颜色数据为(1.0, 0.0, 0.0)、(0.0,0.0,1.0)。
- 顶点着色器这样写:
GLSL复制代码attribute vec3 a_Position; attribute vec3 a_Color; varying vec3 v_Color; void main() { v_Color = a_Color; gl_Position = vec4(a_Position, 1.0); }- 片元着色器这样写:
GLSL复制代码varying vec3 v_Color; void main() { gl_FragColor = vec4(v_Color, 1.0); }顶点着色器第一次执行。
a_Position从缓存中读取点(0.0,0.0,0.0)。a_Color从缓存中读取数据(1.0,0.0,0.0)。- 因为着色的操作是片元着色器来执行的,所以这里将拿到的颜色信息a_Color 赋值给 v_Color;顶点着色器输出的varying 类型的v_Color,会被片元着色器中的同名的varying类型的 v_Color所接收。
- gl_Position必须是一个四个分量的齐次坐标,所以这里需要将a_Position拼凑为四个分量的坐标。同时;为了避免透视除的过程中,影响真正的坐标,所以这里将w分量设置为1.0。
- 将输出的顶点坐标进行透视除之后,输出给图元装配程序。
顶点着色器第二次执行。
a_Position从缓存中读取点(0.0,1.0,0.0)。a_Color从缓存中读取数据(0.0,0.0,1.0)。- 将a_Color 赋值给 v_Color。
- 将a_Position拼凑为四个分量的齐次坐标输出。
- 将输出的顶点坐标进行透视除之后,输出给图元装配程序。
图元装配程序执行(已拿到两个点,可以装配为一个线图元了)
- 由点(0.0,0.0,0.0) 和 点(0.0,1.0,0.0)装配一个线图元。
进行屏幕坐标映射
- 只对点的x、y分量进行映射。
- 点(0.0,0.0,0.0)映射完之后是(200, 200)。
- 点(0.0,1.0,0.0)映射完之后是(200,400)。 (屏幕2维坐标,原点在左下角)
光栅化。为了方便讲述,假设将这条线段光栅化为五个片元。实际上光栅化为几个片元是由对应canvas元素的width 和 height属性决定的(注意不是css的属性的width 与 height)。光栅化后,在横向上会有width个片元;在竖向上会有height个片元;拢共会有width x height片元。
那么插完值后,五个片元对应的WebGL可视空间范围的三维顶点坐标、v_Color的值、屏幕上WebGL-2维绘制区域的二维坐标如下:
| 可视化空间范围中的三维坐标 | 屏幕上绘制区域的二维坐标 | v_Color的值 | |
|---|---|---|---|
| 第一个片元 | (0.0, 0.0, 0.0) | (200, 200) | (1.0, 0.0, 0.0) |
| 第二个片元 | (0.0,0.25,0.0) | (200, 250) | (1.0, 0.0, 0.0) |
| 第四个片元 | (0.0, 0.75, 0.0) | (200, 350) | (0.25, 0.0, 0.75) |
| 第五个片元 | (0.0, 1.0, 0.0) | (200, 400) | (0.0, 0.0, 1.0) |
光栅化的过程中的插值方式是线性插值。
屏幕坐标映射完之后,webgl可视空间范围内的点还会继续存在。在片元着色器中可以通过内置变量拿到当前处理的片元所对应的“webgl可视空间内的坐标” 和 “屏幕上webgl-2d绘制区域”中的坐标。
与之相关联的两个内置变量是gl_PointCoord 和 gl_FragCoord。gl_PointCoord是vec2类型,gl_FragCoord是vec4类型。
gl_PointCoord的x,y分量对应webgl可视空间内的坐标的x和y值。
gl_FragCoord的x,y分量对应屏幕上webgl-2d绘制区域中坐标中的x和y值。z分量对应webgl可视空间内的坐标的z值,不过gl_FragCoord的z分量的范围是0.0 - 1.0。这里实际是将-1.0->1.0范围的可视空间坐标的z值映射到了0.0 - 1.0范围内。gl_FragCoord的w分量,目前我这边不了解它是用来干嘛的,感兴趣的朋友可以看这里:registry.khronos.org/OpenGL-Refp…
- 片元着色器执行。由于这条线段被光栅化成了五个片元,所以对应的片元着色器要执行五次。
- 第一次拿到的v_Color是 (1.0, 0.0, 0.0)
- 第二次拿到的v_Color是 (0.75, 0.0, 0.25)
- 第三次拿到的v_Color是 (0.5, 0.0, 0.5)
- 第四次拿到的v_Color是 (0.25, 0.0, 0.75)
- 第五次拿到的v_Color是 (0.0, 0.0, 1.0)
- (深度、透明度)测试与颜色混合
- 由于这里只渲染一条线段,所以这个阶段的处理程序拿到像素之后直接就输出到了颜色缓存中。不存在深度、透明度、颜色混合相关的计算过程。
- 等待所有的像素都输入颜色缓存中之后,通知浏览器。
- 浏览器调用底层api,将颜色缓冲中的图像渲染到屏幕上。
- 这时在屏幕上,就可以看到渲染出来的线段了。
5-5 深度检测与颜色混合
为了提高效率,深度检测默认是关闭的,需要手动开启。不开启的话,会按照从缓存中读取顶点的顺序,后面读取的顶点会覆盖前面的顶点;因为这样很方便,基本不用计算和判断;即使后面读取的顶点在可视空间内的位置在 先读取顶点的后面。
5-6 完整的渲染管线
所以最后的比较完整详细的渲染管线流程是这个样子。红色方块是基于文章开头的流程图新添加的。
6 左右手坐标系统
webgl规范本身是左手坐标系。但是社区大家伙都习惯基于右手坐标系去做思考、去做开发,所以一些基于webgl的库都对webgl的左手坐标系做了一层封装,使其对使用者表现为右手坐标系,这样我们就可以基于右手坐标系去做开发了。
具体怎么做?
如下,左手坐标系,用左手比划一下,食指指向y轴方向,大拇指指向x轴方向,中指指向的就是z轴方向。在y轴朝上,x轴朝右的情况下,z轴垂直于屏幕指向屏幕内。
这里我们借用物理学中对磁场或电流垂直屏幕(纸面)朝内和朝外的表示方法,既用“圈里面一个叉” 表示垂直于屏幕朝内,用“圈里面一个点”表示垂直于屏幕朝外。
如下,右手坐标系,用右手比划一下,食指指向y轴方向,大拇指指向x轴方向,中指的指向就是z轴的方向。在y轴朝上,x轴朝右的情况下,z轴垂直于屏幕指向屏幕外。
通过上面两幅图,可以得知,在y轴朝向和x轴朝向一致的情况下,左右手坐标系的z轴的朝向是恰好相反的。所以我们基于右手坐标系创建的点,在做完所有的坐标变换之后,最后一步再做一个关于XOY平面的对称变换就可以了(既z值符号取反),就可以完成右手坐标系到左手坐标系的变换。
对称变换矩阵如下:
csharp复制代码[ // 行主序表示
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0,-1, 0],
[0, 0, 0, 1]
]
注意点
- 本篇文章中所使用的着色器语言是GLSL-ES 2.0。WebGL2.0默认支持的是GLSL-ES 3.0的语法,但在threejs中为了编写方便、统一,使用宏定义将3.0语法中区别于2.0语法的相关关键字,映射为了2.0语法中对应的关键字,让使用者可以使用2.0的语法去编写着色器。
- 对于操作透视除。我在学习的过程发现,在有些文章或书本中,它被描述为在顶点着色器之后立马去执行;而在有些文章或书本中,它被描述在图元装配、裁剪之后,光栅化之前去执行(裁剪过程可能会依赖齐次坐标)。我个人自认为,不需要纠结这一点,只需要知道透视除操作在顶点着色器之后、光栅化之前去执行的就可以了,具体在哪个阶段执行,估计不同的标准、不同的硬件厂商会有不同的实现。
写在最后
我在学习渲染管线的过程中发现,很难通过一本书的描述 或 一篇文章的阅读就搞明白渲染管线是怎么回事。通常都需要对比看好多篇文章,看一本以上的书,去多次理解和思考,才能对其有比较深入且深刻的理解。我学习过程中看了这些文章与书中对于渲染管线的描述,也推荐给你,希望对你有所帮助。
- 《WebGL编程指南》
- 《交互式计算机图形学》—— 基于WebGL自顶向下的方法
- learnopengl-cn.github.io/01%20Gettin…
- learnopengl-cn.github.io/01%20Gettin…
- learnopengl-cn.github.io/01%20Gettin…
