想象一下粉刷匠老王接到一个活儿:在一面贴着整齐方格瓷砖的墙上,刷出一个巨大的三角形图案。瓷砖就是屏幕上的像素,三角形就是 3D 模型投射到屏幕上的形状。老王得先想清楚三件事:第一,哪些瓷砖该刷、哪些不该刷?第二,三角形边缘只盖住一半的瓷砖,是刷还是不刷?第三,如果客户要求渐变漆——离红角近的偏红、离蓝角近的偏蓝——每块瓷砖的颜色怎么调?这面墙上有上万个三角形要画,怎么才能刷得又快又不漏? 这套问题,正是图形渲染管线里「三角形设置与遍历」要解决的核心。三角形设置,相当于老王提前在墙上用粉笔画出三角形的三条边,并圈出一个工作范围;遍历,则是老王一格格扫过瓷砖,判断每块要不要刷漆、刷什么颜色;再配合重心坐标调色、边缘抗锯齿和平行施工,最终把一个个三角形变成屏幕上细腻逼真的画面。

岗位关联

理解光栅化的底层原理,是进入渲染引擎、实时图形学与三维 AI 感知方向的必备基础。它能帮你读懂 GPU 在“画”什么、为什么快、瓶颈在哪,无论是做游戏引擎、工业可视化还是神经渲染,都能少走很多弯路。

目录
  1. 可视化演示
  2. 渲染管线定位
  3. 三角形设置
  4. 像素遍历
  5. 重心坐标
  6. 边缘抗锯齿
  7. 并行加速
  8. 优化技巧
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示图形渲染管线光栅化核心流程:三角形边方程计算、包围盒像素遍历与重心坐标插值,步进式探索光栅化各阶段。

本节配有交互动效(进入视口后自动加载,约 3KB)。

边方程:三角形设置 三顶点确定三条边的隐式方程 E(x,y) = ax + by + c 符号判定像素在三角形内外

渲染管线定位

老王的装修队分工非常明确:量尺寸的师傅先上门,用测量仪把客厅的角角落落都定位好,在图纸上标出三个角的位置;老王是刷漆工,专门负责上墙;等漆刷完,贴壁纸的师傅再接手,把好看的纹理纸糊上去。这样一条流水线,每道工序各司其职,谁都不必操心别人的活儿。

这条流水线在图形学里叫渲染管线,里面的角色分工与装修队如出一辙。顶点处理就像量尺寸的师傅:它把 3D 模型的每个顶点做坐标变换、光照计算,最后投影到屏幕坐标,输出一个个二维三角形。老王对应的就是光栅化阶段——把二维三角形变成屏幕上覆盖的像素。而光栅化结束后的片元着色,则是贴壁纸的师傅:拿着老王标注好的像素,逐一算出最终颜色。

光栅化之所以是管线中央的瓶颈工序,是因为它的工作量与屏幕像素直接挂钩:一个三角形在屏幕上可能只占巴掌大,也可能覆盖半面墙,需要处理的像素数量从几十到几百万不等。三角形设置与遍历,正是光栅化的两半:设置是提前把三角形在屏幕上的“施工图纸”准备好,遍历是按照图纸一格格落实。到了现代 GPU 里,老王一个人变成了一整支施工队——成百上千个着色器核心同时开刷,靠的正是这套图纸足够清晰、足够规则。

三角形设置

老王接到图纸后,第一件事不是拿刷子,而是拿起粉笔,在墙上沿着三角形的三条边描出笔直的粉笔线。这三条线就是他的“边界线”:瓷砖中心落在线内侧才值得刷漆。接着他又从兜里掏出一个硬纸框,套在三角形外面——纸框正好是能框住三角形的最小矩形,老王把精力全集中在这个框里,墙的其他角落一概不看。

