GPU Gems 3 · Chapter 14. Advanced Techniques for Realistic Real-Time Skin Rendering

从皮肤的镜面反射与次表面散射出发,解释 diffusion profile 的高斯和近似、纹理空间 U/V 卷积、stretch correction 与薄区域透射如何组合成实时皮肤管线。

学习目标

  • 能解释实时皮肤管线如何把镜面 BRDF、次表面散射与薄区域透射放在不同阶段,并指出各自的输入输出
  • 能修改 Real-Time Skin Rendering Lab 的 diffusion profile、卷积路径、Gaussian 数量、纹理分辨率与透射开关,观察画质、柔软度和带宽的取舍
  • 能回答:为什么要用 sum-of-Gaussians、stretch correction 和 translucent shadow map,而不是一次大 blur 解决所有皮肤问题

先问:为什么高清扫描的脸仍然会“干硬”

想象一张拥有皱纹、毛孔和雀斑的高清照片。把它贴到一个模型上并直接做普通漫反射,细节虽然齐全,皮肤却像石膏:表面高光不对,耳朵和鼻翼也没有柔和的透光。

本章要解决的是:怎样让 GPU 同时表现皮肤表面的高光、光在组织内短距离扩散后的柔和颜色,以及薄区域的透射。没有这些层次,几何和纹理越清晰,错误反而越显眼。

1. 皮肤是三种光学行为的叠加

皮肤至少要拆成三条线索:第一条是表面直接反射,负责随观察角变化的亮斑;第二条是光进入表皮和真皮后在邻域内扩散,负责柔软的肤色;第三条是耳朵等薄区域中光穿过表面,从背光侧显出暖色。

skin = surface reflectance + subsurface transport + transmissionsurfacespecular BRDFgrazing highlightsepidermis / dermislight exits nearbysoft glow + local detail只做 diffuse 会“干硬”;只做 blur 又会丢掉表面高光与薄耳朵透光
真实皮肤不是一层漫反射颜色:表面高光、组织内的散射吸收和薄区域透射共同决定柔软外观。

如果只把第二条当作普通 Lambert diffuse,光会在进入皮肤后立刻“停在原地”,脸颊、鼻翼与耳廓就会失去柔和的空间联系。反过来,如果只做大范围 blur,又会抹掉毛孔、皱纹和接触边界。因此实时方案把表面反射、局部散射和透射分开计算,再在最终 3D pass 合成。

2. 表面高光需要物理合理的镜面 BRDF

Phong 很便宜,但它在掠射角下的反射趋势不够可信,也可能输出比入射更多的能量。更物理的 Kelemen/Szirmay-Kalos 模型接近 Torrance/Sparrow 一类的外观,同时保留了实时 shader 可以承受的成本。

specular response:grazing angle 是人眼敏感区reflected intensitygrazing angle →physical BRDFPhong镜面不是独立“加一层”:反射掉的能量会减少可供次表面散射的入射光
Phong 能快速给出高光,但在掠射角和能量守恒上不够可信;更物理的 BRDF 只增加少量 shader 工作就能改善脸部反射。

镜面与次表面不是两个可以任意相加的滤镜。若一部分入射能量已经被表面反射,剩下能进入组织的能量就应减少。实际管线可以预计算粗糙度与 N · L 相关的积分表,用它在生成 irradiance texture 前衰减可用于散射的 diffuse 能量。

3. diffusion profile:先量出“一点光晕多远”

把一束很细的白色激光打到暗房中的皮肤薄片上,你会看到入射点周围的彩色光晕。diffusion profile 就是这团光晕随距离变化的曲线;红、绿、蓝三个通道的宽度不同,红光通常传播得更远。

one laser dot → three color-dependent halosincident pointred · widergreenblue · narroweremitted lightdistance on surface →物理 profile 的形状很重要:宽底色 + 窄尖峰共同构成柔软又有细节的皮肤
diffusion profile 描述一点入射光在不同距离、不同颜色上重新出射多少;红光通常比绿光和蓝光扩散得更远。

直接用真实的径向 profile 做二维卷积很贵,而且 profile 通常不可分离。一个可实时的表达是 :先拟合目标曲线,再分别计算每个 Gaussian 的卷积纹理。

profile ≈ weighted sum of Gaussianstarget profileradial, not separableG1 · w1G2 · w2G3 · w3G4 · w4G5 · w5G6 · w6each Gaussian → separable blurΣ wi · Gi一次 2D 4096-tap blur → 两次 1D 64 + 64 taps,再复用六张中间纹理
把径向 profile 拆成多个 Gaussian 后,每个 Gaussian 都能做可分离 U/V 卷积,最后按同样的权重线性组合。

六个 Gaussian 不是“六次随便 blur”。每个核都有自己的方差和颜色权重;最窄的核可以近似不模糊的 irradiance,其他核逐渐扩大。最后的线性组合必须与拟合 profile 时使用的权重相同,否则只会得到一团好看的、但物理形状不对的红雾。

4. 纹理空间扩散:把 3D 邻域变成两次 1D blur

因为皮肤散射是局部的,可以把模型用 UV 展平后直接在二维纹理上处理。每帧先把网格以 UV 作为 render coordinate,得到不含镜面项的 irradiance texture;然后为每个 Gaussian 执行 U 方向和 V 方向的可分离卷积,保留最终的多张中间纹理;最后重新以 3D 坐标渲染网格,读取并线性组合这些纹理,再加回 specular。

