107-【八股】引擎&图形学

图形学

渲染管线

渲染管线的构成

在概念上可以将图形渲染管线分为四个阶段:

  1. 应用程序阶段:在CPU端完成,该阶段主要是在软件层面上执行的一些工作,包括空间加速算法、视锥剔除、碰撞检测、动画物理模拟等。

    大体逻辑是,执行视锥剔除,查询出可能需要绘制的图元并生成渲染数据,设置渲染状态和绑定各种Shader参数,调用DrawCall,交给GPU渲染。

  2. 几何处理阶段:负责大部分多边形操作和顶点操作,将三维空间的数据转换为二维空间的数据。

    1. 顶点处理阶段:执行顶点变换和着色的工作,通过MVP矩阵将顶点从局部空间转换到屏幕裁剪空间,方便后续转为NDC坐标。

      还可以进行顶点着色计算,如Flat shading和Gouraud Shading。

      之后有一些可选阶段,例如曲面细分等。

    2. 裁剪阶段:裁剪掉不在屏幕内部的图元,由硬件控制。

      在CPU已经视锥体剔除了,为什么这里还要裁剪?

      主要是两次裁剪的粒度不同。CPU端的视锥体剔除是根据物体包围盒是否在视锥体内,针对整个物体裁剪;而这里的裁剪则是针对图元单位裁剪。

    3. 屏幕映射阶段:将之前步骤得到的坐标映射为标准屏幕坐标NDC。

  3. 光栅化阶段:将图元离散化成片段的过程.

    1. 三角形设置:计算出三角形的一些重要数据(如三条边的方程、深度值等)以供三角形遍历阶段使用,这些数据同样可用于各种着色数据的插值。
    2. 三角形遍历:找到哪些像素被三角形所覆盖,并对这些像素的属性值进行插值。通过判断像素的中心采样点是否被三角形覆盖来决定该像素是否要生成片段。通过三角形三个顶点的属性数据,插值得到每个像素的属性值。此外透视校正插值也在这个阶段执行。
  4. 像素处理阶段:给每一个像素正确配色,最后绘制出整幅图。

    1. 像素着色:进行光照计算和阴影处理,决定屏幕像素的最终颜色。各种复杂的着色模型、光照计算都是在这个阶段完成。
    2. 测试合并:包括各种测试和混合操作,如裁剪测试、透明测试、模板测试、深度测试以及颜色混合等。经过了测试合并阶段,并存到帧缓冲的像素值,才是最终呈现在屏幕上的图像。

各种测试及其顺序

  1. 裁剪测试:在裁剪测试中,允许程序员开设一个裁剪框,只有在裁剪框内的片元才会被显示出来,在裁剪框外的片元皆被剔除。裁切测试可以避免当视口比屏幕窗口小时造成的渲染浪费问题。

  2. 透明测试(Alpha测试):根据物体的透明度来决定是否渲染。

  3. 模板测试:根据物体的位置范围决定是否渲染。

  4. 深度测试:根据物体的深度决定是否渲染。

PBR

概念

基于物理的渲染,是指在渲染过程中,有关材质、光照、相机、光传输等都要基于准确的物理定律。

实现

使用Cook-Torrance模型实现PBR,这个BRDF模型有漫反射和镜面反射两部分组成: 首先是漫反射项: 其中是表面颜色,是常数。可在预处理后的环境贴图(即辐照度贴图)中采样获取。除以是为了对漫反射光进行标准化,因为前面含有BRDF的积分方程受影响。

然后是镜面反射项: 包含三个函数,此外分母部分还有一个标准化因子。三个函数如下:

  • 法线分布函数:Normal Distribution Function,估算在受到表面粗糙度的影响下,朝向方向与半程向量一致的微表面的数量。
  • 菲涅尔函数Fresnel Equation,描述的是在不同的表面角下表面所反射的光线所占的比率。
  • 几何函数Geometry Function,描述了微表面自成阴影的属性。当一个平面相对比较粗糙的时候,平面表面上的微表面有可能挡住其他的微表面从而减少表面所反射的光线。

这三个函数有很多实现。UE4 中所使用的是:

  • D 项使用 Trowbridge-Reitz GGX,
  • F 项使用 Fresnel-Schlick 近似,
  • G 项使用 Smith’s Schlick-GGX。

阴影技术

Shadow Mapping

原理

  1. 从每个光源的位置渲染一遍场景,将得到的深度信息写入到贴图中,
  2. 正常渲染一次场景,利用得到的shadowmap来判断哪些片段落在了阴影中。