在渲染管线里,这三条粉笔线就是边方程。屏幕上的三角形由三个顶点定义,任意一条边都可以写成一个直线方程:把该边的两个端点代入一个特定算式,就能得到该直线的数学表达;再把任意一个像素坐标代入,得到的结果就能告诉我们这个像素在边的哪一侧。三角形有三条边,于是有三个边方程,它们共同构成内外判定规则。而那根纸框,就是包围盒——取三个顶点横纵坐标的最小值和最大值,围出一个轴对齐矩形。因为三角形一般只占据屏幕一小块,包围盒能把遍历缩小到最小必要范围。

边方程的具体计算并不神秘,它就是二维叉积的展开:

老王把三条粉笔线画完,又把纸框套好,施工范围一目了然。到了这一步,三角形的“施工图”已经齐活:边方程管边界判定,包围盒管工作范围,接下来就能一格格检查瓷砖了。

EAB(p)=(px−xA)(yB−yA)−(py−yA)(xB−xA)E_{AB}(p) = (p_x - x_A)(y_B - y_A) - (p_y - y_A)(x_B - x_A)
这个算式就像一个“左右手测试器”:把像素中心点代入,正数在边的一侧,负数在另一侧,零则正好落在线上。
  1. 构造边向量先取顶点 A 到顶点 B 的有向边向量,它指明了这条粉笔线的方向和倾斜程度
    AB⃗=(xB−xA, yB−yA)\vec{AB} = (x_B - x_A,\ y_B - y_A)
  2. 代入待测点把像素中心点 p 与顶点 A 的连线和边向量做二维叉积,叉积展开后就是上面的公式
    EAB(p)=(p−A)×AB⃗E_{AB}(p) = (p - A) \times \vec{AB}
  3. 符号定区域叉积结果的正负表示 p 落在有向边的哪一侧,0 表示恰好在线上
    EAB(p)>0  ⟹  p 在 AB⃗ 左侧E_{AB}(p) > 0 \implies p \text{ 在 } \vec{AB} \text{ 左侧}

像素遍历

画好线的老王有个好习惯:不跳着刷,也不乱点。他从纸框的左上角开始,像扫地一样,先沿着第一行从左往右一路看过去,看完一行再回到第二行开头,一行一行地扫到右下角。这样扫过每一块瓷砖,他都会停下来蹲下看一眼,瓷砖中心在粉笔线内侧就点一刷子,在外侧就跳过。

这个“扫地式”的检查过程就是像素遍历。计算机并不知道三角形内部具体覆盖了哪些像素,只能笨办法:把包围盒里每个像素都问一遍“我是不是在三角形里”。具体做法是,对每个像素取其中心点坐标,代入三个边方程,如果结果都为非负(约定三角形内部在三条边的同一侧),就判定该像素被三角形覆盖。这个判定过程称为覆盖测试。

覆盖测试的条件写成数学式是:

其中 \(w_0, w_1, w_2\) 分别是三个边函数代入像素中心后的值。

老王扫完整面墙后,三角形内部的瓷砖都被他点了漆。不过细心的他会发现一个问题:有些瓷砖真的很想被刷到——可它们的中心点恰好落在粉笔线外一点点。为了照顾这些“边缘户”,老王会在后续环节想些辙,那是抗锯齿的事;眼下遍历已经把“该扫的地方”全部扫过了,不重不漏。

w0≥0∧w1≥0∧w2≥0w_0 \ge 0 \quad \land \quad w_1 \ge 0 \quad \land \quad w_2 \ge 0
三个边函数同时非负,意思是像素中心同时位于三条有向边的“内侧”,就像老王蹲下确认瓷砖中心点在三条粉笔线围成的区域内。
  1. 取像素中心把像素看作一个小方格,取其中心作为测试点,避免方格四角跨越多块区域造成歧义
    p=(x+0.5, y+0.5)p = (x + 0.5,\ y + 0.5)
  2. 三个边函数分别代入依次把 p 代入 AB、BC、CA 三条有向边的方程
    w0=EAB(p),w1=EBC(p),w2=ECA(p)w_0 = E_{AB}(p), \quad w_1 = E_{BC}(p), \quad w_2 = E_{CA}(p)
  3. 联立判断只有当三个值都大于等于零时,p 才被判定在三角形内部
    w0≥0∧w1≥0∧w2≥0w_0 \ge 0 \land w_1 \ge 0 \land w_2 \ge 0

