新闻详情

新闻详情

首页 / 资讯中心 / 详情

Unity Shader实现Fast SSS:快速次表面散射的移动端近似方案

发布时间:2026/9/30 18:35:00来源:尧图网络
Unity Shader实现Fast SSS:快速次表面散射的移动端近似方案
先说明一点标题里的“快速”加引号我觉得特别贴切。真实的次表面散射Subsurface ScatteringSSS在渲染里是个很重的效果尤其是皮肤、蜡烛、树叶这类半透明材质光线进入表面内部再散射出来那种通透感是普通漫反射贴图很难模拟的。做全屏散射成本太高移动端基本不用想。所以这个Fast SSS的思路本质是找一个视觉上够用的近似方案在性能和效果之间取一个平衡。这篇文章我会把实现思路、核心参数、完整Shader代码和踩过的坑都拆开讲适合已经在用Unity Shader写基础光照、但对SSS只有概念没动过手的同学。1. 内容整体设计与思路拆解1.1 为什么需要“快速”次散射方案先聊聊为什么我们不能直接上PBR那套完整的次表面散射模型。真正的SSS需要计算光线在材质内部多次散射后的出射分布业界常见做法是屏幕空间的SSSSSScreen-Space Subsurface Scattering先渲染出带光照的场景再在屏幕空间做高斯模糊来模拟光在皮肤下的扩散。效果确实好但它的开销在于需要单独渲染一张屏幕空间的散射图分辨率跟屏幕一致要做两次甚至多次高斯模糊采样次数多内存带宽、GPU占用都往上走。这个成本放在PC端可以接受但放到移动端或者需要大量角色同屏的场景比如手游里的NPC、怪物群帧率压力就非常明显。我做过的项目里一个场景20个角色用全屏SSSSS帧率直接从60掉到40优化到后面只能砍角色数量项目策划那边天天催说真的挺头疼。Fast SSS的思路完全不一样。它不是物理正确的散射模拟而是用光照模型里的一个近似技巧来“骗”过眼睛——光泽度、半角偏移、阴影渐变一起配合让物体看起来像是光线在表面内部透射出来。它不是真实计算散射路径而是伪造出那种光晕、透光、过渡柔和的效果。只要参数调得准在90%的日常渲染场景里肉眼几乎分不出和全屏SSS的差别。1.2 方案选型为什么不用次表面轮廓透射Subsurface Translucency做SSS的时候很多人第一反应是去查“透射”相关方案比如透射贴图、次表面透射材质或者看Unity的HDRP里那个Subsurface Scattering材质。但我个人建议如果只是做“角色皮肤看起来有血色”“树叶透光”“蜡烛耳廓发亮”这种程度的视觉效果不要直接上完整的透射方案。原因有两方面第一完整透射方案需要额外的透射贴图或者厚度贴图来驱动涉及美术制作成本。你要让TA去出图、调参数不是每个项目都有这个时间。第二现有的一些透射效果在移动端做了很多近似但本质上还是需要额外的采样和计算。而Fast SSS只需要在现有的漫反射光照模型里改几行代码额外开销几乎可以忽略。我经常跟别人打比方完整SSS是拍电影需要好的演员、灯光、后期Fast SSS是舞台剧灯光道具用得好效果也能很沉浸。关键是看你的舞台有多大——对大多数项目来说Fast SSS绝对够用。1.3 技术原理在BRDF层面“作弊”Fast SSS最常见的一种实现方式是基于“Wrap Lighting”的变体。正常情况下漫反射光照用NdotL法线方向点乘光照方向来计算当NdotL小于0时物体直接进入阴影边缘特别生硬。而Wrap Lighting把NdotL做了一个偏移Wrap处理[ NdotL_{wrap} max(0, \frac{NdotL wrap}{1 wrap}) ]这里的wrap是一个0到1之间的参数。当wrap0.2时原本NdotL从1到-1的范围被压缩到1到-0.2也就是说光照不会在法线半球的边界处直接消失而是往后“包裹”了一小段距离边缘会有渐变而不是一刀切。利用这个Wrap Lighting之后再用一个幂运算比如pow(light, scatteringPower)来控制散射的衰减速度就能模拟出光在材质内部扩散、逸出的感觉。再配合视角方向、阴影信息可以让遮蔽区域里面透出一点被光“穿透”的颜色效果会更接近真实的次表面散射。2. 核心细节解析与实操要点2.1 最重要的三个参数Wrap值、散射强度和散射颜色先明确一点Fast SSS并不复杂但能不能调出好看的效果全看参数理解。第一个参数是Wrap值它决定了光“包裹”的宽度。Wrap值越大整个物体受光的范围就越大阴影区域越少但光感会越“平”。我个人建议数值范围控制在0.2到0.4。小于0.2效果跟普通漫反射几乎没区别大于0.4整个模型容易变成灰蒙蒙的一片丢失立体感。第二个是散射强度这个参数在代码里表现为幂运算的指数数值我习惯叫它scatterPower。它控制的是光衰减到0的速度。数值在2到8之间数值越高边缘过渡越锐利光晕越集中在明暗交界处数值越低散射范围越大但整体会偏灰。第三个是散射颜色这很好理解。皮肤通常是偏红色因为光线穿透表皮和真皮时红光波长最长穿透力最强所以你会看到皮肤在背光边缘透出红色。树叶是偏黄绿色蜡烛耳朵是亮黄色具体要看你做的是什么材质。2.2 处理实时阴影与SSS的互动这个点特别容易踩坑当你给角色开了阴影用Fast SSS之后阴影区域和光照过渡区域会出现很明显的断层。阴影内部是纯黑阴影边缘却透出光晕看起来特别假。原因在于直接光照经过Wrap Lighting之后原本的明暗分界线变成了渐变过渡但阴影贴图Shadow Map里的采样结果仍然是二值的——在阴影里就是0在阴影外就是1光线从0到1的突变让Wrap的效果全部失效。解决办法有几种为了“快速”我推荐给阴影采样加一层一定范围的渐变软化也就是对ShadowCoord做一次平滑float shadow saturate(shadowMap * fresnel (1 - fresnel) * shadowSoftness);这个思路是阴影边缘根据视角方向上菲涅尔效应的强度做插值让阴影边缘有一定的柔和度。不要直接在阴影里用硬切光否则Fast SSS的效果会大打折扣。2.3 透光效果增强厚度估算的简单做法真实SSS里光线能不能穿透材质取决于厚度。耳朵、手掌、叶片边缘比较薄所以透光更强。做Fast SSS时很多人会忽略这一点整个模型用统一参数结果就是所有部位出现同样的光晕看起来不像皮肤像是塑料上了层油。这里有一个很方便的近似做法用模型背面的法线方向来模拟厚度就像正常渲染时背面法线朝里可以把背面法线转换到世界空间再取负。用当前片元的法线和光线方向的夹角来判断float thickness saturate(dot(-lightDir, normalWS) * thicknessScale thicknessBias);当这个数值越接近0说明光线方向跟法线垂直通常意味着材质边缘越接近1说明法线正对光源基本上就是受光最强的地方。用这个thickness值去控制散射强度可以让边缘透光更明显中间区域保持常规漫反射。如果你有次表面散射贴图或者AO贴图也可以用它来替代这个近似厚度值效果会更可控。2.4 纹理采样的选择与注意事项在实际项目中Fast SSS的材质大概率会用到主纹理、法线贴图、高光贴图等。在做SSS效果时有一个常被忽略的点主纹理的Alpha通道是否要参与计算。很多人把Alpha通道留给了平滑度或者金属度但当启用了_ALPHABLEND_ON做半透明透光时Alpha会和SSS的厚度估算冲突导致边缘出现多余的透明或发亮。我建议做皮肤、叶子这类材质时不要开半透明混合用“Alpha Test”或者直接不透明。用透贴也可以但阈值要调得比较高。总之别让透明混合的Alpha值波动牵扯到SSS的厚度计算否则调试时期你会非常崩溃。3. 实操过程与核心环节实现3.1 环境准备与Shader结构拆解我这边用的环境是Unity 2021.3 LTS内置渲染管线URP下也是一样可以改后面会提。先建一个Unlit Shader然后自己把光照模型加上去这样最干净不会受Unity自带Standard的干扰。Shader的整体结构分几个部分Properties定义主纹理、法线贴图、光照参数Wrap值、散射强度、散射颜色等SubShaderPass里定义Tags比如LightModeForwardBase顶点/片元着色器实现Fast SSS光照计算处理阴影、视角等。3.2 完整Shader代码与逐段解析下面这个是我在项目里实际用过的精简版本默认走Forward渲染路径单盏平行光支持阴影。想接URP的话把UnityObjectToClipPos换成TransformObjectToHClipUnityWorldSpaceLightDir换成GetMainLight系列函数即可。Shader Custom/FastSSS { Properties { _MainTex (Albedo (RGB), 2D) white {} _NormalMap (Normal Map, 2D) bump {} _SSSColor (Scatter Color, Color) (0.8, 0.2, 0.2, 1.0) _WrapValue (Wrap Amount, Range(0.1, 0.9)) 0.3 _ScatterPower (Scatter Power, Range(1.0, 12.0)) 4.0 _ShadowSoftness (Shadow Softness, Range(0.0, 1.0)) 0.3 _ThicknessScale (Thickness Scale, Range(0.0, 1.0)) 0.5 } SubShader { Tags { RenderTypeOpaque LightModeForwardBase } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase #include UnityCG.cginc #include AutoLight.cginc sampler2D _MainTex; float4 _MainTex_ST; sampler2D _NormalMap; float4 _SSSColor; float _WrapValue; float _ScatterPower; float _ShadowSoftness; float _ThicknessScale; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 normalWS : TEXCOORD1; float3 posWS : TEXCOORD2; SHADOW_COORDS(3) }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.normalWS UnityObjectToWorldNormal(v.normal); o.posWS mul(unity_ObjectToWorld, v.vertex).xyz; TRANSFER_SHADOW(o) return o; } fixed4 frag (v2f i) : SV_Target { float3 lightDir normalize(_WorldSpaceLightPos0.xyz); float3 viewDir normalize(_WorldSpaceCameraPos.xyz - i.posWS); float3 normalWS normalize(i.normalWS); // Fast SSS diffuse term float ndl dot(normalWS, lightDir); float wrap (ndl _WrapValue) / (1 _WrapValue); float scatter pow(saturate(wrap), _ScatterPower); float sssIntensity saturate(1 - saturate(dot(-lightDir, normalWS)) * 0.5 0.5); // Thickness approximation float thickness saturate(dot(-lightDir, normalWS) * _ThicknessScale 0.5); // Shadow with soft edge float shadow SHADOW_ATTENUATION(i); shadow saturate(shadow (1 - shadow) * _ShadowSoftness); // Combine float3 albedo tex2D(_MainTex, i.uv).rgb; float3 scattering _SSSColor.rgb * scatter * thickness * shadow; float3 diffuse albedo * max(0.1, wrap) * shadow; float3 finalColor diffuse scattering; return float4(finalColor, 1.0); } ENDCG } } }先解释一下几个关键计算公式为什么这么写。float wrap (ndl _WrapValue) / (1 _WrapValue);这是前面提到的Wrap Lighting标准公式。当ndl-0.3、_WrapValue0.3时分子是0分母是1.3wrap0刚好是不会亮的位置。这个偏移量让阴影区域向光源方向“让”了一点距离过渡更软。float scatter pow(saturate(wrap), _ScatterPower);为什么散射要用幂运算因为我们希望光在材质内部扩散时靠近直接光照区域的部分衰减慢一点远离的部分衰减快一点。pow函数天然能做这个非线性映射。_ScatterPower4时wrap0.5时scatter0.0625衰减极快wrap0.9时scatter0.6561偏亮但离暗部远。如果你的模型边缘想要更透可以把_ScatterPower降到2左右。关于thickness的近似我的写法是float thickness saturate(dot(-lightDir, normalWS) * _ThicknessScale 0.5);当光线方向和法线方向正好垂直比如模型边缘时dot结果为0加上0.5就是0.5不算特别薄。如果想强调边缘透光可以把Bias调小一点让整体数值往0偏散射更强。3.3 在Unity里的接入步骤与效果调试跟我之前项目里的操作一样按下面几步来就可以复现在Project窗口右键 - Create - Shader - Unlit Shader把上面代码粘贴进去保存。右键 - Create - Material把Shader换成Custom/FastSSS。给场景里建一个默认的Unity球体Sphere把材质拖上去。场景里建一盏平行光开启阴影Shadow Type选Soft Shadows。运行调整Inspector面板上的_WrapValue、_ScatterPower和_SSSColor。调试的时候我有几个习惯先用纯色贴图调参数不要一上来就用带纹理的皮肤否则会分不清是纹理明暗还是SSS光晕。把_ScatterPower从低到高推观察明暗交界处的过渡带宽度变化。打开Scene视图的线框模式观察边缘处透光颜色是否均匀如果出现局部过曝多半是thickness计算需要调_ThicknessScale。3.4 URP下的迁移要点如果你项目用的是URP别慌改动量不大。把Shader里的光照相关API换成URP版本就行。核心改动UnityObjectToClipPos换成TransformObjectToHClip平行光方向用GetMainLight()拿注意里面有light.shadowAttenuation阴影接收用TransformWorldToShadowCoord配合MainLightRealtimeShadow。URP里做SSS有个好处可以借助Scriptable Render Pipeline的渲染特征做更多屏幕空间的微调但Fast SSS作为代码量极小的光照模型依旧很实用。4. 常见问题与排查技巧实录4.1 突然全黑/全白多半是方向向量没有归一化新手特别容易犯的一个问题是在片元着色器里对lightDir、viewDir、normalWS这些向量做运算时忘了归一化结果dot值超出[-1,1]范围pow函数一出问题画面要么过曝要么全黑。我之前调一个角色皮肤效果时就遇到过把法线贴图采样后直接放入计算没做UnpackNormal结果整个模型全是噪点运行起来像黑白电视机雪花。排查半天发现是法线贴图没有正确解包。解决办法很简单用了法线贴图就用UnpackNormal(tex2D(_NormalMap, i.uv))所有向量参与点乘之前确保normalize。4.2 阴影边缘生硬像贴了块创可贴这个前面提过因为阴影贴图是二值的。Fast SSS的Wrap Lighting虽然把明暗过渡做了软化但阴影采样仍然是硬切。如果你用了上面的代码把_ShadowSoftness调到0.5以上应该会好转。如果调到0.8还是生硬可以考虑对Shadow Coord采样做两次或三次PCF软阴影。不过这是额外开销移动端慎用。更好用的方式是用屏幕空间软阴影或者用VSMVariance Shadow Map替代硬阴影。4.3 散射光晕出现在不透明物体内部边缘没效果要排查的是你的法线是否正确。如果用内置球体测试没问题但用某些模型时法线可能是软边Smoothing Groups边缘过渡特别多导致dot(-lightDir, normalWS)的值在边缘和中间没太大区别看起来哪哪都亮。建议检查Mesh导入设置里的Normals和Tangents确认是Import或者Calculate如果是从别的软件导出的有时候法线会丢失。把Normals设为CalculateTangents设为Calculate Tangent Space可以解决大部分异常。4.4 移动端性能评估能省就省这个Shader在移动端的开销其实非常低几乎可以忽略不计。主要消耗是阴影采样、Pow函数和纹理采样各一次。但如果要批量渲染大量角色建议把_ScatterPower、_WrapValue这些参数合并进一个Vector4里减少uniform参数数量更有利于合批。如果你需要在移动端跑大世界记住一个原则给SSS材质单独建一个Shader变体不要跟完整PBR材质混在一起。不然一个渲染队列里既有的材质走复杂PBR、有的走Fast SSS合批率会非常难看。4.5 常见问题速查表问题表现可能原因解决办法模型全白/全灰法线未归一化、Wrap值过大检查向量归一化_WrapValue控制在0.4以内边缘发黑散射强度太低、厚度Bias太高降低_ScatterPower或增大_SSSColor强度阴影边缘断层ShadowMap硬采样加上阴影软化参数或换软阴影光晕出现在内部法线/厚度计算异常检查法线导入设置调整_ThicknessScale移动端帧率骤降阴影采样次数过高降低阴影质量减少PCF次数散射颜色过脏散射颜色和主纹理混合过度降低_SSSColor的Alpha系数或把光照强度和散射分离5. 实际项目中Fast SSS的扩展用法5.1 角色皮肤血色透光的正确打开方式做角色皮肤时_SSSColor建议调成(0.8, 0.2, 0.15)这种偏向深红的颜色_WrapValue在0.25到0.35之间_ScatterPower在3到5之间效果最好。另外一个技巧是给散射强度乘一个菲涅尔项Fresnel让面向视角的皮肤区域散射更强而边缘散射更弱这会更接近真实皮肤的在薄边缘和厚中心交替变化的感觉。5.2 树叶与植物材质植物的SSS关键在于“边缘透光”。树叶被阳光照射时从背面看边缘亮度极高叶脉透亮。_SSSColor用黄绿色_ScatterPower可以在5到8之间比较高让叶片的散射范围变窄集中在边缘。厚度估算可以配合一张叶脉贴图来用效果会特别细。5.3 蜡烛、玉石、冰面等半透明硬材质这类材质与皮肤的区别是散射范围小、衰减快且高光比较明显。建议_ScatterPower调到8以上_SSSColor选择跟材质主色调一致的颜色。比如蜡烛用淡黄色玉石用浅绿冰面用淡蓝。高光部分用Blinn-Phong或者GGX叠加一层会比纯Fast SSS更完整。5.4 与后处理的配合Bloom是神器Fast SSS如果配合Bloom后处理效果会上一个台阶。SSS在暗部透出的光晕经过Bloom后会有一种柔和自发光的效果特别适合做科幻风格的霓虹材质和能量护盾。我在一个项目里做过能量护盾直接给材质加了一个强度稍微高一点的SSS散射项配合Bloom护盾被击中时边缘亮光异常好看关键是代码量几乎没有增加。6. 与真实SSS方案的对比与选型建议方案性能开销效果真实度适用场景Fast SSS本文方案极低几乎无额外开销视觉近似缺乏真实透射路径移动端、大批量角色、实时交互屏幕空间SSSSS高需要多pass和模糊非常接近真实皮肤质感PC端、高保真角色展示、CG级效果光线步进SSS极高需要体积计算物理正确可用于电影级渲染离线渲染、高端Demo透射贴图深度偏移中低需要美术出图可控性强但制作成本高需要特写的角色/物体选型建议很简单如果你只需要视觉上“像”SSSFast SSS是首选别犹豫。如果你要做角色面部特写、皮肤质感成为核心卖点那可以考虑全屏SSSSS。一般项目里先用Fast SSS出问题了再升级性价比最高。7. 写在最后的实操总结文章写到这我不太想用那种“综上所述”的收尾方式就说一点个人体会。Fast SSS这个方案我从一开始做就被它“骗”过以为它只能做一个廉价的伪造直到有次在项目里用它做角色皮肤导演在屏幕上看了半天说了一句“这质感不错怎么做的”我才意识到它对大多数人来说已经足够真实了。它真正的价值在于用极低的成本让材质有了“生命感”而不是冷冰冰的塑料反射。要记住参数一定要自己亲手调过才能理解你看我再多的数值建议都不如自己在场景里把_WrapValue从0.1拖到0.9感受那一瞬间的光影变化来得实在。如果在调试过程中遇到什么问题欢迎在评论区一起交流。我见过有人用这个方案做怪物皮肤、做剧情动画里的光线角色、做科幻载具的能量管线每一个都很有巧思。剩下的就交给你手里的项目了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

UltraEdit 安装时请注意:环境变量与 TaoToken 配置避坑指南 2026/9/30 19:33:06

UltraEdit 安装时请注意:环境变量与 TaoToken 配置避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
2025年人工智能十大趋势 - 智能行动者的崛起:TaoToken 统一 Key 接入 AI Agent 实战 2026/9/30 19:33:06

2025年人工智能十大趋势 - 智能行动者的崛起:TaoToken 统一 Key 接入 AI Agent 实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
芯片焊接工艺包选型:订单碎片化后要盯住的4个硬指标 2026/9/30 19:33:06

芯片焊接工艺包选型:订单碎片化后要盯住的4个硬指标

跟一位做功率器件的朋友吃饭,他吐槽最近半年的采购清单:芯片焊接工艺包从过去一年谈一次,变成一个季度来三单,每一单的焊料类型、焊接方式、装片数量都不一样,数量从八千颗一路砍到六百颗。他问我,这种碎单…

阅读更多 →
一条停水通知花了几小时,TaoToken 能不能把它压缩到几秒? 2026/9/30 19:32:59

一条停水通知花了几小时,TaoToken 能不能把它压缩到几秒?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
初识MCP(Model Context Protocol):从零搭建一个可用的MCP Server 2026/9/30 19:32:59

初识MCP(Model Context Protocol):从零搭建一个可用的MCP Server

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
【Bug已解决】npm error code ENOTEMPTY: directory not empty, rename claude-code 解决方案:用 TaoToken 统一 Key 通道修 2026/9/30 19:32:53

【Bug已解决】npm error code ENOTEMPTY: directory not empty, rename claude-code 解决方案:用 TaoToken 统一 Key 通道修

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