第二章 图形渲染管道
(1)渲染管道可分为哪四个阶段? 一是应用阶段,二是几何处理阶段,三是光栅化阶段,四是着色阶段(Pixel Processing)。
(2)渲染管道分为几个阶段,每个阶段主要做什么? 渲染管道主要分为应用程序阶段、几何处理阶段、光栅化阶段段。碰撞检测、动画、物理模拟主要用于应用阶段。在几何处理阶段,主要决定在哪里绘制哪些物体。在光栅阶段,三个顶点形成的三角形中主要判断哪些像素。在着色阶段,主要是对每个像素进行着色,并进行深度测试来判断片元是否可见。
(3)几何处理阶段主要做什么? 主要分为四点处理、投影、裁剪、屏幕映射四部分。
(4)几何处理阶段可以做额外的事情? 按顺序细分,几何着色器和stream output。
(5)光栅化阶段主要做什么? 光栅化阶段主要有两部分。第一部分是Triangle Setup,该功能是计算下一部分操作所需的数据,如微分计算,edge function计算等。第二部分是Triangle Traversal,三角形中主要确定采样点或像素。
(6)着色阶段(Piexl Processing)该怎么办? 第一部分是像素着色阶段(Piexl Shading),光照计算等着色操作主要用于每个像素。第二部分是Merge阶段(merge 阶段), 混合操作和z-test。
(7)双缓冲(double buffer)的作用? 采用双缓冲防止渲染管染Merging阶段的Color buffer用户可以看到,整个场景是在离屏缓冲中绘制的。画完后,垂直扫描前后缓冲(vertical retrace)交换角色。
PS:交换双缓冲时,用户需要保持前缓冲,后缓冲需要保持数据不变,因此系统在此期间无法访问缓冲。解决此问题请参见图形硬件一章(23章)中三个缓冲的介绍。
第三章 图形处理单元
(1)单指令单数据(SISD)多数据与单指令(SIMD)的区别?
以加法指令为例,单指令单数据(SISD)的CPU翻译加法指令后,执行部件首先访问内存,获得第一个操作数;然后再访问内存,获得第二个操作数;然后进行求和操作。SIMD型的CPU在指令翻译后,几个执行部件同时访问内存,并一次获得所有操作数。
(2)GPU中的warp与thread? 通过计算单个顶点、像素或片元等元素thread来完成,和CPU中的thread不同,GPU中的thread存储输入数据和着色器执行所需的寄存器空间包含一些内存。多个使用相同的着色器程序thread小组执行后称为warp(NVIDIA叫法,在AMD中为wavefronts)。
(3)warp读取内存时执行的操作? 当一个warp当需要读取内存时,warp中所有的thread它们都会停止,因为它们执行相同的指令。读取内存意味着它warp拥塞正在等待它的结果。此时,系统将切换到另一个warp继续执行。因为两个warp之间没有数据交互,所以切换到另一个warp不会消耗时间。
(4)影响SIMD执行效率的几个因素? 第一个因素是着色器程序结构,因为寄存器总量有限,所以着色程序thread所需的寄存器越多,所以threads数量越少,留下来的数量就越少GPU中warp数量会越少。过少的warp这意味着当发生拥塞时,GPU空闲可能不再存在warp,也不能靠切换warp(switch)减轻问题。
另一个因素是动态分支,因为每个因素warp执行的指令是一样的,如果应该的话warp的所有thread执行相同的分支,那么warp如果有一个,你不需要考虑其他分支thread如果实施了不同的分支,整个分支就会实施warp中所有其他thread都需要遍历这些分支。
(5)渲染管道各阶段的可编程度? 渲染管道中可编程的部分是顶点着色器、细分、几何着色器和片元着色器(Piexl Shader)。细分和几何着色器为可选编程,并非所有GPU都支持这两部分,比如移动设备。
可设置状态的部分是屏幕映射和merge,完全不可编程的部分是切割和光栅。后续可以送到光栅阶段,也可以作为非图形,而不是后续的光栅。stream processor。处理后的数据可以再次发回管道,对流体模拟或其他粒子有效。
(5)stream output阶段的作用? 顶点处理阶段结束后,可以选择以有序数组的方式将输出的顶点送到stream中。后续Stream output 阶段让GPU它可以用作几何引擎。在这个阶段,数据可以来自数组CPU使用,也可以再给GPU使用。这个阶段可以用来做物理模拟。
stream out返回的数据只是浮点形式,所以要注意内存消耗。由于stream out它作用于三角形图元,而不是顶点,因此原始网格的拓扑关系将消失。因此,我们发送到渲染管道的数据都是点集。在OpenGL中,stream out阶段被称作transform feedback。图元数据的输出顺序与输入顺序相同。
(6)如何避免数据race condition? 数据的race condition也就是说,多个着色程序竞争影响一些值,导致结果不确定。GPU这个问题是通过一个着色器独特的原子单元来避免的,但原子也意味着另一个着色器在访问一个正在修改的内存时会堵塞。
(7)简述ROVs(Rasterizer order views)? ROVs在DX11.引入,它指定了程序执行的顺序,无论片元着色器计算结果的顺序如何,ROVs将结果按三角形输入顺序发送到merge stage。这有利于半透明物体的渲染。ROVs和UAVs很像,不过ROVs指定数据被访问的顺序。ROVs让开发者定义自己blend 方法,因为ROVs任何地方都可以访问和修改,不需要merge阶段了。ROVs如果遇到无序的访问,代价是pixel shader在当前顺序之前,所有调用执行可能总是被拥塞。
(8)early-z算法? 假如片元着色器(Piexl Shader)计算出fragment后发现这个fragment无法通过z-test,所以片元着色器中的所有计算都是无效的。为了避免这种浪费,很多GPU在片元着色器执行前进行深度测试。
第四章 变换
(1)如何在任何点绕pz轴旋转 ? \phi ?? X = T ( p ) R z ( ? ) T ( ? p ) X=T(p)R_z(\phi)T(-p) X=T(p)Rz(ϕ)T(−p) 首先移动到原点 T ( − p ) T(-p) T(−p),再旋转 ϕ \phi ϕ,最后移动回去即可 T ( p ) T(p) T(p)。
(2)图形系统中的常用变换顺序? 因为矩阵乘法运算不满足交换律,所以不同运算顺序对结果的作用也不同。在图形系统中常用的交换顺序为C=TRS。即TRSp=T(R(S§))。虽然矩阵乘法运算不满足交换律,但满足结合律,也就是TRSp=(TR)(Sp)。
(3)OpenGL中lookAt函数使用的矩阵推导? M = [ r x r y r z 0 u x u y u z 0 v x v y v z 0 0 0 0 1 ] [ 1 0 0 − t x 0 1 0 − t y 0 0 1 − t z 0 0 0 1 ] = [ r x r y r z − t r u x u y u z − t u v x v y v z − t v 0 0 0 1 ] M=\begin{bmatrix} rx & ry &rz&0\\ ux & uy &uz&0\\vx & vy &vz &0\\0&0&0&1\end{bmatrix}\begin{bmatrix} 1 & 0 &0&-t_x\\ 0 & 1 &0&-t_y\\0 & 0 &1 &-t_z\\0&0&0&1\end{bmatrix}=\begin{bmatrix} rx & ry &rz&-tr\\ ux & uy &uz&-tu\\vx & vy &vz &-tv\\0&0&0&1\end{bmatrix} M=⎣⎢⎢⎡rxuxvx0ryuyvy0rzuzvz00001⎦⎥⎥⎤⎣⎢⎢⎡100001000010−tx−ty−tz1⎦⎥⎥⎤=⎣⎢⎢⎡rxuxvx0ryuyvy0rzuzvz0−tr−tu−tv1⎦⎥⎥⎤
为了从世界空间变换到相机空间,首先进行平移,将世界空间坐标变换到以相机为中心,之后进行基向量的变换,将 X = [ x , y , z ] X=[x,y,z] X=[x,y,z]变换到以r,u,v为基向量的坐标系中。
[ r x r y r z u x u y u z v x v y v z ] [ x y z ] = r X + u X + v X \begin{bmatrix} rx & ry &rz\\ ux & uy &uz\\vx & vy &vz \end{bmatrix}\begin{bmatrix} x\\ y\\z \end{bmatrix}=rX+uX+vX ⎣⎡rxuxvxryuyvyrzuzvz⎦⎤⎣⎡xyz⎦⎤=rX+uX+vX
(4)如何正确的变换normal? 原始的做法是使用变换矩阵的伴随矩阵的转置,又因为矩阵的逆等于伴随矩阵除以原矩阵的行列式,所以计算矩阵的逆的转置即可。当然,如果行列式为0便没有逆矩阵了。
计算伴随矩阵的消耗很大,并且这不是必要的步骤。因为normal是一个向量,所以平移对它是没有影响的,只需要计算变换矩阵左上角的3X3的伴随矩阵即可。
甚至这个伴随矩阵也不需要计算,因为transform可以分解为平移旋转缩放,平移对法线没有影响,均匀的缩放只影响法线的长度,所以只需要考虑旋转矩阵了。旋转矩阵是正交阵,其转置就是它的逆矩阵,所以它的逆矩阵的转置还是它本身。
综上所述,对于均匀缩放,只需要乘上原始的矩阵即可,最后通过归一化或者除上缩放比例就得到了正确的法线。对于没有缩放的变换,就不需要第二步。
对于非均匀缩放,则需要进行下面的操作 Normal = mat3(transpose(inverse(model))) * Normal;
(5)渲染管线中计算逆矩阵的几种技巧? 1.如果矩阵是一系列的简单变换组合成的,它的逆矩阵可以通过翻转参数得到,即 M = T ( t ) R ( ϕ ) M=T(t)R(\phi) M=T(t)R(ϕ), M − 1 = R ( − ϕ ) T ( − t ) M^{-1}=R(-\phi)T(-t) M−1=R(−ϕ)T(−t) 2.如果矩阵是正交阵,那么它的逆就等于它的转置。比如,一系列仅有旋转变换组成的矩阵还是正交阵。 3.如果上述方法都不行,可以采用的是克莱姆法则或伴随矩阵方法,因为它们涉及的if分支更少。 4.如果逆矩阵是为了变换向量,那么只需要左上3x3矩阵的逆矩阵即可,因为平移对向量没有影响。
(6)如何根据深度缓冲中的值还原出它在相机空间中的z值? OpenGl中的透视矩阵如下所示 透视投影带来的结果就是深度值不再是线性的了,将该矩阵作用于相机空间中的p点,得到: 其中 再除以w后就得到下面的式子,在OpenGL中 Z N D C Z_{NDC} ZNDC属于 [ − 1 , 1 ] [-1,1] [−1,1] 以上推导给出了深度缓冲中的z值与相机空间中的z值的关系。
首先将深度缓冲从[0,1]变换到[-1,1],得到NDC空间中的值 z N D C = d e p t h × 2 − 1 z_{NDC}=depth\times2-1 zNDC=depth×2−1 再将 z N D C z_{NDC} zNDC的值带入上式 p z = 2 f n Z N D C ( f − n ) + ( f + n ) p_z=\frac{2fn}{Z_{NDC}(f-n)+(f+n)} pz=ZNDC(f−n)+(f+n)2fn
(7)骨骼蒙皮算法? 骨骼蒙皮算法主要分为三步,第一步是将模型空间中的某个顶点变换到关联的所有关节空间中,在此之后,顶点在关节空间的坐标保持不变。第二步是移动关节到当前姿势,第三步是将顶点变换到模型空间,并加权形成新的顶点。
整个流程的空间变换为模型空间->关节空间->模型空间。就类似于先将世界空间中的顶点变换到相机空间,在相机空间内进行变换,之后再回到世界空间,不同的是,骨骼蒙皮算法的一个顶点要绑定到多个关节。
在该算法中,每个顶点需要存储要绑定的关节索引,一般存储四个,因为超出四个之后效果提升就不是很大了。对于每个绑定的关节,同时存储一个权重因子,以表示该关节对最终顶点的影响。
第五章 着色基础
(1)走样产生的几种类型? 主要分为三种类型,第一种是几何走样,它是由于对几何图形的可见性函数采样不足导致。第二种是着色走样,它是由于着色阶段对连续的着色函数采样不足导致的。第三种是时间走样,它由于渲染帧率的限制使其对运动过程的采样不足导致的走样。比如车轮效应。可以采用动态模糊解决。
(2)各种基本的抗锯齿算法及其变种算法? 第一种是全屏反走样(SSAA||FSAA),它的变种算法为HRAA。第二种是多采样抗锯齿(MSAA),它的变种算法为英伟达的CSAA和AMD的EQAA。第三种是形态反走样,它的变种算法为SRAA、SMAA、FXAA。除此之外还有时间抗锯齿(TAA)等方法。
(3)超采样反走样(全屏反走样SSAA||FSAA)? FSAA对场景以一个更高分辨率进行渲染,然后对相邻的采样点去平均值得到最终图像。对相邻点的采样有许多方式,英伟达的高分辨率反走样(HRAA)就是在FSAA的基础上采用五点采样模式得到的。该类算法解决了几何走样与着色走样。
(4)多采样抗锯齿(MSAA)? MSAA的思想是将可见性测试分离出来,每个像素关联着多个子采样点,每个子采样点都存储着对应的深度值,模板值和颜色值。不过,对于颜色值,如果三角形覆盖了一个像素中的多个采样点,该像素只会进行一次着色计算,并将计算结果复制给每个被覆盖的采样点。
对于每个通过深度和模板测试的子采样点,其深度和模板值将被写入到缓冲区。而颜色值则为着色器计算出的结果乘以该像素点的覆盖率,覆盖率是由几何图形所占区域中所有可见的子采样点与总采样点之比。
(5)MSAA与FSAA对比? 相对于全屏反走样,MSAA在着色阶段进行的计算更少,因而性能更好。全屏反走样可以解决几何和着色走样,而MSAA只可解决几何走样。
(6)MSAA中的质心采样? 在MSAA中,像素着色器根据像素点中心的位置计算颜色值。如果一个三角形在某个像素内的子采样点没有覆盖像素中心,那么着色计算就会得出错误的结果。为了解决该问题,像素着色器使用的采样位置可以调整到覆盖区域中某个点的位置,这种技术就被称作质心采样。对于一个被几何图形覆盖的像素点,它首先从像素中心寻找,如果中心点没被几何图形覆盖,则向外延伸直到找到一个被覆盖的子采样点,以该点的位置作为着色计算时的位置。
(7)EQAA和CSAA相对于MSAA的改进思路? MSAA将覆盖率与深度值放在一起,将颜色值计算分开。前面两种算法将覆盖率、深度值、颜色值都分开计算
(8)CSAA? MSAA相对于FSAA减少了计算量,但是内存占用并没有减少,为了准确计算覆盖率,需要许多子采样点,每个子采样点都需要存储深度值和颜色值。
CSAA使用一个二进制结构的数组蒙版表示覆盖率,这个覆盖率比子采样点具有更高的分辨率。在光栅化阶段,光栅器首先投影几何图形到该蒙版以计算覆盖率,所有被覆盖的子采样点共用同一覆盖率。然后对子采样点计算深度,模板以及颜色值。
CSAA将覆盖率单独计算,从而可以使用更少的采样点得到更准确的覆盖率,并同时降低了内存占用。
(9)EQAA? 在MSAA中四个采样点都需要存储颜色值和深度值。EQAA的2f4x模式只存储两个颜色值和深度值来供四个采样点使用。深度值和颜色值不再存储附在四个采样点上而是用一个专门的表存储,原来的四个采样点则存储对应表的索引。如果一个像素覆盖超过三种颜色,则表中一项及其对应采样点索引会被删除。
(10)形态学反走样(MLAA)?
MLAA主要包含三步, 1.寻找给定图像中不连续的像素。 2.确定边缘类型。 3.对周围临近的像素按照混合权重进行混合计算求出轮廓上像素的颜色值。
边缘线段可以被分成LZU三种类型。一条直线可能是多个像素的边,对于起点和终点,它们所在的线段和这条直线是重合的,连接起点和终点所在线段的终点就得到了轮廓线。轮廓线经过的每个像素都被分成两个题型,根据这两个梯形的面积计算覆盖率,并确定最终颜色。
从本质上讲,MLAA以 图像处理的方式进行反走样,它并没有对几何走样或着色走样做出解决,而是近似抗锯齿后的图像,这种特性使得它可以与其他抗锯齿算法结合到渲染管线中。
(11)时间抗锯齿(TAA)? TAA的思路是将采样点分布到多个帧中,可以解决几何走样和着色走样,这使得它几乎呈现和全屏反走样(FSAA)一样的效果。
为了获得多个采样点,在不同的帧中相同的像素应当使用一个抖动操作,以使当前帧的像素被移动到一个超采样中子采样的位置。抖动操作一般发生在顶点着色器中,通过直接修改投影矩阵,将原本正常的位于像素点中心的采样点修正到偏移位置。
Project[2][0]+=(SampleX*2.0-1.0)/ViewRect.Width();
Project[2][1]+=(SampleY*2.0-1.0)/ViewRect.Height();
最后对这些子采样点求加权平均值: s t = 1 n Σ k = 0 n − 1 x t − k s_t=\frac{1}{n}\Sigma_{k=0}^{n-1}x_{t-k} st