ARTICLE DETAIL

资讯详情

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

Unity冰面折射着色器实战:GrabPass与URP扭曲效果全解析

Unity冰面折射着色器实战:GrabPass与URP扭曲效果全解析 最近在一个科幻场景里补地面细节接了个挺有画面感的需求要在Unity里做一块冰面让冰下的地面纹理和灯光全部像隔着一层厚薄不均的凸透镜一样被扭曲。这类效果在低温和秘境场景里很常见早年很多项目直接用Photoshop修图糊弄过去可一旦角色会站在上面、镜头会靠近就必须交给着色器来算。我踩完一圈坑用扭曲着色器把冰纹理折射做成了一套能看的通用材质今天就把从思路到踩坑的过程彻头彻尾拆一遍。这篇文章不打算聊那种“理论很美但没法落进工程”的折射方案更多的是基于Unity可编程渲染管线的实际做法。我会先用Built-in管线的GrabPass手写一个可运行版本再讲URP和Shader Graph里对应的替代思路最后集中说说我调参和解决兼容性问题时总结出来的经验。已经会写基础Shader的朋友可以直接跳去代码部分抄作业刚开始学Unity Shader的朋友也不用慌前面会有比较多的原理解释尽量说人话。1. 冰面折射的整体设计思路1.1 先弄清楚“折射”到底在屏幕上画了什么做冰面折射前我们先得想明白一个问题玩家看到的“折射扭曲”从计算角度看究竟是一个什么东西屏幕上的任何画面都是由成千上万个像素的颜色组成的。你想要透过冰面看到后面的场景被扭曲本质上就是调整这些像素“该显示谁的颜色”。原本屏幕坐标(0.5, 0.4)处的像素显示的是地面的某个花纹如果冰层在这个位置有倾斜或厚度变化那么实际上肉眼看到的光线来自地面花纹的另一个位置于是你就应该把这个像素改成采样地面花纹另一个位置后的颜色。这个逻辑在Shader里落地时就是两步操作第一步把当前冰面物体绘制之前留下的整张屏幕画面抓下来作为一张纹理第二步在片元着色器里用当前像素对应的法线、噪声或冰纹纹理去扰动“屏幕UV”再用这个被扰动后的UV去采样刚才抓下来的画。这样一来背景就被“掰”成了凹凸不平的样子我们把它叫做扭曲着色器其实本质上是一种屏幕空间UV偏移技术。这里我为什么不建议直接用C#做真实的物理折射原因很简单实时渲染里对每一束穿过冰面的光线做斯涅尔定律计算完全不现实。真正的冰里还会包含气泡、裂纹、杂质和内部散射这些物理细节如果全都要模拟整个项目美术成本会起飞。游戏和数字孪生项目里大家追求的是让眼睛相信“这是冰”而不是让科学家来测量冰的折射率。所以用屏幕UV偏移“骗”视觉是最可靠、性价比最高的方案。1.2 为什么选GrabPass而不是额外摆一台相机我在项目里对比过两条主流路线一是给场景额外增加一台相机把场景渲染到RenderTexture中再传给冰面材质采样二是用Unity自带的GrabPass把当前帧抓成纹理。后一条最终胜出。额外相机的实现灵活能决定折射物体的范围和显示层级但代价相当大。你得处理相机跟随、裁剪范围、渲染顺序、分辨率、多相机开销尤其在PC上还好一到移动端和VR头显上额外相机就是性能毒药。GrabPass的优势是Shader内部直接搞定引擎会在绘制这个物体前自动把当前画面存出来你只需要声明一个纹理名字然后采样不需要C#端配合。GrabPass还有两个隐藏好处。一是它抓到的画面和当前冰面的绘制顺序天然匹配冰面在透明队列里绘制它之前的所有不透明物体都已经画完了画面内容正好是“冰后面会看到的那块背景”。二是在编辑器里调试非常直观Frame Debugger能清楚看到GrabPass实际发生在哪个节点。缺点是移动带宽开销不低这个后面URP部分我会专门展开说。理论上你还可以用后处理阶段做“全屏折射”把整张画面按深度图做扰动。这种效果适合用于“潜水面罩”或者“热浪”这类全屏特效但它没法区分冰面物体本身的边界和重叠关系。如果场景里只有一块冰全屏后处理会连同屏幕上方无关区域一起扭曲做起来非常拧巴。按物体走的GrabPass方案明显更适合冰面材质这种局部需求。2. 手写Shader一个能跑的GrabPass冰面折射2.1 基础Shader代码拆解下面这段代码是我在Built-in管线里整理出来的默认可用版本。它没加复杂的贴图混合只是先把GrabPass的抓屏、法线扰动、UV流动和颜色微调做完整这样你能先跑通再往里面加东西。Shader Demo/IceRefraction { Properties { _Color (着色叠加, Color) (1,1,1,0.92) _BumpMap (法线贴图/冰纹, 2D) bump {} _NormalPower (扭曲强度, Range(0,2)) 0.45 _FlowSpeed (UV流动速度, Float) 0.02 _NoiseTex (裂纹噪声贴图, 2D) gray {} _NoisePower (噪声混入强度, Range(0,1)) 0.35 _Roughness (表面粗糙度, Range(0,1)) 0.15 } SubShader { Tags { Queue Transparent RenderType Transparent IgnoreProjector True } GrabPass { _IceBackBuffer } Pass { Name ICEREFRACTION ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc fixed4 _Color; sampler2D _BumpMap; float4 _BumpMap_ST; sampler2D _NoiseTex; float4 _NoiseTex_ST; half _NormalPower; half _FlowSpeed; half _NoisePower; half _Roughness; sampler2D _IceBackBuffer; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float4 uvGrab : TEXCOORD0; float2 uvBump : TEXCOORD1; float2 uvNoise : TEXCOORD2; }; v2f vert(appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uvGrab ComputeGrabScreenPos(o.pos); o.uvBump TRANSFORM_TEX(v.uv, _BumpMap); o.uvNoise TRANSFORM_TEX(v.uv, _NoiseTex); return o; } fixed4 frag(v2f i) : SV_Target { float time _Time.y; // 两层法线贴图分别朝不同方向滚动制造冰面下仍有一点流动或裂变的感觉 float2 uvA i.uvBump float2(time * _FlowSpeed, 0); float2 uvB float2(i.uvNoise.x, i.uvNoise.y time * _FlowSpeed * 0.6); fixed3 bumpA UnpackNormal(tex2D(_BumpMap, uvA)); fixed3 bumpB UnpackNormal(tex2D(_BumpMap, uvB * 1.7 3.1)); // 用裂纹噪声细节去额外“掰弯”法线而不是简单叠加颜色 fixed noise tex2D(_NoiseTex, i.uvNoise time * 0.03).r; fixed2 noiseVec (noise * 2.0 - 1.0).xx; bumpB.xy lerp(bumpB.xy, noiseVec, _NoisePower); // 两层法线xy叠加后作为屏幕UV偏移量 float2 distortion (bumpA.xy bumpB.xy * 0.6) * _NormalPower; float2 grabUV i.uvGrab.xy / i.uvGrab.w; grabUV distortion * 0.02; fixed3 scene tex2D(_IceBackBuffer, grabUV).rgb; // 冰层内部的雾化不能所有区域都百分百折射否则像玻璃 fixed4 outColor fixed4(scene, _Color.a); outColor.rgb lerp(outColor.rgb, scene.rgb * 0.6 fixed3(0.05, 0.10, 0.12), saturate(noise * _NoisePower * 0.45)); return outColor; } ENDCG } } Fallback Diffuse }这段Shader有几个地方需要重点解释。首先是GrabPass { _IceBackBuffer }这句话声明了Shader在渲染前需要把当前帧的内容抓入一张名为_IceBackBuffer的纹理。注意GrabPass大括号内部是可以带纹理名字的带名字的好处是整个Pass和材质类别内复用同一份抓屏结果不会每个使用该Shader的材质都额外抓一次。其次是ComputeGrabScreenPos。顶点着色器里这个函数和常见的ComputeScreenPos很像但它考虑了某些平台下半屏坐标的偏移和方向翻转问题更安全。片元里i.uvGrab.xy / i.uvGrab.w做完透视除法后才得到我们真正能在屏幕上使用的UV坐标。还有一个小坑透明度设置。如果你把冰面放在透明队列但是忘了关ZWrite冰面不仅会写入深度缓冲还会让后续的透明物体错误地排斥掉画面层次会乱。所以透明折射物体一般需要ZWrite Off。而Blend SrcAlpha OneMinusSrcAlpha是用来做半透明混合的这个系数可以根据需求改但不要完全关掉否则冰会显得像一块没有体积的印刷贴纸。2.2 为什么画面看起来“歪”才是对的很多第一次做折射的同学容易有个误区他们以为只要采样背景画面然后用一个固定向量去偏移UV就能得到好的折射效果。试过你就知道这种固定偏移只会让整个背景像平移了一截完全不像折射。折射的视觉本质是“光路经过不同倾斜角度时发生的偏转程度不同”。所以正确的做法必须让每个像素的偏移方向、偏移大小都不一样而且这种不一样最好符合某种随机但有规律的分布。上面代码里直接叠加两层法线贴图并随时间滚动就是让每个像素的偏移量不同同时在时间维度上又保持连贯这样冰下的地面会像被水波和裂纹一点点拉扯一样而不是整块平移。关于偏移强度你可以把它看作“冰面厚度”。偏移量越大光路穿越冰层后角度被放大得越厉害看起来冰越厚偏移量越小背景越接近原来的样子看起来像特别薄的冰壳。这个关系不是线性的具体调参数时会需要反复尝试后面我在调参表里会给参考范围。3. 冰纹理的“冰感”从哪里来3.1 单靠背景折射还不够要靠噪声控制强弱用上面基础Shader渲染出来的冰面如果你拿一张普通法线贴图放进去其实效果更接近“水”“波纹玻璃”而不是“冰”。冰区别于水的地方在于它表面更硬、内部有碎裂纹路、气泡和雪花状的半透明晶体结构。为了把这几个特征压进材质我做了一层“噪声强度控制”。一个最常见的失败做法是把噪声贴图直接当作颜色贴图叠在冰面上。这样确实会出现白色裂纹但裂纹区域和透明区域之间就像是两张不同的图片硬拼在一起背景折射效果和表面颜色完全脱节。正确的做法是让噪声参与折射扰动的“权重计算”。裂纹所在的位置法线扰动应该更强背景扭得更凶同时在颜色上略微微调成发白、发蓝的雾感其他地方尽量保持透明但不要全清晰。具体到Shader节点就是我不把noise当作一个颜色去混合而是当作一个范围在0到1之间的遮罩用来调节distortion强度。简单说就是float noiseMask pow(noise, _CrackFalloff); float2 finalDistortion baseDistortion crackDirection * noiseMask;如果你没有噪声贴图资源也可以把场景中连续出现的噪声强度变成参数暴露在材质面板上然后用程序纹理临时顶替。不过我建议正式项目里还是专门出一张带方向性的冰裂纹贴图因为完全随机噪声会让冰面看起来像水面方向性的裂纹才是“冰”的身份证。3.2 边缘发白和表面高光是冰的“最后一口气”我调试过程中发现一个很明显的规律背景折射做得再真实如果冰面边缘没有冷白色高光或者反射玩家还是会在第一眼觉得它是“贴了图的玻璃”。人的视觉对透明物体边界非常敏感真实冰层在边缘会因为视角入射角变大而发生更强的反射和散射视觉上会有一圈发白、发亮的轮廓。想模拟这个效果可以不依赖C#层直接在Shader里算菲涅尔项。简化的菲涅尔算起来很便宜只需用到法线方向与视角方向。在给不了完整PBR光照代码的情况下你也可以这样处理受光后的最终颜色half rim pow(1 - saturate(dot(viewDir, normalDir)), 3); finalColor.rgb rim * _RimColor.rgb * _RimPower;这圈边缘光并不需要非常强颜色一般控制成淡蓝白或淡灰白强度给到0.1到0.3就能营造冰块切面感。如果你想更进一步提升质感还可以在冰面物体旁放一个反射探针或者用Planar Reflection作为替身让冰的镜面反射内容更明显。对于二维场景这些都可以按需取舍。4. URP版本和Shader Graph里的替代方案4.1 URP要开启Opaque Texture不然场景会抓不到冰后面的画面现在不少新项目会用URP。URP里GrabPass也可以用但它在URP 12以上并不是首推方案。URP更常见的路子是“Opaque Texture”也就是渲染管线自动保存一张不透明物体的画面贴图Shader通过采样这张贴图拿到背景。你在URP中开启它的入口在管线资产里选中URP Renderer Data资产在Rendering列表下找到Depth Texture和Opaque TextureOpaque Texture要勾选打开。如果还是打开后Shader Graph里的折射没有反应请检查Opaque Downsampling选项PC端调成None移动端为了省内存可以选2x或4x但2x以上会让冰下的背景变糊这个要权衡。这里有个容易忽略的坑Opaque Texture只包含不透明物体。你把冰面物体放在了透明队列那冰自己能正确从Opaque Texture里采样背景没问题但如果冰后面还有另一层半透明纱帘、水、雾之类的东西这些半透明物是不会被捕获进Opaque Texture的。此时折射结果里会缺失它们甚至出现“冰后的透明物体突然消失”的错觉。处理这类需求往往要把这些半透明物体的渲染顺序理清楚或者用自定义RenderFeature把半透明内容预先烘焙到另一张RT里。4.2 Shader Graph里接Scene Color的三种连法Shader Graph做冰纹理折射比手写代码多一层“节点可视化”但原理其实是同样的。你需要把Scene Color节点当作抓屏纹理把Screen Position节点当作当前屏幕坐标再在两者之间插入自己算出的扰动值。我建议这样搭最小链路新建URP Lit Shader Graph或Unlit Shader GraphSurface Type改成TransparentAlpha Clipping根据需求开或不开。从Scene Color节点引出到一个Sample Texture 2D或者直接把UV端口接到Scene Color这里的关键是我要提醒你Scene Color节点本身有UV输入口如果你要偏移就必须把偏移量加到ScreenPosition节点输出的UV上再接到Scene Color的UV端口。Screen Position节点模式默认是Pixel。Pixel模式下输出的是以像素为单位的值如果要接Scene Color当作UV通常要除以屏幕尺寸不太方便。更直接的做法是把Screen Position改成Default这样输出已经是归一化的UV形式配合加偏移更安全。用法线贴图采样后的xy去偏移ScreenPosition UV偏移值要乘一个很小的强度系数再接到Scene Color上然后把Scene Color输出连接到Base Color。如果你只想快速验证也可以用URP的Refraction节点。这个节点会根据厚度或者法线自动算折射偏移但它在不同URP版本里的表现差异比较大部分版本需要配合Roughness和RefractionModel才能用。考虑到复杂性我反而建议用最朴素的“Scene Color加手动UV偏移”节点数量多不了几个但每个环节都能自己控制排查问题也方便。4.3 移动端和VR设备上的性能平衡这里必须说点性能层面的实话。GrabPass和URP的Opaque Texture本质上都是在画面前多拷一份屏幕图像移动GPU对这类操作的带宽开销是非常敏感的。如果你在Android低端机上做一大片冰面再用手机录屏看很可能会发现整体帧率掉得很厉害。我自己的选择是能不动画面的地方尽量不动。如果冰面只在固定区域很小用GrabPass还算能接受但如果是角色脚下大面积动态冰面建议把折射背景降到半分辨率采样并将两层法线扰动简化成一到两层。在真机上比较保险的做法是先确认Shader变体不会因为开了多个GrabPass而疯狂膨胀再尽量只保留一个抓屏纹理。VR项目比移动端更敏感尤其用PICO这类头显做开发时画面会被渲染两遍带宽开销翻倍。若折射效果跟两只眼睛的焦点对不上还会引起强烈的眩晕。我建议你在VR里跑冰面折射时能忍则忍优先减少法线贴图层数并把Opaque Texture的分辨率降到2x。不要一上来就追求极致清晰折射后的画面本身就是模糊的玩家注意不到细节但能很明显感觉到卡顿。5. 手把手调试一版冰纹理折射的实操过程5.1 在Unity里搭一个最小验证场景为了能快速验证效果我建议你新建一个工程不要直接在大型场景里调试。场景里放以下东西就够了一个Plane作为冰面。平面下方放几个颜色反差很大的Cube或Sprite作为“被折射的内容”。颜色反差越大扭曲越明显调试起来越直观。一盏平行光把阴影调暗一点避免阴影干扰视觉判断。一个始终能看到冰面倾斜角度的摄像机。把上面那段Shader挂到材质上材质拖给Plane。你可能会发现一个奇怪的现象Plane的默认坐标和背景平面重叠如果下面的Cube离得太远扭曲并不明显。这时候不要急着改Shader把被折射的物体离冰面近一点让背景内容尽可能靠近冰面所在区域效果就立刻出来了。调试时最好是让摄像机固定住或者缓慢旋转不要一边动一边调参。冰面的折射效果是随视角变化的你不固定视角就很难判断到底是参数调好了还是镜头位置碰巧让效果变好。5.2 参数表和现场调参记录下面这张参数表是我的实测参考范围你可以按项目表现微调但不建议一上来就脱离表里的范围否则容易出现极端噪点或者完全看不出折射的情况。参数建议范围效果观察_NormalPower0.2 - 0.8太低看不出扭曲太高会出高频闪烁和边缘脏色_NoisePower0.2 - 0.6控制裂纹噪声对背景的掰弯程度_FlowSpeed0.01 - 0.05太快会像水面流动太慢会像静态玻璃_Color.a0.8 - 0.95透明度越高越能保留背景折射内容_Roughness0.1 - 0.4影响高光锐利程度数值越低反射越锐利clip边缘光强度0.1 - 0.3给边缘补充冷白轮廓但不抢主画面的光我现场调试时往往先锁定_NormalPower把值调到0.5左右此时冰下的物体应该能看出明显但不过分的扭曲。接下来调_NoisePower一点一点加直到裂纹区域出现那种“被冻住又微微在动”的错位感。最后才是动颜色和粗糙度。不要一开始就去抠颜色颜色调得再准背景扭曲不对整个材质都站不住。5.3 一个小技巧用两层法线反方向滚动增加丰富度只给一张法线贴图做滚动实际动态会显得很“机械”。想看出一层法线在平移并不难一旦玩家发现了这个规律效果就穿帮了。我在基础Shader里是让A方向和B方向以不同速度、不同方向滚动的。这样做的原理你品一下两个不同周期、不同方向的噪声叠加在一起会产生新的干涉纹理。这种干涉图案不是简单两张贴图相加而是让视觉系统无法快速锁定单一运动方向看起来更像冰面内部有复杂流动。这个技巧同样可以放在水面上很多人问为什么别人的水面特别“活”就是因为层数叠得够好。不过层数也不是越多越好。三层以上时移动GPU的纹理采样压力大幅上升而肉眼对第四层的感知已经非常弱。我在项目中保持两层到二层半左右两层是法线滚动另外半层是噪声干扰做出来成本舒服效果也干净。6. 常见问题与排查建议6.1 一图速查冰面折射常见的五种翻车现场现象常见原因解决方向冰面完全不透明或看不出背景材质Queue仍然是Opaque或者误开了ZWrite改为Transparent队列并关闭ZWrite背景有扭曲但像水面不像冰缺少噪声裂纹控制和冷色边缘光增加裂纹噪声遮罩叠加菲涅尔边缘屏幕边缘出现黑色裁切或脏色UV偏移过大采样跑出屏幕范围降低_NormalPower给偏移加clampURP下Scene Color采样全黑Opaque Texture没有开启勾选URP Renderer Data中的Opaque Texture冰后的半透明物体消失Opaque Texture不包含透明物体调整渲染顺序或预先烘焙透明内容到RTVR里折射错位或闪烁XR单通道实例化和抓屏纹理不兼容先用单眼验证再做单通道实例化兼容这里我想重点说一下屏幕边缘黑边的问题。当冰面的UV偏移量太大有一个像素在采样背景图时跑出了屏幕范围而背景图又不是设置了Clamp或Repeat的普通贴图时采样结果会变成黑色或边缘脏色。解决方法是限制最大偏移量或者在Shader里手动对grabUV做一次clamp。不要指望美术调参数能完全规避最好在代码里把边界处理掉。6.2 像素对齐、平台翻转和采样精度几个暗坑很多人在手机上跑GrabPass折射会出现细微的“像素抖动”这个问题的元凶往往不是Shader逻辑而是UV坐标的精度不足。抓屏纹理的分辨率一般就是屏幕分辨率在高DPI手机上屏幕像素可能超过两千万如果法线贴图采样和扰动偏移都在half精度下计算会让UV坐标产生量化误差画面就会轻轻抖动。对策是把偏移相关变量保持为float精度只在最终输出颜色时再用half。虽然看起来增加了一些GPU压力但移动端的GPU对float操作的代价没有大家想的那么可怕和画面抖动比起来这个取舍非常划算。平台翻转问题同样要重视。Unity的屏幕坐标在OpenGL平台是左下角为原点在DirectX平台是左上角为原点。ComputeGrabScreenPos帮我们处理了一部分平台差异但如果你在Shader Graph或者手写URP采样_CameraOpaqueTexture时自己拼UV极容易在Windows上看起来正常、到Android上却上下颠倒。遇到这个情况直接加一个平台判断翻转Y轴或者统一使用Unity提供的宏来完成坐标转换。6.3 关于Shader变体和项目迭代的一点心得最后补充一个和大型项目更相关的点。不管你是用GrabPass还是URP Opaque Texture一旦材质选项变多Shader变体会悄悄膨胀。尤其在冰面这种经常需要在普通版/高性能版之间切换的材质上一定要用关键字或MaterialPropertyBlock来控制特性而不是靠复制一堆几乎一样的Shader文件。我曾经在项目里为了赶进度直接复制了三个版本的冰面Shader一个PC高画质版、一个移动版、一个VR版。结果美术调参时经常发现改动只反映在其中一个版本上他们不得不反复确认哪个材质用的是哪个Shader后期维护成本非常高。正确做法是一个Shader对应一套核心效果用开关控制是否开启高层次法线、是否开启额外噪声层。这样才能把扭曲着色器做得既好看又能稳定跑在不同平台上。这套流程走下来我最大的体会是做冰纹理折射的时候不要追求算得准而要追求演得真。抓屏、噪声、双层法线扰动、边缘冷光这几个步骤叠在一起已经能骗过绝大多数人的眼睛。剩下的事情就是根据目标设备不断做减法直到性能和视觉达到你能接受的那个平衡点。
返回列表