一帧画面是怎么来的

UNIT 06渲染约 24 分钟 · 核心线

游戏里的画面是怎么「作弊」算出来的?

红魔 v3 的机甲,盔甲上的反光一块黑一块白,边上还发虚。你看完问:「那些反光,你是用光栅还是光追的?会不会光追更好?还是说这是材质贴图导致的bug」C20。这一课把前半句答清楚:光栅化是怎么画画面的,它为什么快,它的反射为什么会怪。

06 · 1每个像素追一条光,够不够快?

屏幕是一格一格的像素(第 1 课)。要知道某一格该涂什么颜色,最直接的办法是:从相机出发,穿过这一格射一条线,看它先碰到谁。场景是三角形拼的(第 3 课),「先碰到谁」就得一个三角形一个三角形地试。算一下账:

1920 × 1080 ≈ 207 万个像素
广州片里的沙面 506 万个三角面 C01
207 万 × 506 万 ≈ 10 万亿次「这条线碰没碰到这个三角形」,一帧

游戏要每秒 60 帧,一帧只有 16.7 毫秒。挨个试当然笨:真正的光线追踪器会先把三角形按位置分组装箱,一条线先问「碰没碰到这一整箱」,没碰到,箱里的三角形一个都不用试。可哪怕这样,每一条线还是要在整个场景里「找」一次。游戏几十年来走的是另一条路:不找。

06 · 2反过来:把三角形投到屏幕上

不从像素出发去找三角形,而是从三角形出发去找像素。三角形的三个角各落在屏幕哪儿,第 2 课的透视已经给了答案:屏幕上的横坐标 ≈ 焦距 × 横向偏移 ÷ 深度。三个角投上去,把它们围住的格子涂上颜色,这个三角形就画完了。

506 万个三角面 × 3 个角 ≈ 1500 万次投影,再把 207 万个像素各涂几遍

每个三角形只碰它自己盖住的格子,谁也不用在场景里找谁。这就叫光栅化(rasterization):把形状变成一格一格的像素。显卡的硬件就是为它造的。

下面是同一个场景从正上方看的剖面:相机在下,浅蓝的扇形是它拍得到的范围,「画面」只有一排 14 个像素。先看「每个像素追一条光」画出的这一排,再切到光栅化。

实验台 A-1一排像素是怎么来的
来自材料
光栅化、深度缓冲与子样本的做法(LearnOpenGL、Scratchapixel);半透明先画不透明、再从远到近混合(LearnOpenGL「Blending」),半透明通常不写深度(Unity 手册 ZWrite)。
本实验的设定
三维场景在球心高度横切一刀,画面缩成一排 14 格;两种画法用同一组形状:球是 16 条线段围成的(二维里的「三角形拼的」);这里球先画成灰色,只看每格看到的是谁。
它能证明
两种画法在「相机直接看到谁」上结果一样:浏览器把两种算法各算一遍,逐格比较(node 探针里 6 个方块位置 × 1/4/16 个子样本,18 组全部相同)。
它不能证明
两种画法的速度:三个形状看不出一千五百万和十万亿的差别。

切到光栅化,这一排和追光画出来的一模一样。可光栅化是一个形状一个形状画的:墙和球在屏幕上有几格叠在一起,同一格被涂了两次。

显卡先画了近处的金属球,再画远处的白墙。屏幕上两者叠在一起的那几格,最后是什么?

  1. 白墙:后画的盖住先画的
  2. 金属球:近的挡住远的,和先画后画无关
  3. 两种颜色混在一起

是金属球。显卡除了一张画面,还有一张同样大小的表,每一格记着「目前涂在这里的东西离相机多远」。新来一个形状要涂这一格,先比一比:更近才涂上去、改掉记下的距离;更远就不涂。这张表叫深度缓冲(depth buffer),这一比叫深度测试。

所以先画谁都一样。实验台 A-1 的 02 已经打开:关掉深度测试、顺序换成「最后画墙」,第 10 到 12 格的球就被墙盖掉了——那正是「后画的盖住先画的」。猜「混在一起」的,想的是半透明的东西;那种要单独处理,这里的球和墙都不透明。

早年的办法是按远近排好队、从远往近画(画家算法);两个东西互相穿插时远近说不清,它就错。深度缓冲逐格比,不怕穿插。光栅化画完一帧,手里就有两样东西:一张画面和一张深度缓冲。游戏后面的「作弊」都在这两张表上做文章。

