ARTICLE DETAIL

资讯详情

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

Unity雨天渲染:Shader实现屏幕雨滴与空间雨幕效果

Unity雨天渲染:Shader实现屏幕雨滴与空间雨幕效果 做天气系统那段时间我最大的感触是很多团队一提到“下雨”就急着往场景里堆粒子结果粒子数量冲上几万、Draw Call爆表画面却依然不像在下雨。后来我把重心挪回Shader本身用两个纯Shader方案解决了“镜头代入感”和“空间氛围感”两个方向的问题。这篇文章就把这两个效果拆开讲清楚一个是屏幕空间的雨滴滑落效果模拟雨水打在镜头上再流下去的折射感另一个是全屏Shader做的空间雨丝雨幕带场景遮挡判断配合粒子做近景补充。两个效果都不依赖第三方插件适合想自己掌控渲染细节的Unity开发者参考。内容里会包含核心原理、贴图准备、可运行代码片段以及我在移动端实机测试时踩过的坑希望能帮你在项目里少走几步弯路。1. 先把雨天的视觉构成拆开Shader负责哪几层1.1 一帧雨天画面里其实藏着四层效果站在玩家视角看一场雨画面信息大致可以分为四层。第一层是天空和景物的整体压暗、雾气和云的阴沉感这一层通常由环境光照和雾效解决。第二层是空间中的雨丝也就是雨水从天上落下来的线条感这一层负责告诉玩家“正在下雨”属于氛围核心。第三层是地面、车顶、树叶等表面的湿润反射雨后材质会变亮、出现镜面反射这一层一般靠材质切换或贴图混合。第四层是镜头上的水珠和雨滴滑落痕迹这一层不是真实世界里“镜头前”存在的东西而是模拟玩家眼睛或相机镜头上沾了水属于沉浸感最强的近景层。很多项目的问题在于把第二层做得太重中远景铺满粒子近景却完全没做镜头水痕导致玩家总觉得“雨和我之间有距离感”。所以我在设计雨天方案时始终把空间雨丝和屏幕雨滴滑落作为两个互补的必选项前者负责空间氛围后者负责镜头代入感。1.2 两个Shader效果的分工与配合这篇文章要讲的两个效果正好覆盖上面说的第二层和第四层。屏幕雨滴滑落效果是后处理方案它在画面输出前采样当前帧的屏幕结果按照雨滴法线贴图做UV偏移再叠加滑落轨迹模拟出“透过布满雨水的镜头看世界”的效果。空间雨丝雨幕则是用全屏Shader重建世界坐标在一层或多层虚拟平面上滚动雨丝噪声纹理让画面中出现持续下落的雨线而且会被场景物体正确遮挡。这样分工的好处是性能消耗可控。空间雨幕不会因为雨滴数量增多而增加Draw Call屏幕雨滴滑落也只是额外一两次纹理采样。相比粒子方案动辄几百上千个粒子实例Shader方案的设备兼容性和帧率稳定性都要好很多。我在项目里最终采用的是“后处理雨幕 屏幕水痕 少量近景粒子”的组合方案效果和性能都比较理想。2. 效果一镜头上的雨滴滑落效果2.1 核心原理折射的本质就是屏幕UV扰动屏幕空间雨滴效果的原理说破了一层窗户纸就能理解一个正常的不透明物体不会影响画面采样但水珠是透明的折射体它会让光线经过时发生偏折于是我们看到的水珠后面的画面位置就发生了偏移。在屏幕后处理Shader里这种偏移体现为采样屏幕纹理时的UV坐标修正。具体来说后处理默认把当前画面纹理_MainTex原样输出也就是采样坐标直接用屏幕UV。做了雨滴后我们在雨滴覆盖的区域对UV加上一个由法线贴图决定的偏移量再去采样画面纹理这个区域的画面就会像被水珠扭曲了。法线贴图中R和G通道通常存储法线在切线空间的方向我们将其映射为UV偏移的方向B通道则可以作为雨滴覆盖范围的Mask来控制扰动强度范围。用生活化的类比来说隔着玻璃杯看手指手指的位置会发生偏移和扭曲并不是因为手指动了而是光路被杯壁改变了。屏幕雨滴效果就是在采样阶段改变了“光路”画面内容本身并没有变化。2.2 三张贴图的准备与制作思路要做这个效果至少需要准备三张贴图。第一张是雨滴法线贴图尺寸不用太大512x512或1024x1024都够用内容是多颗大小不一的圆润水珠水滴中心鼓起、边缘较平法线方向从中心向边缘过渡。第二张是雨量分布图用低频噪声图就行灰度值高的区域表示那里雨滴更密集。第三张是滑落条纹图灰度图即可内容是有一定倾斜角度的短线或条纹用来模拟水珠滑落后留下的水痕。如果手头没有美术资源法线贴图可以用两张图合成先用PS画一张雨滴形状的高度图黑色背景、白色圆形水滴然后用Shader做Sobel梯度算子求法线这样获得的就是一张可用的法线贴图。雨量分布图可以直接用Perlin噪声贴图降低对比度甚至可以在运行时用Unity的Mathf.PerlinNoise生成到一张Texture2D里。制作时要注意法线强度不要调得太大否则出现在屏幕上的扭曲会非常夸张看起来像空间扭曲而不是水珠折射。我的经验是把法线贴图导入设置里的Create from Grayscale选项配合Bumpiness拉到0.2左右比较接近物理折射的观感。2.3 分层实现静态水珠、滑落尾迹、大颗滑落直接采样一张法线贴图做UV偏移出来的效果是“屏幕上均匀分布着静止的水滴”缺少滑落的动态。要让画面活起来需要拆成三层来叠加。第一层是静态水珠层负责基础折射感。这个层采样雨滴法线贴图通过雨量分布图控制强度UV偏移后采样屏幕纹理。第二层是滑落尾迹层采样条纹图让条纹沿风向滚动配合时间偏移模拟水珠滑落时拉出的细长痕迹。第三层是大颗滑落层用时间周期控制少量大水滴从上往下滑动这部分最能增强真实感因为现实中雨滴在镜头上是持续滑落的而不是一直静止。下面给出一段简化但可运行的后处理Fragment代码思路在Built-in管线里通过OnRenderImage的材质Blit执行sampler2D _MainTex; sampler2D _DropNormal; sampler2D _RainMap; sampler2D _StreakTex; float _RainTile; float _SlideSpeed; float _DistortStrength; float4 frag(v2f_img i) : SV_Target { float2 uv i.uv; // 雨量分布控制哪些位置有雨 float rainAmount tex2D(_RainMap, uv * _RainTile).r; // 大颗雨滴滑落用时间分布图决定下落进度 float t frac(_Time.y * _SlideSpeed rainAmount * 2.0); float2 dropUV uv * 4.0 float2(0.0, -t * 2.0); // 雨滴法线扰动 float4 normal tex2D(_DropNormal, dropUV); float waterMask smoothstep(0.02, 0.35, normal.b); float2 offset (normal.rg - 0.5) * _DistortStrength * waterMask * rainAmount; // 滑落尾迹沿风向滚动条纹图 float2 streakUV uv * 3.0 float2(_Time.y * 0.15, 0.0); float streak tex2D(_StreakTex, streakUV).r; offset.y - streak * 0.05 * rainAmount; // 边缘UV保护防止采样到画面外 float2 finalUV clamp(uv offset, 0.001, 0.999); return tex2D(_MainTex, finalUV); }这段代码演示的是核心思路真正使用还要根据自己的美术需求调整。注意normal.b作为Mask的做法是因为很多法线贴图的B通道近似代表高度或覆盖率用它截断可以让UV偏移集中在雨滴覆盖区域不会让整个屏幕出现无规律的扭曲。2.4 让镜头水痕更自然的几个参数这个效果翻车最常见的原因是扭曲强度失控。_DistortStrength在移动端建议控制在0.02到0.05之间超过0.08画面上会出现明显的网格状拉伸尤其是边缘部分。如果你发现雨滴区域的画面出现“融化”效果先把强度减半再看。另外一个重点是滑落速度。实际雨滴滑落不是匀速直线运动它受重力影响是加速的而且越大的水珠滑落越快。要模拟这种感受可以用一个时间函数做二次方映射比如让滑落进度t frac(_Time.y * _Speed); t t * t;这样雨滴在每段滑落周期内会有“加速下坠”的效果比匀速看起来自然得多。最后一定要记得处理摄像机移动的情况。后处理是在屏幕空间做的画面本身随相机变化水痕固定在镜头表面是合理的。但如果你在第三人称游戏中做这个效果建议把扰动强度降低到原来的三分之一或者干脆做成距离摄像机较近时才出现避免玩家觉得“镜头脏了一天”。3. 效果二空间雨丝雨幕效果3.1 为什么全屏Shader比粒子雨更适合做中远景中远景的粒子雨在视觉上有很多麻烦。首先是粒子数量问题想让雨在中远景看起来连续粒子数量至少几千移动端压力很大。其次是视觉形态问题粒子是用四边形面片渲染的中远景下这些面片会叠加成一条条生硬的短线观感反而像某种噪点干扰而不是雨。第三是排序和遮挡问题粒子在半透明队列里与场景物体的深度关系、与雾效的混合顺序都需要额外处理。全屏雨幕的思路完全不一样它不生成任何几何体而是在一个覆盖屏幕的Pass里通过深度纹理重建每个像素的世界坐标再用世界坐标的平面分量去采样雨丝噪声纹理。这样画面中每个像素的雨丝是连续生成的没有粒子聚散问题Draw Call也没有新增。代价是这个方案的空间感是“虚拟”的。你看到的雨丝其实固定在一层或几层虚拟平面上而不是像真实粒子那样散布在三维空间中。为了弥补这一点一般会做两层到三层雨幕远近各一层通过不同的滚动速度和透明度拉开层次感。3.2 重建世界坐标并生成连续雨丝要用世界坐标采样雨丝首先要从屏幕UV和深度值重建世界坐标。在Unity里最方便的做法是在顶点着色器里计算一条从相机到远裁剪面的射线方向然后按线性深度缩放struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 viewDir : TEXCOORD1; }; v2f vert(appdata_img v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.texcoord.xy; // 从相机位置出发指向远裁剪面的方向 float3 viewDir -unity_WorldToCamera._12_22_32; // 简化取法需按实际管线修正 o.viewDir viewDir; return o; } float4 frag(v2f i) : SV_Target { // 深度重建 float depth SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); float linearDepth LinearEyeDepth(depth); float3 worldPos _WorldSpaceCameraPos linearDepth * i.viewDir; // 用世界XZ坐标采样雨丝 float2 sheetUV worldPos.xz * _SheetTile; sheetUV float2(_Time.y * _FlowSpeed, -_Time.y * _FallSpeed); float rain tex2D(_RainSheet, sheetUV).r; rain smoothstep(_Threshold, _Threshold _Softness, rain); float4 sceneCol tex2D(_MainTex, i.uv); return sceneCol float4(_RainColor * rain * _Intensity, 0.0); }这里有几个值得注意的细节。雨丝纹理建议使用一张高对比度的条纹噪声图条纹方向偏向垂直这样才能被滚动成“雨丝”。_FlowSpeed控制水平风向偏移_FallSpeed控制下落速度两个值不同会让雨丝呈斜向落下。如果只改一个雨丝会出现完全水平或完全垂直的滑动看起来很假。雨幕平面上限要设置好否则当视线看向地平线附近时重建出的世界坐标会非常大远处采样纹理就会变成一片杂色。常见的做法是把worldPos到相机的距离截断超过设定距离的部分直接让雨丝透明度归零。3.3 多雨幕层的近远分层与场景遮挡单层雨幕效果比较平就像头顶贴了一张滚动的贴图。要让雨“落进场景里”至少要有近、远两层。近层雨幕距离相机约2米到4米雨丝粗、透明度高、下落速度快远层距离相机约10米到20米雨丝细、透明度低、下落速度稍慢。两层叠加后视线方向不同时远近层比例会自然变化立体感就出来了。场景遮挡的实现思路是拿当前像素的深度值和雨幕层深度做比较。近层雨幕可以近似放在相机前方2米处如果场景里某个物体距离相机小于2米那就说明这个物体在雨幕层之前雨水应该被它挡住float rainLayerDepth 2.0; float sceneDepth LinearEyeDepth(depth); float occluded step(sceneDepth, rainLayerDepth 0.2); rainMask * occluded;但要注意这个遮挡只是近似判断它没法做到像粒子雨那样“地面上的雨在地面上方”的精确效果。全屏雨幕在做地面处理时通常会结合场景法线或Mask图把朝上的面片区域雨丝透明度降低否则看起来像是雨丝穿过地面。原本雾效和后处理顺序也会影响雨幕表现。如果项目启用了指数雾建议把雨幕Pass放在雾效之前执行否则远处的雨丝会被雾完全吃掉导致“雨在近处有、远处没有”的断层感。3.4 近景粒子雨与后处理雨幕的搭配后处理雨幕中远景表现很好但近景缺少真实雨滴的颗粒感。最理想的方案是保留少量近景粒子雨同时用Shader控制粒子的形态。近景粒子的数量控制在200到500之间粒子面片需要做Billboard朝向摄像机并且沿下落方向拉伸。粒子Shader的核心思路是把粒子的UV在垂直方向做额外缩放让每个粒子显示成一条细长的雨线而不是一个方块。叠加模式用Additive颜色用浅蓝色或灰色透明度控制在0.3左右。粒子受风场影响时用一个全局风速向量统一驱动粒子的偏移和雨丝Shader的风向偏移这样空间雨幕和近景粒子的倾斜方向才一致不会出现“镜头前雨往左飘远处雨往右飘”的尴尬。这套组合下来Space雨幕负责氛围和距离感近景粒子负责颗粒感屏幕雨滴滑落负责镜头代入感。三者配合时我只在很有限的场景里遇到过性能压力大部分移动端设备都扛得住。4. 移动端性能实测与常见坑位4.1 两类Shader效果的开销对比我在项目里用Unity 2021.3、URP管线做了对比测试测试机包括骁龙865、骁龙778G和麒麟990分辨率统一为1080p。屏幕雨滴滑落效果全屏Pass大约增加2到3次纹理采样实际帧率影响约为1到2毫秒。空间雨幕效果取决于雨幕层数单层大约1毫秒三层大约2.5到3毫秒。近景200个粒子雨的耗时取决于粒子屏幕占比一般在0.5毫秒左右。方案新增Draw Call像素采样次数移动端压力场景遮挡画面价值屏幕雨滴滑落03~4低无需镜头代入感强全屏雨幕单层03低近似遮挡氛围基础全屏雨幕三层05~7中多层遮挡更自然立体感好200粒子雨1~2合批后取决于覆盖低真实近景颗粒感实测下来屏幕上雨滴滑落因为本身有扰动效果会掩盖一部分渲染瑕疵所以可以用比较低的中间分辨率来做进一步省性能。空间雨幕如果做三层建议对最远一层单独降低采样密度。4.2 时间精度、MipMap、半分辨率这些坑第一个高频坑是_Time.y累计后UV平铺次数变得极大导致长时间运行后雨丝密度分布不均匀或者闪烁。解决办法是在Shader脚本侧维护一个累计时间每次帧更新叠加经过缩放的deltaTime传给Shader。也可以直接用frac(_Time.y)控制周期但要用较小的周期值。第二个坑是雨丝纹理没开MipMap。屏幕尺寸较大时雨丝纹理缩小采样后如果生成MipMap会出现大量白色噪点闪烁。我在项目里遇到类似问题时第一反应是优化Shader算法后来发现只是纹理导入设置漏开了MipMap打开后问题立刻消失。雨滴法线贴图也一样建议打开MipMap并适当降低Generate Mip Maps的锐化强度。第三个坑是在URP 12及以上版本里OnRenderImage被弃用需要通过Renderer Feature配合CommandBuffer的Blit来注入后处理Pass。很多从Built-in迁移过来的项目在这步会卡住主要原因是Blit的API签名和纹理格式处理方式都变了。需要确认两点中间RT的格式要支持ColorBuffer类型以及Camera Depth Texture需要在Renderer Feature里主动启用。第四个坑是深度纹理精度。移动端常见的深度纹理精度不足在场景较远的区域LinearEyeDepth计算出的深度值会出现跳动导致雨幕遮挡边缘闪烁。我常用的缓解方案是先对深度值做一层平滑再与雨幕层深度做smoothstep的比较而不是用硬边step。4.3 URP Renderer Feature的接入思路当前项目如果是URP管线推荐把两个效果打包进同一个Renderer Feature里在RenderPassEvent.BeforeTransparents之后、后处理之前插入。用ScriptableRenderPass执行一个全屏BlitShader里根据关键字区分是雨幕Pass还是雨滴滑落Pass。接入时有一个容易忽略的细节命令缓冲里需要先SetRenderTarget到临时RT再执行材质Pass最后把临时RT拷回相机目标。如果直接对相机目标执行Blit在某些移动端GPU上会出现无法读取上一帧内容的问题导致雨滴滑落效果失效。我自己初期就遇到过这类问题排查了很久才确认是RT切换顺序不对。4.4 与场景雾效、抗锯齿的顺滑融合雨幕和雾效的融合我前面提过这里再多说一点。URP的雾效在后处理阶段执行如果雨幕Pass在后处理之前输出雨丝会被雾覆盖一层如果在后处理之后输出则无论多远的雨丝都比场景物清晰这不符合真实效果。折中做法是给每个雨幕层单独设置一个“雾影响系数”在Shader里手工把雨丝颜色向雾色过渡这样远处雨幕自动变淡变灰近处雨幕保持清晰。抗锯齿和雨滴滑落也有冲突。使用MSAA时后处理输入的是解析后的颜色问题不大但TAA这类时间性抗锯齿会明显降低屏幕空间扰动的稳定性雨滴边缘可能出现抖动。实测中我发现TAA开启后雨滴滑落的_DistortStrength超过0.04时边缘拖影明显建议适当降低强度。5. 我在实际项目中总结的几条经验这个部分不写代码写我踩过之后记住的东西。第一条是关于雨量参数的联动。雨不是平均值最好在全局脚本里维护一个wetness浮点从0到1过度0代表干燥、1代表暴雨。这个值同时驱动雨幕纹理采样密度、雨滴滑落强度、地面材质湿滑程度、雾气浓度和天空盒亮度。只调Shader不动别的雨永远是一层“贴上去”的特效不会和场景融为一体。我早期只做了Shader效果白天切雨天时没有同步压暗天空和地面反射结果雨看起来就像悬浮在场景外面的半透明噪点。第二条是关于相机运动的适配。第一人称游戏里镜头水痕是加分项玩家会觉得雨打在镜头上了。第三人称游戏里镜头水痕反而容易让玩家觉得镜头一直被水糊住、视野不干净甚至会怀疑“镜头脏了”。我的建议是给屏幕雨滴滑落效果加一个基于相机速度的强度曲线相机快速旋转时水痕轻微相机静止时水痕慢慢累积变明显。这和真实物理上雨水在静止镜头上累积、风一吹或一动就流走的逻辑是一致的。第三条是所有Shading效果先跑通最简单的版本再谈优化。我有段时间折腾雨幕的三层深度遮挡消耗了大量时间在边缘闪烁问题上最后发现单层遮挡加两层雨幕滚动已经能达到90%的观感反而最省性能。先实现、再对比、最后针对性优化顺序不能反。如果你也想在项目里做下雨效果我建议至少把屏幕雨滴滑落和空间雨幕这两个Shader方案搭起来它们不是复杂技术但带来的画面信息量非常可观。等这两层稳定了再去考虑地面涟漪、湿润度这些外围效果也不迟。雨天氛围这件事Shader决定上限参数联动决定真实度剩下的就是不断在真机上微调。
返回列表