重心坐标

客户这次要求很高,要刷渐变漆:离红色角近的瓷砖偏红,离蓝色角近的偏蓝,离黄色角近的偏黄。老王掏出一个小天平,先把三角形大区域分成三块小三角形——从这块瓷砖的位置向三个顶点各拉一条线,就得到三块小三角形,它们的面积之比,恰好决定了三桶漆各倒多少。这个面积比例,就是图形学里的重心坐标。

重心坐标是光栅化里最重要的调色工具。像素 p 在三角形内部时,三个边函数值两两组合可以算出三块小三角形的面积,将它们归一化就得到三个权重 \(\lambda_0, \lambda_1, \lambda_2\)。这三个权重满足和为 1,且分别对应三个顶点的“影响力”:权重越大,像素越靠近那个顶点。任何顶点属性——颜色、法线、纹理坐标,都能用这三个权重做线性组合,插值出当前像素处应有的值。

重心坐标的计算公式如下:

不过,老王很快发现一个陷阱:墙是平面的,但客户的图案是 3D 投影到墙上的,直接用屏幕上的平面比例调色,会让远处的地砖纹理“漂移”。要修正这个问题,需要透视校正:在插值前,先把每个顶点的属性值除以该顶点在裁剪空间的 w 分量(它近似等于深度),插值完成后再除以权重的 w 倒数之和。这样,3D 空间里原本均匀的纹理,投到屏幕上才依然均匀。

有了老王的这杆“天平”,每一块瓷砖的色号都精确无比。这个看似简单的面积比例,撑起了整个片元着色阶段对颜色、深度、法线等无数属性的插值需求。

λ0=S0S0+S1+S2,λ1=S1S0+S1+S2,λ2=S2S0+S1+S2\lambda_0 = \frac{S_0}{S_0 + S_1 + S_2}, \quad\lambda_1 = \frac{S_1}{S_0 + S_1 + S_2}, \quad\lambda_2 = \frac{S_2}{S_0 + S_1 + S_2}
透视校正的直觉:每个顶点的属性先按深度“压缩”,混合后再按深度“拉开”,还原出正确的外观。这种插值也是深度测试里深度值插值的标准做法。
  1. 顶点属性的深度压缩每个顶点的属性值先除以它的裁剪坐标 w,让离相机远的数据在混合时权重变小
    s0w0,s1w1,s2w2\frac{s_0}{w_0}, \quad \frac{s_1}{w_1}, \quad \frac{s_2}{w_2}
  2. 用重心坐标做线性混合对压缩后的数值做普通线性插值
    λ0s0w0+λ1s1w1+λ2s2w2\lambda_0 \frac{s_0}{w_0} + \lambda_1 \frac{s_1}{w_1} + \lambda_2 \frac{s_2}{w_2}
  3. 恢复正确的插值结果再除以同样插值得到的 \(1/w\),完成深度修正
    s=∑λisi/wi∑λi/wis = \frac{\sum \lambda_i s_i / w_i}{\sum \lambda_i / w_i}

边缘抗锯齿

刷完大半面墙后,老王退后几步端详,发现三角形边缘的瓷砖出现了明显的“狗啃”——有的瓷砖只盖住一小半也被刷成了全漆,远看就是参差不齐的阶梯。他挠挠头:这个问题不在粉笔线,而在瓷砖本身的判定规则。每一块瓷砖要么全刷要么不刷,覆盖率只有 0% 和 100% 两个档位,边缘自然毛糙。

这就是锯齿的本质:像素是离散的,三角形边缘与像素网格相交处,覆盖率无法用连续值表达,只好近似成“非黑即白”。要磨平它,老王想了个聪明的办法:把每块瓷砖想象成有四个采样点(四个角),数一数有几个采样点落在粉笔线内,再按比例决定刷多浓——四个点都在内的刷全漆,三个点在内刷七成浓,两个点在内刷半浓。边缘的瓷砖从“全有或全无”变成了“浓淡渐变”,远看就是平滑的直线。