常见问题

  1. 光源VP矩阵的选择:平行光选用正交投影,点光源和聚光灯选用透视投影。需要注意的是,在透视投影得到的深度贴图中,深度值是 非线性的,在正式使用之前需要进行线性化操作。

    // 线性化操作
    float LinearizeDepth(float depth)
    {
        float z = depth * 2.0 - 1.0; // Back to NDC 
        return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
    }
  2. 阴影抖动问题:可以通过偏移技术来解决,增加一个bias来比较片段深度,还有更好的一种方式是使用一种自适应偏移的方案,基于斜率去计算当前深度要加的偏移;

  3. 阴影锯齿问题:可以使用百分比渐进过滤(PCF)技术进行解决:从深度贴图中多次采样,每次采样坐标都稍有些不同,比如上下左右各取9个点进行采样(即一个九宫格),最后加权平均处理,就可以得到柔和的阴影。标准PCF算法采样点的位置比较规则,最后呈现的阴影还是会看出一块一块的Pattern(图块),可以采用一些随机的样本位置,比如Poisson Disk来改善PCF的效果.

  4. 采样Shadowmap的时候,需要将标准设备坐标系的坐标范围由[-1,1]修正到[0,1],否则贴图的坐标范围是[0,1],会采样错误。

PCSS

抗锯齿

光栅化的时候,是以像素中心点是否被三角形覆盖来决定是否生成片段,因此有些片段覆盖了采样点就生成,有些没有覆盖就不生成,最终导致了锯齿现象。

SSAA

向原画面大 x 倍的画面进行降采样,性能要求高。

MSAA

只对必要的地方(如边缘处)进行单画面 x 倍采样,也有一定性能要求,尤其是场景三角形数量极大时。

FXAA

即 Fast Approximate AA,快速近似 AA。它将每帧画面的边界提取出来,然后对其进行插值处理以达到快速近似 AA 的目的。

步骤:

  1. 寻找整体画面边界:将画面的颜色空间从 RGB 转换到 HSL/HSV,根据亮度寻找;
  2. 计算 AA 的混合朝向:对边界进行水平和垂直方向的滤波计算,比较二者的值得出朝向,方便进行后序混合操作;
  3. 搜寻与朝向点相邻的部分边界:使用边界搜寻算法找到和朝向点相邻的部分边界;
  4. 计算混合程度:知道朝向和部分边界后,就能求边界点沿朝向采样的程度了。利用类似相似三角形的原理,通过边界信息求自身的偏移值。

TAA

利用时序上的数据(上一帧信息 + Motion Vector)进行 AA 操作。

后处理技术

模糊

高斯模糊

优化

在处理千万级别顶点数量的大型3D场景时,如何提升渲染效率和优化用户交互体验

千万级顶点的 3D 场景中,可以通过 渲染优化交互优化 结合使用,以提高效率:

  1. 渲染优化

    • 批处理(Instancing、Mesh Merging)
    • 视锥裁剪(Frustum Culling)
    • LOD 细节控制
    • 纹理优化(Mipmap、压缩)
    • 计算着色器加速
  2. 交互优化

    • 多线程渲染
    • 异步加载
    • 遮挡剔除(Occlusion Culling)

数学

向量

点乘, 叉乘的公式和几何意义

点乘: 使用点乘可以:

  • 求一个向量到另一个向量的投影;
  • 判断两向量是否同方向;
  • 找到两向量夹角;
  • 计算向量大小;
  • 沿某向量进行正交分解;

叉乘: 使用叉乘可以:

  • 判断点在三角形的内/外侧

矩阵

3D的SRT变换

缩放:

绕x轴旋转

前面的文章说过,矩阵其实是向量的数组,因此我们可以用三个轴的方向向量来表示一个旋转矩阵。按照上图描述,基本旋转矩阵为: 用齐次坐标描述:

类似的,绕y轴旋转为:

绕z轴旋转为:

平移:

通过SRT变换,也就是MVP变换中的M,将模型的点由局部坐标转换为世界坐标。

常见问题:

  1. 是否可逆:SRT变换属于线性变换,线性变换与平移变换合起来为仿射变换,它们都是可逆的(除了投影变换外都可逆)。
  2. SRT和TRS的结果是否相同:结果并不一样,这是因为像旋转和缩放这样的转换是相对于坐标系原点进行的。 缩放以原点为中心的对象产生的结果不同于缩放远离原点的对象所产生的结果。 同样,旋转以原点为中心的对象产生的结果不同于旋转远离原点的对象所产生的结果。

参考资料

  • 【游戏开发面经汇总】- 图形学基础篇 - 知乎 (zhihu.com)