第 4 章 三角形与网格:3D 世界的乐高积木

上一章我们扒开了”画面的最小颗粒”——像素。你记住了:再细腻的图,底层也是一堆小格子;再顺的动画,底层也是一串静画。全是”连续被切成离散”。

这一章,我们往深里走一层,进到”3D 物体本身”。你先在脑子里放一个问题:

游戏里那个栩栩如生的角色、那辆跑车、那座城堡,在计算机眼里到底是什么?是照片吗?不是,照片是 2D 的。是数学公式吗?也不是,公式画不出复杂的怪兽。那它到底是什么被存进了硬盘、又被显卡画了出来?

答案有点反直觉:那些酷炫的 3D 物体,在计算机眼里,全都是用无数个三角形拼出来的。对,就是你小学画的那种△。本章我就把这件事从头讲透——为什么偏偏是三角形,一个模型里到底存了什么,以及为什么”面数”能直接拖垮你的帧率。

4.1 为什么是三角形,不是方块或圆

先别接受”三角形”这个结论,咱们较真一下:屏幕上要表示一块曲面,为什么不用方块、不用圆、不用五边形?

这问题问得好,因为答案能帮你理解整个实时渲染的底层取舍。三角形有四条”得天独厚”的理由,每一条都戳中 GPU(显卡)的痛点。

4.1.1 三点定一平面:三角形永远不会”扭”

你回忆一下几何:三个不共线的点,唯一确定一张平面。不管你怎么挪这三个点,它们永远躺在同一张 flat 平面上,绝不会扭成麻花。

对比一下四边形(四个点):四个点完全可以不在同一张平面上(想象一张纸被你捏起一个角,四个角就分处不同高度)。所以”四边形面”在数学上是不稳定的——它到底是平的那张,还是你捏歪的那张?没说清。而三角形天然就是平的,没歧义。

这对渲染至关重要:光照计算、贴图坐标计算,都假设表面是平的(或者局部平的)。 用永远平整的三角形,每个面算起来干净利落,不用操心”这面扭没扭”。

4.1.2 三角形永远”凸”:不会有洞、不会自交

凸(convex)是说:图形内部任意两点连线,都还在图形内部。三角形永远是凸的。而一个多边形(比如星形、凹字形)可能是凹的,凹多边形在填充颜色、判断内外时会出麻烦。

三角形永远凸,意味着显卡往里”涂颜色”时,绝不会涂出界、绝不会留洞。这点对光栅化(把三角形变成屏幕上的像素,下一章细碰)是个巨大便利——算法最简单、最不会出错。

4.1.3 GPU 的硬件就是为”三角形”造的

这是最现实的一条。现代显卡的渲染流水线,是专门为”吃一堆三角形、一个一个光栅化”而设计的硬件。它内部有一整套电路,专门干”给我三个顶点,我还你这一片像素”的活儿。

你给方块,它也得先切成两个三角形再处理;你给圆,它先用很多小三角形去逼近圆,再处理。既然最终都要变三角形,干脆统一标准:所有模型都存成三角形,GPU 只写一套处理逻辑,又快又省。这就像快递柜只认”标准纸箱”,你寄圆桶也得装进方箱——不是圆桶不行,是统一方箱后分拣机最简单。

4.1.4 任意曲面都能用三角形”逼近”:三角化(Tessellation)

最关键的一条:任何复杂的 3D 表面,无论多弯多扭,都能用足够多的三角形拼出来,拼到你看不出棱角。

这又回到老主线了——连续→离散。现实里的球面是光滑连续的,计算机存不了”光滑”,就用很多小三角形去”近似”它。三角形越多(越密),越接近真球面;越少,越像低多边形(low-poly)的宝石感。

生活类比:你用乐高搭一个圆球,单个方块是方的,但几百个方块错落排布,远看就是个球。三角形就是 3D 世界里最小的乐高块。模型师(建模)干的事,本质就是”用三角形这把最小的砖,去拼出想要的形体”。

