深度测试

多个三角形覆盖同一个像素时,需要确定最靠近相机的表面。画家算法 Painter’s Algorithm 先按深度从远到近排序,再依次覆盖颜色,排序成本为 O(nlogn)O(n\log n),而且循环遮挡的三角形无法得到一致的全局顺序。

深度缓冲 Z-buffer 为每个样本保存当前最近深度。这里比较的是经过投影和规范深度映射后的样本深度 dd,并非相机空间中带符号的 zz。第三篇采用近裁剪面 zndc=1z_{\mathrm{ndc}}=1、远裁剪面 zndc=1z_{\mathrm{ndc}}=-1 的推导;为了使用数值越小越近的约定,可再映射

d=1zndc2.d = \frac{1-z_{\mathrm{ndc}}}{2}.

此时近处 d=0d=0、远处 d=1d=1。缓冲区初始化为无穷大;每产生一个覆盖该样本的片元,就执行

d<dbuffer(x,y){dbuffer(x,y)d,C(x,y)Cfragment.d<d_{\mathrm{buffer}}(x,y) \quad\Longrightarrow\quad \begin{cases} d_{\mathrm{buffer}}(x,y)\leftarrow d,\\ C(x,y)\leftarrow C_{\mathrm{fragment}}. \end{cases}

颜色缓冲与 Z-buffer 分别保存表面颜色和最近深度

它的正确性来自一个简单不变量:处理完任意数量片元后,

dbuffer=min{dii 已处理且覆盖该样本}.d_{\mathrm{buffer}} = \min \left\{ d_i\mid i\text{ 已处理且覆盖该样本} \right\}.

初始集合为空,对应 ++\infty。加入新片元时,若它更远,不变量不变;若它更近,用新值替换后仍得到集合最小值。归纳到所有片元,留下的就是最近表面,因此结果与三角形提交次序无关。

算法成本与实际产生的覆盖片元数成正比:

O(icoverage(Ti)).O\left( \sum_i\operatorname{coverage}(T_i) \right).

当平均覆盖范围有限时,它常近似随图元数线性增长,也适合按像素并行。Z-buffer 只解决相机可见性;一个点是否被光源照到,还需要阴影算法。

已经推导出透视深度含有 1/z1/z,远处分到的深度值更密集。有限位数或浮点量化会让两个近乎共面的表面落到同一深度区间,形成深度冲突 z-fighting。反向深度 Reversed-Z 会改变远近数值和比较方向,但维护“当前最近值”的不变量没有改变。

深度测试使用哪种大小关系,取决于投影后的深度约定;不能直接拿相机空间的负 zz 与 Z-buffer 比较。

局部着色

局部着色 Local Shading 计算表面某点沿观察方向离开的光。简单局部模型使用:

  • 单位表面法线 n\mathbf n
  • 从着色点指向光源的单位方向 l\mathbf l
  • 从着色点指向相机的单位方向 v\mathbf v
  • 光源强度 II 与距离 rr
  • 表面的颜色、光泽等材质参数。

从位置相减得到光线或视线方向后,进入点积前必须归一化;法线 Normal 是描述表面朝向的方向约束,不是一个可直接代入的 position。法线插值、变换或贴图解码后也要重新归一化,否则点积会同时受到向量长度影响。

局部模型只处理当前点和直接光源,不追踪光在其他表面上的多次反射。它可以产生明暗和高光,却不会自动产生遮挡阴影与间接光照。

漫反射

理想漫反射 Lambertian Reflection 向各方向均匀散射,因此结果与观察方向无关。课程的 Blinn–Phong 经验着色式常把材质比例、单位换算和 1/π1/\pi 一并吸收到系数 kdk_d 中:

Ld=kdIr2max(0,nl).L_d=k_d\frac{I}{r^2}\max(0,\mathbf n\cdot\mathbf l).

ρ\rho 明确表示物理反照率、II 表示点光源的辐射强度,则 I/r2I/r^2 是到达表面的入射照度尺度。能量归一化的 Lambertian BRDF 为 fr=ρ/πf_r=\rho/\pi,相应出射辐亮度贡献应写成

Ld=ρπIr2max(0,nl).L_d = \frac{\rho}{\pi} \frac{I}{r^2} \max(0,\mathbf n\cdot\mathbf l).

因此不能在遗漏 1/π1/\pi 的同时把经验系数 kdk_d 直接解释为反照率。余弦项来自投影面积。面积为 dA\mathrm dA 的斜面面对光线时,有效截面只有

dA=dAcosθ=dA(nl).\mathrm dA_\perp = \mathrm dA\cos\theta = \mathrm dA(\mathbf n\cdot\mathbf l).

点光源向固定立体角发出的能量落在半径为 rr 的球面上,球面面积随 r2r^2 增长,所以单位面积能量按 1/r21/r^2 衰减。kdk_d 是漫反射系数或表面颜色。夹角超过 9090^\circ 时,光从表面背面到达,余弦项必须截断为零;方向光通常不随场景中的位置改变。

镜面反射

镜面高光出现在观察方向接近理想反射方向的位置。Blinn–Phong 反射模型 Blinn–Phong Reflection Model 使用半程向量 Half Vector

h=v+lv+l\mathbf h=\frac{\mathbf v+\mathbf l}{\lVert\mathbf v+\mathbf l\rVert}

v+l=0\mathbf v+\mathbf l=\mathbf0 时,半程向量没有定义;这对应光照方向与观察方向完全相反,镜面项应跳过或置零,不能继续做归一化。

比较法线与光线、视线的中间方向:

Ls=ksIr2max(0,nh)p.L_s=k_s\frac{I}{r^2}\max(0,\mathbf n\cdot\mathbf h)^p.

理想镜面反射时,法线恰好平分光照方向 l\mathbf l 与观察方向 v\mathbf v,两个单位方向的归一化和正是角平分线,所以 nh\mathbf n\cdot\mathbf h 能衡量观察方向离理想高光还有多远。pp 越大,反射瓣越窄,高光越集中;ksk_s 控制高光颜色和强度。这里描述的是经验模型,并不是后续基于微表面的物理 BRDF。

环境光项 La=kaIaL_a=k_aI_a 用一个常量近似所有未计算的间接光。最终模型为

L=La+Ld+Ls.L=L_a+L_d+L_s.

Blinn-Phong 模型由环境光、漫反射和镜面项组成

环境光能避免无直接光区域完全变黑,但没有几何依据,也不能表达真实的颜色互染和遮挡关系。

Blinn–Phong 是经验性的局部光照模型;BRDF Bidirectional Reflectance Distribution Function 描述给定入射和出射方向下的反射分布,BSDF Bidirectional Scattering Distribution Function 还把透射、折射等散射纳入同一描述,后续可以用它们讨论比经验项更一般的材质响应。

着色频率

三角网格可以在不同频率上执行着色:

  • Flat shading:每个三角形使用一个面法线和一个颜色,棱角清晰。
  • Gouraud shading:在顶点计算颜色,再把颜色插值到三角形内部;小而尖锐的高光可能被顶点采样漏掉。
  • Phong shading:插值顶点法线,在每个像素重新计算着色,平滑效果和高光质量更好。

Phong shading 指法线插值策略,Phong 或 Blinn-Phong reflectance model 指反射公式,两者含义不同。

Flat、Gouraud 与 Phong 分别在面、顶点和像素级计算着色,平滑度与高光范围随之变化

顶点法线可以由相邻面法线加权平均得到。若三角形 ii 的法线为 ni\mathbf n_i、面积或夹角权重为 wiw_i,则

nv=iwiniiwini.\mathbf n_v= \frac{\sum_iw_i\mathbf n_i} {\left\lVert\sum_iw_i\mathbf n_i\right\rVert}.

三角形内部插值得到的法线一般不再是单位向量,送入点积前必须重新归一化。硬边两侧不应共享同一顶点法线,否则本应尖锐的棱边会被错误地抹平。

图形管线

图形管线 Graphics Pipeline 把实时光栅化拆成一条流水线:

  1. 应用阶段准备场景、相机、材质和绘制命令。
  2. 顶点处理执行模型、视图与投影变换,并输出顶点属性。
  3. 图元装配把顶点组成三角形,随后进行裁剪等处理。
  4. 光栅化计算覆盖样本,并对顶点属性做插值,产生片元。
  5. 片元处理执行着色和纹理查询。
  6. 帧缓冲阶段完成深度、模板、混合等测试,再写入图像。

实时光栅化管线中的主要阶段

GPU 以大量并行处理单元执行顶点和片元程序,同时保留光栅化、纹理过滤等固定功能硬件。顶点着色器描述单个顶点如何变换,片元着色器描述单个候选像素如何计算颜色;同一程序会并行应用于大批数据。

重心坐标

重心坐标 Barycentric Coordinates 用三个顶点的仿射组合表示三角形平面上的点:

P=αA+βB+γC,α+β+γ=1.P=\alpha A+\beta B+\gamma C, \qquad \alpha+\beta+\gamma=1.

令有向三角形面积为

[ABC]=12((BA)×(CA))z.[ABC] = \frac12 \big((B-A)\times(C-A)\big)_z.

由于面积对顶点位置是线性的,

α=[PBC][ABC],β=[PCA][ABC],γ=[PAB][ABC].\alpha=\frac{[PBC]}{[ABC]}, \qquad \beta=\frac{[PCA]}{[ABC]}, \qquad \gamma=\frac{[PAB]}{[ABC]}.

三个子三角形的有向面积之和等于原三角形面积,所以权重和为 1。点位于内部时,三个子三角形与原三角形朝向一致,因而 α,β,γ0\alpha,\beta,\gamma\ge0;越过某条边后,对应权重会变成负数。

若变换是仿射的,顶点颜色、法线、纹理坐标等属性可按相同权重插值:

V(P)=αVA+βVB+γVC.V(P) = \alpha V_A+\beta V_B+\gamma V_C.

透视投影 Perspective Projection 含有齐次除法,因此不保持普通仿射权重。设三角形上一个点在投影前的重心权重为 λA,λB,λC\lambda_A,\lambda_B,\lambda_C,顶点裁剪坐标的第四分量为 wA,wB,wCw_A,w_B,w_C。MVP 变换在齐次坐标中仍是线性的:

P~=λAP~A+λBP~B+λCP~C.\widetilde P = \lambda_A\widetilde P_A +\lambda_B\widetilde P_B +\lambda_C\widetilde P_C.

PA,PB,PCP_A,P_B,P_C 表示除法后的屏幕点。因为 P~i=wiPi\widetilde P_i=w_iP_i,再做齐次除法可得

Pscreen=λAwAPA+λBwBPB+λCwCPCλAwA+λBwB+λCwC.P_{\mathrm{screen}} = \frac{ \lambda_Aw_AP_A +\lambda_Bw_BP_B +\lambda_Cw_CP_C }{ \lambda_Aw_A +\lambda_Bw_B +\lambda_Cw_C }.

屏幕空间计算出的重心坐标记为 α,β,γ\alpha,\beta,\gamma。与上式逐项比较:

α=λAwAλAwA+λBwB+λCwC,\alpha = \frac{\lambda_Aw_A} {\lambda_Aw_A+\lambda_Bw_B+\lambda_Cw_C},

其余两项同理。反解得到

λi=αi/wiα/wA+β/wB+γ/wC.\lambda_i = \frac{\alpha_i/w_i} {\alpha/w_A+\beta/w_B+\gamma/w_C}.

真正应当插值的是投影前的权重,所以

V(P)=αVA/wA+βVB/wB+γVC/wCα/wA+β/wB+γ/wC.V(P)= \frac{ \alpha V_A/w_A +\beta V_B/w_B +\gamma V_C/w_C }{ \alpha/w_A +\beta/w_B +\gamma/w_C }.

这就是 透视正确插值 Perspective-Correct Interpolation。硬件实现通常分别线性插值 Vi/wiV_i/w_i1/wi1/w_i,再用前者除以后者。

一个一维例子能看出差异。设屏幕线段中点的权重为 α=β=0.5\alpha=\beta=0.5,两端 wA=1,wB=4w_A=1,w_B=4,则投影前的权重为

λA=0.5/10.5/1+0.5/4=0.8,λB=0.2.\lambda_A = \frac{0.5/1}{0.5/1+0.5/4} =0.8, \qquad \lambda_B=0.2.

若属性从 VA=0V_A=0 变到 VB=1V_B=1,中点正确值是 0.20.2,并非屏幕线性插值得到的 0.50.5。远端在透视后被压缩,屏幕距离已经不能直接代表原三角形上的距离。

屏幕重心坐标适合判断覆盖;属性插值还必须除以齐次分量 ww,否则纹理会随透视发生“游动”和扭曲。

纹理映射

纹理映射 Texture Mapping 把二维参数域中的数据附着到三维表面。网格顶点保存纹理坐标 (u,v)(u,v),光栅化器对其做透视正确插值,片元着色器再查询纹理。纹理既可以保存颜色,也可以保存法线、高度、粗糙度、遮挡或任意供着色器读取的数据。

三种空间必须区分:顶点位置位于模型或世界空间,最终片元位于屏幕空间,纹理样本位于纹理空间。UV 展开决定三维表面如何切开并铺到二维图像上;接缝、拉伸和重复铺贴都由这层映射控制。

纹理放大

当一个纹素覆盖多个屏幕像素时,需要在离散纹素之间重建连续值。最近邻只取距离查询点最近的纹素,边缘会呈块状。双线性插值 Bilinear Interpolation 读取相邻四个纹素:

纹素通常以中心而非边界作为采样位置;对宽高为 Wt,HtW_t,H_t 的纹理,索引 (i,j)(i,j) 的归一化中心可写成

(i+0.5Wt,j+0.5Ht).\left(\frac{i+0.5}{W_t},\frac{j+0.5}{H_t}\right).

UV 超出 [0,1][0,1] 时还要明确 address mode:clamp 把坐标截到边界,wrap/repeat 按周期重复,mirror 则交替翻转。采样中心和越界规则不一致,会造成半纹素偏移、接缝或边缘颜色异常。

u0=lerp(s,u00,u10),u1=lerp(s,u01,u11),u_0=\operatorname{lerp}(s,u_{00},u_{10}), \qquad u_1=\operatorname{lerp}(s,u_{01},u_{11}),

f(u,v)=lerp(t,u0,u1),lerp(x,a,b)=a+x(ba).f(u,v)=\operatorname{lerp}(t,u_0,u_1), \qquad \operatorname{lerp}(x,a,b)=a+x(b-a).

双三次插值使用更大邻域,过渡更平滑,代价也更高。插值解决的是重建方式,不会凭空恢复原纹理中不存在的细节。

Nearest 产生块状边缘,Bilinear 平滑四邻域,Bicubic 使用更大邻域重建细节

纹理缩小

当一个屏幕像素覆盖很多纹素时,点查询会漏掉大部分纹理信息,产生摩尔纹和闪烁。正确问题从“这个点是什么颜色”变为“这个像素在纹理空间覆盖的区域平均是什么”。屏幕上不同位置的像素 footprint 还会因透视而具有不同大小与形状。

Mipmap 预先把纹理逐层缩小一半。层级 DD 的宽高约为原纹理的 2D2^{-D},所有层总存储量为原图的

1+14+116+=43,1+\frac14+\frac1{16}+\cdots=\frac43,

额外开销是原图的三分之一。层级选择要估计一个屏幕像素在纹理空间中的覆盖范围 Footprint。

在当前片元附近,UV 映射可以用一阶 Taylor 展开近似。它的 Jacobian 为

J=(uxuyvxvy).J= \begin{pmatrix} \frac{\partial u}{\partial x} & \frac{\partial u}{\partial y} \\ \frac{\partial v}{\partial x} & \frac{\partial v}{\partial y} \end{pmatrix}.

屏幕沿 xxyy 移动一个像素,乘以 JJ 后得到纹理空间中的两条边。若原纹理宽高为 Wt,HtW_t,H_t,把归一化 UV 转成纹素坐标后,两条边的长度为

ρx=(Wtux)2+(Htvx)2,\rho_x = \sqrt{ \left(W_t\frac{\partial u}{\partial x}\right)^2 + \left(H_t\frac{\partial v}{\partial x}\right)^2 },

ρy=(Wtuy)2+(Htvy)2.\rho_y = \sqrt{ \left(W_t\frac{\partial u}{\partial y}\right)^2 + \left(H_t\frac{\partial v}{\partial y}\right)^2 }.

L=max(ρx,ρy)L=\max(\rho_x,\rho_y)

作为方形 footprint 的近似尺度。当 L2DL\approx2^D 个原始纹素时,第 DD 层把这段范围缩成约一个纹素。取数值下限 ε>0\varepsilon>0 以避免退化 footprint 导致 log20\log_2 0,连续层级为

D=log2max(L,ε).D=\log_2\max(L,\varepsilon).

实际查询还要按可用层数截断:

Dquery=clamp(D,0,Dmax).D_{\mathrm{query}} = \operatorname{clamp}(D,0,D_{\max}).

D0D\le0 表示纹理正在放大,应使用最高分辨率层;DqueryD_{\mathrm{query}} 非整数时,在 Dquery\lfloor D_{\mathrm{query}}\rfloorDquery\lceil D_{\mathrm{query}}\rceil 两层分别做双线性查询,再沿层级方向线性插值。这一过程称为三线性过滤 Trilinear Filtering。

若直接在相邻层级之间取整,物体移动或视角变化时会出现明显的层级切换 level transition;三线性过滤按 DqueryD_{\mathrm{query}} 的小数部分混合两层,能把这种突变变成平滑过渡,但不会改变每一层近似方形 footprint 的限制。

Mipmap 的 level 0 是原始纹理,level 越大,分辨率越低;第 DD 层的一个纹素大致汇总原图 2D×2D2^D\times2^D 的区域。LL 的含义是“一个屏幕像素跨过多少原始纹素”:近处正视表面通常 LL 小,使用较低 level;远处或斜视表面通常 LL 大,使用较高 level。真正决定 level 的是 UV Jacobian,单看相机距离并不充分,纹理重复次数和表面朝向同样会改变 footprint。

Mipmap 以多级低分辨率纹理近似范围查询

Jacobian 实际把屏幕像素映射为平行四边形,局部更准确的形状常近似为椭圆。Mipmap 只用最长边构造方形范围,斜视表面的 footprint 很细长时会包含过多无关纹素,造成过度模糊。Anisotropic Filtering 沿 footprint 的长、短方向分解尺度,在长轴方向使用更多样本或不同层级,减少这种过度模糊;Ripmap 和 Summed-Area Table 适合轴对齐矩形,Elliptical Weighted Average 使用椭圆核处理更一般的 footprint。

各向异性过滤沿斜视纹理 footprint 的长短方向分解尺度,保留更清晰的远处细节

环境贴图

环境贴图 Environment Mapping 按方向存储远处环境光。球面参数化容易在极点产生强烈扭曲,立方体贴图 Cube Map 使用六张方形纹理:给定方向后,选择绝对值最大的坐标分量确定立方体面,再计算面内坐标。只要环境可视为无限远,查询只依赖方向而不依赖物体位置。

几何细节

Bump Mapping 用高度函数的局部导数扰动法线,却不改变轮廓和真实交点。设切线空间高度为 h(u,v)h(u,v),局部切平面沿 u,vu,v 的斜率为 hu,hvh_u,h_v。高度场上的两条切向量可写成

tu=(1,0,hu),tv=(0,1,hv).\mathbf t_u=(1,0,h_u), \qquad \mathbf t_v=(0,1,h_v).

二者叉积给出局部法线

n=normalize(hu,hv,1).\mathbf n'=\operatorname{normalize} \left(-\frac{\partial h}{\partial u}, -\frac{\partial h}{\partial v},1\right).

贴图只给出了切线空间 Tangent Space 中的法线,还要变换到着色使用的坐标系。设表面沿 UV 的单位切线为 T\mathbf T,副切线为 B\mathbf B,宏观法线为 N\mathbf N,则

MTBN=(TBN),M_{\mathrm{TBN}} = \begin{pmatrix} \mathbf T&\mathbf B&\mathbf N \end{pmatrix},

nshade=normalize(MTBNn).\mathbf n_{\mathrm{shade}} = \operatorname{normalize} \left( M_{\mathrm{TBN}}\mathbf n' \right).

T\mathbf T 应由表面位置对 UV 的变化求得,B\mathbf B 再与 T,N\mathbf T,\mathbf N 正交化;随意取一条垂直于 N\mathbf N 的方向虽然能构造坐标系,却可能让贴图方向在三角形之间跳变。

Normal Mapping 直接保存切线空间扰动法线。若 RGB 通道位于 [0,1][0,1],常先解码为

ntan=2c1,\mathbf n_{\mathrm{tan}} = 2\mathbf c-\mathbf1,

再乘 TBN。模型带非均匀缩放时,法线使用逆转置矩阵,切线按普通方向变换,之后重新正交化。

Displacement Mapping 则真实移动表面:

p=p+sh(u,v)N.\mathbf p' = \mathbf p+s\,h(u,v)\mathbf N.

它能改变轮廓、交点和阴影,但几何必须足够细密,或者在细分阶段动态增加顶点。

Bump/Normal Mapping 只改着色法线,轮廓仍是原几何;Displacement Mapping 真正改变表面位置。

程序化噪声可以生成木纹、岩石等实体纹理;环境光遮挡等预计算结果也能存入纹理;三维纹理把标量或颜色场存到体素网格中,可用于医学数据和体渲染。