ARTICLE DETAIL

资讯详情

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

Unity冰材质折射实现:GrabPass屏幕扭曲Shader全解析

Unity冰材质折射实现:GrabPass屏幕扭曲Shader全解析 前阵项目里要加一块被冰冻住的场景物件甲方开口就三个字“要像冰。”我当时第一反应是冷笑——像冰这三个字背后是半透明、折射、扭曲、高光、菲涅尔、次表面散射一整套东西真要写实够喝一壶的。但需求落到画面上抓重点其实就两个一个是透过冰看背后东西会被扭曲另一个是冰的表面要有那种冷冽的纹理和高光。这就绕回到Unity里最常用的屏幕扭曲方案GrabPass抓屏 法线/噪声扰动UV也就是所谓的“扭曲着色器”。这篇就把我在Unity中实现冰纹理折射的完整过程整理出来从效果拆解、Shader逐段解释、参数调优到移动端性能和常见渲染坑一条龙讲透。想用它做冰面、水面、玻璃、热浪甚至传送门扭曲的朋友都可以拿这份思路去改。1. 先把冰材质的视觉拆明白折射、扭曲、半透明不是一回事很多第一次做冰材质的人上来就想把折射率、入射角、斯涅尔定律全塞进Shader里做出来反而又慢又假。我个人的习惯是先对着参考图把视觉感受拆成几个要素再决定哪些用物理近似、哪些直接用假效果顶上。冰在画面里之所以“像冰”主要是四件事在起作用背后物体的扭曲、内部隐约可见的裂纹和杂质、边缘和薄处的通透感、以及表面那层细腻高光。这四个要素的实现成本完全不同先理清楚它们之间的关系再动手Shader效率会高很多。1.1 冰到底该是什么样视觉层面的解构先说“扭曲”。你透过一块冰看后面的树或石头看到的画面不是清晰的而是被冰的表面起伏和内部结构带偏了方向边缘还常常和背景“糊”在一起。这个效果本质上就是对背景画面做采样偏移。放进Shader里就是两个点一是拿到屏幕空间背面的颜色纹理二是对它的UV坐标做一些非线性的扰动。问题是扰动怎么产生最自然答案不是直接给个随机的UV偏移而是用法线方向去驱动采样偏移。这样做的好处很直接法线本身就能表达冰表面的微小起伏法线越乱采样偏移越明显扭曲越强。等于把“冰表面的凹凸”通过“光线偏折”转换成了画面上的扭曲从视觉原理上是说得通的。第二是“通透感”。冰这个东西很有意思它不像玻璃那样让你完全看穿也不像磨砂玻璃那样全糊它是在通透和模糊之间反复横跳的。冰的比重、厚度、表面雕刻痕迹、内部气泡都会影响这种通透度。实际开发里不是真去模拟光线在冰里的多次散射而是用半透贴图的Alpha做混合再配合噪声纹理把某些区域弄“脏”一点、某些区域弄得更透一点模拟出“不是均匀通透”的真实感。第三点是“高光”。冰表面是光滑但有细微冷感的UNITY里面一般用高光滑度加一层偏蓝的镜面反射再配合CubeMap来模拟环境里的高光反射。很多朋友做冰会忽略高光形状顺手拿默认的GGX高光来用。但冰是偏脆、偏硬质的材质它对高光的敏感度很高表面需要有一点“碎裂细节”来打散高光才不会看起来像一块塑料或果冻。这也是我在下面要把法线细节叠加进去的原因——不只是为了折射扭曲同时也是为了让高光碎一点、真实一点。最后是“边缘和薄处的光透感”。真实冰块在边缘处或者比较薄的地方你会看到那里的颜色比中心更浅、更亮甚至透着一点青白色这是光线在冰里走了较短距离就散射出来的结果。Shader里最常用的近似手法就是菲涅尔效果也就是让视线方向和法线夹角大的区域也就是边缘位置增加一层半透光感或者干脆改变它的颜色。这种处理成本极低但视觉提升非常大。从某个角度看它甚至有“次表面散射”的错觉。做冰、做玉、做宝石、做玻璃这类材质时菲涅尔是必加的一层。1.2 核心器件只靠一张主贴图撑不起冰很多新手的Shader都只有一个MainTex加一个Glossiness然后指望通过换贴图得到冰、塑料、木头三种材质。这个思路在大型写实项目里走不通。像冰这种材质至少要准备好这么几张输入一张表面颜色贴图控制冰的底色和不均匀色块一张法线贴图用于扰动光照和折射采样一张控制通透/浑浊区域的遮罩贴图比如有些地方掺了沙土或裂纹比较深这些地方就不该那么透如果需要冰面上有裂纹细节噪声贴图也基本是必用的。做扭曲效果时如果想省掉一张专门的噪声图直接从法线贴图上取rg通道也可以但叠加一张流动噪声能做出“冰内部缓慢生长”的动态感觉这在表现“活着的冰”或者带法术能量的场景里很加分。之前有个做水面效果的同事问我为什么他直接用法线贴图取样得到的扭曲很生硬排查之后发现他用的法线贴图是DirectX格式而Shader里根本没有处理法线平台差异。这正是很多人的通病法线贴图在不同图形API下的格式不同必须通过UnpackNormal之类的函数把压缩法线还原到正确的空间。也就是说扭曲效果能不能正常不只取决于你写了多少偏移代码还取决于法线贴图的格式基调和切换规则是否正确。否则在Windows上正常一到Android就发现扭曲方向完全不对。1.3 为什么选“抓屏加扰动”而不是真的物理折射写真正的物理折射听起来很酷让光线进入冰体后再折射一次再叠加散射但实际项目的实时性要求通常撑不住这种计算量。视觉上没有哪个玩家会拿量角器去验证折射角准不准他们看到的是画面“够不够冰”。所以业界通用的思路就是把“冰背后有什么”先抓一次屏幕内容在Screen Space里采样这个结果并将采样UV根据法线方向和滑块强度做偏移。理论公式并不复杂最终显示的颜色约等于采样冰折射后的背景颜色再用菲涅尔等参数将边缘变成冰体颜色。这样做直接得到了“屏幕坐标经过扰动后看到一个隐约扭曲的背景”的效果成本比真正的光线追踪或体素折射低了几个数量级同时画面观感已经骗过大多数非专业眼睛了。选GrabPass时还有一个点要明确GrabPass会额外把当前画面抓到一个纹理里所有对它采样都会产生一个较重的带宽消耗。如果用不好整颗冰就把手机帧率拉到很低所以后面的章节我也会花篇幅专门讲优化手段。不要认为写一个看起来效果很漂亮的扭曲Shader就万事大吉性能测试这一关才是真正的分水岭。2. 基础方案实现GrabPass抓屏加法线扰动的完整Shader原理聊得差不多了现在落到实际操作。我会先讲需要准备的工程配置和贴图资源再完整给出一个能在Unity里直接跑的Shader然后逐字段拆解解释每一段到底在干什么、为什么要这么写。代码版本上我用的是Unity 2021.3 LTS内置渲染管线。URP的版本虽然也流行但GrabPass在URP里默认不可用你需要在URP里改用抓屏接口或者升级用RenderFeature去实现这个到后面Shader Graph部分再讲。如果你当前工程就是内置管线下面的代码可以直接用。2.1 准备工程和贴图资源新建工程就选3D模板保持默认渲染管线。场景里要有一个Cube充当冰块本体背后放一些颜色比较丰富的物体比如条纹木箱子、带有彩色贴图的球体这样扭曲效果才看得出来。如果背后放一面白墙就算扭曲写对了也看不出来这属于调Shader时常犯的“观赏环境错误”。调试这种透明/扭曲材质时一定要保证背景有足够丰富的纹理变化否则你无法确认扰动是否生效。贴图方面准备列表如下一是法线贴图可以去网上搜免费岩石/冰缝法线贴图或者直接在Substance Designer、Photoshop里生成。如果没有也可以先用内置的默认法线纹理但我更推荐准备一张带明显条状和空洞变化的法线图表现的冰裂纹更自然。二是噪声贴图用Perlin噪声的灰度图用多少看你想表现多少内部扰动。噪声不需要很大的分辨率512×512足够毕竟它只是用来给UV做小幅偏转。三是一张简单的冰面颜色贴图白蓝色系带一点透明度变化如果实在没有可以暂时让Shader内部直接用纯色渐变来生成。2.2 逐段拆解Shader实现下面给出一段可以在Unity中直接使用的Shader我把关键功能都集中在一个表面着色器里它包含两个PassGrabPass先抓取当前已绘制画面然后Main Pass在Surf中采样它。代码是Cg/HLSL混写风格适合内置管线的Surface Shader语法。建议你把它存成IceRefraction.shader并新建一个材质来拖上去。Shader Custom/IceRefraction { Properties { [NoScaleOffset] _NormalMap (冰面法线贴图, 2D) bump {} _NormalIntensity (法线强度, Range(0, 2)) 1.0 [NoScaleOffset] _NoiseMap (扭曲噪声图, 2D) gray {} _RefractionStrength (折射扭曲强度, Range(0, 0.2)) 0.05 _FresnelPower (边缘通透强度, Range(0.5, 8)) 2.5 _TintColor (冰体颜色, Color) (0.7, 0.9, 1.0, 0.5) _Smoothness (光滑度, Range(0, 1)) 0.85 _Metallic (金属度, Range(0, 1)) 0.0 _Speed (流动速度, Range(0, 1)) 0.2 } SubShader { Tags { QueueTransparent RenderTypeTransparent } GrabPass { _IceBackGroundTex } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _IceBackGroundTex; float4 _IceBackGroundTex_TexelSize; sampler2D _NormalMap; sampler2D _NoiseMap; float4 _NormalMap_ST; float4 _NoiseMap_ST; float _NormalIntensity; float _RefractionStrength; float _FresnelPower; float4 _TintColor; float _Smoothness; float _Metallic; float _Speed; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float4 grabPos : TEXCOORD0; float4 uv : TEXCOORD1; float3 viewDir : TEXCOORD2; float3 worldNormal : TEXCOORD3; }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.grabPos ComputeGrabScreenPos(o.pos); o.uv.xy TRANSFORM_TEX(v.uv, _NormalMap); o.uv.zw TRANSFORM_TEX(v.uv, _NoiseMap); o.viewDir WorldSpaceViewDir(v.vertex); o.worldNormal UnityObjectToWorldNormal(v.normal); return o; } fixed4 frag (v2f i) : SV_Target { // 1. 采样两张贴图法线 噪声 float4 normalData tex2D(_NormalMap, i.uv.xy _Time.y * _Speed); float3 normalTS UnpackNormal(normalData); normalTS.xy * _NormalIntensity; normalTS.z sqrt(1 - saturate(dot(normalTS.xy, normalTS.xy))); float noiseValue tex2D(_NoiseMap, i.uv.zw - _Time.y * _Speed * 0.6).r; // 2. 用噪声让法线扭曲随时间轻微变化 float2 distortion normalTS.xy * _RefractionStrength (noiseValue - 0.5) * _RefractionStrength; // 3. 用扰动后的UV去采样抓屏纹理 float4 bgColor tex2Dproj(_IceBackGroundTex, i.grabPos float4(distortion * i.grabPos.w, 0, 0)); // 4. 简单地用法线和视线点积算菲涅尔 float3 worldNormal normalize(i.worldNormal); float3 viewDir normalize(i.viewDir); float fresnel pow(1.0 - saturate(dot(worldNormal, viewDir)), _FresnelPower); // 5. 背景颜色与冰体颜色根据折射率倾向混合 float3 baseColor lerp(bgColor.rgb, _TintColor.rgb * fresnel, _TintColor.a * fresnel); // 再加一点冰表面高光方向的简单模拟可选不追求物理正确 float3 halfDir normalize(viewDir float3(0.3, 0.8, -0.2)); float spec pow(saturate(dot(worldNormal, halfDir)), _Smoothness * 128.0) * fresnel; float3 finalColor baseColor spec * (0.4 0.6 * fresnel); return fixed4(finalColor, _TintColor.a); } ENDCG } } FallBack Diffuse }这个Shader看起来不长但里面有五个比较关键的构造。第一个关键是ComputeGrabScreenPos和tex2Dproj这两个函数负责把顶点坐标换算成正确的屏幕坐标并完成投影纹理采样。很多人手写屏幕坐标时容易犯“采样的纹理和屏幕坐标不一致”的毛病表现是扭曲区域歪歪扭扭用Unity提供的这套函数是最保险的选择。第二个关键是UnpackNormal它把所有平台上的法线贴图统一转换成标准切线空间法线。第三步则是给屏幕坐标添加偏移注意这里乘上i.grabPos.w是因为抓屏纹理是投影纹理需要经过透视除法在tex2Dproj内部会把xyz都除以w如果扰动没有乘ww1时没差别但场景一旦有透视变化就会出现偏移强度不稳定。第四步的菲涅尔方式是一个简化版本直接对暴露出的法线和视线方向点乘。在这个Shader里我直接用的是对象空间的法线换算后的世界法线而没有把切线空间的细节法线整合进来。严格说这么做高光细节会缺一部分但好处是写起来简单容易看懂。若你想做完整一些就在顶点着色器里构建TBN矩阵把世界法线通过UV扰动后的法线贴图重新映射出来。第五步其实很粗犷——半角向量我只写了几个固定数这样能让表面高光在没环境反射时也能看到不过真要好看还是建议在完整光照环境下使用Surface Shader或者Lit管线里面有现成的PBR光照模型。2.3 在场景里跑起来看效果把材质赋给一个有碰撞体的Cube放进场景确保相机能看到它再让Cube背后有带色彩的物体。此时你会看到冰块背后应该出现了一点点“不正常”的偏移就像透过磨砂稍微偏光的玻璃看后方。如果在Scene视图中看到Cube是完全不透明的那基本就是混合模式没设置对——这个Shader没写混合命令所以哪怕返回透明颜色依然会覆盖背景。解决方法是继续在这个Pass中把Blend命令打开或者在Cull Off的前提下用透明队列但Surface Shader天然支持这种混合。我的建议是先在Shader里加上Blend SrcAlpha OneMinusSrcAlphaZWrite Off这样透明才成立。这个配置在后面的优化部分还会涉及。不过上面这个简化版本只能作为理解原理的玩具真要交付到项目里还差得远。接下来我们进入调优部分把材质从一个“概念验证品”打磨成能放进实机画面的成品。3. 从“能用”到“好看”参数打磨与效果调优扭曲着色器这种东西特别“吃参数”。如果你只是把代码堆出来丢给美术同学去拉滑块他们大概率会拉到很夸张的扭曲值做出一个像被哈哈镜扭曲的果冻而不是冰。问题不在代码而在你给他们的参数范围和参数之间的关联还不够工程化。我个人经验是做材质不是把几个滑块丢过去就叫完成了而是要把参数组织成“一套有逻辑的控件”调起来才顺手。3.1 参数组织思路先调底色再调扭曲最后调光泽调冰材质的时候建议顺序跟Shader里的参数顺序不一样。第一步一定是把基础体色《_TintColor》调整到一个看起来冷静的颜色。冰不是纯白也不是纯蓝颜色应该带有观感上的高透亮度。我常用RGB范围大致在170到230之间蓝通道略高于红绿通道的浅灰蓝Alpha值设置在0.35到0.6之间即可。第二步再把RefractionStrength慢慢拉高看立方体背后物体边缘的偏移情况。这里有个技巧扭曲值是否合适的衡量标准不只是“能不能看到明显扭曲”而是物体边缘是否仍然稳定。如果边缘被扭曲到闪烁或撕裂说明采样UV偏移得太激进需要降低强度或做一个强度平滑。第三步才是调整法线强度和噪声速度让表面高光产生细腻变化。整体顺序从大到小调能避免调到最后推倒重来。关于RefractionStrength的参数上限不同的项目数值都不一样因为它跟模型大小、屏幕分辨率、距离摄像机的远近都有关系。为了不至于总去手动适配各种场景我建议把_ScreenParams相关的那一步结合进来使用一个基于屏幕分辨率的缩放因子。比如把_RefractionStrength乘以_ScreenParams.x / 1920.0这样让高分辨率屏幕上效果的视觉偏移量保持接近用户就不必在1080p与2K屏之间折腾两套参数了。这个做法在移动端多分辨率适配时也有用。3.2 菲涅尔边缘怎么加才自然很多实现里菲涅尔直接用于让边缘变亮做法是拿到Fresnel值之后乘一个白色叠加看起来就像给冰描了个白边。这在技术上没错但实际视觉偏“描边感”像塑料玩具的边缘发光。我自己更推荐的做法是让边缘不只变亮还要增强透明度倾向和颜色倾向。用第二个颜色来控制边缘光比如在边缘处从_BgColor后混合成一种更清澈的淡青色再叠加一点低强度的镜面高光这样会更有冰的冷感。菲涅尔Power值控制的是“从哪个角度开始边缘变透”。Power越高边缘通透带越窄越集中在轮廓处Power越低整片冰面都会显得通透。做厚冰块时我建议Power保持在2.5到4之间做薄冰片时可以降低到1.5甚至1因为薄冰各处透光差异不大。另一个实用技巧是使用视野依赖而不是固定的视向量直接调用UnityObjectToViewPos或者视角方向。如果在移动端为了避免方向向量归一化导致的抖动可以在片元里做归一化的同时用dot夹角来平滑过渡。移动端如果性能可以的话用float2 screenUV i.screenPos.xy / i.screenPos.w先算好否则当模型离远近变化时边缘效果会莫名变化显得像Bug。还有个容易被忽略的点是冰体本身的厚度变化。GrabPass算法只能告诉你屏幕背后这一层颜色没有办法得到冰厚度。如果你有几块不同厚度的冰模型只用一个参数的材质会让它们看起来厚度都一样。建议在顶点色或者模型Scale里取一些信息将RefractionStrength与Scale相乘。模型越厚背景离得越远扭曲效果越弱模型越薄背后纹理偏移不必太大。但真实世界中厚冰的内部光路更长折射并不一定弱只是我们看到的背景会更模糊。所以如果要做厚冰块可以考虑再加一个模糊采样用冰的厚度参数控制对背景周围几个采样点做平均这也是提升真实度的隐藏技巧。3.3 用裂纹和气泡让冰有“故事感”一张均匀颜色的冰看起来是死的。你要在材质上加入随机的裂纹亮线、气泡暗点、夹杂的尘土区域等。这一点把噪声图和主贴图混合即可实现。噪声的灰度值本身可以作为气泡亮斑也可以在噪点某一阈值下压暗颜色。一般我用一张Worley噪声做气泡得到的是好看的细胞状结构边界亮中心暗翻制到Shader里用法线扰动的B通道去把粗糙度抬高气泡看起来像内部有小凹痕。也可以用Perlin噪声取单一通道做成白色裂纹线让它们从某个“中心点”向四周发散。这一块可以在美术侧做好几张贴图让Shader按遮罩混合。如果项目周期紧也可以直接让程序在Shader内对噪声图做两个频率采样的差集用abs算法自己合成一种类似裂纹的形态。这样做的好处是程序化效果可以随机变化做一个可拖动的Seed让美术同志每次点一个随机种效果每次都不同。坏处是Shader多几次贴图采样移动端容易吃不消。因此衡量后我通常在PC端用程序化纹理在移动端把结果烘焙成一张轻量Map再进来。4. 性能、透明排序与动画驱动的隐藏坑一个效果不光是画面好不好看还要跑得稳、合得对。透明度加上GrabPass叠加起来在Unity里非常容易触发排序问题。我们需要知道整个管线的渲染顺序否则冰块一多或者冰块后面还有半透明物体就很容易出现冰块把后面的饮料瓶或者粒子当背景抓进去造成视觉穿帮。4.1 透明队列和抓屏时机的困扰GrabPass有一个比较隐蔽的特性只要任一物体在渲染时使用了该GrabPassUnity会在该物体的渲染Pass之前把当前屏幕内容抓取到_RevervedTexture等相关纹理中。它是“当时已经渲染完的不透明物体和透传物体”的快照。也就是说如果你有个半透明的魔法球在冰块后面魔法球尚未被渲染时冰块就先去抓屏了那冰块上就不会折射出魔法球。遇到这种情况要么限制“冰块背后只能是不透明物体”要么把后方的重点透明物体放到晚些队列并配合第二次抓屏。但第二次抓屏就意味着双倍代价所以项目范围较大时要提前定好方案。同理当你有多块冰互相叠在一起时后面的冰块在后屏幕上的结果不会出现在前冰块采样的抓屏纹理中。如果你的场景设计里存在“透过一块冰看另一块冰后的物体”那你需要一种“排序无关透明”OIT方案或者用多Pass渲染先渲染所有冰块到一张缓冲再统一用该缓冲做折射。这个项目复杂度和上面提到的基础方案完全不是一个量级。作为初级和中级项目最好在场景构图时把冰块设计成单层使用或者避免要求冰块之间相互折射通透。这样能躲掉一大半视觉Bug。4.2 移动端性能优化别让抓屏变成带宽灾难GrabPass性能问题主要集中在分辨率带宽。手机屏幕现在动不动1080P往上了甚至2K屏也有不少。一次全屏纹理的抓取和后面再用它做全屏采样等于每帧向带宽塞进去两个全屏大小的数据传输。这对于移动GPU来往往是比Shader复杂度更容易卡顿的根本原因。如果你的目标是中低端机建议这样优化冰块在屏幕中占比不大又能接受背景有些许模糊的话可以将抓屏纹理通过Camera的PixelRect限制到较小分辨率再传入或者干脆用低分辨率RenderTexture重新获取相机画面。比如程序里新建一个RenderTexture分辨率只有屏幕的四分之一把相机渲染到上面Shader再对它采样并稍微模糊一点。观众在冰面上看到的是一个带颗粒感的扭曲背景反而有些老电影里寒冰的朦胧美。Blit的模糊也可以有意识地使用在Shader内做两次双线性采样或者多偏移平均采样让扭曲背景下采样数增多。减少采样次数通常是性能优先级较高的方向。因此另一个优化关键是控制屏幕区域。如果当前镜头里根本没有冰材质可以不执行GrabPass节省带宽。用OnRenderImage的方式或者用CommandBuffer动态启停抓屏是更稳妥的工程方案逻辑上等效于“需要用的时候才抓不用的时候不抓”。对刚入门的朋友来说这些话可能有点超前但你至少要意识到GrabPass是要花钱的不是免费的。4.3 与URP的适配思路如果你的项目用的是URP那实现这条路径要换一个口子。URP移除了内置GrabPass暴露方式官方推荐做法是用CameraOpaqueTexture也就是URP里的“Opaque Downsample”选项在URP Asset里打开OpaqueTexture之后在Shader中可以用自带的_ScreenTexture访问从UISpace.Camera获取的不透明物体渲染结果。这一招的优缺点都很明显它同样只能抓不透明物体同时获得的是整个相机的不透明背景图。优点是启用起来简单多数URP项目已经默认开启用于水、雾效果缺点是没法细粒度控制抓屏时机。你用Shader Graph做URP冰面扭曲也很直接先拿到Screen Position节点再用法线扰动给它加一个Offset用Scene Color节点做采样后面接上Noise和Fresnel做混合。Shader Graph的优点对美术太友好了缺点是调试节点效率有时候比代码更难理清。代码版与Shader Graph版在同一个项目里最好只保留一种路线否则遇到bug还要重复调试很耗时间。5. 常见问题和排查技巧实录技术上讲了这么多实际做的时候一定会遇到奇奇怪怪的问题。我以自己遇到过的为主把这些现象、原因、解法整理成一个速查表方便你直接对照排查。5.1 现象与原因对照表现象最可能原因解决/排查方向冰块完全看不到扭曲RefractionStrength为0或者法线贴图为空/未被赋值把强度拉高到0.05以上确认法线贴图生效扭曲边缘出现撕裂或杂色点屏幕坐标偏移过大或远距离相机下有精度抖动降低强度用扰动前对屏幕坐标做四舍五入或使用全分辨率纹理冰块变成完全透明混合命令或Alpha值设置错误确认Queue为TransparentAlpha值不能为0关闭ZWrite时渲染层级自己评估冰块背后的红色物体折射不出来GrabPass只抓了不透明物体半透明或UI不在其中把相关物体改成不透明或单独指定相机分层抓取扭曲方向和期望相反法线贴图平台格式问题/向量方向反了尝试UnpackNormal之后直接加负号做对比手机跑起来帧数骤降GrabPass全屏开销在移动端放大开低分辨率渲染纹理、减少采样数量、限制冰块覆盖范围摄像机旋转时高光闪烁归一化向量精度问题或法线贴图低模硬边缘非必要可把视线归一化移动到顶点着色器减小子像素步进IceBackGroundTex显示为黑/无内容GrabPass名字引用对不上或未执行抓屏就采样检查GrabPass是否定义在SubShader内、名字和采样器名字是否一致5.2 三类高发Bug的定位方法第一类颜色对不上看起来像蒙了层脏东西。这通常是色彩空间转换问题。如果你的项目是Linear色彩空间硬件抓屏得到的背景纹理可能经软件再转为sRGB但你的Shader还按Gamma空间处理结果整体颜色偏亮或偏暗。最简单的排查法是先在Shader输出一个纯色比如return fixed4(0.5, 0, 0, 0)看画面是不是正好一半红。如果颜色不符合预期再检查你采集的背景纹理是否设置了Linear/sRGB标志。内置管线的_GrabTexture一般情况下会自动处理但当你自己建RenderTexture手动Blit时一定要留意格式否则颜色偏移让你百思不得其解。第二类材质放在场景里的刷新和摄像机在动的时候闪烁或者像有人在一个像素一个像素地跳。这个大概率是MipMap和屏幕DDX/DDY带来的问题。在做屏幕空间UV扰动时不是常规的贴图UV在物体表面连续变化其导数值可能在边缘处特别大。当你调用tex2Dlod或对不透明贴图做屏幕空间采样时如果贴图没有MipMap或MipMap偏置设置不当会让采样点在相邻像素之间疯狂跳动。解决方案是让Shader用tex2Dbias或者给扰动UV做一次锐度级别松动的Clamp二是确保你的GrabTexture有生成MipMap内存有损耗但这个在某些高分辨率屏幕下也能提升稳定性。第三类从某个角度看冰块突然有一块区域被“切断”。这种问题一般是模型处于透明队列渲染同时ZWrite被关闭深度排序不正常导致物体间互相遮挡错乱。透明物体是不写入Depth的所以从某些角度看后面的冰块片和前面的冰块片会被当成同一层处理先画的物体被后画的物体覆盖。解决思路几种要么把冰块独立到一个特殊的渲染队列要么在Shader里对透明物体分两次Pass模拟简单的透明度深度也可以给模型烘焙一张内部厚度AO图来辅助手写深度。最简单的是规则上让冰块在镜头内尽量避免互相穿插重叠或者把某些冰块干脆改成表面不透明的裁剪型模拟材质而折射效果只在背景模型里用了。5.3 调试工具链推荐针对GrabPass这种全屏效果按F12暂停根本看不出问题。我的调试习惯是两步走第一步直接用Frame Debugger看当前帧Pass一步一步看冰块在什么时候抓取的背景图确认抓屏时机是否在期望的时间点。第二步写一个可视化Shader把_BackgroundTex用屏幕坐标直接输出到屏幕上看整个抓屏图像是否正常。如果在帧调试器里看到的图片本身是黑屏、平面、偏移的问题往往不在你的Shader而是在相机和渲染纹理配置。没有可视化光凭想象去修很难判断是采样坐标错还是贴图没抓到。如果项目已经使用URP或HDRP还可以用Render Pipeline Debugger来查看Camera Opaque Texture是否成功生成。很多时候你以为自己在调试折射实际上却是在调试渲染管线的某个开关这和Shader没关系盲目调代码会浪费一晚上不如先打开管道调试器看看每个中间纹理和它的尺寸。6. Shader Graph还是手写Shader两条路线的选择文章基本接近尾声时我还是要给出一个不同方向的补充Shader Graph怎么做这个效果。毕竟现在团队里很多人尤其是美术向的技术美术倾向于Shader Graph因此有必要把这套GrabPass折射核心用可视化的方式说清。6.1 Shader Graph里的操作思路首先打开URP或者通用渲染管线的项目在Asset里启用Depth/Opaque Texture。新建一个Shader Graph类型选Unlit或者Lit但若想采样Scene Color建议将一个颜色输入节点连接在Scene Color节点上用Screen Position作为UV坐标。接着从一个Texture 2D里面采样法线贴图把法线用Decode方式解码取它的RG通道再乘上你自己想要的强度系数加上噪声补偿用它作为UV偏移输入去采样Scene Color。可以再把一张噪声图乘以时间做Flowmap式扭曲形成动态“冰内流动”的视觉效果。后半段接入菲涅尔效果简单做法是用Fresnel Effect节点然后用Lerp将Scene Color和冰体颜色混合最后输出到Base Color和Alpha。这样几步就搭出来了。Shader Graph的好处是参数暴露更直观美术直接看到颜色的Lerp结构不需要看晦涩的代码。坏处是Node Overhead相对高相同效果节点版本可能会引入比纯手写更多指令数如果你的项目里没有专门的TA职责去维护Shader Graph图最后很容易变成谁也不敢改的节点意大利面。我的建议是如果团队的渲染工作由程序主导优先手写Shader如果由美术主导且不苛求极致性能可以用Shader Graph。两种路线没有绝对优劣关键是谁来维护这份资产。6.2 版本迁移时的注意点还有一个“隐藏坑”必须提醒一旦写Shader写顺手了你在内置管线的Shader换到URP时会遇到兼容性问题。内置的ComputeGrabScreenPos在URP里不可用UnityCG.cginc不再是唯一依赖你需要用URP的Common.hlsl采样屏幕纹理时要通过GetCameraOpaqueTexture或者类似函数来获得。如果代码里直接使用内置坐标系和纹理名URP下会报错或者输出奇怪结果。更好的策略是维护一份包含宏分支的Template Shader在宏定义UNITY_VERSION或具体管线的条件分支间切换采样策略项目足够大时这几乎是必然要求。这部分如果你正在做UPR升级或者URP改造千万要做好预备不然原先看起来挺好的冰面可能在URP里渲染出破损式的死斑。这不是Shader奇幻而是纹理命名和接口变化导致的常见事故。不要把“在Built-in里能跑”当作“到URP一定会跑”尽量在定方案前选定管线。说到这可能有人在想我能不能用一个相机渲染RT从侧面拍摄后显示在其它屏幕上不去用GrabPass也可以。采用Screen Space Camera的渲染目标用相机配一个Layer只渲染IceCulling层后面的东西然后把这份RT作为纹理传给材质去采样。这方法从物理上更可控但要做的RT生命周期管理、相机同步、多相机排序都不简单。对于环境物件和演示项目通常没必要这么折腾。结尾想补充的一点实操体会这篇文章写下来很长实操上很多地方可能还需要你根据项目场景去调。我自己的最终感受是冰纹理折射在引擎层面并不算什么复杂的“高级特效”它是一道“把视觉需求转换成采样扰动”的基础题。只要掌握抓屏、UV扰动、菲涅尔和透明排序这四个关键词你不仅能做冰水面、玻璃、热浪、全息投影、毒液扭曲通通都能接得住。以前有个项目要做“一根冰柱从地面长出来再炸开”的效果我就是在同一个材质上临时加了溶解噪点和顶点拉伸让灰冰朝着扭曲的Shader变体一路扩展最后效果出奇好。你以后做这类效果时也可以从一套基础扭曲材质开始再按需求往上面堆特性别一上来就把所有参数和效果全写完那样反而会把核心效果闷死。如果真想把这个冰材质再往下深挖建议可以往这几条线走一条是加厚度贴图和Z-Buffer重建来模拟更准确ICEBRDF的光路另一条是直接上一块低模的冰体Mesh并倒入后期Bloom让边缘散射更柔顺还有一条是接上动态阴影接收让地面上的冰面有真实的半透明度阴影。每一块深入研究下去工作量都不小但起点永远是抓屏和对画面的扰动希望这篇能帮朋友少走一些弯路。
返回列表