拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

风格化渲染系统搭建:LUT调色与NPR光照实战指南

风格化渲染系统搭建:LUT调色与NPR光照实战指南 风格化渲染这件事我最早是从一个很朴素的需求开始的手上有个场景PBR 材质跑出来的效果太标准了金属就是金属、布料就是布料画面干净但没性格。客户要的是那种一眼能认出来的视觉调性——可能是偏手绘的、可能是偏胶片感的、也可能是偏某种特定美术风格的。于是就有了这套风格化渲染系统。它本质上是一套在标准 PBR 管线之上叠加风格化处理层的方案核心手段包括 LUT 调色、NPR 光照模型替换、Shader 层面的材质重写以及针对双面材质、描边、色阶压缩等具体问题的处理。适合谁看如果你已经能写基本的 Shader、理解 PBR 的工作流但一到怎么让画面有风格就卡壳那这篇东西就是给你准备的。下面我按实际搭建这套系统的顺序把每个环节的选择理由、踩过的坑和能直接抄的配置都摊开讲。1. 先想清楚风格化渲染到底在改什么很多人一上来就找 LUT 滤镜包觉得套个滤镜画面就有风格了。我试过效果很有限而且容易翻车。原因在于风格化不是单一环节的事它至少涉及三个层面——光照模型、材质表现、后期调色。你只动后期等于在标准光照的结果上硬压颜色暗部和高光的信息已经被 PBR 的物理模型锁死了压出来的风格是浮在表面的。1.1 PBR 和 NPR 的根本分歧在哪PBR 的核心是能量守恒和微表面理论光照结果由 BRDF 决定金属度、粗糙度、法线这些参数共同作用出来的画面物理正确、可预测。NPR 不追求物理正确它追求看起来对。比如卡通渲染里漫反射不是连续的余弦衰减而是被量化成几档色阶高光不是 GGX 分布而是一个硬边的小圆点或者一条拉长的光斑。这个分歧决定了你的系统架构如果你要的是强风格化光照模型必须换掉不能只在 PBR 结果上做后处理。我一般的做法是保留 PBR 作为基础光照层然后在它之上叠加一个 NPR 层通过一个权重参数控制风格化强度。这样既能保留 PBR 的材质细节又能拿到 NPR 的造型感。1.2 风格化强度的可控性是系统设计的第一原则我见过太多项目风格化参数一调就回不去了美术想微调一下发现整个画面崩掉。问题出在参数耦合。比如你把色阶量化做得太狠暗部直接变成纯黑这时候再想通过调色把暗部提回来就无能为力了因为信息已经丢了。所以这套系统的第一原则是每一层风格化处理都要可逆或至少可旁路。LUT 可以旁路NPR 光照层可以用权重混合回 PBR色阶量化要保留原始亮度信息用于后续调整。具体实现上我会在 Shader 里保留一份未风格化的中间结果风格化操作都基于它做而不是链式地在上一层的输出上继续改。1.3 一个常见的架构误区新手容易把所有风格化逻辑塞进一个 Shader 里一个文件几千行改一个参数要重新编译半天而且不同材质复用困难。我的做法是拆成三层基础层标准 PBR 光照输出 albedo、normal、roughness、metallic 和直接光照结果风格层NPR 光照、色阶量化、描边、材质特殊处理比如双面材质的背面处理后期层LUT 调色、颗粒、暗角、色差等屏幕空间效果三层之间通过明确的接口传递数据风格层可以单独开关后期层可以整体旁路。这样调试的时候能快速定位问题出在哪一层。2. LUT 调色最容易被低估也最容易翻车的环节LUT 这东西看起来简单导入一张图采样一下完事。但实际项目里LUT 环节出的问题能占风格化 bug 的一半以上。热词里那些 LUT 滤镜包、slog3 还原 LUT、Blackmagic 的 LUT 包本质上都是色彩查找表原理一样但用法差异很大。2.1 LUT 的两种类型和选择依据LUT 分 1D 和 3D 两种。1D LUT 只做通道独立的映射比如把 R 通道的 0.2 映射到 0.3它处理不了通道之间的交叉影响。3D LUT 是一个三维查找表输入 RGB 三个值输出映射后的 RGB能处理色彩偏移、色相旋转这类复杂变换。风格化渲染里用的基本都是 3D LUT。选择 LUT 的时候我关注三个指标尺寸、精度、色彩空间。尺寸常见的有 16x16x16、32x32x32、64x64x64。16 的精度在暗部容易出现色带64 的精度够但采样开销大。我一般用 32配合三线性插值画质和性能平衡得比较好。色彩空间这块是重灾区后面单独说。2.2 LUT 色彩空间不匹配导致的偏色问题这是我最常遇到的坑。你从网上下载一个 LUT 滤镜包导入引擎画面直接偏绿或者偏灰。原因通常是 LUT 的制作色彩空间和你的渲染管线色彩空间不一致。LUT 可能是在 sRGB 空间下制作的而你的管线是线性空间直接采样就会出问题。正确的做法是如果 LUT 是 sRGB 空间制作的采样前要把输入颜色从线性转到 sRGB采样后再转回线性。或者更省事的办法在导入 LUT 纹理时把它的 sRGB 标志关掉让引擎按线性数据处理但这要求 LUT 本身是按线性空间生成的。我实测下来最稳的方案是在 Shader 里显式做色彩空间转换不依赖引擎的自动处理。// 线性转 sRGB 近似 float3 LinearToSRGB(float3 c) { return c 0.0031308 ? c * 12.92 : 1.055 * pow(c, 1.0/2.4) - 0.055; } // sRGB 转线性 float3 SRGBToLinear(float3 c) { return c 0.04045 ? c / 12.92 : pow((c 0.055) / 1.055, 2.4); } // LUT 采样 float3 ApplyLUT(float3 color, Texture3D lutTex, SamplerState lutSampler) { float3 sRGBColor LinearToSRGB(color); // 蓝色通道决定切片注意偏移 float blue sRGBColor.b * (lutSize - 1) / lutSize 1.0 / (2.0 * lutSize); float3 lutCoord float3(sRGBColor.r, sRGBColor.g, blue); float3 lutResult lutTex.Sample(lutSampler, lutCoord).rgb; return SRGBToLinear(lutResult); }这段代码里那个蓝色通道的偏移是很多人忽略的细节。3D LUT 在纹理里是平铺成 2D 的采样时如果直接拿 b 通道当坐标会在切片边界出现插值错误导致画面出现细条纹。加上半个纹素大小的偏移能解决这个问题。2.3 LUT 强度控制和混合策略直接套 LUT 往往太重风格化痕迹太明显。我一般会加一个强度参数在原始颜色和 LUT 结果之间做插值。但这里有个细节插值应该在什么空间做在线性空间插值会导致中间调偏暗在 sRGB 空间插值更符合视觉直觉。我的做法是转 sRGB、采样 LUT、插值、再转回线性。另外不同场景可能需要不同 LUT。比如白天场景用一个夜晚场景用另一个。这时候可以用 LUT 混合把两张 LUT 按权重混合后再采样或者分别采样后混合结果。前者性能好一点后者灵活度高一点。我一般用后者因为可以给不同 LUT 单独控制强度。提示LUT 文件格式要注意.cube 和 .png 是两种常见格式。.cube 是文本格式精度高但加载慢.png 是图片格式加载快但要注意色彩空间和位深。如果 LUT 是 8 位 png暗部精度可能不够建议用 16 位。3. NPR 光照模型把物理正确换成视觉正确NPR 光照是风格化渲染的核心。PBR 的光照是连续的、物理正确的NPR 的光照是离散的、造型优先的。这一层做得好不好直接决定画面有没有风格。3.1 漫反射的色阶量化怎么做才不脏卡通渲染最典型的特征就是漫反射被量化成几档。最简单的做法是floor(NdotL * steps) / steps但这会出问题色阶边界是硬的而且暗部直接压到 0丢失细节。我的做法是分三步先做标准的 Lambert 计算然后用 smoothstep 在色阶边界做一点过渡最后把量化后的结果和原始结果按权重混合。这样既有色阶的造型感又保留了暗部细节。float3 NPRDiffuse(float3 lightDir, float3 normal, float3 lightColor, float3 albedo, float steps, float smoothness, float blendWeight) { float NdotL dot(normal, lightDir); // 映射到 0-1 float halfLambert NdotL * 0.5 0.5; // 量化 float quantized floor(halfLambert * steps) / steps; // 在色阶边界做平滑 float smoothed smoothstep(0.0, smoothness, frac(halfLambert * steps)); quantized smoothed / steps; // 混合 float finalDiffuse lerp(halfLambert, quantized, blendWeight); return albedo * lightColor * finalDiffuse; }这里steps控制色阶数量一般 2 到 4 档比较自然太多就失去风格感太少画面会显得很平。smoothness控制边界过渡宽度0.1 到 0.3 之间比较合适。blendWeight是风格化强度0 就是纯 PBR1 就是纯 NPR。3.2 高光的风格化处理PBR 的高光是 GGX 分布一个连续的亮斑。NPR 的高光通常是一个硬边的小圆点或者一条拉长的光斑。实现上我用 Blinn-Phong 的中间向量然后对高光做阈值化。float3 NPRSpecular(float3 lightDir, float3 viewDir, float3 normal, float3 lightColor, float specSize, float specSmooth, float specIntensity) { float3 halfVec normalize(lightDir viewDir); float NdotH dot(normal, halfVec); // 阈值化形成硬边高光 float spec smoothstep(1.0 - specSize, 1.0 - specSize specSmooth, NdotH); return lightColor * spec * specIntensity; }specSize控制高光大小specSmooth控制边缘硬度specIntensity控制亮度。这三个参数配合调能做出从小圆点到大光斑的各种效果。我一般会把高光颜色和光源颜色解耦允许高光单独染色这样能做出一些非物理但好看的效果。3.3 边缘光和轮廓光的处理边缘光Rim Light是风格化渲染里很常用的手法能在物体边缘加一圈亮边增强轮廓感。原理很简单用视角方向和法线的点积边缘处点积接近 0取反后接近 1。float3 RimLight(float3 viewDir, float3 normal, float3 rimColor, float rimPower, float rimIntensity) { float rim 1.0 - saturate(dot(viewDir, normal)); rim pow(rim, rimPower); return rimColor * rim * rimIntensity; }rimPower控制边缘光的宽度值越大边缘越细。rimIntensity控制强度。这里有个技巧边缘光可以只在暗部出现或者只在特定方向出现通过和光照结果做乘法或条件判断来实现。比如rim * (1.0 - NdotL)就能让边缘光只在背光面出现模拟透光效果。3.4 NPR 和 PBR 的混合策略纯 NPR 会丢失 PBR 的材质细节纯 PBR 又没有风格感。我的方案是分层混合基础颜色和粗糙度这些材质属性还是走 PBR光照计算走 NPR最后按权重混合。这样金属还是金属的质感但光照造型是风格化的。具体实现上我会在 Shader 里同时计算 PBR 光照和 NPR 光照然后用一个全局的风格化权重混合。这个权重可以按材质类型分别设置比如角色用高权重场景用低权重这样画面有主次。4. Shader 层面的材质特殊处理风格化渲染不只是光照的事材质本身也需要特殊处理。热词里提到的 Unity 双面材质 Shader 就是一个典型场景。4.1 双面材质的法线翻转问题双面材质在渲染布料、树叶、纸张这类薄片物体时很常用。但直接开双面渲染会有一个问题背面的法线是朝里的光照计算会出错背面看起来是黑的或者光照方向反了。正确的做法是在片元着色器里判断当前渲染的是正面还是背面如果是背面就把法线翻转。在 Unity 里可以用SV_IsFrontFace语义来获取这个信息。// 片元着色器输入 struct FragmentInput { float4 pos : SV_POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; float3 viewDir : TEXCOORD1; bool isFrontFace : SV_IsFrontFace; }; // 片元着色器 float4 Frag(FragmentInput input) : SV_Target { float3 normal input.normal; if (!input.isFrontFace) { normal -normal; } // 后续光照计算用 normal ... }这个翻转操作看起来简单但漏掉的话背面光照会完全错误。我踩过这个坑当时调了半天以为是光照参数问题最后发现是法线没翻转。4.2 材质 ID 和风格化参数的分层控制一个场景里不同物体可能需要不同的风格化参数。比如角色的皮肤要柔和一点金属盔甲要高光锐利一点布料要色阶明显一点。如果所有物体用同一套参数画面会很单调。我的做法是给每个材质加一个材质 ID在 Shader 里根据 ID 查一张参数表拿到该材质对应的风格化参数。参数表可以是一张纹理也可以是一个常量缓冲区。这样美术可以在不重新编译 Shader 的情况下调整每个材质的风格化表现。材质类型色阶数高光大小边缘光强度风格化权重皮肤40.30.50.6金属20.10.80.8布料30.50.20.7头发30.21.00.9这张表是我在一个项目里实际用的参数效果还不错。皮肤色阶多、风格化权重低保持柔和金属色阶少、高光锐利强调质感头发边缘光强做出透光感。4.3 描边的几种实现和选择描边是风格化渲染的另一个标志性特征。常见实现有三种法线外扩、后处理边缘检测、几何着色器外扩。法线外扩是在顶点着色器里沿法线方向把顶点往外推一点渲染背面形成描边。优点是简单、可控缺点是硬边物体比如立方体的描边会断开。后处理边缘检测是在屏幕空间做优点是能描出所有边缘缺点是无法控制单个物体的描边粗细而且深度不连续的地方容易出问题。几何着色器外扩效果最好但性能开销大移动端基本不能用。我一般用后处理边缘检测做整体描边配合法线外扩做特定物体的描边。后处理描边用深度和法线做边缘判断深度差异大的地方或者法线夹角大的地方认为是边缘。// 后处理描边 float4 EdgeDetect(float2 uv, float2 texelSize, Texture2D depthTex, Texture2D normalTex, float depthThreshold, float normalThreshold, float4 edgeColor) { float depth depthTex.Sample(sampler, uv).r; float3 normal normalTex.Sample(sampler, uv).rgb; float depthDiff 0; float normalDiff 0; // 采样周围像素 for (int i -1; i 1; i) { for (int j -1; j 1; j) { if (i 0 j 0) continue; float2 offset float2(i, j) * texelSize; float neighborDepth depthTex.Sample(sampler, uv offset).r; float3 neighborNormal normalTex.Sample(sampler, uv offset).rgb; depthDiff max(depthDiff, abs(depth - neighborDepth)); normalDiff max(normalDiff, dot(normal, neighborNormal)); } } float edge (depthDiff depthThreshold || normalDiff normalThreshold) ? 1.0 : 0.0; return edge * edgeColor; }depthThreshold和normalThreshold需要根据场景尺度调太小会满屏描边太大会漏掉边缘。我一般从 0.01 和 0.9 开始试。5. 风格化渲染系统的性能优化和调试风格化渲染比纯 PBR 多了不少计算性能优化是绕不开的。而且风格化效果主观性强调试方法也和 PBR 不一样。5.1 哪些环节可以省哪些不能省LUT 采样是一次 3D 纹理采样开销不大但如果在移动端每帧对全屏做累积起来也可观。我的做法是 LUT 只在后期做一次不在每个物体的 Shader 里做。NPR 光照的计算量比 PBR 小因为色阶量化比 GGX 简单所以光照层不是瓶颈。描边是开销大头尤其是后处理描边全屏采样周围像素移动端要控制采样数量。优化策略上我会把风格化参数按重要性分级核心参数色阶数、风格化权重每帧更新次要参数边缘光颜色、高光染色可以低频更新或者烘焙到纹理里。描边在移动端用 4 邻域采样代替 8 邻域画质损失不大但性能提升明显。5.2 风格化效果的调试方法PBR 调试有明确的参考你可以对比离线渲染结果。风格化没有标准答案调试靠眼睛。我一般会做几个调试视图只显示 NPR 光照结果、只显示 LUT 结果、只显示描边、显示风格化权重。这样能快速定位问题出在哪一层。另外风格化参数最好做成实时可调的不要每次改参数都重新编译 Shader。用常量缓冲区传参数配合引擎的实时调参面板美术能自己调效果程序不用陪着改。5.3 不同平台的适配注意事项PC 端和主机端基本不用太担心性能可以用完整的风格化管线。移动端要砍LUT 用 16 尺寸的描边用 4 邻域NPR 光照的色阶数控制在 3 以内边缘光可以关掉或者只在角色上开。Switch 这类设备介于两者之间可以保留大部分效果但要控制分辨率。还有一个容易忽略的点不同平台的色彩空间和 Gamma 校正可能不一样。LUT 和色彩相关的处理在不同平台上要验证不能只在 PC 上调好就完事。我一般会在目标平台上重新校准一遍 LUT 强度。6. 从零搭建这套系统的实操顺序如果你现在要动手搭一套我建议按这个顺序来每一步都能看到效果不会搭到一半发现方向错了。6.1 第一步搭好 PBR 基础层并验证先把标准 PBR 跑通确保光照、材质、阴影都正常。这一步不要加任何风格化就是纯 PBR。验证方法是找一个参考物体比如一个金属球和一个布料球看反射和漫反射是否符合预期。基础层不稳后面风格化都是空中楼阁。6.2 第二步加 NPR 光照层用权重混合在 PBR 基础上加 NPR 光照计算用权重参数混合。先把权重设成 0确认画面和纯 PBR 一致然后慢慢加大权重观察变化。这一步重点调色阶数和高光参数找到你想要的风格方向。6.3 第三步接入 LUT 后期NPR 光照调好后加 LUT 后期。先旁路 LUT确认画面正常然后开启 LUT从低强度开始加。注意色彩空间转换如果画面偏色先检查 LUT 的色彩空间和管线是否匹配。6.4 第四步处理材质特例主体效果出来后处理双面材质、描边、特殊材质这些细节。双面材质记得翻转背面法线描边先用法线外扩做简单版本效果不够再上后处理。6.5 第五步性能优化和平台适配最后做性能优化砍掉不必要的计算在目标平台上验证效果和帧率。这一步可能要反复调整参数因为性能和画质要平衡。注意每一步都要做版本备份。风格化参数很容易调乱没有备份的话想回到之前的状态都回不去。我一般用 Git 管理 Shader 和参数配置每调出一个满意的效果就提交一次。这套系统我前后迭代了大概三个月中间推翻重来过两次。最大的体会是风格化渲染没有银弹LUT 解决不了光照的问题NPR 解决不了材质的问题每个环节都要单独处理然后用权重把它们串起来。参数的可控性和可逆性比效果本身更重要因为风格是调出来的不是一次写死的。
返回列表