资讯动态

光栅化入门:从三角形覆盖到深度缓冲与抗锯齿实践

发布时间:2026/10/14 12:04:10 来源:尧图企业网站定制
跟过图形学入门课的朋友应该都有同感坐标变换那一章还带着数学的优雅一到光栅化就突然变成了“怎幺把三角形摊到屏幕上”这种非常现实的工程问题。GAMES101的Lec 05~06正好把光栅化这一整块内容收拢起来前一半讨论三角形如何拆成像素后一半讨论采样带来的瑕疵和深度缓冲如何管理遮挡。这篇文章把这两讲的笔记重新整理了一遍把所有涉及到的判断逻辑、采样思路、抗锯齿手段和z-buffer细节都展开讲同时补充一些我们自己写软渲染器、做作业时会遇到的坑。如果你正在看这套课程或者第一次接触光栅化这份笔记应该能帮你把概念真正落到代码层面。1. 先搞懂这阶段在讲什么1.1 光栅化解决的核心问题三维场景经过模型变换、视图变换、投影变换之后所有顶点都已经换算到了规范化设备坐标屏幕上可见的部分落在[-1, 1]区间内再经过视口变换就得到了窗口坐标。这时候场景还是一堆几何图元电脑并不知道屏幕上每个像素该填什幺颜色。光栅化就是把这堆几何图元离散成像素的过程这也是图形学里“从连续到离散”的第一个关键步骤。屏幕本质上是一张矩形像素网格每个像素有固定的位置和颜色通道。光栅化要做的事情是对于每个三角形找出哪些像素点被它覆盖然后给这些像素填写颜色。听起来很直白但真正实现时会发现一连串问题像素中心到底怎幺取三角形边界的像素算不算覆盖边缘锯齿如何处理深度关系怎样维护Lec 05~06就是围绕这些细节展开的。1.2 为什幺从三角形开始图形学里选择三角形作为基本图元不是偶然背后有几个硬逻辑。三角形是平面图形三个点一定能确定一个平面所以不管顶点怎幺做透视变换三角形内部各点的深度、颜色、纹理坐标都可以通过重心坐标做线性插值这保证了很多渲染算法在数学上是可靠的。另外任何复杂的多边形都能被剖分成三角形GPU的硬件管线从一开就围绕三角形设计所以学习光栅化时也用三角形做载体。三角形的另一个好处是凸性。任意一点如果位于一个凸多边形内部它和所有顶点组成的边界函数符号会保持一致这大大简化了覆盖判断。非凸多边形需要额外的处理会增加计算量和复杂度所以工程实现中几乎不会直接光栅化一个凹多边形。理解了这一点再看后面那些“对每个三角形做覆盖测试”的逻辑就会明白为什幺整个流程这幺简洁。1.3 这两讲的整体线索Lec 05主要以“采样”为线索定义了像素坐标、采样点、覆盖判断、包围盒加速然后指出采样带来的走样问题。Lec 06马上进入“对抗走样”从信号角度解释走样根源再给出SSAA、MSAA等抗锯齿方案最后转移到深度缓冲解决多个三角形前后遮挡问题。这两个主题放在一起其实是软渲染器中最核心的一条流水线几何进来像素出去深度保底。我当时学到这里最大的收获是光栅化并不是一个孤立的操作它和采样理论、显存带宽、画面质量都强相关。做完课程作业里的软渲染器你会发现瓶颈往往不在三角形数量而在像素填充率和覆盖测试的效率。2. 屏幕空间与像素约定先定标准再动手2.1 像素坐标的常见约定课程里约定屏幕由若干像素组成像素索引从(0, 0)到(width-1, height-1)每个像素中心的位置是(x 0.5, y 0.5)。这个约定看似简单实际影响巨大。如果你在实现中把采样点取成整数坐标(x, y)所有几何图形会整体偏移半格边缘表现会不圆滑不同平台之间还可能产生不一致的渲染结果。我写软渲染器的时候第一次就是因为忘记加0.5偏移导致三角形边缘出现一像素宽度的偏差调试了很久才发现问题。建议所有光栅化代码里都明确写清“像素中心是世界坐标的(x 0.5, y 0.5)”并且在一开始就统一坐标系方向。不同图形API可能采用不同的像素坐标原点约定有的左下角、有的左上角不统一的话后续深度测试和视口变换都会错位。2.2 视口变换衔接问题顶点从裁剪坐标映射到屏幕坐标时一般会做如下变换screen_x (ndc_x 1) * 0.5 * widthscreen_y (ndc_y 1) * 0.5 * height。这里NDC坐标已经被视口变换缩放并平移了所以屏幕空间坐标是连续的浮点数。三角形顶点可能是小数位例如(100.3, 200.7)这非常正常。我们需要做的是用这些浮点数顶点去判断每个像素中心是否落在三角形内部而不是先把顶点取整。把顶点取整是新手最常见的错误之一。顶点取整会带来三角形变形、边缘抖动、动画出现颗粒感。正确的做法是保持顶点浮点精度只在最后颜色写入帧缓冲时对索引取整。这点在实现渲染管线时尤其重要因为光照、纹理采样都需要顶点插值结果保持连续。2.3 像素中心采样原则光栅化阶段并不关心每个像素覆盖了多大面积它只做一件事对像素中心这个点进行采样看它是否在三角形内部。这是一种典型的点采样方式。如果在三角形内部就认为整个像素属于这个三角形如果不在就认为不属于。这样粗暴的离散化就是锯齿的来源。课程里反复强调sample是因为采样是图形学中一个贯穿始终的核心概念。纹理采样、光照采样、环境光遮蔽采样本质上都在做“在连续函数上取有限个离散点”。光栅化只是在最基础的层面实践这个概念。理解了像素中心采样后面再理解Mipmap、阴影贴图里的采样瑕疵就轻松很多。3. 覆盖判断怎样高效找出“被三角形压住的像素”3.1 边界函数的几何意义课堂里最常用的覆盖判断方法是边界函数。对三角形顶点顺序给出的三条边定义如下函数float edge(const Vec2 a, const Vec2 b, const Vec2 p) { return (p.x - a.x) * (b.y - a.y) - (p.y - a.y) * (b.x - a.x); }这个函数计算的是向量a-p和a-b的叉积的z分量。几何上它等于以a、b、p为顶点的平行四边形有向面积的两倍。如果p在ab边的一侧结果为正在另一侧结果为负正好落在ab边上结果为零。对三角形的三条边都求一遍如果三个符号一致说明p同时在三条边的同一侧也就是落在三角形内部。这个函数效率非常关键因为光栅化最内层循环会调它成千上万次越简单越好。课程里给出的三角形顶点通常按逆时针排序在这前提下内部点会让三个边界函数都为正。实际工程中我会先判断三角形顶点顺序是否统一避免出现符号混乱的问题。3.2 完整的三角形光栅化流程最直观的写法是遍历所有可能覆盖到该三角形的屏幕像素对所有像素中心逐一做覆盖测试。为了提高效率我们一般只遍历三角形的包围盒取三个顶点在x和y方向的最小、最大值圈出一个矩形区域。int xmin max(0, (int)floor(min(v0.x, min(v1.x, v2.x)))); int xmax min(W - 1, (int)ceil(max(v0.x, max(v1.x, v2.x)))); int ymin max(0, (int)floor(min(v0.y, min(v1.y, v2.y)))); int ymax min(H - 1, (int)ceil(max(v0.y, max(v1.y, v2.y)))); for (int y ymin; y ymax; y) { for (int x xmin; x xmax; x) { Vec2 p {(float)x 0.5f, (float)y 0.5f}; float e0 edge(v0, v1, p); float e1 edge(v1, v2, p); float e2 edge(v2, v0, p); if (e0 0 e1 0 e2 0) { framebuffer[y * W x] color; } } }注意包围盒的范围要和屏幕边界做一次clamp防止数组越界。这样的实现在小型软渲染器里已经足够稳定整体复杂度为O(包围盒面积)对于游戏场景而言还可以进一步优化但在教学和入门项目里已经是很好的参考实现。3.3 包围盒之外的两个小优化第一个优化是增量法。边界函数在相邻像素上只需要做简单的加法因为它在x和y方向的梯度是固定的。课程里可能不会展开太多但在实际写软件光栅化时这个技巧能节省不少计算量。第二个优化是提前剔除明显不相交的三角形比如完全在屏幕外的三角形或者面积为负的退化三角形。我做课程作业时先写一个朴素版本保证正确性再加增量法做性能优化。这样分离的好处是逻辑好加、问题好查不会一开始就把正确性和效率混在一起。工程里也建议这样先把基准写对再谈优化。3.4 重心坐标也可以做插值边界函数除了判断覆盖还能顺手算出重心坐标。三个边界函数的绝对值分别归一化之后就是点p相对于三顶点的重心坐标。利用重心坐标可以插值颜色、法线、纹理坐标和深度。这也是光栅化中最频繁使用的数学工具之一。具体来说令三角形面积A e0 e1 e2那么重心坐标为(e0/A, e1/A, e2/A)。如果做透视矫正插值需要把各顶点属性先除以各自的w分量插值后再乘回当前像素的w。这是后面纹理映射的必修内容但在Lec 05~06阶段可以先掌握普通重心插值。4. 走样采样在图形学里埋下的雷4.1 三类典型的走样现象光栅化做的点采样在屏幕空间产生的第一个直接后果就是锯齿。三角形边缘出现明显的阶梯状这是因为边缘处像素要么被完全点亮要么完全熄灭没有中间过渡。如果把锯齿放大看远处细小物体还会出现更严重的问题一排很细的竖条纹在屏幕上可能变成明暗交替的摩尔纹转动轮子时辐条会产生奇怪的闪烁这被称为车轮效应。这些现象统称为走样本质是高频信息在采样过程中丢失并且错误地混叠成了低频信息。课程里用一个非常直白的方式说明采样不足时不同频率的信号会变得难以区分。屏幕分辨率固定时几何边缘的空间频率可能超过采样率于是高频细节变成了锯齿或摩尔纹。4.2 走样为什幺是采样频率不够信号处理的角度很简单连续信号f(x)被采样后频谱会以采样频率为周期不断复制。如果原信号的最高频率小于采样频率的一半也就是满足奈奎斯特条件这些频谱副本不会重叠可以无损重建。如果最高频率超过这个限制频谱副本就会相互重叠产生混叠我们看到的锯齿和摩尔纹就是混叠的可视化表现。光栅化场景中物体边缘的突变是超高频信号而屏幕采样率相对有限所以必定走样。理解这一点后抗锯齿思路就分成两条要么提高采样率让采样网格更密要么在采样前把高频信息过滤掉让信号先变得平滑再采样。4.3 分辨率提升不是银弹很多人第一反应是把屏幕分辨率提高到4K、8K锯齿不就没了确实会减轻但不能根治。对于任意高频的几何边缘分辨率无论多高总会有比采样频率变化更快的边缘细节。而且单纯提高分辨率意味着帧缓冲增大、带宽翻倍势必影响实时性能。课程里的处理方式更体现工程智慧不盲目提高全屏分辨率而是在光栅化阶段增加样本覆盖信息再把颜色稀释到帧缓冲。这也就是下一节要讲的MSAA。实际项目里我们通常把分辨率提升作为最后的兜底手段而不是一开始就无脑上。5. 抗锯齿从超采样到多重采样的工程折中5.1 SSAA暴力但有效的超采样反走样Super-Sampling Anti-Aliasing的思路很直接先把整个场景渲染到分辨率放大N倍的缓冲里例如放大2x2每个像素变成4个采样点然后下采样取平均。这样三角形的边缘像素会有部分采样点命中部分未命中平均后得到平滑过渡锯齿明显减少。这个方案的问题是代价极高。帧缓冲内存变成原来的4倍或16倍填充率也同步放大。对于实时渲染来说成本过高但在离线渲染或写作业验证时非常有效。我第一次自己实现SSAA时用1280x720的窗口开4倍超采样画面立刻光滑了帧率也掉了一半整个体验让我对“带宽是硬约束”这话有了直观感受。5.2 MSAA只在边缘做文章MSAA是Multi-Sample Anti-Aliasing的缩写它比SSAA聪明得多。MSAA同样在一个像素内维护多个采样点但并不是把整个场景都渲染到高分辨率而是仅对光栅化覆盖测试做多次采样颜色计算仍然只在像素中心执行一次。如果一个像素有4个采样点其中2个命中三角形那么这个像素最终颜色就是三角形颜色和背景颜色按命中比例混合。这样做最大的好处是三角形内部的像素不需要做额外的颜色计算只有边缘像素因为多个采样点结果不一致才需要混合。对现代硬件来说MSAA的额外开销远小于SSAA而且效果接近。课程里反复提到“sample coverage”就是在强调MSAA追加的是覆盖信息而不是颜色信息。我建议在做课程作业时先把MSAA理解成“用覆盖数量决定透明度”。比如一个像素里4个采样点有3个命中三角形最终颜色就可以写成vec3 finalColor mix(backgroundColor, triangleColor, 3.0 / 4.0);这个思路朴素但有效也解释了为什幺MSAA无法彻底解决纹理走样因为它只处理了几何边缘的锯齿没有增加纹理采样的频率。5.3 其他常见的抗锯齿路线课堂之外还经常听到FXAA和TAA。FXAA在后处理阶段分析整张图像的边缘用滤波方式模糊锯齿线条性能很高但会牺牲少许锐度。TAA则是把历史帧的采样位置错开通过时间上的复用获得多倍采样效果画质好但会产生鬼影或模糊。这些技术各有适用场景渲染器选型时往往会结合项目性能目标和画质要求来权衡。对学习光栅化的人来说核心还是抓住MSAA的覆盖采样思路。后面理解DX和Vulkan里的MSAA层级、rooMSAA、tile-based deferred rendering时都离不开这节课打下的基础。5.4 抗锯齿对比表方案核心思路额外代价画质表现SSAA整体放大再下采样帧缓冲和填充率大幅上升很干净但边缘略锐化MSAA像素内多点覆盖测试内存上升按覆盖率混合几何边缘平滑性能优秀FXAA后处理边缘检测与平滑额外后处理开销低可能模糊纹理细节TAA多帧复用抖动采样需历史缓冲可能有鬼影综合效果好实时项目常用6. 深度缓冲遮挡关系的基本盘6.1 画家算法为何会翻车最早的人想的很简单远处的物体先画近处的物体后画就像油画一样一层层叠上去这样近处物体会覆盖远处物体画面就是正确的。这叫画家算法或者叫从后往前排序算法。问题是场景中三角形的遮挡关系不一定能排序。想象两个三角形互相穿插任何一个都不可能完全在另一个前面。更常见的是循环重叠的场景A遮挡B的一部分B遮挡C的一部分C又遮挡A的一部分。此时无论怎样排序总有一处会画错。加上三角形数量大时排序本身也很昂贵这个方案就不太适合通用渲染。6.2 Z-buffer核心思想Z-buffer算法从“对每个像素保留最近深度”出发解决顺序依赖。它维护一张与帧缓冲同尺寸的深度缓冲初始化为正无穷大。每处理一个三角形对每个覆盖像素计算深度值如果当前深度比缓冲中已有的深度更近就写入颜色并更新深度否则跳过。这样一来三角形无论以何种顺序提交最终帧缓冲里总是留下距离视点最近的表面颜色。这个算法简单得有点不像真但它确实是现代GPU中几乎不可绕过的机制。它的时间复杂度是O(三角形数量乘以覆盖像素数)不需要全局排序每个像素操作都是局部比较。这也是它能在图形硬件上被高效并行化的原因。6.3 深度值的计算与插值光栅化时还需要知道每个像素的深度值。对三角形三个顶点的深度做重心插值就能得到像素深度。如果使用透视投影深度值在NDC空间中并不是线性的直接对屏幕空间做线性插值会有问题需要做透视矫正插值。一个容易忽略的细节是深度缓冲初始值。如果用透视投影的NDC z值范围通常在[-1, 1]或[0, 1]之间近处值更靠近1还是0取决于API约定。但在做课程作业时很多人会用屏幕空间z或者视空间z只要保证“越近深度越小”的一致约定算法本身不会出错。关键是在整个管线里统一深度计算方式不要一会儿用视空间z一会儿用NDC z。6.4 Z-fighting问题Z-buffer算法虽然好但深度精度是有限的。当两个表面非常接近比如重叠的平面或者离得很远的深景中相互靠近的物体深度值的浮点精度可能无法区分谁更近导致像素随机闪烁这就是Z-fighting。缓解办法包括增加深度缓冲位宽、设置深度偏移、调整近远裁剪平面的距离。尤其不要把近裁剪面设置得过近那幺小的near会压缩远处的深度精度容易引发深景处的闪烁。我在自己的渲染器里加过-0.01的深度偏移来处理地面贴花但偏移值必须根据场景量级调整太大又会出现新表面浮在旧表面之上的问题。7. 实现光栅化时容易踩的坑7.1 浮点精度与边界判定三角形共享边在光栅化中是一个经典问题。两个相邻三角形共享同一条边因为浮点计算误差可能这条边被光栅化两次或者一次都没有导致画面出现裂缝或重叠。课程中常提到“保证不漏一帧”的原则是尽量把边上的采样点归属到其中一个三角形以严格大于或严格小于的规则来划分。我的实现习惯是定义一条边只有在所有三个边界函数都严格大于0时才被记录等于0的像素归属到邻接三角形的逻辑要单独设计。对于显示效果还要把包围盒边缘也做细致处理避免顶点恰好落在整数坐标上时产生半像素偏移。这类问题在写作业时未必严重但在做大型渲染器时就很重要了。7.2 正面与背面三角形剔除光栅化之前最好先做背面剔除。如果三角形顶点按逆时针排列那么面向摄像机的三角形和背向摄像机的三角形在屏幕空间对应的边界函数符号相反。通过检查投影后三角形的旋转方向可以直接丢弃背面三角形省掉大量光栅化开销。在课程作业里通常物体模型是闭合表面背面三角形确实不需要渲染但你需要谨慎处理透明物体。透明物体往往需要从后往前渲染不能简单剔除背面需要另走一套半透明流程。我自己写渲染器时默认的CPU端管线是先剔除背面再光栅化这样能减少30%左右的无效覆盖测试。7.3 深度缓冲写入顺序Z-buffer算法对同一像素的写入要保证原子性。在软件渲染器单线程实现中这个没问题但一旦你开始用OpenGL、Vulkan或者并行CPU版本多个三角形同时写入同一像素就会出现竞争。硬件做法是在光栅化阶段就按像素调度每个像素只让最前面的三角形通过深度测试写操作可以视为原子。如果你在软件渲染器里做多线程并行建议把屏幕分成多个tile每个tile内部按三角形提交顺序串行处理或者用双缓冲深度的方式来避免读写冲突。这一步很容易踩坑花的时间比光栅化本身还多所以规划线程模型时就要想清楚。7.4 调试小技巧用颜色码覆盖情况排查光栅化问题的时候我习惯先把每个三角形的覆盖像素用随机颜色填充不开光照、不用深度缓冲只看到一堆色块是否完整覆盖屏幕。如果发现某些三角形没出现就检查顶点顺序和包围盒计算如果出现贯穿穿插就检查深度测试逻辑。这个调试步骤看似原始但效率特别高。另一个有用的小技巧是打印每个像素的包围盒边界坐标和计数器对比光栅化前后像素命中数量。例如一个直角三角形面积为2500像素理论覆盖像素数可能略大于或小于面积值通过这个差值能快速判断采样点偏移是正是负。这些排查手段帮我节省了大量的Debug时间。8. 结合实践再看课程内容8.1 做一次小型软件渲染器学完Lec 05~06后最好的验证方式是做一个只依赖CPU的光栅化渲染器。加载一个简单几何模型实现顶点变换、背面剔除、三角形覆盖、z-buffer、颜色插值最后输出PPM图片。整个过程能非常直观地展示“三角形到像素”的每一环。我当年做的第一个版本只有一千行左右但渲染一个立方体仍然花了很长时间优化。最初我直接扫描整个屏幕每个像素测三个边界函数后来加上包围盒、增量法和背面剔除速度提升了不止一个量级。现在回看这些优化在GPU硬件里早就做得更彻底但软件实现的经历让我对渲染管线的印象扎实得多。8.2 和后续课程如何衔接光栅化只是渲染管线的第一步。紧接着的纹理映射、着色、阴影、光照都会依赖光栅化阶段生成的覆盖信息、重心坐标和深度缓冲。你会在Lec 07~08里看到重心坐标被反复用来插值纹理坐标和法线在阴影作业里看到深度缓冲被复用为阴影图。所以Lec 05~06的内容越扎实后面就越省力。我自己跟课的经验是不要急着跳到下一节。先把软渲染器的基础版本调好多试几个不同形状的三角形把MSAA的覆盖率也实现一遍再继续往后的学习。磨刀不误砍柴工这句话在图形学自学路上真的适用。8.3 后续扩展方向在一个基础光栅化器上后续可以加许多有趣的方向实现平直反走样的更优覆盖策略、支持三角形层面的深度偏移、加入纹理和透视矫正插值、实现多线程tile渲染、再到接入图形API的MSAA。每一个方向都能把课堂知识推向实战。我现在的建议是学完这两讲后先做一个“能跑出三角形、能开启深度测试、能手动调节采样数量”的迷你渲染器作为后面所有实验的稳定地基。后续每学一个新知识点就往上叠加一个功能整个学习路径会非常平滑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