1use crate::{
2 context::DrawContext,
3 sprite::SpriteTexture,
4 utils::{Drawable, ShaderRef, Vertex, transform_to_matrix},
5};
6use smallvec::SmallVec;
7use spitfire_glow::{
8 graphics::{GraphicsBatch, GraphicsTarget},
9 renderer::{GlowBlending, GlowUniformValue},
10};
11use std::{borrow::Cow, cell::RefCell, collections::HashMap, marker::PhantomData};
12use vek::{Quaternion, Rect, Rgba, Transform, Vec2, Vec3};
13
14#[derive(Debug, Default, Clone)]
15pub struct ParticleEmitter {
16 pub shader: Option<ShaderRef>,
17 pub textures: SmallVec<[SpriteTexture; 4]>,
18 pub uniforms: HashMap<Cow<'static, str>, GlowUniformValue>,
19 pub blending: Option<GlowBlending>,
20 pub screen_space: bool,
21}
22
23impl ParticleEmitter {
24 pub fn single(texture: SpriteTexture) -> Self {
25 Self {
26 textures: vec![texture].into(),
27 ..Default::default()
28 }
29 }
30
31 pub fn shader(mut self, value: ShaderRef) -> Self {
32 self.shader = Some(value);
33 self
34 }
35
36 pub fn texture(mut self, value: SpriteTexture) -> Self {
37 self.textures.push(value);
38 self
39 }
40
41 pub fn uniform(mut self, key: Cow<'static, str>, value: GlowUniformValue) -> Self {
42 self.uniforms.insert(key, value);
43 self
44 }
45
46 pub fn blending(mut self, value: GlowBlending) -> Self {
47 self.blending = Some(value);
48 self
49 }
50
51 pub fn screen_space(mut self, value: bool) -> Self {
52 self.screen_space = value;
53 self
54 }
55
56 pub fn emit<I: IntoIterator<Item = ParticleInstance>>(
57 &'_ self,
58 instances: I,
59 ) -> ParticleDraw<'_, I> {
60 ParticleDraw {
61 emitter: self,
62 instances: RefCell::new(Some(instances)),
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
68pub struct ParticleInstance {
69 pub region: Rect<f32, f32>,
70 pub page: f32,
71 pub tint: Rgba<f32>,
72 pub transform: Transform<f32, f32, f32>,
73 pub size: Vec2<f32>,
74 pub pivot: Vec2<f32>,
75}
76
77impl Default for ParticleInstance {
78 fn default() -> Self {
79 Self {
80 region: Rect::new(0.0, 0.0, 1.0, 1.0),
81 page: Default::default(),
82 tint: Rgba::white(),
83 transform: Default::default(),
84 size: Default::default(),
85 pivot: Default::default(),
86 }
87 }
88}
89
90impl ParticleInstance {
91 pub fn region_page(mut self, region: Rect<f32, f32>, page: f32) -> Self {
92 self.region = region;
93 self.page = page;
94 self
95 }
96
97 pub fn tint(mut self, value: Rgba<f32>) -> Self {
98 self.tint = value;
99 self
100 }
101
102 pub fn transform(mut self, value: Transform<f32, f32, f32>) -> Self {
103 self.transform = value;
104 self
105 }
106
107 pub fn position(mut self, value: Vec2<f32>) -> Self {
108 self.transform.position = value.into();
109 self
110 }
111
112 pub fn orientation(mut self, value: Quaternion<f32>) -> Self {
113 self.transform.orientation = value;
114 self
115 }
116
117 pub fn rotation(mut self, angle_radians: f32) -> Self {
118 self.transform.orientation = Quaternion::rotation_z(angle_radians);
119 self
120 }
121
122 pub fn scale(mut self, value: Vec2<f32>) -> Self {
123 self.transform.scale = Vec3::new(value.x, value.y, 1.0);
124 self
125 }
126
127 pub fn size(mut self, value: Vec2<f32>) -> Self {
128 self.size = value;
129 self
130 }
131
132 pub fn pivot(mut self, value: Vec2<f32>) -> Self {
133 self.pivot = value;
134 self
135 }
136}
137
138pub struct ParticleDraw<'a, I: IntoIterator<Item = ParticleInstance>> {
139 emitter: &'a ParticleEmitter,
140 instances: RefCell<Option<I>>,
141}
142
143impl<I: IntoIterator<Item = ParticleInstance>> Drawable for ParticleDraw<'_, I> {
144 fn draw(&self, context: &mut DrawContext, graphics: &mut dyn GraphicsTarget<Vertex>) {
145 let instances = match self.instances.borrow_mut().take() {
146 Some(instances) => instances,
147 None => return,
148 };
149 let batch = GraphicsBatch {
150 shader: context.shader(self.emitter.shader.as_ref()),
151 uniforms: self
152 .emitter
153 .uniforms
154 .iter()
155 .map(|(k, v)| (k.clone(), v.to_owned()))
156 .chain(std::iter::once((
157 "u_projection_view".into(),
158 GlowUniformValue::M4(
159 if self.emitter.screen_space {
160 graphics.state().main_camera.screen_matrix()
161 } else {
162 graphics.state().main_camera.world_matrix()
163 }
164 .into_col_array(),
165 ),
166 )))
167 .chain(
168 self.emitter
169 .textures
170 .iter()
171 .enumerate()
172 .map(|(index, texture)| {
173 (texture.sampler.clone(), GlowUniformValue::I1(index as _))
174 }),
175 )
176 .collect(),
177 textures: self
178 .emitter
179 .textures
180 .iter()
181 .filter_map(|texture| {
182 Some((context.texture(Some(&texture.texture))?, texture.filtering))
183 })
184 .collect(),
185 blending: self
186 .emitter
187 .blending
188 .unwrap_or_else(|| context.top_blending()),
189 scissor: None,
190 wireframe: context.wireframe,
191 };
192 graphics.state_mut().stream.batch_optimized(batch);
193 let parent = context.top_transform();
194 for instance in instances {
195 let transform = parent * transform_to_matrix(instance.transform);
196 let offset = instance.size * instance.pivot;
197 let color = instance.tint.into_array();
198 graphics.state_mut().stream.transformed(
199 |stream| {
200 stream.quad([
201 Vertex {
202 position: [0.0, 0.0],
203 uv: [instance.region.x, instance.region.y, instance.page],
204 color,
205 },
206 Vertex {
207 position: [instance.size.x, 0.0],
208 uv: [
209 instance.region.x + instance.region.w,
210 instance.region.y,
211 instance.page,
212 ],
213 color,
214 },
215 Vertex {
216 position: [instance.size.x, instance.size.y],
217 uv: [
218 instance.region.x + instance.region.w,
219 instance.region.y + instance.region.h,
220 instance.page,
221 ],
222 color,
223 },
224 Vertex {
225 position: [0.0, instance.size.y],
226 uv: [
227 instance.region.x,
228 instance.region.y + instance.region.h,
229 instance.page,
230 ],
231 color,
232 },
233 ]);
234 },
235 |vertex| {
236 let point = transform.mul_point(Vec2::from(vertex.position) - offset);
237 vertex.position[0] = point.x;
238 vertex.position[1] = point.y;
239 },
240 );
241 }
242 }
243}
244
245pub trait ParticleSystemProcessor<D, C> {
246 fn process(config: &C, data: D) -> Option<D>;
247 fn emit(config: &C, data: &D) -> Option<ParticleInstance>;
248}
249
250pub struct ParticleSystem<P: ParticleSystemProcessor<D, C>, D, C> {
251 pub config: C,
252 source: Vec<D>,
253 target: Vec<D>,
254 _phantom: PhantomData<fn() -> P>,
255}
256
257impl<P: ParticleSystemProcessor<D, C>, D, C> ParticleSystem<P, D, C> {
258 pub fn new(config: C, capacity: usize) -> Self {
259 Self {
260 config,
261 source: Vec::with_capacity(capacity),
262 target: Vec::with_capacity(capacity),
263 _phantom: Default::default(),
264 }
265 }
266
267 pub fn len(&self) -> usize {
268 self.source.len()
269 }
270
271 pub fn is_empty(&self) -> bool {
272 self.source.is_empty()
273 }
274
275 pub fn push(&mut self, data: D) {
276 if self.source.len() < self.source.capacity() {
277 self.source.push(data);
278 }
279 }
280
281 pub fn extend(&mut self, iter: impl IntoIterator<Item = D>) {
282 self.source.extend(iter);
283 }
284
285 pub fn clear(&mut self) {
286 self.source.clear();
287 self.target.clear();
288 }
289
290 pub fn process(&mut self) {
291 self.target.clear();
292 self.target.reserve(self.source.len());
293 for item in self.source.drain(..) {
294 if let Some(item) = P::process(&self.config, item) {
295 self.target.push(item);
296 }
297 }
298 std::mem::swap(&mut self.source, &mut self.target);
299 }
300
301 pub fn emit(&self) -> impl Iterator<Item = ParticleInstance> + '_ {
302 self.source
303 .iter()
304 .filter_map(|item| P::emit(&self.config, item))
305 }
306}
307
308impl<P: ParticleSystemProcessor<D, C>, D: std::fmt::Debug, C: std::fmt::Debug> std::fmt::Debug
309 for ParticleSystem<P, D, C>
310{
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 f.debug_struct("ParticleSystem")
313 .field("config", &self.config)
314 .field("data", &self.source)
315 .finish_non_exhaustive()
316 }
317}