风格化渲染系统这两年几乎成了游戏团队找TA的标配需求,但大部分团队踩的坑也高度一致:先丢过来一个画风参考,让“做个卡通渲染”,等做完了才发现角色像塑料、场景又闷又脏、特效和角色完全不是一个画风,整体画面像几个不同游戏拼在一起。
真正的问题不是某个shader写得不够好,而是整个渲染链路缺一个“系统”来做统一约束。我在几个项目里反复折腾过这套东西,从Unity URP到自研管线都铺过一遍,这篇就把我觉得最值得沉淀的部分写出来,包含架构分层、光照改造、描边工程化、后期调色以及移动端性能这五个核心环节,适合正在搞风格化渲染、又不想把画面做成“一锅乱炖”的小伙伴做参考。
1. 先想清楚系统要长什么样,再动手写shader
很多项目的风格化渲染不是从零开始的,而是先有了一堆标准PBR效果,再零零散散地加卡通组件。结果就是角色的皮肤用了三层高光叠加,场景石头还是PBR粗糙度那一套,视觉语言完全不在一个频道。我后来搭系统的第一件事,不是写shader,而是定架构。
1.1 风格决策集中在光照与后期两个环节
我习惯把整个渲染系统分成三层:材质层、光照层、后期层。材质层只负责输出基础色、法线、遮罩这些“原料”,光照层统一决定漫反射怎么过渡、高光怎么切边、边缘光怎么衰减,后期层再对整体色彩做一次风格化收口。
之所以这么分,是因为风格化渲染的“风格”几乎都体现在光照过渡和色彩映射上。如果让每个材质都自己写一套光照逻辑,那你的项目就有十几种“卡通”,美术调完一个还得调另一个,永远收不了口。把光照决策集中到一套共享代码里,所有shader都调用同一个光照函数,风格才可能全局统一。
我遇到过一个反例:项目早期角色shader里用了MatCap模拟光照,场景shader用兰伯特加半色调贴图,两者在相同光照方向下呈现的明暗关系完全不同,玩家截图里角色像站在另一个世界。所以我把光照抽象成公共函数后,第一件事就是让角色和场景的默认明暗渐变曲线完全一致,后续所有风格微调都在这个光照函数里完成。
1.2 光照输入必须做统一封装
URP里默认的Lighting数据是多样化的,主光源、附加光源、球谐、环境反射各有各的获取方式。风格化系统里我强烈建议不要到处去拿光源,而是做一个自己的LightData结构体,在渲染前统一填好,让所有shader只认这一个结构体。
LightData里至少包含这几个字段:
- 主光源方向(世界空间)
- 主光源颜色和强度(已经做过头尾处理)
- 环境光颜色(通常烘焙成几级灰阶)
- 球谐光照是否参与混合(部分场景需要用球谐补暗部)
这样做的另一个好处是,调试特别方便。我可以在不修改任何shader的情况下,把主光源方向在调试面板里强制改成某个固定值,用来复现某个转角的画面问题。如果是每个shader各取各的光源数据,这种统一调试根本没法做。
1.3 材质参数归类:能并的并,能删的删
风格化系统的shader参数特别容易爆炸。一个角色材质可能会出现十几张贴图加二十几个浮点参数,最后美术根本不敢动,全让TA一个人扛。我的原则是参数必须归成几类:基础表面属性、光照响应属性、描边控制属性、特效开关属性。每一类参数都放进一个Header,并且能合并到一张贴图里的就不要用三张。
比如漫反射渐变宽度和边缘光强度,我常常把它们合并到一张控制贴图的RG两个通道里,美术只需要画一张图就能同时控制两个区域。参数少了,出问题的概率自然就低,这是我从多个项目里总结出的最实用经验。
2. 卡通光照改造:从“物理正确”里跳出来
风格化光照的第一步,是放弃PBR那套基于能量守恒的物理模型。不需要微表面GGX,也不需要BRDF能量归一化,你需要的是一个好控制的、便于美术脑补的明暗模型。但放弃物理模型不等于乱写,得有章法。
2.1 用渐变映射替代物理漫反射
最基础也最核心的改动,是把NdotL从连续值映射成离散的渐变带。做法是在漫反射计算里采样一张ramp贴图:
half ndl = saturate(dot(N, L)); half wrap = (ndl + _WrapValue) / (1.0 + _WrapValue); half shadow = tex2D(_RampMap, float2(wrap, 0.5)).r;这里的_WrapValue是wrap light参数,作用是压缩暗部范围,让背光面不至于太黑。ramp贴图的横轴就是光照强度从暗到亮的分布,美术画这张图的时候,画出的其实是“这个材质的漫反射性格”。
硬边卡通就是一条中间有陡峭台阶的曲线,软边半厚涂风格就是一条平滑的缓坡曲线。我把这幅图理解成“光照等位线”,它决定了整个项目的最基本视觉气质。
ramp图在实际使用中有几个常见坑。一个是采样分辨率太低,导致高光区域出现明显的色阶断层,脸上一圈一圈的。解决方法是ramp图不要用纯硬边,在台阶处留两三像素的过渡带,或者用单通道贴图加lerp做平滑。另一个坑是不同贴图之间采样坐标不一致,比如头发和皮肤用了不同方向的渐变映射,导致同一盏灯下亮暗区域对不上。我统一规定所有材质都采同一个横轴坐标,差异只体现在ramp图内容上,不体现在采样方式上。
2.2 MatCap的引入与法线空间陷阱
MatCap(材质捕获贴图)在风格化渲染里用的极多,尤其是硬表面和皮肤质感。本质是把一张预烘焙的球形光照贴图按法线方向采样,让物体获得类似参考球的光影感,好处是美术可以直接在Photoshop里画“光”,所见即所得。
但MatCap最大的坑是它根本不响应场景里的光源变化。角色在一个方向转了一圈,MatCap光还在原来的地方,看起来特别假。我的做法是保留主光源方向对MatCap采样坐标做旋转修正,至少让光跟着主光源方向大致转动。修正很简单,把法线从世界空间变换到光源空间,再进行贴图采样:
half3 viewNormal = mul((float3x3)_LightRotationMatrix, normalWS); half2 matcapUV = viewNormal.xy * 0.5 + 0.5; half3 matcap = tex2D(_MatCapTex, matcapUV).rgb;加了旋转修正后,MatCap就不再是死光,而是有方向响应的质感光。配合原本的ramp漫反射一起使用,可以做出“底色卡通、材质有立体感”的效果。不过要注意MatCap的强度别盖过主漫反射,否则角色会像抹了一层油。
2.3 高光与边缘光的克制用法
风格化高光最怕的就是“油腻”。PBR那种GGX高光高光点在风格化场景里几乎等于灾难,因为它的能量分布和物理反射太“真实”,和周围的扁平卡通放一起会显得跳。我在风格化系统里用的高光是Blinn-Phong的阈值截断版本,核心是让高光硬切边缘:
half nh = saturate(dot(N, halfDir)); half specStepped = step(_SpecCutoff, nh);配合GGX的粗糙度感受,在高光边缘稍微做一点渐变过渡,就可以得到类似漫画高光的“一笔刷过”效果。高光强度、切边阈值、过渡宽度三个参数做好,已经能覆盖大部分风格。
边缘光也是风格化的重灾区。很多人习惯了Fresnel边缘光狠狠来一笔,结果角色在夜景里直接变电灯泡。我的经验是边缘光必须受遮罩贴图控制,并且强度不能超过漫反射亮部的一半。让边缘光受法线噪声扰动,可以打破那道“水蜜桃轮廓线”的假感,这个做法在角色头发上尤其管用。
3. 描边不是加个“Outline”开关那么简单
风格化渲染系统里,描边往往是最先被注意到、也最容易做破坏的环节。我见过太多项目用Inverted Hull拉条线,结果角色转个头描边的粗细忽大忽小,关节处直接断线。描边的工程化处理,比想象中复杂得多。
3.1 不同描边方案的取舍
风格化描边的主流方案主要有三种,各有各的适用场景:
| 方案 | 原理 | 优势 | 劣势 |
|---|---|---|---|
| Inverted Hull | 背面放大沿法线挤出 | 实现简单,移动端友好 | 需要平滑法线,展UV不规则的软体物品容易断裂 |
| 深度法线描边 | 像素级检测深度和法线不连续 | 轮廓精确,硬表面效果好 | 需要额外depth/normal pass,性能开销大 |
| 后处理Sobel或Lerp方案 | 屏幕空间边缘检测 | 可统一处理全场景 | 风格化可控性弱,大色块场景容易误判 |
我实际的组合方案是:主角和关键NPC用Inverted Hull,大场景建筑和道具用深度法线描边,云和粒子这类特效干脆不描边。一个项目里可以同时存在两套描边技术,但它们的颜色和宽度基准必须统一,保证视觉上是一套系统。
3.2 Inverted Hull的工程细节:平滑法线与遮罩
Inverted Hull的原理是分两个pass渲染同一个模型,正面正常绘制,背面用Cull Front剔除正面、顶点沿法线向外挤出,再把模型填充成描边色。听起来简单,但顶点法线是决定描边质量的关键。
如果一个模型的法线没有重新平滑过,它的顶点法线会因平滑组而分开,挤出后各个面往各自方向顶,就会出现描边断线。所以模型导入后必须让美术或工具把法线按面法线重新平滑一遍,这个环节我习惯用脚本在AssetPostprocessor阶段自动处理,不需要美术手动操作。
另一个问题是描边宽度需要配合视距衰减。摄像机拉远后,屏幕空间里固定像素宽度的描边,在世界空间里会变得非常厚,看起来像剃了头还戴了一圈围巾。我用的做法是让挤出量在远距离时持续衰减,保证描边在屏幕上的宽度尽量稳定:
float outlineWidth = _OutlineWidth * (1.0 - saturate(distance / _OutlineFadeDist));最后是遮罩。头发边缘、裙边这些地方描边会明显破坏轮廓,需要提供一张描边遮罩贴图,或者直接在顶点色里让美术标记哪些区域不描边。我自己在系统里规定顶点色R通道控制描边宽度,G通道控制描边禁用区域,一次导入全搞定。
3.3 描边颜色不能是纯黑
这是风格化系统里最反直觉的一条经验。大部分人觉得描边不就是黑线吗,但纯黑的描边在打光场景下会让整个画面显得特别脏,因为黑色不吸收任何光,和周围亮部形成生硬反差。
我的做法是让描边颜色响应用两个维度:物体暗部的颜色,以及环境光的冷暖倾向。简单办法是描边色从物体的基础色里做暗部推导,再加一个饱和度偏移。也就是说一个红色物体的描边是暗红色而不是黑色,冷色场景里的描边会微微带蓝,暖色场景里的描边微微带棕。这样描边融入了画面而不是钉在画面上。
当然后处理阶段也可以统一调描边色的明度,但我更偏爱在材质阶段就把描边色计算好,因为后处理阶段已经分不清哪个像素是描边了,灰度调整会连带影响整个画面。
4. 后期与色彩:真正决定画面质感的最后一步
很多团队做到shader层面就觉得风格化完成了,剩下的交给后处理。但实际上,一张画面到底是“高级”还是“廉价”,后期调色占了一半以上的权重。风格化渲染系统如果不管色彩映射,等于只做了一半。
4.1 色调映射是从“物理正确”走向“画面气质”的关键
Unity URP默认的色调映射是ACES,它的作用是让HDR图像的明暗过渡更符合人眼感知,能压低高光、提升暗部。但问题是ACES这套曲线太“自然”,会把风格化的硬边光照弄得发灰、发软。
我在风格化系统里推荐用自定义色调映射,或者至少在ACES和自定义之间做一次混合。有一个很经典的思路是Karis风格的“近似拟合”,它比ACES更留得住暗部对比,但又比完全无色调映射更少噪点:
half3 CustomTonemap(half3 color) { color = max(0, color - 0.004); return (color * (6.2 * color + 0.5)) / (color * (6.2 * color + 1.7) + 0.06); }这个曲线看起来平平无奇,但对画面气质的改变非常明显。加上之后,亮部保留住了卡通的“纸面感”,暗部也不会堆成死黑。后期调色时,我会把曝光基准设到一个让中间灰反射率为18的灰球保持正常亮度的位置上,再通过自定义曲线做风格偏移。
4.2 色彩分级是风格化的秘密武器
真正拉开画风差距的不是贴图,是色彩分级。我在系统里放了一个Color Grading模块,里面核心就三个参数:对比度、饱和度、色温。再配合一个LUT贴图做风格化映射,画面气质立刻就不一样了。
对比度对风格化的影响最明显。偏高对比度会让画面去掉中间灰过渡,显得更“漫画”;偏低对比度会让画面暗部泛白、高光压平,产生一种类似水彩纸的柔和感。饱和度同理,高饱和偏卡通和儿童读物,低饱和偏厚涂插画和文艺滤镜。色温则决定了画面是偏“午后温暖”还是“月色清冷”。
我习惯把后期参数也做成一个ScriptableObject,叫StyleProfile,美术在不同的StyleProfile之间切换就能对比不同画风。这个工作流比改一个参数截图一次要高效太多,也让“画风”变成了可版本管理的资产。
4.3 Bloom和噪点:风格化里的两匹野马
Bloom(泛光)在PBR管线里是模拟眼睛对强烈亮光的散射,但在风格化里它经常会破坏画面。卡通的轮廓线边上一圈光晕,画面立刻变“油腻”。我的用法是把Bloom的阈值拉到比较高,只让真正的HDR高光区域出光,同时把Bloom应用到半分辨率渲染目标,强度不超过0.25——它的作用是给高光区域加一点体积感和柔和度,而不是让整张图发光。
噪点和纸纹反而是我强烈推荐的风格化后期。尤其是手绘感的角色,加一层轻微粒度和方向连续的Paper Grain,能极大削弱数字感。我用的做法是把躁点贴图动态偏移加在屏幕空间,让颗粒有轻微流动感,看起来像真实扫描进去的纸纹理。强度控制在0.05以下,达不到肉眼明显看见噪点的程度,但整体画面的“作品感”会提高很多。
5. 移动端性能预算和参数化工作流
风格化渲染系统做出来不是摆着好看的,最终要跑在目标平台上限频达标才算完成。性能优化在移动端尤其吃紧,而风格化系统因为常常叠加多pass、多贴图采样,性能控制不好就会爆炸。
5.1 移动端的开销大头:overdraw、半精度和pass数量
风格化系统在移动端最常见的就是过度绘制。描边pass、阴影pass、高光pass叠加,加上半透明特效,中低端手机GPU的fillrate很快就满了。我给出的预算表通常是这样的,针对骁龙8系和天玑9000级别:
- 屏幕分辨率1280x720左右,overdraw控制在1.5倍以内
- 全屏后处理节点不超过6个,其中Bloom和Color Grading是最大头
- 描边优先用Inverted Hull,不放到深度法线检测里去
- 角色shader变体控制在8个以内,避免UE一样变体爆炸
这里特别提一下半精度浮点。移动端GPU对half2/half3的运算速度远快于float,但需要确保shader里没有隐式的类型转换。很多风格化shader里采样ramp图时用了float2坐标,G-Buffer输出用了float4,结果性能白白损失了20%。我写风格化shader的时候,所有漫反射计算、采样坐标统一用half,只有世界空间坐标和摄像机朝向这类需要大范围操作的才用float。
5.2 用StyleProfile把控全项目风格参数
前面提过把后期参数做成StyleProfile,实际上光照、描边、漫反射也可以全部归入同一个资产文件里。这样一个好处的直观场景是:下午美术提了需求说“画面再暖一点,描边再细一点,阴影再硬一点”,我只需要调StyleProfile的几个字段,重新进游戏看效果,不用在几十个材质球里一个个翻参数。
StyleProfile的字段设计也遵循第一部分的归类原则。光照部分放漫反射wrap值、漫反射曲线贴图引用、高光阈值、边缘光强度;描边部分放描边颜色基调、宽度系数、视距衰减、遮罩开关;后期部分放对比度、饱和度、色温、LUT引用、Bloom阈值和强度。
5.3 调试和预览环境不可省
最后聊一个很多人忽略的事:风格化渲染系统的调试环境。我经历过最痛苦的调试场景是美术改了一个ramp图,说暗部变奇怪了,但我不知道是哪个材质的哪个ramp图引用了它,也不知道灯光方向对它影响多大。
所以我在系统里加了一套Debug View功能,按下快捷键就能把当前画面切换成几个可视化通道:漫反射渐变值、描边宽度、基础色、光照方向示意。排查问题的时候先看这些通道,比盲调参数快十倍。同时在项目里放一个专用的“光照参考球”,这个球上刻了由深到浅的灰阶渐变条,任何光照调整都以这个球上的表现为基准,美术调光调色时先看球,再看角色,心里有谱得多。
预览环境还包括把材质放到一个固定的场景里切换不同时间的光照角度。很多团队角色好看、场景一黑就翻车,就是因为预览时没考虑多种光照角度的表现。我在角色材质上放了一套Second Light的调试组件,模拟太阳从头顶、背后、侧面、底部四个方向照射时的画面状态,确认每个角度都没问题才算调试完成。
最后分享一个小经验:风格化系统最忌讳一味堆参数。一开始我总想着多给美术一点自由度,结果每个材质的裤子都有20多个参数,美术调着调着就不知道自己在调什么了。后来我把参数砍到只剩6到8个核心项,漫反射曲线、高光阈值、边缘光强度、描边宽度、描边颜色冷暖、噪点强度,剩下的全部通过全局StyleProfile控制。反而没人抱怨“调不出想要的效果”了,因为能调的都调得明白,调不了的是全局风格决策,是一整个团队一起定的方向。这套系统跑过的项目里,我自己最满意的不是某个镜头有多惊艳,而是换了一个新画风参考之后,美术团队只花两天就全局调整出了完全不同的气质——这种“系统级”的反应速度,才是我觉得风格化渲染系统真正值钱的地方。