这个办法就是超采样抗锯齿(MSAA),覆盖率公式写出来是:

老王的“半浓油漆”策略相当划算:整块在内部的瓷砖仍只算一次颜色,只有边缘瓷砖才需要多算几次覆盖率。他满意地再看看墙,三角形的边缘已经光滑得像用尺子比着画出来的。这个磨平锯齿的功夫,GPU 硬件里早已内置了 4x、8x 甚至 16x 的采样模式,让现代游戏画面的边缘又细又顺。

\text{coverage} = \frac{1}{N} \sum_{i=1}^{N} I(s_i), \quadI(s_i) = \begin{cases} 1 & s_i \in \triangle \\ 0 & s_i \notin \triangle \end{cases}
覆盖率就是像素内落在三角形里的采样点占比。老王“盖多少刷多少”的原则,让边缘颜色从全色平滑过渡到背景色,视觉上磨掉了锯齿。
  1. 在像素内布置采样点每个像素内部放 N 个采样位置,常见 4x MSAA 就放 4 个
    {s1,s2,s3,s4}\{s_1, s_2, s_3, s_4\}
  2. 逐一测试归属每个采样点用边函数判断是否位于三角形内,在内记 1,在外记 0
    I(si)=1(si∈△)I(s_i) = \mathbb{1}(s_i \in \triangle)
  3. 求覆盖率把 N 个采样点的归属结果取平均,得到 0 到 1 之间的覆盖率
    coverage=14∑i=14I(si)\text{coverage} = \frac{1}{4} \sum_{i=1}^{4} I(s_i)

并行加速

一人一刷,把这面墙刷完得干到天黑。老王灵机一动:不如叫上几个徒弟合伙干。他把墙用粉线切成若干等大的方块,小张负责左上角那一块,小李负责右上角,小赵负责下半部分——大家各刷各的格子,互不干扰,一上午就能收工。这就是分块并行的思路:把大任务切成小块,分给不同的工人同时处理。

GPU 里的光栅化沿用了同样的智慧。现代 GPU 有上千个计算核心,屏幕画面被切成许多渲染分块(tile),每个分块交给一个或多个核心独立完成光栅化与着色。分块带来的额外好处是数据局部性好——同一块屏幕区域的数据能待在高速缓存里,不必频繁回显存。一个分块处理完,整块像素一起写回,带宽开销也小。

分块方案的计算很简单:

其中 \(W\) 是屏幕宽,\(H\) 是屏幕高,\(B\) 是分块边长(如 8、16、32 像素)。

除了分块,光栅化还有两个天然的并行层次。一是像素级并行:三角形内部像素各自独立计算,没有先后依赖,一个三角形里几百个像素可以同时做覆盖测试;二是三角形级并行:场景里的三角形彼此独立,GPU 会把一批三角形一次性分给众多核心同时处理。老王的小团队就这样把三个层次的并行叠加起来——每人刷一块,每块里好几个动作同时做,效率提升几十倍不止。

Ntiles=⌈WB⌉×⌈HB⌉N_{\text{tiles}} = \left\lceil \frac{W}{B} \right\rceil \times \left\lceil \frac{H}{B} \right\rceil
分块数等于横向块数乘以纵向块数。GPU 的调度器就是按这个数目把任务分发给各个并行单元的。
  1. 确定分块尺寸每个分块是边长 B 像素的正方形区域
    tile size=B×B\text{tile size} = B \times B
  2. 计算行列块数屏幕宽除以 B 后向上取整,得到每行分块数;高同理
    nw=⌈W/B⌉,nh=⌈H/B⌉n_w = \lceil W / B \rceil, \quad n_h = \lceil H / B \rceil
  3. 总块数行列块数相乘,即任务总数
    Ntiles=nw×nhN_{\text{tiles}} = n_w \times n_h

优化技巧

