ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

风格化渲染系统实战:PBR打底与NPR后处理管线设计

风格化渲染系统实战:PBR打底与NPR后处理管线设计 1. 风格化渲染系统的整体架构与设计思路1.1 从PBR到NPR为什么需要一套独立的渲染管线做实时渲染的人迟早会碰到一个岔路口继续沿着PBRPhysically Based Rendering基于物理的渲染那条路走追求更真实的材质、更准确的光照还是拐个弯去做NPRNon-Photorealistic Rendering非真实感渲染把画面往卡通、水彩、像素、手绘这些方向推。我这次做的这套风格化渲染系统核心目标就是后者——在同一个渲染框架里既能保留PBR的物理光照底子又能通过后处理和材质层的干预把画面“掰”成想要的风格。为什么不在纯PBR管线上直接改shader因为PBR的整套数学是围绕能量守恒、BRDF、微表面理论建立的你每改一个环节都会牵动全局。比如你想让高光变成硬边色块直接改BRDF会破坏光照一致性阴影、反射、间接光全乱套。更稳的做法是底层仍然跑PBR保证光照和阴影的物理正确性然后在材质输出和后处理阶段做风格化映射。这就是我这套系统的核心思路——PBR打底NPR做面。具体来说系统分成三层第一层是标准PBR光照计算负责漫反射、高光、阴影、环境光第二层是材质风格化层通过自定义shader节点对PBR输出做量化、色阶压缩、边缘检测第三层是后处理层用LUTLook-Up Table颜色查找表做全局调色再叠加描边、纸张纹理、噪点等风格化效果。三层之间是松耦合的你可以只开第一层做写实渲染也可以三层全开做重度风格化。这套架构的好处是灵活。我试过在同一个场景里让角色走卡通风格、场景走写实风格、UI走像素风格互不干扰。每个材质可以单独指定风格化强度后处理也可以按相机分层应用。对于做独立游戏或者风格化短片的团队来说这种可插拔的设计比从头写一套NPR管线要省事得多。1.2 核心模块拆解光照、材质、后处理的分工光照模块我保留了PBR的标准流程直接光用Cook-Torrance BRDF间接光用球谐或者光照贴图。但为了风格化我加了一个“光照量化”开关。开启后漫反射项会被映射到有限的色阶上比如4阶或6阶模拟卡通渲染的硬边阴影。这个量化不是简单的floor而是用smoothstep在阶跃边缘做一点柔化避免锯齿太硬。材质模块是风格化的主战场。每个材质除了标准的albedo、normal、roughness、metallic贴图还可以挂一张“风格化遮罩”用来控制哪些区域参与量化、哪些区域保留PBR原始输出。比如角色的脸部我想保留柔和的渐变但衣服我想做成硬边色块就可以用遮罩区分。材质shader里我预留了几个关键参数_PosterizeSteps控制色阶数_EdgeWidth控制描边宽度_HatchStrength控制排线强度。后处理模块负责全局调色和屏幕空间效果。LUT是这里的主力我通常用一张16x16x16或者32x32x32的LUT贴图把PBR输出的线性颜色映射到风格化色调。LUT的好处是美术可以在Photoshop或者DaVinci里调好颜色导出成标准LUT文件直接丢进引擎用。描边我用了两种方案一种是基于深度和法线的边缘检测适合全屏统一描边另一种是基于材质ID的背面膨胀适合角色单独描边。两种可以叠加使用。注意LUT的采样必须在色调映射之后、伽马校正之前。顺序错了颜色会偏得离谱我踩过这个坑调了一下午才发现是管线顺序问题。1.3 为什么选LUT做全局调色而不是直接改光照颜色很多人问调色直接在光照阶段乘个颜色不就行了为什么要用LUT这里涉及一个很实际的制作流程问题。光照阶段改颜色改的是“光”的属性会影响阴影、反射、间接光的计算牵一发动全身。而且美术调色的时候想要的是“所见即所得”——我在调色软件里拉个曲线引擎里就应该看到同样的结果。LUT正好满足这个需求它是一张颜色映射表输入颜色查表输出颜色跟光照计算完全解耦。另外LUT的另一个优势是性能。一张32x32x32的LUT只有32KB左右采样一次就是一次纹理查找比在shader里写一堆颜色校正公式要快得多。而且LUT可以动态切换白天用一套、夜晚用一套、进入战斗再用一套切换成本几乎为零。我实测过在移动端用LUT做全局调色比用shader逐像素计算颜色校正帧率能差出3到5帧。当然LUT也不是万能的。它只能做全局的颜色映射没法做局部的风格化。比如你想让画面左上角偏暖、右下角偏冷LUT就做不到得靠后处理里的渐变遮罩或者体积雾来补。所以我的系统里LUT是基础层上面还会叠屏幕空间的渐变和纹理。2. 核心细节解析与实操要点2.1 风格化材质shader的关键参数与计算逻辑写风格化shader最容易翻车的地方是参数太多、互相打架。我一开始也是堆了一堆开关结果美术根本不知道怎么调。后来我收敛成四个核心参数每个都有明确的物理或者视觉含义。第一个是_PosterizeSteps色阶数。这个参数控制漫反射的量化等级。计算逻辑是先算标准N·L得到漫反射强度然后乘以色阶数取整再除以色阶数。但直接取整会有硬边所以我在取整前后各加了一个fwidth的过渡。代码大概是这样float diffuse saturate(dot(N, L)); float steps _PosterizeSteps; float quantized floor(diffuse * steps) / steps; float edge fwidth(diffuse * steps) * _EdgeSoftness; quantized smoothstep(quantized - edge, quantized edge, diffuse);_EdgeSoftness一般设0.5到1.5之间太小了边缘还是硬太大了就糊成一片。这个值跟分辨率有关1080p下1.0左右比较合适4K下要适当调大。第二个是_EdgeWidth描边宽度。我用的是屏幕空间深度法线边缘检测所以宽度是以像素为单位的。计算的时候要注意深度边缘和法线边缘的阈值要分开调。深度阈值太小地面上的接缝会被误判成边缘法线阈值太小曲面的细分也会被描出来。我一般深度阈值设0.02到0.05法线阈值设0.3到0.6具体看场景尺度。第三个是_HatchStrength排线强度。这个是在漫反射暗部叠加一张排线纹理模拟手绘的阴影线。排线纹理的UV要跟屏幕空间或者物体空间绑定我一般用物体空间的UV这样排线会跟着物体动不会像屏幕空间那样“浮”在表面。强度控制就是简单的lerp但要注意排线纹理的对比度不能太高否则暗部会显得很脏。第四个是_RimPower边缘光强度。风格化渲染里边缘光很重要能快速把角色从背景里拉出来。我用的是Fresnel项pow(1 - saturate(dot(N, V)), _RimPower)_RimPower一般设2到5太小了边缘光范围太大太大了又太细。边缘光的颜色我一般跟主光颜色互补或者直接用高饱和的色相看具体风格。实操心得这四个参数不要同时拉满。我见过有人把色阶设成2、描边拉到5像素、排线强度1.0、边缘光拉满结果画面糊成一团什么都看不清。风格化是做减法不是做加法。2.2 LUT的制作、导入与采样流程LUT的制作流程我走过不少弯路现在固定成一套标准流程。首先在引擎里截一张标准色卡场景的图包含灰阶、饱和色、肤色、天空色。然后把这图丢进DaVinci或者Photoshop的调色工具里调出想要的风格。调完之后导出成CUBE或者PNG格式的LUT文件。如果是PNG需要是长条形的展开图比如32x32x32的LUT就是1024x32像素。导入引擎的时候要注意色彩空间。LUT贴图的导入设置必须是sRGB关闭、过滤模式Bilinear、Wrap Mode Clamp。如果开了sRGB颜色会二次校正偏得亲妈都不认识。采样的时候UV计算是float3 lutUV; lutUV.r (color.r * (lutSize - 1) 0.5) / lutSize; lutUV.g (color.g * (lutSize - 1) 0.5) / lutSize; lutUV.b color.b * (lutSize - 1) / (lutSize * lutSize) 0.5 / (lutSize * lutSize);这个公式看着复杂其实就是把三维颜色坐标映射到二维贴图坐标。lutSize是LUT的边长比如32。注意蓝通道要除以lutSize * lutSize因为蓝色是分层叠上去的。这个公式我建议直接抄自己推容易漏掉0.5的偏移导致颜色整体偏暗。LUT的强度控制也很重要。我一般会加一个_LUTIntensity参数用lerp在原始颜色和LUT输出之间插值。这样美术可以控制风格化的程度0就是原色1就是全LUT。实测下来大部分风格化场景0.6到0.8之间比较舒服全开容易丢失细节。2.3 描边方案的选型深度法线 vs 背面膨胀描边是风格化渲染里最显眼的效果但也是最容易出问题的环节。我试过三种方案深度法线边缘检测、背面膨胀、以及两者混合。深度法线方案是在后处理里做的对全屏生效优点是实现简单、不挑模型缺点是内部细节描不出来比如角色衣服上的褶皱因为深度变化不够大检测不到。背面膨胀方案是给模型加一个额外的Pass把背面顶点沿法线外扩渲染成纯色。优点是能描出模型内部的细节边缘而且线条粗细均匀缺点是外扩会导致模型体积变大在模型密集的地方会互相穿插而且对硬边模型不友好法线不连续的地方会裂开。我现在的做法是混合角色用背面膨胀场景用深度法线。角色背面膨胀的时候外扩距离不是固定的而是根据顶点到相机的距离动态调整近处细、远处粗这样视觉上线条粗细一致。场景深度法线则加了一个“法线权重”在法线变化剧烈的地方加强边缘检测平坦的地方减弱避免地面上的噪点被误判。注意背面膨胀的Pass一定要关掉深度写入只写颜色。如果开了深度写入后面的物体全被挡住。这个坑我踩过角色描边把整个场景都遮了排查了半天。2.4 双面材质的处理与性能权衡风格化渲染里经常碰到双面材质比如树叶、旗帜、角色的披风。Unity默认的shader是单面的双面需要自己写。最直接的做法是关掉Cull但这样会有光照问题背面法线是反的光照算出来是黑的。我的处理方式是在片元着色器里判断SV_IsFrontFace如果是背面就把法线取反同时翻转切线的副法线方向。if (!isFrontFace) { normal -normal; tangent.w -tangent.w; }这样背面也能得到正确的光照。但双面渲染的性能开销是实打实的像素填充率翻倍。所以我在材质上加了_DoubleSided开关只有需要的材质才开。另外双面材质的阴影投射也要特殊处理Unity的ShadowCaster Pass默认是单面的双面材质需要额外写一个双面阴影Pass否则阴影会缺一半。性能方面我实测过一个场景里如果有30%的材质是双面帧率大概掉10%到15%。如果双面材质面积很大比如大片树叶掉帧会更明显。所以我的建议是能用单面就用单面实在需要双面再开而且尽量把双面材质的面积控制小。3. 实操过程与核心环节实现3.1 从零搭建渲染管线的步骤拆解搭建这套系统的第一步是确定渲染管线。我用的是Unity的URPUniversal Render Pipeline因为它的可编程Render Feature机制很适合做后处理扩展。如果你用的是内置管线思路一样只是实现方式换成Command Buffer。第一步创建URP Asset配置好基础的光照、阴影、后处理。这里要注意URP默认的色调映射是ACES风格化渲染我建议改成Neutral或者None因为ACES的对比度太高会把风格化的色阶压平。我一般用Neutral保留一点电影感但不过分。第二步写一个自定义的Render Feature插入在后处理的最后一步。这个Feature负责LUT采样、描边、排线叠加。Render Feature的Execute方法里用Blit把相机颜色传到临时RT然后跑自定义的shader再传回去。注意RT的格式要用RenderTextureFormat.DefaultHDR否则高光会被截断。第三步写风格化材质的shader。我基于URP的Lit shader改保留PBR光照在输出前插入风格化计算。关键是在UniversalFragmentPBR之后、color返回之前把color拿来做量化、排线、边缘光。这里要注意URP的Lit shader有多个Pass改的时候要确保ForwardLit和ShadowCaster都改到否则阴影和光照不匹配。第四步配置LUT。在Render Feature里暴露一个Texture2D参数美术把LUT贴图拖进去。然后在shader里采样。LUT的采样要在色调映射之后因为LUT是针对显示颜色调的不是线性颜色。第五步调试和优化。用Frame Debugger看每个Pass的耗时重点看Blit的次数和RT的切换。我一开始每帧Blit了5次后来合并成2次帧率直接涨了8帧。合并的原则是能在一个shader里算完的就不要分多个Pass。3.2 关键参数的计算与配置实例拿一个具体的场景举例一个卡通风格的角色站在写实风格的地面上整体色调偏暖有描边和排线。参数配置如下。角色材质_PosterizeSteps 4_EdgeSoftness 1.2_RimPower 3.0_HatchStrength 0.4_DoubleSided false。描边用背面膨胀外扩距离_OutlineWidth 0.02颜色用深棕色而不是纯黑这样更柔和。地面材质_PosterizeSteps 0关闭量化保留PBR_HatchStrength 0描边用深度法线_DepthThreshold 0.03_NormalThreshold 0.5。后处理LUT用一张暖色调的32x32x32贴图_LUTIntensity 0.7。排线纹理用一张45度斜线_HatchTiling 8_HatchSpeed 0.02让排线缓慢移动增加手绘感。光照主光方向角45度强度1.2颜色暖白。环境光用渐变天空色浅蓝地面色暖灰。阴影用软阴影强度0.6。这套参数跑下来角色有清晰的色阶和描边地面保持写实质感整体色调统一偏暖。帧率在1080p下GTX 1060能跑满60帧移动端骁龙865大概45帧。实操心得参数不要一次调到位先调色阶和描边确定大风格再调LUT和排线。顺序反了会反复推翻重来。3.3 渲染管线的性能优化与移动端适配移动端做风格化渲染最大的瓶颈是带宽和填充率。LUT采样、描边、排线都是全屏操作每个像素都要算很容易爆。我的优化策略是分档高端机全开中端机关排线和动态描边低端机只保留LUT和静态描边。具体做法是在Render Feature里加一个质量等级判断根据设备性能动态切换shader关键字。比如_HATCH_ON、_OUTLINE_DYNAMIC_ON用Shader.EnableKeyword和DisableKeyword控制。这样同一个shader可以编译出多个变体运行时切换。另一个优化点是RT的精度。LUT采样和描边不需要HDR精度可以用RenderTextureFormat.RGB111110Float或者更低。我实测过把后处理RT从HDR换成LDR带宽省了40%画面几乎看不出区别因为LUT本身就是LDR的。还有一点是描边的分辨率。全屏描边可以在半分辨率下做然后上采样。描边本来就是粗线条半分辨率完全够用性能能省一半。我一般把描边Pass放在一个半分辨率的RT里算完再和主画面合成。移动端还有一个坑是发热降频。风格化渲染的shader通常比PBR复杂长时间跑容易热。我的建议是加一个动态分辨率根据帧率自动调整渲染分辨率帧率掉了就降分辨率稳在30帧比跑60帧然后掉到20帧体验好得多。4. 常见问题与排查技巧实录4.1 颜色偏差与LUT采样错误的排查颜色偏差是LUT环节最常见的问题表现是画面整体偏绿、偏红或者发灰。排查顺序我总结成三步。第一步检查LUT贴图的导入设置。sRGB必须关Wrap Mode必须ClampFilter Mode必须Bilinear。这三个有一个错了颜色就会偏。我遇到过有人用Point过滤结果色阶之间出现硬边还以为是风格化效果其实是采样错了。第二步检查采样公式。蓝通道的偏移最容易写错。正确的公式是color.b * (lutSize - 1) / (lutSize * lutSize) 0.5 / (lutSize * lutSize)。如果写成color.b / lutSize蓝色层会错位画面会偏蓝或者偏黄。第三步检查管线顺序。LUT必须在色调映射之后。如果URP的色调映射是ACESLUT在ACES之前采样颜色会被ACES二次压缩偏得厉害。我一般把LUT放在FinalBlit之前色调映射之后。如果三步都查了还是偏那就用一张中性LUT就是输入输出一样的LUT测试。如果中性LUT也偏说明采样公式或者管线顺序有问题如果中性LUT正常说明是LUT本身调色的问题。4.2 描边断裂、闪烁与性能开销的解决描边断裂通常出现在背面膨胀方案里原因是模型的法线不连续或者有硬边。解决办法是在外扩的时候不要直接用顶点法线而是用平滑后的法线。可以在建模软件里把法线平滑一下或者在shader里用顶点位置和模型中心计算一个近似法线。描边闪烁一般是深度法线方案的阈值问题。深度阈值太小远处物体的深度变化被误判成边缘就会闪烁。解决办法是给深度阈值加一个距离衰减远处阈值调大近处阈值调小。公式大概是threshold _DepthThreshold * (1 distance * 0.1)。性能开销方面描边是除了LUT之外第二贵的全屏操作。优化手段除了半分辨率还可以做“边缘预判”先用一个低精度的深度图算边缘只在边缘像素上跑完整的描边计算。这样大部分像素直接跳过性能能省60%以上。注意描边颜色不要用纯黑。纯黑描边在暗部会跟阴影混在一起看起来像模型破洞。用深灰或者深棕视觉上更舒服。4.3 双面材质光照异常与阴影缺失的修复双面材质的光照异常表现是背面全黑或者光照方向反了。前面说了用SV_IsFrontFace翻转法线。但还有一个坑切线空间。如果材质用了法线贴图背面的切线空间是反的法线贴图会上下颠倒。解决办法是同时翻转切线的副法线方向也就是tangent.w -tangent.w。阴影缺失是因为ShadowCaster Pass默认只渲染正面。双面材质需要额外写一个Pass设置Cull Off并且把顶点沿法线稍微外扩一点避免阴影自遮挡。这个Pass的LightMode要设成ShadowCaster其他跟主Pass一样。还有一个隐蔽的问题双面材质的深度排序。如果两个双面面片重叠深度写入会互相遮挡导致闪烁。解决办法是双面材质尽量不写深度或者用Alpha Test代替Alpha Blend。如果必须用Alpha Blend那就得手动控制渲染顺序把双面材质放在最后渲染。4.4 常见问题速查表问题现象可能原因排查步骤解决方案画面整体偏色LUT导入设置错误检查sRGB、Wrap、Filter关闭sRGBClampBilinear色阶边缘太硬_EdgeSoftness太小查看参数值调到1.0到1.5之间描边闪烁深度阈值太小检查阈值和距离加距离衰减远处调大阈值背面全黑法线未翻转检查SV_IsFrontFace翻转法线和切线副法线阴影缺失ShadowCaster单面检查Pass设置加双面ShadowCaster Pass帧率骤降Blit次数过多用Frame Debugger合并Pass半分辨率描边LUT无效果管线顺序错误检查色调映射位置LUT放在色调映射之后排线太脏排线纹理对比度高查看纹理降低对比度降低强度这张表是我在实际项目中反复踩坑总结出来的基本上覆盖了80%的常见问题。遇到新问题的时候先对照这张表排查大部分情况能快速定位。4.5 独家避坑技巧与经验总结第一个技巧LUT不要只用一张。我一般会准备三张LUT——白天、黄昏、夜晚根据场景时间动态切换。切换的时候用lerp过渡不要硬切硬切会有明显的跳变。过渡时间0.5秒左右比较自然。第二个技巧描边宽度跟分辨率挂钩。在1080p下调好的描边宽度到4K下会显得太细。解决办法是用_ScreenParams计算一个缩放系数outlineWidth * (_ScreenParams.y / 1080.0)这样在不同分辨率下视觉粗细一致。第三个技巧排线纹理的UV不要用屏幕空间。屏幕空间的排线会“粘”在屏幕上相机一动排线不动看起来很假。用物体空间或者世界空间的UV排线会跟着物体或者场景动手绘感更强。第四个技巧风格化渲染的后期不要加Bloom。Bloom会把色阶边缘糊掉风格化最怕的就是糊。如果非要加强度控制在0.1以下阈值调高只让最亮的部分微微发光。第五个技巧移动端慎用fwidth。fwidth在部分移动GPU上性能很差能用导数的地方尽量用预计算的常量代替。如果必须用把fwidth的结果缓存到一个变量里不要每个像素算多次。这套系统我前后迭代了大概半年从最初的纯后处理方案到现在的三层架构中间推翻重来了好几次。最大的体会是风格化渲染不是把参数拉满而是找到那个“刚刚好”的平衡点。色阶少一阶、描边细一点、LUT淡一点往往比全开效果更好。另外美术和程序的沟通很重要参数的名字和范围要直观最好在编辑器里就能实时预览不然调参效率极低。我现在给每个参数都加了Range属性和中文注释美术自己就能调省了我大量时间。
返回列表