GRAPHICS × PRODUCTION
中EN

皮了个牛

↓

计算机图形学

移动端 Subsurface Scattering:基于 Spherical Gaussian 的实现

比较移动端 SSS 方案,拆解散射的范围、方向和软化三个视觉要素,使用 Spherical Gaussian 拟合 Kernel,并实现 Curvature Scatter、Bump Scatter、Shadow Scatter 与 Transmission,给出参数、性能数据和结果对比。

2022-04-1820 分钟阅读
移动端 Subsurface Scattering:基于 Spherical Gaussian 的实现

一、技术方案选型

在 PC 和 Console 平台上,基于屏幕空间模糊的散射方案因效果显著成为主流。其核心思想是开辟独立的 Post Pass,对屏幕空间 Scene Color 中标记为 Subsurface 的区域执行卷积模糊。这一方案的主要代价是多次全屏 Color Buffer 读取,对带宽和计算量(大散射半径时尤为明显)要求较高。

具体来说,屏幕空间方案(如 Separable SSS / Jorge Jimenez 2015)将散射 Kernel 拆分为水平和垂直两个 Pass,每个 Pass 采样 1125 个 Tap。在 1080p 分辨率下,每帧需要读取约 2 × 1920 × 1080 × 25 × 8Byte ≈ 830MB 的带宽(RGBA16F 格式)。PC 端显卡的带宽预算(300900 GB/s)可以轻松承受,但 Mobile GPU 的带宽(30~60 GB/s)直接被击穿。加上 Tile-Based Renderer 在全屏 Post Pass 时需要将 Tile Memory 内容回写主存再重新加载,实际代价远超理论计算值。

移动端在带宽和计算能力上存在明确约束,屏幕空间方案并不适用。UE5 已开放了屏幕空间次表面散射对 Mobile 的支持:如果目标设备性能足够强劲(如旗舰 SoC),可以直接尝试这一方案。但对于中低端机型覆盖的需求,仍需要更轻量的替代方案。通常有两种:

方案一:Pre-Integrated LUT

预先将散射结果烘焙到 LUT 中,运行时仅需一次纹理采样。代价是增加两张贴图的带宽开销,且由于预烘焙的特性,不同 SSS Color 需要对应不同的 LUT。目前多数 Mobile 甚至 Console 游戏采用此方案,如《教团 1886》和《对马岛之魂》。

Pre-Integrated LUT 的生成方式:将 Diffusion Profile(散射核函数)沿表面曲率和 NdotL 两个维度积分,结果存入一张 2D LUT。横轴为 NdotL(范围 [-1, 1]),纵轴为 1/radius(曲率的倒数)。运行时以当前像素的 NdotL 和曲率作为 UV 采样 LUT,得到散射后的 Diffuse 响应值。这张 LUT 的分辨率通常为 256×256 或 512×512,占用 256KB~1MB。

LUT 方案的局限:

  • 一张 LUT 对应一组固定的散射参数(ScatterColor + ScatterWidth)。如果场景中有不同肤色/材质的角色,需要多张 LUT 或引入额外参数化
  • 无法处理阴影边缘散射:阴影值不在 LUT 的参数空间中
  • 曲率估算精度有限时(如 Screen Space Curvature),LUT 采样的质量直接受影响

方案二:Spherical Gaussian 解析近似

使用球面高斯函数对散射轮廓做解析拟合,省去 LUT 采样以节省带宽,且因为非预计算可以支持任意散射颜色。代价是占用额外的 ALU。目前业界对该方案在产品中的应用案例较少。

这里选择方案二来做实现和优化。

1.1 方案对比

维度Pre-Integrated LUTSpherical Gaussian
运行时计算1次纹理采样 + 简单乘法多次 exp() + 加法(纯 ALU)
带宽开销+1~2 次纹理 Fetch(LUT + Curvature Map)无额外纹理访问
参数灵活度固定(烘焙时确定)完全动态(运行时任意修改)
多肤色支持需多张 LUT 或参数化 LUT修改 ScatterColor 参数即可
Shadow Scatter需额外机制自然支持(Shadow 值代入 SG)
Bump Scatter需额外机制自然支持(多法线代入 SG)
视觉质量受 LUT 分辨率和曲率精度限制受 Lobe 数量和拟合精度限制
ALU 消耗低中偏高(3~4 次 exp 指令)
Mobile GPU 适配带宽敏感时不利ALU 充足时有利