老王是个有心人,干活干多了,总结出三招省力秘诀。第一招:一行瓷砖扫到一半,发现已经出了粉笔线范围,后面几块不用再蹲下看,直接跳到下一行;这就是遍历中的早退测试。第二招:同一行的相邻瓷砖,蹲下判断时发现每次的计算结果都有规律——往右挪一块瓷砖,边函数的数值只需要在上一个基础上加一个固定数,根本不必重新算一遍公式;这就是增量计算。第三招:他把天平、漆桶全放在手边,不走冤枉路;在计算机里,这叫数据局部性——让访问过的数据待在缓存里,下次取用就快。

增量计算的数学直觉很优雅。边函数的表达式里,横坐标每增加 1,结果的变化量恰好是边的纵向分量;纵坐标每增加 1,结果的变化量是边的横向分量的相反数。于是计算量从 O(1) 还是 O(1),但省掉了整条公式里多次乘法和减法,也让流水线更容易做硬件优化。公式如下:

早退测试在大型三角形上效果显著:如果某个分块整体都在三角形内部,就省去逐像素的边方程测试,直接整块填充;如果整体都在外部,也可以整块跳过。老王把这套“省力三招”教给了徒弟们,刷墙队伍从此又快又稳,这正是光栅化工程里性能优化的核心思路——能少算的绝不多算,能递推的绝不重算,能缓存的一定缓存。

E(x+1,y)=E(x,y)+(yB−yA)E(x+1, y) = E(x, y) + (y_B - y_A)
E(x,y+1)=E(x,y)−(xB−xA)E(x, y+1) = E(x, y) - (x_B - x_A)
这两个递推式让老王只需要“记住上一个数,加个小常数”就能得到下一个像素的边函数值,比反复套用完整公式快得多。
  1. 向右走一步横坐标增加 1 时,边函数增加一个与边向量纵向分量相等的常数
    E(x+1,y)=E(x,y)+(yB−yA)E(x+1, y) = E(x, y) + (y_B - y_A)
  2. 向下跨一行纵坐标增加 1 时,边函数减少一个与边向量横向分量相等的常数
    E(x,y+1)=E(x,y)−(xB−xA)E(x, y+1) = E(x, y) - (x_B - x_A)
  3. 两个常数提前算好这两个增量在三角形设置阶段就固定下来,遍历时只需做加法
    Δx=yB−yA,Δy=xB−xA\Delta_x = y_B - y_A, \quad \Delta_y = x_B - x_A

AI 应用场景

游戏引擎实时渲染

阶段:模型训练与推理前的图形数据采样

解决的问题:游戏引擎需要每秒钟把上百万个三角形光栅化成 1080p 甚至 4K 画面的像素。传统手工绘制无法实时响应玩家操作,而 GPU 利用三角形设置与遍历的并行流水线,在 16 毫秒内完成整帧渲染。

具体案例:在 Unity 或 Unreal 引擎中,角色模型由数万个三角形组成,摄像机视角一变,光栅化阶段立刻重新计算每个三角形的覆盖像素,再交给片元着色器输出颜色。优化三角形布局、减少过度绘制,都要基于对遍历和覆盖测试的理解。

自动驾驶点云可视化与 BEV 投影

阶段:感知数据的预处理与可视化

解决的问题:LiDAR 点云是无序的 3D 点集,直接送入神经网络前往往要栅格化成规则的鸟瞰图(BEV)。传统做法是手工体素化,速度慢且信息有损;利用三角网格化与光栅化投影,可以快速把点云路面重建为三角形网格,再用三角形设置与遍历把网格投到 2D 栅格,供检测模型使用。

具体案例:自动驾驶测试中,工程师将路测点云重建为地面三角形网格,光栅化后叠加到前视摄像头画面上,生成带深度标注的训练数据,供 BEV 感知模型学习。

神经渲染的合成数据生成

阶段:训练数据的生成与增强

