☰
Laya引擎下雨效果实战:从粒子系统到屏幕氛围的全流程解析
2026/10/2 4:52:20 网站建设 项目流程

做天气系统是我在游戏开发里最喜欢碰的一类需求,尤其是下雨天。它不像战斗系统那样逻辑复杂,也不像UI那样琐碎,但一个高质量的下雨效果能让整个场景的质感瞬间上一个台阶。这篇文章就完整记录一下我在Laya引擎里落地“下雨环境效果”的全过程,从方案选型、粒子参数调试、地面交互表现到真机性能优化,以及我踩过的那些文档里绝对不会写的坑。

先说我做这个需求的初衷:那是一个俯视角2.5D项目,场景本身偏写实,需要一个让玩家一眼就能感受到“天气变了”的氛围层。最开始我的第一反应是偷懒,直接找两张雨丝贴图做UV滚动叠加,效果出来之后被美术一顿吐槽——雨是“贴在屏幕上”的,跟场景完全没有交互感,人物走过去裤腿都不带湿的,地面溅不起来水花,整个画面像是放了一段加了滤镜的视频。所以老老实实重新做,用粒子系统配合屏幕Shader和场景联动,才拿到了最终这版效果。

这篇文章不是教你复制一个万能下雨脚本,而是把我在Laya里做这套效果时,所有关键节点怎么想、怎么选、怎么调、怎么救回来的全过程写出来。内容比较适合已经会用Laya做基础场景、准备往更高一级的技术美术方向走的开发者阅读,新手也可以跳着看,参数表和排查清单那两节可以直接抄作业。

1. 整体设计思路:雨不是“画”上去的,是“演”出来的

首先要想清楚一个问题:生活中真实的雨幕到底由哪几层视觉信息构成的?很多人做下雨效果失败,就是只抓住了“雨丝”这个单一元素,剩下的全丢掉了。

我的拆解方式是四层叠加:远景雨幕层、近景动态粒子层、地面交互层、屏幕氛围层。远景雨幕是那种半透明的斜线幕布,负责在视觉上铺开“整个场景都在下雨”的空间感;近景粒子是玩家视野内能看清的雨滴轨迹,负责运动和速度感;地面交互层是雨落地后的水花溅射、涟漪扩散,负责让场景“活”起来;屏幕氛围层是镜头上的雨滴划痕和整体偏冷的色调混合,负责情绪的收口。

这四层缺一不可。只做雨丝没有地面反应,雨就像鬼魂在下;只做地面反应没有天空幕布,场景就会显得干瘪。这个分层思路是后面所有技术实现的总纲。

再补一个比较重要的原则:这套效果全部跑在客户端本地,不需要服务器同步,因为天气对所有玩家是一致的,是纯表现层的东西。所以开发时自由度很大,但也意味着性能压力全在自己这边,粒子数量、纹理尺寸、Shader复杂度都要自己扛。

1.1 为什么用Laya自带粒子系统而不是纯Shader?

这是我被问得最多的问题。确实网上有大把用Shader做全屏雨效的教程,思路也很清晰:屏幕空间射线计算雨滴的透视轨迹、按深度夹取场景遮挡关系,效果上限很高。但我的项目是一个大型多人在线游戏,角色、场景、特效同屏压力本来就大,全屏后处理Shader需要逐像素计算,在高端机上是毛毛雨,但在中低端安卓机上会直接拖进fps泥潭。

所以我选了“粒子为主、Shader为辅”的混合方案:所有雨滴的移动、分布、生命周期交给粒子系统,这是Laya最成熟的模块,异步加载、合批、碰撞都有现成能力和坑可踩;Shader只做两件轻量的事——屏幕边缘的雨滴拖尾划痕和地面的漫反射湿润增强,而且都用极简实现。

对比下来,这套方案的上限也许没有纯GPU方案那么“电影级”,但胜在可控、稳定、跨设备一致性好。尤其当你面对的是安卓千元机这种下限很低的平台时,稳定比上限重要得多。

