Deep Read

各向异性

前置知识:反射定律、微表面 BRDF(NDF、粗糙度、半程向量),见 Physically Based RenderingPhysically Based RenderingPBR 的核心目标不是 100% 物理精确——那是离线渲染的事——而是在物理规律的约束下建立一套可预测的光照模型:美术用真实世界的材质属性(金属度、粗糙度)来创作,材质在任何光照环境下都能表现出一致的效果。整套 PBR 可以拆成三层来理解:先是对现实世界光与物质交互的三个物理观察,然后是把这些观察编码成数学的渲染方程与 BRDF,最后是为了实时跑起来所做的一系列工程近似(尤其是环境光照部分)。 三大物理原理 所有复杂的公式,都是在模拟下面三个现象。 微表面理论 (Microfacet Theory)   宏观上光滑的表面,在微观尺度下都是由无数朝向随机的微小镜面(Microfacets)组成的。粗糙度 (Roughness) 描述的就是这些微观平面朝向的混乱程度:Roughness ≈ 0 时微观平面朝向基本一致,整体像一面大镜子,高光集中而清晰;Roughness ≈ 1 时朝向混乱,光线被散射到四面八方,高光模糊且范围大。 数学上用法线分布函数 (Normal Distribution Function, NDF) 来描述这种混乱程度——它回答的问题是:有多大比例

各向异性的原理

保持灯光和相机不动,只旋转物体:

  • 普通金属不管转到哪个方向,高光形状都差不多,这是各向同性;
  • 拉丝金属转动后,高光会跟着拉丝方向变化,这是各向异性。

区别不在表面是否粗糙,而在微观结构有没有统一方向。

同一个表面有两个粗糙度

微表面理论中的粗糙度,描述的是微表面法线的分散程度。法线越集中,反射光越汇聚,高光越锐利;法线越发散,反射光越散开,高光越模糊。

随机凹凸表面的微表面法线在各个方位上均匀分布。物体绕法线旋转后,法线分布没有变化,所以只需要一个粗糙度描述——这就是各向同性。

拉丝金属、缎面和头发的微观结构带有方向。沿沟槽或纤维方向,表面变化较小;垂直该方向,表面起伏更明显。于是切平面中的两个方向具有不同的法线分布,也就需要两个粗糙度描述——这就是各向异性。

image.png

高光为什么垂直于沟槽拉长

把一条沟槽看成细圆柱。平行光照到圆柱后,每个命中点的反射方向由该点法线决定。

  • 沿圆柱轴线取一排点,法线方向相同,反射光也基本平行;
  • 绕圆柱截面取一圈点,法线沿半径向外发散,反射光被摊向不同方向。

反射光在哪个方向上分散,高光就在哪个方向上拉长。因此拉丝金属的高光通常垂直于拉丝纹路:竖直发丝形成横向的“天使环”,CD 的同心圆沟槽形成放射状高光。

anisotropy-cylinder-reflection.svg|997

这里容易把三个方向混为一谈:

  • 物理表面的沟槽或纤维方向;
  • 微表面法线分布被拉伸的方向;
  • Shader 中用来描述材质坐标的 Tangent / Bitangent

Shader 里的 Tangent 只是一根材质坐标轴,不保证就是物理沟槽方向。有的引擎用它表示纹路方向,有的用它表示高光拉伸方向。使用各向异性参数或方向贴图前,必须先确认当前引擎的约定。

光照模型

各向异性的原理相同,建模方式却取决于几何:

  • 拉丝金属、缎面仍可视为粗糙实体表面,用各向异性微表面 BRDF;
  • 头发接近细圆柱,光还会进入纤维内部,需要单独的纤维散射模型。

实体表面:各向异性 GGX

各向同性 NDF 绕法线 NN 旋转对称,只需要一个粗糙度。各向异性 NDF 还要知道切平面中的方向,因此需要 TTBB 两根轴,并分别给出粗糙度 αx\alpha_xαy\alpha_y