解决的问题:NeRF、3D Gaussian Splatting 等神经渲染模型需要大量多视角图像,但真实采集成本高。传统方式手动摆放并渲染 3D 场景,依赖 CPU 渲染速度慢;利用 GPU 光栅化流水线,三角形设置与遍历能以极快速度批量生成不同视角、不同光照的合成图像。

具体案例:某机器人团队在仿真环境中摆放数千个虚拟物体,用光栅化管线从 200 个随机视角渲染出图像,自动标注出每个像素的物体 ID 和深度,作为视觉模型的训练集,一天即可生成过去一个月才能攒下的数据量。

Python 演示

python62 行
class SoftwareRasterizer:    def __init__(self, width, height):        self.width = width        self.height = height        self.frame = [[0.0] * width for _ in range(height)]     def edge_func(self, a, b, p):        return (p[0] - a[0]) * (b[1] - a[1]) - (p[1] - a[1]) * (b[0] - a[0])     def bounding_box(self, v0, v1, v2):        xs = [v0[0], v1[0], v2[0]]        ys = [v0[1], v1[1], v2[1]]        min_x = max(0, int(min(xs)))        max_x = min(self.width - 1, int(max(xs)))        min_y = max(0, int(min(ys)))        max_y = min(self.height - 1, int(max(ys)))        return min_x, min_y, max_x, max_y     def rasterize(self, v0, v1, v2):        area = self.edge_func(v0, v1, v2)        if abs(area) < 1e-9:            return        min_x, min_y, max_x, max_y = self.bounding_box(v0, v1, v2)        for py in range(min_y, max_y + 1):            for px in range(min_x, max_x + 1):                p = (px + 0.5, py + 0.5)                w0 = self.edge_func(v1, v2, p)                w1 = self.edge_func(v2, v0, p)                w2 = self.edge_func(v0, v1, p)                if w0 >= 0 and w1 >= 0 and w2 >= 0:                    lambda0 = w0 / area                    lambda1 = w1 / area                    lambda2 = w2 / area                    self.frame[py][px] = lambda0 * 0.8 + lambda1 * 0.3 + lambda2 * 0.1     def to_ascii(self):        chars = " .:-=+*#%@"        rows = []        for py in range(self.height):            row = []            for px in range(self.width):                v = self.frame[py][px]                if v <= 0:                    row.append(".")                else:                    idx = min(len(chars) - 1, int(v * (len(chars) - 1)))                    row.append(chars[idx])            rows.append("".join(row))        return "\n".join(rows)  def main():    rast = SoftwareRasterizer(56, 24)    v0 = (4.0, 22.0)    v1 = (50.0, 1.0)    v2 = (52.0, 23.0)    rast.rasterize(v0, v1, v2)    print(rast.to_ascii())  if __name__ == "__main__":    main()
目的:用纯 Python 实现一个迷你软件光栅化器,演示边方程、包围盒、覆盖测试和重心坐标插值四大核心步骤,输出字符画渲染结果。
  1. def edge_func(self, a, b, p)计算边函数,返回 p 在有向边 a→b 的哪一侧,正值表示左侧。
  2. def bounding_box(self, v0, v1, v2)取三个顶点在 x、y 方向的最小/最大值,生成遍历用的包围盒。
  3. if w0 >= 0 and w1 >= 0 and w2 >= 0:三个边函数同时非负,判定像素中心在三角形内部。
  4. lambda0 = w0 / area用边函数比值直接得到重心坐标的第一个分量,用于后续颜色插值。
  5. self.frame[py][px] = lambda0 * 0.8 + lambda1 * 0.3 + lambda2 * 0.1按重心坐标混合三个顶点的亮度值,形成渐变填充。
  6. to_ascii()把浮点亮度映射到字符密度,输出可视化结果,亮度高用 @,亮度低用空格。

常见误区

误区我总觉得只要像素方块跟三角形有一点点重叠,这个像素就应该被填色,毕竟“眼见为实”,看起来碰到了嘛。

