11.1 几何的应用

  • 我们之前用三角形代替几何图元,这是一种很简单而基础的表示方法,但在真实世界里,很明显我们并不能用这一种简单的图形表示所有的几何形状,因此本节我们将来看一看几何方面诸多的表示方法与应用。

11.1.1 复杂曲面

  • 几何方面表示的其中一个典型例子就是复杂的曲面,比如赛车游戏中光滑的汽车模型,就不单是用三角形来进行近似表示了。
  • Forza Horizon 4 Screenshot 2024.09.30 - 21.43.24.91.png
    11.1.1 复杂曲面的例子——地平线4

11.1.2 毛发渲染

  • 毛发渲染同样十分需要几何层面的知识与优化,因为其数量巨大,而渲染与存储它更是业界一直在探寻的难题。
  • 通过一些手段我们可以更加顺滑优化地表示它。最典型的毛发渲染就是最终幻想系列。

  • b12fdeb36461e71fd69571f940c64538.jpg
    11.1.2 复杂的毛发渲染例子——最终幻想15

11.1.3 几何体优化

  • 对于场景中大量的几何体聚集的情况,我们也需要通过各式各样的方法进行优化,最耳熟能详的应该是LOD技术
  • 这个技术与前文提到过的MIPMAP技术结合使用可以得到很好的性能优化与画面权衡表现的作用,经典的例子就是漫威蜘蛛侠中城市场景。

  • 1817070_1.jpg
    11.1.3 几何体优化的例子——漫威蜘蛛侠1

11.2 几何的表示方法

  • 几何的表示分为显式与隐式。

11.2.1 隐式的表示

  • 隐式表示实际上是一种判断方法,满足该条件的点就都可以存在。
  • 而一般也是以数学公式体现的,比如大家高中学过的点线面的公式,再比如我们会在高数中学到的体的公式,它不告诉我们具体的位置,我们可以将其理解为一种函数。

  • image.png
    11.2.2 隐式的表示实例

  • 虽然隐式表达非常不直观,但是它可以用来检测一个固定的点在不在这个几何体上。下面我们来看显式的表示方法。

11.2.2 显式的表示

  • 第一种就是直接将几何体给出,比如建模得到的一个复杂几何体。
  • 第二种就是通过参数映射方式,在一个二维平面中的点我们用对应的函数去映射到三维空间之中,这和展UV的概念类似。
  • image.png
    11.2.2 参数映射表示法

11.2.3 CSG表示法

  • 当然几何的表示方法远远不止以上说的这些,CSG(constructive Solid Geometry)就是一种表示法,它是通过两个简单几何体进行各式各样的运算来得到新的几何体,比如交并补等等。这种运算也被称为布尔运算。
  • image.png
    11.2.3 CSG 的具体案例

11.2.4 距离表示法

  • 距离表示法也叫做距离场表示法。这种表示法不去定义具体的几何元素,而是定义任意一点到他的距离,最经典的应用就是融球,在星铁中匹诺康尼场景中就有体现。
  • image.png
    11.2.4 距离场融球案例——星铁

  • 下面我们来细致理解一下距离函数,或者说叫有向距离场。
  • 首先如果我们想混合两个物体在屏幕上呈现的占比,我们如果直接混合的话,会得到并非平滑过度的值。
  • image.png
    11.2.4 非平滑过度的直接混合
  • 而SDF函数就可以让我们根据两个面之间的距离关系,平滑的混合这些值,你可以理解为原本其并没有如此细分属性,而距离函数让其本身具备了这样多元的属性,如果我们再对其进行操作,就可以过度的很顺滑了。
  • image.png
    11.2.4 SDF函数后的混合方式

11.2.5 显示表示方法示例

  • 点云,顾名思义,由很多点形成的画面,最具代表性的就是高斯抛雪球方法呈现出来的3维立体建模,感兴趣的同学可以浏览下面网页。
  • https://lumalabs.ai/
  • image.png
    11.2.5 视频生成的AI三维建模

  • 面片,就是引擎与DCC软件中常用到的基本几何形体表示元素。一般多为三角面或四边形,如果在引擎中(Unity举例)打开线框预览模式就能看出整个物体是以一个一个面片组成。
  • image.png
    11.2.5.1 Unity中的线框显示

  • 我们的EasyShader使用了一种以这样的数据格式作为代表的模型输入格式——TheWavefront Object File(.OBJ)格式。
  • 这种格式的特点就是,一切以数值记录,因此存储空间小,我们可以看到有面法线,面纹理坐标,顶点等一系列数据,我们通过读取这些数据,在软件中用我们的算法绘制图形。
  • image.png
    11.2.5.2 .obj格式实例

