图形渲染管线1.0
【技术美术知识储备】图形渲染管线1.0-基本概念&CPU负责的应用阶段
在上一篇中,从渲染分类开始介绍了什么是渲染流水线、为什么要有流水线以及流水线如何进行的,还介绍了CPU主导的应用阶段的四项小阶段。
这一篇的第1和第2小节主要介绍GPU管线的概念,第3小姐介绍几何处理阶段的顶点着色器、可选的曲面细分着色器和几何着色器、图元装配环节的裁剪和屏幕映射。
而这三个概念阶段其实是GPU管线(GPU Pipeline)来实现的,参考参考RTR4中的叙述过程:
在第2章节《The Graphics Rendering Pipeline》中,分别给几何阶段、光栅化和像素处理每个概念阶段划分了功能阶段(functional stage)
几何处理阶段负责绝大部分逐三角形和逐顶点的操作,进一步划分为顶点着色 Vertex Shading、投影 Projection、剪裁 Clipping和屏幕映射 Screen Mapping四个功能阶段,3rd版本还在顶点着色前加了模型视点变换 Model&View Transform功能阶段。
几何处理阶段给定了变换和投影的顶点及其数据,光栅化阶段的目的是找到渲染图元内的所有像素,光栅化进一步划分为三角形设置 Triangel Setup和三角形遍历 Triangle Traversal两个功能阶段。
找到了所有像素后,进入了像素处理阶段,进一步划分为了像素着色 Pixel Shading和像素合并 Merging两个功能阶段。
在第3章节3.2《GPU Pipeline Overview》中将上述三个概念阶段,分为了一系列硬件阶段(hardware stage),具有不同程度的可配置性或可编程性。
Note that these physical stages are split up somewhat differently than the functional stages presented in Chapter 2.注意,这几个物理阶段跟第2章中介绍的功能阶段有所不同。
——《Real-Time Rendering 4th》
而1.1和1.2展示的正是上述所说的功能阶段,整个GPU管线划分成的一系列硬件阶段如下所示:
其中,绿色表示完全可编程,其中实线表示Shader需要开发者自己编程实现,虚线代表是可选的
蓝色表示该阶段完全由GPU固定实现(开发没有控制权)
黄色表示可进行配置但不是可编程的
可编程着色器让GPU管线更加灵活、可控。
注意,以上出现的一些名称几乎是DirectX的命名方式,但OpenGL和DirectX在命名上也稍有不同。例如,DirectX的像素着色器(Pixel Shader)在OpenGL中是片元着色器(Fragment Shader),而片元着色器更加适合描述这一阶段,因为在进行片元着色器阶段时,此时的片元并不是完全意义上的像素。
同样,上述用DirectX描述的输出合并阶段(Merger)在OpenGL中是逐片元操作(Pre-Fragment Operations),一个重在体现阶段的目的,一个重在体现阶段的操作对象。
避免之后混乱,这里统一一下本篇博客之后的说法:
Pixel Shader和Fragment Shader会选择片元着色器Fragment Shader
Merger和Pre-Fragment Operations会选择逐片元操作 Pre-Fragment Operations
之前管线里的说法仍延续,就不更改前面的了。
从第3节开始,逐一对各个硬件阶段进行介绍,很大部分参考了《入门精要》和RTR4的叙述。
就是字面意思,顶点着色器用以完成顶点着色(Vertex Shading)。顶点着色器与后面的片元着色器一起,都是可动态编程的。动态编程的实现由脚本提供,而脚本能写起来是有对应的着色语言支撑的,常见的着色语言有DirectX的HLSL、OpenGL的GLSL等等。
我们知道渲染过程中,除了模型的大小和位置,还需要进行模型好看外观的渲染,这里涉及到材质以及光源下材质展现的光照效果,而渲染中确定材质光照效果的过程就是着色,即Shading。
顶点着色——着色往往在每个顶点需要计算着色方程(Shading Equation),这些着色需要的信息就是在之前的应用阶段CPU为GPU提供的渲染状态里说包含的信息。顶点着色的结果会被输出给后续的环节进行插值等操作。
顶点着色器分为输入和输出两个部分,输入的内容就是CPU传递过来的顶点信息,实现了主要功能后再输出出去。例如OpenGL中输入参数体现为属性(Attribute)、统一值(Uniforms)、采样器(Samplers)等等,输出参数体现为可变变量(Varying),以及gl_Position等等。
主要功能就是基于输入的信息完成坐标变换和逐顶点光照。
GPU对每个输入网格顶点都会调用一次顶点着色器,进行矩阵变换位置,对于坐标变换必须完成的一项工作是——把顶点坐标从模型空间转换到齐次裁剪空间。
o.pos = mul(UNITY_MVP, v.position);
类似以上这句代码,就是在把顶点坐标转换成齐次裁剪坐标系下,进行齐次空间裁剪输出后再由硬件做透视除法(顶点坐标/w)得到归一化设备坐标NDC(Normalized Device Coordinates)下的坐标。看到这里熟悉了吗?没错!这就是101最开始在正交和透视投影那块儿讲到的内容。
除了这一步必须要完成的操作,坐标变换还可以改变顶点的位置,这一步在顶点动画中是非常有用的,例如模拟水面、布料等。
当需要进行光照计算时,顶点着色器还需要计算光照公式生成顶点颜色并得到纹理坐标。
一般的,顶点着色器会把完成坐标变换的位置和纹理坐标这种参数信息发送给片元着色器。除了片元着色器外,还可以把参数信息传递给曲面细分着色器或者几何着色器,也就是上述硬件阶段的绿色部分接下来的两个环节,但这两个环节并不是必须的。
这部分仅简单介绍,百人计划后续会有章节对曲面细分和几何着色器详细的学习:【技术美术百人计划】图形 3.3 曲面细分与几何着色器 大规模草渲染
与必须有的顶点着色器不同,曲面细分着色器是可选的,用于实现渲染图元的细分。将复杂的曲面转换为简单的点、线、三角形等,增加原有模型的顶点(vertex)数量,再用位移贴图(displacement mapping)真实地更改顶点高度信息,从而增加模型细节。
有了曲面细分着色器,不用加载高面数的高模(high-poly),可以实现应用阶段提到的动态LOD技术
还是从模型高、低角度出发,有了曲面细分着色器,我们可以只保存低模的mesh,在需要增加表面细节的时候使用曲面细分着色器就行!这样就不需要占用特别大的内存来储存高面数的模型。
这部分图均来自技术美术百人计划图形3.3中的内容。
曲面细分着色器可以分为:曲面细分控制器(Tessellation Control Shader)、曲面细分引擎(The Tessellation Engine)、曲面细分求值着色器(Tessellation Evaluation Shader)
同理,这部分仅简单介绍,百人计划后续会有章节对曲面细分和几何着色器详细的学习:【技术美术百人计划】图形 3.3 曲面细分与几何着色器 大规模草渲染
与曲面细分着色器一样,几何着色器也是可选的部分。几何着色器主要是用来进行几何的绘制,将图元转换成其他图元,例如:三角形的mesh -> 线框,线框 -> 实体化等等,这些都是曲面细分着色器无法做到的。
但并非所有GPU都支持几何着色,例如一般手机都是不支持几何着色的。
下图是rtr4中的例图:图1是利用GS给球做一些变形,图2是用GS和stream out实现闪电,图3是布料模拟,也是通过GS实现的。
百人计划课程中还提到的其他应用:做一些简单的几何动画,爆破、破碎之类的效果,以及与TSS结合动态控制一片草的密度。
(如下图,来自渲染管线与GPU(Shading前置知识) - 知乎 (zhihu.com))
有些地方把裁剪和后面的屏幕映射一起称为图元装配(Primitive Assembly)环节。
首先咱们再来看看GPU渲染管线的顺序,裁剪阶段和三角形设置、三角形遍历是由GPU的固定功能硬件(fixed-function hardware)实现的,不可编程也不可自行配置,我们没有控制权。
图元与摄像机视野的关系有:
只有全部或者部分在视觉空间内的图元才会被传入后续的阶段并最终渲染到屏幕上。当面对超级大场景时,渲染场景中的全部图元很显然是不可取的,一个常见的想法是——仅渲染在摄像机视野范围内的图元,而在视野范围外的图元就需要被剔除掉,这就是裁剪这一阶段进行的工作。
这部分参考了一篇文章彻底弄懂齐次裁剪 - 知乎 (zhihu.com)
由上述顺序图可以看出,裁剪是发生在透视投影之后,齐次除法或透视除法(顶点坐标/w)之前的操作,裁剪使用的是四维的齐次裁剪坐标。
个人认为,裁剪的范围是在透视除法后确定的,但裁剪这个操作是在透视除法前。
首先再过一遍透视投影、齐次空间和透视除法的概念。
就是顶点着色器中的坐标变换阶段会进行的,透视投影得到的是齐次坐标,往往是(x, y, z, w)这么个形式。
齐次坐标(homogeneous coordinate),它是齐次坐标空间(homogeneous space)下的一个四维矢量(事实上维度可以超出4维),能表示平行线在透视空间的无穷远处交于一点,也就是能把无穷远处的点表达出来:(1, 2) -> (1, 2, 0)。这部分参考了为什么要引入齐次坐标,齐次坐标的意义(一)
将齐次坐标转换为三维坐标,即:坐标/齐次分量w。
普通裁剪就是在进行MVP透视投影后后直接进入透视除法——将三维空间范围直接转换到[-1, 1]³的NDC标准立方体范围。遍历每个顶点,如果有一个顶点超出了这个标准立方体范围,就将这个顶点所在的三角形mesh完全剔除。
这种方法虽然简单粗暴,但由于在透视除法之后进行裁剪,将无法运用线性插值做插值处理,最终展示在屏幕上的图像并不平滑,会有很多突兀的残缺部分。而如果我们选择在齐次空间就进行裁剪,就能运用线性插值做插值处理啦!得到的图像细节部分将有平滑过渡。
参考了: GPU在进行vertex shading之后,rasterization之前,是怎么剪裁的? - 知乎 (zhihu.com)
还记得裁剪之后进行透视除法获得的坐标,是一个NDC下的单位立方体范围内的三维坐标。输入三维的顶点信息后,屏幕映射就是把每个图元的x和y坐标转换到屏幕坐标系(Screen Coordiantes)下。屏幕坐标系是一个二维坐标,与显示画面的分辨率有很大关系,它和z坐标一起构成了窗口坐标系(Window Coordinates)。
如下图所示的屏幕映射过程,不难发现:屏幕映射其实就是一个缩放的过程!而且这个缩放是针对x和y坐标而言的,对z坐标不进行任何操作。
OpenGL支持笛卡尔坐标系,左下角为最小的(0, 0);而微软的DirectX考虑到用户的“上到下、左到右”阅读习惯,定义左上角为最小的(0, 0)。
我们要留意引擎基于的API,在API之间传递时要注意这点差异。