小结:三角形=永远平 + 永远凸 + GPU 亲儿子 + 能拼任何形状。四条理由叠一起,它就是 3D 世界的”标准积木”。你以后看到”面数""三角化""低多边形风格”,根都在这。

4.1.5 三角化的艺术:密与疏,就是”真实”与”风格”

既然任意形状都能用三角形逼近,那”用多密”就成了美术上的主动选择,而不是纯技术问题。

  • 高密度三角化:球面用上万个三角形,棱角被填平,看着就是真球。写实游戏的角色、电影级场景,都是百万级面数堆出来的光滑。
  • 低密度三角化(Low-Poly):故意只用几百个三角形,棱角分明,反而形成一种”几何感、复古感、风格化”的美学。你现在看到的很多独立游戏、图标、演示动画,就是刻意走 Low-Poly 风——不是做不出高精度,是故意用少面数换风格 + 换性能。

你看,连”美术风格”都是三角化密度决定的。这再次印证主线:离散的粒度(这里是一个三角形多大),直接决定观感。粒度细→真,粒度粗→风格化。 和像素分辨率、帧率,完全同构。

4.2 顶点、边、面:一个模型的零件清单

现在你接受”模型=一堆三角形”。那一个模型在计算机里,到底由哪些”零件”组成?这就是 网格(Mesh) 的概念。

Mesh(网格),就是一个 3D 模型的数据总成。它最基本地由三样东西构成:

  • 顶点(Vertex):三角形三个角上的点。一个顶点就是空间里一个位置(还记得第 2 章坐标?顶点存的就是它在模型局部坐标系里的位置)。
  • 边(Edge):连接两个顶点的线段。
  • 面(Face):由三个顶点、三条边围成的三角形。

这三者的关系像搭帐篷:桩(顶点)→ 杆(边)→ 布面(面)。桩定位,杆连桩,布面铺开成形状。

4.2.1 “面数”就是三角形数

你常听人说”这个模型几万面""那个角色二十万面”。注意:面数 = 三角形数量 = 模型里三角形的个数。 一个立方体,有 6 个正方形面,但每个正方形要切成 2 个三角形,所以立方体其实是 12 个三角形(12 面)。

所以”面”在游戏术语里,默认指”三角形”,不是”四边形”。这点新手极易搞混——美术软件里有时显示”四边面”,但导进引擎、送进 GPU 前,统统会被切成三角形。你听到的”面数”统计,基本是三角形数。

4.2.2 一个模型文件里,顶点和三角形怎么存

你是写代码的,这个结构你一定想看。简化来说,一个 Mesh 在内存里大概是这样两份数据:

vertices(顶点数组):  [ (x,y,z), (x,y,z), (x,y,z), ... ]   // 每个顶点一个位置
triangles(索引数组): [ 0,1,2,  0,2,3,  1,4,2, ... ]        // 每三个索引 = 一个三角形,指向 vertices 里的第几个点

看那个 triangles 数组——它不重复存坐标,而是存”哪三个顶点序号凑成一个三角形”。比如 0,1,2 表示”用第 0、1、2 号顶点连成一个三角形”。这样顶点坐标只存一份,三角形靠”引用序号”拼,省内存、也保证相邻三角形共用顶点不错位。

这像极了你写代码时的”用索引数组引用对象池”——顶点是池,三角形是引用,避免重复数据。工程师的思维,到哪都一样。

4.2.3 绕序(Winding Order)与背面剔除:GPU 怎么知道”哪面朝外”

我再补一个你以后必踩、但现在懂了能省半天懵的概念:背面剔除(Back-face Culling)。

一个三角形有”正面”和”背面”之分,靠三个顶点连成的顺序(顺时针/逆时针,叫绕序 winding order)判断。GPU 利用它做一个极省钱的操作:如果一个三角形”背对着摄像机”(你看到的是它的背面),那它多半被模型自身的前面挡住了,根本看不见——直接跳过不画,省一笔。