11.2.6 曲线——贝塞尔曲线

  • 贝塞尔曲线是一种平滑曲线的表示方式,它在很多软件中都有应用,比如平滑运动,或平滑线条线段等等,我们往往通过它来实现一些自由的曲线表示,下面我们来深入了解贝塞尔曲线的原理。

  • 首先,我们定义曲线的方式可以用近似的思想体现。也就是如果我们希望有一条曲线,那么我们就先定义几条直线。
  • Pasted image 20240604204651.png
    11.2.6 贝塞尔曲线第一步——找到第一个中心点
  • 在B0到B2的阶段,我们让其拥有一个过渡值b1,想象一下,我们规定从b0到b1需要经过一个单位时间。而我们可以在其上任意取一个时间t,现在假设是t = 1/3 处。我们在此处可以取一个点,我们将其命名为b01

  • Pasted image 20240604204716.png
    11.2.6.1 贝塞尔曲线第二步——找到第一个t时间点
  • 现在,我们只需要继续这个过程,通过不同的参数t就可以绘制出不同样式的贝塞尔曲线了,这种绘制方法叫做二阶贝塞尔曲线。


  • 下面我们来看它的数学表达式。本质上得出每一个点的过程就是进行线性插值的过程,每一个点都可以用起始点和终点进行线性插值,而且下一层的点一定可以用上一层进行表示
  • Pasted image 20240604214935.png
    11.2.6.3 贝塞尔曲线的推导

  • 对于B02我们就可以用最底层的B0 B1以及B2表示
  • Pasted image 20240604215109.png
    11.2.6.4 第二阶贝塞尔曲线表达式
  • 你是否能总结出规律?
  • 没错实际上它就是一个二项式的形式,这样我们就可以使用多项式展开将其扩展到N维。

  • Pasted image 20240604215224.png
    11.2.6.5 多项式展开
  • 我们进一步总结
  • Pasted image 20240604215242.png
    11.2.6.6 N阶贝塞尔曲线表示法
  • 至此我们就可以表述任意曲线了,对三维同样适用,至此我们解决了开头提到的第一个问题,现在你也可以在软件中造出一台精美绝伦的车辆模型了~

11.2.7 逐段的贝塞尔曲线

  • 前面我们了解了单独的贝塞尔曲线,但是我们会发现,如果想去定义更复杂的曲线形状,往往单纯用贝塞尔曲线就不够了,我们需要进一步完善这个方法。
  • 各位在使用PS时应该会发现,有一个很常用但是也很难懂的工具——钢笔工具。
  • 它本身实际上就是一个多段的贝塞尔曲线,通过这个工具你可以绘制出几乎任意你想要的特定曲线。
  • image.png
    11.2.7 Ps中的钢笔工具实例

  • 逐段定义的贝塞尔曲线一般具备四个点,分别是起点终点以及两个对应的控制点。通过调控控制点的位置来控制曲线的弯曲程度。
  • 逐段链接的贝塞尔曲线也有不同的链接方式,其中在几何上连续的贝塞尔曲线叫做C0连续,也就是第一段贝塞尔曲线的终点与第二段贝塞尔曲线的起点相等。
  • 而如果他们的切线方向也相同并且控制点等距,我们就称之为C1连续。
  • image.png
    11.2.7.1 贝塞尔曲线连续的方式

11.2.8 样条曲线

  • 样条在很多DCC软件中也有广泛使用,比如blender中颜色渐变节点中渐变方式就有样条这一类型。
  • 样条本身也是一种曲线,满足一定的连续性,是一种简单可控的曲线。
  • B-Splines也叫β样条。叫做基函数样条,我们之前总结出的贝塞尔曲线公式就叫做基函数。β样条也是对贝塞尔曲线的扩展。
  • 因为贝塞尔曲线牵一发动全身的特点,因此在设计上不符合直觉,因此诞生出来了β样条,它可以对局部的细节进行调整。在此我们不多赘述,感兴趣的同学:
  • 【清华大学-计算机图形学基础(国家级精品课)】 https://www.bilibili.com/video/BV13441127CH/?p=13&share_source=copy_web&vd_source=18d60239a339ad21d3b3f050742622f4
  • image.png
    11.2.8 blender中颜色渐变的β样条模式