1.2 场景联动:不只是视觉,还有听感和交互

雨的效果除了眼睛能看到,耳朵也应该收到信息。这一步是很多团队容易忽略的:配了雨声却只做了一段循环音频,下大雨和毛毛雨的音量、音色完全没区分,场景里的物件和角色也不做任何反应。

我的做法是做了三档天气强度:小雨、中雨、暴雨。每档对应不同的粒子发射速率、雨声循环切换、音量Lerp过渡时间。在代码里维护一个WeatherState的枚举,Notify给粒子控制器和音频管理器,用3秒的淡入淡出做切换,避免瞬切导致的听觉突兀。

同时,场景里的非角色元素也要给反应:火把加一个受潮概率熄火、水面加一个涟漪粒子发射器、角色身上的武器加一个湿反光参数。这些工作量不大,但玩家体验到的“真实感”是靠这些细节堆起来的。

1.3 双实现路径:2D粒子还是3D粒子?

Laya引擎里粒子系统分2D和3D两套。2D粒子适合做纯屏幕空间的平面效果,比如UI特效、技能拖尾;3D粒子是真正的世界空间粒子,可以挂在场景节点上随摄像机移动、有真实的深度关系、能和场景模型发生遮挡。

我这个项目是俯视角2.5D,物理空间是3D的,视角是2D化的。所以雨滴必须用3D粒子系统,这样才能保证远处的雨和近处的雨透视关系正确。如果强行用2D粒子贴在屏幕上,一旦摄像机开始推进或横移,雨丝就会像贴纸一样暴露穿帮。

补充一个小知识:Laya的3D粒子和2D粒子在编辑器里的操作逻辑差别非常大,3D粒子的Transform是骨骼动画驱动,所有发射器参数都在LayaParticle3D组件里。如果你之前只碰过2D粒子,第一次打开3D粒子面板会有劝退感,参数多出一个数量级。但别怕,真正核心的参数就那么几个,后面我会挨个拆。

2. 雨丝粒子系统的核心参数解读

粒子系统是这个效果的绝对主力,参数多如牛毛,但真正决定“像不像下雨”的只有几组。我按重要性排了个序,新手从第一条开始改就行,不要一上来就满面板乱调。

2.1 发射器形态:为什么用矩形发射器而不是锥形?

Laya的3D粒子发射器支持球形、锥形、矩形等形状。做雨的时候最自然的直觉是选锥形发射器,从天空的一个区域往下喷。但实际跑起来你会发现,锥形发射器的初始速度方向是从锥尖向外发散的,降落到地面时雨滴的落点会散成一个圆形,雨丝之间互相穿插、密度不均匀,看起来像下“冒烟”而不是下雨。

正确的做法是矩形发射器:发射器的长宽覆盖住玩家的视野范围,发射方向垂直朝下,粒子初速度只在Y轴给一个固定负值,X和Z的初速度给一个很小的随机抖动。这样所有雨滴都是平行下落的,密度均匀、方向统一,视觉上才干净。

我踩过一次大坑:用锥形发射器做了个看起来还行的小区域雨效,但把摄像机拉远后发现雨的范围远不够铺满视野,临时改了发射器尺寸,结果粒子的世界坐标错乱,雨滴从地面往天上飞。最后是把整个发射器参数重置重调的,花了一个晚上。

2.2 发射速率、生命周期和初速度的黄金组合

这三个参数决定了雨的“密度”和“速度感”,它们是联动关系的。发射速率决定单位时间生成多少粒子;生命周期决定粒子从出生到消失存活多久;初速度决定雨滴下落多快。

实际操作上,我是这样定参数的:先定生命周期,因为雨滴从发射到落地的时间决定了可见的雨丝长度。摄像机俯视角下,视野高度大概20米,雨滴下落速度我取35米/秒,那么生命周期就是 20 ÷ 35 ≈ 0.57秒。这个值太小雨丝会短成雨点,太大会让雨丝穿越地面。初速度设成Y = -30到-40的区间,X和Z给±1.5的抖动。

