尧图精选

多光源卡通渲染总翻车?从光照分层到Shader实现一次讲透

🕒 发布时间:2026/10/1 17:39:24 📁 来源:尧图网络
1. 多光源在卡通渲染里为什么总翻车做卡通渲染的朋友大概率都经历过这个场景场景里只放一盏平行光的时候角色脸上的明暗交界线干净利落色块分明看着特别舒服。可一旦场景里多塞了几盏灯——比如补个轮廓光、加个环境点光、再来个特效光——角色脸立刻变成一坨糊掉的灰要么就是明暗交界线碎成好几段要么就是高光到处乱窜整个画面瞬间从二次元跌回廉价3D。这个问题在卡通渲染里比写实渲染严重得多原因其实不复杂。写实渲染靠的是物理光照模型多光源叠加无非是能量相加画面变亮变暗都在预期内。但卡通渲染的核心是对光照结果做阶梯化、阈值化处理也就是把连续的光照强度映射成有限的几个色阶。这个映射过程是非线性的一旦多盏灯的光照结果先叠加再阶梯化叠加后的中间值就会落在阈值附近反复横跳色阶边界自然就碎了。我在做角色渲染的时候踩过最典型的一个坑主光加了一盏强度0.8的补光结果角色脸颊的阴影色阶直接消失整张脸变成一片死白。当时排查了半天以为是材质参数问题最后发现是补光的强度刚好把主光算出来的阴影区光照值顶过了阈值。这就是多光源卡通渲染最反直觉的地方——每盏灯单独看都没问题叠在一起就出事。这一期要聊的就是怎么把多光源这件事在卡通材质里做稳。核心思路不是去调每盏灯的强度凑效果而是从光照计算的结构层面重新设计哪些光参与色阶计算、哪些光只做颜色叠加、哪些光干脆走单独的通道。这套思路在 liltoon 这类成熟的卡通渲染方案里已经有比较完整的实现但很多人只是拿来用不清楚背后的分层逻辑一旦遇到自定义需求就抓瞎。下面我会把这套分层机制拆开讲清楚再给出可以直接落地的实现方案。2. 先搞清楚卡通材质的光照分层逻辑2.1 写实渲染的加法在卡通里为什么不成立写实渲染的光照模型无论是 Lambert 还是 PBR本质都是线性叠加最终颜色 环境光 主光贡献 补光贡献 ……。这个叠加是逐像素的每个光源独立计算贡献然后相加物理上说得通视觉上也符合直觉。卡通渲染要的不是物理正确而是视觉可控。我们希望主光决定明暗交界线的位置和形状补光只负责把暗部提亮一点点轮廓光只负责边缘那一圈亮边。这三件事在视觉上是分工明确的但在数学上如果全部塞进同一个光照累加器它们就会互相污染。举个具体的例子。假设主光算出来某个像素的 NdotL 是 0.3阈值设在 0.5那么这个像素应该落在阴影区。现在加一盏补光方向让它的 NdotL 是 0.4如果直接相加总光照变成 0.7超过阈值像素跳到亮部。结果就是补光把本该在阴影里的区域顶到了亮部明暗交界线被破坏。这里的关键认知是卡通渲染的色阶判断应该由主光单独决定其他光源不应该参与这个判断只能在这个判断结果之上做修饰。2.2 把光源按职责分成三类基于上面的分析我在实际项目里会把场景光源按职责分成三类每类走不同的处理通道光源类型参与色阶判断作用典型光源主光是决定明暗交界线、投影方向场景主平行光辅助光否提亮暗部、补充色彩补光、环境光、点光特效光否边缘光、高光、发光轮廓光、Rim、自发光主光只有一盏它单独跑一遍完整的卡通光照计算输出一个 0 到 1 的明暗系数。辅助光和特效光各自独立计算最后以不同的混合方式叠加到主光结果上。这样主光的色阶边界永远是干净的辅助光再怎么加也不会破坏交界线。这套分层在 liltoon 里的体现就是它的Lighting 设置里可以指定主光以及Receive Shadow和Backlight等通道的独立控制。很多人用 liltoon 觉得参数好多好乱其实就是没理解这些参数分别属于哪一层。2.3 色阶函数的选择直接决定多光源稳定性色阶函数也就是把连续光照映射到离散色阶的那个函数的选择对多光源稳定性影响极大。常见的几种硬阈值Stepstep(threshold, NdotL)只有 0 和 1 两个值。交界线最锐利但对光照变化最敏感多光源下最容易碎。软阈值Smoothstepsmoothstep(threshold - w, threshold w, NdotL)在阈值附近有一个过渡带。过渡带宽度 w 越大交界线越柔和抗干扰能力越强。多段阶梯把 0 到 1 分成 N 段每段一个色阶。适合需要多个明暗层次的风格。我的经验是只要场景里光源超过两盏主光的色阶函数就一定要用 Smoothstep过渡带宽度至少给到 0.05。这个宽度看起来小但足以吸收辅助光带来的微小扰动让交界线保持稳定。如果追求极致锐利的交界线那就必须严格保证辅助光不参与色阶计算两者选其一不能既要又要。3. 主光通道的独立计算与阈值设计3.1 主光怎么隔离出来在 Unity 的 Shader 里主光通常就是那盏LightMode ForwardBase的平行光。卡通材质的主光计算应该放在这个 Pass 里并且只处理这一盏灯。具体做法是在 ForwardBase Pass 里拿到主光方向_WorldSpaceLightPos0算出 NdotL然后走完整的色阶流程。关键点是不要在 ForwardBase 里遍历其他光源。Unity 的 ForwardAdd Pass 会自动为每盏额外的灯调用一次如果你在 ForwardBase 里手动累加其他灯就会重复计算。正确的结构是// ForwardBase Pass只处理主光 float3 lightDir normalize(_WorldSpaceLightPos0.xyz); float NdotL dot(normalize(i.normal), lightDir); float mainLightTerm smoothstep(_Threshold - _Softness, _Threshold _Softness, NdotL); float3 mainColor lerp(_ShadowColor.rgb, _BaseColor.rgb, mainLightTerm);这段代码里mainLightTerm就是主光单独决定的明暗系数它只受主光影响干净可控。3.2 阈值不是拍脑袋定的要看主光角度阈值_Threshold这个参数很多人随手填 0.5其实它应该跟主光的入射角度挂钩。假设主光从角色斜上方 45 度打过来那么角色正面朝向相机的部分 NdotL 大约在 0.7 左右侧面在 0.3 左右。如果你希望明暗交界线落在脸颊侧面阈值就应该设在 0.4 到 0.5 之间。我一般的做法是先在场景里摆好主光然后临时把阈值调成 0.5观察交界线位置再微调阈值直到交界线落在想要的位置。这个值定下来之后就不要频繁改了因为它是整个角色明暗结构的基准。一个容易被忽略的点如果角色会转身或者主光会动阈值固定会导致交界线位置随角度变化。这时候要么接受这种变化很多动画就是这么做的要么用视线方向做补偿把阈值跟角色朝向绑定。后者复杂一些但能让角色在任意角度下都保持好看的明暗结构。3.3 软阈值宽度与多光源数量的关系软阈值宽度_Softness的取值跟场景里光源数量有直接关系。光源越多辅助光带来的扰动越大需要的过渡带就越宽。我实测下来的一组参考值只有主光_Softness可以给 0.01 到 0.02交界线接近硬边。主光 1 盏补光_Softness给 0.03 到 0.05。主光 2 到 3 盏辅助光_Softness给 0.05 到 0.08。光源更多或者有动态光_Softness给 0.08 以上或者干脆改用多段阶梯。这个表不是死的但方向是对的光源越多过渡带越宽。如果你发现交界线开始抖或者碎第一反应应该是加宽_Softness而不是去调光源强度。4. 辅助光与特效光的叠加策略4.1 辅助光只做加法不碰色阶辅助光的处理原则很简单在主光算出的颜色基础上做加法或乘法绝不参与色阶判断。具体实现上辅助光可以在 ForwardAdd Pass 里处理也可以在主 Pass 里手动采样。我倾向于在主 Pass 里手动处理辅助光因为这样能完全控制叠加方式。做法是把辅助光的光照结果算成一个颜色值然后以较低的强度加到主光结果上// 辅助光只提亮不改色阶 float3 fillLight _FillLightColor.rgb * saturate(dot(normal, _FillLightDir.xyz)) * _FillLightIntensity; float3 finalColor mainColor fillLight * (1.0 - mainLightTerm);注意最后那个(1.0 - mainLightTerm)它的作用是让辅助光只在暗部生效。亮部本来就已经是亮色了再加辅助光只会过曝。这个技巧能让辅助光精准地提亮阴影区而不影响亮部是保持画面干净的关键。4.2 轮廓光的独立通道处理轮廓光Rim Light是卡通渲染里提升立体感的重要手段但它在多光源下特别容易出问题。如果轮廓光也走普通光照计算它会跟主光、辅助光混在一起导致边缘亮边时有时无。正确的做法是给轮廓光单独一个通道用视线方向和法线的夹角来判断float rim 1.0 - saturate(dot(normalize(viewDir), normal)); rim smoothstep(_RimThreshold - _RimSoftness, _RimThreshold _RimSoftness, rim); float3 rimColor _RimColor.rgb * rim * _RimIntensity; finalColor rimColor;轮廓光完全不依赖任何光源方向只跟相机和法线有关所以无论场景里有多少盏灯轮廓光都是稳定的。这也是为什么很多卡通渲染方案把 Rim 单独拎出来做——它本来就不该跟光照系统搅在一起。4.3 高光的镜面反射在卡通里怎么处理卡通渲染的高光通常不是物理镜面反射而是一个可控的亮斑。常见做法是用半角向量Half Vector和法线的点积配合一个很高的指数来收窄亮斑float3 halfDir normalize(lightDir viewDir); float spec pow(saturate(dot(normal, halfDir)), _SpecPower); spec smoothstep(_SpecThreshold - _SpecSoftness, _SpecThreshold _SpecSoftness, spec); float3 specColor _SpecColor.rgb * spec * _SpecIntensity;这里有个坑高光应该只跟主光绑定。如果每盏灯都算一遍高光角色脸上会出现好几个亮斑非常难看。所以高光计算里用的lightDir必须是主光方向其他光源不参与高光。5. 实战一套可复现的多光源卡通材质配置5.1 场景光源的摆放建议在动手写 Shader 之前先把场景光源摆对。我的标准配置是三盏灯主光平行光从角色斜上方 45 度、偏前方 30 度打过来强度 1.0开阴影。这盏灯决定明暗交界线和投影。补光平行光或点光从主光反方向偏下打过来强度 0.2 到 0.3不开阴影。作用是提亮暗部避免死黑。轮廓光可以用平行光模拟从角色背后打过来强度 0.5 左右不开阴影。配合 Rim 通道使用。这三盏灯摆好之后角色应该已经有一个不错的明暗结构了。如果还要加特效光比如技能光效那盏灯单独处理不要混进基础光照。5.2 材质参数的推荐初始值下面这套参数是我在多个项目里验证过的起点可以直接抄参数推荐值说明_Threshold0.45主光色阶阈值_Softness0.05软阈值宽度_ShadowColor偏冷的暗色阴影色不要用纯黑_BaseColor角色固有色亮部颜色_FillLightIntensity0.25辅助光强度_RimThreshold0.7轮廓光阈值_RimSoftness0.1轮廓光过渡_SpecPower50高光收窄指数_SpecThreshold0.9高光阈值这套值不是万能的但作为起点能省掉大量试错时间。调的时候一次只动一个参数观察变化别一次改一堆。5.3 完整的主 Pass 结构把前面的内容串起来主 Pass 的结构大致是这样// 1. 基础数据 float3 normal normalize(i.normal); float3 viewDir normalize(_WorldSpaceCameraPos - i.worldPos); float3 lightDir normalize(_WorldSpaceLightPos0.xyz); // 2. 主光色阶 float NdotL dot(normal, lightDir); float mainTerm smoothstep(_Threshold - _Softness, _Threshold _Softness, NdotL); float3 baseColor lerp(_ShadowColor.rgb, _BaseColor.rgb, mainTerm); // 3. 辅助光只提亮暗部 float3 fillDir normalize(_FillLightDir.xyz); float fillTerm saturate(dot(normal, fillDir)) * _FillLightIntensity; baseColor _FillLightColor.rgb * fillTerm * (1.0 - mainTerm); // 4. 轮廓光 float rim 1.0 - saturate(dot(viewDir, normal)); rim smoothstep(_RimThreshold - _RimSoftness, _RimThreshold _RimSoftness, rim); baseColor _RimColor.rgb * rim * _RimIntensity; // 5. 高光只跟主光 float3 halfDir normalize(lightDir viewDir); float spec pow(saturate(dot(normal, halfDir)), _SpecPower); spec smoothstep(_SpecThreshold - _SpecSoftness, _SpecThreshold _SpecSoftness, spec); baseColor _SpecColor.rgb * spec * _SpecIntensity; // 6. 输出 return float4(baseColor, 1.0);这个结构清晰地把主光、辅助光、轮廓光、高光分成了独立的计算块每块职责单一互不干扰。想调哪部分就改哪部分不会牵一发动全身。5.4 阴影接收与多光源的配合阴影是另一个容易出问题的地方。Unity 的实时阴影默认只处理主光的阴影辅助光如果也开阴影会跟主光阴影叠加导致阴影区出现双重暗边。我的建议是只有主光开阴影辅助光一律不开。辅助光的作用是提亮开阴影反而会让暗部更暗跟它的职责矛盾。如果确实需要辅助光投影比如某些特殊场景那就把辅助光的阴影强度调得很低或者用单独的阴影通道处理。另外接收阴影的代码要放在主光色阶计算之后float shadowAtten SHADOW_ATTENUATION(i); mainTerm * shadowAtten;这样阴影会直接压暗主光的明暗系数效果最自然。6. 那些文档里不会写的踩坑记录6.1 辅助光把交界线顶碎的真实案例前面提到的那个补光把阴影顶到亮部的坑我后来复盘发现根因是补光强度 0.8 太高而且补光方向跟主光夹角太小导致补光在阴影区的 NdotL 接近 0.5加上主光的 0.3总和 0.8 直接超过阈值。修复方案有两个一是把补光强度降到 0.3 以下二是把补光方向调整到跟主光夹角更大让它在阴影区的贡献更小。我最后两个都做了补光强度降到 0.25方向也调了交界线立刻恢复干净。经验总结辅助光的强度上限取决于主光在阴影区的 NdotL 和阈值之间的差距。差距越小辅助光能给的强度就越低。如果主光阴影区 NdotL 是 0.3阈值是 0.5那辅助光最多只能贡献 0.2 的 NdotL超过就会顶穿。6.2 多盏点光导致的色阶跳变点光比平行光更难处理因为点光有距离衰减光照强度随距离连续变化。当角色在点光附近移动时点光贡献会连续变化如果它参与了色阶计算交界线就会随移动而抖动。解决办法还是那条点光不参与色阶计算只做颜色叠加。如果点光必须影响明暗比如角色走进一个暗房间点光是唯一光源那就把点光临时提升为主光或者用脚本动态切换主光。这个切换逻辑在游戏里很常见比如角色进入不同光照区域时切换主光引用。6.3 移动端性能与多光源的取舍移动端上每多一盏实时光源就多一次 ForwardAdd Pass性能开销线性增长。卡通渲染本身已经比写实渲染便宜因为光照计算简单但多光源叠加后开销也不容忽视。我的优化策略是主光走实时光照辅助光和轮廓光走顶点色或预烘焙。具体做法是把辅助光的方向和强度烘焙到顶点色里运行时直接采样顶点色省掉一次光照计算。轮廓光如果方向固定也可以烘焙。这样移动端只需要处理一盏实时光源性能压力小很多。如果必须用实时光源那就把辅助光的更新频率降低比如每两帧更新一次或者只在角色移动时更新。卡通渲染对光照变化的敏感度比写实渲染低稍微降低更新频率肉眼几乎看不出来。6.4 材质面板参数太多导致的调参混乱用 liltoon 这类功能齐全的方案时材质面板上参数几十个新手很容易调乱。我的习惯是把参数按职责分组调的时候一组一组来先调主光组阈值、软度、阴影色把明暗结构定下来再调辅助光组强度、颜色把暗部提亮最后调轮廓光和高光组加细节。每组调完就锁定不再回头改避免互相干扰。另外把常用的几个参数阈值、软度、辅助光强度暴露在材质面板顶部其他参数收进折叠区能大幅提升调参效率。这个在 liltoon 里可以通过自定义材质面板实现或者干脆自己写一个简化版的 Shader。7. 从 liltoon 的实现里能学到什么liltoon 作为目前卡通渲染领域比较成熟的方案它的多光源处理有几个值得借鉴的设计。第一是光照分层做得彻底。liltoon 把主光、背光、环境光、轮廓光、高光全部分成独立通道每个通道有自己的开关和参数。这种设计的好处是每层可以单独调试不会互相污染。我在自己写 Shader 的时候也沿用了这个思路把每个光照成分做成独立的函数主 Pass 只负责调用和组合。第二是软阈值参数暴露给用户。liltoon 的明暗交界线软硬度是可以调的这个参数直接对应我们前面说的_Softness。它把这个参数放在显眼位置说明作者很清楚多光源下软阈值的重要性。第三是对移动端的优化。liltoon 有专门的移动端版本砍掉了一些高开销特性保留了核心的卡通光照。这个取舍思路值得学习不是所有效果都要在移动端实现抓住核心的明暗结构和轮廓光其他能省则省。不过 liltoon 也有它的局限比如自定义光照逻辑比较麻烦想加一个特殊的光照通道得改源码。所以如果你的项目有比较特殊的卡通渲染需求自己写一套精简的 Shader 可能比改 liltoon 更省事。我现在的做法是基础角色用 liltoon特殊角色比如需要特殊光照效果的 Boss自己写 Shader两套并存。8. 多光源卡通渲染的调试清单最后给一份我在实际项目里用的调试清单遇到多光源问题时按这个顺序排查确认主光只有一盏。检查场景里是不是有多盏灯被标记为主光或者 Shader 里是不是在 ForwardBase 里错误地累加了其他灯。确认辅助光不参与色阶。检查辅助光的计算是不是在主光色阶之后有没有意外地加到mainTerm上。检查软阈值宽度。如果交界线碎先把_Softness加到 0.08 试试如果好了就是软阈值太窄。检查辅助光强度。如果交界线被顶穿把辅助光强度降到 0.2 以下看是否恢复。检查高光是否只跟主光。如果脸上出现多个亮斑说明高光用了错误的光源方向。检查阴影叠加。如果暗部出现双重暗边检查是不是辅助光也开了阴影。检查点光距离衰减。如果角色移动时交界线抖动检查点光是不是参与了色阶计算。这份清单基本覆盖了多光源卡通渲染 90% 的问题。剩下的 10% 通常是材质参数之间的相互影响那就得靠耐心一组一组调了。调卡通材质这件事说到底是对控制感的追求。写实渲染可以靠物理正确蒙混过关卡通渲染不行每一个色阶边界、每一块阴影形状都是设计出来的。多光源之所以难就是因为它打破了单光源下那种简单的控制关系。但只要把光照分层做清楚让每盏灯各司其职多光源反而能做出比单光源更丰富的画面——主光定结构辅助光提层次轮廓光加立体感高光点睛四者配合好了角色的质感能上一个台阶。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →