6.3 光栅化与片元处理:深度、混合与 G-buffer


6.3 光栅化与片元处理:深度、混合与 G-buffer

本节摘要:光栅化器把裁剪后的三角形打碎成片元(候选像素),做透视校正插值;片元着色器给每个片元算颜色;深度测试裁决可见性,混合处理透明。本节讲透视校正插值为什么必须除以 w、early-z 如何抢在着色前淘汰片元、透明混合的顺序铁律,以及前向与延迟两条渲染路线的架构取舍——第 3、4、5 章的算法在这里全部硬件落地。

能力检验清单

阅读完本节,你应当能够:

  1. 演示屏幕空间线性插值在透视下的错误,写出透视校正公式
  2. 说明深度测试与片元着色器的执行顺序关系及 early-z 的条件
  3. 解释透明混合为什么必须从后往前、不透明为什么最好从前往后
  4. 描述 G-buffer 延迟渲染的流程与它对透明、带宽的天然短板

透视校正插值:一个反直觉的修正

3.1 节用重心坐标插值属性,但那是屏幕空间的线性插值——透视投影下它会出错。原因:投影把直线仍映射为直线,但把"均匀的深度间隔"映射成"不均匀的屏幕间隔",屏幕上等距的采样点对应的 3D 间隔并不等距。修正公式是先对属性除以 w 插值、再乘回插值后的 w:

import numpy as np def lerp(a, b, t): return a + (b-a)*t def naive_vs_correct(): # 三角形两端点:近处 z=1 颜色 0,远处 z=9 颜色 1 # 屏幕上取中点 t=0.5 naive = lerp(0, 1, 0.5) # 屏幕空间直接插:0.5 # 透视校正:先在 1/z 域插值 inv = lerp(1/1 * 0, 1/9 * 1, 0.5) # 属性/w 的插值 w = 1 / lerp(1/1, 1/9, 0.5) # 1/w 的插值后取倒 return naive, inv * w print(naive_vs_correct()) # 输出: (0.5, 0.3) 屏幕中点其实只对应属性 0.3——近处的"半程屏幕"没走完世界的半程

屏幕中点的真实属性是 0.3 而非 0.5:视觉中点偏向近端。硬件光栅化器对顶点属性一律自动做这个校正(纹理坐标尤其受益,否则贴图会在透视中扭曲),软件渲染器必须手写——这是"自己写软渲染器"练习里最有教学价值的一个坑。

深度测试的时机:early-z

标准流程里片元着色后才做深度测试,意味着被挡住的片元白白算了一趟光照。硬件的对策是 early-z:若片元着色器不修改深度(绝大多数情况),测试提前到着色之前,被挡的片元直接淘汰,省下整次着色。这解释了一个重要的绘制顺序习惯:

不透明物体:从近到远画 → 近处先占住深度缓冲,后面大片远处物在 early-z 就出局 半透明物体:从远到近画 → 混合的数学要求,顺序错了立刻浑浊

两种顺序截然相反,引擎的分批策略必须把不透明与透明分成两拨,中间关开深度写入。

混合:半透明的数学与铁律

透明靠混合(blending)实现:片元颜色按透明度 α 与目标已有的颜色加权:

结果 = α·源色 + (1-α)·目标色 前提:目标必须已经是"它身后的样子"——这就是必须从远到近画透明物体的原因

一个推论:透明物体之间"谁先谁后"错一格,画面就错一层。全屏粒子烟雾(大量重叠半透明)是性能与正确性的双重灾区,方案包括按深度粗排序、降低重叠数、或干脆用不透明的体积近似。另外注意 α 混合不满足结合律的交换对称,"加法混合"(发光粒子)无序也无妨,这是粒子特效偏爱 additive 的原因之一。

前向 vs 延迟:两种车间布局

每像素要应付几十盏灯怎么办?前向渲染对每个片元循环所有光源,大量光照计算浪费在被遮挡的片元上。延迟渲染换布局:第一遍只算几何(把法线、深度、材质存进多张全屏纹理,合称 G-buffer),第二遍对每个屏幕像素按 G-buffer 信息逐光源着色——着色只发生在最终可见的像素上,光源数与场景复杂度解耦。

# 延迟渲染第二遍的逐像素伪代码 for px in screen: n, z, albedo, rough = gbuffer_read(px) # 第一遍烤好的几何饼干 color = ambient for light in lights_affecting(px): # 用深度重建世界坐标 if not in_shadow(px, light): # 5.4 的阴影图查询 color += shade(n, light, albedo) # 5.1 的光照公式 write(px, color)

代价与限制同样明显:G-buffer 要三到五张全屏纹理,带宽吃紧(2.1 节算过账);透明物体写不进 G-buffer(一个像素只能存一份几何),半透明仍要走前向;MSAA 与延迟的兼容也要额外补丁。移动端因此常走"前向+ 分块光源剔除"的折中路线(TBDR),把光源按屏幕分块列表,着色时只查本块——思想与延迟同源,布局更省。

案例:一帧半透明烟雾的排序账

背景:战斗场景中一大片半透明烟雾粒子(约两千个互相重叠的 quad),美术反馈烟雾"脏得像淤泥"。操作:先确认混合状态是普通 α 混合而非加法;检查提交顺序——发现粒子按生成顺序而非深度排序提交,重叠层次大量错序;改为按相机距离从远到近排序后重提交。结果:烟雾层次分明,脏感消失。解读:α 混合不满足交换律(本节铁律),错序等于把"后面的先画",中间色整体偏离。变式:两千粒子逐帧排序有成本,工程折中是按粒子簇排序(一簇共享一个深度)或对发光烟雾改加法混合(无序也安全),前者保真、后者省心,按画面需求选。这个案例也顺带解释了为什么引擎把不透明与透明分成两个队列:排序要求相反,队列策略必然分家。

particles = [(3.2, "smoke"), (9.8, "smoke"), (5.1, "smoke"), (7.4, "smoke")] ordered = sorted(particles, key=lambda p: -p[0]) # 远的先画 print([p[0] for p in ordered]) # 输出: [9.8, 7.4, 5.1, 3.2] 从远到近,混合顺序正确

本节要点回顾

  • 透视校正插值:屏幕中点不等于世界半程,先除 w 再插再乘回,硬件自动做
  • early-z 白送性能:不透明从近到远,让遮挡淘汰发生在着色前
  • 透明从远到近:混合的数学铁律,加法混合是例外
  • 延迟渲染解耦光源:G-buffer 存几何、逐像素着色,透明与带宽是代价
  • 分块剔除是移动端答案:思想同源、布局更省

着色完成的像素还差最后一道工序——6.4 节的后处理:整帧出厂前的精修车间。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U