发射速率调起来更像调音台:密度不够画面空,密度过头全是糊掉的线。我做的时候是录屏慢放看每一帧的雨丝数量,中雨档我调到每秒900个粒子。这个数字看着大,但3D粒子单粒子成本很低,而且可以依赖粒子系统的合批能力,900个粒子并不会压垮CPU,前提是你别同时开一大堆别的粒子特效。

2.3 纹理制作:雨滴贴图的自绘与资源避坑

雨丝贴图看起来简单,实际翻车率特别高。从网上下载的免费雨滴贴图大多带着黑色背景或者压缩纹理的白色噪点,直接导入Laya后粒子周围全是黑色方块,效果惨不忍睹。

我强烈建议自己画一张雨滴贴图,真的只需要一分钟:新建一个64×256的画布,用渐变工具拉一条白色斜线,中间亮度高、边缘完全透明,保存为TGA或PNG带透明通道,导入时把纹理类型设成Alpha贴图,Wrap Mode设Clamp。把粒子渲染的混合模式设成Additive加色混合,雨丝就会有一种发光的通透感,比Normal混合和背景生硬叠加自然得多。

如果实在要用下载的资源,那必须确认三件事:图片带不带走Alpha通道(JPG直接pass)、尺寸是不是2的N次幂、压缩格式在目标机型的兼容性。很多“laya模型下载”类的资源包是直接从Unity或U3D项目里扒出来的,纹理格式不一定能在Laya Runtime里正常解压,这问题查起来非常隐蔽,不是画面问题,是资源加载时直接报内存错误。

3. 地面交互:雨滴落地的“最后一厘米”

如果说雨丝粒子是画面的骨架,地面交互就是让效果从“不错”到“惊艳”的分水岭。玩家根本不会盯着空中的雨看太久,他们的视线焦点永远是角色周围一米的位置,而那里最抢眼的就是地面溅射。

3.1 溅射粒子的实现与生命周期策略

地面溅射等于第二套粒子系统,而且参数逻辑和雨丝完全不同。溅射粒子的发射点应该落在雨丝粒子死亡的位置附近,但粒子系统之间没法直接传递事件坐标,所以我用了一个比较朴素的方案:在主雨粒子的脚本Update里做碰撞检测,一旦检测到地面接触就在接触点生成一个溅射粒子的Play操作。

溅射粒子的参数组:初速度向上给5~8,水平方向随机,生命周期0.2~0.4秒,粒子Size从0.3衰减到0.05,颜色从半透明白渐变成完全透明。数量上不需要和雨丝一一对应,每3~5根雨丝触发一次溅射就够,既能保持密度又不至于让粒子数翻倍。

这里有个小心得:溅射粒子不要只做垂直向上的小水珠,我叠加了一层贴地的环形涟漪贴图粒子,速度更慢、生命周期更长,模拟雨水在地面扩散的湿润痕迹。两层叠加之后,地面才真的像被雨浸透了。

3.2 场景物体的受体反馈:怎么让溅射出现在正确的地方

溅射粒子最容易出的问题就是穿模和下穿:粒子发射出来直接嵌进地面模型里,或者在楼梯、台阶这种高低落差位置从侧面穿出去。Laya的粒子系统支持Collision模块,但全场景物理碰撞开销太大,不能真打物理引擎。

我的折中做法:场景中所有能落雨交互的地面,统一挂一个带Collider的透明体积节点,Collider类型用盒体或网格碰撞体,Trigger检测打开。粒子检测到碰撞后只做视觉触发,不移除粒子,而是将粒子速度重置一个向上分量。阶梯和坡道这种特殊位置,额外放几个小体积碰撞节点微调效果。

这一套跑下来的效果,雨滴落在石板路和落在草地上的反应其实是一样的,说实话和现实差异挺大。更好的方案是给地面材质设置一个“湿润度”全局Shader参数,不同材质对湿润度的响应曲线不同,石板更反光、草地更哑光。这个属于后期打磨,如果你的主力Shader可控,强烈建议加上,视觉收益非常大。