深度测试只比远近,不知道「透明」。一块半透明的玻璃要是也把自己的距离写进深度缓冲,它身后后画的东西就一律被挡掉,玻璃后面空一块。所以游戏先画完所有不透明的东西,再把半透明的按从远到近一层层叠上去:每一层和已经画好的颜色按透明度混合(blending),而且只比深度、不写深度。「先画谁都一样」只对不透明的东西成立。

06 · 3斜边上的台阶

光栅化判断一个形状盖没盖住某一格,看的是这一格正中间那一点:盖住了整格涂满,没盖住一点不涂。斜边穿过的格子只能全有或全无,边上就是一级一级的台阶,叫锯齿(aliasing)。办法是一格里多看几个点(子样本),按盖住的比例取平均,叫抗锯齿(anti-aliasing)。光栅化是多判断几个点盖没盖住,追光是一格多射几条线,结果一样。在 A-1 的 03 里拨到 4 条,看第 6 格:4 条里 3 条碰到球,这一格就是四分之三的球色。

每一格都多算几遍太贵,所以有省法:多重采样(MSAA)每格多记几个子样本「盖没盖住」,颜色却只算一次,所以只磨平几何的边;时域抗锯齿(TAA)不在一帧里多看几个点,而是每一帧把看的那个点在格子里挪一点点,再把前几帧的结果借过来平均,等于把子样本摊到了时间上(「时域」)。UE 5 默认的时域超分(TSR)也是这一类,它还先用低一些的分辨率算、再放大补回细节(「超分」)。

投篮课的铁丝网是一张带透明网眼的贴图,它的透明靠多重采样的子样本来「漏」出网眼。成片按 2 倍像素密度渲(每个像素拆成 2×2 个更细的像素来算,画面更细),那种设置下三维画面没有开多重采样。渲出来的铁丝网会怎样?

  1. 更清楚:像素更密了
  2. 网眼没了,变成一堵实心的灰墙
  3. 和平时一样

变成了一堵实心灰墙,第一次全片渲完才在联系表里发现 C43。贴图里每个像素除了颜色,还存一个不透明度(alpha,网眼处接近 0)。那种透明做法叫 alpha to coverage:用「一格里几个子样本算盖住」来表示不透明度。没有多重采样,一格只有一个样本,就只剩「全盖住」。修法是 2 倍像素密度时改用 alphaTest:不透明度低于 0.4 的地方(网眼)直接不画。猜「更清楚」的,把像素密度和子样本当成了一回事:像素多了,每个像素里的子样本却没了。

06 · 4镜子里映的是什么?

球是镜面金属,它上面某一格的颜色要看从这一格反射出去的线碰到谁。光栅化回答的只是「相机看到谁」;为反射线在整个场景里再找一遍,又回到了第一节的账。

游戏的办法是:画面已经画好了,就在画面里找。从球上这一格出发,沿反射方向一小步一小步走,每一步投回屏幕,查那一格的深度缓冲:线钻到了那一格记下的表面后面,就算碰到了,直接拿那一格的颜色。这叫屏幕空间反射(screen-space reflection,SSR)。

金属球左边、画面外 6 厘米,放着一块红方块。用屏幕空间反射画这个球,球上会映出红方块吗?

  1. 会,镜面什么都照得到
  2. 不会
  3. 会,只是暗一点、糊一点

不会。反射线还没碰到红方块,就先走出了画面;画面外没有深度缓冲可查,什么也找不到。找不到时,游戏拿一张事先拍好的环境图凑数(反射探针),那是只拍了不动的东西的,里面没有这块会动的红方块。

下面上一行是屏幕空间反射算的,下一行是在整个场景里找(路径追踪,下一课)算的。点金属球上的格子,看反射线在哪儿断掉。

实验台 A-2反射去哪找
来自材料
屏幕空间反射的做法与盲区:Blender 手册 EEVEE 的「光线追踪」一节(EEVEE 把在画面里一步步找也叫追踪;出了画面退回探针,画面外的东西反射不到)。
本实验的设定
同一个剖面、单层深度缓冲;每步 8 厘米,钻到表面后面 0.6 米以内算碰到;探针只含白墙和天空。
它能证明
只在画面里找,画面外的东西就找不到,和步子多大、画面多细无关。
它不能证明
真实游戏的步长、厚度和探针怎么调。

