
在Unity场景里调冰面的时候我经历过很长一段“参数乱试”的阶段。折射是有的但怎么看都像在屏幕上盖了一层保鲜膜或者像一坨磨砂玻璃唯独不像冰。后来我才想明白冰纹理折射这件事核心根本不在于“折射”两个字而在于冰这个介质对光线做的那些复杂处理边缘偏蓝、内部浑浊、表面有非常细碎的扰动厚度不同折射程度也不同。这篇就拿我实际调过的Unity冰纹理折射扭曲着色器为例把从原理到落地的完整思路讲清楚适合正在做冰雪场景、冰面材质、冰雕道具或者想理解屏幕空间折射的朋友参考。1. 冰在画面里为什么总是看起来像“塑料”视觉特征拆解与目标定稿先说一个我在项目里反复撞墙后总结出的结论大多数“冰不够冰”的材质问题不是输在折射算法上而是输在制作人没有先想清楚“冰到底应该长什么样”。1.1 冰不是“透明的石頭”也不是“半透明的玻璃”如果你拿一块真正的冰放在灯光下看会观察到几个非常明显的现象冰的表面不是光学平滑的细看有微小起伏和划痕所以它对背景的扭曲是高频、细碎、有局部随机性的而不是像玻璃那样整块视野干净地偏移。冰体内部并不是完全通透的空气中总有气泡、细小裂纹、杂质光线穿过时会散射导致冰有一种“有厚度”的乳浊感。从边缘和薄壁处看冰的颜色会明显偏蓝青色因为水分子对红光吸收比对蓝绿光更强光程越长蓝感越明显。这是物理原因不是美术随手挑个蓝色就能解决的。强光从背后透过来时冰会像灯罩一样在内部亮起来这种现象近似次表面散射SSS虽然用不着上真SSS但完全不做的话冰看起来就只是一片印着花纹的透明贴图。对照一下玻璃是另一种材质。玻璃的表面折射非常规则内部几乎没有散射所以清澈、锋锐、环境反射强烈。水的折射振幅通常来自表面波动内部基本均匀。冰恰恰夹在中间还有一个“静止但内部不均匀”的特性。所以一上来就把水面的GrabPass扰动改个参数当冰用只会得到水面效果而不是冰。1.2 屏幕空间“伪折射”不是物理但能骗过眼睛做冰纹理折射业界最常见也最稳的方案是屏幕空间折射也就是把已经渲染好的场景抓下来当成一张纹理采样时按一定规则偏移UV。严格来说这当然不是真折射因为它拿不到CamBuffer里没有的信息也不计算光线穿过介质后的准确路径但它的性价比极高尤其是移动平台和URP项目里只要面片或模型细节足够视觉上完全够用。这里提醒一个概念某些教程会把“扭曲”直接写成“把GrabPass里的UV随法线贴图偏移”。原理是这样没错但直接写往往很难看。因为一张普通法线贴图提供的扰动是高频的、均匀分布的一上来就整块玻璃的背景跟着抖眼睛很快会累。想把冰纹理折射做好重点其实在“扰动该被谁控制”和“扰动范围该怎么分布”这部分我会在第三节专门拆开讲。1.3 调试冰材质前先给自己定三条验收标准做效果最忌没有验收标准。我在调这套Shader之前先给美术和自己定了三条可量化的目标背景物体透过冰层仍然可以辨认轮廓但细节被轻微打散不能出现整片几何漂移或扭曲成漩涡。冰体自身的颜色不能只是给背景叠一个半透明蓝而是要在边缘和厚度处出现蓝青渐变中间区域保持接近自然的通透度。视角转动时扭曲应该跟随视角有微妙变化而不是像贴图水印一样固定粘在屏幕上。有了这三条标准再上手后面每次调参数都有了明确方向不会再陷入“乱调一通最后忘了初始值”的困境。2. 屏幕抓取折射怎么做才不虚幻GrabPass、坐标系与半透明链路的底层逻辑想理解冰纹理折射必须先弄懂它在引擎里到底在“看见”什么。以Unity内置渲染管线为例最经典的做法是GrabPass。2.1 GrabPass 到底抓到了什么GrabPass相当于是把当前屏幕内容拷贝到一张纹理里之后Shader就可以在片元阶段对这张纹理做采样。使用时有两种方式GrabPass { _GrabIceTexture }这是命名抓取整张纹理会被复制到叫_GrabIceTexture的纹理中。还有一种是GrabPass { }不写名字含义是“每个用它的物体都抓一次”性能开销极大强烈不建议。命名抓取更适合冰材质因为它只抓一次且SubShader内的所有Pass都能引用它。我在这套冰Shader里用了GrabPass { _GrabIceTexture }后面所有片元采样都直接用这个纹理名字。需要注意抓取发生的时间点在当前透明物体渲染之前所以画面里已经渲染好的不透明背景都能被抓进去但它抓不到相机画面里还没渲染的半透明物体比如一层玻璃后面有另一层带扭曲的半透明片就很容易出现“后一层看不见前一层”的问题。2.2 从顶点到屏幕坐标ComputeGrabScreenPos 这一行不能省UV偏移的第一步是把顶点的裁剪坐标换算成可以采样屏幕纹理的坐标。很多新手直接用o.pos.xy / o.pos.w这在某些渲染路径下会出问题因为平台差异可能让Y轴方向不一致。Unity给了现成的解决方案float4 screenPos ComputeGrabScreenPos(o.pos);它返回的是一个没有做透视除法的齐次坐标。到片元阶段再手动做除法float2 grabUV screenPos.xy / screenPos.w; float2 refractedUV grabUV offset; float3 bgColor tex2D(_GrabIceTexture, refractedUV).rgb;不理解齐次坐标也没关系只要记住顶点阶段用ComputeGrabScreenPos传递坐标片元阶段先除w再把偏移加到uv上这个流水线是屏幕空间折射的通用骨架。把这段骨架先跑通后面无论换什么扰动源都不会翻车。2.3 为什么偏移不能用世界坐标位移有朋友会问能不能直接让顶点位置沿着法线偏移一点点然后采样理论上可以但那样做出来的效果会像“把冰面后面的画面整体平移”而不是扭曲。因为顶点方向是宏观的“面的朝向”不能反映冰表面那种局部高低起伏引起的偏折变化。正确的思路是偏移量应该来自一张高频变化的法线贴图让不同屏幕位置的像素分别朝不同方向偏移。这样画面才会“碎”才会产生光线穿过凹凸表面后的那种光学扭曲。所以整体而言这套Shader的主链路是抓取当前画面作为背景纹理。计算当前片元的屏幕坐标。用两张法线贴图混合出局部法线。用法线XY分量作为UV偏移让背景纹理出现破碎感。用顶点法线、视线方向做菲涅尔计算混合冰体颜色和次表面光。2.4 URP下GrabPass的替代方案如果项目已经升级到URP事情有一点点变化。URP默认不提供GrabPass但提供了一个替代方案Opaque Texture不透明纹理。在URP Asset里勾选Opaque Texture之后URP会把不透明物体渲染到一张名为_CameraOpaqueTexture的纹理里Shader里直接声明采样它就行。这套冰纹理折射的思路在URP下完全成立只是抓屏来源从GrabPass换成了_CameraOpaqueTexture代码也要从CG换成HLSLShader Graph里做会更加方便。由于URP的这个选项有额外性能开销如果项目里没有别的透明材质需要采样背景记得在不需要时关掉不要在某个场景里常开。3. 把“扭曲”拆成三层主法线、二级扰动与噪声权重的配合方式这是整套Shader的灵魂部分也是“冰纹理折射”最容易被误解的地方。3.1 一张法线贴图引发的“水面感”我刚做第一版时只采样了一张法线贴图然后平铺Tiling拉到4强度也调得比较激进。结果画面像一块被风吹皱的湖水倒映跟冰完全不搭。原因在于一张法线贴图产生的扰动频率太单一要么低频整块动像水面要么高频微抖像屏幕静电唯独不像冰。因为真实冰表面的扰动是“多尺度”的既有厘米级别的粗糙起伏又有毫米级别的细小划痕如果只有一层能量会集中在某一个频率段看起来就假。3.2 我的做法主法线、二级法线和噪声权重三层叠加在这个项目里我给冰材质准备了两张法线贴图和一张噪声贴图。第一层是主法线控制整体比较大的凹凸走向。第二层是细节法线Tiling更高、速度更慢或反向负责提供那种细碎的裂纹和划痕感。两层法线在片元里做线性混合float3 mainNormal UnpackNormal(tex2D(_BumpMap, mainUV)); float3 detailNormal UnpackNormal(tex2D(_DetailBumpMap, detailUV)); float3 blendedNormal normalize(lerp(mainNormal, detailNormal, 0.5));只混合法线还不够。真正的“扭曲权重”我用第三张噪声贴图来控制。噪声的作用不是提供颜色而是给前面算好的偏移量做一个空间调制让画面有些区域扭曲强烈有些区域几乎不动。这个微小差异就是“冰的晶体感”来源。float noiseWeight tex2D(_NoiseMap, noiseUV).r; float finalStrength _RefractionStrength * lerp(0.2, 1.0, noiseWeight); float2 distortionOffset blendedNormal.xy * finalStrength;这里的原理可以类比成马赛克拼图如果你给全画面所有格子都做同样的位移一眼就能看出整体漂移但如果你只让一部分格子动、一部分格子不动眼睛会觉得画面“被某种模糊的介质挡住了”这正是冰给视觉的欺骗感。3.3 三层分别怎么调调参没有绝对标准我把自己常用的初始值和范围列在下面可以作为起步值再根据场景微调参数建议范围作用主法线Tiling1.5 ~ 3决定冰面大块起伏的疏密细节法线Tiling4 ~ 8控制细小裂纹和划痕的密度主法线扰动速度0.02 ~ 0.05模拟随视角轻微变化的动态细节法线扰动速度0.01 ~ 0.02越慢越像冰越快越像水折射强度0.02 ~ 0.08整体扭曲幅度噪声权重下限0.15 ~ 0.3让画面始终保留基础扭曲特别注意如果想做的是湖面冻结冰、冰面道路这类静止冰块动画速度要压得极低最好只让UV以每秒0.01左右的极慢速度流动。真实的冰是固体的内部不会流动稍微给一点时间变化只是为了打破法线贴图采样的重复感不能让它产生流动错觉。3.4 为什么不能用瀑布那种UV流动瀑布、水面这类材质通常会让法线UV以较快速度往复流动来模拟水的运动。冰如果照搬这套逻辑哪怕速度降到三分之一看起来也像冰下面藏着暗流非常出戏。一个更稳的思路是让“视角方向”参与扭曲计算而不是让UV不停做大幅平移。也就是说当玩家移动视角或相机绕冰块转时折射图案会发生自然的视差变化这比依赖时间来驱动更符合冰的物理直觉。4. 从“被扭曲的背景”到“有厚度的冰”着色分层和菲涅尔/散射改造很多冰Shader做到上一步就停了所以结果是“能扭曲背景的透明薄片”。要做出真正的冰感还得继续往片元里加材质层。我更愿意把这一阶段称为“着色分层”。4.1 菲涅尔边缘光为什么冰的棱边会发光菲涅尔效应是指当视线与表面法线角度越大越接近掠射角反射和边缘透射越明显。把它用在这里可以模拟冰边缘那种蓝白透亮的视觉特征。在Shader里实现很便宜float3 viewDir normalize(i.worldViewDir); float3 worldNormal normalize(i.worldNormal); float fresnel pow(1.0 - saturate(dot(worldNormal, viewDir)), _FresnelPower);对于地面上的冰层视线越倾斜的地方例如远处与天空相接的边缘fresnel越高我们就可以在那个位置叠加冰的蓝青色。这里一定要用带方向的光照或屏幕渐变来体现“接近地面边缘的冰较亮”否则只是给边缘发个光看起来又会像普通玻璃的反射。4.2 厚度感用不透明度制造的“内散射”一个比较实用的技巧是冰的不透明度不要做成均匀的。可以通过物体世界坐标的Y分量或模型空间的厚度近似来控制浑浊度float heightFactor saturate((worldPos.y - _IceMinY) / (_IceMaxY - _IceMinY)); float turbidity lerp(0.6, 0.1, heightFactor);这看起来有点粗糙但它很有效。冰面与地面接触的地方厚度大显得白浊冰层比较薄的地方透明度高能看到更多背景。实际制作中哪怕是程序生成或美术手摆的冰块只要厚度、高度、浑浊度之间有渐变质感就会比恒定Alpha好很多。为了在美术侧更好用我会把高度的上下限暴露成属性让策划或美术可以直接在Inspector里根据模型大小填。4.3 次表面散射的廉价近似冰被太阳或方向光从背后照射时边缘和内部会呈现一种“被点亮”的蓝光。真实次表面散射需要采样厚度贴图做起来成本不低但缩水版很简单用光照方向和视线方向关于法线的点积做一个背光衰减。float3 lightDir normalize(i.worldLightDir); float NdotL saturate(dot(worldNormal, lightDir)); float backLit pow(1.0 - NdotL, _ScatterPower); float3 scatteredColor _ScatterColor.rgb * backLit * _ScatterStrength;把_ScatterColor调成偏蓝的青色再把_ScatterPower调到1.6左右冰块在背光状态下会出现非常自然的内部透亮感这比单纯调高Alpha高明得多。4.4 内部浑浊层和气泡最后的临门一脚如果冰的Shader是为了近距离展示的只靠颜色和折射可能还不够可以叠加一个带噪声的内部层。我通常的做法是用之前那张噪声贴图采样一个比较黑的“气泡/杂质”颜色再乘上冰体透明度让画面里出现少量暗斑。这层杂质不需要很多Alpha控制在0.05到0.15之间不然冰看起来会脏。这里特别强调一点不要在高光或颜色里用纯白和纯黑的极端值。自然界里冰的反差没有那么大极端的黑白会让画面像塑料高光。把白场压到0.92左右黑场提到0.06左右画面舒服很多。5. 一套可复现的冰折射扭曲着色器代码与调参顺序下面给出的是我项目里一套简化后的可用CG Shader。它在Unity内置渲染管线中可以直接跑URP下则需要改成HLSL并替换抓屏纹理。注意这段代码更偏方法演示重点看组合逻辑和调参维度如果要应用在产品项目里记得根据自身光照和SRP做适配。5.1 Shader主体代码Shader Custom/Ice/Refraction { Properties { _BumpMap (主法线贴图, 2D) bump {} _DetailBumpMap (细节法线贴图, 2D) bump {} _NoiseMap (噪声权重贴图, 2D) white {} _MainTiling (主法线Tiling, Vector) (2, 2, 0, 0) _DetailTiling (细节法线Tiling, Vector) (6, 6, 0, 0) _ScrollSpeed (UV流动速度, Vector) (0.02, 0.01, 0.01, 0.008) _RefractionStrength (折射强度, Range(0, 0.2)) 0.04 _IceColor (冰体颜色, Color) (0.75, 0.92, 1.0, 1) _IceOpacity (冰体浑浊度, Range(0, 1)) 0.25 _FresnelPower (菲涅尔强度, Range(0.5, 6)) 2.0 _ScatterColor (次表面散射颜色, Color) (0.4, 0.85, 1.0, 1) _ScatterPower (次表面散射衰减, Range(0.2, 3)) 1.6 _ScatterStrength (次表面光强度, Range(0, 2)) 0.6 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } GrabPass { _GrabIceTexture } Pass { Tags { LightModeForwardBase } Cull Back ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc #include Lighting.cginc sampler2D _GrabIceTexture; float4 _GrabIceTexture_TexelSize; sampler2D _BumpMap; sampler2D _DetailBumpMap; sampler2D _NoiseMap; float4 _MainTiling; float4 _DetailTiling; float4 _ScrollSpeed; float _RefractionStrength; fixed4 _IceColor; float _IceOpacity; float _FresnelPower; fixed4 _ScatterColor; float _ScatterPower; float _ScatterStrength; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct v2f { float4 pos : SV_POSITION; float4 grabUV : TEXCOORD0; float2 uv1 : TEXCOORD1; float2 uv2 : TEXCOORD2; float2 uvNoise : TEXCOORD3; float3 worldNormal : TEXCOORD4; float3 worldViewDir : TEXCOORD5; float3 worldLightDir : TEXCOORD6; float3 worldPos : TEXCOORD7; }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.grabUV ComputeGrabScreenPos(o.pos); float2 baseUV v.texcoord; o.uv1 baseUV * _MainTiling.xy; o.uv2 baseUV * _DetailTiling.xy; o.uvNoise baseUV * 3.0; float3 worldPos mul(unity_ObjectToWorld, v.vertex).xyz; o.worldPos worldPos; o.worldNormal UnityObjectToWorldNormal(v.normal); o.worldViewDir UnityWorldSpaceViewDir(worldPos); o.worldLightDir UnityWorldSpaceLightDir(worldPos); return o; } fixed4 frag (v2f i) : SV_Target { float2 uv1 i.uv1 _Time.y * _ScrollSpeed.xy; float2 uv2 i.uv2 - _Time.y * _ScrollSpeed.zw; float2 uvNoise i.uvNoise float2(_Time.y * 0.015, 0); float3 mainNormal UnpackNormal(tex2D(_BumpMap, uv1)); float3 detailNormal UnpackNormal(tex2D(_DetailBumpMap, uv2)); float3 blendedNormal normalize(lerp(mainNormal, detailNormal, 0.5)); float noiseWeight tex2D(_NoiseMap, uvNoise).r; float distortionStrength lerp(_RefractionStrength * 0.2, _RefractionStrength, noiseWeight); float2 offset blendedNormal.xy * distortionStrength; float2 refractedUV (i.grabUV.xy / i.grabUV.w) offset; float3 bgColor tex2D(_GrabIceTexture, refractedUV).rgb; float3 worldNormal normalize(i.worldNormal); float3 viewDir normalize(i.worldViewDir); float ndv saturate(dot(worldNormal, viewDir)); float fresnel pow(1.0 - ndv, _FresnelPower); float3 lightDir normalize(i.worldLightDir); float ndl saturate(dot(worldNormal, lightDir)); float scatter pow(1.0 - ndl, _ScatterPower) * _ScatterStrength; float3 albedo _IceColor.rgb; float3 scattered _ScatterColor.rgb * scatter; float3 finalColor bgColor * (1.0 - _IceOpacity) albedo * _IceOpacity scattered * fresnel; float alpha saturate(_IceOpacity * 0.8 fresnel * 0.4); return fixed4(finalColor, alpha); } ENDCG } } FallBack Diffuse }这段代码直接贴在Unity里新建的Shader文件就能出最基础的冰面扭曲效果。它把前面讲的三层扭曲、菲涅尔、次表面散射都塞进去了。5.2 一定不能漏的贴图要求代码里采样了三张贴图主法线、细节法线、噪声。前两张建议用Unity自带的Default-Normal或者从Water系统里拆出的法线贴图。关键点是贴图导入设置要正确主法线和细节法线的Texture Type必须要设成Normal map否则UnpackNormal读出来的数据是错的而且看不到明显报错。噪声贴图则用灰度图如果是一张带Alpha通道的贴图记得采样时用.r通道而不是.rgb否则颜色信息会污染权重。Wrap Mode设成Repeat否则UV流动到边缘后会出现接缝。5.3 调参顺序比参数本身更重要很多朋友拿过去就调_RefractionStrength调到0.1后说效果太乱。我的经验是调整要按从大到小的顺序来先定大结构再收细碎先把_IceOpacity拉到0.4这时不用管折射先确认冰本身的半透明厚度色是否正确。调_RefractionStrength到0.03左右观察背景能否被看清但不失真。再动_BumpMap的Tiling确定冰层表面大块起伏的尺度。再动_DetailBumpMap的Tiling补充小裂纹。最后才调噪声权重的下限和速度让画面局部有“凝视冰内部”的感觉。所有静态效果稳定后再把_ScrollSpeed降到极低值不然前面全白调。5.4 配合Shader Graph时怎么做如果项目在URP里用Shader Graph同样可以复现这套分层思路。创建Unlit Shader Graph后通过Sample Texture 2D采样法线贴图注意要用Normal From Height或者法线贴图导入方式再用Lerp混合两张法线用Tiling And Offset节点分别驱动UV流动最后把偏移加到Screen Position节点的Screen Position输出上。噪声节点可以用Gradient Noise或Simple Noise生成也可以直接Sample Noise贴图。过程麻烦一点但原理和上面的CG完全同构。6. 实测排障记录颜色、排序、分辨率与效果抖动这一类问题再好的理论落到实机都会遇到各种幺蛾子。这套冰Shader我在不同项目里跑过很多次把最容易翻车的几个问题整理出来。6.1 为什么冰块后面的粒子或半透明火焰不见了这是透明物体排序导致的。GrabPass只能抓到“它执行那一刻已经画好的画面”如果冰后面还有别的半透明粒子排序在冰之后的话粒子根本没画到画面里自然也就无法通过抓屏显示出来。解决思路有这几个把粒子队列调整到冰之前渲染但这往往取决于具体透明项目后期容易出问题。对关键交互对象比如站在冰后面的人物用单独Pass描边或补充渲染让它在半透明队列前多一份深度信息。接受场景限制半透明抓屏是近似方案不要期待能处理任意半透明物体之间的完美折射。6.2 边缘出现奇怪的“描边”或屏幕边缘发黑这种问题多半是采样了屏幕外的UV。当偏移向量让UV跑到[0,1]范围外时GrabPass纹理会因为WrapMode是Clamp而取到最边缘的像素看起来就是一条拉伸的亮线或黑线。排查时可以先降低折射强度再看是否出现在物体轮廓边缘。如果是可以给offset乘一个屏幕空间距离衰减float2 edgeFactor saturate(refractedUV * (1 - refractedUV) * 10); refractedUV lerp(refractedUV, refractedUVWithoutOffset, 1 - min(edgeFactor.x, edgeFactor.y));注意Unity的默认GrabPass纹理如果用Repeat模式则可能没有边缘黑但容易出现对侧平铺的怪画面还是需要处理。6.3 为什么在小分辨率预览和真机上扭曲幅度看起来差很多GrabPass纹理的分辨率等于当前屏幕分辨率。同一套UV偏移值在1920x1080的屏幕上相当于偏了更多像素在低分辨率真机上偏得像素数少整体感官会变“钝”。这也是为什么我会把折射强度做成可调属性而不是写死在Shader里的原因。应对不同设备时可以做一些分辨率归一化float2 dUV blendedNormal.xy * _RefractionStrength * _GrabIceTexture_TexelSize.zw;_GrabIceTexture_TexelSize.zw其实是纹理尺寸的宽高用它做系数可以让偏移在高分辨率下视觉更一致。但对于一些追求画面风格统一的项目这种做法反而会让移动端变得更“透明”建议先测试再决定。6.4 Linear颜色空间下冰色偏灰如果项目使用Linear颜色空间颜色值的处理方式与Gamma空间有差异。Shader中直接给定的_ScatterColor如果美术是在Gamma空间调的进入Linear后会不一致。我的做法是在Inspector属性里给颜色乘一个0.5到0.8的系数或者允许美术微调。如果发现冰块整体偏灰先检查是不是颜色空间的问题再检查是不是_IceOpacity太大把背景全部盖住了。6.5 用Debug视图检查法线方向和扭曲分布在调扭曲强度时很多问题出在“看不到法线方向”。我建议给Shader加一个Debug开关在fragment里把扰动后的法线直接输出为颜色return fixed4(normalTS * 0.5 0.5, 1);如果扭曲太乱用这种Debug视图能看到法线有没有因为Tiling和UV动画出现撕裂如果整个画面是单调的蓝紫色说明采样的是同一张法线且扰动方向有规律不是冰该有的随机粗糙度。检查完再关掉开关。6.6 为什么冰块在远处疯狂闪烁这种问题常见于模型自身有大量细小三角形或者UV接缝配合法线贴图的高频细节导致远处采样出现摩尔纹。解决思路是在不同LOD下降低法线贴图的精度或使用mipmap也可以把折射强度按距离做一个衰减。这里不建议用太复杂的距离函数简单的_RefractionStrength * smoothstep(50, 10, distance)就能让远景稳定下来。7. 这套扭曲方案还能怎么扩展水面、毛玻璃与异形冰面冰纹理折射做出来之后这套“抓屏加多层扰动”的思路完全可以横向迁移。7.1 水面把“冰渣感”换成“大波浪”水面和冰的区别主要在于扭曲尺度不同。水的表面波幅大、频率低所以把主法线的Tiling降到1.5以下细节法线的权重降低把_ScrollSpeed调快再把冰体浑浊度换成高明度低饱和度透明色就能得到一个基本合格的水面。如果希望波纹更有流动感可以加入顶点位移法线扰动部分保持不变。7.2 毛玻璃/磨砂玻璃去掉方向性强化弥散毛玻璃的本质是让背景不再是清晰偏折而是被均匀地模糊掉。可以用噪声对屏幕UV做多次抖动采样或者用低成本的径向模糊近似。在原有冰Shader里只需要把法线贴图替换成一张各方向均匀的法线噪声图提高细节法线的Tiling到12以上让偏移方向尽可能随机就可以模拟出磨砂质感。7.3 异形冰面圆柱、冰锥和道路冰块对异形模型来说直接用切线空间法线的XY做屏幕偏移有一个潜在问题不同朝向的面块可能会共享一个不太准确的偏移方向。如果你做的是冰锥、圆柱冰柱等复杂形状强烈建议在顶点着色器把法线贴图扰动转到世界空间再参与菲涅尔和散射计算至少可以避免明显的方向穿帮。实在不想做完整TBN矩阵可以只对非常大的网格使用单层法线扰动然后通过顶点法线混合不同朝向的折射方向。总之形变越复杂越要求扰动方向尽量贴合表面法线和视线的真实空间关系不能只依赖一张平铺贴图。这种扭曲思路在项目里的一个额外收获是可以做“热浪升腾”把扰动源换成模拟热空气上升的抽搐噪声贴图定时量变化放大然后让扰动沿着屏幕Y方向有偏向性偏移。虽然换了个效果但底层链路的抓屏、采样、偏移逻辑完全复用等于同一套材质模板可以衍生出很多同族效果。我在实操中最大的体会是美术效果类Shader的调试百分之八十的时间花在细节的取舍而不是原理的推翻上。你不需要让折射变得比物理更准确只要让视觉系统相信眼前这块材质真的在偏折光线就够了。而做到这一点最靠得住的顺序永远是先把画面的目标视觉拆解清楚再逐层用材质去填补遇到卡点就打开Debug视图看数据而不是盲猜参数。每次我觉得“这个冰差不多了”的时候往往隔一天再打开又觉得不够透于是继续去调一层次级扰动的权重。这种打磨没有尽头但也是做这类效果最有意思的地方。