第二章 图形渲染管线

  1. 请简述一下图形渲染管线流程。
  2. 请描述和推导下MVP变换(特别是投影变换)

 

 

  1. 渲染管线:

 

最好能达到口述:渲染管线的主要功能是在给定场景,在物体,相机,光源等等条件下,生成一幅二维图像的过程。在概念上可以分为3个阶段:应用阶段、几何阶段和光栅化阶段。

 应用阶段一般是cpu与内存的交互,应用阶段的主要任务是准备好场景数据,将需要绘制的图元输入到几何阶段。场景的数据很多,包括摄像机位置,场景中包含的模型,模型的顶点数据和法线等等。

几何阶段主要负责大部分多边形操作和顶点操作,包括顶点变换,顶点着色,裁剪,屏幕映射。顶点变换就是我们常说的MVP变换,由模型空间变换到世界坐标,由世界空间变换到观察坐标空间。由观察坐标空间变换到裁剪坐标空间,这个观察变换可以有两种投影方式,一个是正交投影,一个是透视投影。坐标变换到一个CVV立方体,然后进行CVV裁剪(和w比较)。然后进行透视除法(除以w),变换后得到NDC坐标。接着我们进行屏幕映射,因为像素是整数,在映射的时候四舍五入。当然这其中还有一个功能是顶点着色,顶点着色可以在世界空间进行,也可以在透视投影之后·,这都是可以的,只要确保顶点和法线是在同一坐标系,而写shaderlab的时候是习惯进行了mvp变换之后再进行着色计算。

光栅化决定哪些像素被集合图元覆盖,光栅化包括三角形设置,像素着色阶段,融合阶段。

三角形设置算法,或者说画线算法和图元填充算法有DDA、Bresenham画线算法;区域图元填充算法,扫描线多边形填充算法等等。(这部分下周再补充,暂时时间不足)。三角形设置完成后进入像素着色阶段,也就是fragment shader,包括贴图,光照处理,可见性等都可以进一步处理。融合阶段主要任务是合成当前储存在缓冲器中的由之前的像素着色阶段产生的片段颜色,当然还负责包括可见性问题的处理。主要有Z缓冲,模版缓冲,alpha测试(可以在着色器中进行),累积缓冲(类似于OnRenderImage后处理)

光栅化完成之后把处理结果发到后置缓冲中,通过交换链把他交换到前置缓冲中,最后在屏幕中显示。

 

 

延伸的问题

  1. 法线是如何变换的?
  2. 裁剪为什么是在投影变换之后,在投影变换之前不是更好吗?
  3. 为什么要进行透视除法,变换到NDC空间?
  4. CVV裁剪是如何进行裁剪的?
  5. 屏幕映射是如何映射的?
  6. 光照在顶点着色器中进行和在像素着色器中进行有什么不同之处?
  7. 多线程渲染?OpenGL里可以使用多线程吗?

 

  1. 法线的变换:https://blog.csdn.net/qq_34552886/article/details/89086206
  2. 裁剪为什么是在投影变换之后?(个人理解)

投影变换前为视锥体,如果在视锥体中进行裁剪的话,因为不同的投影变换前的视锥体可能是不一样的,那么得针对不同的视锥体进行裁剪计算,而且得用AABB包围盒的方法来确定模型是否全部包含在视锥体内,计算量大,情况复杂。而用CVV进行裁剪的话,因为CVV确定,而且经过投影变换后我们只需要与W值进行比较即可确定是否在CVV空间内,计算量小方法简单。

  1. 透视除法和NDC空间

透视除法是为了获取真正的Z值,因为在经过透视投影后,Z的值的线形关系都存储在W分量,而Z的值会转变为近裁面N的值。经过透视除法后我们重新获得Z的线形关系,而且由齐次坐标转变为3维坐标,变到了NDC空间

NDC空间:主要在于我们使用不同的设备,分辨率可能都不一样,实际在写shader的时候,没办法根据分辨率进行调整,而通过这样一个空间,把x,y映射到(-1,1)区间,z映射到(0,1)区间(OpenGL是(-1,1)),在下一步屏幕坐标映射时再根据屏幕分辨率生成像素真正应该在的位置,这样可以省掉很多设备适配的问题,让我们在写shader的时候一般不需要考虑屏幕分辨率的问题。

  1. CVV如何进行裁剪:后续补充公式推导。敬请期待= =
  2. 屏幕是如何进行映射的?

