尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity URP热扭曲Shader完整实现:从_CameraOpaqueTexture到半透明渲染

Unity URP热扭曲Shader完整实现:从_CameraOpaqueTexture到半透明渲染 做URP下的热扭曲效果时我第一反应是照搬以前内置渲染管线里的老套路GrabPass抓一张当前画面再用噪声图扰动UV去采样。结果在URP里折腾一晚上最后发现关键不是Shader语法问题而是UPR处理“抓屏”的方式和内置管线完全不同——它用的是_CameraOpaqueTexture一张专门存放所有不透明物体画面的纹理。之前在网上搜Unity URP热扭曲效果能找到的完整Shader代码很少尤其是针对半透明物体的场景更是寥寥无几。这篇就把我的实现过程完整拆开讲包括URP Shader代码、渲染顺序的逻辑、半透明物体上为什么经常不出效果、以及调参踩坑的经验。适合正在做火焰、引擎热浪、防御罩、传送门这类特效却卡在URP透明渲染上的Unity开发者参考。1. 热扭曲效果的核心原理与URP渲染方案选型1.1 所谓热扭曲本质上是一次“画面重新采样”真实的空气热浪是因为热空气密度变化导致光线折射率不均匀光路发生弯曲你看见的背景就会扭曲抖动。图形学里当然不会去模拟真实光路大家用的都是同一个取巧思路把当前画面当作一张贴图用一张动态噪声图去扰动采样的UV坐标让画面看起来像被“搅动”了。举个例子你在夏天看柏油马路远处路面上的空气像水一样波动这个现象可以拆成两个要素一是扭曲的形态是随机的、连续流动的二是扭曲只发生在局部区域而不是整屏均匀地变形。所以实现热扭曲的Shader核心就两件事准备一张合适的噪声图作为偏移源然后在片元着色器里采样场景颜色时给UV加上一个受噪声驱动的偏移量。关键是这个“场景颜色”从哪来。内置管线可以用GrabPass直接抓屏但在URP里情况不一样。UPR并没有提供和GrabPass完全对等的机制它提倡的是通过管线本身的OpaqueTexture特性来获取屏幕内容。这也是为什么很多人把内置管线的Shader搬到URP会出问题不是Shader语法报错而是物理意义上“抓不到画面”。1.2 为什么URP下要用 _CameraOpaqueTexture 而不是 GrabPass在URP里如果你在管线资产中勾选了Opaque Texture选项它会在一帧渲染完所有不透明物体之后也就是透明物体渲染之前把当前的不透明场景画面拷贝到一张纹理中这张纹理的名字就是_CameraOpaqueTexture。这意味着什么这张纹理里只有不透明物体透明物体在它之后才会被渲染因此不会出现在这张纹理中。对制作热扭曲来说这个特性反而是优点。你想一下如果给一堵半透明的能量护盾添加热扭曲效果护盾背后的地面和墙体都属于不透明物体它们会被完整记录在_CameraOpaqueTexture中护盾材质只要去采样这张纹理就能得到“透过护盾看到背后场景”的正确背景。而如果换成GrabPass抓屏时机在渲染管线中的位置是可配置的抓到的画面可能包含某些透明物体处理起来反而麻烦。再加上URP官方推荐移动端项目减少不必要的全屏抓屏操作GrabPass方案在性能上也不占优。所以URP下做热扭曲基本上就是围绕_CameraOpaqueTexture来展开。1.3 半透明物体上做热扭曲常见的三个“失效”原因很多人做出来的效果放在一个Quad或者粒子面上都正常一旦放到半透明物体上就出各种问题。我在项目里遇到最多的是这三类第一材质队列不在透明队列。如果Shader的Queue标签还是Geometry物体被当成不透明物体渲染_CameraOpaqueTexture抓屏时它自己也在画面里采样后就会出现“自己扭曲自己”的奇怪线条效果非常脏。热扭曲Shader通常要放在Transparent队列也就是Queue Transparent确保在OpaqueTexture生成之后才渲染。第二深度写入没有关闭。半透明物体的核心特征是不遮挡背后物体如果你不给Shader设置ZWrite Off半透明物体会写入深度缓冲导致背后的半透明物体出现排序错乱甚至被裁剪掉一部分。做热浪、护盾这类效果时ZWrite Off几乎是标配。第三直接采样到了“不存在的透明物体”。半透明物体想折射自己背后的另一个半透明物体时_CameraOpaqueTexture里根本没有那个物体所以怎么采样都是空的或者画面撕裂。这一点是URP透明扭曲的硬限制后面第4章我会给出一个可选的替代方案。2. 完整Shader代码与核心参数详解2.1 一份可直接使用的URP热扭曲Shader下面这段Shader代码是我在一个URP项目的火焰和防御罩效果上验证过的基础版本。它能在Unity 2020.3及以上版本、URP 10及以上版本的工程中直接运行。如果你是Unity 6对应URP 17同样可以正常使用。Shader Custom/URPHeatDistortion { Properties { _NoiseTex (Noise Texture, 2D) white {} _DistortionStrength (Distortion Strength, Range(0, 0.1)) 0.02 _FlowSpeed (Flow Speed, Range(0, 5)) 1.0 _TintColor (Tint Color, Color) (1, 1, 1, 0.3) } SubShader { Tags { RenderPipeline UniversalPipeline Queue Transparent RenderType Transparent } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { Name ForwardDistortion Tags { LightMode UniversalForward } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 3.0 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl TEXTURE2D_X(_CameraOpaqueTexture); SAMPLER(sampler_CameraOpaqueTexture); TEXTURE2D(_NoiseTex); SAMPLER(sampler_NoiseTex); CBUFFER_START(UnityPerMaterial) float4 _NoiseTex_ST; half _DistortionStrength; half _FlowSpeed; half4 _TintColor; CBUFFER_END struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; float4 screenPos : TEXCOORD1; }; Varyings vert (Attributes v) { Varyings o (Varyings)0; o.positionHCS TransformObjectToHClip(v.positionOS.xyz); o.uv TRANSFORM_TEX(v.uv, _NoiseTex); o.screenPos ComputeScreenPos(o.positionHCS); return o; } half4 frag (Varyings i) : SV_Target { float2 screenUV i.screenPos.xy / i.screenPos.w; float2 flowUV i.uv _Time.y * _FlowSpeed; float2 noise SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, flowUV).rg; noise noise * 2.0 - 1.0; float2 offset noise * _DistortionStrength; half3 sceneColor SAMPLE_TEXTURE2D_X(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, screenUV offset).rgb; half alpha _TintColor.a; return half4(sceneColor * _TintColor.rgb, alpha); } ENDHLSL } } FallBack Off }复制到工程里创建一个材质把Shader选为Custom/URPHeatDistortion再给Noise Texture指定一张噪点图效果就能跑起来。如果你用的是官方推荐的封装写法也可以把采样_CameraOpaqueTexture那一部分替换成#include Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareOpaqueTexture.hlsl half3 sceneColor SampleSceneColor(screenUV offset);DeclareOpaqueTexture.hlsl是URP官方封装好的工具库里面已经声明了_CameraOpaqueTexture并封装了SampleSceneColor函数代码更简洁适合不在乎原理直接用的场景。手动声明的方式则能让你清楚看到每一步在做什么方便二次修改。2.2 逐段拆解每一个关键步骤在干什么顶点着色器里最关键的一行是o.screenPos ComputeScreenPos(o.positionHCS);。它把裁剪空间坐标转换成屏幕坐标供片元着色器使用。注意这里得到的并不是最终UV还要在片元着色器里做一次透视除法float2 screenUV i.screenPos.xy / i.screenPos.w;原因在于裁剪空间的坐标带有w分量经过硬件光栅化插值后直接让xy除以w才能得到正确的屏幕UV。这一步漏掉的话全屏就会出现扭曲不均匀的问题特别是在透视相机下越靠近画面边缘扭曲会越奇怪。噪声采样部分我从噪声图的RG两个通道取数据。为什么不用灰度图只取R通道因为热浪的流动方向是二维的R和G分别代表U方向和V方向的偏移量能形成更自然的二维漩涡感。如果只用单通道扭曲会偏向一条直线看起来像拉扯而不是热浪。noise noise * 2.0 - 1.0;这一步是为了把噪声值从[0,1]映射到[-1,1]。噪声图是一张贴图采样出来的像素值范围是0到1如果不做映射偏移方向永远只有一个方向看起来就像画面被风一直往右吹而不是自然的上下左右抖动。时间驱动的写法是float2 flowUV i.uv _Time.y * _FlowSpeed;这里的_Time.y是Unity内置的时间变量单位是秒。它让噪声图沿着UV方向缓慢流动这是产生“热浪翻涌”动态感的关键。_FlowSpeed控制流动速度建议从0.5到1.5开始调太小几乎看不出动态太大则会让画面像剧烈抖动。片段着色器最终输出的是采样到的场景颜色与TintColor混合后的结果。这里_TintColor.rgb默认为白色也就是说你看到的基本就是被扭曲后的背景。_TintColor.a用于控制整体透明度这在制作玻璃护盾、能量场时特别有用。2.3 让半透明物体自身也“动”起来很多效果不止需要背景被扭曲还希望物体本身有动态波纹。比如能量护盾既能扭曲背后场景表面又有一层流光扫过的动画。实现思路很简单在片段着色器里把噪声值同时用于扰动物体的UV坐标再采样物体的主纹理。以火焰为例你可以让火焰贴图的采样坐标随着噪声偏移这样火焰边缘会产生不规则的抖动比单纯的UV动画更自然// 假设你有一个 _MainTex 主纹理 TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float2 selfUV i.uv noise * _SelfDistortion; half4 albedo SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, selfUV); half finalAlpha saturate(albedo.a noise.x * 0.3) * _TintColor.a;用噪声的R通道去影响透明通道可以让半透明物体的边缘出现“呼吸感”或“燃烧感”边缘忽明忽暗。这种做法在火焰、毒气、能量盾上效果都很好。需要注意_SelfDistortion这个参数要控制得比较小比如0.01到0.05否则物体会像哈哈镜一样变形。3. 把效果接入URP工程管线配置、噪声贴图与参数调优3.1 URP管线资产中的Opaque Texture设置这一步很容易被忽略。不是Shader写对了效果就能出来你必须在URP管线资产里开启Opaque Texture选项不然_CameraOpaqueTexture不会被创建Shader采样结果就是黑色的。操作步骤是在Project窗口中找到你的URP Asset文件选中后在Inspector里找到General分类展开之后有一个Opaque Texture的勾选项把它打开。URP 12及之后的版本还会多一个Opaque Downsampling选项一般不要改动保持默认即可。这里我要多提一句性能开销。开启Opaque Texture意味着URP需要在每帧渲染完不透明物体后再额外拷贝一份屏幕图像这会带来额外的带宽消耗。在PC上几乎感觉不到但在低端移动设备上要留意。如果只是局部小范围的热浪效果比如火焰上方的一块区域开启全屏Opaque Texture有点亏可以考虑后续替换成Renderer Feature方案只对局部区域做处理。材质球创建好之后把Shader拖上去注意确认材质球的RenderQueue显示为Transparent。如果Inspector里看到Queue是Geometry回到Shader文件检查Tags里的Queue是否写对。受SRP Batcher影响有时修改Shader文件后材质球不自动刷新需要手动点击一下材质Inspector右上角的刷新按钮。3.2 噪声贴图怎么选、怎么生成热扭曲效果好不好一半看参数一半看噪声贴图。千万别随便拿一张彩色图片当噪声图用采样出来的偏移会非常乱。我比较常用的是Perlin噪声或Simplex噪声的灰度图但要注意实际存储时我会把同一张噪声图同时写入R和G通道或者准备两张不同的噪声图分别放在R和G通道。这样采样的RG两个通道数据是独立的如果只放灰度图R、G都相同二维偏移向量会被限制在对角线方向附近运动形态不够丰富。没有现成噪声图的话可以用Photoshop或GIMP生成。新建一个灰度图添加杂色滤镜再使用高斯模糊就能得到一张基础的Perlin风格噪声。也可以用Substance Designer生成平铺无缝噪声。更懒的办法是直接用Unity在编辑器里跑一段生成代码用Mathf.PerlinNoise写像素保存成Texture2D资产。关键点是噪声图要保证Repeat包裹方式也就是Wrap Mode设为Repeat不要留黑边。滤方式设为Bilinear让噪点过渡平滑。尺寸建议256x256到512x512太小会让流动显得颗粒感强太大对内存和带宽都是浪费。3.3 核心参数调优经验与推荐值调参时我总结了一套比较实用的初始值和调整思路。你可以先按照下面这个表格设置再根据实际效果微调。参数推荐初始值调整思路Distortion Strength0.02对应2%的屏幕UV偏移。做火焰热浪建议0.005~0.03做传送门或能量爆发可以到0.05以上超过0.1边缘撕裂感会很明显Flow Speed1.0数值越大噪声滚动越快。0.5~1.5是热浪比较自然的速度区间数值过大会产生画面抖动的眩晕感Noise Tiling2x2到4x4在材质面板的Tiling上调整。平铺太大会让噪点密集得像毛玻璃太小则会出现明显的大块纹路循环Tint Color Alpha0.3控制透明程度的基准值。完全透明的热浪可以设成0.2~0.4护盾可以设成0.5~0.7有一个比较容易犯的错是以为Distortion Strength越大越明显越好结果边缘出现采样撕裂。因为UV偏移量太大时采样坐标会跑到屏幕有效区域外边缘会拉伸成奇怪的条纹。如果一定要大强度建议对screenUV做Clamp或者结合一个边缘遮罩让边界平滑衰减。调速度时有个小技巧不要把_FlowSpeed调成一个定值可以叠加一层正弦波float dynamicSpeed _FlowSpeed * (0.8 0.2 * sin(_Time.y * 0.3)); float2 flowUV i.uv _Time.y * dynamicSpeed;这样噪声的流动速度会周期性变化热浪的“呼吸感”会强很多。我实际做火焰特效时固定速度总感觉像机械地平移加上速度波动之后整个热浪区域看起来就“活”了。4. 实战中遇到的典型问题与排查技巧4.1 效果完全不显示画面没有任何扭曲遇到这种情况第一步不是查Shader代码而是检查URP Asset里Opaque Texture有没有开启。不开的话_CameraOpaqueTexture不会被创建Shader采样到的内容是黑色画面当然不会扭曲。第二步检查材质的Shader是否真的加载成功。URP工程里如果Shader用了不支持的API材质球会显示成洋红色。最容易踩的坑是少了#include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl这一行或者Tags里没有写RenderPipeline UniversalPipeline导致Shader落入旧管线解析采样不到URP的纹理。第三步确认物体是否处在相机可视范围内。如果物体被裁剪或者缩放为0画面自然没有变化。还有一种情况是修改了Shader后没有刷新。因为URP的SRP Batcher会缓存Shader变体有时改完代码在编辑器中按播放旧变体还在生效。可以点击菜单栏Edit - Rendering - Clear Shader Cache清一次缓存或者把材质球重新拖一次到物体上通常能解决。4.2 背景扭曲正常但物体本身闪烁或遮挡错误这个问题几乎都出在深度和队列设置上。半透明物体如果不写ZWrite Off会往深度缓冲里写入深度那么位于它之后渲染的其他透明物体深度测试会失败直接被剔除。闪烁的根源也在这里。要形成稳定的半透明渲染效果记住这几个设置Queue必须为Transparent确保物体在OpaqueTexture之后渲染ZWrite Off必须存在半透明物体不应该阻挡后续物体的渲染Blend SrcAlpha OneMinusSrcAlpha是常见的Alpha混合方式Cull Off按需开启双面显示的防护罩需要它单面粒子面片可以不开如果仍然出现闪烁检查场景中是否存在两个半透明物体相互穿插的情况。半透明物体的排序是按物体中心点距离相机的远近进行的当两个半透明物体靠着很近或者相互穿插时排序结果会不稳定导致每帧渲染顺序变化画面就闪。解决方案是手动控制RenderQueue的数值差异比如护盾用3000内部的门用3001强制排队。4.3 想扭曲半透明物体身后的另一个半透明物体做不到怎么办前面提过_CameraOpaqueTexture只包含不透明物体这是URP透明扭曲方案的硬性限制。如果你需要的是全屏级的热浪效果场景里无论半透明还是不透明物体都要被扭曲那就不能只用Shader需要结合Renderer Feature来做。思路是在URP Renderer上添加一个Full Screen Pass Renderer Feature把整个后处理阶段放在透明物体渲染完成之后执行。此时采样_CameraColorTexture当前画面整体的颜色纹理用噪声扰动UV这样就做到了全屏扭曲包括所有透明物体。不过这个方案的代价是必须为扭曲特效单独写一个Render Feature而且后处理的方式不适用于局部热浪区域它更适合表现“整个场景被火焰高温笼罩”的效果。对于局部热浪Shader方案依然是最优选。如果又想局部又想让半透明物体参与可以把局部区域的遮罩渲染成一张Mask贴图在后处理中根据Mask决定扭曲强度这样就能控制扭曲范围了。4.4 URP版本差异和兼容性注意事项URP在过去几个大版本里有一些接口差异网上搜到的代码对不对先看版本。Unity版本URP版本注意事项2020.3URP 10_CameraOpaqueTexture可以用TEXTURE2D声明SAMPLE_TEXTURE2D_X宏存在2021.3 LTSURP 12推荐使用TEXTURE2D_X声明移动端XR支持更完善2022.3 LTSURP 14UniversalForwardPass Tag正常DeclareOpaqueTexture.hlsl已提供SampleSceneColorUnity 6URP 17基本兼容注意FallBack Off不要改成内置管线的Diffuse等高开销Shader最让开发者头疼的是老版本用TEXTURE2D(_CameraOpaqueTexture)没问题Unity 6的某些平台可能因为缺少_X声明导致采样错乱。我现在的习惯是写新Shader一律用TEXTURE2D_X搭配SAMPLE_TEXTURE2D_X这兼容性最好。如果再结合官方封装的DeclareOpaqueTexture.hlsl基本不会踩版本坑。另外如果项目里同时用了后处理Volume要留意热扭曲和全屏后处理的执行顺序。URP的默认后处理流程是在透明物体渲染完之后执行的而我们的Shader在透明队列中采样OpaqueTexture相当于扭曲发生在后处理之前。如果后处理里有景深或径向模糊热浪会先被扭曲再被后处理视觉上不会冲突但如果出现画面异常可以优先检查后处理效果的优先级。4.5 性能开销实测与移动端优化建议做一个简单的性能估算。一个全屏的热扭曲Shader每个像素需要执行两次纹理采样一次噪声图一次OpaqueTexture。再加上几次加法和乘法开销并不算高。真正的性能大头是URP额外生成OpaqueTexture这一步它会额外增加一次屏幕拷贝分辨率越高拷贝越快不了。移动端优化时我一般会做三件事。第一URP Asset里如果开了Opaque Texture看看能否把Opaque Downsampling设为2x也就是用半分辨率生成OpaqueTexture能显著降低带宽压力画面质量损失肉眼几乎看不出来。第二噪声图尽量用256x256减少纹理采样缓存压力。第三避免在多个物体上重复铺热浪材质能用一张粒子面片解决的事就不要让整个模型都用全屏扭曲的Shader。还有一种优化思路是把噪声图从RGBA改成R16纹理或者用双通道纹理打包两张噪声图方便后续做多层扰动算法。R16纹理在部分移动平台上支持不完美稳妥起见还是用RGBAR和G两个通道做偏移B通道可以存透明度扰动系数A通道存边缘遮罩一举多得。5. 一些写在最后的实操心得这次做热扭曲最大的感触是URP下的透明物体特效很多问题都不是Shader代码本身造成的而是渲染流程理解不到位。_CameraOpaqueTexture是什么时候生成的里面包含了哪些物体为什么会缺失某些透明物体搞懂这三个问题基本就能排查90%的异常。我在火焰场景里最终把DistortionStrength调到了0.008FlowSpeed设成0.8噪声图平铺3x3配合粒子系统渲染的火焰底座热浪区域看起来非常自然。关键要点是不要让扭曲强度一直保持不变给强度加一点时间扰动或者让强度值由一个遮罩贴图控制只在火焰边缘附近生效效果会真实很多。这个Shader后续还可以扩展的方向很多。比如把噪声图换成Flowmap流向图可以让扭曲方向呈现有规律的流动适合制作极光、河流、传送门旋涡。或者叠两层噪声采样一层低频控制整体波动一层高频控制细节抖动会让热浪拥有更多层次。如果你项目中在URP透明特效上还有别的需求这个结构完全可以当作一个可复用的基础模板往上叠加不同的属性即可。
返回列表