现代 Mobile GPU(Adreno 6xx+、Mali G7x+)的 ALU 性能增长快于带宽增长。在 ALU 充足但带宽受限的架构上,SG 方案的"用 ALU 换带宽"策略越来越有利。

二、SSS 的三个视觉要素

对于皮肤散射效果,需要同时满足三个视觉条件才能产生可信的 Skin 质感:

  1. Bump Scatter — 表面凹凸细节在散射范围内被平均化
  2. Curvature Scatter — 表面曲率较大的区域(如鼻翼、耳轮廓)呈现更强的散射
  3. Shadow Scatter — 阴影边缘区域的散射透红

多数公开实现通常只覆盖其中一到两个要素。这次的目标是在移动端把三个效果都做完整。

三者的物理基础统一:光进入皮肤表面后在内部发生多次散射,最终从不同位置射出。散射距离取决于组织的吸收系数和散射系数。对于人类皮肤:

  • 红色光散射距离最远(~3.67mm),因为血红蛋白对红光吸收最弱
  • 绿色光中等(~1.21mm)
  • 蓝色光最短(~0.30mm)

这个 per-channel 不同散射距离的特性直接决定了实现中需要对 RGB 三通道使用不同的散射参数。

三、工程实现

3.1 框架集成

由于追求对任意 SSS Color 的支持,放弃引擎内置的 LUT 版本,选择 Spherical Gaussian 方案。

在 Unreal Engine 中的集成采用非侵入式方案:Shader 层面继承原有的 Pre-Integrated Skin Shading Model 并修改其 HLSL 代码;上层参数通过 Plugin 提供的 Custom Output 节点传递 SG 散射所需的参数(如 Scatter Width、Scatter Color 等)。

具体的修改点集中在三处:MobileBasePass.usf 的 Skin ShadingModel 分支中把 LUT 采样替换为 SG 计算,ShadingModelsMobile.ush 中新增 SG SSS 的评估函数,Material Graph 通过 Custom Output 暴露 ScatterWidth、ScatterColor 参数。

3.2 Spherical Gaussian 拟合散射 Kernel 的数学推导

Spherical Gaussian(SG)函数的基本形式:

G(v;μ,λ,a)=a⋅eλ(v⋅μ−1)G(\mathbf{v}; \mu, \lambda, a) = a \cdot e^{\lambda(\mathbf{v} \cdot \mu - 1)}

其中 μ\mu 是 Lobe 方向,λ\lambda 是 Sharpness(控制宽度),aa 是 Amplitude。

对于 SSS 场景,将问题简化为 1D:以 NdotL(或其替代量)作为输入变量,散射 Kernel 退化为 1D 的 Gaussian-like 函数。目标是用 SG 拟合 Diffusion Profile 积分后的 NdotL→Scatter 响应曲线。

Diffusion Profile 的经典形式(Christensen-Burley 2015):

R(r)=As⋅e−r/d8πdr+As⋅e−r/(3d)24πdrR(r) = A_s \cdot \frac{e^{-r/d}}{8\pi d r} + A_s \cdot \frac{e^{-r/(3d)}}{24\pi d r}

其中 rr 是散射距离,dd 是 mean free path 的缩放因子。将此 Profile 对半球面积分(考虑表面曲率),得到 NdotL→Scatter 的 1D 响应曲线。该曲线可以用若干 SG Lobe 的叠加来拟合:

S(x)≈∑i=1Nwi⋅e−(x−ci)2/(2σi2)S(x) \approx \sum_{i=1}^{N} w_i \cdot e^{-(x - c_i)^2 / (2\sigma_i^2)}

