拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Unity卡通渲染完整指南:从ShaderLab到ShaderGraph实现风格化着色

1. 项目概述为什么我们需要一个“完整指南”如果你在Unity里尝试过卡通渲染大概率经历过这样的场景网上搜到一个看起来很酷的Toon Shader兴冲冲地导入项目结果发现要么效果僵硬得像塑料要么参数多到眼花缭乱根本调不明白最后只能放弃继续用着默认的PBR材质。这正是我写这篇指南的初衷——市面上不缺零散的卡通着色器代码或教程但缺一份能让你从原理到实践、从参数到审美都彻底搞懂的“地图”。卡通渲染或者说非真实感渲染NPR它的核心目标不是模拟物理世界的真实而是模拟艺术家笔下的风格化真实。这听起来很艺术但实现起来却是一系列精密的技术组合。一个“惊艳”的卡通效果背后是光影、轮廓、色彩和细节的协同作战。本指南将围绕Unity ShaderLab和Shader Graph两大工具拆解如何构建一个从基础到进阶、高度可控的卡通着色器。无论你是想为自己独立游戏的角色赋予独特的二次元灵魂还是为项目中的场景添加风格化的滤镜这里的内容都将是你坚实的起点。我们会避开那些华而不实的炫技专注于那些真正决定效果好坏、且能稳定复现的核心技术点。2. 卡通渲染的核心原理从“真实”到“风格”在深入代码之前我们必须先统一思想卡通渲染不是在“简化”真实感渲染而是在用另一套规则“重建”视觉语言。理解这套规则是调出好效果的前提。2.1 光影的离散化阶调与色阶真实世界的光影过渡是连续且平滑的。而卡通风格的核心特征之一就是将这种连续的光照信息进行“离散化”或“分层”处理。这通常通过一个叫做“阶调映射”或“色阶”的技术来实现。技术原理我们计算物体表面每个像素接收到的光照强度通常是兰伯特漫反射模型的dot(N, L)结果即法线向量与光源方向向量的点积。这个值原本在[-1, 1]或[0, 1]的范围内连续变化。为了得到卡通效果我们将这个连续值输入到一个“阶跃函数”中。最简单的实现是使用step或smoothstep函数。// 基础阶调映射示例 float diffuse max(0, dot(normal, lightDir)); // 基础漫反射 float toonDiffuse step(_RampThreshold, diffuse); // 二值化亮部或暗部但这只是最简单的两阶亮/暗。为了获得更丰富的层次感我们通常会使用一张一维的渐变纹理Ramp Texture来进行采样。这张纹理的横轴U方向对应光照强度纵轴V方向可以放置不同的颜色阶。通过采样我们就能将连续的光照值映射为有限的几个色块。实操心得smoothstep比step更常用因为它可以在色阶边界产生一个微小的平滑过渡避免出现生硬的锯齿边缘让光影看起来更自然。_RampSmoothness这个参数就是用来控制这个过渡区间大小的是调节风格软硬的关键。2.2 轮廓的强调描边的多种实现描边是卡通风格的另一大标志。它不仅能强化物体的轮廓还能在视觉上将其从背景中分离出来。在Unity中实现描边的主流方法有以下几种各有优劣后处理描边Post-processing Outline通过屏幕后处理识别法线或深度发生剧烈变化的边缘。优点是实现简单对所有物体生效。缺点是无法区分物体内部的材质边缘比如衣服和皮肤的边界且对透明物体的支持不友好。背面膨胀描边Backface Expansion渲染两遍模型。第一遍用正面剔除Cull Front渲染一个沿法线方向稍微放大的、纯色的模型作为描边第二遍正常渲染模型本身。这是最经典、控制粒度最细的方法可以针对不同材质部分设置不同颜色和宽度的描边。几何着色器描边在几何着色器中生成轮廓线。功能强大但相对复杂且移动平台支持有限。基于平滑法线的描边在建模阶段就准备好一套用于描边的平滑顶点法线通常称为“轮廓法线”。渲染时使用这套法线来计算背面膨胀。这是目前高质量卡通渲染项目如《原神》、《崩坏星穹铁道》的主流方案能完美解决头发、服饰等复杂结构的描边问题。对于大多数项目背面膨胀法是平衡效果与复杂度的最佳起点。它的核心代码片段如下// 描边Pass (通常放在第一个PassCull Front) v2f vert (appdata v) { v2f o; float3 normal normalize(v.normal); float3 viewNormal mul((float3x3)UNITY_MATRIX_IT_MV, normal); // 将法线变换到视图空间 float3 viewPos UnityObjectToViewPos(v.vertex); // 将顶点变换到视图空间 // 沿视图空间法线方向膨胀顶点 viewPos normalize(viewNormal) * _OutlineWidth; o.pos mul(UNITY_MATRIX_P, float4(viewPos, 1.0)); // 投影到裁剪空间 o.color _OutlineColor; return o; }2.3 高光的风格化不是亮点是“色块”卡通风格的高光同样需要离散化。我们不再追求PBR中那种柔和、有形状变化的高光而是追求一种锐利、明亮、甚至带有形状如星形、十字形的色块。实现思路通常基于视角方向V和反射方向R的点积来计算高光强度Blinn-Phong模型。然后同样使用step或smoothstep函数对其进行阈值化处理。更进一步我们可以使用一张特殊的高光遮罩纹理Specular Mask Map来控制高光出现的区域和形状例如只在头发、金属装饰或湿润的眼睛上出现特定形状的高光。float3 halfDir normalize(lightDir viewDir); float specularDot max(0, dot(normal, halfDir)); float specular pow(specularDot, _SpecularGloss); float toonSpecular step(_SpecularThreshold, specular) * _SpecularIntensity;3. 构建一个基础但完整的卡通着色器理论说再多不如动手写一个。我们将使用ShaderLab来构建一个具备基础阶调、描边和高光的卡通着色器。这个着色器将包含两个Pass一个用于描边一个用于主体着色。3.1 着色器结构与属性定义首先我们定义着色器需要的全部属性Properties这些属性会在材质面板上显示为可调节的参数。Shader Custom/ToonShading/BasicToon { Properties { // 基础颜色 _MainTex (主纹理, 2D) white {} _BaseColor (基础色, Color) (1,1,1,1) // 阶调光影 _RampTex (阶调纹理, 2D) white {} _RampSmoothness (阶调平滑度, Range(0, 0.2)) 0.05 _ShadowColor (阴影颜色, Color) (0.5,0.5,0.5,1) // 描边 _OutlineWidth (描边宽度, Range(0, 0.1)) 0.03 _OutlineColor (描边颜色, Color) (0,0,0,1) // 高光 _SpecularColor (高光颜色, Color) (1,1,1,1) _SpecularGloss (高光光泽度, Range(1, 256)) 32 _SpecularThreshold (高光阈值, Range(0, 1)) 0.9 _SpecularIntensity (高光强度, Range(0, 2)) 1 // 自发光可选用于眼睛、魔法特效等 _EmissionMap (自发光贴图, 2D) black {} _EmissionColor (自发光颜色, Color) (0,0,0,1) _EmissionIntensity (自发光强度, Range(0, 10)) 1 }3.2 描边Pass的实现细节描边Pass需要正面剔除并沿顶点法线方向膨胀。这里采用视图空间膨胀法其优点是描边宽度在屏幕上看起来是均匀的不会因为物体远近而改变。SubShader { Tags { RenderTypeOpaque QueueGeometry} // Pass 1: 描边 Pass { Name OUTLINE Cull Front // 剔除正面只渲染背面膨胀出来的部分 CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; fixed4 color : COLOR; }; float _OutlineWidth; fixed4 _OutlineColor; v2f vert (appdata v) { v2f o; // 将法线和顶点变换到视图空间 float3 viewNormal mul((float3x3)UNITY_MATRIX_IT_MV, v.normal); float3 viewPos UnityObjectToViewPos(v.vertex); // 归一化视图空间法线并沿其方向膨胀顶点 viewPos normalize(viewNormal) * _OutlineWidth; // 将膨胀后的视图空间坐标变换到裁剪空间 o.pos mul(UNITY_MATRIX_P, float4(viewPos, 1.0)); o.color _OutlineColor; return o; } fixed4 frag (v2f i) : SV_Target { return i.color; // 直接输出描边颜色 } ENDCG }3.3 主体着色Pass整合光影与高光这是着色器的核心计算漫反射、阶调映射、高光和自发光。// Pass 2: 主体着色 Pass { Name MAIN Tags { LightModeForwardBase } // 使用前向渲染路径的基础光照 CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase // 编译前向渲染所需变体 #include UnityCG.cginc #include Lighting.cginc #include AutoLight.cginc struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; SHADOW_COORDS(3) // 声明阴影坐标宏 }; // 属性变量声明 sampler2D _MainTex; float4 _MainTex_ST; sampler2D _RampTex; sampler2D _EmissionMap; fixed4 _BaseColor; fixed4 _ShadowColor; fixed4 _SpecularColor; fixed4 _EmissionColor; float _RampSmoothness; float _SpecularGloss; float _SpecularThreshold; float _SpecularIntensity; float _EmissionIntensity; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.worldNormal UnityObjectToWorldNormal(v.normal); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; TRANSFER_SHADOW(o); // 传输阴影数据 return o; } fixed4 frag (v2f i) : SV_Target { // 准备向量 float3 worldNormal normalize(i.worldNormal); float3 worldLightDir normalize(_WorldSpaceLightPos0.xyz); float3 worldViewDir normalize(_WorldSpaceCameraPos.xyz - i.worldPos); // 基础纹理和颜色 fixed4 mainTex tex2D(_MainTex, i.uv); fixed4 albedo mainTex * _BaseColor; // 1. 计算基础漫反射光照 float diffuse max(0, dot(worldNormal, worldLightDir)); // 应用阴影衰减 float shadow SHADOW_ATTENUATION(i); diffuse * shadow; // 2. 阶调映射使用smoothstep实现平滑过渡 float rampUV diffuse; // 使用阶调纹理采样如果没有则用smoothstep模拟 #ifdef _RAMP_TEX_ON fixed3 ramp tex2D(_RampTex, float2(rampUV, 0.5)).rgb; #else float threshold 0.5; // 默认阈值 float smoothMin saturate(threshold - _RampSmoothness); float smoothMax saturate(threshold _RampSmoothness); float ramp smoothstep(smoothMin, smoothMax, diffuse); #endif // 混合亮部和暗部颜色 fixed3 diffuseColor lerp(_ShadowColor.rgb, fixed3(1,1,1), ramp) * albedo.rgb; // 3. 计算风格化高光 (Blinn-Phong模型) float3 halfDir normalize(worldLightDir worldViewDir); float specularDot max(0, dot(worldNormal, halfDir)); float specular pow(specularDot, _SpecularGloss); float toonSpecular step(_SpecularThreshold, specular) * _SpecularIntensity; fixed3 specularColor toonSpecular * _SpecularColor.rgb; // 4. 自发光 fixed3 emission tex2D(_EmissionMap, i.uv).rgb * _EmissionColor.rgb * _EmissionIntensity; // 5. 最终颜色合成 fixed3 finalColor diffuseColor specularColor emission; // 应用环境光 fixed3 ambient UNITY_LIGHTMODEL_AMBIENT.rgb * albedo.rgb; finalColor ambient; return fixed4(finalColor, albedo.a); } ENDCG } } FallBack Diffuse }注意事项这个基础着色器没有处理多光源。在复杂的场景中你可能需要额外的Pass如ForwardAdd来处理额外的逐像素光或者考虑使用URP/HDRP的Lit Shader作为模板进行修改它们内置了多光源支持。4. 进阶效果与性能优化有了基础框架我们就可以在此基础上添加更多“惊艳”的效果同时也要考虑如何在移动端或低配设备上稳定运行。4.1 实现“二号影”与“天使环”二号影这是日式卡通渲染中常见的概念指在物体背光侧边缘的一圈柔和的冷色光晕。它模拟了环境光反射或天空光的影响能极大地增加角色的立体感和通透感。实现方法通常基于法线方向N和视角方向V的点积。当视角几乎与表面平行时即dot(N, V)接近0给一个额外的颜色叠加。可以用fresnel效应公式来模拟。float rim 1.0 - saturate(dot(worldNormal, worldViewDir)); float rimLight pow(rim, _RimPower) * _RimIntensity; finalColor rimLight * _RimColor.rgb * (1.0 - diffuse); // 通常在暗部更明显天使环特指在头发高光上出现的、与发丝走向垂直的亮色条纹是很多动漫角色头发的标志性效果。实现方法这需要一张额外的贴图通常叫Hair Mask或Flow Map来指示头发的流向。在着色器中根据流向计算一个各向异性的高光。一种常见技巧是使用tangent切线方向来代替法线进行高光计算或者使用dot(T, H)切线与半角向量的点积。4.2 使用Shader Graph可视化构建对于不熟悉ShaderLab代码的开发者Unity的Shader Graph是绝佳的可视化工具。构建卡通着色器的逻辑是完全相通的创建PBR Graph以URP的PBR Master节点为基础进行修改。构建阶调使用Dot Product节点计算Normal和Light Direction然后通过一个Remap或Sample Gradient节点模拟Ramp Texture将其离散化最后连接到Base Color。添加描边URP通常使用Render Objects渲染器特性来添加后处理描边或者在Shader Graph中通过Normal和Position节点实现一个简单的屏幕空间效果。对于高质量的背面膨胀描边可能仍需配合自定义渲染管线或额外的Pass。风格化高光用Dot Product计算Normal和Half Vector通过Power节点控制光泽度再用Step节点进行阈值化最后连接到Specular输入。实操心得Shader Graph非常适合快速原型设计和参数调试所有参数都可以直观地拖动滑块观察效果。但对于极其复杂、需要精细控制的卡通效果如基于平滑法线的描边手写ShaderLab代码仍然具有不可替代的灵活性。4.3 性能优化关键点卡通着色器也可以很耗性能尤其是在移动平台。以下是几个优化方向减少纹理采样将Ramp Tex、Specular Mask等控制纹理合并到一张贴图的不同通道中如RGBA四个通道分别存储不同的信息。简化计算在片段着色器中进行的pow、smoothstep等计算是性能大户。如果效果允许可以考虑在顶点着色器中计算光照因子并进行插值。使用Shader变体Shader Variants和关键字Keywords通过#pragma multi_compile或shader_feature来编译不同功能组合的变体。例如为“有无描边”、“有无高光”、“有无自发光”分别设置开关让美术人员可以根据模型重要性选择不同复杂度的着色器变体避免为所有模型支付不必要的计算开销。针对移动平台优化使用half或fixed精度变量代替float避免在片段着色器中使用复杂的循环和分支充分利用移动GPU的TBDRTile-Based Deferred Rendering架构特性减少过度绘制。5. 常见问题排查与调试技巧即使按照指南操作你也可能会遇到各种奇怪的问题。这里记录了一些我踩过的坑和解决方法。5.1 描边问题排查表问题现象可能原因解决方案描边闪烁或抖动描边宽度计算在裁剪空间或屏幕空间不稳定。确保在视图空间进行顶点膨胀如示例代码所示这样描边宽度在屏幕上恒定。描边在特定角度断裂背面膨胀时模型某些部位如内凹处的膨胀方向错误导致描边向内收缩。1. 检查模型法线是否正确使用建模软件或Unity的“显示法线”功能。2. 考虑使用平滑法线技术为描边提供一套独立且向外扩张的法线数据。描边穿透前景物体描边Pass的深度写入ZWrite和深度测试ZTest设置不当。描边Pass的ZTest可以设置为Less或LEqual确保描边不会画在更近的物体前面。有时需要将Offset参数调大让描边在深度上稍微“退后”一点。透明物体描边错误背面膨胀法对透明物体Alpha Blend不友好因为描边会与内部颜色混合。对于透明物体考虑使用后处理描边或基于几何着色器的方案。如果必须用背面膨胀需仔细处理渲染队列Queue和混合模式。5.2 光影与颜色问题光影分界处有锯齿Aliasing原因使用step函数导致光照在阈值处发生突变在低分辨率下锯齿明显。解决换用smoothstep函数并提供一个小的平滑区间_RampSmoothness。这个区间不宜过大否则会失去卡通感。物体看起来扁平缺乏立体感原因阶调分层太少只有明暗两阶或者阴影颜色_ShadowColor与亮部颜色对比度不够。解决使用一张包含更多颜色层次的Ramp Texture。尝试在暗部加入一些冷色调如蓝色、紫色在亮部加入暖色调如黄色模拟环境光反射能立刻增加体积感。高光区域太“硬”或不自然原因高光阈值_SpecularThreshold过高或光泽度_SpecularGloss过低导致高光区域过大过散。解决结合使用高光遮罩贴图。在贴图中白色区域表示允许出现高光黑色区域则抑制高光。这样你可以精确控制高光只出现在鼻尖、嘴唇、头发丝等特定部位。5.3 与Unity渲染管线的兼容性在URP/HDRP中不工作内置渲染管线Built-in的Shader不能直接在URP/HDRP中使用。你需要使用URP/HDRP提供的Shader模板创建新的着色器。将光照计算改为使用URP的Lighting.hlsl库函数如GetMainLight()。描边实现可能需要通过Render Objects渲染器特性或自定义渲染通道来完成。接收不到阴影确保在v2f结构体中正确声明了SHADOW_COORDS在顶点着色器中调用了TRANSFER_SHADOW在片段着色器中调用了SHADOW_ATTENUATION。并且物体的MeshRenderer要启用Cast Shadows和Receive Shadows。调试Shader时善用Unity的Frame Debugger和RenderDoc这类图形调试工具。它们可以让你看到每一个Draw Call的渲染状态、输入的顶点数据和输出的像素颜色是定位渲染问题的终极利器。6. 从技术到艺术调出属于自己的风格技术实现只是骨架参数调节才是赋予角色灵魂的血肉。一套好的卡通着色器参数需要美术和技术的紧密配合。参考与分解找到你想要的风格参考图例如某部动画或游戏截图。用你的眼睛做“逆向工程”它的光影分几层阴影是什么颜色偏蓝偏紫描边是均匀的黑线还是根据部位有粗细和颜色变化高光是什么形状出现在哪里分层调试在Unity中不要一次性调节所有参数。可以先把高光、天使环等效果关掉只调漫反射阶调。调出满意的体积感后再打开高光微调其强度和形状。最后再加上边缘光等“调味”效果。建立材质库为不同的材质类型皮肤、布料、金属、头发创建不同的材质球预设。皮肤的阶调过渡可以柔和一些_RampSmoothness稍大阴影偏红润金属的高光可以更锐利、阈值更高头发的阶调可以对比强烈并启用天使环效果。光照环境配合卡通渲染对场景光照非常敏感。尽量使用方向光作为主光源避免使用过多的实时点光源。可以大量使用烘焙光照贴图Lightmap来提供细腻的全局光照和阴影再用一个方向光提供关键的实时高光和动态阴影。三点布光法主光、补光、背光在卡通渲染中同样有效背光能很好地勾勒出“二号影”效果。最终一个惊艳的卡通渲染效果是精确的技术实现与感性的艺术审美结合的产物。它没有唯一的标准答案但通过本指南拆解的核心组件——离散化的阶调、清晰的描边、风格化的高光以及各种增强立体感的技巧——你已经掌握了所有必要的工具。剩下的就是在项目中不断实验、调试和积累感觉。记住最好的学习方式永远是动手实现它然后打破它再看如何重建得更好。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门