现代 PBR 通常直接把 GGX 的一个粗糙度拆成两个:

Daniso(h)=1παxαy1((ht)2αx2+(hb)2αy2+(hn)2)2D_{aniso}(h) = \frac{1}{\pi \alpha_x \alpha_y} \frac{1}{\left( \frac{(h \cdot t)^2}{\alpha_x^2} + \frac{(h \cdot b)^2}{\alpha_y^2} + (h \cdot n)^2 \right)^2}

αx=αy\alpha_x = \alpha_y 时,它退化为各向同性 GGX。Disney 用一个 anisotropic[0,1]anisotropic \in [0,1] 参数控制两根轴的比例:

aspect=10.9×anisotropicaspect = \sqrt{1 - 0.9 \times anisotropic}
αx=roughness2aspect,αy=roughness2×aspect\alpha_x = \frac{roughness^2}{aspect}, \quad \alpha_y = roughness^2 \times aspect

这只是 NDF 的参数化。完整 BRDF 仍然需要 Fresnel 项 FF 和几何遮蔽项 GG。单次散射 GGX 在高粗糙度下还会丢失微表面之间的多次反射能量,是否补偿、如何补偿,由具体渲染器决定。

从形状上看,各向异性就是把圆对称的高光波瓣拉成椭圆:

各向同性 vs 各向异性

Filament 的 anisotropy 从 0.0 增加到 1.0,高光逐渐拉长:

anisotropy 0 到 1

Filament 约定正值沿 tangent 轴产生各向异性,负值切换到 bitangent 轴。这里说的是材质坐标轴,不能直接翻译成物理沟槽方向:

正负各向异性方向

方向贴图可以逐像素旋转切向坐标轴,让拉丝方向沿表面变化:

方向贴图拉丝金属