其中 xx 是变换后的 NdotL 值(考虑曲率偏移),wiw_i、cic_i、σi\sigma_i 是第 ii 个 Lobe 的权重、中心和宽度。

实际实现中使用单 Lobe 近似(N=1)以控制 ALU 开销:

// 单 Lobe SG 散射近似
// x: 输入值(NdotL 或其变换),scatterWidth: 散射宽度参数
half SGScatter(half x, half scatterWidth)
{
    // scatterWidth 越大,exp 衰减越慢,散射范围越广
    half sigma2 = scatterWidth * scatterWidth;
    return exp(-x * x / (2.0 * sigma2));
}

单 Lobe 在散射宽度较小时(scatterWidth < 0.3)拟合精度足够。宽度更大时,单 Lobe 无法准确模拟 Diffusion Profile 的长尾特征,此时可以增加第二个 Lobe(宽且弱)模拟长距离散射:

half3 EvaluateSGSSS(half NdotL, half3 scatterColor, half scatterWidth)
{
    // Lobe 1: 窄、强 — 模拟短距离散射
    half scatter1 = SGScatter(NdotL, scatterWidth);
    // Lobe 2: 宽、弱 — 模拟长距离散射(权重 0.3,宽度 ×3)
    half scatter2 = SGScatter(NdotL, scatterWidth * 3.0) * 0.3;
    
    half scatter = scatter1 + scatter2;
    return scatterColor * scatter;
}

双 Lobe 的参数(权重比 1:0.3,宽度比 1:3)来自对 Christensen-Burley Diffusion Profile 的最小二乘拟合。在 Mobile 上,单 Lobe 版本的 ALU 增加约 2~3 条指令(一次 mul + 一次 exp + 一次 mad),双 Lobe 翻倍。考虑到 exp 指令在多数 Mobile GPU 上是 special function unit 处理(不占 ALU pipeline),实际开销有限。

3.3 Curvature Scatter

在 MobileBasePass.usf 中,通过自定义 Output 函数获取参数后代入 SG 算法。曲率计算不仅考虑表面几何曲率,还需根据 Light Direction 加权一个方向性曲率分量。这样可以使与光线方向夹角更小的区域散射更弱,符合物理预期。具体算法参考《对马岛之魂》的 SIGGRAPH 分享。

3.3.1 Curvature 计算:fwidth 方法

fwidth 方法利用屏幕空间导数近似局部曲率:

// 基础曲率:世界空间法线的散度近似
half curvature = length(fwidth(worldNormal)) * _CurvatureScale;

// 方向性曲率:在光照方向上加权
half NdotL = dot(worldNormal, lightDir);
half directionalCurvature = curvature * saturate(1.0 - NdotL);

fwidth(v) = abs(ddx(v)) + abs(ddy(v)),其物理含义是法线在屏幕空间相邻像素间的变化量。变化越大,表示几何曲率越高。

fwidth 方法的优缺点:

  • 优点:零额外内存开销、不依赖预处理、适用于任意 Mesh
  • 缺点:精度与分辨率耦合,分辨率降低时相邻像素覆盖更大的世界空间面积,曲率估计偏大;在 Triangle Edge 处可能产生不连续
3.3.2 Curvature 计算:Mesh 预计算方法

另一种方法是在离线阶段预计算 Mesh 的逐顶点曲率,存储在 Vertex Color 或额外的 UV Channel 中:

// 预计算方法:从顶点数据读取曲率
half curvature = input.vertexColor.r * _CurvatureScale;
half directionalCurvature = curvature * saturate(1.0 - NdotL);

预计算曲率可以使用 Principal Curvature(最大/最小曲率)或 Mean Curvature。计算方式是对 Mesh 的每个顶点,基于其 1-ring 邻域拟合二次曲面,提取曲率张量的特征值。工具链上可以在 Houdini、Maya 或自定义 Baker 中完成。