3.3 一套参数打天下的幻想破灭:不同地形各调各的

我最初天真地以为溅射粒子一套参数通吃所有地形。后来发现,雨滴落在水面上的涟漪效果和落在地面上的溅射效果完全是两个物种。水面要求粒子有扩散波纹感和一定的反射度,地面要求粒子有反弹跳跃感。

所以我把溅射单独拆成两套:WaterSplashEffect挂在所有水面节点的中心,用同心圆扩散纹理;GroundSplashEffect挂在地面主节点上,用随机方向的短促线形粒子。两套共用一个强度控制接口,下雨强度变化时统一调节发射速率。

4. 屏幕氛围与Shader增强

雨丝和地面都有了,画面还是差最后一口气,差在镜头上。真实雨天你透过窗户或镜头看出去,玻璃上会有雨滴痕,远处的景物会因为水汽有一点模糊发灰。这些氛围感靠粒子是表现不出来的,必须动用Shader。

4.1 屏幕雨痕Shader:轻量实现与性能取舍

玻璃雨痕的常规范式是屏幕空间UV加雨滴法线纹理扰动。听起来很吓人,其实实现很轻:取一张带多个圆形雨滴的法线贴图,在片元着色器里用时间偏移做UV滚动,再叠一层噪声作为水滴滑落的路径扰动,最后用雨滴法线扰动屏幕UV采样主场景即可。

在Laya里,这个效果写成一个后处理Shader挂到Camera上。Shader整体就十几行核心代码,G-Buffer都不需要,只要采样原始屏幕颜色加一个法线贴图。我实测在1080P下跑在骁龙660级别的机器上,帧率影响在3帧以内,属于可以接受的范围。

Shader的具体实现里最容易坑人的是UV滚动速度。雨滴在玻璃上滑落是重力驱动的,速度应该是越往下越快,是个加速过程,而不是匀速滚动。我刚开始图省事用匀速,结果雨滴像被匀速拖动的抹布,非常假。最后用一个二次函数做高度对应速度的映射,效果一下就自然了。

4.2 画面调色:冷色调和对比度的微调技巧

雨天的氛围除了物理表现,很大程度靠调色。做雨效时我会给Camera挂一个极简的ColorAdjust组件,把色温往冷蓝方向偏移,饱和度降低10~15%,对比度略微降低,让画面有一种“湿度感”。

调色的数值不要用固定的,要跟着雨强走。暴雨时色温偏得更冷、对比度更低,小雨时只做轻微偏移。这块建议在代码里写一个基于WeatherState的插值过渡,切换时用Tween补间,避免突然跳变让玩家不适。

顺带提一个细节:雨天场景的雾效也是氛围层的一部分。Laya的3D场景支持指数雾,把雾的密度调高、颜色往灰色偏,远处场景会被一层水汽笼罩,空间深度感立刻拉出来。但注意别把雾开过猛,否则性能扛不住,画面也会像蒙了一层毛玻璃。

4.3 全屏后处理的叠层顺序问题

后处理类效果之间是有优先级顺序的。我项目里顺带还挂了Bloom和色差效果,曾经把雨痕Shader排到了Bloom之前,结果雨痕被Bloom一糊,玻璃上全是光斑,完全看不到雨滴形态了。最后调整的顺序是:场景渲染 -> 雨痕扰动 -> 雾效混合 -> 调色 -> Bloom。这个顺序本身并没有绝对标准,但建议你把雨痕放在所有“模糊类”效果之前,让雨滴边缘保持锐利。

5. 实操过程记录:从一个空场景到完整下雨效果

这一节是复现向的。如果你跟着我上面的设计文档走,下面是我实测从零搭到能玩的全流程,按步骤操作基本能还原我的效果。

5.1 环境准备与Laya版本说明

我用的LayaAir 3.0的稳定版本,3D模块和粒子系统都是相对成熟的。如果你还在用2.x版本,粒子参数面板的命名不太一样,需要对照着稍微调整。编辑器导出项目后,运行在Chrome做预览,真机测试用Android端做性能基线。

