别再只用厚度图了!用深度图实时计算SSS透射距离(含Shader代码)
深度图实时计算SSS透射距离:突破厚度贴图局限的实战方案
当光线穿透玉石、皮肤或蜡质材料时,那种温润的透光效果总能赋予数字资产以生命力。传统教程中预烘焙厚度贴图的方法虽简单直接,却让动态物体陷入"透光僵局"——变形角色的一颦一笑、风中摇曳的花瓣,都因静态厚度数据而失去光学真实性。本文将揭示一种基于深度映射的动态解决方案,通过实时计算光线穿透距离,让次表面散射(SSS)效果真正"活"起来。
1. 厚度贴图的先天局限与深度映射的破局思路
在常规厚度贴图方案中,美术师需要预先烘焙物体各部位的厚度信息到纹理中。这张贴图本质上是一张静态的"透光能力分布图",白色区域代表厚实难透光,黑色区域则薄如蝉翼。这种方法存在三个致命缺陷:
- 动态适应性缺失:任何顶点动画都会导致实际厚度与贴图数据不匹配
- 方向性失真:同一位置从不同角度照射时,光线实际穿透距离不同
- 存储成本:高质量厚度贴图需占用显存,且无法应对程序化生成模型
深度映射方案则另辟蹊径,其核心思想可概括为:
在光源视角生成深度图,渲染时通过视空间坐标转换,实时计算光线在介质中的传播距离
具体实现流程如下表所示:
| 步骤 | 技术手段 | 对应Shader阶段 |
|---|---|---|
| 深度图生成 | 以光源为摄像机渲染场景深度 | 单独渲染通道 |
| 距离计算 | 转换当前像素到光源空间,采样深度差值 | 片元着色器 |
| 吸收模拟 | 根据穿透距离应用指数衰减 | 光照计算阶段 |
// 核心距离计算代码示例 float4 lightSpacePos = mul(_LightMatrix, float4(worldPos, 1)); float depth = tex2Dproj(_LightDepthTex, lightSpacePos).r; float s = length(lightSpacePos.xyz) - depth; // 实际穿透距离2. 深度映射方案的完整实现路径
2.1 深度图生成与优化
不同于阴影映射需要深度比较,SSS深度图只需记录光源到物体表面的最小距离。建议使用R32_FLOAT格式存储原始深度值,避免归一化带来的精度损失。对于移动平台,可采用以下优化策略:
- 视锥裁剪:只渲染可能产生SSS效果的物体层级
- 分辨率分级:根据物体屏幕占比动态调整深度图尺寸
- Mipmap链:为远距离物体使用低分辨率采样
// Vulkan风格的深度图生成Shader layout(location = 0) out float depthOut; void main() { depthOut = gl_FragCoord.z; // 直接输出线性深度 }2.2 穿透距离的物理校正
原始方案中简单的深度差值(s = do - di)存在物理误差,需要引入两项关键修正:
- 法线补偿:当光线斜射入表面时,实际穿透路径长于表面间距
- 曲率因子:高曲率区域(如耳廓)需要增强透光效果
修正后的距离计算公式:
s_actual = (do - di) / max(0.3, dot(N, L))2.3 吸收模型的选择与实现
基于Beer-Lambert定律,透射光强随穿透距离呈指数衰减。建议使用可分段的衰减函数:
float3 ApplySSSAbsorption(float s, float3 albedo) { const float sigma_a = 0.5; // 吸收系数 float scale = exp(-s * sigma_a); // 保持最小亮度避免死黑 return lerp(albedo * 0.1, albedo, scale); }对于皮肤渲染,可引入色散效应——长波红光比短波蓝光穿透更深:
float3 chromaticAbsorption = float3( exp(-s * 0.3), // R exp(-s * 0.6), // G exp(-s * 0.9) // B );3. 动态SSS的进阶技巧
3.1 动画系统的无缝衔接
深度映射方案天然支持蒙皮动画和形变动画,但需注意:
- 每帧更新深度图:在Unity中通过CommandBuffer实现
- 顶点抖动处理:添加微小噪声避免深度值闪烁
- 布料模拟适配:根据拉伸程度动态调整吸收系数
// 动态吸收系数示例 float dynamicSigma = _BaseSigma * (1 + _StretchFactor * 0.5);3.2 凹面体的特殊处理方案
原始方法对凹陷区域(如口腔)会失效,可通过混合方案解决:
- 保留基础厚度贴图用于凹面区域
- 使用深度图主导凸面区域计算
- 通过曲率检测自动混合权重
float blendWeight = smoothstep(-0.2, 0.2, curvature); float s = lerp(thicknessMapValue, depthMapValue, blendWeight);3.3 性能与质量的平衡术
| 优化策略 | 质量影响 | 性能提升 |
|---|---|---|
| 半分辨率深度图 | 边缘轻微锯齿 | 30%帧率提升 |
| temporal重投影 | 运动时轻微滞后 | 减少50%深度图生成开销 |
| 距离渐减采样 | 远距离精度下降 | 节省20%带宽 |
4. 完整Shader实现与调试指南
4.1 Unity URP下的完整代码框架
Shader "Custom/AdvancedSSS" { Properties { _Albedo ("Base Color", 2D) = "white" {} _Sigma ("Absorption", Range(0,2)) = 0.8 _SSSPower ("Scatter Power", Range(1,5)) = 2 } SubShader { Pass { // 深度图生成Pass ... } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _LightDepthTex; float4x4 _LightMatrix; struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD0; float3 normal : NORMAL; }; v2f vert (appdata_base v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.normal = UnityObjectToWorldNormal(v.normal); return o; } float4 frag (v2f i) : SV_Target { float3 N = normalize(i.normal); float3 L = normalize(_WorldSpaceLightPos0.xyz); // 深度图采样 float4 lightSpacePos = mul(_LightMatrix, float4(i.worldPos, 1)); float depth = tex2Dproj(_LightDepthTex, lightSpacePos).r; float s = length(lightSpacePos.xyz) - depth; // 物理校正 s /= max(0.3, dot(N, L)); // 应用吸收 float3 sss = exp(-s * _Sigma); sss = pow(sss, _SSSPower); return float4(sss, 1); } ENDCG } } }4.2 常见问题排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 边缘黑线 | 深度图精度不足 | 启用PCF软阴影 |
| 透光不均匀 | 法线未归一化 | 检查normalize操作 |
| 动画闪烁 | 深度图更新延迟 | 确保PreRender回调 |
| 性能骤降 | 未启用视锥剔除 | 调整深度图渲染层级 |
4.3 美术调参黄金法则
- 玉石材质:σ=0.3~0.5,power=1.2,添加青色散射
- 皮肤:σ=0.7~1.0,power=2.5,红色通道额外+30%
- 植物叶片:σ=0.4~0.6,power=1.8,使用噪声扰动穿透距离
在最近的角色项目中,我们将这套方案应用于精灵耳朵的透光表现,通过动态调整σ值实现情绪变化时的血管显色效果——当角色激动时自动降低吸收系数,使耳朵透出更强烈的红光。这种基于物理的动态响应,是传统厚度贴图永远无法实现的魔法。