11.3 曲面

11.3.1 贝塞尔曲面

  • 前文我们提到了贝塞尔曲线,它是二维的,在三维中,同样我们也可以利用贝塞尔公式去定义曲面,只不过它是一个数组的形式。
  • 我们通过在空间中定义一系列的贝塞尔曲线,来获得所需的控制点。通过这些控制点作为一条贝塞尔曲线的表示方法,来形成一个贝塞尔曲面。
  • 11.3贝塞尔曲面.png
    11.3.1 贝塞尔曲面的形成
  • 因为贝塞尔曲线的性质,我们可以定义两个参数对应之前我们所提到的UV坐标,通过这两个点的变换,我们就可以得到该曲面在UV坐标上的映射,以此得到UV贴图。

11.3.2 模型细分

  • 我们之前了解到,模型是由众多三角面组成。而三角面是由众多顶点组成的,在着色部分我们讲过,增加三角形顶点数量可以拥有更好的着色效果,而增加三角形顶点数量的方法便被成为细分。
  • 我们在Blender等DCC软件中,细分是一个基础的操作。它本质上就是让模型顶点变化频率增加,能够更加顺滑的体现出细节。
  • image.png
    11.3.2 blender中的模型细分

  • 接下来,我们来看它的原理。首先,细分大致可以分为两步操作。
  • 首先第一步,创建更多新的三角形,这一步很简单,我们可以取原三角形三边中点,分别连线,形成四个新的三角形。这样的划分方式也叫做Loop细分(发明人姓Loop)。
  • 接下来,我们就需要处理新形成的三角形的位置。
  • 对于一个新的顶点,往往被几个三角形所共享,我们就可以利用就顶点的位置对新顶点进行加权平均,这样就能获得更加平滑的新三角形。
  • image.png
    11.3.2.1 新生成顶点的处理方式

  • 下一步,我们还需要来看老顶点如何更新,新顶点有一部分是要听从于老顶点的变化,因此老顶点的更新十分重要。
  • 老顶点的更新需要参考两部分,一部分来源于这些老顶点自身,一部分来源于其周围的顶点,同样是进行一定的插值运算,其中有两个参考值,一个是变换目标的’度’,这是图论的概念,实际上很好理解,就是该点与多少个顶点相连接。另一个参考值为u,是根据度来进行的变化,最后通过这两个参考值对图形进行插值。
  • image.png
    11.3.2.2 旧顶点的更新方式

  • 非三角面细分
  • 以上我们提到的Loop细分方法是针对三角面进行细分的,而往往我们在实际操作中遇到的网格体不止有三角面一种。接下来介绍的这种Catmull方法就是针对更普遍的网格细分法。
  • 我们先将网格分为四边形与非四边形两种类型。随后我们定义一个概念叫做奇异点(Extraordinary vertex),也就是度不为4的点,度的概念希望你还记得,也就是所连接的边的数量。
  • 我们进行细分的方式就是,首先取三角形中点以及面的中点,并将其连接,我们就能得到一系列新的多边形。
  • image.png
    11.3.2.3 CatMull细分法

  • 经过上述变换后,我们会发现,奇异点数量增加了。我们可以总结出规律,在非四边形面中增加点,增加出的一定是一个奇异点,非四边形面都消失了。
  • 但如果我们继续细分之后奇异点数量就不会再继续增加了。
  • image.png
    11.3.2.4 CatMull细分法-进一步细分
  • CatMull 等细分方法应用的领域非常多,可以提升画面的表现力以及精度,但有些时候,尤其是在游戏里,我们不希望所有的面都是高精度的,这样会很卡,所以就要牵扯到我们的下一个话题,有细分就有简化,接下来我们来看模型面数的简化,比如Lod技术以及UE的独家技术Nannite网格体。

