一开始学习计算机图形学的小伙伴们肯定对于渲染管线有一点迷茫,至少当初我就有点迷茫,为了能对后来对计算机图形学感兴趣的萌新小伙伴起到一些帮助,在这里简单讲解一下渲染管线(Render-pipeline)。
该文章还有很多不足有待以后完善。如果大家有什么修改意见或问题,欢迎留言。我会定期解答。谢谢大家的关照。
2020/6/12 更新
最近翻看了一下该篇文章。首先严格意义上该篇文章并没有覆盖所有的管线阶段(细分着色器、光栅化等并没有讲解)。只是将其中最常用,最核心的坐标变换拿了出来(其他部分可以说是不可编程的)。你可以理解该篇文章讲解的是模型等数据是如何通过管线并最终渲染到屏幕上的过程讲解。
2020/6/12 更新
为什么会有渲染管线?渲染管线是用来做什么的?
NVIDIA现场形象展示CPU和GPU工作原理上的区别
在计算机图形学初期并没有管线之类的东西,当时连GPU都没有,绘图是通过CPU进行的。当你需要画一个模型时需要CPU每次访问模型上的一个顶点数据,之后挨个访问一遍即可。可以看出这样做效率会非常低。由于工厂中的生产管线效率理想,从此得到启发出现了以管线图形绘制(因为每一个点其实是独立的可以单独运算)的方法。GPU也是基于此设计的(GPU拥有非常多的小处理器核心,数量远远超过CPU的核心数)。
你要问我渲染管线最基本的用途是什么?我会说:是用于定义GPU上的计算方法和流程的。一个比较直观的例子就是 当我们在一个坐标系中任意定一个点A,任意定一个相机坐标B,现在我想将该点A变换到以B代表的屏幕上(屏幕可能只能识别坐标值为0到1之间的点)该点为C,但是A点可能会有任意位置(也许相机跟本就看不到该点)。那么问题来了:我如何才能通过A和B将A映射到C呢?换句话就是:世界中任意一点如何才能知道该点在我眼睛视平面上的坐标?这就需要坐标变换了,就如该篇文章之后要讲的内容。
2020/9/19 更新
最近再次翻阅了该文章,发现有些矩阵是直接给出的,没有说明前因后果,在这里简要说明一下。
图形学中的大将:矩阵
图形学中为什么要使用矩阵?矩阵是用来做什么的?
其实数学意义上的矩阵用途很多,大学时书上讲可以用来解多元方程组,但是在图形学中矩阵最核心的用法是用来做空间变换(当时得知矩阵还能这么用的我当局拍手叫绝!矩阵还能这么用!?妙!妙!妙!)。
那么问题来了:图形学中一定要使用矩阵来做空间变换吗?
说实话,不用矩阵而使用最常见的列方程也能做到空间变换来达到同样的目的。
那为什么都用矩阵呢?
在某些架构中矩阵能够提高计算效率,特别是在GPU上的矩阵运算会比CPU快很多。(比如使用计算着色器或者CUDA等并行计算)。还有就是矩阵说起来高大上,吹牛逼用的。
什么是空间变换?
空间变换就是在两个不同空间中表示同一个点的各种状态进并通过某些手段修改各种状态。举例说明:三个人并排坐,从左到右分别是A,B,C。现在从A的视角来描述B就是B在A的右边,现在从C的视角来描述B就是B在C的左边。B的位置并没有改变但是从A和C的各自角度看B,一个看到的是右一个看到的是左,发生了不一致现象。其实这就是最核心的坐标变换。这就是我后文说的:计算机图形学的坐标系系统中各个系统之间都是相对的!!!(都是一个点,只是不同的描述方式)
图形学中矩阵一般怎么用?
图形学中矩阵一般是做乘法。这里简单讲解一下平移矩阵和缩放矩阵的原理。
对于平移,假如说点A的位置为(x,y,z),我现在想在x轴平移a个单位,y轴平移b个单位,z轴平移c个单位。最简单的做法是(x+a,y+b,z+c)。聪明的先贤们想出了一个牛逼做法:使用矩阵来平移。用矩阵来表示平移如下:
注:像这种4x4的矩阵在图形学中很常见,叫做齐次坐标矩阵。
为变换后的坐标位置,将该式展开为:
即可得到变换后的坐标(x+a,y+b,z+c)。
对于缩放,假如说点A的位置为(x,y,z),我现在想在x轴缩放a个单位,y轴缩放b个单位,z轴缩放c个单位。最简单的做法是(x*a,y*b,z*c)。使用矩阵表示缩放变换即为:
为变换后的坐标位置,将该式展开为:
即可得到变换后的坐标(x*a,y*b,z*c)。
如果平移之后又缩放了的话,只需要将平移和缩放矩阵相乘即可得到想要的变换矩阵。
平移和缩放只是变换的一部分,还有很多复杂的变换,比如投影变换,旋转变换,错切变换,圆幕变换等。
顺带一提:平移矩阵*缩放矩阵*旋转矩阵是物体空间到世界空间变换的核心。其实图形学中矩阵还有很多细节没有说明比如矩阵相乘的顺序不同变换结果也会不同,网上有很多不错的文章。在这里就不一一讲解了。
物体空间->世界空间->观察空间->裁剪空间->屏幕空间
注意:计算机图形学的坐标系系统中各个系统之间都是相对的!!!(都是一个点,只是不同的描述方式)
1.物体坐标和物体模型(object space<物体空间>)
什么是物体坐标呢?讲理论不如举个栗子!!!嘿嘿~~
一个单独物体的物体坐标
如图:这是3dsMax下建模的一个长度为1的正方体(单位立方体),轴心在物体重心,也就是正中心,右手坐标系(图中的这种坐标系专业一点的名字)。
不难想象A,B,C三点的坐标为A(-0.5,0.5,0.5)B(0.5,0.5,0.5) C(-0.5,-0.5,-0.5),这些A,B,C点和所有该物体的点的集合就是我们说的物体坐标(严格上说是物体上的点相对于自身原点的坐标)。
两个至多个物体的物体坐标
轴心,轴向不变,两个一样的单位正方体,A和A'的物体坐标不难想象A(0.5,0.5,0.5) A'(0.5,0.5,0.5),是的 A点和A'点坐标是一样的,也就是说物体坐标并不会因为轴心和轴的位置不同而发生变化。虽然两个完全一样的物体,物体坐标只是相当于其自身的坐标原点和轴向都在各自的坐标系下,也就是说A点只相对于O点,A'点只相对于O'点。这个概念在游戏里经常会有,你打的怪兽是不是很多都长一个鬼样子?那他们都有各自的物体坐标。这样的坐标系统构成了物体空间
2.世界坐标(World Space<世界空间>)
好了现在我们有了两个正方体,我们只知道这两个正方体的各个对应顶点坐标是一样的(比如前面说到的A和A'点),也就是说画出来这两个正方体是重合的,那我想让其中一个偏离不重合并且有自己的大小,位置和旋转,那么好了,欢迎进入世界空间(World Space)。
好了,什么是世界空间?同样举个栗子(Unity中)!
让我们来看看这两个物体各自的轴心坐标:
1号立方体的轴心(原点)位置 (0,0,0)---Cube1
2号立方体的轴心(原点)位置 (0 ,0,2)---Cube2
Cube1的轴心位于(0,0,0),Cube2是我复制Cube1向Z轴平移两个单位轴心变成(0,0,2),现在我们看看A点和A'点的坐标。
A点:Cube1的轴心位于(0,0,0)。Cube1为单位正方体,各轴向见图右上角,所以A点坐标为(-0.5,0.5,-0.5)。
A'点:Cube2的轴心位于(0,0,0)。Cube2为单位正方体,各轴向见图右上角,所以A'点坐标为(-0.5,0.5,2-0.5)=(-0.5,0.5,1.5)。
你会发现A点坐标不等于A'点坐标,对,你没看错就是不相等,因为这两个物体放到了同一个坐标系下世界坐标系,在该坐标系下的所有点的坐标都相对于世界坐标系的原点(0,0,0),也就是说Cube1的轴心坐标和世界坐标系的轴心重合了,但是这两个物体的自身的物体坐标并没有改变(这是相对于自身轴心,也就是物体空间)。
3.观察空间(View Space)
总览图 俯视图
如图是一个摄像机的观察范围(四棱锥)类似人的眼睛。观察空间就是将世界空间中的坐标变换到以摄像机为轴心计算各个顶点的位置。这个四棱锥我们叫视锥体(view frustum)
4.裁剪空间(clip space)<CVV(canonical view volume)>
什么是裁剪?!
首先假如场景是这样的:
摄像机的视锥体范围:
游戏真正能看到的画面:
你会发现在视锥体外面的东西都被剔除了,这就是裁剪,不渲染看不见的东西。
(1)摄像机有两种:
1>透视摄像机(Perspective)
推导过程:(供参考)
nearClipPalneHeight(近截平面的高)=2*Near*tan(FOV/2)
farClipPlaneHeight(远截平面)=2*Far*tan(FOV/2)
Aspect(摄像机的纵横比)=nearClipPlaneWidth/nearClipPalneHeight=farClipPlaneWidth/farClipPlaneHeight
2>正交摄像机(Orthographic)
推导过程:(供参考)
nearClipPlaneHeight=2*Size
farClipPlaneHeight=nearClipPlaneHeight
Aspect=nearClipPlaneHeight/farClipPlaneWidth=nearClipPlaneHeight/nearClipPalneWidth
5.归一化设备坐标---NDC(Normalized Device Corrdinate)
在这一步会进行一个叫齐次除法的步骤,说白了就是各个点(x,y,z,w)会除以w的值(注:计算机图形学中经常使用四元数代表一个点(叫齐次空间,齐次点等 就是一个名字而已),w没什么特别之处,就是计算矩阵乘法时方便)。
透视裁剪空间到DNC:
正交裁剪空间到DNC:
重点:这样在OpenGL中所有能被摄像机看到的点将会被转换成(-1,1),在DirectX中所有能被摄像机看到的点将会被转换成(0,1)中。为什么要这样做?---为了方便投影到显示屏上!!!
6.屏幕空间(Screen Space)
pixelWidth:屏幕横向分辨率
pixelHeight:屏幕纵向分辨率
OpenGL规范:
DirectX规范:
这个过程就是一个缩放的过程:
screenx={clipx*pixelWidth/(2*clipw)}+pixelWidth/2
screeny={clipy*pixelHeight/(2*clipw)}+pixelHeight/2
上式更加形象的描述:
第一步: -1<clipx/clipw<1--->这是之前的齐次除法的
第二步: 0<{(clipx/clipw)+1}/2<1--->对其加1再除以2化成0到1区间
第三步:对于pixelWidth:{{(clipx/clipw)+1}/2}*pixelWidth= screenx
对于pixelHeight:{{(clipy/clipw)+1}/2}*Height=screeny
和之前推导一样!!!
至此你所想要的东西被绘制在了屏幕上!!!
扩展:
(1)顶点着色器(vertex shader)
将物体从物体空间->世界空间->观察空间->裁剪空间就是顶点着色器的工作。
这之中会转换各种点的坐标,我们在哪运算呢?就在顶点着色器中!!!
顶点着色器:
1>将物体空间的数据(点)作为顶点着色器的输入
2>将所有在自己范围中的点全部遍历一遍,就是每个点都会算进行加工
3>高度可编程配置!!!(这点绝了!!!太棒啦!!!),也就是说这东西绘制成啥样我们可以自定义了!!!
(2)片元着色器(fragment shader)
将裁剪空间中的点从裁剪空间->屏幕空间就是片元着色器的工作。
屏幕映射,就是之前说的第六步不是我们做,是显卡固定好了的算法。片元着色器是计算每个像素的颜色的。如果看过相关代码,你会发现片元着色器会返回一个四元数-(r,g,b,a)->分别为(red<红>,green<绿>,blue<蓝>,alpha<透明度>).
什么是片元(fragment)?片元是一种状态,刚开始显示器上的像素点是不知道自己的颜色的,每个像素点就像一个空白的格子等着我们上颜色,类似于还没有装蜂蜜的蜂巢。系统中,我们之所以会看到桌面是因为系统已经给显卡初始化了像素颜色。在任务管理器中能找到。
片元着色器:
1>将裁减空间的数据(点)作为片元着色器的输入
2>将所有在自己范围中的像素全部遍历一遍(三角遍历<Rasterizer>---Triangle Traversal),就是每个片元(像素)都会运算进行加工。
3>高度可编程配置!!!(太棒啦!!!)
===============================================
让我们看看一个简单的栗子!Unity中是怎么做的:
Shader "Test/Shader"
{
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert//告诉编译器 顶点着色器叫什么名字
#pragma fragment frag//告诉编译器 片元着色器叫什么名字
#include "UnityCG.cginc"//包含内置文件,方便写代码
struct appdata
{
float4 vertex : POSITION;//物体坐标
};
struct v2f
{
float4 vertex : SV_POSITION;//裁剪空间坐标
};
v2f vert (appdata v)//顶点着色器,以物体坐标为输入(appdata下的vertex)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);//将物体坐标变换到裁剪空间
return o;//返回裁剪空间的数据
}
fixed4 frag (v2f i) : SV_Target//片元着色器,以裁剪空间数据作为输入(上面顶点着色器的输出)
{
fixed4 col = fixed4(1,1,1,1);//定义一个白色
return col;//返回白色
}
ENDCG
}
}
}
注意32行:其调用了内置函数:
UnityObjectToClipPos()
其定义如下:(在Unity/Editor/Data/CGInclude/UnityShaderUtilities.cginc)
inline float4 UnityObjectToClipPos(in float3 pos)
{
// More efficient than computing M*VP matrix product
return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));
}
inline float4 UnityObjectToClipPos(float4 pos) // overload for float4; avoids "implicit truncation" warning for existing shaders
{
return UnityObjectToClipPos(pos.xyz);
}
return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)))中mul(unity_ObjectToWorld, float4(pos, 1.0))将从物体空间变换到世界空间,unity_ObjectToWorld是物体空间到世界空间的转换矩阵,mul()是矩阵乘法内置函数。之后再乘以UNITY_MATRIX_VP(观察空间和裁剪空间合一起了),乘完后将从世界空间变换到裁剪空间。
完全手动自定义计算的话,这么写:
float4 UnityObjectToClipPos(in float3 pos)
{
float4 objectSpaceData = float4(pos, 1.0f);
float4 worldSpaceData = mul(unity_ObjectToWorld, objectSpaceData);
float4 viewSpaceData = mul(UNITY_MATRIX_V, worldSpaceData);
float4 clipSpaceData = mul(UNITY_MATRIX_P,viewSpaceData );
return clipSpaceData;
}
参考文献:1.《Unity Shader 入门精要》冯乐乐---人民邮电出版社---2017年6月第一版
2.《Real-Time Rendering third edition》Tomas Akenine-Moller,Eric Haines,Naty HoffMan