这就像你从外面看一栋房子,房子背对你的那面墙你反正看不见,画家就不画它。GPU 默认开启背面剔除,瞬间砍掉约一半的三角形工作量(一个闭合物体,朝外的和朝内的三角形各占约一半)。

它解决的问题:省算力。但代价是——如果你建模时三角形绕序搞反了(正面朝里、背面朝外),剔除会把”该看见的面”误删,模型出现”破洞""镂空”。新手导入模型发现”从某个角度看角色肚子是透明窟窿”,八成是绕序/法线翻了。记住这个词,debug 时能救命。

4.2.4 共享顶点 vs 独立顶点:平滑与硬边的取舍

还有个细节:两个三角形相邻时,是”共用同一个顶点”,还是”各有一份重合的顶点”?这决定外观:

  • 共享顶点:相邻面共用顶点,法线在顶点处被”平均”,过渡平滑——球面、圆润物体用这个。
  • 独立顶点:同一位置放两份顶点(各带自己面的法线),交界处出现清晰折角——立方体的硬边、锐利的棱用这个。

所以美术在建模时,会在”该硬的地方”把顶点拆开,在”该圆的地方”让顶点共享。你看到模型”有的地方圆润、有的地方锋利”,底层就是顶点共不共享的把戏。又是”连续→离散”的延伸:用顶点的组织方式,控制表面的连续感。

4.2.5 网格还有”法线""UV”等附属信息(埋个钩子)

光有位置还不够。你很快会发现,一个顶点除了”在哪”,还挂着不少”附加属性”——法线(朝哪)、UV(贴图坐标)等等。这些下一章、下下一章展开。这里你先建立一个清单概念:Mesh = 顶点位置 + 一堆每个顶点都带的信息 + 三角形索引。 就像一份快递,主体是”收件地址(位置)“,但面单上还印着”易碎(法线)""贴哪儿(UV)“等备注。

4.3 顶点里到底存了什么信息

这一节是重点中的重点,也是很多程序员刚碰 3D 最懵的地方:一个顶点,远不止”位置”那么简单。 为啥?因为 GPU 要拿顶点去干两件事——算光照(这面亮不亮)、贴图(这面贴图的哪一块)。这两件事,需要顶点额外携带信息。

我逐个拆,每个都先说”它解决什么问题”。

4.3.1 位置(Position):顶点在哪

最基础的,就是第 2 章讲的坐标——顶点在模型局部坐标系里的 (x, y, z)。所有三角形靠这些位置点连成形体。

它解决的问题:定义形状。没有位置,三角形就没有角,模型不存在。

4.3.2 法线(Normal):这面朝哪个方向,给光照用

法线(Normal),简单说就是”从这个顶点垂直戳出去的一根方向箭头”,表示这一点的表面朝哪。

为什么需要它?因为光照靠”表面朝向”算。回忆现实:你拿一张纸,正对台灯(法线朝向灯),亮;把纸转成侧着(法线偏离灯),暗。同一个光源,表面朝向不同,亮度天差地别。GPU 算”这个点该多亮”时,必须知道”这点的面子朝哪”——这就是法线告诉它的。

  • 平坦的立方体面:法线就是面朝外的方向,每个顶点一样。
  • 光滑的球面:法线沿半径向外,每个顶点方向不同,于是球面看起来圆润有过渡(而不是一块块亮)。

它解决的问题:让光照正确。没有法线,GPU 不知道面朝哪,要么全亮要么全暗,物体像塑料片,没有体积感。

生活类比:法线像你家门口的”朝向牌”——快递员(光)看牌子才知道从哪边照过来、照不照得进屋。每扇窗(顶点)朝向不同,屋里亮暗不同。

4.3.3 UV(纹理坐标):这面贴图的哪一块,给贴图用

