移动端实时皮肤渲染:Pre-integrated SSS与曲率贴图实战指南

发布时间:2026/7/13 2:32:29
移动端实时皮肤渲染:Pre-integrated SSS与曲率贴图实战指南 1. 项目概述从“蜡像”到“活人”的质感跨越在角色渲染尤其是移动端游戏的角色渲染中皮肤质感一直是个老大难问题。你辛辛苦苦建好了高模画好了法线贴图、高光贴图结果角色在游戏里看起来还是像个精致的蜡像或者塑料娃娃缺乏那种“活生生”的感觉。问题的核心就在于皮肤是一种半透明材质光线并非在表面戛然而止而是会渗入皮肤下层经过散射后再部分穿出。这种光学现象就是次表面散射。它带来了皮肤特有的红润感、透光感比如在强背光下耳朵边缘透出的红光以及柔和的光影过渡。直接模拟真实的物理散射计算量巨大在实时渲染特别是移动平台上根本行不通。于是各路图形程序员们发明了各种“作弊”方案来近似这种效果。今天要聊的这套“Pre-integrated SSS 曲率贴图”组合拳就是其中在效果、性能和实现复杂度上取得绝佳平衡的一种实战方案。它不是最物理正确的但很可能是目前移动端或对性能有严格限制的实时渲染项目中实现高质量皮肤透光感最具性价比的选择。简单来说Pre-integrated SSS负责模拟光线在平坦曲面上的散射和颜色渗透而曲率贴图则负责告诉着色器“哪里是鼻尖、耳廓这种高曲率薄的区域光线更容易透过来”。两者结合就能用相对低廉的代价让角色的皮肤瞬间“活”起来。2. 核心原理拆解为什么是这两个技术在深入代码之前我们必须先搞清楚这两个技术各自解决了什么问题以及它们为何能珠联璧合。知其然更要知其所以然这样在调参和排查问题时你才能心中有数。2.1 Pre-integrated SSS把复杂积分“提前算好”次表面散射的完整物理模型非常复杂涉及一个在半球空间上的体积散射积分。实时计算这个积分是天方夜谭。Pre-integrated SSS 的核心思想极其聪明既然这个积分的结果主要取决于表面法线与光照方向的夹角以及材质的散射属性那我们能不能提前把所有可能角度的结果都算出来存成一张查找表这就是“Pre-integrated”预积分的含义。我们假设表面是局部平坦的这是关键近似那么对于给定的材质散射参数可以理解为“厚度”或“散射强度”散射后的颜色只取决于dot(N, L)即法线N和光照方向L的点积。我们预先计算好一张一维纹理通常是一张细长的2D贴图横轴是dot(N, L)从-1到1纵轴是预设的散射强度参数在着色器中我们只需要根据实际的dot(N, L)和美术设定的散射强度去这张纹理里采样就能立刻得到一个经过散射“模糊”和“染色”后的光照结果。它具体做了什么模糊阴影边界在传统的Lambert漫反射中dot(N, L)小于0的区域是完全黑暗的。但皮肤在明暗交界处会有柔和的过渡因为暗部的光线可以从旁边的亮部散射过来。预积分贴图通过在dot(N, L)为负值时仍然提供非零的亮度来模拟这种效果。渗透颜色皮肤下的血液血红素会吸收大部分蓝绿光透过红光。因此散射光会带有更多的红色/橙色成分。预积分贴图在生成时就可以将这种波长相关的吸收考虑进去使得暗部和散射区域呈现出我们想要的“红润感”。注意Pre-integrated SSS 的这个“平坦假设”既是它的优点计算简单也是它的缺点。它无法处理曲率变化带来的厚度差异。例如同样角度的光照打在鼻尖曲率高实际厚度薄和打在脸颊曲率低实际厚度厚上透光程度应该完全不同。这正是需要曲率贴图来弥补的地方。2.2 曲率贴图告诉着色器“哪里薄哪里厚”曲率贴图是一张存储了模型表面曲率信息的贴图。通常我们存储的是平均曲率或者高斯曲率的一个映射。在着色中我们更常用一种简化但非常有效的生成方式将顶点法线N与面法线N_face或相邻顶点的平均法线的差异烘焙成一张贴图。原理是这样的在一个平面上所有顶点的法线方向是一致的。但在一个弯曲的表面上比如球面顶点法线会从中心向四周发散。顶点法线与面法线或局部平均法线的夹角越大说明该点的曲率越高。对于皮肤渲染高曲率区域通常意味着该处模型更“薄”如鼻翼、耳廓、眼皮光线更容易穿透低曲率区域则更“厚”如脸颊中央、额头光线更难穿透。在着色器中我们从曲率贴图中采样得到一个值比如0到10表示平坦/厚1表示弯曲/薄。这个值将作为一个权重去影响Pre-integrated SSS的强度。在薄的、高曲率的区域我们增强散射效果让透光感更明显在厚的、平坦的区域我们减弱散射效果使其更接近普通的漫反射。生成曲率贴图通常有两种主流方法烘焙法在建模软件如Maya, 3ds Max, Blender或专门的烘焙工具如xNormal, Knald中利用“曲率”或“环境光遮蔽AO”的烘焙选项配合调整搜索距离等参数生成一张灰度图。这种方法质量高可控性强是项目中的首选。屏幕空间计算法在着色器中通过当前像素的深度信息和其周围像素的深度实时估算曲率。这种方法动态、无需额外贴图但对深度图精度和滤波非常敏感容易产生噪声在移动端性能开销也较大通常作为备选或补充方案。3. 完整实现流程与Shader剖析理解了原理我们开始动手实现。这里以Unity URPUniversal Render Pipeline为例因为它是目前Unity跨平台项目的标准选择。整个流程分为美术资源准备和Shader编写两部分。3.1 美术资源准备烘焙是关键1. 模型与UV确保你的角色模型拥有良好的拓扑和舒展的UV。UV不要有过度拉伸因为我们需要烘焙高质量的贴图。通常我们会为皮肤材质准备一套专门的UV通道如UV1用于烘焙曲率贴图以避免和颜色贴图UV0的拉伸冲突。2. 烘焙曲率贴图以Blender xNormal组合为例这是很多TA青睐的流程将你的高模和低模分别导入xNormal。在“Baking options”中选择“Normal map”模式但我们要的是曲率。更常用的方法是烘焙一张“Ambient Occlusion”图但将“Search distance”设置得非常小例如0.1-0.5个模型单位并勾选“Ignore backface”。这样烘焙出来的AO图实际上反映了模型表面微观凹陷的信息这些凹陷往往与曲率变化相关可以作为曲率贴图的优秀替代品。这张图我们称之为“Cavity Map”或“Curvature Map”。将烘焙出的灰度图导入PS。我们需要的是“凸起”的区域如鼻尖、眉骨更白“凹陷”或“平坦”的区域更黑。通常需要用一个“曲线”或“色阶”工具进行调整强化对比度让薄区域的亮度更突出。有时还会结合“高反差保留”滤镜来提取边缘信息。最终得到的是一张RGB或单通道灰度图白色代表高曲率/薄区域黑色代表低曲率/厚区域。3. 准备Pre-integrated SSS Lut贴图这是一张标准化的贴图通常不需要项目单独生成可以直接使用社区广泛流传的版本。你可以在网上搜索“Pre-integrated Skin Lut”找到。它通常是一张512x32或256x16的贴图横轴是N·L纵轴是散射强度Curvature或Scale。在Shader中我们将其作为2D LUT进行采样。3.2 Shader编写URP Lit Shader Graph 与 HLSL 两种路径你可以选择使用Shader Graph可视化编辑也可以直接编写HLSL代码。前者更直观适合快速原型和TA-程序协作后者更灵活性能控制更精细。这里我给出HLSL代码的核心思路因为理解了代码再用Shader Graph连节点就轻而易举了。核心思路在URP的PBR光照模型基础上修改其漫反射部分。我们通常不直接替换整个光照模型而是将标准Lit Shader中的“漫反射颜色”替换为我们通过Pre-integrated SSS计算出的颜色。关键步骤输入与定义// 在Properties中定义 _CurvatureMap(Curvature Map, 2D) white {} _SSSLUT(SSS Lut, 2D) white {} _SSSColor(SSS Color, Color) (1, 0.5, 0.3, 1) // 典型的皮肤散射颜色偏红橙 _SSSIntensity(SSS Intensity, Range(0, 2)) 1.0 _CurvaturePower(Curvature Power, Range(0, 5)) 1.0 // 控制曲率影响强度计算曲率权重// 在片元着色器中 float2 curvatureUV input.uv1; // 使用第二套UV float curvature tex2D(_CurvatureMap, curvatureUV).r; // 采样曲率图 curvature pow(curvature, _CurvaturePower); // 使用幂运算调整对比度 // 将曲率值映射到最终的散射强度范围例如从0.5到1.5薄的地方强度高 float sssStrength lerp(0.5, 1.5, curvature) * _SSSIntensity;采样Pre-integrated LUT LUT的UV坐标需要精心构造。U坐标是dot(N, L)从[-1, 1]映射到[0, 1]。V坐标是我们的散射强度sssStrength也需要映射到LUT的V轴范围通常是0-1对应LUT的纵轴。float NdotL dot(normalWS, lightDirectionWS); // 将NdotL从[-1, 1] 映射到 [0, 1] float lutU NdotL * 0.5 0.5; // 假设LUT的纵轴代表散射强度从0到2我们需要将sssStrength映射过去 float lutV sssStrength * 0.5; // 如果sssStrength范围是0-2LUT纵轴范围是0-1 // 确保不采样到贴图边界外避免插值问题 lutU clamp(lutU, 0.01, 0.99); lutV clamp(lutV, 0.01, 0.99); float3 sssLut tex2D(_SSSLUT, float2(lutU, lutV)).rgb;应用散射颜色与混合 从LUT采样到的是一个亮度因子可能还包含一些颜色偏移。我们需要用它来调制基础颜色和光照颜色。// 基础漫反射颜色例如来自Albedo贴图 float3 baseColor tex2D(_BaseMap, input.uv).rgb * _BaseColor.rgb; // 主光源颜色URP中可通过GetMainLight获取 Light mainLight GetMainLight(); float3 lightColor mainLight.color; // 核心混合用LUT值作为乘数并结合SSS染色 float3 sssDiffuse baseColor * lightColor * sssLut * _SSSColor.rgb; // 你可以选择完全替换标准漫反射或者与标准漫反射如Lambert做线性混合 // 混合方案1完全替换 // float3 finalDiffuse sssDiffuse; // 混合方案2根据曲率或自定义权重混合更灵活 float blendFactor curvature; // 或用其他逻辑 float3 standardDiffuse baseColor * lightColor * saturate(NdotL); float3 finalDiffuse lerp(standardDiffuse, sssDiffuse, blendFactor);整合到URP光照函数 你需要将计算出的finalDiffuse替换掉URP内置光照函数如UniversalFragmentPBR中关于直接光漫贡献的部分。这通常需要你自定义一个Lighting函数或者修改SurfaceData。更实用的方法是直接复制URP的Lit Shader源码在BRDFData构建和光照计算环节插入你的修改。实操心得在Shader Graph中实现时关键节点是“Sample Texture 2D”采样LUT而UV的构造使用“Append”节点和“Fraction”节点确保范围正确。混合部分使用“Lerp”节点混合因子可以来自曲率图采样值。一个常见的技巧是将Pre-integrated SSS的效果主要应用于直接光的漫反射部分而让间接光如环境光、IBL保持标准PBR计算这样既能保证皮肤透光感又不破坏整体环境的和谐。4. 参数调节艺术与性能优化实现只是第一步调出自然的效果才是真正的挑战。这套方案有几个核心参数如同音效的均衡器需要你耐心微调。4.1 核心参数调节指南_SSSColor次表面散射颜色作用决定透出光的颜色。对于人类皮肤通常是橙红色例如RGB: (1.0, 0.5, 0.3)。调节在背光或边缘光强烈的场景下观察。颜色太红像灼伤太黄像黄疸。可以准备一张真实皮肤的背光照片作为参考进行比对。_SSSIntensity次表面散射强度作用全局控制散射效果的强弱。调节从1.0开始观察角色在侧光下的明暗交界线。强度太低效果不明显像塑料强度太高整个模型会显得浮肿、缺乏立体感阴影区域会过亮。通常保持在0.8-1.2之间比较安全。_CurvaturePower曲率强度作用控制曲率贴图对比度的幂指数。增大此值会使曲率贴图中亮部更亮、暗部更暗从而让高曲率薄区域和低曲率厚区域的透光差异更明显。调节这是塑造局部细节的关键。调高它能让鼻尖、耳廓、指尖的透光感瞬间突出。但过高会导致效果生硬像在模型上画了白边。通常从1.0开始在1.5-2.5范围内微调找到既能突出特征又不突兀的平衡点。曲率贴图本身作用效果的“地图”。贴图的质量直接决定效果的精细度。调节在PS中反复调整曲率贴图的色阶和曲线。确保只有你希望透光的区域鼻尖、耳廓、嘴唇边缘、关节处是明亮的而大面积平坦区域脸颊、额头中心是深色的。避免出现噪点或非预期的亮斑。4.2 移动端性能优化实战在PC上跑得流畅不代表在手机上也能行。对于移动端我们必须锱铢必较。贴图压缩与尺寸曲率贴图这只是一张灰度图务必使用压缩率最高的格式。在Unity中对单通道贴图使用BC4/DXT5PC或ETC2/ASTC移动端的单通道R压缩格式。尺寸512x512通常足够对于极简模型甚至可以尝试256x256。Pre-integrated LUT这张贴图很小256x16且对精度要求不高。使用RGBA32未压缩格式即可内存占用极小。甚至可以将其烘焙到另一张贴图如自发光贴图的某个通道中节省一个纹理采样。Shader复杂度优化减少纹理采样确保曲率贴图和LUT只采样一次。如果BaseColor、Normal等贴图共用UV可以考虑合并采样。简化计算pow运算开销较大。如果_CurvaturePower是固定值如2.0可以考虑让美术在烘焙曲率贴图时直接使用平方后的结果这样在Shader中就可以省去pow操作直接用原始采样值。分支预测移动端GPU对Shader中的分支if/else很不友好。尽量避免基于NdotL正负进行复杂分支。我们的LUT查找方案本身已经避免了分支。变体剔除 如果你将SSS功能写成了Shader变体使用#pragma shader_feature务必在项目Graphics Settings中或通过自定义Shader打包脚本剔除那些不需要的变体例如非皮肤材质不需要SSS变体以减少包体和运行时内存。针对低端机的降级方案 为最低端的设备准备一个简化版。简化版可以完全去掉曲率贴图只使用一个全局的SSS强度。甚至不用LUT而是用一个简化的公式来近似例如float sss saturate(NdotL _SSSIntensity) / (1.0 _SSSIntensity);这个公式能在一定程度上柔化阴影虽然效果远不如LUT精确但开销极低。5. 常见问题排查与效果调试技巧即使按照步骤做了第一次实现时也难免遇到各种妖魔鬼怪。这里记录几个我踩过的坑和对应的解法。5.1 效果不明显或完全没效果检查纹理采样UV这是最常见的问题确认你的曲率贴图使用的UV通道input.uv1是否与模型导入设置和Shader属性中的UV索引匹配。在Unity编辑器中选中模型在Inspector的Model分页下查看UV通道数量。在Shader中使用TRANSFORM_TEX宏时指定正确的UV变量名和纹理属性名。检查LUT纹理的Wrap Mode必须设置为Clamp。如果设置为Repeat当lutU或lutV接近1时会采样到另一头的错误颜色导致诡异的光斑。检查光照方向确保你传递给NdotL计算的光照方向是正确的世界空间方向。在URP中使用GetMainLight().direction。在自定义渲染流程中务必进行正确的坐标系转换。检查参数范围_SSSIntensity和lutV的映射关系是否正确如果_SSSIntensity为1但你的LUT纵轴设计强度范围是0-2那么lutV应该是0.5。理解你使用的LUT贴图的纵轴含义至关重要。5.2 透光感太强模型显得浮肿降低_SSSIntensity这是最直接的调整。调整曲率贴图你的曲率贴图可能整体太亮了。在PS中压暗中间调让只有真正的边缘和凸起部分保持高亮。检查_SSSColor颜色太亮值太高也会导致整体泛白。尝试降低颜色的亮度或饱和度。混合方式不要完全用SSS结果替换标准漫反射。尝试使用lerp混合并且让混合权重基于NdotL在正面受光区域NdotL大更多使用标准漫反射在背光或侧面区域才加强SSS效果。这能更好地保持模型的体积感。5.3 透光区域出现不自然的硬边或斑块曲率贴图有瑕疵烘焙的曲率贴图可能存在接缝处不连续、或UV岛边缘有黑边/白边的问题。在烘焙软件中检查烘焙 cage 的大小和偏移是否合适。在PS中对烘焙出的贴图使用轻微的模糊滤镜或者用克隆图章工具修复明显的接缝。LUT分辨率过低或插值问题确保LUT贴图的导入设置中Filter Mode不是Point (no filter)建议使用Bilinear。如果LUT本身分辨率极低如16x16在NdotL变化剧烈的地方可能会出现色阶。换用256x16或更高分辨率的LUT。数值精度问题在计算lutU和lutV后进行轻微的钳制clamp(lutU, 0.01, 0.99)可以防止采样到纹理边缘可能出现的异常值。5.4 在不同光照环境下效果不稳定环境光的影响Pre-integrated SSS主要处理直接光。如果场景有很强的环境光如天空盒或间接光它们会“冲淡”SSS效果。考虑在计算环境光贡献时也加入一个基于曲率的弱化因子或者简单地将SSS效果乘以一个基于直接光强度的权重确保只在主光源照射方向效果明显。多光源处理对于额外的逐像素光point light, spot light你需要对每个光源重复SSS计算并叠加。这会增加开销。一个折中方案是只对主光源最亮的方向光进行完整的SSS计算对于其他附加光只使用简单的漫反射或一个强度很弱的通用SSS近似以平衡效果和性能。这套“Pre-integrated SSS 曲率贴图”的方案经过多个移动端项目的验证确实能在有限的预算内极大地提升皮肤质感。它不需要屏幕空间信息不依赖昂贵的体积计算所有的魔法都藏在那两张小小的贴图和几十行精妙的着色器代码里。记住渲染没有银弹最好的方案永远是适合你项目需求和目标硬件的方案。多参考、多实验、多对比你就能调出属于自己角色的那份“生命之光”。