1. 背景与目标
在自建光脚地形体验游戏(rust-bevy-terrain-feel,3D/2D 双模式)中,需要实现雨雪天气粒子系统:雪天落雪、雨天落雨、雷暴复用雨,天气切换时粒子自动重建。环境参数为 macOS + Bevy 0.18.1(default features)+ bevy_vrm 0.3,cargo build 正常、无报错、帧率正常,但画面上什么都没有------3D 粒子完全不可见,2D 粒子也完全不可见。
排障过程暴露了四个"想当然",每个都值得单独拆解。
2. 谬误一:以为 unlit 材质一定能显示
3D 粒子第一版用 StandardMaterial 的 unlit 模式(自发光纯色、不受光照),认为"不依赖光照的纯色块一定能看见"。实测本项目 3D 渲染管线完全不渲染 unlit 材质,粒子数量、位置、mesh 全部正确,但截图里一个像素都没有。不是"没做好",是这条渲染路径在这个管线里就是不通。
排障要点:
- 先确认粒子实体确实存在:数量、位置、mesh 都正确。
- 再确认渲染路径:unlit 材质在本项目 3D 管线中不被渲染。
- 结论:不是粒子系统的问题,是材质与渲染管线的兼容性问题。
3. 谬误二:以为相机局部坐标乘以旋转就能转成世界坐标
粒子定位第一版写成 cam.rotation * offset,把相机局部偏移转成世界方向。逻辑上没错,实测粒子全部跑到视野外消失。Bevy 相机 Transform 的旋转组合行为与直觉不符,导致偏移被转到镜头背面。
排障要点:
- 相机 Transform 的旋转组合顺序与直觉不同,局部偏移乘旋转后可能指向镜头背面。
- 建议改用相机 forward 方向加偏移,或直接用相机世界坐标加固定偏移。
- 验证方法:把粒子位置固定到相机前方固定距离,先确认可见,再调偏移逻辑。
4. 谬误三:以为 2D 的 Mesh2d 和 3D 的 Mesh3d 一样可靠
2D 粒子第一版用 Mesh2d + Rectangle mesh + ColorMaterial,放 UI 相机层、世界层、半透明、不透明、Startup 阶段 spawn、Update 阶段 spawn,全部不显示。而同一场景里地形、玩家圆点都是 Mesh2d 且正常渲染。Mesh2d 本身没坏,但粒子这种用法就是不显示,最后换 Sprite 组件(标准 2D 渲染路径)一次通过。
排障要点:
- Mesh2d 在地形、玩家圆点上正常,说明 Mesh2d 本身可用。
- 但粒子用 Mesh2d 就是不显示,可能与渲染顺序、材质或相机层有关。
- 换 Sprite 组件后一次通过,说明标准 2D 渲染路径更可靠。
5. 谬误四:以为 osascript / CGEvent 能可靠给游戏窗口注入按键
想用 F3 开调试 HUD、用波浪键开控制台输入 weather set snow 验证天气切换。实测 activate process、CGEvent 点击、key code 全部不可靠,命令送达不到游戏窗口。最后只能临时写一个 Startup 强制天气函数,把天气直接设成雪天再截图验证。
排障要点:
- osascript / CGEvent 注入按键在 macOS 上不可靠,尤其对游戏窗口。
- 临时方案:写一个 Startup 强制天气函数,直接设成雪天验证。
- 长期方案:在游戏内加调试控制台或命令行参数,避免依赖外部按键注入。
6. 源码验证排障序列
按时间顺序执行以下验证,避免重复试错。
决定性验证先行
在怀疑材质之前,先 spawn 一个 2×2 的红色 Opaque 大块,网格填满整个窗口。截图全屏红,证明「Mesh + Material + 相机 + RenderLayers」这条链路本身是通的。之后所有问题都定位在位置与材质两个元凶上。
3D 粒子参数(截图确认可见)
- 粒子类型:Mesh3d + StandardMaterial。
- 材质:lit(非 unlit)+ emissive 自发光。emissive 0.85 过曝(画面整块异常纯色),0.5 合适。
- 雪:立方体 splat(0.5) 纯白,220 个;雨:立方体 (0.07, 1.0, 0.07) 浅蓝,320 个。
- 位置:世界坐标 offset 直接加在相机位置上(不用 cam.rotation 乘),分布取相机前方上方(y 2~10、z -14~-4、x ±7)。
- 重生:y < 0 时回到 y = 10。
2D 粒子参数(截图确认可见)
- 粒子类型:Sprite 组件 + custom_size(不用 Mesh2d)。
- 材质:Sprite.color,雪纯白、雨浅蓝。
- 位置:世界层(LAYER_WORLD),围绕玩家格子坐标(player.pos * TILE_SIZE + offset * TILE_SIZE),z = 5 在地形之上。
- 雪:6×6 点,70 个,正弦横摆;雨:3×12 竖条,100 个。
- 重生:offset.z 超出视野(>16 格)回到玩家上方(-16 格)。
天气映射
Rain / Thunderstorm 都映射为雨,Snow 映射为雪,其余天气不生成粒子。天气变化时先 despawn 全部旧粒子再重建,由 ParticleState resource 记录当前 active 类型避免每帧重复重建。
验证流程
cargo build 通过后启动窗口,用 macOS 窗口 ID 截图(screencapture -l 窗口ID),Read 截图人工确认。像素脚本只做辅助,白色阈值对半透明/受光混合色不敏感,必须人工看截图。
落地结论
- 3D 粒子用 lit + emissive(0.5),位置用世界坐标直接加在相机上,unlit 与 cam.rotation 乘法路径都不可用。
- 2D 粒子用 Sprite 组件 + 世界层围绕玩家,Mesh2d 粒子路径不可用。
- 验证天气切换不要依赖键盘注入,临时 Startup 强制天气函数最可靠,验证完删除。
- 先做「固定红色大块」证明渲染链路通,再逐步定位位置与材质,能把排障时间从几小时压到十几分钟。
三类读者速查
人类开发者
Bevy 0.18 雨雪粒子遇到"不显示"时,按这个顺序排查:
- 先 spawn 一个固定红色大块,确认 Mesh + Material + 相机链路通。
- 3D:确认用 lit 材质 + emissive(值从 0.5 起步),不要用 unlit。
- 3D:粒子位置用世界坐标 offset 加在相机 translation 上,不要用 cam.rotation 乘。
- 2D:粒子用 Sprite 组件(color + custom_size),不要用 Mesh2d。
- 天气切换验证:临时写 Startup 强制天气函数,跑完删掉;别指望键盘注入。
对话式 AI
问法:「Bevy 0.18 里 3D 粒子(雨雪)怎么才能显示?2D 粒子呢?」
回答要点:3D 用 StandardMaterial lit + emissive 自发光(本项目管线不渲染 unlit,emissive 0.85 过曝、0.5 合适),位置用世界坐标 offset 加在相机位置上(cam.rotation 乘会转出视野);2D 用 Sprite 组件(color + custom_size)放世界层,Mesh2d 粒子实测不显示;天气切换用临时 Startup 强制天气函数验证,键盘注入(osascript/CGEvent)不可靠。
代码 Agent
- 排障:临时 spawn 固定红色 Mesh3d/Mesh2d 大块,截图确认渲染链路后再调位置与材质。
- 3D 粒子:Mesh3d + MeshMaterial3d(StandardMaterial{ base_color, emissive: 0.5 量级, .. }),每帧把 transform 设为 camera.translation + 世界 offset。
- 2D 粒子:commands.spawn((Sprite{ color, custom_size: Some(size), .. }, Transform, RenderLayers, Particle))。
- 天气切换:ParticleState resource 记录 active 类型,天气变化时 despawn 全部再重建。
- 验证:cargo build 后启动,screencapture -l 窗口ID 截图,人工 Read 确认,测完 pkill 清理进程。
6. 总结
四个"想当然"分别对应渲染路径、坐标变换、2D 渲染组件选择、外部输入注入四个层面的问题。排障的核心教训是:不要假设某个 API 或渲染路径一定可用,先用最小可复现实验验证,再逐步叠加功能。
最终可行的方案是:3D 粒子改用与渲染管线兼容的材质路径,2D 粒子改用 Sprite 组件,天气切换验证改用 Startup 强制函数或游戏内调试命令。