UV 是两个坐标 (u, v),范围通常 0~1,表示”这个顶点,对应贴图(那张 2D 图)上的哪个位置”。

为什么需要它?因为模型是 3D 的,贴图是 2D 的。你想把一张 2D 的图”裹”到 3D 表面上,得告诉引擎:“模型上的这个点,对应图片上的那一点”。UV 就是这张”对应地图”。下一章(第 5 章)我会用”剥橘子皮”细讲,这里你先记住:UV 解决”贴图怎么对准 3D 表面”的问题。

它解决的问题:贴图定位。没有 UV,引擎不知道图该往哪贴,要么全贴同一块,要么乱贴。

4.3.4 切线(Tangent)与顶点色(Vertex Color):进阶信息

还有两个你以后会撞见、现在知道名字就够的:

  • 切线(Tangent):法线只给了”朝外”,但贴图上有凹凸细节时,GPU 还需要”贴图在表面上的横向基准方向”来正确计算凹凸光照。切线就是”贴图 u 方向在 3D 空间里的指向”。它服务于法线贴图(normal map)——一种让平面看起来有凹凸的廉价魔法(第 5 章提)。
  • 顶点色(Vertex Color):直接给每个顶点涂一个颜色,不靠贴图,用来做渐变、做简单着色、或美术手绘的权重。它解决”不想用贴图、想直接给顶点上色”的需求。

4.3.5 把顶点信息想成”一份带备注的面单”

我把 4.3 收一下,给你一个统一比喻:

一个顶点,像一份快递面单。位置 = 收件地址(在哪);法线 = “易碎/朝外”备注(光怎么打);UV = “贴纸贴这”备注(图怎么裹);切线 = “横着摆”备注(凹凸怎么算)。GPU 处理每个顶点,就是按这份面单的备注,决定它亮不亮、贴哪块图。

所以你读模型文件时,每个顶点其实是一串数据:(位置, 法线, UV, [切线], [顶点色])。理解了这一串,你就理解了”为什么模型文件比你想的大”——每个顶点都背着好几个向量。

4.3.6 平滑着色 vs 平面着色:法线插值的把戏

我再点破一个你一定会注意到、但说不清的现象:同一个球体,有的模型看起来圆润光滑,有的看着像”多面体宝石”,差别往往不在面数,而在法线怎么用。

GPU 给一个三角形上色时,要知道面上每一点的法线。两种做法:

  • 平面着色(Flat Shading):整个三角形用”同一个面法线”。于是每个三角形亮暗一致,三角形之间的边界出现清晰的折痕——Low-Poly 宝石感就是这么来的。
  • 平滑着色(Smooth Shading):三角形三个顶点各带一个法线,GPU 在面上把这仨法线按位置插值,算出面上每一点的法线。于是哪怕面数不高,球面看起来也是连续圆润的(因为每个点的”朝外方向”被插值成了弧线感)。

你看,又是一个”连续→离散”的补丁:真实球面法线是连续变化的,计算机用离散顶点法线 + 插值,把连续感骗回来。 和像素用”细格子骗连续”、声音用”密采样骗连续”,同构到骨子里。

生活类比:平面着色像用方块瓷砖拼球,每块一个色,棱分明;平滑着色像给每块瓷砖做了”渐变描边”,接缝被柔化,远看就圆了。

4.4 为什么模型”面数”影响性能

最后一节,把前面所有东西,接到你第 1 章就听过的”餐厅”比喻和”16 毫秒”预算上。这是本章最该带走的一句:面数 = 三角形数 = GPU 每帧要处理的”基本工作量单位”。

先厘清一个容易混的点:本章讲的 Mesh(顶点 + 三角形),只管”形状”,不管”长什么样”。一个没贴图、没上材质的纯 Mesh,画出来是灰扑扑的白模。它”穿什么衣服、什么皮肤”,是下一章(第 5 章)纹理与材质的事。Mesh 是骨架,纹理材质是皮肉——两者在引擎里是分开存、分开管的。你理解性能(本章)和理解外观(下章),也要分开想:卡不卡看面数,好不好看看贴图材质。把这两件事拆开,你调优时才不会乱拧旋钮。