我们在NDC空间下所获取到的图像是(-1,1),而我们的分辨率如果是255x255的话就分别进行线形插值对应,要注意的是在因为像素是整数的,我们转化为像素值的时候必须四舍五入。这里可以引入一个锯齿的问题(个人理解),比如我们有一张255x255的贴图,而我们的分辨率只有100x100,那么这个时候就会产生像素的叠加的问题,也就是说会产生锯齿,一般抗锯齿的方法是提高分辨率或者进行像素的平滑smooth处理。

  1. 光照在顶点着色器计算和像素着色器计算有什么异同点?

在顶点着色光照中,我们的每个顶点上计算光照,然后在渲染图元内部进行插值,最后输出像素颜色。顶点计算量往往比像素计算量小。但是,由于顶点光照依赖于线性插值,当光照模型中有非线形计算(例如高光反射,为什么高光不是线性的呢?这里可以去看伽马空间和线性空间的区别),顶点光照就会出问题。而且由于顶点光照会在渲染图元内部对顶点颜色进行插值,所以导致渲染图元内部的颜色总是暗于顶点处的最高颜色值。会产生明显的棱角。具体差别可参考高洛德着色(点的插值计算)和冯氏着色(点的法向量是通过顶点的法向量插值得到的)。

  1. 多线程渲染问题

OpenGL可以使用多线程,但是不能一个上下文是使用多线程。应该在一个dc下创建多个上下文,然后用sharedlist进行上下文共享达到多线程效果。。(坑很多,有些我也不太了解的,后续补充)。最近补充,有些博客上写的是OpenGL并不能多线程,所有诞生了Vulkan图形API这个东西,VulKan还在学习当中。待补充

 

 

 

2、请描述和推导下MVP变换(特别是投影变换)

Unity中的 UNITY_MATRIX_MVP 矩阵表示的是从模型到裁剪坐标的矩阵变换,Model Matrix ● View Matrix ● Projection Matrix。在Unity2017中使用 UnityObjectToClipPos 进行了替换,MVP也即是 模型(M)、视图(V)、透视(P)三个单词的首字母简写。

模型(M):如果把矩阵的行解释为坐标系的基向量,那么乘以该矩阵就相当于做了一次坐标变换,vM = w,称之为M将v变换到w。用基向量[1,0,0]与任意矩阵M相乘,得到[m11,m12,m13],得出的结论是矩阵的每一行都可以解释为转化后的基向量。根据该结论,就可以通过一个期望的变换,反向构造出一个矩阵来代表这个变换。首先来看一下MVP的M,也就是模型空间转世界空间的变换,这个变换也是分为三个子变换,分别是缩放,旋转,平移。下面分别推导一下几个变换的矩阵。

在unity视图中,当我们对一个物体进行旋转,平移,缩放的时候,其实就是对物体的模型空间进行了一系列操作,最后通过模型变换而在世界中表现。

 

 

 

上面当然可以进一步把缩放和旋转拓展到齐次坐标。。。

 

视图(V):

上一阶段,我们把顶点从模型空间转换到了世界空间,但是有一个很重要的问题,相机才是我们观察世界的窗口,后续的投影,裁剪,深度等如果都在世界空间做的话就太复杂了,如果我们把这些操作的坐标原点改为相机,就会大大降低后续操作的复杂性。所以,下一步就是如何将一个世界空间的对象转化到相机空间。要定义一个相机,首先要有相机位置,还要有相机看的方向。给一个相机注视点以及一个控制相机Y轴方向的向量,也就是UVN相机模型,UVN比较容易定义相机的朝向,类似LookAt的功能,所以这里我们采用这种方式定义我们的相机位置和朝向。给定了相机位置和注视点位置,我们就能求得视方向向量N,然后我们根据给定的上向量V(只是输入临时用的,不是最终的V,输入的V在VN平面没有权重,因为N已经确定了视方向,只有UV平面上可能有权重,所以需要重新计算一个仅影响Roll的角),通过向量叉乘得到一个Cross(N,V)得到右向量U,最终我们再用Cross(N,U)得到真正的V,三者都需要Normalize。这样,我们就构建出了相机所在的空间基准坐标系。

 

我们要的是WorldToCamera的变换,不过,对一个点做一个变换,相当于对其所在坐标系做逆变换,所以我们只要求出整体变换的逆矩阵即可。比如上面的变换称之为WTC,上面的变换包括一个旋转R和平移T,那么我们要求的WTC^-1 (表示逆)=(RT)^-1 = (T^-1)( R^-1)。我们拆开来看:

 

