Unravel Engine C++ Reference
Loading...
Searching...
No Matches
particle_system.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2011-2025 Branimir Karadzic. All rights reserved.
3 * License: https://github.com/bkaradzic/bgfx/blob/master/LICENSE
4 */
5
6#include <bgfx/bgfx.h>
7#include <bgfx/embedded_shader.h>
8
9#include "particle_system.h"
11
12#include <bx/easing.h>
13#include <bx/handlealloc.h>
14#include <math/math.h>
15#include <vector>
16#include <algorithm>
18
20{
21 float x;
22 float y;
23 float z;
24 uint32_t abgr;
25 float u;
26 float v;
27 float blend;
28 float angle;
29
30 static void init()
31 {
32 ms_layout.begin()
33 .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
34 .add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Uint8, true)
35 .add(bgfx::Attrib::TexCoord0, 4, bgfx::AttribType::Float)
36 .end();
37 }
38
39 static bgfx::VertexLayout ms_layout;
40};
41
42bgfx::VertexLayout PosColorTexCoord0Vertex::ms_layout;
43
44// New instanced particle vertex structure (just position and UV)
46{
47 float x;
48 float y;
49 float z;
50 float u;
51 float v;
52
53 static void init()
54 {
55 ms_layout.begin()
56 .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
57 .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float)
58 .end();
59 }
60
61 static bgfx::VertexLayout ms_layout;
62};
63
64bgfx::VertexLayout ParticleVertex::ms_layout;
65
66// Static quad geometry for instanced particles
67static ParticleVertex s_quadVertices[4] = {
68 {-0.5f, -0.5f, 0.0f, 0.0f, 1.0f}, // Bottom-left
69 { 0.5f, -0.5f, 0.0f, 1.0f, 1.0f}, // Bottom-right
70 { 0.5f, 0.5f, 0.0f, 1.0f, 0.0f}, // Top-right
71 {-0.5f, 0.5f, 0.0f, 0.0f, 0.0f} // Top-left
72};
73
74static const uint16_t s_quadIndices[6] = {
75 0, 1, 2, 2, 3, 0
76};
77
79{
80 // Initialize simulation method and transforms
81 m_simulationSpace = SimulationSpace::World; // Default to world simulation
82 m_transform = math::transform(); // Identity transform
83 m_prevTransform = math::transform(); // Identity transform
84
85 // Initialize emission shape scale
86 m_emissionShapeScale = math::vec3(1.0f, 1.0f, 1.0f); // Default: no scaling
87
88 // Initialize velocity gradient with default 2-point gradient (start -> end)
90 m_velocityGradient.add_point(frange_t(0.0f, 1.0f), 0.0f); // Start velocity range
91 m_velocityGradient.add_point(frange_t(2.0f, 3.0f), 1.0f); // End velocity range
92
93 // Initialize color gradient with default 5-point gradient (transparent -> white -> white -> white -> transparent)
95 m_colorGradient.add_point(math::color(0x00ffffff), 0.0f); // Transparent white at start
96 m_colorGradient.add_point(math::color(0xffffffff), 0.25f); // Opaque white
97 m_colorGradient.add_point(math::color(0xffffffff), 0.5f); // Opaque white
98 m_colorGradient.add_point(math::color(0xffffffff), 0.75f); // Opaque white
99 m_colorGradient.add_point(math::color(0x00ffffff), 1.0f); // Transparent white at end
100
101 // Initialize blend gradient with default 2-point gradient (start -> end)
103 m_blendGradient.add_point(frange_t(0.8f, 1.0f), 0.0f); // Start blend range
104 m_blendGradient.add_point(frange_t(0.0f, 0.2f), 1.0f); // End blend range
105
106 // Initialize scale gradient with default 2-point gradient (start -> end)
108 m_scaleGradient.add_point(frange_t(0.1f, 0.2f), 0.0f); // Start scale range
109 m_scaleGradient.add_point(frange_t(0.3f, 0.4f), 1.0f); // End scale range
110
111 m_lifetime = 1.0f;
112
113 m_gravityScale = 0.0f;
114 m_particlesPerSecond = 50.0f; // Default: 50 particles per second
115 m_temporalMotion = 1.0f; // Default: full temporal interpolation
116 m_velocityDamping = 0.0f; // Default: no damping
117 m_forceOverLifetime = math::vec3(0.0f, 0.0f, 0.0f); // Default: no additional force
118 m_sizeBySpeedRange = frange_t(1.0f, 1.0f); // Default: no size change
119 m_sizeBySpeedVelocityRange = frange_t(0.0f, 10.0f); // Default velocity range
121 m_colorBySpeedGradient.add_point(math::color(0xffffffff), 0.0f); // Slow speed: white
122 m_colorBySpeedGradient.add_point(math::color(0xffffffff), 1.0f); // Fast speed: white (no color change by default)
123 m_colorBySpeedVelocityRange = frange_t(0.0f, 10.0f); // Default velocity range
124
125 // Initialize lifetime by emitter speed gradient with default 2-point gradient (no change by default)
127 m_lifetimeByEmitterSpeedGradient.add_point(1.0f, 0.0f); // Slow emitter: no lifetime change
128 m_lifetimeByEmitterSpeedGradient.add_point(1.0f, 1.0f); // Fast emitter: no lifetime change (default)
129 m_lifetimeByEmitterSpeedRange = frange_t(0.0f, 10.0f); // Default emitter speed range
130
131 m_emissionLifetime = 2.0f; // Default: 2 second emission cycle
132 m_blendMultiplier = 1.0f; // Default: no blend modification
133
134 // Initialize playback states
135 m_playing = true; // Default: playing
136 m_paused = false; // Default: not paused
137 m_loop = true; // Default: loop continuously
138
139 m_easePos = bx::Easing::Linear; // Only position easing remains
140 // Generate LUTs for all gradients to optimize sampling performance
141 m_velocityGradient.generate_lut(256);
143 m_blendGradient.generate_lut(256);
144 m_scaleGradient.generate_lut(256);
147}
148
149namespace ps
150{
152{
153 math::vec3 start;
154 math::vec3 end[2];
159
160 // Cached computed properties (updated during update, used during render)
161 math::color color; // Final color with all effects applied
162 math::vec3 position;
163 float scale; // Final scale with all effects applied
164 float blend; // Final blend value
165 float cached_speed; // Cached particle speed to avoid redundant calculations
166
167 float life;
168 float lifeSpan;
169};
170
172{
173 float dist;
174 uint32_t idx;
175};
176
178{
179 void create(EmitterShape::Enum _shape, EmitterDirection::Enum _direction, uint32_t _maxParticles);
180 void destroy();
181
182 void reset()
183 {
184 dt_ = 0.0f;
185
186 num_particles_ = 0;
188 aabb_ = math::bbox(math::vec3(-1.0f), math::vec3(1.0f));
189 first_update_ = true;
190
191 rng_.reset();
192 }
193
194 // Helper function to calculate approximate particle speed
195 float calculateParticleSpeed(const Particle& particle, float ttPos) const
196 {
197 // Use trajectory-based approximation for better performance
198 const math::vec3 initialVelocity = particle.end[0] - particle.start;
199 const math::vec3 finalVelocity = particle.end[1] - particle.end[0];
200
201 // Interpolate velocity based on position in trajectory
202 const math::vec3 currentVelocity = math::mix(initialVelocity, finalVelocity, ttPos);
203
204 // Scale by lifetime to get velocity per second
205 const math::vec3 velocityPerSecond = currentVelocity * (1.0f / particle.lifeSpan);
206
207 return math::length(velocityPerSecond);
208 }
209
210 // Update particle properties that were previously calculated in render
212 float avgSystemScale,
213 bx::EaseFn easePos,
214 bool hasColorBySpeed,
215 bool hasSizeBySpeed,
216 const math::mat4& effectiveTransform)
217 {
218 const float ttPos = easePos(particle.life);
219
220 // Calculate particle speed for speed-based effects and cache it
221 const float particleSpeed = calculateParticleSpeed(particle, ttPos);
222 particle.cached_speed = particleSpeed;
223 // Sample color from gradient based on particle life
224 math::color sampledColor = uniforms_.m_colorGradient.sample(particle.life);
225
226 // Apply color by speed if enabled
227 if(hasColorBySpeed)
228 {
229 const float speedFactor =
230 bx::clamp((particleSpeed - uniforms_.m_colorBySpeedVelocityRange.min) /
232 0.0f,
233 1.0f);
234
235 const math::color speedColor = uniforms_.m_colorBySpeedGradient.sample(speedFactor);
236
237 // Blend the speed color with the original color (multiply blend)
238 sampledColor.value *= speedColor.value;
239 }
240
241 // Cache final color
242 particle.color = sampledColor;
243
244 // Calculate blend and apply global blend multiplier
245 particle.blend = math::mix(particle.blend_start, particle.blend_end, particle.life) * uniforms_.m_blendMultiplier;
246
247 // Calculate scale with system scaling
248 float scale = math::mix(particle.scale_start, particle.scale_end, particle.life) * avgSystemScale;
249
250 // Apply size by speed if enabled
251 if(hasSizeBySpeed)
252 {
253 const float speedFactor =
254 bx::clamp((particleSpeed - uniforms_.m_sizeBySpeedVelocityRange.min) /
256 0.0f,
257 1.0f);
258
259 const float sizeMultiplier =
260 math::mix(uniforms_.m_sizeBySpeedRange.min, uniforms_.m_sizeBySpeedRange.max, speedFactor);
261 scale *= sizeMultiplier;
262 }
263
264 // Cache final scale
265 particle.scale = scale;
266
267 // Calculate position - apply transform for local simulation
268 const math::vec3 p0 = math::mix(particle.start, particle.end[0], ttPos);
269 const math::vec3 p1 = math::mix(particle.end[0], particle.end[1], ttPos);
270 const math::vec3 localPos = math::mix(p0, p1, ttPos);
271
273 {
274 // Transform local space position to world space
275 const math::vec4 worldPos4 = effectiveTransform * math::vec4(localPos, 1.0f);
276 particle.position = math::vec3(worldPos4.x, worldPos4.y, worldPos4.z);
277 }
278 else
279 {
280 // Already in world space
281 particle.position = localPos;
282 }
283 }
284
285 void update(EmitterUniforms* _uniforms, float _dt)
286 {
287 auto& uniforms_ = *_uniforms;
288
289
290 if(first_update_)
291 {
292 uniforms_.m_prevTransform = uniforms_.m_transform;
293 }
294
295 bool was_playing = playing_;
296 bool was_loop = loop_;
297 playing_ = uniforms_.m_playing;
298 loop_ = uniforms_.m_loop;
299
300 if(was_playing != playing_ || was_loop != loop_)
301 {
303 }
304
305
306 if(uniforms_.m_paused)
307 {
308 // If paused, set delta time to 0 (particles don't advance but remain visible)
309 _dt = 0.0f;
310 }
311
312 if(!uniforms_.m_loop && total_particles_spawned_ >= max_particles_)
313 {
314 uniforms_.m_playing = false;
316 }
317
318 // Get effective transform properties based on simulation method
319 math::vec3 effectivePosition, effectiveScale, effectiveEmissionShapeScale;
320 math::mat4 effectiveTransform;
321 getEffectiveTransform(uniforms_, effectivePosition, effectiveScale, effectiveEmissionShapeScale, effectiveTransform);
322
323 math::bbox aabb;
324 aabb.reset();
325
326 aabb.add_point(effectivePosition - math::vec3(0.5f));
327 aabb.add_point(effectivePosition + math::vec3(0.5f));
328
329 uint32_t num = num_particles_;
330
331 // Pre-calculate per-frame constants to avoid recalculating per particle
332 const float avgSystemScale = (effectiveScale.x + effectiveScale.y + effectiveScale.z) / 3.0f;
333 const bx::EaseFn easePos = bx::getEaseFunc(uniforms_.m_easePos);
334
335 // Pre-calculate speed-based effect conditions
336 const bool hasColorBySpeed =
337 (uniforms_.m_colorBySpeedVelocityRange.max > uniforms_.m_colorBySpeedVelocityRange.min);
338 const bool hasSizeBySpeed =
339 (uniforms_.m_sizeBySpeedVelocityRange.max > uniforms_.m_sizeBySpeedVelocityRange.min &&
340 uniforms_.m_sizeBySpeedRange.min != uniforms_.m_sizeBySpeedRange.max);
341
342
343
344 for(uint32_t ii = 0; ii < num; ++ii)
345 {
346 Particle& particle = particles_[ii];
347 particle.life += _dt * 1.0f / particle.lifeSpan;
348
349 if(particle.life > 1.0f)
350 {
351 if(ii != num - 1)
352 {
353 bx::memCopy(&particle, &particles_[num - 1], sizeof(Particle));
354 --ii;
355 }
356
357 --num;
358 continue; // Skip processing for dead particles
359 }
360
361 // Update cached properties for living particles
362 updateParticleProperties(uniforms_, particle, avgSystemScale, easePos, hasColorBySpeed, hasSizeBySpeed, effectiveTransform);
363
364 // Add particle position with some padding for scale
365 math::vec3 padding(particle.scale * 0.5f);
366 aabb.add_point(particle.position - padding);
367 aabb.add_point(particle.position + padding);
368 }
369
370 num_particles_ = num;
371
372 if(0.0f < uniforms_.m_emissionLifetime && uniforms_.m_playing)
373 {
374 // For looping emitters, always spawn
375 // For non-looping emitters, only spawn if initial emission hasn't completed
376 bool initial_emission_complete = total_particles_spawned_ >= max_particles_;
377 if(uniforms_.m_loop || !initial_emission_complete)
378 {
379 spawn(uniforms_, aabb,_dt);
380 }
381 }
382
383 // Safety check: ensure num_particles_ never exceeds max_particles_
384 BX_ASSERT(num_particles_ <= max_particles_, "Particle count exceeded maximum! num_particles_=%d, max_particles_=%d", num_particles_, max_particles_);
386
387
388 if(first_update_)
389 {
390 first_update_ = false;
391 }
392
393
394 aabb_ = aabb;
395
396 }
397
398 // Helper function to get effective transform properties (now unified for both simulation methods)
400 math::vec3& outPosition,
401 math::vec3& outScale,
402 math::vec3& outEmissionShapeScale,
403 math::mat4& outTransformMatrix) const
404 {
405 // Extract transform components directly (efficient for both simulation methods)
406 outPosition = uniforms_.m_transform.get_position();
407 outScale = uniforms_.m_transform.get_scale();
408 outEmissionShapeScale = uniforms_.m_emissionShapeScale * outScale; // Apply transform scale to emission shape
409 outTransformMatrix = uniforms_.m_transform; // Implicit conversion to mat4
410 }
411
412 void spawn(EmitterUniforms& uniforms_, math::bbox& aabb, float _dt)
413 {
414 // Skip emission if rate is zero or negative
415 if(uniforms_.m_particlesPerSecond <= 0.0f)
416 {
417 return;
418 }
419
420 // Calculate time per particle and accumulate time
421 const float timePerParticle = 1.0f / uniforms_.m_particlesPerSecond;
422 dt_ += _dt;
423
424 // Calculate how many particles to emit this frame
425 const uint32_t numParticlesToEmit = uint32_t(dt_ / timePerParticle);
426 dt_ -= numParticlesToEmit * timePerParticle; // Remove emitted time from accumulator
427
428 // Don't emit more particles than we have space for
429 const uint32_t maxEmittable = max_particles_ - num_particles_;
430 const uint32_t actualEmitCount = math::min(numParticlesToEmit, maxEmittable);
431
432 if(actualEmitCount == 0)
433 {
434 return;
435 }
436
437 // Get effective transform properties based on simulation method
438 math::vec3 effectivePosition, effectiveScale, effectiveEmissionShapeScale;
439 math::mat4 effectiveTransform;
440 getEffectiveTransform(uniforms_, effectivePosition, effectiveScale, effectiveEmissionShapeScale, effectiveTransform);
441
442 // Pre-calculate constants for new particle property calculation
443 const float avgSystemScale = (effectiveScale.x + effectiveScale.y + effectiveScale.z) / 3.0f;
444 const bx::EaseFn easePos = bx::getEaseFunc(uniforms_.m_easePos);
445 const bool hasColorBySpeed =
447 const bool hasSizeBySpeed =
449 uniforms_.m_sizeBySpeedRange.min != uniforms_.m_sizeBySpeedRange.max);
450
451 // Pre-calculate emitter speed for lifetime by emitter speed effect
452 const bool hasLifetimeByEmitterSpeed =
454 float emitterSpeed = 0.0f;
455 float lifetimeMultiplier = 1.0f;
456 if(hasLifetimeByEmitterSpeed && _dt > 0.0f)
457 {
458 // Calculate motion delta for emitter speed (now unified)
459 const math::vec3 currentPos = uniforms_.m_transform.get_position();
460 const math::vec3 prevPos = uniforms_.m_prevTransform.get_position();
461
462 const math::vec3 motionDelta = currentPos - prevPos;
463 emitterSpeed = math::length(motionDelta) / _dt; // Speed in units per second
464
465 // Calculate speed factor and sample gradient
466 const float speedFactor =
467 bx::clamp((emitterSpeed - uniforms_.m_lifetimeByEmitterSpeedRange.min) /
469 0.0f, 1.0f);
470
471 lifetimeMultiplier = uniforms_.m_lifetimeByEmitterSpeedGradient.sample(speedFactor);
472 }
473
474 // Extract rotation matrix from effective transform
475 const math::mat3 rotationMatrix = math::mat3(effectiveTransform);
476
477 // Pre-calculate common transformation components (optimization)
478 const math::vec3 systemScale = effectiveScale;
479 const math::vec3 emissionShapeScale = effectiveEmissionShapeScale;
480 const float lifeSpan = uniforms_.m_lifetime;
481 const float lifeSpanSquared = lifeSpan * lifeSpan;
482 const math::vec3 gravityVector = math::vec3(0.0f, -9.81f * uniforms_.m_gravityScale * lifeSpanSquared * systemScale.y, 0.0f);
483 const math::vec3 forceOverLifetimeVector = uniforms_.m_forceOverLifetime * lifeSpanSquared * systemScale;
484 const float velocityDampingFactor = (1.0f - uniforms_.m_velocityDamping);
485
486 // Calculate motion delta for temporal emission gap handling
487 const math::vec3 currentPos = effectivePosition;
488 const math::vec3 prevPos = uniforms_.m_prevTransform.get_position();
489
490 const math::vec3 up = math::vec3(0.0f, 1.0f, 0.0f);
491
492 // Emit particles with temporal interpolation
493 for(uint32_t ii = 0; ii < actualEmitCount; ++ii)
494 {
495 // Calculate emission phase for temporal motion interpolation
496 // Distribute particles evenly across the frame, scaled by temporal motion factor
497 const float baseEmissionPhase = float(ii) / float(actualEmitCount);
498 const float emissionPhase = baseEmissionPhase * uniforms_.m_temporalMotion;
499
500 // Find next available particle slot
501 Particle* particle = &particles_[num_particles_];
504
505
506 math::vec3 pos;
507 switch(shape_)
508 {
509 default:
511 pos = math::ballRand(1.0f);
512 break;
513
515 {
516 math::vec3 spherePos = math::ballRand(1.0f);
517 if(math::dot(spherePos, up) < 0.0f)
518 spherePos = -spherePos;
519 pos = spherePos;
520 }
521 break;
522
524 {
525 math::vec2 circlePos = math::diskRand(1.0f);
526 pos = math::vec3(circlePos.x, 0.0f, circlePos.y);
527 }
528 break;
529
531 pos = math::vec3(math::linearRand(-1.0f, 1.0f),
532 math::linearRand(-1.0f, 1.0f),
533 math::linearRand(-1.0f, 1.0f));
534 break;
535
537 pos = math::vec3(math::linearRand(-1.0f, 1.0f), 0.0f, math::linearRand(-1.0f, 1.0f));
538 break;
539 }
540
541 // Apply emission shape scale (use pre-calculated value)
542 pos = pos * emissionShapeScale;
543
544 math::vec3 dir;
545 switch(direction_)
546 {
547 default:
549 dir = up;
550 break;
551
553 dir = math::normalize(pos);
554 break;
555 }
556
557 // Use pre-calculated system scale for better performance
558 const math::vec3 scaledPos = systemScale * pos;
559 const math::vec3 start = scaledPos;
560
561 // Sample velocity range from gradient at particle end (t=1)
562 const frange_t endVelocityRange = uniforms_.m_velocityGradient.sample(1.0f);
563 const float endVelocity = math::mix(endVelocityRange.min, endVelocityRange.max, bx::frnd(&rng_));
564 const math::vec3 scaledDir = systemScale * dir;
565 const math::vec3 tmp1 = scaledDir * endVelocity;
566 const math::vec3 end = tmp1 + start;
567
568 particle->life = 0.0f; // Always start at 0 for new particles
569 particle->lifeSpan = lifeSpan * lifetimeMultiplier; // Apply emitter speed-based lifetime modifier
570
571 // Calculate interpolated emitter position for temporal emission gap handling
572 math::vec3 interpolatedEmitterPos = math::mix(prevPos, currentPos, emissionPhase);
573
575 {
576 // For local simulation, store particles in local space (no transform applied)
577 // The transform will be applied during rendering
578 particle->start = start; // Local space position
579 particle->end[0] = end; // Local space end position
580 }
581 else
582 {
583 // For world simulation, apply rotation and translation as before
584 particle->start = rotationMatrix * start + interpolatedEmitterPos;
585 particle->end[0] = rotationMatrix * end + interpolatedEmitterPos;
586 }
587
588 // Apply damping to the velocity (use pre-calculated damping factor)
589 if(uniforms_.m_velocityDamping > 0.0f)
590 {
591 const math::vec3 velocity = particle->end[0] - particle->start;
592 const math::vec3 dampedVelocity = velocity * velocityDampingFactor;
593 particle->end[0] = particle->start + dampedVelocity;
594 }
595
596 // Use pre-calculated force vectors
597 const math::vec3 totalForce = gravityVector + forceOverLifetimeVector;
598 particle->end[1] = particle->end[0] + totalForce;
599
600 // Color will be sampled from gradient during rendering - no need to copy here
601
602 // Sample blend range from gradient at particle spawn (t=0) and end (t=1)
603 const frange_t startBlendRange = uniforms_.m_blendGradient.sample(0.0f);
604 const frange_t endBlendRange = uniforms_.m_blendGradient.sample(1.0f);
605 particle->blend_start = math::mix(startBlendRange.min, startBlendRange.max, bx::frnd(&rng_));
606 particle->blend_end = math::mix(endBlendRange.min, endBlendRange.max, bx::frnd(&rng_));
607
608 // Sample scale range from gradient at particle spawn (t=0) and end (t=1)
609 const frange_t startScaleRange = uniforms_.m_scaleGradient.sample(0.0f);
610 const frange_t endScaleRange = uniforms_.m_scaleGradient.sample(1.0f);
611 particle->scale_start = math::mix(startScaleRange.min, startScaleRange.max, bx::frnd(&rng_));
612 particle->scale_end = math::mix(endScaleRange.min, endScaleRange.max, bx::frnd(&rng_));
613
614 // Calculate properties immediately for new particles
615 updateParticleProperties(uniforms_, *particle, avgSystemScale, easePos, hasColorBySpeed, hasSizeBySpeed, effectiveTransform);
616
617 // Add particle position with some padding for scale
618 math::vec3 padding(particle->scale * 0.5f);
619 aabb.add_point(particle->position - padding);
620 aabb.add_point(particle->position + padding);
621 }
622
623 }
624
627
628 float dt_;
629 bx::RngMwc rng_;
630
632
638
640 bool loop_;
641
642 bool first_update_; // Track if this is the first update to avoid interpolation
643};
644
645static int32_t particleSortFn(const void* _lhs, const void* _rhs)
646{
647 const ParticleSort& lhs = *(const ParticleSort*)_lhs;
648 const ParticleSort& rhs = *(const ParticleSort*)_rhs;
649 return lhs.dist > rhs.dist ? -1 : 1;
650}
651
653{
654 void init(uint16_t _maxEmitters, bx::AllocatorI* _allocator)
655 {
656 m_allocator = _allocator;
657
658 if(nullptr == _allocator)
659 {
660 static bx::DefaultAllocator allocator;
661 m_allocator = &allocator;
662 }
663
664 m_emitterAlloc = bx::createHandleAlloc(m_allocator, _maxEmitters);
665 m_emitter.resize(_maxEmitters);
666
667 // Initialize vertex layouts
670
671 // Create static quad geometry for instanced rendering
672 m_quadVBH = bgfx::createVertexBuffer(
673 bgfx::makeRef(s_quadVertices, sizeof(s_quadVertices)),
675 );
676
677 m_quadIBH = bgfx::createIndexBuffer(
678 bgfx::makeRef(s_quadIndices, sizeof(s_quadIndices))
679 );
680
681 s_texColor = bgfx::createUniform("s_texColor", bgfx::UniformType::Sampler);
682 }
683
684 void shutdown()
685 {
686 bgfx::destroy(s_texColor);
687 bgfx::destroy(m_quadVBH);
688 bgfx::destroy(m_quadIBH);
689
690 bx::destroyHandleAlloc(m_allocator, m_emitterAlloc);
691 // bx::free(m_allocator, m_emitter);
692
693 m_allocator = nullptr;
694 }
695
696
698 uint8_t _view,
699 bgfx::ProgramHandle _program,
700 const float* _mtxView,
701 const math::vec3& _eye,
702 bgfx::TextureHandle _texture)
703 {
704 BX_ASSERT(isValid(_handle), "renderEmitterById handle %d is not valid.", _handle.idx);
705
706 Emitter& emitter = m_emitter[_handle.idx];
707
708 if(0 == emitter.num_particles_ || !bgfx::isValid(_texture))
709 {
710 return; // Skip emitters with no particles or invalid texture
711 }
712
713 // Use instanced rendering - much more efficient!
714 const uint16_t instanceStride = 32; // 32 bytes per instance
715
716 // Get available instance buffer space
717 uint32_t maxInstances = bgfx::getAvailInstanceDataBuffer(emitter.num_particles_, instanceStride);
718
719 if(maxInstances == 0)
720 {
721 BX_WARN(false, "No instance buffer space available.");
722 return;
723 }
724
725 // Allocate instance data buffer
726 bgfx::InstanceDataBuffer idb;
727 bgfx::allocInstanceDataBuffer(&idb, maxInstances, instanceStride);
728
729 // Generate instance data
730 generateInstanceData(emitter, idb, maxInstances, instanceStride, _eye);
731
732 // Set static quad geometry
733 bgfx::setVertexBuffer(0, m_quadVBH);
734 bgfx::setIndexBuffer(m_quadIBH);
735
736 // Set instance data
737 bgfx::setInstanceDataBuffer(&idb);
738
739 // Set render state and texture
740 bgfx::setState(0 | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A |
741 BGFX_STATE_DEPTH_TEST_LESS | BGFX_STATE_CULL_CW |
742 BGFX_STATE_BLEND_NORMAL);
743 bgfx::setTexture(0, s_texColor, _texture);
744
745 // Single draw call for all particles!
746 bgfx::submit(_view, _program);
747 }
748
749 void generateInstanceData(Emitter& emitter, bgfx::InstanceDataBuffer& idb, uint32_t maxInstances, uint16_t instanceStride, const math::vec3& _eye)
750 {
751 uint8_t* data = idb.data;
752
753 // Sort particles for proper alpha blending (simplified - just by distance)
754 for(uint32_t i = 0; i < emitter.num_particles_; ++i)
755 {
756 const Particle& particle = emitter.particles_[i];
757 const math::vec3 tmp0 = _eye - particle.position;
758 emitter.particle_sort_[i].dist = math::dot(tmp0, tmp0);
759 emitter.particle_sort_[i].idx = i;
760 }
761
762 // Sort by distance (back to front for alpha blending)
763 qsort(emitter.particle_sort_, emitter.num_particles_, sizeof(ParticleSort), particleSortFn);
764
765 // Generate instance data for sorted particles
766 uint32_t numToRender = math::min(emitter.num_particles_, maxInstances);
767 for(uint32_t i = 0; i < numToRender; ++i)
768 {
769 const ParticleSort& sort = emitter.particle_sort_[i];
770 const Particle& particle = emitter.particles_[sort.idx];
771
772 // Position + Scale (16 bytes)
773 float* posScale = (float*)data;
774 posScale[0] = particle.position.x;
775 posScale[1] = particle.position.y;
776 posScale[2] = particle.position.z;
777 posScale[3] = particle.scale;
778
779 // Color + Blend + Angle + Padding (16 bytes)
780 float* colorBlend = (float*)&data[16];
781 colorBlend[0] = particle.color.value.r;
782 colorBlend[1] = particle.color.value.g;
783 colorBlend[2] = particle.color.value.b;
784 colorBlend[3] = particle.color.value.a;
785
786 // Speed + Padding (8 bytes)
787 float* rotationBlend = (float*)&data[32];
788 rotationBlend[0] = 0.0f; // angle (could add rotation later)
789 rotationBlend[1] = particle.blend;
790 rotationBlend[2] = 0.0f;
791 rotationBlend[3] = 0.0f; // padding
792
793 data += instanceStride;
794 }
795 }
796
797 // Batch rendering support structures and functions
799 {
800 float dist; // Squared distance from camera for sorting
801 uint32_t emitter_idx; // Which emitter this particle belongs to
802 uint32_t particle_idx; // Index within that emitter's particle array
803 };
804
805 static int32_t batchedParticleSortFn(const void* _lhs, const void* _rhs)
806 {
807 const BatchedParticle& lhs = *(const BatchedParticle*)_lhs;
808 const BatchedParticle& rhs = *(const BatchedParticle*)_rhs;
809 // Sort by squared distance (back to front for proper alpha blending)
810 return lhs.dist > rhs.dist ? -1 : 1;
811 }
812
813 uint32_t renderEmitterBatch(const EmitterHandle* _handles, uint32_t _count,
814 uint8_t _view, bgfx::ProgramHandle _program,
815 const float* _mtxView, const math::vec3& _eye,
816 bgfx::TextureHandle _texture)
817 {
818 if(_count == 0 || !bgfx::isValid(_texture))
819 {
820 return 0; // Nothing to render
821 }
822
823 APP_SCOPE_PERF("Rendering/Particle Pass/Render Batched Emitters");
824
825
826 // Count total particles across all emitters
827 uint32_t totalParticles = 0;
828 for(uint32_t i = 0; i < _count; ++i)
829 {
830 if(!isValid(_handles[i]))
831 {
832 continue;
833 }
834
835 const Emitter& emitter = m_emitter[_handles[i].idx];
836 totalParticles += emitter.num_particles_;
837 }
838
839 if(totalParticles == 0)
840 {
841 return 0; // No particles to render
842 }
843
844 // Use instanced rendering for the batch
845 const uint16_t instanceStride = 48; // 48 bytes per instance
846
847 // Get available instance buffer space
848 uint32_t maxInstances = bgfx::getAvailInstanceDataBuffer(totalParticles, instanceStride);
849
850 if(maxInstances == 0)
851 {
852 BX_WARN(false, "No instance buffer space available for batch rendering.");
853 return 0;
854 }
855
856 // Allocate instance data buffer
857 bgfx::InstanceDataBuffer idb;
858 bgfx::allocInstanceDataBuffer(&idb, maxInstances, instanceStride);
859
860 // Generate batched instance data
861 generateBatchedInstanceData(_handles, _count, idb, maxInstances, instanceStride, _eye);
862
863 // Set static quad geometry
864 bgfx::setVertexBuffer(0, m_quadVBH);
865 bgfx::setIndexBuffer(m_quadIBH);
866
867 // Set instance data
868 bgfx::setInstanceDataBuffer(&idb);
869
870 // Set render state and texture
871 bgfx::setState(0 | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A |
872 BGFX_STATE_DEPTH_TEST_LESS | BGFX_STATE_CULL_CW |
873 BGFX_STATE_BLEND_NORMAL);
874 bgfx::setTexture(0, s_texColor, _texture);
875
876 // Single draw call for all particles from all emitters!
877 bgfx::submit(_view, _program);
878
879 return totalParticles;
880 }
881
882 void generateBatchedInstanceData(const EmitterHandle* _handles, uint32_t _count,
883 bgfx::InstanceDataBuffer& idb, uint32_t maxInstances,
884 uint16_t instanceStride, const math::vec3& _eye)
885 {
886 // First, collect all particles from all emitters and calculate distances
887 static std::vector<BatchedParticle> batchedParticles; // Static to avoid allocations
888 batchedParticles.clear();
889
890 for(uint32_t emitterIdx = 0; emitterIdx < _count; ++emitterIdx)
891 {
892 if(!isValid(_handles[emitterIdx]))
893 {
894 continue;
895 }
896
897 const Emitter& emitter = m_emitter[_handles[emitterIdx].idx];
898
899 batchedParticles.reserve(batchedParticles.size() + emitter.num_particles_);
900 for(uint32_t particleIdx = 0; particleIdx < emitter.num_particles_; ++particleIdx)
901 {
902 const Particle& particle = emitter.particles_[particleIdx];
903 const math::vec3 tmp0 = _eye - particle.position;
904 const float distSquared = math::dot(tmp0, tmp0);
905
906 batchedParticles.emplace_back(BatchedParticle{distSquared, emitterIdx, particleIdx});
907 }
908 }
909
910 // Sort all particles by distance (back to front for alpha blending)
911 std::sort(batchedParticles.begin(), batchedParticles.end(),
912 [](const BatchedParticle& a, const BatchedParticle& b) {
913 return a.dist > b.dist; // Back to front
914 });
915
916 // Generate instance data for sorted particles
917 uint8_t* data = idb.data;
918 uint32_t numToRender = math::min(static_cast<uint32_t>(batchedParticles.size()), maxInstances);
919
920 for(uint32_t i = 0; i < numToRender; ++i)
921 {
922 const BatchedParticle& batchedParticle = batchedParticles[i];
923 const Emitter& emitter = m_emitter[_handles[batchedParticle.emitter_idx].idx];
924 const Particle& particle = emitter.particles_[batchedParticle.particle_idx];
925
926 // Position + Scale (16 bytes)
927 float* posScale = (float*)data;
928 posScale[0] = particle.position.x;
929 posScale[1] = particle.position.y;
930 posScale[2] = particle.position.z;
931 posScale[3] = particle.scale;
932
933 // Color + Blend + Angle + Padding (16 bytes)
934 float* colorBlend = (float*)&data[16];
935 colorBlend[0] = particle.color.value.r;
936 colorBlend[1] = particle.color.value.g;
937 colorBlend[2] = particle.color.value.b;
938 colorBlend[3] = particle.color.value.a;
939
940 // Speed + Padding (8 bytes)
941 float* rotationBlend = (float*)&data[32];
942 rotationBlend[0] = 0.0f; // angle (could add rotation later)
943 rotationBlend[1] = particle.blend;
944 rotationBlend[2] = 0.0f;
945 rotationBlend[3] = 0.0f; // padding
946 data += instanceStride;
947 }
948 }
949
950 EmitterHandle createEmitter(EmitterShape::Enum _shape, EmitterDirection::Enum _direction, uint32_t _maxParticles)
951 {
953
954 if(UINT16_MAX != handle.idx)
955 {
956 m_emitter[handle.idx].create(_shape, _direction, _maxParticles);
957 }
958
959 return handle;
960 }
961
962 void updateEmitter(EmitterHandle _handle, float _dt, EmitterUniforms* _uniforms)
963 {
964 BX_ASSERT(isValid(_handle), "destroyEmitter handle %d is not valid.", _handle.idx);
965
966 Emitter& emitter = m_emitter[_handle.idx];
967
968 if(nullptr == _uniforms)
969 {
970 emitter.reset();
971 }
972 else
973 {
974
975 emitter.update(_uniforms, _dt);
976 }
977 }
978
979 void getAabb(EmitterHandle _handle, math::bbox& _outAabb)
980 {
981 BX_ASSERT(isValid(_handle), "getAabb handle %d is not valid.", _handle.idx);
982 _outAabb = m_emitter[_handle.idx].aabb_;
983 }
985 {
986 BX_ASSERT(isValid(_handle), "getNumParticles handle %d is not valid.", _handle.idx);
987 return m_emitter[_handle.idx].num_particles_;
988 }
989
991 {
992 BX_ASSERT(isValid(_handle), "hasUpdated handle %d is not valid.", _handle.idx);
993 return !m_emitter[_handle.idx].first_update_;
994 }
995
997 {
998 BX_ASSERT(isValid(_handle), "destroyEmitter handle %d is not valid.", _handle.idx);
999
1000 m_emitter[_handle.idx].destroy();
1001 m_emitterAlloc->free(_handle.idx);
1002 }
1003
1004 bx::AllocatorI* m_allocator;
1005
1006 bx::HandleAlloc* m_emitterAlloc;
1007 std::vector<Emitter> m_emitter;
1008
1009 // Static geometry for instanced rendering
1010 bgfx::VertexBufferHandle m_quadVBH;
1011 bgfx::IndexBufferHandle m_quadIBH;
1012
1013 bgfx::UniformHandle s_texColor;
1014};
1015
1016static ParticleSystem s_ctx;
1017
1018void Emitter::create(EmitterShape::Enum _shape, EmitterDirection::Enum _direction, uint32_t _maxParticles)
1019{
1020 reset();
1021
1022 shape_ = _shape;
1023 direction_ = _direction;
1024 max_particles_ = _maxParticles;
1025 particles_ = (Particle*)bx::alloc(s_ctx.m_allocator, max_particles_ * sizeof(Particle));
1026 particle_sort_ = (ParticleSort*)bx::alloc(s_ctx.m_allocator, max_particles_ * sizeof(ParticleSort));
1027}
1028
1030{
1031 bx::free(s_ctx.m_allocator, particles_);
1032 particles_ = nullptr;
1033 bx::free(s_ctx.m_allocator, particle_sort_);
1034 particle_sort_ = nullptr;
1035}
1036
1037} // namespace ps
1038
1039using namespace ps;
1040
1041void psInit(uint16_t _maxEmitters, bx::AllocatorI* _allocator)
1042{
1043 s_ctx.init(_maxEmitters, _allocator);
1044}
1045
1047{
1048 s_ctx.shutdown();
1049}
1050
1051// Sprite functions removed - use bgfx::TextureHandle directly in EmitterUniforms
1052
1054{
1055 return s_ctx.createEmitter(_shape, _direction, _maxParticles);
1056}
1057
1058void psUpdateEmitter(EmitterHandle _handle, float _dt, EmitterUniforms* _uniforms)
1059{
1060 s_ctx.updateEmitter(_handle, _dt, _uniforms);
1061}
1062
1064{
1065 BX_ASSERT(isValid(_handle), "psResetEmitter handle %d is not valid.", _handle.idx);
1066
1067 s_ctx.m_emitter[_handle.idx].reset();
1068}
1069
1070void psGetAabb(EmitterHandle _handle, math::bbox& _outAabb)
1071{
1072 s_ctx.getAabb(_handle, _outAabb);
1073}
1074
1076{
1077 return s_ctx.getNumParticles(_handle);
1078}
1079
1081{
1082 return s_ctx.hasUpdated(_handle);
1083}
1084
1086{
1087 s_ctx.destroyEmitter(_handle);
1088}
1089
1091 uint8_t _view,
1092 bgfx::ProgramHandle _program,
1093 const float* _mtxView,
1094 const math::vec3& _eye,
1095 bgfx::TextureHandle _texture)
1096{
1097 s_ctx.renderEmitterById(_handle, _view, _program, _mtxView, _eye, _texture);
1098}
1099
1100uint32_t psRenderEmitterBatch(const EmitterHandle* _handles,
1101 uint32_t _count,
1102 uint8_t _view,
1103 bgfx::ProgramHandle _program,
1104 const float* _mtxView,
1105 const math::vec3& _eye,
1106 bgfx::TextureHandle _texture)
1107{
1108 return s_ctx.renderEmitterBatch(_handles, _count, _view, _program, _mtxView, _eye, _texture);
1109}
entt::handle b
entt::handle a
auto add_point(const T &element, float progress) -> size_t
Definition gradient.hpp:8
void generate_lut(size_t lut_size=256)
Definition gradient.hpp:222
auto sample(float progress) const -> T
Definition gradient.hpp:115
auto get_position() const noexcept -> const vec3_t &
Get the position component.
auto get_scale() const noexcept -> const vec3_t &
Get the scale component.
range< float > frange_t
float scale
Definition hub.cpp:25
transform_t< float > transform
void psResetEmitter(EmitterHandle _handle)
bool psHasUpdated(EmitterHandle _handle)
EmitterHandle psCreateEmitter(EmitterShape::Enum _shape, EmitterDirection::Enum _direction, uint32_t _maxParticles)
void psShutdown()
uint32_t psRenderEmitterBatch(const EmitterHandle *_handles, uint32_t _count, uint8_t _view, bgfx::ProgramHandle _program, const float *_mtxView, const math::vec3 &_eye, bgfx::TextureHandle _texture)
uint32_t psGetNumParticles(EmitterHandle _handle)
void psGetAabb(EmitterHandle _handle, math::bbox &_outAabb)
void psRenderEmitter(EmitterHandle _handle, uint8_t _view, bgfx::ProgramHandle _program, const float *_mtxView, const math::vec3 &_eye, bgfx::TextureHandle _texture)
void psUpdateEmitter(EmitterHandle _handle, float _dt, EmitterUniforms *_uniforms)
void psDestroyEmitter(EmitterHandle _handle)
void psInit(uint16_t _maxEmitters, bx::AllocatorI *_allocator)
#define APP_SCOPE_PERF(name)
Create a scoped performance timer that only accepts string literals.
Definition profiler.h:160
math::gradient< math::color > m_colorGradient
math::transform m_transform
frange_t m_sizeBySpeedRange
math::transform m_prevTransform
bx::Easing::Enum m_easePos
math::gradient< frange_t > m_blendGradient
frange_t m_lifetimeByEmitterSpeedRange
math::gradient< frange_t > m_scaleGradient
math::gradient< frange_t > m_velocityGradient
math::gradient< float > m_lifetimeByEmitterSpeedGradient
frange_t m_sizeBySpeedVelocityRange
SimulationSpace::Enum m_simulationSpace
math::gradient< math::color > m_colorBySpeedGradient
math::vec3 m_forceOverLifetime
math::vec3 m_emissionShapeScale
frange_t m_colorBySpeedVelocityRange
static void init()
static bgfx::VertexLayout ms_layout
static bgfx::VertexLayout ms_layout
Storage for box vector values and wraps up common functionality.
Definition bbox.h:21
bbox & add_point(const vec3 &point)
Grows the bounding box based on the point passed.
Definition bbox.cpp:924
void reset()
Resets the bounding box values.
Definition bbox.cpp:28
vec4 value
Definition color.h:80
uint32_t max_particles_
float calculateParticleSpeed(const Particle &particle, float ttPos) const
void create(EmitterShape::Enum _shape, EmitterDirection::Enum _direction, uint32_t _maxParticles)
void getEffectiveTransform(const EmitterUniforms &uniforms_, math::vec3 &outPosition, math::vec3 &outScale, math::vec3 &outEmissionShapeScale, math::mat4 &outTransformMatrix) const
void spawn(EmitterUniforms &uniforms_, math::bbox &aabb, float _dt)
void updateParticleProperties(EmitterUniforms &uniforms_, Particle &particle, float avgSystemScale, bx::EaseFn easePos, bool hasColorBySpeed, bool hasSizeBySpeed, const math::mat4 &effectiveTransform)
uint32_t total_particles_spawned_
void update(EmitterUniforms *_uniforms, float _dt)
ParticleSort * particle_sort_
EmitterShape::Enum shape_
uint32_t num_particles_
EmitterDirection::Enum direction_
Particle * particles_
math::vec3 end[2]
math::vec3 position
math::color color
EmitterHandle createEmitter(EmitterShape::Enum _shape, EmitterDirection::Enum _direction, uint32_t _maxParticles)
uint32_t getNumParticles(EmitterHandle _handle)
bool hasUpdated(EmitterHandle _handle)
void getAabb(EmitterHandle _handle, math::bbox &_outAabb)
bgfx::VertexBufferHandle m_quadVBH
void updateEmitter(EmitterHandle _handle, float _dt, EmitterUniforms *_uniforms)
static int32_t batchedParticleSortFn(const void *_lhs, const void *_rhs)
bx::AllocatorI * m_allocator
void generateInstanceData(Emitter &emitter, bgfx::InstanceDataBuffer &idb, uint32_t maxInstances, uint16_t instanceStride, const math::vec3 &_eye)
uint32_t renderEmitterBatch(const EmitterHandle *_handles, uint32_t _count, uint8_t _view, bgfx::ProgramHandle _program, const float *_mtxView, const math::vec3 &_eye, bgfx::TextureHandle _texture)
void init(uint16_t _maxEmitters, bx::AllocatorI *_allocator)
void destroyEmitter(EmitterHandle _handle)
void generateBatchedInstanceData(const EmitterHandle *_handles, uint32_t _count, bgfx::InstanceDataBuffer &idb, uint32_t maxInstances, uint16_t instanceStride, const math::vec3 &_eye)
bx::HandleAlloc * m_emitterAlloc
bgfx::IndexBufferHandle m_quadIBH
void renderEmitterById(EmitterHandle _handle, uint8_t _view, bgfx::ProgramHandle _program, const float *_mtxView, const math::vec3 &_eye, bgfx::TextureHandle _texture)
std::vector< Emitter > m_emitter
bgfx::UniformHandle s_texColor
gfx::uniform_handle handle
Definition uniform.cpp:9
bool isValid(SpriteHandle _handle)
Definition debugdraw.h:34