4.4.1 每个三角形都要被 GPU 跑一遍

回忆 4.1.3:GPU 的活儿是”给我三个顶点,我还你一片像素”。场景里有 100 万个三角形,GPU 每帧就得把这一百万个三角形逐个拿去光栅化(变成像素)、上色、算光照。

三角形越多 → 每帧要处理的量越大 → 单帧耗时越长 → 越容易超过 16 毫秒预算 → 帧率掉 → 卡。

这就是”面数”直接和性能挂钩的物理原因,不是玄学,是工作量正比于三角形数。你第 3 章算过:像素量是 O(分辨率) 的大循环;这里三角形是 O(面数) 的另一个大循环。两者相乘,就是显卡每帧的总账单。

4.4.2 连回第 1 章餐厅:16 毫秒的约束

第 1 章我说:厨房(引擎)每 16 毫秒要上齐一道菜(一帧)。模型面数就是”这道菜里要切的食材份数”——食材越多,切得越久。

所以工程师的常态是在”好看”和”算得完”之间拉锯:

  • 主角、近景:面数给足,细节拉满(你盯着看,值得花算力)。
  • 远处小兵、背景:用低面数模型,甚至用一个”简模”(LOD,Level of Detail,细节层次)——离得远你看不清,就用三角形少的版本,省下的算力去保近处。

这又是第 1 章那句老话:把贵的步骤省掉,只在对你重要的地方花。 LOD 就是”远处用便宜积木、近处用精细积木”的具象化。

4.4.3 “过度绘制”和”顶点数”的额外账单(顺带提一句)

除了三角形数量,还有两笔隐性账你可能撞见:

  • 顶点数:每个顶点都要过一遍”变换”(第 2 章的矩阵搬运),顶点太多也费。但一般而言,面数(三角形)比顶点数更常是瓶颈。
  • 过度绘制(Overdraw):一个像素被多个三角形反复涂(比如前面一个角色、后面一堵墙,都画到同一片像素),白算。这跟”面数”不是一回事,但同样吃预算。

你只要记住主因:三角形越多,单帧越重,越容易掉帧。 调模型面数、开 LOD、做剔除(不画看不见的),都是冲着”别让这一帧的三角形超出 16 毫秒能消化的量”去的。

4.4.3 一笔显性账:一帧能”吃”多少三角形

光说”越多越慢”太虚,我给你一个量级感。假设目标 60 fps,16 毫秒预算。现代中端 GPU 大约每秒能处理几十亿个三角形(不同硬件差很多)。摊到一帧 16 毫秒,大概一帧能从容处理几百万三角形。

那一个场景怎么分配?粗略思路:

  • 主角:5~20 万面(你盯着看,值得)。
  • 同屏十几个敌人:各 1~5 万面。
  • 环境:地形、建筑可能几十万到上百万面,但大量用 LOD 和剔除砍掉看不见的。
  • 加起来若失控到上千万面,中端机就扛不住,必须靠 LOD/剔除把”实际送进 GPU 的面”压回几百万。

所以美术有个铁律:不是”模型本身多少面”,而是”同屏实际渲染多少面”决定卡不卡。 你硬盘里模型有五百万面没关系,只要同屏只画得到其中三百万,就还行。这就是为什么”剔除”(不画摄像机看不到的)和”LOD”(远处换简模)是性能救星——它们直接砍”实际面数”。

4.4.4 顺带一句:Draw Call 比”面数”更隐蔽的瓶颈

还有个你可能提前听见的词 Draw Call(绘制调用):CPU 每”下令画一样东西”就是一次调用。即使总面数不多,若场景里散落一万个独立小物体(每根草一个物体),CPU 要下一万次命令,反而被 CPU 拖垮——这叫”Draw Call 爆炸”,和 GPU 面数无关。

