Chapter 30: Subsurface Scattering
次表面散射(Subsurface Scattering,SSS)描述一类半透明材质的光传输:光线进入表面,在介质内部被散射和吸收,然后从邻近位置射出。读完本章后,读者应能定位一个皮肤、蜡、玉石或叶片材质中的柔化阴影、透光边缘和红色渗出来自哪条渲染路径,并能判断实时项目应使用 diffusion profile、screen-space SSS、thickness map、wrap lighting 还是 pre-integrated skin。
本章使用一个近景人脸 frame 作为贯穿材料。这个 frame 包含鼻翼、面颊、嘴角、耳廓和眼下阴影区域。常规表面反射模型把光照计算限制在一个表面点上,因此面颊阴影边界容易显得硬,耳廓背光区域容易显得暗,鼻翼附近的红色渗透会缺失。SSS 的目标是在有限预算内补上“光从相邻位置射出”的部分,同时保持法线、粗糙度和镜面高光仍由表面模型控制。
工程上要把 SSS 拆成三层判断。第一层是物理现象:入射点、介质路径、吸收、散射和出射点。第二层是近似模型:把真实体积传输压缩成 profile、屏幕空间滤波、厚度项或预积分查表。第三层是实现路径:哪些 buffer 参与计算,哪些颜色通道被模糊,哪些高频信号保持锐利,哪些工具证据能说明效果有效。
30.1 次表面散射的物理现象解释
SSS 的核心差异是空间位置发生分离。BRDF(Bidirectional Reflectance Distribution Function)描述同一个表面点上的入射方向和出射方向关系;BSSRDF(Bidirectional Surface Scattering Reflectance Distribution Function)允许入射点和出射点不同。对皮肤而言,光可以在鼻翼亮侧进入,在薄组织中扩散一段距离,再从阴影边缘附近离开表面。这个位移让阴影边界变软,也让部分区域出现带颜色的透光。
一个可观察路径可以拆成五步。光线先到达表面,一部分按 Fresnel 项形成表面反射,一部分进入介质。进入介质后的光与细胞、纤维、血液、脂肪或其他内部结构发生散射,方向被多次改变。每次传播都会被吸收,吸收强度与波长和路径长度相关。经历若干次散射后,剩余能量从某个邻近表面点离开。离开表面的能量再进入相机方向,参与最终像素颜色。
这条路径会留下三个典型视觉证据。第一,光照明暗交界会变宽,因为亮侧能量扩散到暗侧。第二,薄区域在背光下出现透亮感,因为光在内部路径较短,剩余能量较多。第三,皮肤阴影边缘常带暖色,因为不同波长的吸收和散射半径不同,实时材质常把红色通道设置成更大的扩散半径。
下面的图把这条路径放回渲染 frame。图中的“表面反射”通常仍由前几章讨论的局部光照、Fresnel 和微表面模型负责;“内部散射”才是本章要补的部分。
这张图的关键在于分工。表面反射负责锐利高光、视角相关 Fresnel 和粗糙度变化;内部散射负责低频柔化、透光和颜色渗出。把这两部分混成一个 diffuse color 乘法会让调试失去方向。正确的排查顺序是先固定表面高光,再检查 diffuse 或 irradiance 的扩散半径,最后观察厚度项和背光项。
SSS 还受几何尺度控制。相同的 diffusion radius 放在人脸近景和远景角色上,会产生完全不同的像素宽度。半径在世界空间表达时,渲染器需要把它转换成屏幕空间采样步长;半径直接在屏幕空间表达时,镜头距离变化会改变视觉强度。实时项目通常把 profile 参数绑定到材质或角色资产上,并在 pass 内结合 depth 估计像素到世界距离,让散射宽度随镜头和分辨率保持相对稳定。
吸收决定颜色和强度衰减。若把进入介质的距离记为 ,把消光系数记为 ,一个常用的直觉近似是透过能量随 下降。这里的公式用于建立判断:厚区域会迅速变暗,薄区域保留更多背光,红、绿、蓝通道可以使用不同衰减参数。公式本身无法覆盖完整的多次散射几何路径,实时渲染会把这部分折叠到 profile、LUT 或 blur kernel 中。
对近景人脸 frame,物理现象可以转成具体观察点。耳廓背后有强光时,厚度小、路径短,透光项贡献应明显。鼻翼和嘴角周围的阴影边缘应比纯 Lambert 模型更软。面颊大面积区域应保留皮肤纹理和毛孔的高频细节,因为 SSS 主要扩散低频 diffuse irradiance,镜面高光和法线高频不应被整体抹平。
30.2 常见 SSS 模型与近似方法
实时 SSS 模型的选择取决于要近似哪一段物理路径。真实 BSSRDF 会同时考虑入射位置、出射位置、方向、介质密度和边界条件。实时渲染通常把这些变量压缩成少量可调参数:散射半径、颜色 profile、厚度、曲率、法线深度权重和屏幕空间滤波范围。GPU Gems 的实时 SSS 章节展示了 wrap lighting、深度估计和 texture-space diffusion 这些经典近似,它们的共同目标都是以低成本复现柔化和透光。
diffusion profile 是较常见的高质量近似。它把“一个入射点的光会在周围多远范围内出射”表达成一个径向分布函数,通常按 RGB 通道分别给半径和权重。对皮肤,profile 会让红色扩散更宽,让绿色和蓝色保留更短半径。渲染时可以用这个 profile 生成 separable blur kernel,也可以预积分成若干采样权重。它适合 close-up skin、蜡、玉石等需要稳定材质身份的场景。
screen-space SSS 把扩散操作放在屏幕空间。渲染器先得到 depth、normal、diffuse lighting、albedo、material id 或 profile id,然后在后处理或 lighting pass 后对指定像素邻域做加权采样。采样权重来自 diffusion profile,同时用 depth 和 normal 阻止跨物体、跨轮廓和跨大法线变化区域的错误渗色。它的优势是能直接接入 deferred renderer,成本可控;边界是屏幕外信息缺失、遮挡背后的入射点不可见、极薄结构需要额外 thickness 数据补充。
thickness map 近似的是光穿过物体的距离。资产阶段可以烘焙一张厚度贴图,或者运行时从光源视角 depth map 估计 entry 和 exit 距离。着色时用厚度、背光方向和吸收参数生成 translucency 项。它特别适合耳廓、鼻翼、手指边缘、叶片、蜡烛边缘这类薄区域。它回答的问题是“背光穿过了多少介质”,而 screen-space blur 回答的问题是“表面邻域里有多少低频光照可以扩散”。
wrap lighting 是最低成本的近似。普通 Lambert 项使用 ,wrap lighting 会把光照响应向背光侧扩展,让明暗交界变宽。它几乎不需要额外 buffer,也能在移动端和风格化角色中快速增加柔和感。它的边界也清楚:它没有真实入射点和出射点分离,也没有厚度和材质内部路径,因此适合弱 SSS 或风格化补光。
pre-integrated skin 把皮肤散射、法线光照和曲率相关的结果预先存入 LUT。运行时 shader 使用 、曲率或散射半径等少量输入查表,得到比普通 diffuse 更像皮肤的颜色响应。它适合角色皮肤材质,尤其是需要稳定美术控制的实时项目。它的代价是表格和输入维度决定了表达范围;当几何厚度、遮挡和复杂背光成为主要视觉来源时,仍要结合 thickness 或 screen-space pass。
这些方法可以放入同一组选择维度中比较。
| 方法 | 输入数据 | 主要解决的问题 | 适用场景 | 主要边界 |
|---|---|---|---|---|
| diffusion profile | 材质半径、RGB 权重、profile id | 用材质参数控制扩散形状 | 近景皮肤、蜡、玉石 | 需要合适的空间尺度和能量归一 |
| screen-space SSS | depth、normal、diffuse lighting、albedo | 在可见表面邻域传播低频光照 | deferred 或 tiled renderer | 屏幕外和遮挡后信息缺失 |
| thickness map | 烘焙厚度或光源 depth 差 | 估计背光穿透强度 | 耳廓、叶片、手指、薄蜡 | 依赖资产质量或光源视角数据 |
| wrap lighting | 法线、光向、wrap 参数 | 扩展明暗交界 | 移动端、风格化、低成本材质 | 空间位移和厚度表达有限 |
| pre-integrated skin | LUT、、曲率或半径 | 稳定皮肤颜色响应 | 角色皮肤 shader | 表达范围受 LUT 维度限制 |
对近景人脸 frame,常见组合是 diffusion profile 加 screen-space SSS 处理面颊和鼻翼的低频柔化,用 thickness map 处理耳廓和鼻翼背光,用表面 BRDF 保留油脂高光和毛孔细节。wrap lighting 和 pre-integrated skin 可以作为低成本或风格化路径,也可以在没有完整后处理预算时提供基础皮肤响应。
一个实用判断是先明确视觉目标,再选择模型。目标是“阴影边缘像皮肤一样柔和”,优先使用 profile 驱动的屏幕空间扩散。目标是“耳朵背光透红”,优先使用厚度和吸收。目标是“低端设备上角色摆脱塑料感”,可先使用 wrap lighting 或 LUT。目标是“电影级近景皮肤”,通常需要 profile、厚度、表面 specular 和高质量纹理共同约束。
30.3 实时渲染中实现 SSS
实时实现的核心是把 SSS 放到明确的 frame 数据路径中。以 deferred renderer 为例,几何 pass 写入 depth、normal、albedo、roughness、material id、SSS mask 和 profile id;lighting pass 计算直接光与间接 diffuse;SSS pass 读取 diffuse lighting、depth 和 normal,按 profile 做屏幕空间扩散;combine pass 把扩散后的 diffuse 与未扩散的 specular、透明项和后处理结果合成。这个分工让每个 buffer 的职责可检查。
SSS pass 的输入应保持低频化。扩散对象通常是 diffuse irradiance 或 diffuse lighting,而非已经乘上全部高频 albedo 的最终颜色。若把完整 beauty color 直接 blur,毛孔、眉毛边缘、眼睑线、嘴唇轮廓和镜面高光都会被扩散,角色会显得油腻或蜡化。更稳定的做法是扩散低频光照,再乘以或混合材质颜色,同时用 mask 控制皮肤区域。
screen-space blur 需要三个权重共同决定采样贡献。第一个是 profile 权重,决定不同距离上的 RGB 扩散强度。第二个是 depth 权重,限制跨轮廓采样,让脸颊像素不会从背景、头发或衣领吸收颜色。第三个是 normal 权重,限制跨锐利折线和鼻翼边界的错误扩散。工程上可以把采样写成“空间半径 × profile 权重 × 深度相似度 × 法线相似度 × SSS mask”。
下面的伪代码展示了一个 separable screen-space SSS pass 的关键路径。它省略具体 API 绑定,只保留资源关系和判断点。
vec3 EvaluateSSS(vec2 uv, Profile profile) {
float centerDepth = texture(depthTex, uv).r;
vec3 centerNormal = DecodeNormal(texture(normalTex, uv));
vec3 result = vec3(0.0);
float weightSum = 0.0;
for (int i = 0; i < profile.sampleCount; ++i) {
vec2 sampleUv = uv + profile.offsets[i] * pixelRadius;
float sampleDepth = texture(depthTex, sampleUv).r;
vec3 sampleNormal = DecodeNormal(texture(normalTex, sampleUv));
float depthWeight = ComputeDepthWeight(centerDepth, sampleDepth);
float normalWeight = ComputeNormalWeight(centerNormal, sampleNormal);
vec3 profileWeight = profile.weights[i].rgb;
vec3 diffuseLight = texture(diffuseLightTex, sampleUv).rgb;
result += diffuseLight * profileWeight * depthWeight * normalWeight;
weightSum += max(profileWeight.r, max(profileWeight.g, profileWeight.b)) * depthWeight * normalWeight;
}
return result / max(weightSum, 1e-4);
}
这段代码证明三个实现点。采样半径来自 profile 和当前像素尺度,决定散射宽度。depth 和 normal 权重决定边界保护,控制鼻梁、嘴唇和脸部轮廓处的错误渗色。输入纹理使用 diffuseLightTex,说明高光和最终后处理颜色没有进入扩散路径。
半径换算是 SSS 稳定性的关键。profile 通常在材质单位或世界单位中定义,例如皮肤散射半径是若干毫米到厘米级的美术尺度。屏幕空间 shader 需要依据当前像素深度、投影矩阵和分辨率把这个半径换成 uv 偏移。若直接写死像素半径,角色靠近相机时扩散相对变窄,远离相机时扩散相对变宽,镜头运动会暴露问题。
性能预算通常从分辨率、采样数和材质筛选三处控制。SSS 可以在 half resolution 执行,再用 depth-aware upsample 回到 full resolution。blur 可以拆成横向和纵向两个 pass,使用 7 到 13 个采样点逼近较宽 kernel。tile 或 stencil 可以标记皮肤区域,让非 SSS 材质跳过这条路径。profile 数量也应受控,因为过多 profile 会增加分支、绑定和缓存压力。
translucency 项处理背光透射。实现上可以使用 thickness map、light-space depth 差或艺术家绘制的 thin mask。shader 根据光向、视线、法线和厚度估计透光强度,再乘以吸收颜色。耳廓 frame 中,背光从耳后进入,厚度小的区域会更亮且更偏红;厚度大的面颊区域贡献较弱。这个项通常作为额外 diffuse 能量加入,并受阴影、光强和材质 mask 共同限制。
工具复查时应把问题拆成 buffer 证据。RenderDoc、Nsight 或 Xcode GPU tools 中先确认 G-buffer 有正确的 skin mask、profile id、normal 和 depth。再查看 diffuse lighting 输入,确认未包含镜面高光。接着单独查看 SSS blur 输出,观察阴影边界是否变宽、轮廓是否出现跨物体渗色。最后对比 combine 输出,确认 specular 仍保持锐利,眼睛、眉毛、衣物和背景没有被皮肤 profile 扩散。
常见失败可以按症状定位。若整张脸像蜡像,通常是扩散半径过大、输入包含高频 albedo 或 specular 被混入 blur。若效果几乎不可见,通常是 SSS mask、profile id、半径单位或 combine 权重没有连上。若脸部轮廓向背景渗色,优先检查 depth 权重和 upsample。若鼻梁、唇线和眼窝边界被抹平,优先检查 normal 权重和材质 mask。若耳廓背光缺失,优先检查 thickness、光向和阴影项。
30.4 皮肤材质中的次表面散射效果观察
皮肤 SSS 的观察应从具体部位开始。耳廓是最容易验证 thickness 的区域。强背光或侧后方光源下,耳廓边缘和薄组织应出现透亮的暖色,内部厚处应更暗。若整只耳朵均匀发红,thickness map 过平或吸收衰减缺少距离差异。若耳廓完全没有透光,背光方向、thin mask 或 translucency combine 可能没有参与当前灯光。
鼻翼和嘴角适合观察局部阴影边缘。纯表面 diffuse 会让鼻翼投影边界更硬,SSS 应让边缘略微展开,并出现微弱暖色过渡。这个过渡需要受到 normal 和 depth 保护,鼻孔、唇线和鼻翼折线仍应保留结构。若边缘被大面积糊开,采样半径或 normal 权重偏宽。若只有红色叠加却没有柔化,模型可能在使用简单 tint,缺少空间邻域扩散。
面颊适合观察低频扩散与高频细节的分离。健康的 SSS 会让大面积阴影过渡更柔和,同时保留毛孔、细纹和油脂高光。毛孔来自 normal、roughness、albedo 或 detail map;高光来自表面 BRDF;SSS 主要改变 diffuse light 的低频分布。若面颊细节整体下降,扩散对象可能选择了最终颜色。若高光边界被模糊,specular 进入了 SSS pass 或 combine 顺序错误。
粗糙度变化会改变 SSS 的可见程度。高粗糙度区域表面高光弱,SSS 带来的柔化更容易被观察到。低粗糙度区域油脂高光强,表面反射会覆盖部分散射观感。皮肤 shader 应让 roughness 控制 specular lobe,让 SSS profile 控制 diffuse 扩散,两者共享同一材质但承担不同视觉任务。把 roughness 当成 SSS 强度旋钮会让高光和透光关系失真。
肤色贴图也需要和 SSS 分工。albedo 决定表皮颜色、斑点和局部色差,SSS profile 决定低频光照如何跨空间传播。过强的红色 albedo 会让 SSS 误判为红光渗透;过强的 SSS tint 会掩盖真实肤色变化。调试时可以临时固定 albedo 为中性肤色,只看 diffuse lighting 和 profile 输出,再恢复 albedo 观察色彩是否过饱和。
近景人脸 frame 的观察顺序可以固定为六步。第一,关闭 SSS,保存普通表面光照作为基线。第二,开启 screen-space SSS,只观察 diffuse 输出是否柔化。第三,开启 thickness,只观察耳廓和鼻翼背光是否随厚度变化。第四,恢复 specular,检查高光是否保持锐利。第五,切换相机距离,确认散射半径没有随像素比例异常变化。第六,切换光源方向,确认背光透射和正面柔化由不同数据路径控制。
皮肤材质还要和妆容、胡须、头发边缘、眼球和牙齿分开处理。妆容区域可能需要较弱的 SSS mask,因为粉底会改变表面散射观感。胡须和眉毛属于高频遮挡和毛发着色,通常不进入皮肤 profile。眼球有独立的角膜、巩膜和虹膜模型。牙齿和嘴唇虽然也有半透明性,但 profile、厚度和颜色响应与面部皮肤不同。把这些区域交给同一个 profile 会让角色材质身份混乱。
最终判断应回到视觉结果和资源路径。一个合格的皮肤 SSS 通过低频 diffuse 跨邻域扩散获得柔和过渡,通过厚度控制薄区域发光,并让高频表面细节保留在 normal 和 specular 路径中。调试时看到的每个变化,都应能回溯到 profile 半径、mask、depth 权重、normal 权重、thickness 或 combine 权重中的一个对象。
最小自检任务
给定一个 deferred renderer 的近景角色 frame:普通光照下脸部阴影边界偏硬,耳廓背光没有透红,开启现有 SSS 后面颊毛孔和高光被明显抹平。请设计一条最小排查路径,说明应该检查哪些 buffer、如何调整 SSS pass、如何判断 diffusion profile、screen-space blur 和 thickness map 是否各自承担了正确职责。
答案要点
先保存关闭 SSS 的基线 frame,确认表面 BRDF、normal、roughness 和 specular 高光本身稳定。随后在 G-buffer 中检查 skin mask、profile id、depth、normal 和 albedo,确认 SSS 仅作用于皮肤材质区域。接着查看 SSS pass 输入,扩散对象应是 diffuse lighting 或低频 irradiance;若输入是最终 beauty color,毛孔和高光会随 blur 一起下降。然后检查 screen-space blur 的 profile 半径、depth 权重和 normal 权重:阴影边缘应变宽,鼻梁、嘴唇、脸部轮廓处的跨背景或跨材质采样应被 depth 与 normal 权重拦截。
耳廓背光应单独检查 thickness 路径。观察 thickness map 或 light-space depth 差是否能区分薄边缘和厚组织,再确认背光项受光向、阴影、吸收颜色和 skin mask 控制。若 thickness 输出稳定但 combine 后看不到变化,应检查 translucency 权重和最终合成顺序。最后恢复 specular 路径,确认 specular 保持在独立路径中,roughness 仍控制高光形状,SSS profile 仅控制 diffuse 扩散。完成这些步骤后,三个职责应清楚分离:diffusion profile 决定散射半径和颜色分布,screen-space blur 负责可见表面邻域扩散,thickness map 负责薄区域背光透射。
本章知识点总结
- 空间分离:SSS 允许光在一个表面点进入介质,并从邻近表面点射出。
- 表面分工:BRDF 负责同点表面反射,BSSRDF 负责跨位置的内部散射。
- 视觉证据:柔化阴影、薄处透光和暖色渗出是皮肤 SSS 的主要观察点。
- 尺度控制:散射半径需要从材质尺度稳定映射到屏幕采样尺度。
- Profile 作用:diffusion profile 用 RGB 半径和权重描述不同通道的扩散分布。
- 屏幕扩散:screen-space SSS 通过 depth、normal 和 mask 约束可见邻域的低频光照传播。
- 厚度路径:thickness map 或 light-space depth 差用于估计背光穿透距离。
- Wrap 近似:wrap lighting 通过扩展 响应获得低成本柔化。
- 预积分皮肤:pre-integrated skin 使用 LUT 稳定皮肤颜色响应和明暗过渡。
- 输入选择:SSS pass 应扩散 diffuse lighting 或低频 irradiance,specular 路径保持锐利。
- 边界保护:depth 权重和 normal 权重共同限制跨轮廓、跨折线和跨材质渗色。
- 性能预算:half resolution、separable blur、tile 筛选和 profile 数量共同控制成本。
- 皮肤观察:耳廓验证厚度,鼻翼验证阴影柔化,面颊验证低频扩散和高频细节分离。
- 排查顺序:先看 G-buffer,再看 SSS 输入,再看 blur 输出,最后看 combine 和高光保留。