11.4 模型的简化

  • 首先,模型的简化与模型的细分相反,是为了减少三角形面数而诞生的优化手段,不同面数的三角形带来的整体质感是不一样的。
  • image.png
    11.4 Blender中应用了不同等级的细分修改器后的结果
  • 而对于一些距离我们很遥远的物体,其往往不需要呈现出复杂的细节,因此在游戏中,会将其应用为面数比较低的模型。
  • 这样的模型先前是需要美术人员手动生成低模,并添加到对应的LOD层级之中。而在Ue中,一种叫做Nannite网格体的技术,可以为我们自动生成这样的低模,今年大火的黑神话就是得益于这样的的技术,让其扫描出的模型能够放心大胆的放入场景之中。

11.4.1 Nannite网格体

  • 根据Ue官方文档,我们可以了解到,Nannite在保证帧率流畅的同时,将渲染精度提升了数个数量级。并且支持自动处理细节级别,也就是我们前文提到的LOD。不需要再手动设置这些单个的网格体。
  • Ue会根据摄像机远近,自然地调整LOD级别,增加或减少三角形面数。Nannite会将模型拆分为三角形集群,并根据流动只渲染所能见到的细节。
  • 11.4.1 Nannite网格体实例.png
    11.4.1 Nannite网格体实例
  • 想了解更多内容的各位可以在Ue官方文档中查看
  • https://dev.epicgames.com/documentation/zh-cn/unreal-engine/nanite-virtualized-geometry-in-unreal-engine

11.4.2 原理及细节

  • 接下来,我们来看一下模型简化的原理,是如何做到不破坏原有三角面的大致结构的前提下,还能减少三角面面数的呢?
  • 边坍缩算法
  • 边坍缩算法的理解上很简单,我们需要减少三角面数,三角面是由诸多边组成的,我们想要减少三角面数,只需要减少三角边数就好了,也就是将一条边压缩为一个点。
  • image.png
    11.4.2 边坍缩算法

  • 原理上很好理解,但实现上并不容易,因为我们会发现,如果单纯的压缩,会导致新生成的模型与原本模型差距巨大。导致模型走样。
  • 因此我们引入了一个新的方法,叫做二次误差度量,这个意思是我们检测这个新生成的点,到原本的点所在平面的距离的平方和,合适此值最小,何时为最佳生成点。
  • image.png
    11.4.2.1 二次误差度量

  • 但是面对大量的三角形面,我们需要用更加效率的方式存储并排序,这就涉及到数据结构中的优先队列结构,以及贪心算法了。

  • 优先队列
  • 优先队列是一种抽象的数据类型,每个元素都有各自的优先级,优先级最高的元素首先得到服务,通常使用堆实现。是找到我们所需元素的合适结构。
  • 贪心算法
  • 贪心算法叫做算法,实际上是一种策略,一切都为了找到最优解,实际上我们所做的这件事本身就是贪心算法的一种体现。
  • 贪心算法可以有以下几步
      1. 建立数学模型来描述我们的问题
      1. 将求解问题分出若干子问题,也叫单一原则
    • 3.对每一子问题求解,得到局部最优解
    • 4.将子问题的局部最优解汇总形成总体最优解。
  • 当然实际的应用有很多,各位可以自行了解。
  • https://pleasant233.oss-cn-beijing.aliyuncs.com/20241202100132.png

11.5 写在最后

  • 至此,我们初入图形学的脚步就告一段落了,回顾我们的历程,一共前前后后写了将近4万多字,大致带领大家与我一起初识了图形学光栅化的入门内容,并且编写了一个大家自己的渲染器。
  • 我分享本课的目的是总结自己学习的过程,说实话,很多东西第二遍看才算基本弄懂,我也是现学现卖。作为第一节分享课,我们在实践与理论中进行学习,所有枯燥乏味的知识我都尽量通俗并寻找实际应用中的案例讲解。希望各位阅读时不会有太多不理解的地方吧。(尽力了~)
  • 本系列课将持续增添删改内容。下一部分打算做光线追踪方面的入门课,并且做Shader的语法与基础入门。带领大家正式进入应用的领域。
  • 最后,再次感谢各位的支持,感谢我校的zy老师,101课程的闫令琪老师等诸多学术大牛的启蒙。祝大家能够在学业中,有所精进,有所收获!我们下一段分享课再见!

参考资料