ARTICLE DETAIL

资讯详情

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

Unity Shader后处理:将Low Poly模型转换为水墨画风格

Unity Shader后处理:将Low Poly模型转换为水墨画风格 相信不少做风格化渲染的同学第一次看到“把Low Poly模型变成水墨画”这个需求时心里都会咯噔一下。Low Poly的精髓是清晰的大块面、硬朗的棱线、明快的色彩过渡而水墨画讲究的是“似与不似之间”的朦胧感、墨色晕染的随机性以及笔触的书写感——这两个风格在视觉语言上几乎是反着来的。但正因如此用Unity Shader做后处理把两者桥接起来才特别有意思。我先说结论这个效果完全可行甚至不需要改动任何模型资源只靠屏幕空间的后处理栈就能把低模场景“揉”成一幅动态的、带有墨韵的画卷。这篇博文会从思路、原理、实操到踩坑完整走一遍我在项目中实现的方案。你不需要是图形学大牛但至少要能看懂C#和Shader的基础语法知道后处理OnRenderImage或CommandBuffer的大致流程。如果你正准备做国风游戏、水墨风特效或者单纯想在作品集里放一个风格化Demo这篇文章应该能帮你省下几个星期的试错时间。1. 内容整体设计与思路拆解1.1 为什么选择后处理而不是直接改材质一开始我也纠结过要不要直接写一套水墨风Surface Shader或Unlit Shader把模型本身的渲染风格改掉。试了一周之后我放弃了原因很实际Low Poly场景往往有成百上千个物体逐个替换材质、处理描边、协调透视关系工作量巨大而且一旦某个物体漏改画面风格就断裂了。更关键的是水墨画很看重“整体感”——远山、近石、树木、云雾它们之间的墨色浓淡和虚实关系是互相呼应的这种全局氛围用逐物体材质几乎不可能统一。后处理方案则完全不同。它把整个画面当成一幅“待画的宣纸”拿到的是屏幕空间最终的像素和深度信息。在这个层面可以做全局的边缘提取、亮度分层、颜色抽离、噪声扰动甚至局部模糊。Low Poly所有几何体的棱角特征会被整体感知并转译成“笔触”风格一致性好也方便动态调节墨色浓淡。1.2 目标效果拆解从“看得清Low Poly”到“看得出水墨画”后处理不是简单加个滤镜它得先回答一个问题什么样的画面在人眼看来是“水墨画”而不是“黑白卡通”或“模糊照片”我拆解了参考图之后总结出水墨画区别于其他风格的核心特征墨色分层画面并不是单纯去色而是保留微弱的色相偏向通常偏向青灰或赭石同时以浓墨、淡墨、焦墨几个梯度区分前后景。边缘“写意”而非“写实”传统卡通描边是干净的粗黑线水墨的边缘则是断续的、带有深浅变化的像毛笔水分不够时飞白的感觉。内部细节被“揉”掉Low Poly里那些清晰的三角面高光、锐利的明暗交界线在水墨里会被晕染和扩散变成大块灰度。随机性宣纸吸收墨汁不是均匀的同一遍渲染两帧之间也会有微妙的墨迹差异。留白与空气感远景往往直接淡出到纸色而不是像雾一样均匀变灰。所以这个后处理的本质是基于深度、法线、亮度、噪声这四类信息对屏幕图像做四次“翻译”边缘化、墨色分层、局部模糊、抖动留白。下面逐个展开。2. 核心细节解析与实操要点2.1 在Unity中搭建后处理管线的基础配置我用的是Unity 2021.3 LTSURP管线。如果你用内置渲染管线思路完全一致只是获取深度法线纹理的方式略有不同。先罗列基础配置将相机设置为Depth Texture模式URP下在相机组件里勾选Depth Texture。后处理脚本挂在相机上在OnRenderImage里执行或者在URP里写一个ScriptableRendererFeature。为了快速原型验证我推荐先在OnRenderImage里做调试起来最快。Shader写入后处理材质使用Graphics.Blit进行全屏Pass。我记得第一版踩过一个隐性坑URP默认的相机输出颜色空间是Gamma还是Linear会直接影响水墨的灰度感觉。建议项目使用Linear空间墨色层次更接近真实墨水叠加的效果Gamma空间下中间调容易发闷。另外摄像机的HDR选项建议开启因为后面做Bloom变体或高光抽离时会用到超过1.0的亮度值不开HDR高光部分会直接被截断。2.2 边缘提取的两种实现路线Sobel算子与深度法线检测水墨画的边缘和卡通描边不一样不能直接拿亮度差做Sobel就完事那样出来的线条又齐又死像复印出来的。我试过两种方案第一种是颜色亮度Sobel。把屏幕图转灰度然后用3x3卷积核对每个像素做梯度计算。优点是实现简单、不容易漏边缘缺点是Low Poly物体内部的明暗交界线也会被提取出来导致画面被密密麻麻的线网覆盖水墨的“留白感”全没了。第二种是深度法线边缘检测。只根据深度差和法线夹角去提取轮廓这样模型内部平滑的明暗过渡不会被误判为边缘提取出来的线条主要集中在物体的几何轮廓上。Low Poly模型因为面大且平深度法线变化明显正好适合这种方案。我的最终Shader里两种方案合并使用但权重不同深度法线边缘作为主轮廓亮度边缘仅用于提取极暗的浓墨区域。在实际参数调整上深度边缘的阈值在0.3到0.8之间调节法线夹角阈值在0.15到0.5之间。具体取多大得看场景里物体的大小。如果你场景里有一些特别小的物体建议降低深度阈值否则远处的小石头边缘会被直接忽略。2.3 墨色分层与去饱和的平衡术Low Poly场景之所以好看很大程度上是因为它的颜色饱和度高、色块干净。但水墨画恰恰要削弱这种高饱和。直接把饱和度降为0是最懒的做法画面会变得灰扑扑缺乏水墨画那种“墨分五色”的层次。我的做法是这样先把颜色转换到YCoCg空间或直接用Luminance权重计算出一个InkValue然后设定三段式映射InkValue 0.25映射为焦墨接近纯黑但保留极少色相InkValue 在0.25到0.75之间映射为淡墨灰度平滑过渡稍微压暗InkValue 0.75映射为留白直接输出背景纸色通常带一点点暖灰或米白为了让画面更“画意”我还会根据原始颜色做一个低饱和的色相偏移偏绿的草木稍微往青色调偏土色的岩石往赭石色调偏天空的蓝色往灰青色淡。这一步模拟的是国画颜料“花青赭石”的设色习惯。用一句代码概括就是finalColor lerp(originalColor, inkColor, inkStrength)但这里的inkStrength不是全局常量而是基于亮度、深度和噪声三者的采样结果。3. 实操过程与核心环节实现3.1 Shader主体框架一次Blit完成85%的工作我最终实现的Shader有4个Pass第一个Pass做边缘检测和墨色分层合成主画面第二个Pass做方向性模糊模拟笔触晕染第三个Pass做飞白噪声叠加第四个Pass做最后的颜色校正与留白边缘软化。先看主Shader的Properties部分Properties { _MainTex (Screen Texture, 2D) white {} _DepthTex (Depth Texture, 2D) white {} _PaperColor (Paper Color, Color) (0.95, 0.92, 0.85, 1) _InkDark (Ink Dark Color, Color) (0.08, 0.08, 0.1, 1) _InkMid (Ink Middle Color, Color) (0.35, 0.35, 0.38, 1) _EdgeThreshold (Edge Threshold, Range(0.0, 1.5)) 0.5 _NormalEdgePower (Normal Edge Power, Range(0.0, 3.0)) 1.2 _NoiseScale (Dry Brush Noise Scale, Range(0.0, 0.5)) 0.15 }片元着色器里我会采样深度纹理和法线纹理计算边缘强度float depth LinearEyeDepth(SAMPLE_TEXTURE2D(_DepthTex, sampler_ScreenTextures, uv).r); float3 normal DecodeNormal(SAMPLE_TEXTURE2D(_CameraNormalsTex, sampler_ScreenTextures, uv)); float depthEdge length(float2( depth - LinearEyeDepth(SAMPLE_TEXTURE2D(_DepthTex, sampler_ScreenTextures, uv float2(_MainTex_TexelSize.x, 0)).r), depth - LinearEyeDepth(SAMPLE_TEXTURE2D(_DepthTex, sampler_ScreenTextures, uv float2(0, _MainTex_TexelSize.y)).r) )); float normalEdge length(float2( length(normal - DecodeNormal(SAMPLE_TEXTURE2D(_CameraNormalsTex, sampler_ScreenTextures, uv float2(_MainTex_TexelSize.x, 0)))), length(normal - DecodeNormal(SAMPLE_TEXTURE2D(_CameraNormalsTex, sampler_ScreenTextures, uv float2(0, _MainTex_TexelSize.y)))) ));这里深度用的是LinearEyeDepth比较的是实际距离差单位是米阈值才具备场景语义。如果直接用uv空间深度比近处的边缘会过强远处的边缘基本消失。3.2 方向性模糊模拟毛笔的笔触走向普通的高斯模糊会让画面像蒙了一层雾不符合水墨画“笔触有方向”的感觉。我在第二个Pass里做了拉长的椭圆高斯核方向和屏幕空间的梯度方向垂直也就是说沿着边缘的方向模糊垂直边缘的方向保留。这样墨色会顺着轮廓“渗开”而不是均匀向四周晕。模糊核的实现可以用两个Pass的分离模糊代替二维卷积。先横向模糊再纵向模糊但两次模糊的sigma不一样横向大、纵向小再配合一个旋转角度。运行期每帧动态旋转这个角度会带来画面抖动我建议固定在45度方向因为传统国画的皴法多用斜向笔触45度最自然。3.3 飞白与墨迹随机性让每一帧都不一样水墨画最迷人的地方在于偶然性而3D渲染最讨厌的就是偶发性闪烁。这两者之间的调和需要技巧。我用了一张Perlin噪声纹理在片元着色器里基于屏幕坐标和噪声强度对边缘的绘制概率做减法float noise tex2D(_NoiseTex, uv * _NoiseScale _Time.y * 0.03).r; float dryBrush smoothstep(0.4, 0.9, noise); edgeIntensity edgeIntensity * (1.0 - dryBrush * _DryBrushAmount);这里的时间项如果太快画面会出现明显的“爬动感”看起来很脏。我的经验是_Time.y的系数控制在0.02到0.05之间让人感觉到墨迹在微微呼吸而不是在闪烁。如果你需要完全静态的截图可以把时间系数设为0只用空间噪声画面依然有飞白效果。3.4 深度雾化为留白让远处淡出纸面水墨里远处的山不是被雾霾吞掉的而是直接“没有画完”留白给你想象。这一版我用深度信息做了一个非线性远近映射。具体做法是取相机深度纹理设定一个前景距离和背景距离两者之间做Smoothstep得到fadeAmount然后将这个因子乘到墨色强度上float fade 1.0 - smoothstep(_NearFadeDistance, _FarFadeDistance, depth); inkColor lerp(_PaperColor, inkColor, fade);这样离相机近的物体墨色重远的物体墨色轻直到完全融进纸色。这个参数必须根据场景尺寸调节我这边场景的单位是米_NearFadeDistance设为8米_FarFadeDistance设为60米正好保证中景的石头还保留清晰轮廓而背景的山体只有若隐若现的线条。4. 常见问题与排查技巧实录4.1 问题一编辑器中效果好打包后边缘闪烁这是我最先遇到也最头疼的问题。编辑器里怎么调都正常打包成Windows独立版后边缘出现大量白边和闪烁。排查了半天最终发现是深度纹理精度不一致。编辑器默认使用32位深度缓冲而某些打包配置下会自动降为24位或16位。近距离的边缘提取对深度精度极其敏感深度稍微跳动边缘强度就剧烈变化。解决办法是强制在GraphicsSettings里关闭深度压缩或者在Shader里对深度值做一次平滑float depthSmoothed depth; for (int i -2; i 2; i) { depthSmoothed LinearEyeDepth(SAMPLE_TEXTURE2D(_DepthTex, sampler_ScreenTextures, uv float2(_MainTex_TexelSize.x * i, 0)).r); } depthSmoothed / 5.0;做一次5次采样的水平平滑后闪烁肉眼不可见代价是边缘会稍微变粗把边缘阈值调高0.1即可抵消。4.2 问题二Low Poly的棱边全部变成了黑色粗线这个问题特别典型。Low Poly模型因为面数低棱边处法线变化大深度值在同一条棱上也有明显跳变。当深度边缘阈值和法线边缘阈值同时作用时棱边会被叠加两次结果就是粗细不均的黑色线条画面瞬间从“水墨”变成“炭笔画”。解决策略是降低法线边缘的权重只让深度边缘做主轮廓。在提取法线边缘之前先对深度做一次median采样过滤掉深度噪声。使用edgeIntensity max(depthEdge, normalEdge * 0.6)而不是相加。实测下来Low Poly模型用max合并边缘比sum更干净因为不会出现两条边缘重叠导致的黑线。4.3 问题三画面整体偏灰缺少“墨分五色”的对比后处理只能基于已有画面对比度做调整如果原画面本身灰蒙蒙后期也救不回来。Low Poly场景通常光照对比强烈但全局光照不够容易出现暗部死黑、亮部死白的情况。我在水墨Shader里额外加了一步选择性对比度float luma dot(color.rgb, float3(0.299, 0.587, 0.114)); float contrastFactor 1.2 (1.0 - luma) * 0.4; color.rgb lerp(_PaperColor, color.rgb, contrastFactor);这一步的意思是暗部区域低Luma对比度增强亮部区域对比度轻微下降从而让暗部更“沉”亮部更接近纸色。如果你发现画面仍然发灰可以检查场景里是否开启了环境光遮蔽适当的AO能显著增加水墨画的体积感。4.4 问题四后处理执行顺序导致的色偏后处理之间是有顺序的。如果项目里同时使用了Color Grading、Bloom或Vignette水墨Shader应该放在这些效果之后还是之前结果截然不同。我建议把水墨Shader放在最后一步也就是所有颜色调整已经完成之后再执行。这样水墨风格的墨色不会被后续的Color Grading二次篡改能够稳定输出纸色和墨色。如果你用的是Post Processing Volume需要把水墨Shader写成自定义Renderer Feature并挂在Volume的最上层或者在C#脚本里手动控制Blit的执行顺序。我在这个项目里没有用Post Processing Stack而是纯粹用OnRenderImage原因就是想彻底控制顺序不被URP的默认后处理干扰。5. 性能优化与参数调优建议5.1 在真机上的性能表现与瓶颈定位我的测试环境是PC端GTX 10601080p分辨率全屏后处理整体开销大约在2.3ms。这个耗时对于PC可以接受如果目标是移动端就得很认真地优化。性能瓶颈主要在三处方向性模糊的两次全屏采样。深度法线边缘检测的多次深度采样。噪声纹理的采样。移动端我建议把模糊降为单Pass的双边滤波采样次数从13次降到5次。另外边缘检测的采样可以降为4次上下左右四点放弃对角线采样画面边缘会稍微粗糙一些但帧率能稳定在60FPS以上。5.2 高清分辨率的锯齿问题到4K分辨率后画面锯齿主要出现在墨色分层的边界上那种走一步跳一级的阶梯感非常明显。我在Shader里加了一个dither函数在墨色分层的阈值处叠加一个微小的伪随机抖动float dither (frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453) - 0.5) / 32.0; float layer step(threshold dither, inkValue);这让分层的边界产生类似抖动的过渡视觉上不再是生硬的阶梯而像是墨迹在宣纸边缘的轻微洇开。注意这个dither幅度不能太大否则会出现噪点感。5.3 动态调节把水墨浓淡变成可交互参数这个效果天然适合做动态调节。我在C#脚本里暴露了几个公开变量墨色浓度、边缘强度、留白距离、飞白强度。在运行时用Lerp让这些参数随时间变化可以实现“墨色渐浓/渐淡”的呼吸感。配合UGUI做一个滑动条调整这些参数观众可以实时体验水墨风格从偏写实到偏写意的变化演示效果非常好。具体C#核心代码很简单[Range(0f, 2f)] public float inkStrength 1f; [Range(0f, 1f)] public float dryBrush 0.3f; [Range(0f, 1f)] public float paperFade 0.8f; private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (material null) { Graphics.Blit(src, dest); return; } material.SetFloat(_InkStrength, inkStrength); material.SetFloat(_DryBrushAmount, dryBrush); material.SetFloat(_PaperFadeAmount, paperFade); Graphics.Blit(src, dest, material, 0); }5.4 参数速查表一个我试过还算稳的起步配置参数建议值说明Edge Threshold0.45深度边缘主阈值场景大则调高Normal Edge Power0.6法线边缘辅助权重不宜高过1.0Ink Dark Color(0.06, 0.06, 0.08, 1)墨色不宜纯黑带一点蓝灰更通透Paper Color(0.94, 0.92, 0.85, 1)米白纸色不要用纯白Noise Scale0.12飞白噪声密度太高会花Near Fade8近景墨色保持距离单位米Far Fade55远景完全融进纸色的距离单位米Blur Sigma1.8笔触晕染强度太大糊成一片从这套配置出发微调10分钟基本就能出一个自己满意的效果。先跑通管线再追求艺术的微妙感受比一开始就死磕某一个参数要高效得多。6. 最后的个人实操收获说实话这个项目最让我意外的不是Shader本身怎么写而是后处理风格化真正的生命力来自“信息的选择性丢失”。Low Poly场景提供了非常干净的深度和法线信息这恰好是水墨边缘提取的绝佳素材。你不需要去搞一堆复杂的BRDF模拟宣纸只要把人类视觉对水墨画的先验——留白、飞白、墨色分层——翻译成屏幕空间的几行数学运算效果就已经相当有说服力了。还有一个技巧想分享给看到这里的人别一开始就把所有效果全开。先用一个Pass只做边缘提取把画面调成“白底黑线稿”确认线稿结构符合预期后再逐步加入墨色分层、模糊和噪声。这样的迭代调试方式能减少大量无效工作也更容易定位是哪一步破坏了画面。如果想在这个基础上继续扩展我可以明确告诉你几个方向从单层水墨扩展到多层“浅绛山水”设色叠加一个视差映射模拟宣纸纹理或者加入角色描边的动态粗细变化。每一步都有不少坑但也很有意思。希望这篇文章能帮你跳过那些我踩过的坑早点做出自己满意的水墨效果。
返回列表