
做日式卡通渲染的人第一眼看到动画电影的分镜稿往往会愣一下明明只是一张干净到近乎单调的画面角色脸上清清楚楚只有两三块颜色可看起来就是比一堆物理材质堆出来的模型要生动。问题出在哪不是模型精度不是贴图信息量而是着色思路里的“分区意识”。传统渲染追逐连续的光照过渡卡通渲染恰好相反它把世界简化成一小块一小块明确的明暗区域再用颜色本身去讲故事。这也是为什么我一直觉得Unity美术向的卡通渲染入门第一课不该是描边而是着色。这篇文章就是“着色篇”的第一篇目标是把日式卡通渲染中最关键的光影分层思路和Shader实现讲透。我会从赛璐璐的明暗分区逻辑出发给出可以直接抄进Unity工程的基础Toon着色Shader然后逐个拆解漫反射色阶、卡通高光、边缘光和阴影Ramp的实现细节。文章定位是给有一定Shader基础、想认真做动漫风格角色的开发者和技术美术看也欢迎零基础但愿意边抄边学的新手对照着试。为了保证节奏清晰这篇只讲“光打在模型上之后怎么变成色块”描边、头发丝材质、URP下的完整配置留给后面的文章。1. 画师的分层思维才是卡通渲染的核心1.1 赛璐璐着色法中的明暗分区你随便找一张赛璐璐动画的截图放大角色脸部会发现颜色区块少得惊人额头、颧骨、鼻梁是一整块亮色下颌线到耳朵下面是一整块暗色刘海投下来的投影又是一块更深的颜色中间几乎没有渐变。这种把体积感压缩成“两到三块平面颜色”的做法就是赛璐璐着色法。这里有个容易被忽略的细节动画师并不是简单地把模型“调暗”而是在暗部做了明确的色相偏移。比如受光面是偏暖的肤色暗面就会偏灰紫、偏冷白衬衫的亮部是白色暗部会带一点天蓝的倾向。这种颜色选择本质上是在用色彩关系暗示空间关系而不是用亮度递减模拟物理光照。所以做卡通Shader时第一原则就是放弃“亮度连续变化”的执念把光照结果当成一个开关要么亮、要么暗、要么中间过渡然后让你能自由控制每一块的颜色。理解了这个后面所有参数就都有了解释。1.2 传统光照为什么不能直接搬很多新手做卡通渲染第一反应是拿Standard Shader调低光滑度、加一张黑白贴图觉得“看起来有点平”就够了。结果往往是一张灰扑扑、油腻腻的脸既没有动画感又丢了体积。问题就出在PBR光照模型的底层逻辑和卡通风格是冲突的。PBR模型的输入是法线、粗糙度、金属度输出是一路连续变化的亮度值它追求的是“光怎么反射就怎么呈现”。而卡通画面需要的是大色块、明确的明暗交界线、可控的高光形状背后的语言体系更像平面设计不追求物理正确追求视觉秩序。强行靠降低粗糙度去逼近只会得到“脏”而不是“干净”。这也是为什么成熟项目的卡通角色几乎不用内置Standard着色器而是写自定义光照模型。自定义光照模型的自由度决定了你能不能在漫反射、高光、边缘光三块里分别用上step、smoothstep这类阶梯函数把连续光切成色块。2. 明暗阶数的选择三阶体系还是四阶体系2.1 色阶数由什么决定第二个要想清楚的问题是角色身上到底要分几层色块早期像素风或复古风格游戏喜欢用“一阶硬切”也就是只有亮和暗两个颜色干净但信息量低现代手游角色大多数用三阶或四阶脸部、头发、衣服各取所需。具体怎么选可以从三个维度判断。第一是美术风格如果参考动画是那种很现代的、吉卜力或新海诚式的细腻光影三阶起步比较稳妥如果是复古赛璐璐或者偏Q版二阶就够。第二是材质类型皮肤一般需要一到两个过渡层模拟颧骨、鼻梁的柔和感衣服大面积布料反而适合更少的色阶避免显得“碎”。第三是场景光照场景里多光源多的时候色阶太多会导致画面抢戏单主光时则可以加一层暗部补光。我比较推荐的起步方案是“三阶基础 一个可选的闭塞层”。也就是说亮部、中间调或阴影色、以及贴地/关节处的更深投影。这个结构既能Hold住绝大部分角色又不会让Shader复杂到难调。2.2 阈值点与过渡带参数的经验值确定阶数之后最关键的就是阈值点放在哪。这里没有任何物理公式可以套完全靠肉眼和审美但还是有几个经验值可以当作起点。假设NdotL的取值范围是-1到1且你已经把它映射到0到1区间那么第一阈值亮部和中间调的分界通常放在0.35到0.45之间第二阈值中间调和暗部的分界放在0.15到0.25之间。阈值越高暗部面积越大整体就越“阴”阈值越低画面越亮但容易丢失体积感。过渡带宽度也就是smoothstep的平滑范围一般控制在0.02到0.08之间。这个值决定明暗交界线是“一刀切”还是“有一点点斜向过渡”。纯step的硬切远处看很好看近看全是锯齿和闪烁所以我会永远用smoothstep但把过渡带压到很小视觉上依然像硬边边缘却稳得多。3. 阶梯化漫反射Shader实现从Lambert到可用的卡通色阶3.1 自定义光照模型比内置Lambert更贴近卡通需求Surface Shader里写自定义光照模型是Unity内置渲染管线中最适合做卡通着色的方式。它的好处是光照函数可以逐光源执行能拿到主光源方向、视角方向、阴影衰减atten并且天然兼容Unity的阴影贴图系统不需要自己处理阴影矩阵。核心思路是写一个Lighting函数替换掉默认的Lambert。内置Lambert做的事情很简单NdotL乘以光源颜色。我们要做的就是在它后面加一道工序把连续乘积结果送进smoothstep变成阶梯化的值再用这个值去混合亮部和暗部颜色。这里要注意Lighting函数的签名。如果想在光照模型里同时使用视角方向做高光和边缘光必须用带viewDir参数的版本half4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)要是只写漫反射也可以省掉viewDir但为了后面扩展高光我建议一次到位。3.2 smoothstep代替step硬切里藏一点点柔和直接给NdotL套step确实能出色块但实际运行时你会看到边界处严重锯齿而且角色稍微动一下边界会一帧帧地跳观感非常差。原因是屏幕上的硬边很难被抗锯齿处理尤其是偏高光反射区域边缘更是闪得厉害。smoothstep的作用就是把“开/关”变成一个极窄的渐变带。视觉上体感依然是硬切但边缘像素有了过渡就不会高频闪烁。这个渐变带宽度就是我在上一章说的“过渡带”由两个阈值决定_DiffuseThreshold - _DiffuseSmooth 和 _DiffuseThreshold _DiffuseSmooth。顺带说一句不要把smoothstep的过渡带调太大否则角色会变成“软软的塑料”失去赛璐璐的干脆感。一般0.05左右是安全线。3.3 完整的基础着色Shader下面是我用于起步的一套OnionShading基础Shader完整可跑内置渲染管线Unity 2019及以上版本都支持。把它丢进工程建一个材质挂到任意模型上就能看到色阶化的效果。Shader Custom/ToonShadingBasic { Properties { _BaseColor (Base Color, Color) (1,1,1,1) _ShadowColor (Shadow Color, Color) (0.7,0.7,0.85,1) _DiffuseThreshold (Diffuse Threshold, Range(0,1)) 0.4 _DiffuseSmooth (Diffuse Smoothness, Range(0,0.2)) 0.05 _SpecularColor (Specular Color, Color) (1,1,1,1) _SpecularPower (Specular Power, Range(1,128)) 24 _SpecularThreshold (Specular Threshold, Range(0,1)) 0.5 _SpecularSmooth (Specular Smoothness, Range(0,0.2)) 0.05 _RimColor (Rim Color, Color) (1,0.7,0.5,1) _RimPower (Rim Power, Range(0,10)) 4 _RimIntensity (Rim Intensity, Range(0,3)) 1 } SubShader { Tags { RenderTypeOpaque } CGPROGRAM #pragma surface surf Toon fullforwardshadows #pragma target 3.0 half4 _BaseColor; half4 _ShadowColor; half _DiffuseThreshold; half _DiffuseSmooth; half4 _SpecularColor; half _SpecularPower; half _SpecularThreshold; half _SpecularSmooth; half4 _RimColor; half _RimPower; half _RimIntensity; struct Input { float2 uv_MainTex; }; half4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten) { half NdotL dot(s.Normal, lightDir); half diffStep smoothstep( _DiffuseThreshold - _DiffuseSmooth, _DiffuseThreshold _DiffuseSmooth, NdotL ); half3 lightColor _LightColor0.rgb * s.Albedo.rgb; half3 shadowColor _ShadowColor.rgb * s.Albedo.rgb; half3 diffuse lerp(shadowColor, lightColor, diffStep); half3 H normalize(lightDir viewDir); half NdotH saturate(dot(s.Normal, H)); half spec pow(NdotH, _SpecularPower); half specStep smoothstep( _SpecularThreshold - _SpecularSmooth, _SpecularThreshold _SpecularSmooth, spec ); specStep * diffStep; half rim 1.0 - saturate(dot(s.Normal, viewDir)); rim pow(rim, _RimPower) * _RimIntensity; half4 final; final.rgb diffuse * atten _SpecularColor.rgb * specStep _RimColor.rgb * rim; final.a s.Alpha; return final; } void surf(Input IN, inout SurfaceOutput o) { o.Albedo _BaseColor.rgb; o.Alpha _BaseColor.a; } ENDCG } FallBack Diffuse }这个版本看起来简单但已经把“亮部、暗部、高光、边缘光”四层都搭好了。实际使用的时候你可以把_BaseColor换成一张角色贴图然后在surf函数里采样阴影色也可以单独拆成贴图通道。等这套跑通后再谈Ramp贴图和多层材质的进阶玩法。4. 卡通高光从Blinn-Phong到形状可控的带状高光4.1 高光为什么要单独做一阶如果你把PBR材质里的高光直接套在卡通角色上会得到一个很不协调的结果漫反射已经被压成大色块了高光却还在那边连续渐变像一块油渍。卡通风格的高光应该是“装饰性”的它不需要精确模拟反射只需要在正确的位置出现一个合适的形状。所以卡通高光的基本做法是先算Blinn-Phong高光得到一个0到1的连续值然后再对这个值做一次smoothstep或step把它变成“要么有、要么没有”的色块。阈值越高高光面积越小Power值控制高光在表面上的相对集中程度Power越大高光越锐利、越小。这里有个需要注意的细节暗部的高光要不要保留大部分情况下高光应该只出现在受光面因为赛璐璐里阴影里的高光通常是另一层动画层一般只在头发、眼睛上出现。所以在代码里我把specStep乘了diffStep让暗部默认没有高光如果你需要暗部点缀可以单独加参数放开。4.2 参数化高光的核心代码上面的基础Shader里已经包含简单的高光实现。如果你要微调核心参数就两个_SpecularPower和_SpecularThreshold。前者控制高光在模型表面的紧凑程度后者控制“多亮才算高光”本质上是在切割pow之后的连续结果。我给一个常见的皮肤高光参数组合作为参照_SpecularPower 24_SpecularThreshold 0.45_SpecularSmooth 0.05。这个组合在脸颊、鼻梁、肩膀这些位置会出现圆润的小块高光不会过爆也不会完全消失。如果是金属铠甲、武器这类硬表面可以把Power拉到64甚至128阈值适当提高到0.6以上得到的高光会接近“刀切般”的锐利亮片很有日系游戏武器质感。一句话总结Power管集中度Threshold管亮片大小。4.3 头发带状高光的做法日式卡通里最标志性的高光其实是头发上那条像丝带一样的带状高光。用刚才的Blinn-Phong直接算高光会是一团散开的圆斑完全不对味。正确的做法是让高光只在一个方向上扩散另一个方向被压扁成带子。实现思路是利用发丝的切线Tangent方向。你可以把切线方向传进Shader用视角和光源在切线方向上的投影值替代原本的NdotH。简化版本是这样half hairSpec dot(viewDir, tangent); half3 lightTangent dot(lightDir, tangent); half wrap saturate(hairSpec lightTangent); half band pow(wrap, _SpecularPower); half hairBand smoothstep(_HairSpecThreshold - _HairSpecSmooth, _HairSpecThreshold _HairSpecSmooth, band);实际项目中我建议在模型里把头发单独一组材质头发模型本身要有合理的切线方向否则带子位置会乱跑。这个属于比较进阶的玩法一文讲不清后面单独开篇再往细里说。5. 边缘光让角色从背景里“跳”出来5.1 菲涅尔的简化使用不要依赖物理精确边缘光几乎是所有二次元风格角色的标配。它的原理来自菲涅尔现象视线与表面法线夹角越大反射越强所以边缘会发光。在卡通Shader里我们不需要精确的物理公式只需要简单地用1减去法线和视线点积取pow再乘上颜色和强度。代码里我用的是half rim 1.0 - saturate(dot(s.Normal, viewDir)); rim pow(rim, _RimPower) * _RimIntensity;_RimPower越大边缘光越窄越贴合轮廓_RimIntensity控制亮度。经验值方面皮肤和头发的边缘光Power一般取3到5强度控制在0.8到1.2之间太强会像“塑料发光”。机械角色的边缘光可以更强甚至从边缘往内部渗入一点营造“能量感”。5.2 与描边、高光叠加时的遮蔽关系如果你后面加了描边边缘光和描边会存在明显的遮挡冲突。最常见的做法是背面放大法描边把角色法线翻转把背面顶点沿法线向外挤出用纯色渲染。此时视线边缘会先被描边覆盖边缘光其实被描边盖住了一部分看起来会像描边发光反而不干净。我的做法是给边缘光加一个“朝向约束”也就是让边缘光只出现在法线朝向摄像机的一侧并且用深度偏移或描边宽度参数去错开两者。更简单的方法先不开描边调边缘光再开描边后手动把边缘光Power调小一点让两者分层。还有个容易忽略的问题边缘光在暗部会特别抢眼。比如角色背对主光源时整个身体是暗的结果发丝边上一圈橙光非常突兀。所以我会把边缘光乘以漫反射的结果至少在纯阴影区域减弱它。具体乘不乘取决于你想要的风格游戏里“夜店风”角色反而要刻意保留阴影里的边缘光。6. 阴影Ramp用一张贴图替代纯数学色阶6.1 为什么成熟项目最终都会走向Ramp贴图数学公式再灵活也画不出美术想要的微妙渐变。比如脸颊上“从亮到暗稍微带一点粉紫色过渡”、布料褶皱里“暗部偏蓝一点点再转深灰”用smoothstep一层层堆堆到后面你根本调不动。Ramp贴图就是专为这种情况准备的把NdotL当作UV的U坐标在一张横向渐变的贴图里取颜色想要什么过渡美术直接画在贴图里。这也是为什么吉卜力风格的游戏或个人项目最终几乎都会改成Ramp方案。它把“调光照”这件事从写代码/拖参数变成了“画一张图”。这个转换对美术极其友好对程序员来说Shader反而变简单了。6.2 Ramp采样代码与主光源阴影的合并用Ramp替换smoothstep的代码非常直接。先把NdotL从-1到1映射到0到1作为U坐标再指定一行Ramp行用tex2D取色。你可以把不同材质的状态放到不同行材质球上用一个_RampRow参数切换一张图管全身。half rampUV saturate(NdotL) * 0.5 0.5; half3 rampColor tex2D(_RampTex, float2(rampUV, _RampRow)).rgb; half3 diffuse rampColor * _BaseColor.rgb;此时主光源阴影衰减atten还是要乘到结果里。要注意一个问题如果角色站在建筑的阴影里atten会变得很小diffuse会整个变黑画面立刻失去卡通感。所以我会把衰减结果做一个clamp保留下限atten saturate(atten _MinIndirectLight);_MinIndirectLight一般给0.2到0.4保证角色始终有一层底色。阴影交界处还是会出来只是不会死黑。这个参数在Ramp方案里特别重要很多新人第一次接阴影后角色“一片黑”就是忽略了它。7. 五个高频调试问题与我的处理顺序7.1 问题表格与快速定位在实际操作中Shader写完只是开始调试才是大头。我把最常踩的五个问题整理成一张表方便你对照排查。问题现象根本原因解决办法明暗交界线锯齿闪烁step硬切过渡带太小把_DiffuseSmooth提到0.03以上必要时开MSAA角色全身发黑阴影衰减没有下限用atten _MinIndirectLight保底阴影和暗部混成一片缺乏层次阴影色饱和度过低阴影色适当偏色别直接乘灰度高光边缘像狗啃高光阈值smooth不够增大_SpecularSmooth并确保MSAA开启URP下材质变紫Surface Shader不兼容URP改为HLSL Shader或Shader Graph实现表格里列的都算“症状明显的案例”下面再展开两个我反复被问的问题。7.2 阴影色为什么会发灰很多人的阴影色喜欢设置成灰黑色然后模型一进游戏就“脏”。原因很简单现实里的暗部并不是把固有色变暗而是受到环境光和漫反射影响后的另一种颜色。皮肤暗部偏紫红白衣服暗部偏蓝树叶暗部偏绿灰。你直接给整数灰度值等于把所有色相的差异都抹平了。我的习惯是阴影色 固有色乘以一个偏色的倍率再叠加一个环境色倾向。具体到Shader里可以把_ShadowColor拆成两个参数一个是倍率0.6到0.8一个是叠加色冷色或暖色。这样调整起来既直观又不容易发灰。7.3 我平时调卡通的先后顺序每次接到新角色我都不是上来就拉参数而是按固定顺序来第一步把角色模型放在纯灰色环境只保留主方向光关掉其他光确保只有一盏平行光在起作用。第二步先调漫反射阈值和阴影色得到基本明暗结构。第三步开高光调整位置和大小确保高光和阴影不打架。第四步边缘光放上去微调Power和强度。第五步才打开场景里的环境光、Light Probe和描边做整体调和。这个顺序能最大程度减少变量让你清楚每一步改动到底影响了什么。遇到问题也别急着改材质球先检查灯光方向和阴影设置很多时候卡通感出不来恰恰是场景光照太“物理”了。把默认平行光强度和Skybox强度改成适合卡通的软光环境着色器本身的压力会小很多。如果按照这套流程跑通了下一篇我打算把描边、Ramp贴图和多层材质串起来讲——那是让角色从“还能看”到“有动画感”的关键一步。着色篇一就先到这里你可以把文章里的Shader丢进Unity里试试有问题欢迎在评论区带上截图聊我看到都会回。