维度fwidth 方法Mesh 预计算
内存开销0+1 Vertex Channel(~4Byte/vertex)
精度依赖分辨率固定精度,与分辨率无关
动态 Mesh自动适应(骨骼动画、Morph)需要实时更新或预计算所有 Blend Shape
Edge 连续性可能在三角形边界跳变连续(顶点插值)
LOD 兼容性自动适应不同 LOD每级 LOD 需独立预计算
适用场景快速原型、动态物体多产品级品质、静态角色

这里选择 fwidth 方法,理由是:(a) 目标平台为 Mobile,额外 Vertex 数据增加内存和带宽;(b) 角色有大量 Blend Shape 动画,预计算方案需要为每个 Shape 烘焙曲率,工程成本高。

方向性曲率的意义:与光线方向正对的区域(NdotL 高)散射范围应更小(光直接穿透的路径短),而侧面区域(NdotL 低)光路更长、散射更明显。fwidth 利用屏幕空间导数近似局部曲率,代价是依赖分辨率,低分辨率下曲率估计会偏大。

完成基于双曲率的散射后,面部会呈现曲率驱动的透红效果。但此时整体仍有明显的塑料感——这是因为缺少 Bump 层面的散射处理。

3.4 Bump Scatter

根据散射现象的物理机制:表面凹凸细节会在散射范围内被空间平均。对于 RGB 三通道具有不同散射宽度的皮肤(红色通道散射最远),R 通道对应的法线应更为平滑(模糊),而 G/B 通道的法线应保持较锐利的细节。

3.4.1 四张法线的完整 Interpolation 流程

实现方式:以 Detail Normal 和 Geometry Normal 作为两个极端,利用 Scatter Color 的三个通道作为 Alpha 插值权重,生成四张不同细节程度的法线(原始 + R/G/B 三通道对应)。

// 输入:两张极端法线
half3 geometryN = normalize(input.worldNormal);           // 最平滑:仅几何法线
half3 detailN   = BlendNormal(geometryN, normalMapSample); // 最锐利:叠加 Normal Map

// Scatter Color 归一化为插值系数
// scatterColor.r 最大(红光散射最远)→ normalR 最接近 geometryN
// scatterColor.b 最小(蓝光散射最短)→ normalB 最接近 detailN
half3 scatterColor = _ScatterColor.rgb;

// 按 RGB 通道的散射强度生成三张插值法线
half3 normalR = normalize(lerp(detailN, geometryN, scatterColor.r));
half3 normalG = normalize(lerp(detailN, geometryN, scatterColor.g));
half3 normalB = normalize(lerp(detailN, geometryN, scatterColor.b));
// 第四张:原始 detailN,用于非散射区域的 Specular 和常规 Diffuse
half3 normalFull = detailN;

插值逻辑的关键:scatterColor.r 通常最大(如 (0.8, 0.4, 0.2) 代表皮肤的红色散射偏强),所以 normalR 最接近 geometryN(最平滑),normalB 最接近 detailN(最锐利)。这精确地模拟了"红光在更大范围内被平均化,蓝光保持局部细节"的物理行为。

3.4.2 Per-Channel NdotL 计算与 SG 评估

使用三张法线分别计算 NdotL,代入 SG 得到三个通道的散射响应:

// Per-channel NdotL
half NdotL_R = dot(normalR, lightDir);
half NdotL_G = dot(normalG, lightDir);
half NdotL_B = dot(normalB, lightDir);

// Per-channel SG SSS 评估
// 每个通道使用自己的 NdotL 和对应的 scatterWidth
half sssR = SGScatter(NdotL_R - directionalCurvature, _ScatterWidth * scatterColor.r);
half sssG = SGScatter(NdotL_G - directionalCurvature, _ScatterWidth * scatterColor.g);
half sssB = SGScatter(NdotL_B - directionalCurvature, _ScatterWidth * scatterColor.b);

// 最终 Bump Scatter 贡献
half3 bumpSSS = half3(sssR, sssG, sssB) * scatterColor;

// 非散射区域使用原始法线的 Diffuse
half NdotL_Full = saturate(dot(normalFull, lightDir));
half3 directDiffuse = NdotL_Full * albedo;

