Skip to main content

dreamwell_engine/physics/
particle_state.rs

1use serde::{Deserialize, Serialize};
2
3/// GPU-compatible particle state.
4/// Layout must match the WGSL struct exactly for GPU buffer uploads.
5/// Uses repr(C) for deterministic field ordering.
6#[repr(C)]
7#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
8#[cfg_attr(feature = "physics-gpu-types", derive(bytemuck::Pod, bytemuck::Zeroable))]
9pub struct ParticleState {
10    // Position + lifetime
11    pub position: [f32; 3],
12    pub lifetime: f32,
13    // Velocity + age
14    pub velocity: [f32; 3],
15    pub age: f32,
16    // Acceleration + mass
17    pub acceleration: [f32; 3],
18    pub mass: f32,
19    // Rotation quaternion
20    pub rotation: [f32; 4],
21    // Angular velocity
22    pub angular_velocity: [f32; 3],
23    pub drag: f32,
24    // Color
25    pub color: [f32; 4],
26    // Scale + temperature + energy
27    pub scale: [f32; 2],
28    pub temperature: f32,
29    pub energy: f32,
30    // IDs and flags
31    pub emitter_id: u32,
32    pub material_id: u32,
33    pub flags: u32,
34    pub seed: u32,
35    // Tag masks (128-bit total for fast GPU bitwise operations)
36    pub tag_mask_lo: u32,
37    pub tag_mask_hi: u32,
38    // Custom data slots
39    pub custom_f32: [f32; 2],
40}
41
42impl ParticleState {
43    /// Particle flag: alive.
44    pub const FLAG_ALIVE: u32 = 1 << 0;
45    /// Particle flag: promotion candidate.
46    pub const FLAG_PROMOTE_CANDIDATE: u32 = 1 << 1;
47    /// Particle flag: resting (velocity near zero).
48    pub const FLAG_RESTING: u32 = 1 << 2;
49    /// Particle flag: collided this frame.
50    pub const FLAG_COLLIDED: u32 = 1 << 3;
51    /// Particle flag: signal carrier.
52    pub const FLAG_SIGNAL_CARRIER: u32 = 1 << 4;
53
54    pub fn is_alive(&self) -> bool {
55        self.flags & Self::FLAG_ALIVE != 0
56    }
57
58    pub fn is_promotion_candidate(&self) -> bool {
59        self.flags & Self::FLAG_PROMOTE_CANDIDATE != 0
60    }
61}
62
63impl Default for ParticleState {
64    fn default() -> Self {
65        Self {
66            position: [0.0; 3],
67            lifetime: 5.0,
68            velocity: [0.0; 3],
69            age: 0.0,
70            acceleration: [0.0; 3],
71            mass: 1.0,
72            rotation: [0.0, 0.0, 0.0, 1.0],
73            angular_velocity: [0.0; 3],
74            drag: 0.0,
75            color: [1.0; 4],
76            scale: [0.1, 0.1],
77            temperature: 0.0,
78            energy: 0.0,
79            emitter_id: 0,
80            material_id: 0,
81            flags: Self::FLAG_ALIVE,
82            seed: 0,
83            tag_mask_lo: 0,
84            tag_mask_hi: 0,
85            custom_f32: [0.0; 2],
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn particle_state_size() {
96        // 144 bytes: 36 f32-sized fields = 144 bytes
97        assert_eq!(std::mem::size_of::<ParticleState>(), 144);
98    }
99
100    #[test]
101    fn particle_state_default() {
102        let p = ParticleState::default();
103        assert!(p.is_alive());
104        assert!(!p.is_promotion_candidate());
105    }
106
107    #[test]
108    fn particle_state_flags() {
109        let mut p = ParticleState::default();
110        p.flags |= ParticleState::FLAG_PROMOTE_CANDIDATE;
111        assert!(p.is_promotion_candidate());
112        p.flags &= !ParticleState::FLAG_ALIVE;
113        assert!(!p.is_alive());
114    }
115
116    #[test]
117    fn particle_state_alignment() {
118        // Must be aligned to 4 bytes for GPU compatibility
119        assert_eq!(std::mem::align_of::<ParticleState>(), 4);
120    }
121}