正解标准光栅化的判定依据是像素中心点(或采样点)是否落在三角形内部,而不是像素方块是否与三角形相交。这是因为 GPU 需要一种确定性的规则避免邻接三角形同时点亮同一个像素;如果按方块相交来算,三角形会“膨胀”一圈,而且边缘的归属会摇摆不定。

为何常见:生活中判断“这块砖要不要刷漆”看的是砖面是否被颜料碰到,而 GPU 是从采样点出发的数学模型,两者天然不同,初学者最容易踩这个坑。

误区重心坐标的数值大,说明这个点离对应顶点更远,就像距离一样,重心坐标衡量的是几何远近。

正解重心坐标是三个子三角形面积的比例,是归一化到和为 1 的仿射量。它确实与“靠近哪个顶点”正相关,但并不是欧氏距离。两个点在三角形内可能重心坐标相同,而实际到顶点的直线距离完全不同;重心坐标的价值在于它随位置线性变化,方便插值。

为何常见:从“离顶点近颜色占比高”这个直觉出发,容易把它理解成距离的度量,忽略了它其实是面积比的数学本质。

误区抗锯齿嘛,就是让画面更清晰,把渲染分辨率提高一些再缩放下来,边缘自然就平滑了。

正解MSAA(多重采样抗锯齿)并不是简单提升分辨率或全屏模糊。它的做法是在像素内部放置多个采样点,仅对处于三角形边缘的像素计算覆盖率并混合颜色;像素内部完全不额外做片元着色。这比全屏超采样便宜得多,也不会让画面变糊。

为何常见:从“锯齿是像素不够密”出发,自然想到提高分辨率这条最直接的路,而没意识到问题出在边缘覆盖率无法连续表达,MSAA 恰恰是在这个局部位置上做文章。

误区遍历到某一行,发现有一个像素不在三角形里,就提前跳到下一行,那这一行后面本来该刷的像素岂不就漏了?

正解光栅化中的提前终止并不是“遇到一个不覆盖就放弃整行”,而是利用三角形在行方向的跨度信息,只遍历当前扫描线与三角形相交的有效区间。配合增量边方程,可以精准知道一行的起点和终点,跳出区间外的无效测试,绝不会漏掉内部像素。

为何常见:字面意思上“提前终止”容易被误解为无限期提前收工,实际它是建立在几何单调性之上的区间裁剪,属于遍历的数学性质而非粗暴剪枝。

面试题

Q1. 光栅化阶段最初的输入是什么?

单选题 · easy

  • A.屏幕上的四边形网格
  • B.变换后的二维三角形← 正确答案
  • C.顶点着色后的像素颜色
  • D.处理完毕的纹理贴图列表

答案:B。光栅化的输入是顶点处理完成后、投影到屏幕坐标的二维三角形。四边形不是基本图元,像素颜色是光栅化之后片元着色才有的产出,纹理贴图也只是辅助属性,选 B 最准确。

Q2. 边函数 E_AB(p) > 0 表示点 p 位于边 AB 的哪一侧?

单选题 · easy

  • A.位于三角形的外部区域
  • B.位于有向边 AB 的右侧
  • C.位于有向边 AB 的左侧← 正确答案
  • D.恰好落在边 AB 的中点上

答案:C。边函数本质是向量 AB 与向量 Ap 的二维叉积,正负对应左右侧。约定正值表示点在有向边的左侧,负值在右侧,零在线上。这点决定了三角形覆盖测试的方向约定,也解释了为什么换顶点顺序会得到相反符号。

Q3. 标准光栅化中判断一个像素是否被三角形覆盖的依据是?

单选题 · easy

  • A.像素四个角都落在三角形内
  • B.像素中心点位于三角形内部← 正确答案
  • C.像素与三角形有面积重叠
  • D.三角形边长超过像素边长

答案:B。标准做法是取像素中心点作为采样点,用边方程判定其是否位于三角形内部。四角全在过于严格,面积重叠会过度扩张三角形,边长对比与覆盖无关。中心点规则是硬件实现里最常用的确定性测试。

Q4. 关于重心坐标,下列说法正确的是?