// 最终合成
half3 diffuseResult = directDiffuse + bumpSSS * albedo * _ScatterIntensity;

SGScatter 的输入是 NdotL - directionalCurvature:曲率偏移把 SG 的响应中心从 NdotL=0(明暗交界线)推向暗面一侧,模拟光在弯曲表面的更长散射路径。scatterWidth 也按通道缩放,红色通道乘以更大的系数,SG Lobe 随之更宽。

这样散射宽度越大的通道法线越平滑,最终效果上 Skin 的层下质感就出来了。

3.5 Shadow Scatter

Shadow 散射的核心思想是将阴影值作为 NdotL 的替代输入,在阴影区域再计算一次 SG SSS。由于阴影值范围为 [0, 1],需要重映射至 [-1, 1](即 shadow * 2 - 1)以匹配 SG 函数的输入域。

3.5.1 Remap 函数设计与边界行为分析
half3 ShadowScatter(half shadow, half3 scatterColor, half scatterWidth)
{
    // 将 shadow [0,1] 重映射为 [-1,1] 以匹配 SG 输入域
    half remapped = shadow * 2.0 - 1.0;
    
    // 对三通道分别评估
    half ssR = SGScatter(remapped, scatterWidth * scatterColor.r);
    half ssG = SGScatter(remapped, scatterWidth * scatterColor.g);
    half ssB = SGScatter(remapped, scatterWidth * scatterColor.b);
    
    return half3(ssR, ssG, ssB) * scatterColor;
}

边界行为分析:

shadow 值remapped 值SGScatter 响应物理含义
0.0(全阴影)-1.0exp(-1/(2σ²)) ≈ 0(σ<0.5 时)深阴影内部无可见散射
0.5(半影中心)0.0exp(0) = 1.0(最大值)阴影边缘散射最强
1.0(全光照)1.0exp(-1/(2σ²)) ≈ 0(σ<0.5 时)完全光照区域无额外散射

SG 函数在 remapped=0(即 shadow=0.5,半影区域)处取得最大值,这精确地对应了阴影散射发生在明暗过渡区域的物理预期。当 scatterWidth 较大时,SG 的 Lobe 展宽,散射效果向全光照和全阴影两侧扩展——但衰减是连续且平滑的,不会产生硬边界。

边界情况:

  • 如果 Shadow Map 使用硬阴影(无 PCF),shadow 值非 0 即 1,remapped 为 ±1,SG 响应几乎为零,Shadow Scatter 失效。因此此方案要求 Shadow Map 具备软边缘(PCF / PCSS / Contact Shadow)
  • scatterWidth 过大(>1.0)会导致即使在 remapped=±1 时 SG 响应仍然显著,视觉上表现为全阴影/全光照区域出现不自然的红晕。实测中 scatterWidth 在 [0.2, 0.6] 范围内效果最佳
// 最终散射合成
half3 totalSSS = bumpSSS + shadowScatter;
// 注意:curvature 已经融合在 bumpSSS 的 NdotL 偏移中,不再单独加

// 应用到最终着色
half3 finalColor = (directDiffuse + totalSSS * _ScatterIntensity) * lightColor * shadow;

加入 Shadow Scatter 后,阴影边缘区域呈现出红润的散射过渡,进一步强化了皮肤的次表面感知。

Shadow 散射只在阴影过渡区域(shadow ∈ (0,1))产生可见效果。完全在光照中(shadow=1 → remapped=1)或完全在阴影中(shadow=0 → remapped=-1)时,SG 的 Lobe 衰减使得贡献接近零,符合预期。

四、Transmission 透射效果延伸

耳部、鼻翼等薄组织区域在强逆光下会出现光透射现象:光从一侧穿透薄层组织后从另一侧射出,呈现深红色或橘红色。这是 SSS 的另一个重要视觉特征,与前述的散射(入射/出射在同侧)互为补充。

4.1 实现思路

Transmission 的核心是估算光从物体背面穿透到正面的路径长度:

