
按下启动按钮那一刻我倒不是怕它崩而是怕镜头里的东西看起来还是那股“塑料味”。项目做了大半年我们给角色和场景重新搭了一套风格化渲染系统目标很直接让画面有手绘质感同时还能在普通移动设备上稳定跑起来。这套系统最终覆盖了角色、场景、特效三类对象的着色统一了描边、色块光照、脸部阴影修正和后期风格化处理几条链路。如果你也在做卡通渲染、动漫感场景或者手头有一套写实PBR流程想往风格化转这篇文章值得往下看。我先把话说明白这不是一篇贴代码式的Shader教程而是从一个完整渲染系统的角度讲清楚风格化渲染有哪些关键决策点、每个决策背后的理由以及我们在实际落地过程中踩过的坑。这中间有架构拆分、有具体Shader实现思路、有参数调试记录也有DCC工具链的配合规范尽量做到你拿着就能往自己项目里套。1. 这个渲染系统到底在解决什么问题1.1 写实渲染和风格化渲染的根本区别写实渲染的目标是模拟物理世界里光的行为PBR管线里的漫反射、镜面反射、环境光遮蔽、次表面散射本质上都是在拟合“真实感”。但风格化渲染的目标完全不同它追求的是“画出来的感觉”是人对物体的视觉概括。所以NPRNon-Photorealistic Rendering不是做得不够好的PBR而是另一种独立的视觉语言。举个例子同样一个金属茶壶PBR要求高光形状符合真实光源、粗糙度参数对应真实的微表面分布但风格化渲染可能只需要一个清晰的白色高光块边缘带一点硬切中间过渡直接砍掉。这不是“粗糙度调低了”能调出来的而是整个光照模型都换了。所以做风格化渲染系统第一件事是别在PBR的参数空间里凑效果。你会发现怎么调都有一股“塑料感”因为底层模型还在响应基于物理的反射方程它天生就带着真实感信息。你要做的是改动光照计算本身而不是给参数打补丁。1.2 参考案例与需求拆解我们当时手里拿到的参考图集中在三类一是《蜘蛛侠平行宇宙》那种漫画网点风格强调半调网点和硬边描边二是类似《原神》的角色渲染面片层次干净脸部和头发有明显的手绘阴影分界三是《双城之战》那种带笔触感、色块边缘略有不规则抖动的风格。这三类视觉语言差异很大不能用一个Shader通吃。所以系统从一开始就按“可组合”来设计描边强度、色块阈值、边缘软化、网点密度、脸部阴影修正全部做成可调参数而不是写死在某一个效果里。这样同样一套渲染系统换一套参数就能从干净赛璐璐风格切到带笔触的厚涂风格。需求端还有一个硬约束项目要上手机端且目标帧率不低于60。这就意味着全屏后处理特效必须克制描边不能依赖高成本的图像边缘检测光照模型要尽量控制在几次运算之内。后面很多技术选型都是在这个性能框架下做出的。1.3 为什么不能直接拿现成卡通Shader对付Asset Store里有很多卡通Shader也做得挺好看但你把它丢进一个真实项目时往往撞墙。首先是批量渲染的兼容性问题一套角色可能有十几件装备每件装备换不同颜色的高光区域如果Shader没有设计好的顶点色或遮罩通道你就得复制材质球DrawCall直接翻倍。其次是光照响应。很多现成卡通Shader只写了一个简单的半兰伯特加阈值场景里一旦出现多光源、实时光影、雾效角色身上的色块就会乱闪。更麻烦的是这类Shader通常不考虑角色脸部的特殊修正也不管模型布线是否规整拿来做Demo可以放进生产管线就是灾难。所以我们决定自己搭一套。不是觉得现成的都不行而是我们需要一个能跟着项目美术规范长期迭代的底座。这个底座允许TA在不同角色之间做差异允许场景特效共享同一套描边逻辑也允许美术后期在材质面板上一眼找到“阴影阈值”而不是去翻代码。2. 系统架构风格化渲染管线怎么拆2.1 按Pass拆分描边、光照、后处理整套系统在渲染流程上分成三个大阶段Base Pass负责主要着色包括色块光照、Ramp采样、脸部和头发的特殊处理描边Pass负责生成轮廓线和内部硬边最后的后处理阶段负责网点、边缘光、颜色分级这类需要全屏像素操作的风格效果。这个拆法的核心好处是互不干扰。描边如果有问题你不用去动主着色器后处理要砍掉换一种风格也只要撤销一个Renderer Feature。每个Pass是独立的小单元各自有输入输出约定出错时能快速定位到具体环节。有一点容易被忽略描边Pass和Base Pass之间还存在一个深度关系。描边一般是在主物体旁边膨胀出一个背面渲染的壳如果描边Pass写进深度缓冲就会把本该透出的物体挡住。所以描边Pass通常在深度测试上做特殊处理要么不写深度要么深度偏移到最远保证它永远贴在自己主体表面之下。2.2 数据流和渲染顺序渲染顺序上是先不透明物体、再透明物体、最后是特效。风格化系统里角色、场景、特效的描边宽度和风格参数是不同的所以每个Pass会通过一个全局的Renderer Feature统一注入参数而不是每个Shader各自查参数这样能保证同一帧里大家用的风格参数是一致的。透明物体是个特殊类别比如头发片、布料轻纱、半透明特效。透明物体不能简单按顺序排因为它们之间还有遮挡关系。我们的做法是给透明物体单独开了一个队列按与摄像机的距离排序然后描边Pass对透明物体使用更保守的宽度衰减避免从边缘漏出难看的粗线。数据流上还有一层法线。主着色器需要顶点法线描边需要法线后处理如果需要重建法线还得存法线缓冲。这层数据的来源和精度在整个系统里是统一约定好的模型导出时必须平滑组正确法线不能被压缩得过于粗糙否则同一套参数在不同模型上表现会差异很大。2.3 风格化参数从哪来顶点色、贴图、材质球一个很关键的设计决策是风格化参数到底放在哪里。我们同时用了三种通道每种有自己的用途。顶点色主要用于控制描边宽度变化和局部的颜色微调比如衣服上的缝线区域描边要细一点角色脸部轮廓描边要柔一点不需要额外贴图直接用顶点色通道省资源。贴图通道用于Ramp曲线采样、脸部阴影SDF、头发高光Mask这些需要更多自由度的地方。比如脸部阴影修正通常用一张SDF贴图存的是从眉心到脸颊的深度梯度这样美术可以调整阴影分界线位置到想要的地方而不是依赖模型法线算出来的结果。材质球参数则是最直观的那一层包括阴影阈值、软边范围、高光强度、描边粗细这类全局数值。三个渠道各有侧重顶点色适合空间上连续变化贴图适合表达复杂形状材质参数适合跨角色快速调整。它们最终会在Shader里被乘到一起或者分别采样组合出最终效果。3. 核心效果逐个拆解与实现3.1 描边外描边与轮廓线的取舍描边是风格化渲染辨识度最高的效果之一也是最容易做砸的地方。我们最终采用的是外描边方案在正常渲染之后把模型沿着法线方向向外膨胀一点然后渲染背面并剔除正面。因为只有背面片元会被画出来膨胀后露出的外圈就是轮廓线。Shader逻辑大致是这样// 顶点着色器沿法线外扩并缩放 float3 expandedPos vertexPos vertexNormal * _OutlineWidth; float4 clipPos TransformObjectToHClip(expandedPos);这段代码看起来简单实际操作里问题很多。首先是膨胀方向如果模型有尖锐凹角或者法线方向不一致外扩以后描边会出现断头和交叉。解决办法是预计算时把法线在模型空间里做一遍平滑也就是把邻接面片的法线做面积加权和只保留平滑后的方向用于描边。其次是描边的深度表现。角色转身时描边会从正面绕到背面如果描边始终写进深度正面轮廓线就会和背景物体互穿。我们加了深度淡出和距离衰减两个参数离摄像机越近描边越细离得远就稍微加粗一点保住轮廓存在感。这个数值不固定需要按场景和镜头距离试几轮。内部硬边比如衣服的褶皱分割线、眼睛轮廓用外描边做不出来我们单独开了一个额外贴图通道把需要硬边的区域标红然后在这部分区域做一个基于视线方向的内描边。其实就是计算NdotV够尖锐的夹角就压暗一点形成一个类似线条的效果。3.2 色块化光照从半兰伯特到硬阈值主着色的核心是把连续的光照过渡压成色块。我们用的光照模型本身还是半兰伯特但输出的亮度值会经过一个阈值函数把中间带切分成几个离散的层级。// 半兰伯特光照基础 float halfLambert dot(worldNormal, mainLightDir) * 0.5 0.5; // 用Ramp图采样得到卡通化亮度 float3 toonColor tex2D(_ToonRamp, float2(halfLambert, 0.5)).rgb;Ramp图即便是一张1D高度图我们也按2D坐标采样因为可以顺便在纵向维度存多套风格变体。正式项目里我把Ramp图做成了三行第一行是干净赛璐璐三段式暗、中、亮第二行是带一点暖暗部过渡的厚涂感第三行是高对比硬切版本。切换风格时只要改材质面板上的UV偏移量不用换Shader。真正容易出问题的是高光。写实模型的高光柔和风格化模型的高光通常是一块带硬边的亮点。我们给高光单独设了阈值和边缘软化距离数值小的会缩成一个小圆点适合表现眼神高光数值大了会变成一个扇形区域适合金属物件上的冷光条。这个阈值对数值精度很敏感甚至不同贴图压缩格式都会影响结果调试时建议先把贴图压缩换成RGBA32定位问题后再压回去。3.3 脸部阴影与手绘贴图的配合角色脸是风格化渲染里最微妙的部分也是最容易“翻车”的地方。真实光照下角色的鼻翼和下巴会产生复杂的投影这在写实脸上自然但赛璐璐风格的脸通常只有一块干净的下颌阴影。我们用SDF贴图做修正把脸的阴影区域在贴图里预先画好光照方向再决定这块阴影是出现还是消失。实际操作上SDF贴图存的是“从脸部中心向外围的最近距离”。当主光源从左侧照过来我们取贴图右侧的梯度用光照角度控制采样位置阴影就会沿着脸颊平滑地偏移。这个方案不需要模型有特别密的布线美术在Photoshop里画两笔就能看到效果是非常适合风格化项目的实用解。头发高光也是专门写的。普通PBR的高光会随着视角跨整个发梢闪耀这不符合手绘头发的分层逻辑。我们给头发加了Mask贴图高光只会出现在美术指定的纵向区域再配合一个能随角度蠕动的阈值边就能模拟出“一束光扫过发丝”的感觉。这里最怕的是Mask贴图画得太规整高光边缘露出生硬的锯齿解决方法是给阈值边加上大约0.03的噪声抖动。3.4 后处理边缘光、抖动与颜色分级后处理阶段我们不铺开做太多只保留了三个效果边缘光、颜色分级和可选的半调网点。边缘光是为了给角色从背景里“抠”出来用深度和法线差异检测轮廓叠加一层暖色或者冷色的边缘光晕。这个效果的强度必须很克制调得太高会让画面发灰发糊我们最终定的是0.18左右的强度。颜色分级是通过调整LUTLookup Table颜色查找表来统一画面氛围。美术在Photoshop里把一张基准帧调成他们想要的色调分离效果再导出LUT图运行时全屏替换颜色。这个过程在移动端开销极低却能让不同角色、不同场景共享同一套色彩语言。半调网点按需开启。网点边缘和带密度是两个关键参数密度太密会变成灰蒙蒙的噪点太稀又看不出漫画感。这个效果适合做特定剧情演出平时默认关闭。如果你在PC上跑还可以再加一点轻量模糊和高斯扰动模拟画布纹理但移动端我建议直接在Shader里用简单的伪随机抖动替代不然全屏模糊的开销承受不起。4. 实操落地从原型到可上线版本4.1 环境搭建与渲染器配置这套系统基于Unity URP实现版本是Unity 2022.3 LTS配URP 14。管线配置里需要注册一个自定义Renderer Feature用来向所有材质注入风格化参数。public class StylizedRendererFeature : ScriptableRendererFeature { StylizedRenderPass pass; public override void Create() { pass new StylizedRenderPass(); pass.renderPassEvent RenderPassEvent.AfterRenderingOpaques; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderer.EnqueuePass(pass); } }Shader方面用的HLSL手写没有用Shader Graph。Shader Graph在调颜色、接节点时确实快但风格化系统里要处理多个Pass、不同层级参数用代码维护更可控。这也是我给团队定的规矩试验性效果用Shader Graph上生产线的效果一律转成手写HLSL方便做版本diff和参数批量流转。渲染器配置里第二项是抗锯齿。风格化渲染特别怕看色块边缘那圈“狗牙”我们最终在移动端用MSAA 4x在PC端用TAA。MSAA对描边效果保值TAA会把边缘轻微抖动掉两者都不会破坏硬边色块的核心观感。这个选择在下面“常见问题”部分还会展开说。4.2 关键Shader片段与调参记录拿描边Width举例我保留了我们在正式版里用的一组参数方便你有个初始值可参考参数数值说明近景描边宽度0.02距离小于2米时使用远景描边宽度0.045距离大于10米时使用描边深度淡出12.0用于描边与背景深度差过大时消隐内部硬边强度0.4控制褶皱线明显程度边缘光强度0.18全屏边缘亮度阴影阈值0.35半兰伯特亮度低于此值进入暗部这组参数是反复调出来的。刚开始描边宽度全用同一个常量近景时线条粗得像炭笔涂鸦远景时又细到看不见。后来改成按距离线性插值近景细、远景粗才勉强稳住。有一个重要技巧描边宽度不要直接用世界空间固定值最好转到屏幕空间按像素宽度控制不然在不同分辨率下表现完全不一致。调色块阈值时我也有个体会阈值不能设得过于整数。阈值刚好是0.5时边缘会非常敏感任何细小的光照扰动都会导致大面积闪动。我们最终把阴影阈值放在0.35附近让暗部范围稍微大一点过渡反而更稳定。这个“不完美”的数值就是一种经验积累。4.3 性能预算与分级策略风格化渲染的性能优化和PBR不太一样它没有那么多高频采样但容易在Pass数量上失控。我们的目标基准机是骁龙778G级别一版全效果跑下来Base Pass加描边Pass加后处理大约7个DrawCall每帧可接受的范围内。为了这个目标我们做了三级性能分级。高画质档是全效果包括边缘光、LUT颜色分级和半调网点中档去掉半调网点边缘光降一半低档连边缘光都去掉描边宽度也减半只保留主着色和硬边描边。这个分级不是简单开关几个Shader Pass而是通过Renderer Feature里控制Uniform参数来切换所有分级设置都存在同一份配置表中。最耗的部分其实不是着色是描边Pass。描边把整个模型重新膨胀渲染一遍等于角色切换了两套顶点变换。我们试过用模板缓冲法的单Pass描边确实省半个DrawCall但写法比较复杂对SkinnedMeshRenderer容易出现三角面穿插。目前生产版本稳定用双Pass描边通过合批减少总体开销。4.4 DCC工具链模型、法线与贴图规范这套系统对美术资产是有要求的。风格化渲染里模型布线的重要性比PBR还要高。尤其描边它依赖法线方向面朝一个方向的平整区域描边会非常细转角处则会突然变粗。为了减少这种意外我们在DCC里对法线做了预处理把需要描边硬边的物体统一用加权法线烘焙表面软硬边分界平滑过渡。贴图规范上Ramp图和SDF图都不能用过度压缩的格式。Ramp图如果被压缩到ASTC 6x6色块边界可能偏移几个像素在人物脸上就会出现“阴阳脸”。我们最后把Ramp图固定在RGBA32尺寸只留256x128内存开销并不大但精度足够稳定。脸部阴影SDF我们规定必须由美术手绘不依赖算法生成。手绘的SDF在表达“美术想要的阴影形状”方面远比算法可靠。整个DCC规范一句话总结模型要清楚哪些地方该硬、哪些地方该软把风格决策前移到制作阶段而不是最后靠Shader弥补。5. 常见问题与排查技巧实录5.1 描边断裂、抖动与深度穿帮描边断裂是出现频率最高的。原因通常是模型法线没有被正确平滑外扩方向在尖锐边上下跳跃。排查时先把模型导入Unity的调试模式里显示法线如果看到相邻顶点法线方向相差很大就回DCC里修正。描边抖动比断裂更隐蔽。现象是角色原地不动描边宽度一帧一帧轻微变化。这往往来自Screen-Space宽度计算时的浮点精度问题。我们最后把宽度计算放到顶点着色器里并且在Project Settings里把“Graphics Settings”的FP16开关设为关闭强制使用FP32抖动才消失。如果你的项目里不能全局关FP16那至少描边Shader要标一句#pragma prefer_hlslcc gles避免编译器在低精度路径上偷懒。深度穿帮则是描边和背景物体穿插。角色站在墙前描边如果有深度写入就会嵌进墙里看不见。我们的解法是开启描边Pass的深度偏移选项把深度测试的模式改成LessEqual同时加一段基于视角距离的淡出。这个淡出系数和我前面给的12.0一样属于需要反复试的数值建议先设大一点看效果再逐步缩小。5.2 色块边缘锯齿与闪烁赛璐璐风格的硬边非常考验抗锯齿。如果不开MSAA色块边缘会有阶梯状锯齿开了MSAA又会和半透明物体产生排序问题。我们在移动端最终选择MSAA 4x配合硬边抖动在色块阈值边缘加一个基于屏幕空间像素坐标的伪随机值让锯齿变成微小的颗粒感视觉上反而接近手绘纸纹。闪烁通常来自Ramp图采样精度太低。Ramp图必须保证在过渡带附近有足够像素密度不然移动时色块会出现爬动感。我的建议是Ramp图宽度不要小于128像素尤其是暗部和亮部的过渡区要留足渐变宽度。如果你用程序化生成Ramp图可以在过渡中心加一点可视化标线方便对齐阈值。另一个闪烁场景是多光源混光。风格化渲染如果同时开着主光和辅助光两个光源各自生成的色块边界碰撞就会闪烁。我们的方案是强制除了主光之外的光源只提亮整体色阶不参与生成新的阈值边缘。美术在面板里能调一个“附加光源柔化系数”默认值是0.25数值越大附加光源越接近PBR效果数值越小越卡通。5.3 多平台表现差异PC上看起来干净利落的描边拿到手机上可能粗了一倍。主要原因还是Screen-Space宽度的分辨率适配没做对。我们在PC上是用相对视口比例来计算宽度但移动端的视口尺寸和DPI差异很大直接沿用同一个系数就会出现偏差。完善的方案是在管线启动时根据当前屏幕分辨率重算一组描边宽度系数。简单一点的做法是以1080p为基准然后按当前屏幕高度/1080的比例缩放。这个方案在绝大多数情况下够用。另一个多平台差异来自贴图压缩格式。PC用DXT纹理移动端用ASTC或ETC2压缩后色阶和描边Mask都可能产生偏差。测试时一定要在真机上检查Ramp图和脸部SDF不能只在编辑器里调。我们团队内部定了一条规矩任何风格化效果调整必须在专用测试手机上截图对比才算真正完成。5.4 多个角色共用一套参数时的串色问题角色A的皮肤暗部阈值调到0.4角色B同样用这套材质结果B的暗部面积明显不对。这个问题来自贴图明度差异而非Shader参数。角色B的皮肤贴图本身偏暗半兰伯特结果就更容易低于阈值于是暗部区域扩大。两个解决方案。一是在贴图规范层面统一各角色贴图明度范围但这是一件非常需要美术纪律的事很难强制执行。二是在Shader里加一个“基础明度归一化”参数把它乘到半兰伯特值之前让每张贴图的亮度基准对齐到同一个参考值。我们目前团队里同时保留两个方案主要角色走明度归一化NPC和量产角色靠贴图规范约束。这个问题提醒我一件事风格化渲染系统做得越完善它就越像一套内部的美术生产规范而不仅仅是渲染代码。Shader、参数、规范、工具链是一体的缺一块都会在项目后期某个角落冒出来找你要债。6. 最后想说的这套系统从搭骨架到跑通首版再到调出能上线的画面效果前后反复了快三个月。按我的体会风格化渲染的真正难点不在某个单独的技术点而在角色的整体一致性描边、色块、脸部阴影、头发高光、后期颜色每一个环节都影响最后的“手绘感”。哪个环节做的太过整个画面就会从“卡通片”滑向“劣质3D手游”。几个我最后想强调的经验第一参数体系一定要做成可分级、可复用的别写死在Shader里第二所有风格化效果都要在真实目标设备上验证尤其贴图压缩和抗锯齿这两项第三Ramp图和脸部SDF是决定角色颜值的核心资产值得花时间反复打磨第四把美术规范和技术方案一起打包让模型师和TA之间有一份共同语言这才是渲染系统能持续迭代的基础。如果你正准备搭自己的风格化渲染系统我建议不要一开始就追求复杂效果先把半兰伯特加Ramp加外描边这三件事做透再逐步叠加脸部修正和后处理。基础稳了后面一切都是锦上添花。