1. 为什么“下雨”在Unity里不是加个粒子就完事了很多人第一次想做下雨效果打开Unity新建一个Particle System调大发射率、拉长生命周期、加个拖尾材质再配上点“哗啦啦”的音效——看起来好像雨下得挺猛。但只要把镜头拉远、让角色跑起来、或者切换到不同光照环境问题立刻冒出来雨滴像悬浮在半空的蚊子打不到地面雨丝方向僵硬得像PPT动画雨势和镜头移动完全脱节更别说雨打在玻璃、金属、布料上该有的不同反馈全都没有。我做过7个带天气系统的项目其中4个是写实风格的开放世界或城市模拟最深的教训就是Unity原生粒子系统能“画出雨”但不能“模拟雨”。真正的下雨效果本质是视觉欺骗物理响应环境耦合三者的精密配合。它要解决的不是“怎么让屏幕上有白线往下掉”而是“如何让玩家相信此刻正站在一场真实的雨里”——这个“相信”取决于雨滴与摄像机的相对运动是否符合人眼经验、雨迹在不同表面的衰减是否合理、雨声随距离和遮挡的变化是否自然、甚至雨雾对远景对比度的削弱是否匹配真实大气散射模型。关键词“Unity下雨特效”背后实际藏着三个不可割裂的层次基础雨迹渲染粒子层、动态交互响应碰撞与遮蔽层、环境氛围融合后处理与音频层。漏掉任何一层效果都会掉进“廉价CG”的陷阱。这篇教程不讲“怎么快速糊弄一个雨”而是带你从零搭起一套可复用、可调节、能进正式项目的雨系统。它适配URPUniversal Render Pipeline兼顾中低端设备性能所有资源都可在Asset Store免费获取或手写实现不需要Shader Graph高级功能但会明确告诉你哪些地方值得为高端项目升级。如果你正在做城市漫游、生存游戏、剧情向步行模拟或者只是想彻底搞懂Unity里“动态天气”的底层逻辑接下来的内容就是你调试三天没搞定的那个雨滴偏移bug的答案。2. 雨滴粒子系统的核心设计为什么不能只靠Transform移动绝大多数新手的雨粒子用的是最简单的“Velocity over Lifetime”模块给Y轴一个负向初速度然后靠重力让它加速下落。这在静态场景里勉强能看但一旦摄像机开始移动问题就来了雨滴在屏幕上的运动轨迹会严重失真。比如摄像机向前平移时雨滴本该呈现“迎面扑来”的透视收缩感但你的粒子却像贴在固定Z深度的图层上平行滑过屏幕——这违背了人眼对高速运动中雨幕的直觉。根本原因在于粒子的位置更新是在世界空间计算的而人眼感知雨的运动本质上是基于摄像机视角的屏幕空间速度。直接给粒子世界坐标系下的速度等于强行把雨钉死在某个绝对位置上忽略了观察者自身的运动矢量。解决方案是将摄像机运动补偿Camera Motion Compensation注入粒子速度计算。这不是Unity内置选项需要手动在Shader或脚本中实现。我最终采用的是“顶点着色器动态偏移”方案因为它性能开销最低且能完美适配URP的Lit Shader。具体做法分三步2.1 构建雨滴材质与Shader变体首先创建一个Unlit Shader避免受光照影响导致雨滴忽明忽暗核心是修改顶点着色器中的vertex.position。关键代码段如下使用HLSL适配URP 14// 在顶点着色器中添加 float3 camMotion _WorldSpaceCameraPos - _PrevWorldSpaceCameraPos; float3 worldPos TRANSFORM_TEX(vertex.position, _MainTex); worldPos.xyz camMotion * _RainSpeedFactor; // _RainSpeedFactor是可控参数通常0.3~0.8 vertex.position TransformWorldToHClip(worldPos);这里_PrevWorldSpaceCameraPos是上一帧摄像机位置需在C#脚本中每帧更新并传入Shader。_RainSpeedFactor控制雨滴对摄像机运动的响应强度——值越大雨越“追着镜头跑”模拟高速移动时的压迫感值越小雨越“沉稳”适合慢节奏叙事场景。这个参数必须暴露为Material Property方便美术实时调整。提示不要用_WorldSpaceCameraPos直接减去当前帧位置必须用“上一帧位置”否则会导致雨滴在摄像机转向瞬间产生撕裂感。我在第三个项目里就因这个细节返工了两天。2.2 粒子系统参数的反常识配置有了运动补偿Shader粒子系统本身的参数就要做颠覆性调整Start Speed设为0因为速度由Shader动态计算粒子初始世界速度必须归零否则会叠加出诡异的双重运动。Gravity Modifier设为0重力由Shader统一控制避免粒子系统重力与Shader逻辑冲突。Simulation Space必须设为World这是运动补偿生效的前提Local空间下摄像机位移无法正确映射。Max Particles建议设为5000~8000低于5000雨幕稀疏感明显高于10000在中端手机上易触发GPU瓶颈。我们用“分层雨”策略解决密度问题见第3节。2.3 雨滴形态的物理可信度强化纯白色细线太假。真实雨滴在空气中下落时受湍流影响会轻微摆动且近处清晰、远处模糊。我在材质中加入了两项低成本增强UV扰动动画用sin(_Time.y * 3 worldPos.xz * 2)生成微小噪声驱动雨滴纹理的UV偏移模拟空气扰动。幅度控制在0.02以内肉眼几乎看不出“动画”但能打破机械感。深度渐隐Depth Fade通过LinearEyeDepth读取片段深度当雨滴离摄像机超过15米时透明度线性衰减至0。这比简单用Distance Fade更符合人眼聚焦特性——远处雨丝本就因大气散射而淡化不是单纯“看不见”。实测下来这套Shader粒子配置在骁龙778G设备上维持60帧无压力且雨滴运动完全贴合摄像机转向、平移、俯仰连VR项目都直接复用。3. 分层雨系统如何用3套粒子撑起从近景到天际线的完整雨幕单靠一套粒子系统永远无法兼顾“近处雨滴清晰可数”和“远处雨幕连绵成片”这两个矛盾需求。试图用超高粒子数填满远景GPU立刻报警降低粒子数近景又显得空洞。我的解法是把雨拆成三层每层用不同策略、不同材质、不同生命周期各司其职。层级距离范围粒子数量核心作用关键技术要点近雨层Foreground Rain0~5米1200~1800表现雨滴撞击地面/物体的飞溅、水花、湿滑反馈使用Trail Renderer碰撞检测生命周期短0.8~1.2秒带随机旋转模拟风偏中雨层Midground Rain5~30米2500~3500构成主体雨幕提供运动透视和密度感主力层采用2.1节的运动补偿Shader生命周期2.5~3.5秒粒子尺寸随距离微调远景层Background Rain30~150米800~1200渲染天际线雨雾、山峦轮廓的柔化、远景对比度压低使用SpriteRendererAlpha混合非粒子系统纯2D面片带大气散射模拟3.1 近雨层让雨“打在身上”的关键这一层的目标是建立玩家与雨的物理连接。我放弃用粒子系统模拟飞溅改用预制体实例化Object Pooling BoxCollider触发。流程如下创建一个RainSplash预制体含SpriteRenderer水花贴图、CircleCollider2D设为Trigger、AudioSource短促“啪”声。在角色脚底、车辆轮胎、窗台边缘等高频接触点挂载RainContactDetector脚本void OnTriggerStay2D(Collider2D col) { if (Time.time - lastSplashTime 0.15f Random.value 0.7f) { // 降低触发频率防卡顿 var splash ObjectPool.Instance.Get(RainSplash); splash.transform.position col.transform.position Vector3.up * 0.1f; splash.GetComponentAudioSource().pitch Random.Range(0.9f, 1.1f); lastSplashTime Time.time; } }水花贴图用Substance Designer生成带Alpha通道的径向渐变播放0.3秒后自动销毁。注意不要用ParticleSystem.Play()触发飞溅粒子系统启动有毫秒级延迟连续触发会导致大量未销毁粒子堆积内存飙升。对象池是唯一稳定方案。3.2 中雨层动态密度与风向耦合中雨层是视觉重心但它的密度不能恒定。真实雨势随风向变化顺风时雨丝拉长、密度增高逆风时雨丝短促、密度降低。我在Shader中引入了风向量参数// 在Shader中添加风向控制 float3 windDir normalize(_WindDirection); // 世界空间风向 float windInfluence dot(worldPos - _WorldSpaceCameraPos, windDir) * 0.5; float finalSpeed _BaseRainSpeed windInfluence * _WindStrength; worldPos.xyz camMotion * _RainSpeedFactor windDir * finalSpeed * _TimeDelta;_WindDirection和_WindStrength由全局天气管理器每帧更新。这样当风从左吹来左侧雨丝会明显更长、更密集右侧则稀疏——无需额外粒子仅靠Shader运算就实现了方向性雨幕。3.3 远景层用2D面片骗过人眼的终极技巧远景雨不用粒子用100个左右的SpriteRenderer面片每个面片是半透明的垂直条纹带轻微噪波按Z轴分层排列。关键技巧有三Z轴抖动Z-Jitter每帧给面片Z坐标加sin(_Time.y * 0.3 i * 0.7) * 2的微小偏移模拟雨幕的“呼吸感”避免静止面片带来的塑料感。动态缩放面片尺寸随距离增大而线性缩小scale 1.0 - distance / 150.0但透明度反向增强alpha distance / 150.0 * 0.4模拟大气散射的灰蒙感。天空盒联动当启用动态天空盒时远景层的_WindDirection与天空盒云层运动方向同步确保雨与云的运动逻辑一致。这套分层方案总粒子数控制在5000以内却营造出远超10000粒子的纵深感。测试数据显示在iPhone XR上三层雨系统GPU耗时稳定在1.2ms内。4. 雨与环境的交互从“雨打玻璃”到“路面湿滑”的全流程实现雨的效果是否真实70%取决于它如何与环境互动。玩家不会盯着雨滴看但会本能地注意“车窗上的雨痕”、“积水倒影的扭曲”、“角色跑过水洼溅起的水花”。这些交互点才是沉浸感的锚点。4.1 动态雨痕让玻璃、金属、屏幕产生真实水渍核心思路是用Render Texture捕获雨滴碰撞点作为Mask驱动材质的湿滑度Wetness参数。步骤如下创建一个128x128的Render Texture命名为RainImpactRT设置为ReadWrite Enabled。在摄像机上挂载RainImpactRecorder脚本每帧将RainSplash预制体的位置转换为屏幕坐标绘制到RT上void OnPostRender() { Graphics.SetRenderTarget(rainImpactRT); GL.Clear(false, true, Color.clear); // 清空RT foreach (var splash in activeSplashes) { Vector3 screenPos Camera.main.WorldToScreenPoint(splash.transform.position); // 绘制一个半径为3像素的白色圆点到RT DrawPointToRT(screenPos, Color.white, 3); } }在玻璃/金属材质的Shader中采样RainImpactRT用其R通道值驱动_Wetness参数float wetness tex2D(_RainImpactMap, i.uv).r * _WetnessIntensity; o.albedo lerp(baseColor, wetColor, wetness); o.smoothness lerp(_BaseSmoothness, _WetSmoothness, wetness);实测心得RT分辨率不必太高128x128足够。分辨率过高反而因采样模糊导致水渍扩散失真。_WetnessIntensity建议设为0.6~0.8值太大水渍蔓延过快失去“刚被砸中”的新鲜感。4.2 路面湿滑反馈物理与视觉的双重欺骗让角色在雨天走路打滑不能只靠降低摩擦力——那会让玩家觉得“操作失灵”。我的方案是视觉滑动音频提示微小位移补偿三位一体。视觉在角色脚下生成一个半径0.3米的圆形湿滑区域SpriteRenderer带径向模糊持续1.5秒后淡出。音频每次脚步落地根据_Wetness值选择不同音效干燥脚步声→轻微水声→明显“噗嗤”声并加入0.1秒延迟的“水花回响”。位移补偿在CharacterController.Move后额外添加一个与移动方向垂直的随机偏移Random.insideUnitCircle * 0.05f但仅当_Wetness 0.5时生效。这个偏移极小玩家感觉是“脚下一滑”而非“被推歪”既保留操控感又强化湿滑反馈。4.3 雨声的空间化为什么BGM音量要随雨势动态调整很多人忽略音频。真实雨天环境音会显著压制其他声音。我的音频系统包含三层基础雨声Ambient Rain循环播放的中频雨声1~5kHz音量由全局_RainIntensity控制0晴天1暴雨。近处雨击Impact Rain由RainSplash触发的短促高频音效8~12kHz带距离衰减。环境掩蔽Ambient Masking最关键的一步——当_RainIntensity 0.3时动态降低BGM音量BGM.volume 1.0 - _RainIntensity * 0.4并给所有非雨音效添加低通滤波Cutoff Frequency 10000 * (1 - _RainIntensity)。这模拟了雨声对人耳听觉的物理掩蔽效应。测试时美术同事说“终于不是‘背景音乐雨声’的简单叠加了现在雨大的时候BGM真的像被水汽捂住了一样。”5. 性能优化与跨平台适配如何让雨在千元机上也不掉帧再炫酷的效果卡成幻灯片也是零分。我在红米Note 9Helio G85上做了完整性能剖分以下是实测有效的优化清单按优先级排序5.1 GPU瓶颈的根治方案合并材质球近雨层的RainSplash、中雨层的雨滴材质、远景层的面片材质全部共用同一套Shader通过Keyword区分功能减少Draw Call。实测从47个Draw Call降至12个。禁用不必要的渲染通道在雨滴Shader中#pragma exclude_renderers gles gles3强制URP使用Metal/Vulkan路径避免OpenGL ES的指令集兼容开销。粒子剔除Frustum Culling增强默认剔除只判断粒子中心点。我添加了Bounds扩展计算——对每个粒子预估其最大拖尾长度生成包围盒再进行视锥剔除。代码在ParticleCullingExtension.cs中增加约0.02ms CPU耗时但GPU节省0.8ms。5.2 内存与GC的隐形杀手对象池容量严格限制RainSplash对象池最大容量设为30超出时自动销毁最旧实例。避免雨天持续10分钟导致内存暴涨。Shader参数批量更新所有雨层共用的全局参数如_RainIntensity,_WindDirection用Shader.SetGlobalVector()一次性更新而非逐个Material.SetVector()。后者在100材质时GC Alloc达2MB/帧。纹理压缩策略雨滴纹理用ETC2Android/ASTCiOS尺寸严格控制在256x256以内。一张512x512的未压缩PNG在低端机上加载耗时高达120ms。5.3 URP管线的专属优化关闭雨层的Shadow Casting雨滴不投阴影但在URP中默认开启。在粒子系统Inspector中Renderer模块下取消勾选Cast Shadows省下0.3ms。使用Simple Lit Shader替代StandardURP的Universal Render Pipeline/LitShader在移动端开销过大。我基于Universal Render Pipeline/Simple Lit魔改移除了Normal Map、Occlusion Map等雨滴用不到的通道Shader变体数从128降至8编译时间减少70%。后处理栈精简雨天场景中关闭Bloom雨雾已提供光晕、降低Vignette强度雨天视野本就收窄后处理耗时从3.2ms降至1.1ms。最后分享一个硬核技巧在PlayerSettings Other Settings中将Color Space设为Gamma而非Linear。虽然Linear更准确但Gamma模式下雨滴的Alpha混合在低端GPU上更稳定且_RainIntensity参数的线性调节更符合美术直觉——我们在三个项目中验证过Gamma模式下的雨幕观感差异小于5%但帧率提升12%~18%。务实有时候比“正确”更重要。6. 最后一个没人告诉你的细节雨停之后的“余韵”如何做所有教程都教你“怎么下雨”但没人提“雨停之后”。真实体验中雨停不是戛然而止而是有余韵屋檐滴水、地面水洼未干、空气湿度仍高、远处仍有薄雾。这个收尾决定了整个天气系统的完成度。我的“雨停过渡系统”包含三个阶段衰减期0~30秒_RainIntensity从1.0线性降至0.2同时_WindStrength同步衰减。此时雨滴数量减少但近雨层的RainSplash仍会偶尔触发模拟最后几滴。滴水期30~120秒关闭所有雨层仅激活DrippingManager——它扫描场景中所有RoofEdge标记的物体如屋檐、窗沿在标记点生成缓慢下落的水滴用Trail Renderer速度0.1m/s生命周期5秒。水滴落地无声但会在地面生成短暂3秒的浅色水渍贴图。消散期120~300秒_RainIntensity从0.2降至0同时_AtmosphericHumidity参数从0.8降至0.3。这个参数驱动两个效果a) 后处理中的Fog Density微增模拟湿气残留b) 所有金属/玻璃材质的_Wetness值按指数衰减wetness * 0.97每帧直到归零。这个设计让雨停不再是“关掉开关”而是一个有呼吸感的自然过程。玩家不会意识到“雨停了”只会感觉“天放晴了但地上还潮着”。我在做城市漫游项目时特意让主角在雨停后驻足窗边看最后一滴水从玻璃滑落——那个瞬间美术总监发来消息“就是这个感觉找到了。”