1. 卡通渲染在UE里到底难在哪很多人第一次在Unreal里尝试卡通风格化渲染都会经历一个相似的困惑材质编辑器里明明调出了不错的色块效果一放进场景就发现光影完全不对味。要么是明暗交界线糊成一团要么是描边粗细随距离疯狂变化再或者角色一动起来高光位置就跟着乱跑。这些问题的根源其实不在于某个节点连错了而在于卡通渲染和UE默认的PBR管线在底层逻辑上就是两套东西。UE的默认光照模型是基于物理的它追求的是能量守恒和真实感。漫反射用Lambert高光用GGX阴影用级联阴影贴图加距离场环境光遮蔽。这套东西做写实场景非常强但卡通渲染要的是“人为控制的色阶”和“清晰的轮廓线”它不追求物理正确追求的是视觉上的干净和可控。所以做卡通渲染的第一步不是去调材质而是先想清楚你要在哪一层去干预UE的渲染管线。常见的干预层级有三个。最浅的一层是纯后处理用PostProcessMaterial做边缘检测和颜色量化好处是不动原始材质坏处是描边容易受深度和法线精度影响细碎物体上全是噪点。中间一层是自定义光照模型通过Material里的ShadingModel或者LightFunction去改漫反射和高光的计算方式这是目前最主流的做法灵活度高性能也可控。最深的一层是改渲染管线本身用CustomMeshPass或者SceneViewExtension去插入自己的绘制阶段这适合做大规模的风格化场景但门槛高调试成本也大。我个人的经验是百分之八十的卡通渲染需求在自定义光照模型这一层就能解决。你不需要去动引擎源码也不需要写复杂的全局着色器只要把材质的ShadingModel改成Unlit然后在Emissive通道里自己算光照就能得到非常干净的色阶效果。这个思路听起来简单但它绕开了UE默认光照模型里那些为写实服务的复杂计算让你对最终画面的控制力强了很多。当然Unlit方案也有它的代价。你失去了引擎自带的阴影投射和接收能力需要自己用LightVector和ShadowMap去算阴影。而且Unlit材质不参与延迟渲染的GBuffer写入如果你后面还要做SSR或者SSAO就得额外处理。所以到底选哪条路取决于你的项目是角色特写为主还是大场景漫游为主。角色特写用Unlit加自定义阴影完全够用大场景的话还是建议在默认光照模型基础上做改造。2. 色阶分层与明暗交界线的控制逻辑卡通渲染最核心的视觉特征就是明暗之间不是平滑过渡而是被切成几段明确的色阶。这个切分动作在数学上就是一个阶梯函数。UE材质编辑器里没有现成的Step节点但你可以用Floor或者Ceil配合除法来实现。具体做法是先把光照结果归一化到0到1之间然后乘以你想要的色阶层数取整之后再除回去。比如你要三层色阶就是Floor(LightResult * 3) / 3。这样得到的结果就只有0、0.33、0.66、1这四个值明暗交界线就出来了。但这里有个细节很多人会忽略直接对光照结果做阶梯化在明暗交界处会产生很硬的锯齿。因为光照是逐像素计算的相邻像素的光照值可能刚好跨过阶梯阈值一个像素在亮部旁边一个像素就掉到暗部视觉上就是一条狗牙边。解决办法是在阶梯化之前先对光照结果做一个平滑处理或者用SmoothStep在阈值附近做一个窄范围的过渡。我通常会在阶梯化之后再用一个Lerp把原始光照和阶梯化结果按一定比例混合混合系数控制在0.1到0.2之间既能保留色阶感又能让边缘不那么刺眼。明暗交界线的位置控制是另一个需要精细调节的地方。在真实光照里交界线位置由光源方向和表面法线决定但在卡通渲染里你往往希望交界线能按照美术的意图偏移。比如角色脸部你希望交界线刚好落在颧骨下方而不是被鼻子的阴影打断。这时候就需要引入一张控制贴图用它的R通道去偏移光照计算的阈值。这张贴图可以是手绘的也可以用顶点色烘焙。我见过一些项目直接用角色的法线贴图反相来做偏移效果也不错但可控性不如手绘贴图。还有一个容易被忽视的点是环境光的处理。UE的默认环境光会参与漫反射计算导致暗部不够暗色阶感被冲淡。做卡通渲染时通常要把环境光对漫反射的贡献降到很低或者干脆在材质里把环境光项去掉只保留一个固定的暗部颜色。这个暗部颜色不要用纯黑用主色调的补色或者深紫色会更有卡通感。我在一个项目里试过用深蓝色做暗部角色看起来就像被月光照着氛围感一下子就出来了。色阶的数量也不是越多越好。两层色阶适合极简风格三层是最常见的四层以上就开始接近写实了卡通感反而会减弱。如果你做的是日式动画风格三层色阶加一个高光层基本就够了。高光层单独用一张贴图控制形状不要用光照计算出来的高光因为计算出来的高光形状太随机不符合动画里高光要“画”出来的逻辑。3. 描边方案的取舍法线外扩还是后处理描边是卡通渲染的另一半灵魂。没有描边的卡通角色看起来就像没勾线的草稿。UE里做描边主流方案就两种法线外扩和后处理边缘检测。这两种方案各有各的坑选哪个取决于你的模型精度和性能预算。法线外扩的原理很简单把模型顶点沿着法线方向往外推一点用一个只渲染背面且不写深度的材质画出来就形成了一圈轮廓。这个方案的优点是描边粗细均匀不受场景深度影响而且可以很方便地用法线贴图或者顶点色去控制局部粗细。缺点是遇到硬边模型会裂开因为硬边的顶点法线是分裂的外扩之后每个面各走各的描边就断了。解决办法是在建模阶段就把法线平滑好或者在引擎里用SmoothNormal工具重新计算一套平滑法线存到UV通道里。我一般会在导入模型时勾选“Recompute Normals”然后用一个简单的材质函数把平滑法线写进顶点色描边材质读顶点色而不是读原始法线。法线外扩的另一个坑是描边在屏幕空间里的粗细会随距离变化。离相机远的时候外扩的顶点在屏幕上移动的距离变小描边就变细了。如果你想要恒定屏幕粗细的描边需要在材质里根据相机距离去动态调整外扩量。具体做法是用相机到顶点的距离除以一个系数再乘到外扩方向上。这个系数需要根据你的相机FOV和分辨率去调没有万能值。我通常会在材质里暴露一个参数让美术在场景里实时调。后处理描边的原理是在屏幕空间做边缘检测通常用Sobel算子或者Roberts算子去检测深度和法线的突变。这个方案的优点是描边不会裂而且可以做出粗细变化丰富的效果比如根据深度调整粗细近处粗远处细。缺点是容易在细碎物体上产生噪点比如树叶、毛发、栅栏这些地方深度和法线变化剧烈边缘检测会疯狂触发。解决办法是加一个深度阈值和法线阈值只有超过阈值的边缘才画描边。但阈值调高了会漏掉一些该有的描边调低了又全是噪点需要反复试。我个人的选择是角色和主要道具用法线外扩场景和背景用后处理描边。这样角色描边干净可控场景描边又有深度变化。两者结合的时候要注意渲染顺序法线外扩的描边材质要设置成不写深度、不读深度后处理描边要在所有不透明物体之后执行。如果场景里有半透明物体后处理描边还要决定要不要对半透明物体生效这个就看具体需求了。还有一个取巧的做法是用UE的MeshDrawCommand或者CustomDepth通道来做描边。把需要描边的物体渲染到CustomDepth然后在后处理里对这个深度图做边缘检测。这个方案的好处是描边和物体本身解耦你可以自由控制哪些物体有描边哪些没有。坏处是CustomDepth的精度有限远距离物体的描边会闪烁。我一般会把CustomDepth的精度调到最高然后配合一个距离衰减来缓解闪烁。4. 高光与边缘光的风格化处理卡通渲染里的高光和写实渲染里的高光完全是两码事。写实高光是光源在粗糙表面上的物理反射形状由法线和粗糙度决定。卡通高光则是美术画上去的它要的是形状可控、位置可控、大小可控。所以做卡通高光不要用UE默认的Specular通道那个是给物理高光用的。你应该在Emissive通道里自己算一个高光形状出来。最常用的卡通高光算法是Blinn-Phong的变体先算半角向量然后和法线点乘得到一个高光强度再用一个阈值把它切成硬边。这个阈值可以用一张高光贴图来控制贴图上画什么形状高光就是什么形状。贴图的UV通常用模型的UV但有时候为了高光跟着相机走也会用屏幕空间UV或者视角空间UV。我见过一些项目用MatCap来做高光效果很稳定因为MatCap本身就是一张预渲染的球面光照图高光形状完全由贴图决定不受场景光照影响。边缘光也叫Rim Light是卡通渲染里用来增强轮廓感的另一个手段。它的原理是当法线垂直于视线时边缘光最强。计算方法是1 - dot(Normal, ViewDir)然后取幂次控制边缘光的宽度。边缘光通常加在Emissive通道里颜色可以和环境光互补也可以和主光同色。我一般会把边缘光和高光分开控制高光用高光贴图边缘光用顶点色或者单独的一张遮罩贴图。这样美术可以单独调整每个部分的边缘光强度比如头发边缘光强一点身体边缘光弱一点。边缘光的一个常见问题是会在角色正面也出现因为正面法线和视线夹角小按理说边缘光应该弱但如果模型法线不平滑正面也会出现边缘光。解决办法是用一个Fresnel节点把边缘光限制在法线变化剧烈的区域。或者用一张遮罩贴图把正面区域遮掉。我通常会在材质里加一个Saturate(dot(Normal, ViewDir))的项用它去乘边缘光这样正面边缘光自然就弱了。高光和边缘光的颜色不要用纯白。纯白在卡通渲染里会显得很突兀像贴上去的。用主色调的浅色或者带一点环境色倾向的白色会自然很多。比如角色主色是红色高光可以用浅粉色主色是蓝色高光可以用浅青色。边缘光同理用主色的补色或者邻近色能增强画面的色彩层次。我在一个项目里试过用暖色边缘光配冷色主光角色看起来就像被夕阳照着氛围感很强。还有一个细节是高光和边缘光要不要受阴影影响。在写实渲染里阴影里的高光会被遮挡但在卡通渲染里高光通常是画上去的不应该被阴影影响。所以计算高光时不要乘阴影因子。边缘光也一样它本身就是用来强调轮廓的如果被阴影遮掉就失去意义了。这一点在材质里要注意不要把阴影计算的结果混进高光和边缘光的通道里。5. 材质函数封装与性能优化当你把色阶、描边、高光、边缘光都调通之后下一步就是把这些东西封装成可复用的材质函数。这一步很重要因为卡通渲染的参数很多如果每个材质都从头连一遍不仅效率低而且容易出错。封装成材质函数之后美术只需要暴露几个关键参数比如色阶层数、描边颜色、高光强度、边缘光宽度就能快速应用到新模型上。封装的时候要注意参数的命名和分组。我通常会把参数分成三组基础色组、光照组、描边组。基础色组包括主色、暗部色、高光色光照组包括色阶层数、明暗阈值、边缘光宽度描边组包括描边颜色、描边粗细、描边距离衰减。每组参数在材质实例里会折叠在一起美术调起来很直观。参数命名不要用缩写用完整的英文单词比如BaseColor、ShadowColor、RimLightWidth这样别人一看就懂。性能优化是卡通渲染里容易被忽视的一环。因为卡通渲染通常用Unlit材质很多人就觉得Unlit一定比Lit快。其实不一定。Unlit材质虽然省掉了光照计算但如果你在Emissive里塞了太多纹理采样和数学运算它的开销可能比默认的Lit材质还大。我见过一个项目角色材质里采了八张纹理每张纹理都做了一遍UV变换最后在移动端上帧率直接掉到二十几。后来把纹理合并成两张图集UV变换也简化了帧率就回到了六十。纹理采样的优化原则是能合并就合并能不用就不用。色阶控制贴图、高光贴图、边缘光遮罩这三张图完全可以合并到一张RGB贴图里R通道做色阶控制G通道做高光B通道做边缘光遮罩。这样一次采样就能拿到三个信息比采三次省了三分之二的纹理带宽。UV变换也一样如果三张图用的是同一套UV就不要分别做变换先变换UV再采样或者干脆不变换。数学运算的优化主要是减少依赖纹理读取的运算。比如dot(Normal, LightDir)这种计算如果LightDir是固定的可以在顶点着色器里算好传给像素着色器。如果Normal是固定的可以烘焙到顶点色里。UE的材质编辑器里顶点着色器和像素着色器的分界不是很明显但你可以用VertexInterpolator节点把一些计算强制放到顶点阶段。这个节点在移动端上特别有用因为移动端的像素着色器开销比顶点着色器大得多。还有一个性能陷阱是描边的Overdraw。法线外扩的描边材质虽然简单但它是在物体背面渲染的如果场景里有很多重叠的物体描边的Overdraw会很高。解决办法是给描边材质加一个深度测试只渲染没有被遮挡的部分。或者用Stencil Buffer把描边限制在特定区域。UE里可以用Allow Custom Depth Writes配合后处理来做但配置起来比较麻烦。我通常会在项目设置里把描边的渲染队列调后一点让它在不透明物体之后渲染这样深度测试就能自然剔除掉被遮挡的描边。6. 从单角色到全场景的扩展思路单个角色的卡通渲染调好之后下一步就是把它扩展到整个场景。这一步的难度不在于技术而在于一致性。角色上的色阶、描边、高光参数放到场景物件上可能就不合适了。比如角色的描边粗细是2个像素放到一棵树上2个像素的描边可能细得看不见放到一栋建筑上2个像素又粗得像马克笔画的。所以场景物件的卡通渲染参数需要根据物件的尺寸和距离单独调整。我的做法是给场景物件分三类近景、中景、远景。近景物件用和角色一样的参数描边粗细2到3个像素色阶三层。中景物件描边减到1到2个像素色阶减到两层因为中景物件细节少三层色阶反而显得脏。远景物件干脆不要描边色阶也只用一层靠颜色和雾效来区分层次。这样分下来整个场景的卡通感是统一的但每个距离段又有自己的细节层次。场景物件的材质不要每个都单独调。用Material Instance和参数集来管理。UE的Material Parameter Collection可以跨材质共享参数比如全局的描边颜色、全局的色阶层数。这样你改一个参数所有用了这个参数集的材质都会跟着变。我通常会把描边颜色、暗部色、高光色这三个参数放到参数集里色阶层数和描边粗细则根据物件类型单独设。这样既保证了大色调的统一又保留了单个物件的调整空间。光照环境的一致性也很重要。角色在室内和室外的卡通渲染效果应该是不一样的因为环境光变了。但如果你用的是Unlit材质环境光根本不影响材质角色在室内室外看起来一模一样这就很假。解决办法是在材质里加一个环境光探针的采样用探针的颜色去影响暗部色。UE的SkyLight节点可以拿到环境光颜色把它和暗部色做Lerp就能让角色在不同光照环境下有色调变化。这个变化不需要很强烈一点点就够了但有了它角色就和场景融合了。最后是后处理链的配合。卡通渲染通常需要配合一些后处理来增强风格感比如颜色分级、暗角、噪点。颜色分级可以把整体色调往暖色或者冷色偏暗角可以聚焦视线噪点可以增加手绘感。这些后处理不要加得太重一点点就够了。我见过一些项目后处理加得太猛画面糊成一团卡通感反而没了。后处理的顺序也有讲究颜色分级要在描边之前暗角和噪点要在描边之后这样描边不会被颜色分级影响暗角和噪点又能覆盖整个画面。还有一个扩展方向是动态天气和时间变化。卡通渲染的场景如果一直是同一个光照会显得很死板。你可以用UE的Time of Day系统去驱动主光方向和颜色然后让材质的暗部色和高光色跟着主光颜色走。这样早上是暖色光配冷色暗部傍晚是冷色光配暖色暗部场景就有了时间感。这个做法在开放世界项目里很常见技术难度不大但效果提升很明显。7. 调试工具与常见问题排查卡通渲染的调试比写实渲染要麻烦因为很多问题是“看起来不对”但说不清哪里不对。我一般会准备几个调试视图用来快速定位问题。第一个是光照结果视图把材质的Emissive直接输出成光照计算的原始结果看看色阶切分之前的数值分布。第二个是法线视图把世界空间法线输出成颜色检查法线有没有异常。第三个是描边视图把描边材质单独渲染出来看看描边有没有断裂或者过粗。色阶切分不干净是最常见的问题。表现是明暗交界处有噪点或者渐变。原因通常是光照结果在阈值附近抖动。解决办法是在阶梯化之前先对光照结果做一个模糊或者用SmoothStep在阈值附近做一个窄过渡。如果还是不行就检查一下光照计算里有没有用到屏幕空间导数比如ddx、ddy这些在移动端上精度很低容易产生噪点。把导数计算换成解析计算或者干脆去掉通常能解决。描边断裂通常发生在硬边模型上。检查一下模型的法线是不是分裂的如果是就在建模软件里把法线平滑好或者在UE里用SmoothNormal重新计算。如果模型法线没问题就检查描边材质的外扩方向是不是用了顶点法线。有些模型导入UE后法线会被重新计算导致和建模软件里的不一致。可以在导入设置里把Normal Import Method改成Import Normals保持和建模软件一致。描边闪烁通常发生在后处理描边上。原因是深度图精度不够远距离物体的深度值变化很小边缘检测就检测不到。解决办法是提高CustomDepth的精度或者给后处理描边加一个距离衰减远距离物体不画描边。如果闪烁发生在法线外扩描边上那可能是外扩量太小顶点在屏幕上移动的距离不到一个像素。把外扩量调大一点或者根据相机距离动态调整外扩量就能解决。高光位置不对通常是高光计算里用了错误的向量。检查一下半角向量是不是用LightDir ViewDir算的法线是不是世界空间法线。如果高光跟着相机转那可能是用了视角空间法线改成世界空间法线就好。如果高光形状不对检查一下高光贴图的UV是不是和模型UV匹配贴图有没有被压缩或者拉伸。性能问题通常出在纹理采样和Overdraw上。用UE的Shader Complexity视图看一下红色区域就是开销大的地方。如果角色材质是红色的就检查纹理采样次数能合并的合并能去掉的去掉。如果场景是红色的就检查描边的Overdraw给描边材质加深度测试或者用Stencil Buffer限制描边区域。移动端上还要注意材质里的数学运算尽量用VertexInterpolator把计算放到顶点阶段。还有一个隐蔽的问题是材质实例的编译。卡通渲染的材质函数通常很复杂如果每个材质实例都单独编译编译时间会很长。解决办法是把材质函数里的分支和静态开关都用StaticSwitch节点这样不同的配置会编译成不同的Shader变体运行时切换不需要重新编译。但StaticSwitch用多了会导致Shader变体爆炸编译时间反而更长。我一般只把最影响性能的开关做成StaticSwitch比如描边开不开、色阶层数变不变其他的都用动态分支或者参数控制。最后说一个我踩过的坑卡通渲染的材质在编辑器里预览正常打包之后效果不对。原因通常是材质里用了编辑器专用的节点比如DebugScalar、DebugVector这些节点在打包后会被优化掉。或者用了SceneTexture节点但打包后SceneTexture的配置和编辑器里不一样。解决办法是在打包前把所有调试节点去掉SceneTexture的配置在项目设置里统一好。打包之后一定要在真机上跑一遍编辑器里的效果和真机上的效果经常有差异尤其是移动端。