
做图形这些年我最怕听到一句话“咱们项目要改成风格化渲染了。”倒不是怕技术难而是绝大多数人对风格化的理解停留在“加个描边、把光照切成两档”。真上手之后才会发现一套能支撑整个项目跑完的风格化渲染系统难点根本不在于某个单一的Shader技巧而在于光照模型的思维切换、描边方案的取舍以及参数体系怎么设计才不会被美术同事追着骂。这篇文章我就拿自己落地过的项目为例把这个系统的几个关键模块拆开讲一遍包括色阶化光照与Ramp贴图、三类描边方案的对比、材质模板与后处理的组织方式还有高光、阴影、穿插这三个高频翻车点。适合正在做风格化改造的TA和图形程序员也适合想给产品定渲染技术方案的技术负责人参考。1. 风格化渲染的底层思维从物理正确切换到美术正确1.1 PBR与NPR的根本分歧很多团队做风格化其实是这么干的先接一套PBR管线让它正常渲染然后在后处理里加滤波、加描边、降饱和度。我见过好几个项目这么搞最后得到的画面又脏又油腻既要物理正确又想要插画感两头都不讨好。问题出在出发点。PBR管线的核心目标是能量守恒和材质参数的物理化Roughness、Metalness这些参数是朝着“还原真实材质”去的。但NPR的风格化系统目标压根就不是还原而是传达。它要的是画面的可读性、情绪的倾向性、以及造型语言的统一。这两者的指令是完全相反的。举一个最简单的例子PBR里漫反射从亮到暗是连续渐变这是物理上的能量分布结果而风格化通常会故意丢掉一大段中间调把光照归纳成几个色阶。这在物理上是不正确的但在视觉上却能让形体结构更清晰。所以我的经验是别想着“PBR渲染结果 后期滤镜 风格化”。风格化必须从光照函数开始重新设计。打个比方PBR是在拍照片镜头决定一切NPR更像插画师在画一张画他首先决定的是“这张画里明暗归纳成几块、每块什么颜色、边缘软还是硬”然后才考虑细节。你要做的那套系统其实是替所有美术去回答这些“绘画决策”。1.2 色阶化光照从硬Step到Ramp贴图最朴素的风格化做法是这样的算一个NdotL然后用step函数把亮面和暗面切出来。原理上没错但实际效果会让人抓狂。问题在于硬step在光照阈值边缘会产生非常明显的抖动模型稍微一动、光源稍一移那条明暗交界线就在不停闪烁更别说多光源场景下每个光源切一刀整个模型像块被切割的木头。真正实用的方案是用Ramp贴图来做色阶化。思想很简单把一维的NdotL结果当作UV坐标去采一张预先生成的色阶渐变贴图由贴图决定明暗每一步的颜色和过渡软硬。这样美术可以直接控制渐变带而不是改代码。代码上大致长这样half ndl saturate(dot(N, L)); half rampCoord ndl * 0.5 0.5; half3 rampColor SAMPLE_2D(_RampMap, sampler_RampMap, float2(rampCoord, 0.0)).rgb;注意我把NdotL从[-1, 1]映射到了[0, 1]因为Ramp贴图里希望暗部也能采到颜色而不是直接变成0。这张贴图横向就是从纯暗到纯亮纵向我习惯把它做成多行一行是一种风格预设比如第0行是日系两段式、第1行是美漫硬朗式、第2行是接近写实的多段式。这样同一个Shader可以通过贴图切换风格不用改任何代码。色阶数量没有标准答案。两段式的对比最强适合美漫和硬朗Q版三段式亮部、中间调、暗部是日系卡通最常见的配置兼顾体积感和简洁感四段以上就是介于半写实和卡通之间的路子。真正难调的是中间调。中间调一旦压得太窄画面就发灰像蒙了一层雾压得太宽又会出现色带感Banding。我的做法是在色阶过渡处用相机的导数做一点点像素级软边让交界处不至于完全一刀切这样既能保持卡通画的“归纳感”又不会在屏幕上留下难看的阶梯。1.3 阴影色不是黑色冷暖偏移与暗部透气新手做风格化最容易把暗面直接设成深黑或者纯灰。我在项目里见过太多被这一步毁掉的场景整体死黑一片轮廓全糊在一起角色身上的体积感彻底消失。真实世界中暗部并不是纯黑它受到环境反射、天空光、周围物体反弹光的影响。风格化做的是“归纳”不是“消灭”。日系卡通角色渲染里一个非常常见的手法是让亮部偏暖、暗部偏冷。比如角色肤色亮部往橙黄色偏暗部往蓝紫色偏。这种冷暖对比在视觉上会形成一种主动的色彩节奏画面一下子“活”了。实现上不需要复杂的全局光照在Ramp贴图的暗端给一个饱和度适中、明度不要太低的冷色即可。还要注意一点暗部颜色饱和度不能拉太高否则大面积暗面会泛出廉价塑料感。我给美术的要求一般是暗面颜色里能看出色相偏向但又不会一眼抢走主光的戏。再补充一个细节——半阴影的处理。现实里的暗部并不是一块死板的面它有过渡层次尤其皮肤、布料这类材质。风格化里我们可以用NdotL的半影范围模拟这种过渡但过渡范围通常只给一小段软边而不是PBR那种大范围的柔和漫反射。我之前用smoothstep控制过渡宽度调成大概10度到20度的变化范围效果就非常接近手绘里“大面积色块局部过渡笔触”的感觉了。2. 描边系统选型方案决定项目调性2.1 三种主流描边方案的对比与使用边界描边是风格化系统里辨识度最高、也是最容易做崩的模块。我先把工程里最常用的三类方案放在一起对比一下再讲它们各自的坑。方案基本原理优点典型缺点适合场景Invert Hull壳层法正面剔除把背面沿法线向外挤出形成一圈描边实现简单、可精确控制粗细、能随距离淡出、可被遮挡模型接缝处容易断线骨骼动画中皮肤穿插会导致破面Q版低模、卡通角色、植物石头等无精细动画物体屏幕空间边缘检测后处理里对深度和法线做梯度检测识别轮廓线不受模型拓扑影响高清模型非常稳定难以控制局部粗细线条锯齿明显需要单独调阈值三渲二角色、高模场景、风格化写实融合贴图描边在模型UV里预先绘制线稿线条位置完全可控可以画出内部装饰线不随光照和视角变化骨骼形变时线不跟着走漫画风格、平面感强的特殊需求我在项目里的选择通常不是“只用其中一种”而是混合使用。角色的主轮廓用Invert Hull因为需要线条随角色整体移动、有被遮挡关系而脸部或衣服上需要精细表现的刻线、褶皱线用屏幕空间法线检测补一层。两个方案叠加的时候要小心线条重复变粗的地方所以我会给两种描边各自一个输出通道让美术手动控制谁在哪个部位优先显示。还有个观点值得说风格化未必等于描边。像吉卜力那种水彩质感的风格或偏写实的细腻三渲二可能完全不要外层描边只保留边缘光或明暗分界。描边是语言不是义务。做系统的时候描边功能必须能一键全局关闭否则这套系统已经把自己绑架在一种固定画风上了。2.2 Invert Hull的稳定挤出与断边处理Invert Hull的原理非常简单我直接给一段核心代码v2f vert(appdata v) { v2f o; float3 normal normalize(v.normal); float outlineWidth _OutlineWidth * (1.0 - saturate(dist * _FadeDistance)); float3 pos v.vertex.xyz normal * outlineWidth; o.pos TransformObjectToHClip(pos); return o; }这段代码能跑但直接用在角色上第一个星期就会被反馈bug。最大的坑是断边。很多建模软件导出的模型在硬表面接缝处两个相邻顶点的法线方向是不连续的沿各自法线向外挤出后描边会在接缝处撕裂。解决思路有两个一是建模阶段要求模型烘焙平滑法线Compute Vertex Normals把所有顶点法线焊接成一套连续法线二是在Shader里不做顶点法线挤出而是沿“物体中心到顶点”的方向挤出这对封闭体积比较有效但角色这种开放曲面很多的地方不适用。骨骼动画是另一个大坑。角色手臂旋转时皮肤会挤在一起如果描边壳层也跟着一起挤压线条就会穿进模型再冒出来非常丑。最粗暴的解法是直接不让描边变形把挤出放在顶点色里离线烘焙一个固定的法线方向这样描边不会跟着皮肤抽搐但代价是动画幅度大时描边会脱离模型。折中做法是给描边设一个深度偏移让它永远比原模型离相机近一点这样即使轻微穿插也会被主模型的深度盖住一部分。宽度衰减的经验公式我一般这么控制近处的线粗远处的线细但不能无限细。我给一个最小值防止远景角色线条消失成模糊一团。同时在远处让描边颜色向雾色过渡这样天空背景下的远处物体线条不会突兀得像贴上去一样。2.3 屏幕空间描边的参数控制屏幕空间描边本质是个后处理。对深度缓冲和法线缓冲各做一次Sobel算子深度不连续的点判断为轮廓线法线不连续的点判断为折痕线。好处是稳定不论模型拓扑多复杂、动画多夸张线条都能贴住轮廓。坏处也很明显它会把所有边缘都变成线包括衣服上的每个褶皱、远处建筑的每一个窗框全图瞬间变成线稿。所以这套方案里的参数设计很关键我一般用四个关键量深度阈值、法线阈值、线宽、距离衰减。深度阈值控制“这个深度跳变算不算轮廓”调大了会忽略细碎的轮廓线调小会满屏都是线法线阈值专门处理平滑曲面上的细微折痕用来过滤衣服纹路线宽不要直接给屏幕像素固定值否则远景和近景的线条粗细差异极大最好乘以一个基于线性深度的衰减系数。线条锯齿是另一个容易翻车的点。屏幕空间的边缘检测结果天然一像素一像素地跳不处理直接输出会看到严重锯齿。解决办法是输出前对边缘强度做一次软阈值相当于把线宽内填充值做成梯度过渡让边缘产生半透明像素视觉上会平滑很多。我在项目里会在后处理链里叠加一点点模糊只模糊边缘图不动主图。还要注意屏幕空间描边和Invert Hull一旦同时启用两者的线条在模型轮廓处会重叠产生双线。我在彩色图合成阶段会把内描边压到外描边之下用深度比较让外描边只出现在没有被内描边覆盖的区域这个问题基本就消失了。3. 系统落地材质参数结构、Shader组织与后处理3.1 材质参数按类型分组别把Shader变成美术的灾难我在项目前期犯过一个严重错误在一个Master Material上堆了三十多个Float参数涵盖高光强度、混合权重、轮廓宽度、边缘光范围、Ramp贴图坐标偏移……结果就是美术打开材质面板直接懵掉谁都不敢动最后所有角色都长一个样。后来我重新设计了参数结构把参数分成三类独立逻辑组光照表现组暗面颜色、中间调软硬、色阶过渡范围、高光强度、高光阈值、边缘光强度。这组参数直接决定整体“画风”。描边表现组外描边宽度、内描边阈值、描边颜色、描边雾化强度、深度偏移。这组参数控制在造型边缘画重笔的方式。质感微调组高光贴图强度、高光形状、法线强度、Ramp贴图切换行。这组用于在同一风格里区分不同角色材质。分组之后每个参数还要有明确的单位或范围。我用滑杆控件替代原始Float输入把常见的安全范围直接锁死。比如中间调软硬我只给0到10是完全硬切1是一个极宽的平滑过渡。这样美术即使用户乱拖画面也不会彻底崩掉。更重要的是一个角色材质暴露给美术的参数控制在10个以内。剩下的参数由TA层预设好美术不需要理解。这套设计还有个隐性价值它让同一个团队的不同角色之间天然拥有了风格一致性。因为底层模板锁定了色阶数量、冷暖倾向、描边宽度基准值每个角色只做有限度的微调整体美术风格就不会跑偏。3.2 Shader模板与Pass组织方式风格化系统落地成代码时不能是单体一个Shader写到底。我习惯把它拆成一个模板Shader加若干可选特性以URP为例多Pass结构大致是这样Pass 0: Outline Pass描边壳层仅输出深度偏移后的描边色 Pass 1: Base Pass主光照Ramp采样、高光、边缘光 Pass 2: Shadow Caster Pass保证风格化模型正确投射阴影 Pass 3: DepthOnly/DepthNormals Pass供屏幕空间描边和后期使用抽丝剥茧地看Base Pass承载风格化渲染的核心逻辑Outline Pass负责造型线Shadow类Pass保证模型和场景的光影关系一致性。这里有个细节Shadow Pass里绝对不能直接复用Base Pass的色阶化因为阴影图只需要写深度不需要光照否则等于白白浪费性能。所以在模板里把这些Pass独立出来并在Shader里用Tags区分渲染阶段。变体管理也是这套系统的生死线。如果美术想让一个角色既有日系皮肤又有美漫机械装备你就得支持两套风格在同一个Shader里共存。但千万别为每一个参数组合开Feature不然变体数量会指数爆炸。我的做法是给三个可控KeywordTOON_RAMP是否使用Ramp贴图色阶化、SCREEN_SPACE_OUTLINE是否启用屏幕空间描边、ANISOTROPIC_HIGHLIGHT是否启用各向异性高光。默认情况下三关三开组合控制在八个变体以内。SRP Batcher兼容性也要提前考虑。风格化Shader里最容易破坏合批的错误是直接给材质实例改全局Uniform或者用SetGlobalXxx在Pass里传参数。这会让合批崩溃DrawCall飙升。正确做法是把参数全部收进材质自身的CBUFFER让SRP Batcher按材质属性自动拆批。3.3 风格化后处理最后一道色光当Base Pass已经能把模型渲染得接近目标画风后后处理才值得上马。我把它称为“最后一道色光”它解决的是全局色调统一和画面语言问题而不是帮你补基础渲染的漏洞。我常用的后处理序列是从这三层开始的色彩量化Posterize将画面色阶压到有限级数配合色相偏移做出类似插画的色块感。量化层级千万不能小于4否则人物皮肤会变成试色卡。风格化Bloom普通Bloom会把高光糊成一片奶油风格化里我会把Bloom阈值拉高只让真正亮的区域发光并限制泛光半径让它更像手绘高光的光晕。RGB Split色差在做受击、技能演出时把画面RGB三通道做径向分离模拟漫画里的速度线和冲击感。这个效果平时要关闭只作为特效演出的动态元素。后处理所有的色彩空间运算都放在线性空间里做。如果在Gamma空间做色彩量化或混合叠加结果会偏色尤其是暗部会明显发灰。很多时候美术反馈“画面不透”排查到最后其实是后处理色彩空间设错了。最后再说一个原则后处理的强度必须能一键归零。不是每个场景都适合强风格化游戏里的UI界面、过场演出、或者是战斗激烈的时刻都需要把风格化程度降下来。我在后处理参数里加了一个全局风格化强度系数从0到1线性控制0是完全写实、1是完全风格化。这个滑杆救了我无数次因为它让TA和美术能用同一套系统做A/B比较而不是来回改Shader参数。4. 最容易翻车的地方高光、阴影锯齿与描边穿插4.1 高光死白与亮片感的控制PBR管线的微表面高光在风格化里最常见的表现就是“死白”。GGX高光的能量集中加上金属度、粗糙度组合脸上和衣服上经常会爆出一块纯白像是把灯管贴在了模型上。这不是参数调错是物理模型本身不适合NPR的审美。我的做法是把NPR高光当成一种“亮片感”来设计。首先砍掉微表面高光改为一个“阈值软边”的可控形状。思路是在NdotH上做一次smoothstep小于阈值的地方完全没有高光大于阈值才逐渐亮起。这样高光看起来就像手绘里刻意留的白点或亮带而不是物理反射的集中斑块。代码示意half nh saturate(dot(N, H)); half highlight smoothstep(_HighlightThreshold, _HighlightThreshold _HighlightSoftness, nh); highlight * _HighlightStrength;阈值控制高光的面积大小软边控制边缘锐利程度。日系角色高光的阈值一般设置在0.75到0.9之间出来的高光是很小的一块日式“灯泡眼”式亮斑然后再配合一张高光形状贴图在特定UV区域比如发梢、嘴角可以画出额外的高光带。高光颜色也很讲究。全部用白色会显得廉价我习惯在高光色里混入主光源色的偏移让高光带一点暖色或冷色看起来像真实光线在材质上的反射残留。船新版本里我还会给皮肤材质单独做一层“皮下散射模拟”让边缘处透出一点红润色但强度严格控制得很低否则会变成果冻感。高光翻车的另一个常见场景是头发。头发需要各向异性高光也就是沿着发丝方向拉长的光带。直接套用上面的圆形高光会变成头顶一个白点。我这里开了ANISOTROPIC_HIGHLIGHT的Keyword在切线方向拉伸高光形状并叠加两层高光一层大而弱呈扫把形一层小而锐像发丝的高光点。两层叠加起来才有“丝绸感”。4.2 阴影边缘的锯齿、闪烁与Banding风格化阴影的大方向是“硬边”但硬边在工程上有一堆麻烦。第一个麻烦是阴影锯齿。用Shadow Map采样出来的阴影边缘在光照分辨率不够时会呈块状。日系卡通里阴影线条本来就少锯齿块就会特别扎眼。我的方案是阴影贴图分辨率给到2048以上开启更高倍的Cascade同时在阴影边缘做一次PCF软阴影但软阴影的宽度控制在2到3个纹素以内。太宽的软阴影会破坏“硬边归纳”的风格所以这里是技术正确性和美术风格之间的平衡。第二个麻烦是阴影边缘闪烁。动态物体在光源方向反复旋转时硬边会在相邻帧间跳动视觉上就是高频闪烁。加一个基于法线方向的偏移量能缓解即除了深度偏移再把阴影采样点沿法线方向外推一点引擎里常见的Normal Offset Bias就是这个思路。这在慢动作镜头下尤其重要因为闪烁在这种镜头里会被放大十倍。第三个麻烦是NdotL色阶边缘的Banding。Ramp采样出来的色带在暗部过渡处很容易出现一圈一圈的等高线效应。解决办法是在smoothstep之前引入导数fwidth计算过渡宽度让像素的过渡宽度至少大于一个像素。直接在表达式上除以当前像素的导数可以让边界处产生自然的抗锯齿过渡而不需要额外的后处理。我给Ramp替换后的色阶边界都用这种导数抗锯齿方式这样做比开FXAA或者后处理模糊更精准且不会模糊掉描边。别忘了阴影边缘同样要接一层这样的平滑在ShadowCoord采样之后再做一次NdotL相关的smoothstep把阴影贴图本身的锯齿和色阶过渡一起柔化。4.3 描边与模型的穿插问题描边穿插在正式项目中几乎无法完全避免但可以把它控制到肉眼不易察觉的程度。这里我分享几个从失败中总结的方法。先说Invert Hull的自穿插。角色大腿和裙子交界处裙摆描边向外挤出后会有一部分钻入大腿模型内部看得人浑身难受。我的第一版处理是在描边Pass里追加一个小的深度偏移让描边永远比主模型更靠近摄像机。这样即使发生穿插描边也会被正确的模型深度覆盖掉视觉上只剩干净的轮廓线。骨骼动画带来的穿插更麻烦。当手臂弯曲到极限时皮肤顶点会相互挤压描边壳也随之变形。这个问题在顶点动画较大的地方只能靠“固定描边厚度放宽穿插容差”缓解比如让描边壳的顶点在处理时跳过部分形变用骨骼之前的静态法线方向。如果你做的角色有大量极限动画我强烈建议先做一个静止姿势版本跑通全流程再接动画验证否则你会把模型问题、动画问题和渲染问题混在一起排查效率极低。屏幕空间描边最常见的穿插问题是内部线误判。角色头发和脸部的法线跳变过大时屏幕空间会把发丝和额头判定成两条相邻边缘结果头发轮廓和脸部轮廓之间出现一条混乱的暗线。解决方法是把深度阈值和法线阈值分开用脸部这种同一深度连续的区域只用法线阈值检测描边头发和身体之间深度跳变明显的区域优先用深度阈值。我在实现里给深度边缘的高权重、法线边缘的低权重做了一个可调混合美术滑块一拖就知道哪里该出来线条。还有一个容易被忽略的细节被雾或大气遮挡的远处角色其描边应该优先于雾色消失。我在描边颜色合成阶段加了与雾混合的处理当描边距离超过阈值让它逐渐过渡成雾的颜色。这样远景场景里所有物体的描边不会像一根根黑色火柴棍一样矗立在雾气中。5. 交付风格化渲染系统的一点点经验5.1 给美术的不仅是Shader是参数说明文档我见过太多团队把一整套风格化Shader丢给美术然后就不管了。结果美术怎么拖参数都达不到参考图效果最后悄悄去PS里修贴图整个风格在资产层面开始走形。我的习惯是每交付一套风格化模板附一页说明文档。文档不用长但要写清楚三件事核心风格由哪些参数决定哪些参数只是微调哪些参数千万不要动。比如“Ramp贴图第0行”是日系三段式的基底“暗面颜色偏移”决定冷暖倾向这两个就是风格的核心参数而“高光强度”只是局部微调必须控制在0.2到0.6的范围。同时附上两张参考图一张是这个风格最理想的状态一张是这个风格跑偏之后的负面样例。美术拿着文档和参考图调参差不多一两个小时就能出想要的效果。5.2 建立风格化验证场景风格化渲染系统最难的不是做出来而是保证它在项目所有场景里都不崩。我在项目里固定了一个“风格SP”验证场景里面摆齐各种典型物体角色带骨骼动画、植物、石头、金属道具、玻璃瓶光照方向预设了顺光、侧光、逆光、夜景多光源四套并保存同一组摄像机位。每次改完Shader先渲染验证场景的截图集合看风格是否保持一致、有没有新增的锯齿或穿插。甚至可以把这个验证场景接入自动截图流程每天凌晨出一次渲染报告。养成这个习惯后很多回归问题能在几天内就被发现。没有这一步风格化系统越改越偏等美术告诉你“颜色不对”的时候你已经不知道是哪个参数在贡献错误了。5.3 先把Base Pass调满意再谈后处理最后一条也是最重要的一条经验后处理永远是锦上添花不是救火队员。我在项目里见过一种危险需求——美术想不调光照靠后处理的Bloom和色差把画面“顶”出风格化效果。结果就是人物灰蒙蒙一到过场演出还得单独裁剪后处理参数整个渲染表现根本不稳。正确顺序是先花两周把Base Pass里Ramp色阶、阴影色、高光形状调出基本满意的状态再叠加后处理的量化和Bloom。后处理加上之后再回头微调Base Pass的参数。因为后处理会改变全局色调感知你对着带后处理的效果调底层的参数才能得到最终那个“一体的画面”。反之底层定死了再叠后处理经常会发现高光被Bloom糊掉、暗部被量化吃掉又被迫回头改底层。说实话一个风格化渲染系统要踩的坑远不止文章里写的这些。材质贴图的线性空间设置、SRP Batcher的合批冲突、Shader变体在移动端的编译时间每一样都能让人折腾到深夜。但只要你把思维切到“美术正确”、把描边系统的选型和调性绑死、把参数结构设计到连美术都能愉快操作这个系统的骨架就已经稳了。剩下的都是在它上面不断添砖加瓦的过程。