ARTICLE DETAIL

资讯详情

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

手绘质感实时渲染:Unity Shader实现PBR纹理的画笔与画布效果

手绘质感实时渲染:Unity Shader实现PBR纹理的画笔与画布效果 去年接了一个文旅项目的实时渲染片甲方丢过来一堆文物扫描的PBR贴图开口就是要“手绘质感的画面”。我第一反应是改饱和、叠滤镜、套个风格化后处理结果预览出来的东西怎么看都像塑料玩具。后来我才想明白一件事绘画感从来不是调色能调出来的它是材质自身对纹理采样方式、对光照交互逻辑的一次重新解释。后面我把这套逻辑落成了一个自写的Unity Shader材质核心就干一件事——任何一张普通纹理贴进去不需要重新绘制它都会自带笔触、色阶断层和画布肌理。这篇不聊大而全的NPR渲染管线就专门拆这款材质它的原理、代码、纹理预处理、坑点以及调参数时容易翻车的地方。想给写实PBR换个手绘皮肤的话这套思路直接可以拿去做底子。1. 先把“绘画感”拆成三个可计算的维度1.1 为什么照片级PBR纹理放进引擎只剩“塑料感”先复盘一下最初的失败。常规PBR材质的渲染目标是“物理正确”BRDF要保证能量守恒粗糙度、金属度各司其职纹理从高光到暗部是连续渐变的过渡。这套逻辑非常适合还原真实物体但它天然和“绘画感”冲突。因为手绘作品里颜料本质上是在做有限次数的叠加每一层的边缘都有明确的边界不可能出现物理世界那种无限细分的光照过渡。你把一张照片级纹理丢进标准PBR Shader里得到的一定是照片而不是画。还有个容易被忽略的点PBR通道里的金属度、粗糙度要求干净准确但绘画材质的金属感基本是画出来的粗糙度变化也倾向于“区域块状”而不是像素级起伏。很多人在照片上硬叠滤镜结果高光反射还是物理正确的画布没有肌理笔触没有方向看起来就像“AI手绘风”假得不行。1.2 绘画感的三个可计算特征在动手写Shader前我把“绘画感”强行拆成了三个能落在数学上的特征这也是整个材质的三个处理层色阶断层Posterization颜料的混合是有限的所以明暗过渡应该出现阶梯感而不是平滑渐变。这对应数字图像里的色调分离也是最容易出“画味”的一步。笔触痕迹Stroke Distortion画笔会在画布上留下方向性轨迹颜色不是平铺上去的而是被“拖”出来的。体现在Shader里就是对UV采样坐标做方向性扰动。画布肌理Canvas Bump布纹、纸纹的微表面起伏会让颜料在不平整的介质上产生斑驳的高光和局部的浓淡差异。这一步需要靠法线扰动解决。只要把这三个维度组合好不管是照片纹理、材质库纹理还是程序化生成的纹理都会瞬间带上一层手绘质感。后面我所有的代码、参数、踩坑记录都围绕这三条展开。2. 核心Shader三件套色阶断层、笔触扰动、画布法线这套材质我用的是Unity URP下的HLSL写法实际逻辑在片元着色器里按顺序执行先采样Albedo颜色做色阶量化再基于坐标噪声做笔触扰动最后把画布法线叠到原本的法线细节上参与光照。下面拆开讲。2.1 第一步在色阶上做“颜料分层”色调分离的代码非常简单就是把连续的颜色值映射到有限的梯级上float3 QuantizeColor(float3 color, float levels) { float3 quantized floor(color * levels 0.5) / levels; return saturate(quantized); }levels代表单通道分几阶。设成5就意味着每个颜色通道只有5个梯度整个画面的明暗过渡会呈现明显的“分层感”像是用彩铅叠了几次色。设成2就直接往海报化方向走颗粒感极重。但直接这么做会有一个严重问题大面积純色区域会因为梯度边界产生肉眼可见的色带banding尤其暗部特别明显。解决方式不是把梯度调多而是加抖动dithering用随机噪声把梯度边界的硬边“敲碎”模拟颜料混色时残留的细小颗粒float3 DitherColor(float3 color, float levels, float2 uv) { float rand frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453); float3 band floor(color * levels) / levels; return saturate(band (rand - 0.5) / levels); }这个抖动幅度我建议控制在半个梯级以内太大会让画面发脏太小又压不住色带。实际项目里我还会在暗部区域单独调大抖动权重因为人眼对阴影区域的颜色断层最敏感。2.2 第二步用方向性噪声模拟笔触痕迹这一步是整个材质“有没有绘画感”的分水岭。很多仿手绘效果只做了色调分离结果画面像动漫滤镜没有“笔触”那种手工涂抹的味道。笔触的核心理念是在采样Albedo之前先对UV坐标做一个有方向的偏移。偏移量需要来自一个连续噪声不能是纯白噪音否则画面只会变脏。float SimplexNoise(float2 p) { // 这里用你习惯的Simplex/Perlin噪声实现即可 } float2 uv i.uv; float2 worldPos i.worldPos.xz; float freq _StrokeFrequency; // 笔触噪声频率 float noise SimplexNoise(worldPos * freq); float2 strokeDir normalize(float2(0.8, 0.35)); // 全局笔触方向向量 float2 uvOffset strokeDir * (noise * 2.0 - 1.0) * _StrokeStrength; fixed4 albedo tex2D(_MainTex, TRANSFORM_TEX(uv uvOffset, _MainTex));这里有两个细节是常规教程不会写的但我建议你重点注意扰动一定要放在世界坐标空间而不是UV空间。如果直接用uv做噪声输入物体一旋转笔触方向就会跟着转看起来像整张贴图在水下漂。改用worldPos.xz后无论模型怎么摆放笔触方向都保持稳定这才像画师固定用同一方向的笔法。噪声频率和UV密度是两个维度。频繁的笔触会碎低频的长笔触才有“一笔到底”的感觉。默认我习惯把_StrokeFrequency控制在2到6之间数值再大就成了噪点不像绘画。如果你的画面有大量曲面还可以在这个阶段叠加顶点颜色Vertex Color来控制局部笔触强弱。比如脸部区域强手绘感金属饰品弱化这个扩展放在后面第6章细说。2.3 第三步给表面叠一层“画布法线”有了色阶和笔触画面会接近“插画风”但放在3D场景里旋转视角时会发现高光反射过于光滑像喷了清漆的插画少了画布本身的粗糙感。这一步需要程序化生成布纹法线叠到原有法线细节上float2 CanvasBump(float2 worldPos) { float weaveX sin(worldPos.x * _CanvasScale) * 0.5 0.5; float weaveY sin(worldPos.y * _CanvasScale) * 0.5 0.5; float weave weaveX * weaveY; return float2(weave - 0.5, weave - 0.5) * _CanvasStrength; } float3 normalTS UnpackNormal(tex2D(_BumpMap, uv)); float2 canvasBump CanvasBump(worldPos); float3 finalNormalTS normalize(float3(normalTS.xy canvasBump.xy, normalTS.z));这里借用的是细节法线混合的思路原法线贴图负责表面的大起伏布纹噪声只负责在微观层面制造“凹凸不平”的反射扰动。Shader里的布纹法线是程序化生成的不用额外贴图带宽友好。但有个天坑画布法线强度不要超过0.3。一旦超过高光区域会像闪光纸一样剧烈闪烁旋转摄像机时画面全是噪点第一眼看上去非常“脏”。我踩过这个坑后在材质面板里直接把_CanvasStrength钳制到0到0.3美术同学想拧都拧不坏。3. 让“任何纹理”都能直接接入的预处理流程写好了Shader接下来就是“任何纹理”这四个字的兑现。理论上拍照纹理直接贴进去也能出效果但如果导入、压缩、tiling这些环节没管好绘画感会被硬生生打折。我整理了一套预处理流程按顺序走一遍基本不会翻车。3.1 纹理压缩乱设绘画感直接报废很多项目默认使用ASTC或BC7压缩这本身没问题但压缩质量等级会直接影响色阶断层的表现。本来你的张色阶量化就在人为制造梯度边界压缩算法再引入块状噪斑这两者叠加会让暗部区域产生一块一块的“脏色块”和颜料颗粒完全是两回事。我的建议是Albedo这张图在Unity导入设置里手动选择“High Quality”条件允许的情况下用RGB Compressed BC7或完全不压缩。法线贴图和画布高度图可以走低一档的压缩因为Step 2和Step 3的噪声扰动本质上会掩盖一部分压缩痕迹但最终颜色通道的压缩痕迹是藏不住的。另外如果你用Substance Painter导出贴图注意导出路径不要带中文和特殊字符否则进Unity后经常出现贴图紫色或路径丢失。这个和“SU贴图不显示纹理”是同一类问题多数时候不是纹理坏了而是导入阶段路径或压缩格式已经出错。3.2 重复纹理的接缝与“一眼假”问题纹理一tiling画面上就会出现肉眼可辨的重复感。普通PBR里可以通过贴图排布来缓解但绘画感材质里色阶断层会把重复边界放得更大因为接缝处往往是色带跳变的最强位置。处理重复感有两个方向。第一个方向是源头处理在Blender或Substance Designer里烘焙可平铺贴图注意烘焙时的镜像边缘以及X/Y方向的交叉重叠这是Blender重复纹理烘焙的关键步骤方法网上一搜一大把关键是烘焙前要把UV边界seam清理干净。第二个方向是Shader侧处理适用现有不可平铺纹理。我给Albedo叠加一个低频噪声打碎重复感float tileNoise tex2D(_NoiseTex, uv * _NoiseTiling).r; albedo.rgb * 0.9 0.1 * tileNoise;这个技巧的原理是让相邻tile区域的明暗细微变化记忆点不再集中在同一个接缝上人眼就不容易识别出重复单元。注意噪声tiling尽量不要和主贴图tiling成整数倍关系否则两者周期叠加反而出现更大的摩尔纹。3.3 没有法线贴图时如何凑出画布细节现实中经常遇到这样的纹理只有一张漫反射没有配套法线。大部分人的做法是放弃法线细节直接让表面光洁如镜——对绘画感材质来说这不可接受。解法是用亮度信息生成虚拟法线。对采样得到的灰阶高度图求相邻像素差就能得到近似的法线方向float h tex2D(_MainTex, uv).r; float hL tex2D(_MainTex, uv float2(_MainTex_TexelSize.x, 0)).r; float hD tex2D(_MainTex, uv float2(0, _MainTex_TexelSize.y)).r; float2 grad float2(hL - h, hD - h) * _BumpFromAlbedo; float3 normalTS normalize(float3(-grad.x, -grad.y, 1.0));这个办法在近距离或高频细节上表现一般但对绘画感材质的画布肌理层面足够用了。代价是比普通材质多了两次纹理采样移动端建议离线烘焙一张高度图不要用动态求导。3.4 顺带处理一下“贴图不显示”的乌龙热词里连续出现“SU贴图不显示纹理”“UG表面材质渲染”我估计不少人是在这里卡住了。实话说这类问题90%不是纹理本身坏而是三件事没检查一是UV展开丢失模型全挤在一个像素二是材质球通道没接对Shader里取了_BumpMap材质球上却把法线贴图拖进了Albedo槽三是导入设置把纹理类型设置成了Sprite或Single Channel导致采样出来是黑的。处理顺序建议是先在材质球上确认Shader匹配的通道再切到场景视图看模型线框确认UV分布符合预期最后检查导入面板的Texture Type和压缩格式。把这个顺序走完大概能覆盖绝大多数“纹理不显示”的问题。4. 双面材质、视差替身与“纹理内部/像素内部”的解惑到这里材质已经能出不错的“画感”。但真正把它放进实际项目还要处理几个让新手头大的概念双面渲染、视差贴图以及最近被反复讨论的“纹理内部和像素内部的区别”。4.1 双面材质的法线翻转陷阱绘画感材质常被用在纸片式模型、树叶、旗子这类薄片几何上需要双面可见。只把Unity的Cull Mode设为Off是不够的——背面虽然渲染了但顶点顺序反了法线会指向几何内部光照结果完全错误轻则黑面重则高光乱跳。正确的做法是在Shader里手动做一次朝向调整float facing dot(normalWS, viewDirWS); float3 finalNormalWS facing 0 ? normalWS : -normalWS;这样正面和背面的法线都能保证朝向摄像机一侧参与光照计算。当然开启双面会带来Overdraw翻倍如果你在同一片面上又叠加了视差映射绘制开销会明显上涨这两个功能通常不建议同时打开。4.2 用视差映射做出“颜料堆积”的立体感绘画不是平涂颜料在画布上是有厚度的。尤其是油画风格颜料堆积处的光影偏移感很强。简单法线扰动模拟不了这种“高度错位”需要上视差映射Parallax Mapping。最实用的轻量实现是单层视差偏移采样float2 ParallaxOffset(float2 uv, float3 viewDirTS, float heightScale) { float depth tex2D(_HeightMap, uv).r; return uv - viewDirTS.xy / viewDirTS.z * depth * heightScale; }_HeightMap就是一张灰度高度图拿Albedo转的灰度也行。viewDirTS要放在切线空间计算不然视角方向在曲面上的投影会错位笔触厚度看起来像在滑动。标准的视差遮蔽映射POM会在循环里分层采样效果更好但开销高好几倍。我在这套材质里只用单层因为绘画感的视觉本身偏概括颗粒太大反而不像画。高度缩放控制在0.02到0.05之间别超过0.1否则边缘位移会穿帮。4.3 纹理内部和像素内部到底差在哪这是最近群里讨论很多的一个概念。我的理解是这样的“纹理内部”指纹理坐标所代表的连续空间信息包括texel之间插值、Mipmap层级选择、UV方向和缩放“像素内部”指单颗屏幕像素上的颜色与亮度值是离散的、非连续的。绘画感的扰动必须发生在“纹理内部”这个空间而不是“像素内部”。用前面笔触扰动的例子说对UV坐标加一个方向性偏移让采样点顺着画笔方向流动这是纹理空间内部的连续形变。如果你换成对像素颜色做随机抖动得到的不是笔触是电视雪花而且一旦Mipmap介入抖动模式会被平均掉画面在远处会变成均匀的灰雾。同理色阶量化虽然作用于像素颜色但它的“阶数”是在纹理内部定义好的连续梯级像素只是映射到这个梯级上。理解这个区别能帮你少走弯路一切物理上的“流动”“拖拽”“堆积”效果先在纹理坐标上找解法一切“颗粒”“噪点”“闪烁”效果才轮到像素层面。这套材质里笔触属于前者抖动属于后者。5. 参数调优、性能压榨与常见翻车现场Shader写完只是第一步材质要出效果靠的是反复调参。我把这套材质最终沉淀成了几个关键参数美术在Inspector里拧起来很直观。下面给出一份速查表以及我压性能时常用的三板斧。5.1 关键参数速查表与默认值参数默认值推荐范围作用_PaletteLevels色阶数5212控制色调分离强度越低越接近海报化_DitherAmount抖动强度0.501压色带太大会让暗部发脏_StrokeStrength笔触强度0.0300.1控制UV偏移量太大画面糊成一片_StrokeFrequency笔触频率4120控制笔触疏密越高越碎_CanvasScale布纹密度402080画布纹理的网格密度过高会闪_CanvasStrength布纹法线强度0.1500.3法线扰动强度大于0.3高光开始爆炸_ParallaxScale视差厚度0.0200.05颜料堆积的高度感我个人的默认组合是色阶5、抖动0.5、笔触强度0.03、频率3、布纹强度0.15。这个组合在大多数场景都能稳定输出“有画味但不脏”的结果。美术如果觉得太淡先动笔触强度再动色阶数如果觉得太油腻把布纹法线强度降下来通常能救回来。5.2 片元着色器性能优化三板斧这套材质的基础开销其实很低但如果没管住手一个片元里可能会塞进五六次纹理采样移动端直接完蛋。我压性能习惯用这三招噪声全部程序化。笔触扰动里的Simplex噪声、画布法线的正弦函数都是纯数学运算不占带宽。别去采样噪声贴图看似省了GPU计算实际上多一个纹理取指和mip选择开销更高。视差单层到底。不做多层视差遮蔽循环。真要更好的效果就把高度图预处理成法线传递用Step 2的扰动替代视觉收益接近性能差距巨大。避免dFdx/dFdy做动态求导。前面第3章用亮度求法线的方案如果写成fwidth(dot(...))会高效很多但部分移动端GPU对屏幕空间导数支持不佳会出现条纹状闪烁。稳妥做法是离线烘焙好高度图运行时只采样一次。另外如果工程用的是URP写材质时注意尽量走单Pass。开双面之后Overdraw翻倍视差再叠进去移动端掉帧是必然的。5.3 几个高频翻车现象的完整排查链路我在这套材质的迭代过程中攒了一堆问题记录挑几个最有代表性的列出来。这里直接给排查链路不绕弯子。现象根因修复方法大面积色带横向/竖向条纹色阶量化被纹理压缩噪斑放大提高Albedo导入质量适度加强抖动笔触方向随着模型旋转而旋转噪声采样用了UV坐标改成世界坐标控制向量保持全局统一摄像机一动高光像雪花闪烁画布法线强度过大压到0.3以下必要时降低布纹密度双面渲染后背面全黑法线未翻转用facing 0 ? normalWS : -normalWS远处看纹理糊成灰团笔触扰动没有随Mip变化把噪声解耦出Mip之外或用世界坐标偏移采样贴图显示不出来/紫色纹理路径、压缩格式、通道接错按3.4节的流程逐项检查排查的时候记得一个原则一次只动一个变量。很多翻车是多个因素叠加出来的同时调三个参数会让人完全失去对照依据。我自己刚开始调这材质时一晚上改了几十版参数最后都不知道是哪个改动让画面变好后来老老实实一个个变量调效率反而高得多。6. 这套材质能用到哪以及后面还能怎么玩这套材质做好之后我手里几个项目都在用但并不是所有场景都适合。这里把它的能力边界说清楚免得你堆上去被美术同事骂。6.1 适合的场景与反例最适合的是这几类数字展厅/文博漫游文物扫描件套上这套材质色阶断层直接把照片级贴图“翻译”成插画配合暖光氛围很容易出视觉记忆点。风格化游戏道具原本从真实物体扫描的纹理不用重新绘制一次导入直接变手绘风大大缩短资产制作周期。动画电影Preview灯光预演阶段用这套材质快速模拟美术定调的“绘画感”比纯白模直观得多。不太适合的场景也有需要真实材质俯视感的电商产品模型凭直觉是不可能跑到这套材质里来的。6.2 扩展玩法顶点笔刷、顶点色流向与Substance预设最后分享几个我在这套材质基础上做的扩展。第一个是顶点着色器层面的笔刷位移读取顶点色R通道在顶点偏移阶段直接按笔触方向做一个微小位移这样不仅颜色有笔触感几何轮廓本身也带手绘的“毛边”。配合法线扰动效果非常绝。第二个是顶点色控制笔触方向。默认的strokeDir是全局向量但不同区域的笔触方向应该跟着形体走比如圆柱侧面是环绕的圆柱顶面是放射的全局向量完全不适用。把顶点色存成方向角度在片元里map到向量材质就拥有了局部可控的笔触流向这是进阶玩法里我优先推荐你尝试的。第三个其实是工作流技巧把这套Shader在Substance Painter里做成一个智能材质滤镜烘焙出来的Chanels直接匹配Shader的属性。这样平时做新资产时烘焙完贴图直接用这个材质球出图风格始终统一不需要每次重新调Shading Model。我之前断断续续调了三周最大的领悟是这类风格化材质的难点往往不在Shader本身而是“松手”。不要追求每个像素都符合物理直觉把参数留出来交给美术去试核心算法稳定即可。现在的习惯是默认值全部取中庸进场景后根据灯光、距离和氛围再拧细节。这套材质你拿去做底子在此基础上继续叠滤镜、叠后处理潜力还大得很。
返回列表