开始前需要准备资源:雨丝纹理一张、溅射水滴纹理一张、涟漪纹理一张、雨滴法线贴图一张。全部64到256量级的小图,丢进项目的assets/textures/weather目录。不做图集,因为这些贴图只被粒子系统和Shader各自引用一次,图集会增加额外的加载和引用复杂性。

5.2 创建3D粒子雨丝系统的步骤清单

  1. 在场景中新建一个3D节点,命名为RainRoot,挂上LayaParticle3D组件。
  2. 发射器Shape选Rectangle,尺寸设(80, 40, 0),X轴覆盖视野宽度,Y轴作为厚度。
  3. StartColor的渐变曲线设从纯白到浅白带一点透明,模拟远近距离的雨滴亮度差异。
  4. StartSize设0.03到0.06,雨丝细一点才像雨,粗了就是下铁丝。
  5. GravityModifier设(-2, -9.8, -2),额外施加一点水平风力和垂直重力,让雨有自然的斜飘感。
  6. Duration设为Infinity,Looping打开。

这段配置做完,先别急着往下走,跑一下看看雨丝密度和方向是不是符合直觉。我第一次就是跳步了,结果后面调溅射时才发现雨压根不是垂直下的,又重新改了发射器角度。

5.3 溅射粒子和脚本控制代码骨架

溅射粒子系统建好后,需要挂一个自定义脚本控制粒子发射事件。核心代码是检测雨丝粒子位置的地面碰撞并触发溅射,示意如下:

