逛街时你一定注意过商场橱窗里的模特:正对街道的一面妆容精致、衣服笔挺,背后却只是光秃秃的塑料骨架。店主心里门儿清——顾客只能隔着玻璃看正面,把预算花在没人看得见的后脑勺上,纯属浪费。3D 渲染的世界里也有这样一位精明的“店主”,它叫 GPU,橱窗里的“模特”则是由无数三角形拼成的三维模型。相机就是橱窗外的顾客,三角形的某条边朝不朝相机,决定了它值不值得被绘制。 背面剔除(Backface Culling)就是这套“精明生意经”:在三角形进入光栅化流水线之前,先判断这个面的朝向,凡是背对相机的面直接跳过,一个像素都不画。一个封闭的三维网格,任意视角下大约有一半三角形是背向的,这一刀砍下去,渲染负担几乎减半。你不需要理解复杂的数学,只要记住橱窗模特的逻辑:正面朝顾客的才打扮,背面的连妆都不用上。接下来,我们会从法线方向、判定条件、性能收益到实现流程,把这块知识完整拆开,看看 GPU 这位“店主”到底是怎么省下这笔钱的。
对 AI 从业者而言,掌握背面剔除意味着你能驾驭大规模三维数据的实时可视化,无论是数字孪生、机器人仿真还是 XR 应用,渲染优化的基本功都是绕不开的护城河。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
背面剔除效果 F = Front (\u6b63\u9762): N\u00b7V > 0, \u4fdd\u7559\u7ed8\u5236
剔除概念
商场橱窗里的模特永远只把正面留给街道。店主不会在模特后脑勺上画眼影,因为那条街的行人绕不过玻璃,看不见背面。这就是背面剔除的雏形:先想清楚“什么值得被看到”,再决定“什么值得被绘制”。
三维模型由成千上万个三角形拼成。当相机对准模型时,一部分三角形的“脸”朝着相机,叫前向面;另一部分“后脑勺”朝着相机,叫背向面。一个封闭的球体或立方体,任意角度看过去,大约一半三角形是背向的。如果 GPU 老老实实把它们全部画出来,这些背向面绘制的像素最终也会被前面的面挡住,属于纯粹的浪费——相当于店主给模特后脑勺抹了昂贵的粉底。
背面剔除发生在光栅化之前:GPU 拿到一个三角形,先算它的朝向,发现是背向面就直接丢弃,不进入逐像素阶段。它判断朝向的依据,是三角形法线与视线方向的夹角关系。设 $\vec{N}$ 为三角形面法线,$\vec{V}$ 为从三角形指向相机的向量,则判定规则为:
店主省下的是化妆费,GPU 省下的是光栅化与像素填充的开销。背面剔除不做任何“看心情”的取舍,它只有一条铁律:看不见的面,干脆不画。
- 法线向量$\vec{N}$ 是垂直于三角形平面的方向向量,代表“模特脸的朝向”,由三角形顶点绕序决定,而非美术手工指定。
- 视线向量$\vec{V}$ 是从三角形上某点指向相机位置的向量,代表“顾客目光投来的反方向”,即顾客站在哪里看这个面。
- 点积运算两个向量做点积,结果等于二者长度之积乘以夹角余弦,符号直接反映夹角是锐角还是钝角。
- 符号判定点积大于零说明法线与视线大致同向,面朝着相机,保留绘制;小于零说明面背着相机,果断剔除。
法线方向
橱窗模特凭什么知道自己哪边是正面?答案藏在组装说明书里:所有零件按统一方向摆放,头朝上、脸朝街。三角形也有这样一份“组装说明书”,它叫顶点绕序。三个顶点按什么顺序连接,决定了这个面的正反。
图形学中有一个通用约定:当从三角形正面看过去时,三个顶点呈逆时针排列;从背面看则是顺时针。这个约定被写入 OpenGL、WebGL、DirectX 等所有主流图形接口。只要顶点索引顺序一致,整个模型就能做到“所有面朝外”——就像所有模特都整齐地面朝街道。
把三个顶点记为 $\vec{v}_0, \vec{v}_1, \vec{v}_2$,先构造两条边向量 $\vec{u}$ 和 $\vec{w}$,再用叉积求出垂直于此平面的法线:
法线方向是整套剔除机制的“地基”:绕序决定法线,法线决定朝向,朝向决定生死。只要模型师按统一绕序建模,GPU 就能自动分辨每一张“脸”的正反,不需要任何额外标注。
- 构造边向量从 $\vec{v}_0$ 出发指向 $\vec{v}_1$ 得到 $\vec{u}$,指向 $\vec{v}_2$ 得到 $\vec{w}$,两条边张成三角形所在平面。
- 叉积定义$\vec{u} \times \vec{w}$ 的结果垂直于这两条边,也就是垂直于整个三角形平面,即法线方向。
- 分量展开叉积可用行列式展开,$x$ 分量由 $u_y w_z$ 减去 $u_z w_y$ 得到,其余分量同理轮换。
- 右手定则四指从 $\vec{u}$ 弯向 $\vec{w}$,拇指指向就是法线方向;逆时针绕序恰好让法线指向观察者。
判定条件
店主站在橱窗里侧,顾客走远走近、从左到右,他都能一眼看出模特是正脸还是后脑勺。GPU 也一样,不管相机怎么转、模型怎么翻,它总能快速回答同一个问题:“这个面,朝不朝我?”
答案藏在三角形投影到屏幕后的“旋转方向”里。想象你从相机的位置看出去,前向三角形因为逆时针绕序,在屏幕上看起来也是逆时针;背向三角形则变成顺时针。于是 GPU 只需要做一次简单的算术:取三角形三个顶点的屏幕坐标,计算有符号面积:
这样,GPU 在光栅化之前就完成了“目测”,连三维空间中的法线和点积都省了。判定条件如此廉价,以至于它对每个三角形只花几个时钟周期——这正是它能成为渲染管线标配的原因。店员扫一眼模特朝向,比把模特转过来细细端详要快得多。
- 屏幕坐标三角形经过模型、视图、投影变换后投影到屏幕上,得到三个二维坐标 $(x_0,y_0)$、$(x_1,y_1)$、$(x_2,y_2)$。
- 构建向量以 $p_0$ 为起点,分别指向 $p_1$ 和 $p_2$,构造屏幕空间的两条边。
- 二维叉积在二维平面上,叉积退化为一个标量 $S$,数值上等于平行四边形面积,符号表示绕行方向。
- 符号判决约定逆时针绕序为正面时,$S > 0$ 保留,$S < 0$ 剔除,$S = 0$ 说明三角形退化为一条线,通常也剔除。
性能收益
店主算了一笔账:给模特后脑勺化妆要 10 分钟,却从没有一个顾客看见过。去掉这道工序,每天可以多陈列 3 个新模特。GPU 也算过这笔账:一个背向三角形从光栅化到像素填充,涉及成千上万个片元的计算,而它最终会被更近的面完全盖住。
背面剔除的收益首先体现在几何处理上:三角形在进入光栅化器之前就被拦截,后续的顶点插值、片元着色、深度比较全部免谈。其次是像素填充率:被剔除的面不会产生任何片元,这对高分辨率、多重采样抗锯齿(MSAA)等填充率受限的场景尤为重要。一个闭合网格,无论相机朝向如何,可见的前向面大约只占一半,这意味着渲染负载近乎腰斩:
这里要澄清一个关键区别:背面剔除节省的是“面”,而遮挡剔除(Occlusion Culling)节省的是“被挡住的面”,二者不是一回事。墙壁后面的敌人属于遮挡问题,由深度缓冲在像素阶段解决;而背对相机的面,在源头就被掐掉了。店主省下的钱用来多摆几个新模特,GPU 省下的算力则用来渲染更丰富的场景。
- 绘制三角形数$N_{\text{绘制}}$ 是剔除后真正进入光栅化的前向三角形数量,它是“值得打扮的正面”。
- 全部三角形数$N_{\text{全部}}$ 是网格里所有三角形的总数,包含前向面与背向面,是流水线原本要处理的量。
- 节省比例用被剔除的三角形数除以总数,得到省下的处理比例;闭合网格在随机视角下约为一半。
- 约等一半封闭凸体与大多数闭合网格,任意视点下可见面与不可见面数量大致相等。
实现流程
模特从仓库到橱窗要经过开箱、组装、摆位多道工序。店主在“摆上展台”之前设了一道检查岗:先看正面朝不朝街,不朝的直接退回仓库。这道检查岗在渲染管线里的位置,就是顶点变换之后、光栅化之前。
显卡拿到顶点数据后,先经过模型变换、视图变换、投影变换,把三维坐标换算到屏幕附近的空间。此时每个三角形都已经“就位”,正好可以检查朝向。由于屏幕空间的二维叉积足够廉价,GPU 通常在这里完成剔除,然后才把幸存的前向面交给光栅化器。整个流程可以概括为:
工程上开启背面剔除只需一行代码。OpenGL / WebGL 中调用 gl.enable(gl.CULL_FACE) 即可启用剔除,用 gl.cullFace(gl.BACK) 指定剔除背向面;DirectX 则在光栅化状态中设置 RasterizerDesc.CullMode。绝大多数渲染引擎默认开启这个选项。店主在入口把关,省下的每一分算力都转化为流畅的画面帧率。
- 相机空间坐标顶点经过模型和视图变换后位于相机空间,$z_c$ 表示顶点到相机的深度。
- 透视投影将三维坐标除以深度 $z_c$,实现“近大远小”的透视效果,$f$ 控制视野缩放。
- 屏幕映射把归一化坐标平移到屏幕中心 $(c_x, c_y)$,得到光栅化使用的像素坐标。
- 剔除介入拿到 $(x_s, y_s)$ 后立即计算有符号面积 $S$,背向面在进入光栅化器之前被丢弃。
特殊场景
并不是所有模特都只有一张脸。橱窗正中央的旋转展示台上,模特前后都穿着精心设计的衣服,顾客转一圈能看个遍。这样的“双面展示品”,店主绝不会拦下。渲染中对应的是双面材质:叶片、纸张、布料这类薄片模型,正反两面都可能被看到,背面剔除反而会帮倒忙。
处理这类特殊情况,规则只有一条:看模型是否需要双面可见。树叶、翅膀、告示牌等开放网格,关闭剔除或开启双面渲染;而墙体、箱子等闭合网格,保持剔除以获得最佳性能。判定逻辑可以写成:
另一个常见坑是绕序不统一:部分美术工具导出的模型顶点顺序混乱,导致本该朝外的面被判成背向面,出现“面消失”的破洞。排查思路是先关掉剔除确认模型完整,再检查法线可视化,最后统一重算绕序。真正的精明店主知道:什么时候该省、什么时候不该省,比一味省钱更重要。
- 前向面判定$\text{front}$ 为真表示面正对相机,此时无论是否双面都应当保留绘制。
- 双面开关$\text{twoSided}$ 为真表示该材质允许看到背面,此时背向面也被保留。
- 逻辑或合并前向面保留,或双面模式下背向面也保留,二者满足其一即可通过。
- 代价权衡双面渲染让每帧处理的三角形数接近翻倍,仅在薄片类模型上划算。
AI 应用场景
游戏引擎大规模场景渲染
阶段:渲染性能优化与场景管理
解决的问题:开放世界游戏中,场景可能包含数百万三角形。若全部绘制,GPU 几何处理与像素填充双双超载,帧率骤降。背面剔除作为第一道关卡,在光栅化前砍掉约一半三角形,是引擎性能预算的基石。
具体案例:在 Unity 中,模型导入设置的 “Generate Colliders“ 旁通常默认开启 ”Backface Cull”;Unreal 的材质面板也有 Two Sided 选项。当玩家在城市场景中奔跑时,每帧数万栋建筑背向面被静态剔除,配合遮挡剔除,最终只绘制可见的前向面,保证 60 FPS 流畅体验。
数字孪生与 Web 端三维可视化
阶段:三维数据可视化与前端渲染管线搭建
解决的问题:BIM 建筑模型、工业设备点云等动辄百万级三角形,浏览器端受 CPU/GPU 资源限制,直接全量绘制会卡顿甚至崩溃。利用背面剔除配合 LOD 技术,可显著降低绘制指令数与填充压力。
具体案例:在 Three.js 中,MeshBasicMaterial 默认 side: THREE.FrontSide,即自动剔除背面。当开发者加载一栋楼的 BIM 模型时,墙体、楼板的背向面被自动跳过;遇到玻璃幕墙或半透明围栏时,改用 THREE.DoubleSide 保证双面可见,兼顾真实感与性能。
移动端 AR/XR 低功耗渲染
阶段:端侧推理与渲染协同优化
解决的问题:手机处理器与电池容量有限,AR 应用需要同时运行相机、SLAM 与渲染。背面剔除能减少一半以上的片元着色工作量,直接降低 GPU 功耗与发热,延长设备续航。
具体案例:在 ARKit/ARCore 应用中放置虚拟家具时,家具模型背面永远不会被用户看到(相机总是从外部观察)。默认开启背面剔除后,单个沙发的三角形绘制量从数万降至数千,帧率稳定在 30 FPS 以上,设备温度下降约 2-3 摄氏度。
Python 演示
import math def compute_normal(v0, v1, v2): # 用叉积计算三角形面法线, 方向由顶点绕序决定 u = (v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]) w = (v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]) return (u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]) def dot(a, b): # 三维向量点积 return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] def is_front_face(v0, v1, v2, camera): # 法线与视线同向则为前向面, 点积大于零保留 n = compute_normal(v0, v1, v2) view = (camera[0] - v0[0], camera[1] - v0[1], camera[2] - v0[2]) return dot(n, view) > 0 def cube_triangles(): # 立方体 8 个顶点, 索引 0-7 V = [(-1,-1,-1), (1,-1,-1), (1,1,-1), (-1,1,-1), (-1,-1, 1), (1,-1, 1), (1,1, 1), (-1,1, 1)] # 六个面, 每个面 4 个顶点按逆时针绕序(从外部看) quads = [(1,2,6,5), (0,4,7,3), (3,7,6,2), (0,1,5,4), (4,5,6,7), (0,3,2,1)] tris = [] for q in quads: tris.append((V[q[0]], V[q[1]], V[q[2]])) tris.append((V[q[0]], V[q[2]], V[q[3]])) return tris def main(): camera = (3.0, 2.0, 5.0) # 相机放在右上前方 tris = cube_triangles() kept = [t for t in tris if is_front_face(*t, camera)] culled = len(tris) - len(kept) print('total triangles:', len(tris)) print('front faces kept:', len(kept)) print('back faces culled:', culled) print('culling rate: %.1f%%' % (100.0 * culled / len(tris))) if __name__ == '__main__': main()
compute_normal(v0, v1, v2)用两条边做叉积,得到垂直三角形面的法线,方向由顶点绕序决定。dot(n, view) > 0法线与视线点积大于零表示面朝相机,这是前向面的核心判定。view = (camera[0]-v0[0], ...)构造从三角形指向相机的视线向量。quads = [(1,2,6,5), ...]每个面按“从外部看逆时针”的顺序给出 4 个顶点索引,保证法线朝外。tris.append((V[q[0]], V[q[1]], V[q[2]]))把四边形拆成两个三角形,方便逐个判定。kept = [t for t in tris if ...]列表推导一次性筛出所有前向面,模拟剔除后的幸存者。culling rate相机在右上前方时,恰好 6 个面被剔除,比例 50%,与理论吻合。
常见误区
误区我以为背面剔除能把被遮挡的物体一起省掉,比如站在墙后面的敌人,因为看不见所以会被剔除。
正解背面剔除只处理“背对相机”的三角形,墙体后面的敌人属于“被更近的物体挡住”,要靠深度缓冲在像素阶段解决。前者在光栅化前删面,后者在光栅化后挡像素,分工完全不同。橱窗比喻里,背面剔除是让店主不打扮模特后脑勺;而顾客前面站了个人挡住模特,那是换站位(遮挡)的问题。
为何常见:两者都叫“剔除”,听起来都在省掉看不见的东西,初学者极容易把职责搞混。
误区判断背面必须有三维法线和相机位置,三角形一旦投影到屏幕就丢了深度信息,怎么可能还分得清正反?
正解投影确实丢失了深度值,但保留了顶点的绕行方向。逆时针的前向面投影后仍是逆时针,背向面会变成顺时针,用有符号面积公式一个符号就能判定。GPU 正是利用这个性质,在屏幕空间完成剔除,省去一次三维点积。
为何常见:直觉上“朝向”是三维属性,很难相信二维投影还能保留方向信息,忽略了绕序的拓扑不变性。
误区法线不是美术在建模软件里指定好的吗?剔除直接用那个法线和相机点积不就行了?
正解美术提供的是顶点法线,用于光照明暗计算,与几何面的朝向无关。GPU 做背面剔除时用的是几何法线,它由三角形顶点索引顺序(绕序)经过叉积算出。顶点法线像模特脸上的妆容,绕序才是模特骨架的朝向,两者不能混用。
为何常见:图形学里“法线”一词高频出现,面法线、顶点法线、法线贴图含义各异,初学者容易张冠李戴。
误区反正背面看不见,开启剔除对所有模型都没有副作用,那就全开呗。
正解开放网格或双面材质一旦开启剔除,就会出现“面消失”的破洞——树叶只剩一半、纸张只剩一面。剔除的正确性依赖网格的封闭性和绕序一致性。叶片、布料等薄片模型必须关闭剔除或开启双面渲染,才能保证视觉完整。
为何常见:在闭合网格上测试时确实看不出差别,于是把结论错误地推广到所有网格类型。
误区只要剔除比例高,比如砍掉 80% 的三角形,性能就一定大幅提升。
正解背面剔除节约的是光栅化前的几何处理和光栅化后的像素填充。如果三角形极小、过绘制本身很低,或者瓶颈在顶点变换而非像素着色,收益会大打折扣。桌面端顶点瓶颈时剔除省下的填充不算数,必须用性能分析工具确认瓶颈所在,再决定优化方向。
为何常见:把“节省三角形数量”直接等同于“帧率提升”,忽略了渲染管线的瓶颈可能在不同阶段。
面试题
Q1. 背面剔除的主要目标是什么?
- A.删除被遮挡的像素
- B.合并相邻网格减少批次
- C.跳过背向相机的三角形← 正确答案
- D.压缩纹理以节省显存
答案:C。背面剔除在光栅化前跳过背向相机的三角形,减少几何与填充开销。被遮挡的像素由深度缓冲处理,合并批次是合批优化,压缩纹理是显存优化,三者都与背面剔除无关。橱窗比喻中,这是店主决定不给模特后脑勺化妆。
Q2. 屏幕空间中如何判断三角形是否背向相机?
- A.求三角形面积的绝对值
- B.比较三个顶点的深度值
- C.检查顶点的颜色是否相同
- D.看屏幕坐标叉积符号← 正确答案
答案:D。三角形投影到屏幕后,计算有符号面积(二维叉积),符号代表顶点绕行方向,逆时针为前向、顺时针为背向。面积绝对值丢掉了方向信息,深度值比较属于深度缓冲的职责,顶点颜色与朝向无关。
Q3. 逆时针绕序约定下,法线与视线点积大于零表示什么?
- A.三角形正对相机
- B.三角形平行于视线
- C.三角形背对相机← 正确答案
- D.三角形处于裁剪区外
答案:C。法线指向三角形正面,视线从三角形指向相机,二者点积大于零说明夹角小于 90 度,面正对相机,应当保留绘制。平行时点积接近零,背面时点积为负,裁剪区与朝向判定无关。
Q4. 背面剔除在渲染管线中的哪个阶段进行?
- A.深度测试完成之后
- B.顶点光栅化之前← 正确答案
- C.像素着色器之后
- D.纹理采样流程之后
答案:B。背面剔除发生在顶点变换之后、光栅化之前,此时三角形已投影到屏幕但尚未生成像素。深度测试、像素着色、纹理采样都在光栅化之后,顺序上晚于剔除。这相当于橱窗检查岗设在摆上展台之前。
Q5. 背面剔除无法替代以下哪种功能?
- A.顶点数据缓存机制
- B.动态合批减少切换
- C.法线贴图采样细节
- D.深度缓冲的遮挡消除← 正确答案
答案:D。背面剔除只删背向面,被前方物体遮挡的面仍需深度缓冲逐像素比较决定显隐。二者一个在光栅化前、一个在光栅化后,是互补关系。顶点缓存、合批、法线贴图都与可见性判断无关。
Q6. 渲染双面可见的树叶模型,正确的做法是?
- A.关闭该物体的剔除开关← 正确答案
- B.增加三角形的顶点数量
- C.使用深度缓冲偏移技巧
- D.强制所有面单面绘制
答案:A。树叶是开放网格,正反两面都可能被看到,必须关闭剔除或开启双面渲染,否则翻转后叶片会消失。增加顶点不改变朝向判定,深度偏移只解决面片重叠,强制单面绘制会加剧破洞问题。
Q7. 一个闭合网格在任意视角下,背面剔除的典型节省比例约为?
- A.只有一成左右
- B.全部的三角面片
- C.接近一半的三角形← 正确答案
- D.几乎没有任何节省
答案:C。闭合网格任意视角下,面向相机的前向面与背向面数量大致相等,剔除率接近 50%。一成左右偏低,全部面片不可能被剔除(否则画面全空),零节省说明网格可能全是双面材质。
Q8. 下列哪些情况下背面剔除的性能收益最明显?
- A.闭合网格大量充满屏幕← 正确答案
- B.相机围绕封闭物体运动← 正确答案
- C.物体全部使用双面材质
- D.屏幕分辨率设置极小
答案:AB。闭合网格充满屏幕时背向面占比高,剔除收益大;相机围绕封闭物体运动时始终有一半面被剔除。双面材质关闭了剔除,收益归零;分辨率极小时填充成本本就很低,剔除的填充收益不明显。
Q9. 关于法线方向与背面剔除的关系,正确的是?
- A.顶点绕序决定法线朝向← 正确答案
- B.叉积结果必须归一化后才能使用
- C.交换两顶点会翻转朝向← 正确答案
- D.法线只与顶点坐标位置相关
答案:AC。法线由顶点索引顺序经叉积算出,逆时针绕序对应正面朝向;交换任意两个顶点会反转叉积方向,从而翻转正反面。判定朝向只需要叉积符号,无需归一化;顶点坐标位置只是输入,真正决定朝向的是连接顺序。
Q10. 封闭网格使用背面剔除带来的好处有哪些?
- A.减少几何处理阶段的开销← 正确答案
- B.降低像素填充率的压力← 正确答案
- C.替代所有遮挡剔除工作
- D.消除所有边缘锯齿现象
答案:AB。背向面在光栅化前被丢弃,几何处理量减半,同时不产生任何片元,填充率压力随之降低。但被遮挡的面仍靠深度缓冲处理,背面剔除无法替代遮挡剔除;抗锯齿属于采样技术,与剔除无关。