一分半的导览:屏幕空间反射为什么找不到画面外的东西,方块刚进画面时倒影为什么缺一块、带齿。它会带着下面的三维实验台走一遍,你随时可以接手。

换到三维:左边是光栅化加屏幕空间反射,右边是下一课的路径追踪,当作标准答案。分割线可以拖。红方块默认在画面左边缘外 6 厘米。

实验台 A-3一束光的旅程
同一个场景左右分屏:中间一只镜面金属球,右半边球上映出一小块红色,左半边没有。
来自材料
左边按游戏引擎的套路做:一块矩形的面光源(像柔光箱);影子用阴影贴图(先从灯的位置拍一张深度图,画面上的点比灯「看到」的更远,就在影子里);屏幕空间反射,出画面退回只拍不动物体的反射探针。右边是路径追踪。
本实验的设定
几何全是简单形状,金属是理想镜面;屏幕空间反射每 1.5 个像素比一次深度,钻到表面后面约 1.5 米以内算碰到。
它能证明
红方块一离开画面,左边球上的红像素掉到 0,右边一直在(页面实测:画面外 6 厘米时左 0、右 19209 个;进画面 3 厘米时左 5297、右 20369 个)。
它不能证明
EEVEE、UE 的具体效果:它们的步长、厚度、探针更讲究,但画面外那条线谁也越不过去。

把红方块往画面里拖。刚进来两三厘米,左边的倒影冒出来了,却只有一截,上沿还带着一排齿。用刚才的模型想一想为什么。

再往里拖,倒影全了,还比右边胖一圈:深度缓冲里只有方块朝着相机的那两个面,背后有多厚它不知道,只好假定每个表面后面都有一段厚度。反射线从方块背后穿过去,也被当成碰到了,倒影就胖了。画面外的、被挡住的,对它来说是同一件事:这张画面里没有。

红魔机甲同一帧的左右对照:左边整片发灰发粉,盔甲上的反光是一块块发白的色块;右边背景是黑的,金属上只有细细的红色灯带和蓝色光环的倒影。
红魔机甲,同一帧的两种渲染。左:EEVEE(光栅化,反射是屏幕空间的);右:Cycles(路径追踪)。2026-09-28 的测试图,第 890 帧。两边的灯光、雾和辉光算法也不同,发灰不能全算到反射头上 C20。

这张图里的毛病出在哪一环?先自己诊断一下,再往下看。

回到你那句问话。红魔 v3 的机甲用 EEVEE,渲染脚本里反射设成屏幕空间(ray_tracing_method = 'SCREEN',EEVEE 把「在画面里一步步找」也叫追踪,但它追的是画面,不是场景),出画面的部分退回一张低清的光照探针(Blender 对反射探针的叫法),边上发虚就是这么来的;盔甲又是近乎镜面的铬,暗场里只映得到几条灯带,于是一块黑一块白。三件事叠在一起,就是那些碎块。v4 把机甲换到 Cycles、铬改成缎面金属、加了大面柔光反射卡,怪反光没了 C20。不是贴图的 bug。

同样的「作弊」不止反射:墙角缝隙里暗下去的那一层(环境光遮蔽,AO),游戏常见的做法也是在深度缓冲里看周围,叫屏幕空间环境光遮蔽(SSAO)。它们都快,也都只知道画面里有的东西。

带走一个问题

游戏里有一段平静的水面,映着对岸的一排楼。镜头慢慢往下压,楼一点点移出画面上边缘。如果水面用的是屏幕空间反射,倒影会怎么变?要让倒影一直在,你会让 AI 改哪一环,要付出什么?

不看上面,写下这一课撞上的几堵墙,再展开对照
  1. 每个像素追一条光、挨个试三角形 → 一帧十万亿次;反过来把三角形投到屏幕上填格子:光栅化。
  2. 投上去的形状叠在一起,谁在前?按先画后画 → 错;深度缓冲逐格只留最近的。
  3. 一格只看正中间一点 → 锯齿;一格多射几条光取平均;靠子样本的透明做法,没了子样本就失效。
  4. 镜子里映什么?只在画好的画面里找(屏幕空间反射)→ 画面外的、被挡住的都找不到;刚进画面的只找到一截。