class RainSplashController extends Script { @property({type: Particle3D}) splashParticle: Particle3D; private hitPoints: Vector3[] = []; onUpdate(): void { // 遍历活动雨粒子,检测与GroundCollider的碰撞 let activeParticles = this.rainSystem.getActiveParticles(); for (let p of activeParticles) { if (p.position.y <= groundY) { // 触发溅射 this.splashParticle.transform.position = p.position; this.splashParticle.particleSystem.play(); p.reset(); // 回收雨粒子 } } } }

注意你看到的这段是简化骨架,实际Laya粒子系统的粒子遍历要基于内部的ParticleData数组做遍历,不能直接for of,否则GC压力会大。建议用一个固定上限的粒子池做复用,超过上限时丢掉最老的粒子而不是继续创建。

5.4 参数调试的实测经验表

调试粒子参数是个痛苦且反直觉的过程。我把最终跑通的一组参数和调试时的观察结果整理成了一个速查表,方便你对照着调:

参数我的最终值调大后果调小后果
发射速率900/s画面糊成线团雨稀稀疏疏
生命周期0.55s雨丝落地穿透雨丝变短
初速度Y-35雨像子弹雨在半空漂浮
初速度X/Z抖动±1.5雨像乱刮风雨变成直线
粒子Size0.04铁丝雨灰尘雨
段数/粒子纹理分割3段雨丝割裂看不出是雨

这张表不是玄学,每一项背后都有视觉原理。加速度、生命周期和Size共同决定了雨丝在视觉上的“长度”;发射速率和视角范围共同决定“密度”;抖动参数决定“自然度”。你把数值调成我的终值,效果是稳的,但不同场景的视野高度和摄像机距离是需要微调的。

6. 常见问题与排查实录

这篇特意把排查放在最后,因为下雨效果调试时出的问题千奇百怪,每个都能让人卡半天。我用最典型的几个问题做个速查表,并说说背后的排查思路。

6.1 粒子穿透地面和穿墙问题

问题描述:雨丝和溅射粒子在接近地面或墙体时直接穿过,或者嵌进模型一半。

排查逻辑:先看碰撞是否开启。Laya粒子碰撞检测需要CollisionShape有实际碰撞体参与,只挂Trigger不行。再看碰撞体的层级,粒子节点的Local坐标和场景节点的世界坐标不一致时,碰撞检测的坐标转换容易出错。

解决方式:给地面统一挂碰撞体积,粒子碰撞用Shape Query模式而不是物理模拟模式。穿透特别严重的斜坡,考虑直接把雨丝粒子生命周期截短到落地点,而不是依赖碰撞体拦截。

6.2 雨丝闪烁或呈点状断续

问题描述:雨丝在移动的镜头下闪烁不定,或者明明设置了长条纹理却显示成一个个光点。

排查逻辑:这是贴图过滤和粒子UV动画设置的问题。纹理的MinFilter/MagFilter如果设成了Nearest,每帧采样都会跳像素,雨丝就闪。粒子组的UV Animation如果开了,但贴图没有按帧分割,就会采样到空白区域显示成点状。

解决方式:纹理过滤设成Bilinear,关掉粒子的UV Animation,让整条纹理始终作为完整雨丝被拉伸。顺带检查一下粒子系统的RenderMode是不是设成了StretchedBillboard,如果不是,粒子永远显示为贴图原始形状而不是被拉伸的雨丝。

6.3 真机性能骤降或发热明显

问题描述:编辑器里跑得飞起,一到真机上就发热、掉帧。

排查逻辑:编辑器预览用的是PC性能,和真机差距巨大。先用Laya Profiler看真机上的粒子Active数量和DrawCall。粒子数量超过2000或者DrawCall超过30,基本就是性能瓶颈所在。

解决方式:把粒子发射速率降下来,真机版本我通常会降到编辑器的70%;溅射粒子和雨丝粒子合并到同一个纹理图集以减少DrawCall;屏幕雨痕Shader在低端机上关闭,用一个静态贴图代替。最后给设置面板加一个“极致/标准/流畅”三档画质选项,不同档位对应不同粒子密度和Shader开关。

6.4 雨声循环切换时的爆音

问题描述:从小雨切到暴雨,音频切换瞬间有一声“啪”的爆音。

排查逻辑:音频淡入淡出没有做逐帧渐变,直接用AudioSource.Stop()再Play()新的clip,波形在相位不连续处会生成爆音。

解决方式:配合WeatherState切换写一个音量管理协程,3秒内从当前音量线性Lerp到目标音量,期间不切换clip,只切换clip前先降低音量到0。或者干脆只用一个雨声循环,通过调整音量和EQ模拟不同强度,省去切换clip的麻烦。

6.5 粒子系统在场景切换时的内存泄漏

问题描述:切场景后内存不释放,连续切几次之后开始卡顿。

排查逻辑:粒子系统在场景销毁时如果没有主动Stop和Clear,粒子数据数组会滞留在内存里等待GC,而Unity和Laya的GC策略对托管堆的释放都不及时。

解决方式:在场景卸载事件里手动调用粒子系统的Stop()并设置Emitter.emit = false,再把粒子池引用置空。如果项目里经常切场景,建议做一个PoolManager统一管理粒子系统的创建和回收,而不是每次new一个新的。

7. 我个人的一个习惯性收尾小技巧

整篇写到这里,最后分享一个让下雨效果更容易出彩的小习惯:给你的雨效加一个可控的“时间缩放”参数。具体做法是把粒子的SimulationSpeed暴露到运维后台的热更配置里,策划可以随时在后台调快调慢雨丝下落速度,实时预览效果。

我当初是为什么加这个呢?因为有次美术提了个需求说要雨“更暴躁一点”,我只能火速改代码改参数重新出包。但加了SimulationSpeed之后,美术自己拖一下滑杆就能找到合适的“暴躁度”,完全不用等程序发版。这个看似不起眼的自由度,实际上挽救了好几次本来要通宵改包冲刺的版本。

做环境和天气效果,技术上真的没有多少高深的东西,比的是谁愿意多花时间观察真实世界,谁愿意在参数堆里多试几轮,谁踩完坑之后愿意把它记下来。希望这篇文章能帮你把已经踩过或者即将踩的坑提前填平,让你能把更多时间花在真正让画面变好看的事上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询