Skip to main content

anvilkit_render/renderer/
particle.rs

1//! # 粒子系统
2//!
3//! 提供 CPU 端粒子发射器、粒子生命周期管理和力场支持。
4//!
5//! ## 核心类型
6//!
7//! - [`ParticleEmitter`]: 粒子发射器组件
8//! - [`Particle`]: 单个粒子运行时状态
9//! - [`ParticleSystem`]: 粒子池管理和更新逻辑
10
11use bevy_ecs::prelude::*;
12use glam::Vec3;
13use bytemuck::{Pod, Zeroable};
14use wgpu::util::DeviceExt;
15
16/// 单个粒子的运行时状态
17///
18/// # 示例
19///
20/// ```rust
21/// use anvilkit_render::renderer::particle::Particle;
22/// use glam::Vec3;
23///
24/// let p = Particle::new(Vec3::ZERO, Vec3::Y, 2.0);
25/// assert!(p.is_alive());
26/// assert_eq!(p.age, 0.0);
27/// ```
28#[derive(Debug, Clone, Copy)]
29pub struct Particle {
30    pub position: Vec3,
31    pub velocity: Vec3,
32    pub color: [f32; 4],
33    pub size: f32,
34    pub age: f32,
35    pub lifetime: f32,
36}
37
38impl Particle {
39    pub fn new(position: Vec3, velocity: Vec3, lifetime: f32) -> Self {
40        Self {
41            position,
42            velocity,
43            color: [1.0, 1.0, 1.0, 1.0],
44            size: 0.1,
45            age: 0.0,
46            lifetime,
47        }
48    }
49
50    /// 粒子是否存活
51    pub fn is_alive(&self) -> bool {
52        self.age < self.lifetime
53    }
54
55    /// 归一化年龄 [0, 1]
56    pub fn normalized_age(&self) -> f32 {
57        (self.age / self.lifetime).clamp(0.0, 1.0)
58    }
59
60    /// 更新粒子状态
61    pub fn update(&mut self, dt: f32, gravity: Vec3) {
62        self.velocity += gravity * dt;
63        self.position += self.velocity * dt;
64        self.age += dt;
65        // 淡出:alpha 随年龄线性衰减
66        self.color[3] = 1.0 - self.normalized_age();
67    }
68}
69
70/// 发射形状
71///
72/// # 示例
73///
74/// ```rust
75/// use anvilkit_render::renderer::particle::EmitShape;
76/// let shape = EmitShape::Sphere { radius: 1.0 };
77/// ```
78#[derive(Debug, Clone)]
79pub enum EmitShape {
80    /// 从一个点发射
81    Point,
82    /// 从球体表面发射
83    Sphere { radius: f32 },
84    /// 从圆锥体发射(角度弧度)
85    Cone { angle: f32, radius: f32 },
86    /// 从长方体区域发射
87    Box { half_extents: Vec3 },
88}
89
90impl Default for EmitShape {
91    fn default() -> Self { EmitShape::Point }
92}
93
94/// 粒子发射器组件
95///
96/// # 示例
97///
98/// ```rust
99/// use anvilkit_render::renderer::particle::{ParticleEmitter, EmitShape};
100/// use glam::Vec3;
101///
102/// let emitter = ParticleEmitter {
103///     emit_rate: 50.0,
104///     lifetime: 2.0,
105///     initial_speed: 3.0,
106///     gravity: Vec3::new(0.0, -9.8, 0.0),
107///     shape: EmitShape::Cone { angle: 0.3, radius: 0.1 },
108///     max_particles: 500,
109///     ..Default::default()
110/// };
111/// assert!(emitter.enabled);
112/// ```
113#[derive(Debug, Clone, Component)]
114pub struct ParticleEmitter {
115    /// 每秒发射粒子数
116    pub emit_rate: f32,
117    /// 粒子生命周期(秒)
118    pub lifetime: f32,
119    /// 初始速度大小
120    pub initial_speed: f32,
121    /// 速度随机偏差
122    pub speed_variance: f32,
123    /// 初始粒子大小
124    pub initial_size: f32,
125    /// 大小随机偏差
126    pub size_variance: f32,
127    /// 起始颜色
128    pub start_color: [f32; 4],
129    /// 结束颜色(生命周期末端)
130    pub end_color: [f32; 4],
131    /// 重力
132    pub gravity: Vec3,
133    /// 发射形状
134    pub shape: EmitShape,
135    /// 最大粒子数
136    pub max_particles: usize,
137    /// 是否启用
138    pub enabled: bool,
139    /// 发射累积器(内部使用)
140    pub emit_accumulator: f32,
141}
142
143impl Default for ParticleEmitter {
144    fn default() -> Self {
145        Self {
146            emit_rate: 20.0,
147            lifetime: 1.5,
148            initial_speed: 2.0,
149            speed_variance: 0.5,
150            initial_size: 0.05,
151            size_variance: 0.02,
152            start_color: [1.0, 1.0, 1.0, 1.0],
153            end_color: [1.0, 1.0, 1.0, 0.0],
154            gravity: Vec3::new(0.0, -9.8, 0.0),
155            shape: EmitShape::Point,
156            max_particles: 200,
157            enabled: true,
158            emit_accumulator: 0.0,
159        }
160    }
161}
162
163/// 粒子系统(粒子池 + 更新逻辑)
164///
165/// # 示例
166///
167/// ```rust
168/// use anvilkit_render::renderer::particle::ParticleSystem;
169///
170/// let mut sys = ParticleSystem::new(100);
171/// assert_eq!(sys.alive_count(), 0);
172/// assert_eq!(sys.capacity(), 100);
173/// ```
174pub struct ParticleSystem {
175    particles: Vec<Particle>,
176    capacity: usize,
177}
178
179impl ParticleSystem {
180    pub fn new(capacity: usize) -> Self {
181        Self {
182            particles: Vec::with_capacity(capacity),
183            capacity,
184        }
185    }
186
187    /// 存活粒子数
188    pub fn alive_count(&self) -> usize {
189        self.particles.iter().filter(|p| p.is_alive()).count()
190    }
191
192    /// 最大容量
193    pub fn capacity(&self) -> usize {
194        self.capacity
195    }
196
197    /// 发射一个粒子
198    pub fn emit(&mut self, particle: Particle) {
199        if self.particles.len() < self.capacity {
200            self.particles.push(particle);
201        } else {
202            // 复用已死亡粒子的槽位
203            if let Some(dead) = self.particles.iter_mut().find(|p| !p.is_alive()) {
204                *dead = particle;
205            }
206        }
207    }
208
209    /// 更新所有粒子
210    pub fn update(&mut self, dt: f32, gravity: Vec3) {
211        for p in &mut self.particles {
212            if p.is_alive() {
213                p.update(dt, gravity);
214            }
215        }
216    }
217
218    /// 获取存活粒子的迭代器
219    pub fn alive_particles(&self) -> impl Iterator<Item = &Particle> {
220        self.particles.iter().filter(|p| p.is_alive())
221    }
222
223    /// 清除所有粒子
224    pub fn clear(&mut self) {
225        self.particles.clear();
226    }
227}
228
229// ---------------------------------------------------------------------------
230//  ParticleRenderer — GPU pipeline for particle point-sprite rendering
231// ---------------------------------------------------------------------------
232
233const PARTICLE_SHADER: &str = include_str!("../shaders/particle.wgsl");
234
235/// 粒子 GPU 顶点 (32 bytes)
236#[repr(C)]
237#[derive(Copy, Clone, Debug, Pod, Zeroable)]
238pub struct ParticleVertex {
239    pub position: [f32; 3],
240    pub color: [f32; 4],
241    pub size: f32,
242}
243
244impl ParticleVertex {
245    pub fn layout() -> wgpu::VertexBufferLayout<'static> {
246        const ATTRIBUTES: &[wgpu::VertexAttribute] = &[
247            wgpu::VertexAttribute {
248                offset: 0,
249                shader_location: 0,
250                format: wgpu::VertexFormat::Float32x3,
251            },
252            wgpu::VertexAttribute {
253                offset: 12,
254                shader_location: 1,
255                format: wgpu::VertexFormat::Float32x4,
256            },
257            wgpu::VertexAttribute {
258                offset: 28,
259                shader_location: 2,
260                format: wgpu::VertexFormat::Float32,
261            },
262        ];
263
264        wgpu::VertexBufferLayout {
265            array_stride: std::mem::size_of::<ParticleVertex>() as u64,
266            step_mode: wgpu::VertexStepMode::Instance,
267            attributes: ATTRIBUTES,
268        }
269    }
270}
271
272/// 粒子渲染器场景 uniform (64 bytes)
273#[repr(C)]
274#[derive(Copy, Clone, Pod, Zeroable)]
275pub struct ParticleSceneUniform {
276    pub view_proj: [[f32; 4]; 4],
277}
278
279/// GPU 粒子渲染器
280pub struct ParticleRenderer {
281    pub pipeline: wgpu::RenderPipeline,
282    pub scene_buffer: wgpu::Buffer,
283    pub scene_bind_group: wgpu::BindGroup,
284    /// Cached instance buffer for per-frame reuse
285    cached_instance_buf: Option<(wgpu::Buffer, u64)>,
286}
287
288impl ParticleRenderer {
289    pub fn new(device: &super::RenderDevice, format: wgpu::TextureFormat) -> Self {
290        let shader = device.device().create_shader_module(wgpu::ShaderModuleDescriptor {
291            label: Some("Particle Shader"),
292            source: wgpu::ShaderSource::Wgsl(PARTICLE_SHADER.into()),
293        });
294
295        let scene_bgl = device.device().create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
296            label: Some("Particle Scene BGL"),
297            entries: &[wgpu::BindGroupLayoutEntry {
298                binding: 0,
299                visibility: wgpu::ShaderStages::VERTEX,
300                ty: wgpu::BindingType::Buffer {
301                    ty: wgpu::BufferBindingType::Uniform,
302                    has_dynamic_offset: false,
303                    min_binding_size: None,
304                },
305                count: None,
306            }],
307        });
308
309        let pipeline_layout = device.device().create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
310            label: Some("Particle Pipeline Layout"),
311            bind_group_layouts: &[&scene_bgl],
312            push_constant_ranges: &[],
313        });
314
315        let pipeline = device.device().create_render_pipeline(&wgpu::RenderPipelineDescriptor {
316            label: Some("Particle Pipeline"),
317            layout: Some(&pipeline_layout),
318            vertex: wgpu::VertexState {
319                module: &shader,
320                entry_point: "vs_main",
321                buffers: &[ParticleVertex::layout()],
322            },
323            fragment: Some(wgpu::FragmentState {
324                module: &shader,
325                entry_point: "fs_main",
326                targets: &[Some(wgpu::ColorTargetState {
327                    format,
328                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
329                    write_mask: wgpu::ColorWrites::ALL,
330                })],
331            }),
332            primitive: wgpu::PrimitiveState {
333                topology: wgpu::PrimitiveTopology::TriangleList,
334                ..Default::default()
335            },
336            depth_stencil: None,
337            multisample: wgpu::MultisampleState::default(),
338            multiview: None,
339        });
340
341        let initial = ParticleSceneUniform {
342            view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
343        };
344        let scene_buffer = device.device().create_buffer_init(&wgpu::util::BufferInitDescriptor {
345            label: Some("Particle Scene UB"),
346            contents: bytemuck::bytes_of(&initial),
347            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
348        });
349
350        let scene_bg = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
351            label: Some("Particle Scene BG"),
352            layout: &scene_bgl,
353            entries: &[wgpu::BindGroupEntry {
354                binding: 0,
355                resource: scene_buffer.as_entire_binding(),
356            }],
357        });
358
359        Self {
360            pipeline,
361            scene_buffer,
362            scene_bind_group: scene_bg,
363            cached_instance_buf: None,
364        }
365    }
366
367    /// 从 ParticleSystem 收集存活粒子并渲染
368    pub fn render(
369        &mut self,
370        device: &super::RenderDevice,
371        encoder: &mut wgpu::CommandEncoder,
372        target: &wgpu::TextureView,
373        particle_system: &ParticleSystem,
374        view_proj: &glam::Mat4,
375    ) {
376        let vertices: Vec<ParticleVertex> = particle_system
377            .alive_particles()
378            .map(|p| ParticleVertex {
379                position: p.position.into(),
380                color: p.color,
381                size: p.size,
382            })
383            .collect();
384
385        if vertices.is_empty() {
386            return;
387        }
388
389        // Update view-projection
390        let uniform = ParticleSceneUniform {
391            view_proj: view_proj.to_cols_array_2d(),
392        };
393        device.queue().write_buffer(&self.scene_buffer, 0, bytemuck::bytes_of(&uniform));
394
395        // Reuse cached instance buffer if large enough
396        let data = bytemuck::cast_slice(&vertices);
397        let needed = data.len() as u64;
398        let reuse = self.cached_instance_buf.as_ref().map_or(false, |(_, cap)| *cap >= needed);
399        if !reuse {
400            self.cached_instance_buf = Some((
401                device.device().create_buffer(&wgpu::BufferDescriptor {
402                    label: Some("Particle Instance VB (cached)"),
403                    size: needed,
404                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
405                    mapped_at_creation: false,
406                }),
407                needed,
408            ));
409        }
410        let instance_buffer = &self.cached_instance_buf.as_ref().unwrap().0;
411        device.queue().write_buffer(instance_buffer, 0, data);
412
413        {
414            let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
415                label: Some("Particle Pass"),
416                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
417                    view: target,
418                    resolve_target: None,
419                    ops: wgpu::Operations {
420                        load: wgpu::LoadOp::Load,
421                        store: wgpu::StoreOp::Store,
422                    },
423                })],
424                depth_stencil_attachment: None,
425                timestamp_writes: None,
426                occlusion_query_set: None,
427            });
428
429            rp.set_pipeline(&self.pipeline);
430            rp.set_bind_group(0, &self.scene_bind_group, &[]);
431            rp.set_vertex_buffer(0, instance_buffer.slice(..));
432            // 6 vertices per quad (billboard), one instance per particle
433            rp.draw(0..6, 0..vertices.len() as u32);
434        }
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn test_particle_vertex_size() {
444        assert_eq!(std::mem::size_of::<ParticleVertex>(), 32);
445    }
446
447    #[test]
448    fn test_particle_lifecycle() {
449        let mut p = Particle::new(Vec3::ZERO, Vec3::Y, 1.0);
450        assert!(p.is_alive());
451
452        p.update(0.5, Vec3::ZERO);
453        assert!(p.is_alive());
454        assert!((p.position.y - 0.5).abs() < 0.001);
455
456        p.update(0.6, Vec3::ZERO);
457        assert!(!p.is_alive());
458    }
459
460    #[test]
461    fn test_particle_system() {
462        let mut sys = ParticleSystem::new(10);
463        sys.emit(Particle::new(Vec3::ZERO, Vec3::Y, 1.0));
464        sys.emit(Particle::new(Vec3::ZERO, Vec3::X, 0.5));
465
466        assert_eq!(sys.alive_count(), 2);
467
468        sys.update(0.6, Vec3::ZERO);
469        assert_eq!(sys.alive_count(), 1); // second particle died (0.5s lifetime)
470
471        sys.update(0.5, Vec3::ZERO);
472        assert_eq!(sys.alive_count(), 0);
473    }
474
475    #[test]
476    fn test_particle_system_recycle() {
477        let mut sys = ParticleSystem::new(2);
478        sys.emit(Particle::new(Vec3::ZERO, Vec3::ZERO, 0.1));
479        sys.emit(Particle::new(Vec3::ZERO, Vec3::ZERO, 0.1));
480
481        // Both die
482        sys.update(0.2, Vec3::ZERO);
483        assert_eq!(sys.alive_count(), 0);
484
485        // Recycle slot
486        sys.emit(Particle::new(Vec3::ONE, Vec3::ZERO, 1.0));
487        assert_eq!(sys.alive_count(), 1);
488    }
489}