texture-space diffusion:unwrap once, blur many, shade once3D meshUV as render coordsirradianceincoming lightUVblur passescombine in final 3D passworld-space distance ≠ UV distance → stretch map adjusts tap spacing in U and V纹理空间让卷积便宜,但 seam、拉伸和重叠 UV 成为新的正确性边界
纹理空间扩散把 3D 邻域问题变成 2D 纹理处理:UV 展开、irradiance、U/V 可分离卷积、最终 3D 合成。

一次直接二维卷积如果半径是 64 texels,需要约 4096 次采样;可分离卷积只需约 64 + 64 次。这样做的前提是 UV 距离足够接近表面距离。曲面、拉伸和 seam 会破坏这个前提,所以还需要下一节的校正。

// U/V 两次一维卷积;每个 Gaussian 都保留自己的结果纹理
vec3 blurU(sampler2D irradiance, vec2 uv, vec2 stretch, float radius) {
  vec3 sum = vec3(0.0);
  for (int tap = -TAPS; tap <= TAPS; ++tap) {
    float offset = float(tap) * radius * stretch.x / float(TAPS);
    sum += texture(irradiance, uv + vec2(offset, 0.0)).rgb * gaussianWeight(tap);
  }
  return sum;
}
// V pass 读取 blurU 的临时纹理,再写出当前 Gaussian 的最终纹理。

5. stretch correction 与薄区域透射

用 fragment shader 对 world-space position 求屏幕导数,可以快速估计 U 和 V 方向的局部伸缩,把它们的倒数写入 stretch texture。卷积时读取对应值,分别调节 U/V 的 tap 间隔。高频 stretch 在大半径卷积中仍可能不稳定,因此复杂表面可以进一步做多尺度 stretch map。

耳朵、鼻翼边缘等薄区域还需要 :它不是普通阴影图,而是沿光线计算穿过表面的厚度,并把背光位置连接到光照侧已经卷积好的 irradiance texture。

thin region transmission:耳朵不是普通 diffuselight-facing sidesurface depthtranslucent shadow mapthickness through skinconnect to lit UVreuse blurred irradiancewarmglowsame convolution textures can serve local scattering and thin-surface transmission
薄区域透射不是把背面直接加亮:translucent shadow map 提供穿透厚度,并连接到光照侧可复用的散射纹理。

透明透射路径会增加 shadow map 与额外查找,但复用了同一套 Gaussian 卷积纹理。这样耳朵可以呈现暖色透光,同时脸部大多数区域继续走局部散射的便宜路径。

6. 三步把物理模型落到实时 GPU 管线

分步1 / 3

先拆分表面反射与组织散射

先用物理合理的 specular BRDF 得到表面反射,再生成不含 specular 的 irradiance texture。不要把镜面高光一起拿去 blur,否则高光会被扩散成错误的宽亮斑。

skin = surface reflectance + subsurface transport + transmissionsurfacespecular BRDFgrazing highlightsepidermis / dermislight exits nearbysoft glow + local detail只做 diffuse 会“干硬”;只做 blur 又会丢掉表面高光与薄耳朵透光
真实皮肤不是一层漫反射颜色:表面高光、组织内的散射吸收和薄区域透射共同决定柔软外观。
specular response:grazing angle 是人眼敏感区reflected intensitygrazing angle →physical BRDFPhong镜面不是独立“加一层”:反射掉的能量会减少可供次表面散射的入射光
Phong 能快速给出高光,但在掠射角和能量守恒上不够可信;更物理的 BRDF 只增加少量 shader 工作就能改善脸部反射。

Real-Time Skin Rendering Lab

猜一猜:把 Gaussian kernels 调多会带来多少柔软度,又会付出多少带宽?打开薄区域透射后,画质提升来自哪条额外路径?

physical profile · six-blurs · 6 kernels · 1024pxincident light → RGB diffusion halo → translucent appearanceprofile quality88%softness82%seam risk58%thin transmission 86%bandwidth 46 unitsnot ground truth趋势示意:用真实 GPU profile 决定最终预算

多张 Gaussian 纹理按物理 profile 组合,画质更稳但要保留中间 buffer。 薄区域复用透射厚度,耳朵等部位更可信。

本章小结

  • 皮肤外观由表面高光、次表面散射和薄区域透射共同决定
  • specular BRDF 改善掠射角高光,并约束可分配给散射的能量
  • diffusion profile 用 RGB 曲线描述光在组织内的空间传播
  • sum-of-Gaussians 把昂贵二维卷积拆成多组可分离 U/V blur
  • stretch correction 与 translucent shadow map 处理 UV 拉伸和耳朵透光

练习

问题 1|修改 Demo 代码。 现在的最终皮肤颜色是 final = scattered + specular。请改成先让镜面反射消耗一部分入射能量,再把剩余能量送入散射,并说明为什么不能只用固定的 Kd

vec3 specular = evalSkinSpecular(N, L, V, roughness);
vec3 scattered = combineGaussianTextures(uv, profileWeights);
vec3 finalColor = scattered + specular;

问题 2|诊断 UV 伪影。 一张均匀方格贴图在平面上经过 blur 正常,但映射到鼻翼后,散射半径明显变宽且出现条带。你会检查哪张辅助纹理?它应由什么数据估计?

问题 3|场景选型。 远处 NPC、近景脸部和逆光耳朵需要三种不同预算。你会怎样选择 Gaussian 数量、纹理分辨率和透射路径?

名词解释

本章出现的专业名词,用大白话再讲一遍。

subsurface scattering
specular BRDF
diffusion profile
sum-of-Gaussians
texture-space diffusion
stretch correction
translucent shadow map

讨论

评论区加载中…