(图片来自 Filament 材质文档

UE、Unity HDRP、Filament 都使用各向异性 GGX 这一类方案,但参数映射和能量补偿不完全相同。它适合拉丝金属、缎面等方向性微结构;CD 的彩色反射还需要衍射模型。

Ward(1992)用 αx\alpha_xαy\alpha_y 描述两个方向上的椭圆高斯分布,是早期常用的各向异性微表面模型。它形式简单,也能较好拟合测量材质,但原始形式的归一化和能量处理不如现代 GGX 完整,现在更多见于历史代码、离线渲染和教材。

Ashikhmin–Shirley(2000)是满足能量守恒和互易性的各向异性 Phong BRDF。两个指数 nun_unvn_v 分别控制切向轴上的高光锐度,同时处理 Fresnel,以及漫反射与镜面反射之间的能量分配。它解决了传统 Phong 的不少问题,但现代实时 PBR 更常用各向异性 GGX。

头发:Kajiya–Kay 与 Marschner

头发不能只当作普通粗糙表面。发丝接近细圆柱,光不仅在表面反射,还会折射进入纤维,再经过吸收和内部反射离开。

Kajiya–Kay(1989)只依赖发丝切线,计算便宜,适合移动端和风格化头发。Marschner(2003)进一步建模三条主要光路:

  • R:表面反射,形成接近光源颜色的主高光;
  • TT:穿过纤维,在逆光下产生透光;
  • TRT:进入纤维、内部反射一次后离开,形成受发色影响的次级高光。

需要折射、吸收、逆光透射和双层高光时,应使用 Marschner 系模型;只需要便宜、稳定的发丝高光时,Kajiya–Kay 仍然实用。

模型怎么选

  • 拉丝金属、缎面等实体表面:各向异性 GGX;
  • CD 彩色反射:各向异性高光只负责形状,还要补衍射;
  • 移动端、风格化头发:Kajiya–Kay + 偏移切线;
  • 强调透射和双层高光的真实头发:Marschner 系模型。

头发的半透明排序

为什么透明混合依赖绘制顺序

普通 Alpha 混合会把当前片元与颜色缓冲区中的旧颜色合成:

Cout=αCsrc+(1α)CdstC_{out} = \alpha C_{src} + (1-\alpha)C_{dst}

这个运算不满足交换律。先画近处再画远处,与先画远处再画近处,结果不同,所以透明表面通常必须从后往前绘制。

不透明物体可以开启 ZWrite:最近表面写入深度,后面的片元直接被深度测试拒绝。半透明物体不能照搬这个做法,因为最近的一层并没有完全遮住后面;如果它先写入深度,后面的发片会被整层挡掉。因此常规透明材质使用 ZWrite Off,也因此失去了深度缓冲提供的自动遮挡关系。

Unity 的透明排序为什么不够

Unity 会按 Render Queue 和相机距离对透明 Renderer 做从后往前排序。这个排序粒度通常是一个 Renderer,而不是 Renderer 内部的每张发片、每个三角形,更不是每个像素。

头发卡片会互相穿插,同一张卡片也可能同时包含离相机较近和较远的区域。此时不存在一个对所有像素都正确的物体级顺序。调整 Render Queue、Sorting Priority 或物体 Pivot,只能修正两个 Renderer 之间的先后,不能解决单个头发网格内部的交叉。

双面发片还有一层问题:正面和背面若在同一次绘制中混合,三角形提交顺序未必符合相机看到的前后关系。先画背面、再画正面可以让单张发片更稳定,但仍解决不了发片之间的循环遮挡。

实时项目常用方案

Alpha Clip

Alpha 高于阈值的像素当作完全不透明,低于阈值的像素直接丢弃。它可以开启 ZWrite,因此没有半透明排序问题,成本也最低;缺点是发梢和细丝边缘会变硬、产生锯齿。

开启 MSAA 时可以使用 Alpha-to-Coverage,把 Alpha 映射为多重采样覆盖率,减轻 Clip 边缘的锯齿。它仍然不是连续的透明混合,效果受 MSAA 采样数限制。

深度预写 + 半透明边缘

工程中常把发片拆成两层覆盖:

  1. 深度或 Cutout Pass 使用较高的 Alpha 阈值,写入头发主体的深度, 并且深度测试设置为: ZTest LEqual;
  2. Transparent Pass 使用较低阈值或 Alpha Blend,只补发梢和边缘,保持 ZWrite Off,并用 ZTest LEqual 只绘制被AlphaTest Cutout的Pass。

这能让大部分内部遮挡交给深度缓冲,只把真正需要柔和过渡的像素留给透明混合。代价是多一次 Draw Call,而且深度阈值过低会把后层头发错误遮住,过高又压不住排序穿帮。

拆分并排序发片

把头发拆成多个 Renderer 或 Draw Call,再按相机距离从后往前排序,可以提高排序粒度。它适合层次明确、数量有限的发片,但会增加 CPU 排序、状态切换和 Draw Call。交叉发片形成循环遮挡时,单一顺序仍然无解。

顺序无关透明 OIT

Depth Peeling、Per-Pixel Linked List 等方法会保存或逐层提取多个透明深度,再按像素合成;Weighted Blended OIT 则用加权累积近似顺序无关结果。它们能处理复杂交叉,但需要额外 Render Target、显存、带宽或多次绘制,通常只在 PC/主机的高质量方案中考虑。

头发卡片的实用选择

移动端和大多数实时角色,优先使用:

  1. 主体区域 Alpha Clip 并写深度;
  2. 边缘使用 Alpha-to-Coverage,或增加一遍 ZWrite Off 的透明混合;
  3. 美术制作阶段让发片尽量分层,减少互相穿插;
  4. 只有 Renderer 之间顺序错误时,才调整 Sorting Priority 或拆分 Renderer。

如果只是切换到 Transparent Queue,问题通常不会消失。它只提供 Renderer 级从后往前排序,没有改变发片内部的几何关系。

参考