多选题 · mid

  • A.可用来插值颜色和深度等顶点属性← 正确答案
  • B.可用来求三角形外接圆的半径尺寸
  • C.三个分量非负且和为 1 时可以判定点在三角形内← 正确答案
  • D.数值大小与摄像机远近有直接正比关系

答案:AC。重心坐标是面积比的归一化,天然用来做属性插值,A 正确;同时它也是内外判定的一种形式,三个分量都非负等价于点在三角形内部,C 正确。重心坐标与外接圆、相机深度没有直接关系,B、D 错误。

Q5. 4x MSAA 在每个像素内部做了什么事?

单选题 · mid

  • A.把整个画面连续渲染四遍再合并
  • B.在边缘像素内放 4 个采样点求覆盖率← 正确答案
  • C.把画面放大 4 倍再进行全屏锐化
  • D.计算四张不同分辨率的缩略图

答案:B。4x MSAA 在每个像素内布置 4 个采样点,在三角形边缘处统计落在内部的采样点比例得到覆盖率,并将覆盖率用于颜色混合。只有边缘像素承担额外开销,内部像素依然只做一次片元着色。

Q6. 包围盒在光栅化遍历中的主要作用是?

单选题 · mid

  • A.防止三角形顶点越出视口边界
  • B.把遍历范围缩小到三角形外接矩形← 正确答案
  • C.预先存储三角形所有的像素颜色
  • D.提升顶点坐标变换的精度等级

答案:B。包围盒是三角形顶点横纵坐标的最小外接轴对齐矩形。光栅化只在这个矩形内扫描,避免了全屏幕逐像素测试的无谓开销,是遍历加速的第一层保障。

Q7. 下列哪些手段可以加速像素遍历阶段?

多选题 · mid

  • A.用包围盒限制扫描范围← 正确答案
  • B.用增量递推代替逐像素重算边方程← 正确答案
  • C.把屏幕划分为分块并行处理← 正确答案
  • D.给每个顶点重新分配索引编号

答案:ABC。包围盒缩小扫描范围、增量递推减少单个像素计算量、分块并行提升多核心利用率,都是经典加速手段。顶点索引重排主要影响顶点缓存命中率,与像素遍历阶段无直接关系。

Q8. 透视校正插值的根本目的是?

单选题 · hard

  • A.让远处的物体在屏幕上变得更小
  • B.修正深度缓冲区的比较精度
  • C.让纹理坐标在透视投影下保持正确← 正确答案
  • D.减少三角形边缘出现的锯齿现象

答案:C。屏幕空间线性插值在透视投影下并不等于三维空间线性变化,直接插值会导致纹理扭曲。透视校正把顶点属性先除以 w 再插值,最后除以插值后的 1/w,从而把屏幕像素正确映射回三维空间的原始位置。

Q9. 关于边函数的增量计算,下列说法正确的是?

多选题 · hard

  • A.同一行内相邻像素的边函数差值恒定← 正确答案
  • B.每个像素的边函数必须从头完整重算
  • C.跨行移动也会产生一个固定差值← 正确答案
  • D.增量只对三角形的顶点有效果

答案:AC。边函数 E(x,y) 对 x 和 y 的偏导都是常数,所以同行相邻像素只差一个固定增量,跨行同理。正因如此,硬件遍历器只需维护几个整数累加器,不必每像素重算公式,A、C 正确而 B 错误;增量法与顶点本身无关,D 错误。

Q10. 覆盖测试中采用“三个边函数都大于等于 0”而非“大于 0”的原因是什么?

单选题 · hard

  • A.保证点在三角形内部时数值更大
  • B.避免共享边上的像素被重复点亮← 正确答案
  • C.让三角形的颜色插值更均匀
  • D.提高包围盒命中率

答案:B。零值代表点恰好在边上。若边界像素被两个相邻三角形同时判定为内部,就会出现重复绘制与闪烁。通过约定零值归一侧三角形,再配合“左上填充规则”,可以确保共享边上的像素只属于一个三角形,从而避免重复光栅化。