工程师的解法(你只需知道名字):把很多小物体合并成一个(静态合批 Batching)、或用 GPU 实例化(Instancing)一次画一千个相同的草。这些优化,根上还是第 1 章那句话:把贵/频繁的步骤合并、提前,运行时只发最少命令。 面数是 GPU 的账,Draw Call 是 CPU 的账,两本账都得平。

4.4.5 一个反问,留给你想

我留个值得琢磨的点,不急着给答案:

既然”三角形越少越省”,那为什么游戏不统统用最低面数?为什么 3A 大作反而敢堆几百万面?

因为”算得完”的预算在涨——显卡越来越强,16 毫秒里能消化的三角形从几万涨到几百万。所以面数天花板是跟着硬件走的:硬件强,你就敢堆;硬件弱(手机),你就得抠。这也是为什么同一款游戏,PC 高画质面数爆炸、手机版得砍一半。永远回到那句话——在实时约束下,用感知弱点 + 硬件余量,换最好的观感。

4.5 回头看三句话

按套路,你该能答这三句:

  1. 现实对应什么? 3D 物体是连续光滑的,计算机用无数三角形去近似它(又是连续→离散)。
  2. 没有它会出什么岔子? 不离散成三角形,GPU 没法定点算光照、贴图、光栅化;三角形乱用(凹的、扭的)会让计算出错。
  3. 引擎给了哪几个旋钮? 模型面数(三角形数)、LOD(细节层次)、法线/UV/切线等顶点属性、剔除(不画看不见的)。每个都对应”拼得多细 / 怎么算光 / 怎么贴图 / 别白算”。

下一章我们讲”给这些三角形贴上皮肤”——纹理与材质。你会发现 UV(4.3.3 埋的钩子)正是贴图能贴准的关键,而”材质”决定光打上去是什么脾气。主线继续:现实连续→离散→用感知弱点换性能。

【思考题】

  1. 为什么 3D 模型用三角形,而不用方块或圆?至少说出两条理由。
  2. 一个顶点除了”位置”,还常带着哪两种最关键的信息?它们分别解决什么问题?
  3. “面数”为什么直接影响游戏帧率?用第 1 章”餐厅/16 毫秒”的比喻解释。
  4. 什么是 LOD(细节层次)?它体现了本教程反复说的哪条工程哲学?

【参考答案】

  1. 至少两条:(a) 三点定一平面,三角形永远平整无歧义,光照/贴图计算干净;(b) 三角形永远凸,填充不会出洞/出界;(c) GPU 硬件专为”吃三角形”设计,统一成三角形最快最省;(d) 任意曲面都能用足够密的三角形逼近(连续→离散)。方块可能扭、圆需近似,都不如三角形省事。
  2. 最关键的两种:法线(Normal)——表示顶点表面朝哪,解决”光照怎么算、这面该多亮”;UV(纹理坐标)——表示顶点对应贴图上哪一点,解决”2D 贴图怎么准确裹到 3D 表面”。二者分别服务”光”和”图”两件 GPU 必做的事。
  3. 因为 GPU 每帧要把场景里每个三角形逐个光栅化、上色、算光照,三角形数≈单帧工作量。面数越多,单帧耗时越长,越容易超过 16 毫秒上菜预算→帧率掉→卡。餐厅比喻:面数是”这道菜要切的食材份数”,食材越多切得越久,超时就上菜慢、顾客(玩家)觉得卡。
  4. LOD(Level of Detail,细节层次)是”同一物体按离摄像机远近,用不同面数的模型版本”——远处用低面数简模,近处用高面数精模。它体现的哲学是:在实时约束下,只对玩家看得清、看得重的地方花算力,远处看不清的就省(用感知弱点当掩护),正是第 1 章”把贵的步骤省掉、便宜的留到用时”的延伸。