先是旋转矩阵的逆,其实上面的计算,我们构建的UVN就是R矩阵了(把相机转换到世界空间,但是在第四行没有分量,换句话说就是3X3的矩阵,没有位移,也没有缩放,那么就只有旋转),下面一步就是求R的逆。矩阵正常求逆的运算是很费的,所以一般来说要避免直接求逆,因为3X3的旋转矩阵其实是一个正交矩阵(各行各列都是单位向量,并且两两正交,可以把上一节的旋转矩阵每个看一遍,抽出左上角3X3部分)。正交矩阵的重要性质就是MM^T (转置)= E(单位矩阵),进一步推导就是M ^ T = M ^ -1,所以上面的旋转矩阵的逆实际上就是它的转置。转置的话,我们只需要沿着对角线把数据互换一下,计算量比起求逆要小得多。 

 

接下来是平移矩阵的逆,这个其实不需要去推导,比如向量按照(tx,ty,tz)进行了平移变换,那么对它的逆变换其实就是取反(-tx,-ty,-tz)。

 

最终两个矩阵以及相乘结果如下,其中T = (tx,ty,tz),UVN同理:

https://i-blog.csdnimg.cn/blog_migrate/574b9ecebcee3e88dc2bb8dbf748224f.png

 

我们直接结果构建最终的矩阵,避免多一次矩阵运算,代码如下:

Matrix ApcDevice::GenCameraMatrix(const Vector3& eyePos, const Vector3& lookPos, const Vector3& upAxis)

{

       Vector3 lookDir = lookPos - eyePos;

       lookDir.Normalize();

 

       Vector3 rightDir = Vector3::Cross(upAxis, lookDir);

       rightDir.Normalize();

 

       Vector3 upDir = Vector3::Cross(lookDir, rightDir);

       upDir.Normalize();

 

       //构建一个坐标系,将vector转化到该坐标系,相当于对坐标系进行逆变换

       //C = RT,C^-1 = (RT)^-1 = (T^-1) * (R^-1),Translate矩阵逆矩阵直接对x,y,z取反即可;R矩阵为正交矩阵,故T^-1 = Transpose(T)

       //最终Camera矩阵为(T^-1) * Transpose(T),此处可以直接给出矩阵乘法后的结果,减少运行时计算

       float transX = -Vector3::Dot(rightDir, eyePos);

       float transY = -Vector3::Dot(upDir, eyePos);

       float transZ = -Vector3::Dot(lookDir, eyePos);

       Matrix m;

       m.value[0][0] = rightDir.x;  m.value[0][1] = upDir.x;  m.value[0][2] = lookDir.x;  m.value[0][3] = 0;

       m.value[1][0] = rightDir.y;      m.value[1][1] = upDir.y;  m.value[1][2] = lookDir.y;  m.value[1][3] = 0;

       m.value[2][0] = rightDir.z;  m.value[2][1] = upDir.z;  m.value[2][2] = lookDir.z;  m.value[2][3] = 0;

       m.value[3][0] = transX;           m.value[3][1] = transY;   m.value[3][2] = transZ;     m.value[3][3] = 1;

       return m;

}

 

透视(P):

FOV角度,N近裁剪面,F远裁剪面,Aspect屏幕宽高比即可,如下图:

 

 

https://i-blog.csdnimg.cn/blog_migrate/284ef53d094cbf7ede8c1b2e12e40373.png

 

那么BF也就是的高度就是tan(0.5*fov)* N 最终的H = 2 tan(0.5fov)*N,最终的W = Aspect * H。带入上述矩阵:

 

2N/H = 2N/2tan(0.5fov)N = 1/tan(0.5fov) = cot(0.5fov)

 

2N/W = 1/(Aspect * tan(0.5fov)) = cot(0.5fov)/Aspect

 

最终矩阵结果如下,暂且还是以tan表示

https://i-blog.csdnimg.cn/blog_migrate/5da96810d377b4a64e43c606898c756d.png

Matrix ApcDevice::GenProjectionMatrix(float fov, float aspect, float nearPanel, float farPanel)

{

       float tanValue = tan(0.5f * fov * 3.1415 / 180);

 

       Matrix proj;

       proj.value[0][0] = 1.0f / (tanValue * aspect);

       proj.value[1][1] = 1.0f / (tanValue);

       proj.value[2][2] = farPanel / (farPanel - nearPanel);

       proj.value[3][2] = -nearPanel * farPanel / (farPanel - nearPanel);

       proj.value[2][3] = 1;

       return proj;

}            

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值