half3 TransmissionSSS(half3 worldPos, half3 worldNormal, half3 lightDir, 
                       half3 transColor, half thickness)
{
    // 反转法线方向,模拟光从背面入射
    half3 backLightDir = lightDir + worldNormal * _Distortion;
    half VdotBackL = saturate(dot(viewDir, -normalize(backLightDir)));
    
    // 透射强度随厚度衰减
    half transmission = pow(VdotBackL, _TransPower) * exp(-thickness / _TransScale);
    
    return transColor * transmission;
}

关键参数:

参数含义
thickness组织厚度,通常存储在 Mesh 的 Vertex Color 或单独的 Thickness Map 中,可通过 AO Baker 的反向(从内表面向外采样遮蔽率)烘焙获得
_Distortion法线偏移量,控制透射方向的扩散程度,值越大透射效果越 Diffuse
_TransPower控制透射的集中度(类似 Specular Power)
transColor透射颜色,通常为深红/橘红色(模拟血液对红光的选择性透过)

Transmission 未纳入当前实现的原因:需要额外的 Thickness 数据(增加美术制作成本和内存),且其视觉贡献主要集中在耳部和手指等极薄区域,对整体效果的影响相对 Bump/Curvature/Shadow 三要素更为次要。后续可作为优化方向,优先在关键角色的耳部区域局部启用。

五、完整参数表

参数名类型范围默认值说明
_ScatterColorhalf3[0,1] per channel(0.8, 0.3, 0.15)RGB 散射颜色,R>G>B 模拟皮肤
_ScatterWidthhalf[0.05, 1.0]0.35散射宽度,控制 SG Lobe 展宽
_ScatterIntensityhalf[0, 2.0]1.0散射强度乘数
_CurvatureScalehalf[0.1, 10.0]1.0fwidth 曲率缩放因子
_ShadowScatterWidthhalf[0.1, 0.8]0.4Shadow Scatter 专用宽度
_BumpScatterStrengthhalf[0, 1.0]0.7Bump Scatter 强度
_TransColorhalf3[0,1] per channel(0.9, 0.2, 0.05)透射颜色(预留)
_Thicknesshalf[0, 1.0]0.5组织厚度(预留)

六、性能数据

测试平台:Adreno 660(Snapdragon 888)、1080×2400 分辨率、单方向光

方案Fragment Shader ALU纹理采样次数额外带宽GPU 耗时(全屏 Skin)
无 SSS(Standard Lit)基准基准0基准
Pre-Integrated LUT+8 ALU+2(LUT + Curvature)+0.5 MB/帧+0.12 ms
SG 单 Lobe+18 ALU+00+0.15 ms
SG 双 Lobe + Shadow+32 ALU+00+0.24 ms
屏幕空间 Separable SSS+6 ALU(模糊 Pass)+22(11-tap × 2 Pass)+16 MB/帧+1.8 ms

SG 单 Lobe 方案在耗时上与 LUT 方案接近(+0.03ms),但省去了 LUT 纹理的内存和带宽开销,且支持完整的三要素散射。双 Lobe + Shadow 的额外 0.09ms 开销换来明显更好的阴影边缘质量。屏幕空间方案在 Mobile 上的 1.8ms 开销使其基本不可用。

七、结果与对比

完成 Curvature + Bump + Shadow 三层散射的完整实现后,相比 Unreal Engine 内置的 Pre-Integrated Skin,皮肤质感有明显提升,表面呈现出更强的层下感和通透性,不再是单纯的表面着色。

各要素的独立贡献:

  • 仅 Curvature Scatter:鼻翼、眉弓等高曲率区域出现红色散射,但平坦区域(如脸颊)缺乏层次
  • +Bump Scatter:平坦区域的皮肤纹理被柔化,R 通道的细节法线平滑产生温暖的底色透出感
  • +Shadow Scatter:阴影边缘出现连续的红色过渡带,消除了硬阴影的"贴片感"

三者叠加后,面部在各种光照角度下都能呈现出可信的皮肤质感。

耳部 Transmission 透射和环境光散射尚未实现,对整体效果的影响相对次要,可作为后续优化方向。

Reference