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"
10#include "bx/bx.h"
12
13#include <bx/easing.h>
14#include <bx/handlealloc.h>
15#include <math/math.h>
16#include <glm/gtc/random.hpp>
17#include <algorithm>
18#include <cstdint>
19#include <vector>
20
23
24#define POOLSTL_STD_SUPPLEMENT 1
25#include <poolstl/poolstl.hpp>
26// New instanced particle vertex structure (just position and UV)
28{
29 float x;
30 float y;
31 float z;
32 float u;
33 float v;
34
35 static void init()
36 {
37 ms_layout.begin()
38 .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
39 .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float)
40 .end();
41 }
42
43 static bgfx::VertexLayout ms_layout;
44};
45
46bgfx::VertexLayout ParticleVertex::ms_layout;
47
48// Static quad geometry for instanced particles
50 {-0.5f, -0.5f, 0.0f, 0.0f, 1.0f}, // Bottom-left
51 { 0.5f, -0.5f, 0.0f, 1.0f, 1.0f}, // Bottom-right
52 { 0.5f, 0.5f, 0.0f, 1.0f, 0.0f}, // Top-right
53 {-0.5f, 0.5f, 0.0f, 0.0f, 0.0f} // Top-left
54};
55
56static const uint16_t s_quadIndices[6] = {
57 0, 1, 2, 2, 3, 0
58};
59
61{
62 // Initialize simulation method and transforms
63 m_simulationSpace = SimulationSpace::World; // Default to world simulation
64 m_transform = math::transform(); // Identity transform
65 m_prevTransform = math::transform(); // Identity transform
66
67 // Initialize emission shape properties
68 m_emissionShapePosition = math::vec3(0.0f, 0.0f, 0.0f); // Default: no offset
69 m_emissionShapeScale = math::vec3(1.0f, 1.0f, 1.0f); // Default: no scaling
70
71 // Initialize spawn location
72 m_spawnLocation = EmitterSpawnLocation::Inside; // Default: spawn inside shape
73
74 // Initialize velocity gradient with default 2-point gradient (start -> end)
76 m_velocityGradient.add_point(frange_t(0.0f, 1.0f), 0.0f); // Start velocity range
77 m_velocityGradient.add_point(frange_t(2.0f, 3.0f), 1.0f); // End velocity range
78
79 // Initialize color gradient with default 5-point gradient (transparent -> white -> white -> white -> transparent)
81 m_colorGradient.add_point(math::color(0x00ffffff), 0.0f); // Transparent white at start
82 m_colorGradient.add_point(math::color(0xffffffff), 0.25f); // Opaque white
83 m_colorGradient.add_point(math::color(0xffffffff), 0.5f); // Opaque white
84 m_colorGradient.add_point(math::color(0xffffffff), 0.75f); // Opaque white
85 m_colorGradient.add_point(math::color(0x00ffffff), 1.0f); // Transparent white at end
86
87 // Initialize scale gradient with default 2-point gradient (start -> end)
89 m_scaleGradient.add_point(frange_t(0.1f, 0.2f), 0.0f); // Start scale range
90 m_scaleGradient.add_point(frange_t(0.3f, 0.4f), 1.0f); // End scale range
91
92 m_initialScale3D = math::vec3(1.0f, 1.0f, 1.0f); // Default: uniform scale (square particles)
93
94 m_lifetime = 1.0f;
95
96 m_gravityScale = 0.0f;
97 m_particlesPerSecond = 50.0f; // Default: 50 particles per second
98 m_temporalMotion = 1.0f; // Default: full temporal interpolation
99 m_velocityDamping = 0.0f; // Default: no damping
100 m_forceOverLifetime = math::vec3(0.0f, 0.0f, 0.0f); // Default: no additional force
101 m_sizeBySpeedRange = frange_t(1.0f, 1.0f); // Default: no size change
102 m_sizeBySpeedVelocityRange = frange_t(0.0f, 10.0f); // Default velocity range
104 m_colorBySpeedGradient.add_point(math::color(0xffffffff), 0.0f); // Slow speed: white
105 m_colorBySpeedGradient.add_point(math::color(0xffffffff), 1.0f); // Fast speed: white (no color change by default)
106 m_colorBySpeedVelocityRange = frange_t(0.0f, 10.0f); // Default velocity range
107
108 // Initialize lifetime by emitter speed gradient with default 2-point gradient (no change by default)
110 m_lifetimeByEmitterSpeedGradient.add_point(1.0f, 0.0f); // Slow emitter: no lifetime change
111 m_lifetimeByEmitterSpeedGradient.add_point(1.0f, 1.0f); // Fast emitter: no lifetime change (default)
112 m_lifetimeByEmitterSpeedRange = frange_t(0.0f, 10.0f); // Default emitter speed range
113
114 m_emissionLifetime = 2.0f; // Default: 2 second emission cycle
115 m_opacity = 1.0f; // Default: no opacity modification
116 m_colorIntensity = 1.0f; // Default: no HDR intensity scaling
117
118 // Initialize playback states
119 m_playing = true; // Default: playing
120 m_paused = false; // Default: not paused
121 m_loop = true; // Default: loop continuously
122 m_startDelay = 0.0f; // Default: no start delay
123
124 m_easePos = bx::Easing::Linear; // Only position easing remains
125
126 // Initialize texture mode
127 m_textureMode = TextureMode::MultiChannel; // Default: standard RGBA texture
128
129 // Initialize texture sheet animation
130 m_texSheetTiles = math::vec2(1.0f, 1.0f); // Default: 1x1 grid (no animation)
131 m_texSheetCycles = 0.0f; // Default: 0 (disabled)
132 m_texSheetRandomize = false; // Default: all particles start at frame 0
133
134 m_renderMode = RenderMode::Billboard; // Default: always face camera
135 m_blendMode = BlendMode::Normal; // Default: alpha blending
136 m_billboardRight = math::vec3(1.0f, 0.0f, 0.0f); // Will be updated from camera
137 m_billboardUp = math::vec3(0.0f, 1.0f, 0.0f); // Will be updated from camera
138
139 m_alignToDirection = false; // Default: particles don't rotate to align with direction
140 m_pivot = math::vec2(0.5f, 0.5f); // Default: center pivot
141
142 // Generate LUTs for all gradients to optimize sampling performance
143 m_velocityGradient.generate_lut(256);
145 m_scaleGradient.generate_lut(256);
148}
149
150namespace ps
151{
153{
154 math::vec3 start;
155 math::vec3 end[2];
158
159 // Cached computed properties (updated during update, used during render)
160 math::color color; // Final color with all effects applied
161 math::vec3 position;
162 float scale; // Final uniform scale with all effects applied
163 float cached_speed; // Cached particle speed to avoid redundant calculations
164
165 float life;
166 float lifeSpan;
167
168 // Texture sheet animation UV data (calculated during update)
169 math::vec2 uv_offset;
170 math::vec2 uv_scale;
171 float texsheet_random_offset; // Random offset [0-1] for texture sheet animation start frame
172
173 // Rotation quaternion (calculated when align_to_direction is enabled)
174 math::quat rotation; // Quaternion representing particle rotation
175};
176
177namespace
178{
179constexpr float k_min_particle_lifespan = 1.0e-4f;
180constexpr float k_emit_dir_zero_len_sq = 1.0e-12f;
181} // namespace
182
184{
185 void create(EmitterShape::Enum _shape, EmitterDirection::Enum _direction, uint32_t _maxParticles);
186 void destroy();
187
188 void reset()
189 {
190 dt_ = 0.0f;
191
192 num_particles_ = 0;
194 aabb_ = math::bbox(math::vec3(-1.0f), math::vec3(1.0f));
195 first_update_ = true;
197
198 rng_.reset();
199
200 // Reset temporal position buffer
202 temporal_time_buffer_.clear();
203
207 billboard_right_ = math::vec3(1.0f, 0.0f, 0.0f);
208 billboard_up_ = math::vec3(0.0f, 1.0f, 0.0f);
209 particle_scale_3d_ = math::vec3(1.0f, 1.0f, 1.0f);
210 pivot_ = math::vec2(0.5f, 0.5f);
211 }
212
213 // Temporal position buffer for smooth emitter speed calculation
214 // This handles physics fixed timestep discontinuities
215 static constexpr size_t TEMPORAL_BUFFER_SIZE = 8;
216
217 void update_temporal_buffer(const math::vec3& position, float dt)
218 {
219 // Add new position and time to the buffer
221 temporal_time_buffer_.push_back(dt);
222
223 // Keep buffer size limited
225 {
228 }
229 }
230
231 float calculate_smoothed_emitter_speed(float max_speed) const
232 {
233 // If buffer isn't filled yet, assume max speed to prevent lifetime inconsistencies
234 // This ensures particles spawned early have shorter lifetimes matching later frames
236 {
237 return max_speed;
238 }
239
240 // Calculate total distance and time over the buffer
241 float total_distance = 0.0f;
242 float total_time = 0.0f;
243
244 for(size_t i = 1; i < temporal_position_buffer_.size(); ++i)
245 {
246 const math::vec3 delta = temporal_position_buffer_[i] - temporal_position_buffer_[i - 1];
247 total_distance += math::length(delta);
248 total_time += temporal_time_buffer_[i];
249 }
250
251 if(total_time > 0.0f)
252 {
253 return total_distance / total_time;
254 }
255
256 return 0.0f;
257 }
258
259 // Helper function to calculate approximate particle speed
260 float calculateParticleSpeed(const Particle& particle, float ttPos) const
261 {
262 // Use trajectory-based approximation for better performance
263 const math::vec3 initialVelocity = particle.end[0] - particle.start;
264 const math::vec3 finalVelocity = particle.end[1] - particle.end[0];
265
266 // Interpolate velocity based on position in trajectory
267 const math::vec3 currentVelocity = math::mix(initialVelocity, finalVelocity, ttPos);
268
269 // Scale by lifetime to get velocity per second
270 const math::vec3 velocityPerSecond = currentVelocity * (1.0f / particle.lifeSpan);
271
272 return math::length(velocityPerSecond);
273 }
274
275 // Calculate AABB bounds for a rotated particle with pivot offset
276 // Returns the min and max offsets from particle position
277 void calculateRotatedAABBBounds(const math::quat& rotation, const math::vec3& half_extents, const math::vec2& pivot,
278 math::vec3& out_min, math::vec3& out_max) const
279 {
280 // Calculate pivot offset in local space
281 // pivot (0,0) = bottom-left, (0.5,0.5) = center, (1,1) = top-right
282 // Convert to offset: (0,0) -> (+0.5,+0.5), (0.5,0.5) -> (0,0), (1,1) -> (-0.5,-0.5)
283 const math::vec2 pivot_offset = pivot - math::vec2(0.5f, 0.5f);
284 const math::vec3 pivot_shift_local = math::vec3(
285 pivot_offset.x * half_extents.x * 2.0f,
286 pivot_offset.y * half_extents.y * 2.0f,
287 0.0f
288 );
289
290 // For an identity quaternion, calculate AABB without rotation
291 const float rot_len_sq = math::dot(rotation, rotation);
292 if(rot_len_sq < 0.01f)
293 {
294 // AABB that contains both the extents and the pivot-shifted center
295 out_min = pivot_shift_local - half_extents;
296 out_max = pivot_shift_local + half_extents;
297 return;
298 }
299
300 // Convert quaternion to rotation matrix
301 const math::mat3 rot_matrix = math::mat3_cast(rotation);
302
303 // Rotate the pivot shift
304 const math::vec3 pivot_shift_rotated = rot_matrix * pivot_shift_local;
305
306 // Calculate the AABB of the rotated box by taking absolute values
307 // of the rotated basis vectors scaled by half-extents
308 math::vec3 aabb_half_extents(0.0f);
309 for(int i = 0; i < 3; ++i)
310 {
311 aabb_half_extents.x += math::abs(rot_matrix[i].x) * half_extents[i];
312 aabb_half_extents.y += math::abs(rot_matrix[i].y) * half_extents[i];
313 aabb_half_extents.z += math::abs(rot_matrix[i].z) * half_extents[i];
314 }
315
316 // Combine rotated extents with rotated pivot offset
317 // The AABB needs to contain the pivot-shifted and rotated particle
318 out_min = pivot_shift_rotated - aabb_half_extents;
319 out_max = pivot_shift_rotated + aabb_half_extents;
320 }
321
322 // Update particle properties that were previously calculated in render
324 float avgSystemScale,
325 bx::EaseFn easePos,
326 bool hasColorBySpeed,
327 bool hasSizeBySpeed,
328 const math::mat4& effectiveTransform)
329 {
330 const float ttPos = easePos(particle.life);
331
332 // Calculate particle speed for speed-based effects and cache it
333 const float particleSpeed = calculateParticleSpeed(particle, ttPos);
334 particle.cached_speed = particleSpeed;
335 // Sample color from gradient based on particle life
336 math::color sampledColor = uniforms_.m_colorGradient.sample(particle.life);
337
338 // Apply color by speed if enabled
339 if(hasColorBySpeed)
340 {
341 const float speedFactor =
342 bx::clamp((particleSpeed - uniforms_.m_colorBySpeedVelocityRange.min) /
344 0.0f,
345 1.0f);
346
347 const math::color speedColor = uniforms_.m_colorBySpeedGradient.sample(speedFactor);
348
349 // Blend the speed color with the original color (multiply blend)
350 sampledColor.value *= speedColor.value;
351 }
352
353 // Cache final color
354 particle.color = sampledColor;
355 particle.color.value.a *= uniforms_.m_opacity;
356 particle.color.value.r *= uniforms_.m_colorIntensity;
357 particle.color.value.g *= uniforms_.m_colorIntensity;
358 particle.color.value.b *= uniforms_.m_colorIntensity;
359
360 // Calculate uniform scale with system scaling
361 float scale = math::mix(particle.scale_start, particle.scale_end, particle.life) * avgSystemScale;
362
363 // Apply size by speed if enabled
364 if(hasSizeBySpeed)
365 {
366 const float speedFactor =
367 bx::clamp((particleSpeed - uniforms_.m_sizeBySpeedVelocityRange.min) /
369 0.0f,
370 1.0f);
371
372 const float sizeMultiplier =
373 math::mix(uniforms_.m_sizeBySpeedRange.min, uniforms_.m_sizeBySpeedRange.max, speedFactor);
374 scale *= sizeMultiplier;
375 }
376
377 // Cache final scale
378 particle.scale = scale;
379
380 // Calculate position - apply transform for local simulation
381 const math::vec3 p0 = math::mix(particle.start, particle.end[0], ttPos);
382 const math::vec3 p1 = math::mix(particle.end[0], particle.end[1], ttPos);
383 const math::vec3 localPos = math::mix(p0, p1, ttPos);
384
386 {
387 // Transform local space position to world space
388 const math::vec4 worldPos4 = effectiveTransform * math::vec4(localPos, 1.0f);
389 particle.position = math::vec3(worldPos4.x, worldPos4.y, worldPos4.z);
390 }
391 else
392 {
393 // Already in world space
394 particle.position = localPos;
395 }
396
397 // Calculate rotation if align to direction is enabled
398 if(uniforms_.m_alignToDirection)
399 {
400 // Calculate particle velocity direction from trajectory
401 // Use the derivative of the bezier curve at current position
402 const math::vec3 velocity0 = particle.end[0] - particle.start;
403 const math::vec3 velocity1 = particle.end[1] - particle.end[0];
404 const math::vec3 current_velocity = math::mix(velocity0, velocity1, ttPos);
405
406 const float velocity_len_sq = math::dot(current_velocity, current_velocity);
407 if(velocity_len_sq > 0.0001f)
408 {
409 // Normalize velocity to get direction
410 const math::vec3 direction = math::normalize(current_velocity);
411
412 // Create rotation that aligns particle's forward (+Z) with velocity direction
413 // Use world up as reference, but fall back to world right if velocity is nearly vertical
414 math::vec3 up_ref(0.0f, 1.0f, 0.0f);
415 if(math::abs(math::dot(direction, up_ref)) > 0.99f)
416 {
417 up_ref = math::vec3(1.0f, 0.0f, 0.0f);
418 }
419
420 particle.rotation = math::look_rotation(direction, up_ref);
421 }
422 else
423 {
424 // No meaningful velocity - use emitter rotation
425 particle.rotation = uniforms_.m_transform.get_rotation();
426 }
427 }
428 else
429 {
430 // No rotation - use identity
431 particle.rotation = math::identity<math::quat>();
432 }
433
434 // Calculate texture sheet animation UV offset and scale
435 if(uniforms_.m_texSheetCycles > 0.0f && uniforms_.m_texSheetTiles.x > 0 && uniforms_.m_texSheetTiles.y > 0)
436 {
437 const float uvScaleX = 1.0f / float(uniforms_.m_texSheetTiles.x);
438 const float uvScaleY = 1.0f / float(uniforms_.m_texSheetTiles.y);
439 const uint32_t totalFrames = uniforms_.m_texSheetTiles.x * uniforms_.m_texSheetTiles.y;
440
441 // Calculate current frame based on particle life and cycles
442 float animProgress = particle.life * uniforms_.m_texSheetCycles;
443
444 // Add random offset if randomization is enabled
445 if(uniforms_.m_texSheetRandomize)
446 {
447 animProgress += particle.texsheet_random_offset;
448 }
449
450 animProgress = math::fmod(animProgress, 1.0f);
451 const uint32_t currentFrame = uint32_t(animProgress * float(totalFrames)) % totalFrames;
452
453 // Calculate tile position in grid (row-major order)
454 const uint32_t tileX = currentFrame % uint32_t(uniforms_.m_texSheetTiles.x);
455 const uint32_t tileY = currentFrame / uint32_t(uniforms_.m_texSheetTiles.x);
456
457 // Store UV offset and scale
458 particle.uv_offset = math::vec2(float(tileX) * uvScaleX, float(tileY) * uvScaleY);
459 particle.uv_scale = math::vec2(uvScaleX, uvScaleY);
460 }
461 else
462 {
463 // No animation - use full texture
464 particle.uv_offset = math::vec2(0.0f, 0.0f);
465 particle.uv_scale = math::vec2(1.0f, 1.0f);
466 }
467 }
468
469 void update(EmitterUniforms* _uniforms, float _dt)
470 {
471 auto& uniforms_ = *_uniforms;
472
473 // Cache texture mode, render mode, 3D scale, and pivot for rendering
474 texture_mode_ = uniforms_.m_textureMode;
475 render_mode_ = uniforms_.m_renderMode;
476 blend_mode_ = uniforms_.m_blendMode;
477 particle_scale_3d_ = uniforms_.m_initialScale3D;
478 pivot_ = uniforms_.m_pivot;
479
480 if(first_update_)
481 {
482 uniforms_.m_prevTransform = uniforms_.m_transform;
483 }
484
485 bool was_playing = playing_;
486 bool was_loop = loop_;
487 playing_ = uniforms_.m_playing;
488 loop_ = uniforms_.m_loop;
489
490 if(was_playing != playing_ || was_loop != loop_)
491 {
493 // Reset start delay when emitter starts playing
494 if(playing_ && !was_playing)
495 {
497 }
498 }
499
500 // Update start delay elapsed time if playing and not paused
501 if(playing_ && !uniforms_.m_paused)
502 {
504 }
505
506
507 if(uniforms_.m_paused)
508 {
509 // If paused, set delta time to 0 (particles don't advance but remain visible)
510 _dt = 0.0f;
511 }
512 const math::vec3 currentPos = uniforms_.m_transform.get_position();
513 if(!uniforms_.m_paused)
514 {
515 update_temporal_buffer(currentPos, _dt);
516 }
517
518 if(!uniforms_.m_loop && total_particles_spawned_ >= max_particles_)
519 {
520 uniforms_.m_playing = false;
522 }
523
524 // Get effective transform properties based on simulation method
525 math::vec3 effectivePosition, effectiveScale, effectiveEmissionShapeScale;
526 math::mat4 effectiveTransform;
527 getEffectiveTransform(uniforms_, effectivePosition, effectiveScale, effectiveEmissionShapeScale, effectiveTransform);
528
529 math::bbox aabb;
530 aabb.reset();
531
532 aabb.add_point(effectivePosition - math::vec3(0.5f));
533 aabb.add_point(effectivePosition + math::vec3(0.5f));
534
535 uint32_t num = num_particles_;
536
537 // Pre-calculate per-frame constants to avoid recalculating per particle
538 const float avgSystemScale = (effectiveScale.x + effectiveScale.y + effectiveScale.z) / 3.0f;
539 const bx::EaseFn easePos = bx::getEaseFunc(uniforms_.m_easePos);
540
541 // Pre-calculate speed-based effect conditions
542 const bool hasColorBySpeed =
543 (uniforms_.m_colorBySpeedVelocityRange.max > uniforms_.m_colorBySpeedVelocityRange.min);
544 const bool hasSizeBySpeed =
545 (uniforms_.m_sizeBySpeedVelocityRange.max > uniforms_.m_sizeBySpeedVelocityRange.min &&
546 uniforms_.m_sizeBySpeedRange.min != uniforms_.m_sizeBySpeedRange.max);
547
548
549
550 for(uint32_t ii = 0; ii < num; ++ii)
551 {
552 Particle& particle = particles_[ii];
553 if(particle.lifeSpan <= 0.0f)
554 {
555 if(ii != num - 1)
556 {
557 bx::memCopy(&particle, &particles_[num - 1], sizeof(Particle));
558 --ii;
559 }
560 --num;
561 continue;
562 }
563 particle.life += _dt / particle.lifeSpan;
564
565 if(particle.life > 1.0f)
566 {
567 if(ii != num - 1)
568 {
569 bx::memCopy(&particle, &particles_[num - 1], sizeof(Particle));
570 --ii;
571 }
572
573 --num;
574 continue; // Skip processing for dead particles
575 }
576
577 // Update cached properties for living particles
578 updateParticleProperties(uniforms_, particle, avgSystemScale, easePos, hasColorBySpeed, hasSizeBySpeed, effectiveTransform);
579
580 // Add particle position with bounds that account for rotation and pivot
581 const math::vec3 local_half_extents = particle_scale_3d_ * particle.scale * 0.5f;
582 math::vec3 aabb_min, aabb_max;
583 calculateRotatedAABBBounds(particle.rotation, local_half_extents, pivot_, aabb_min, aabb_max);
584
585 aabb.add_point(particle.position + aabb_min);
586 aabb.add_point(particle.position + aabb_max);
587 }
588
589 num_particles_ = num;
590
591 if(0.0f < uniforms_.m_emissionLifetime && uniforms_.m_playing)
592 {
593 // Check if start delay has elapsed
594 bool start_delay_elapsed = start_delay_elapsed_ >= uniforms_.m_startDelay;
595
596 // For looping emitters, always spawn (after start delay)
597 // For non-looping emitters, only spawn if initial emission hasn't completed (after start delay)
598 bool initial_emission_complete = total_particles_spawned_ >= max_particles_;
599 if(start_delay_elapsed && (uniforms_.m_loop || !initial_emission_complete))
600 {
601 spawn(uniforms_, aabb,_dt);
602 }
603 }
604
605 // Safety check: ensure num_particles_ never exceeds max_particles_
606 BX_ASSERT(num_particles_ <= max_particles_, "Particle count exceeded maximum! num_particles_=%d, max_particles_=%d", num_particles_, max_particles_);
608
609
610 if(first_update_)
611 {
612 first_update_ = false;
613 }
614
615
616 aabb_ = aabb;
617
618 }
619
620 // Helper function to get effective transform properties (now unified for both simulation methods)
622 math::vec3& outPosition,
623 math::vec3& outScale,
624 math::vec3& outEmissionShapeScale,
625 math::mat4& outTransformMatrix) const
626 {
627 // Extract transform components directly (efficient for both simulation methods)
628 outPosition = uniforms_.m_transform.get_position();
629 outScale = uniforms_.m_transform.get_scale();
630 outEmissionShapeScale = uniforms_.m_emissionShapeScale; // Apply transform scale to emission shape
631 outTransformMatrix = uniforms_.m_transform; // Implicit conversion to mat4
632 }
633
634 void spawn(EmitterUniforms& uniforms_, math::bbox& aabb, float _dt)
635 {
636 // Skip emission if rate is zero or negative
637 if(uniforms_.m_particlesPerSecond <= 0.0f)
638 {
639 return;
640 }
641
642 // Calculate time per particle and accumulate time
643 const float timePerParticle = 1.0f / uniforms_.m_particlesPerSecond;
644 dt_ += _dt;
645
646 // Calculate how many particles to emit this frame
647 const uint32_t numParticlesToEmit = uint32_t(dt_ / timePerParticle);
648 dt_ -= numParticlesToEmit * timePerParticle; // Remove emitted time from accumulator
649
650 // Don't emit more particles than we have space for
651 const uint32_t maxEmittable = max_particles_ - num_particles_;
652 const uint32_t actualEmitCount = math::min(numParticlesToEmit, maxEmittable);
653
654 if(actualEmitCount == 0)
655 {
656 return;
657 }
658
659 // Get effective transform properties based on simulation method
660 math::vec3 effectivePosition, effectiveScale, effectiveEmissionShapeScale;
661 math::mat4 effectiveTransform;
662 getEffectiveTransform(uniforms_, effectivePosition, effectiveScale, effectiveEmissionShapeScale, effectiveTransform);
663
664 // Pre-calculate constants for new particle property calculation
665 const float avgSystemScale = (effectiveScale.x + effectiveScale.y + effectiveScale.z) / 3.0f;
666 const bx::EaseFn easePos = bx::getEaseFunc(uniforms_.m_easePos);
667 const bool hasColorBySpeed =
669 const bool hasSizeBySpeed =
671 uniforms_.m_sizeBySpeedRange.min != uniforms_.m_sizeBySpeedRange.max);
672
673 // Pre-calculate emitter speed for lifetime by emitter speed effect
674 const bool hasLifetimeByEmitterSpeed =
676 float emitterSpeed = 0.0f;
677 float lifetimeMultiplier = 1.0f;
678 if(hasLifetimeByEmitterSpeed)
679 {
680 // Use smoothed emitter speed from temporal buffer
681 // This handles physics fixed timestep discontinuities gracefully
682 // Pass max speed so buffer assumes max speed until filled, preventing lifetime inconsistencies at spawn
684
685 // Calculate speed factor and sample gradient
686 const float speedFactor =
687 bx::clamp((emitterSpeed - uniforms_.m_lifetimeByEmitterSpeedRange.min) /
689 0.0f, 1.0f);
690
691 lifetimeMultiplier = uniforms_.m_lifetimeByEmitterSpeedGradient.sample(speedFactor);
692 }
693
694 // Extract rotation matrix from effective transform
695 const math::mat3 rotationMatrix = math::mat3(effectiveTransform);
696
697 // Pre-calculate common transformation components (optimization)
698 const math::vec3 systemScale = effectiveScale;
699 const math::vec3 emissionShapeScale = effectiveEmissionShapeScale;
700 const float lifeSpan = uniforms_.m_lifetime * lifetimeMultiplier;
701 const float lifeSpanSquared = lifeSpan * lifeSpan;
702 math::vec3 gravityVector = math::vec3(0.0f, -9.81f * uniforms_.m_gravityScale * lifeSpanSquared, 0.0f);
703 math::vec3 forceOverLifetimeVector = uniforms_.m_forceOverLifetime * lifeSpanSquared;
704
706 {
707 gravityVector.y *= systemScale.y;
708 forceOverLifetimeVector *= systemScale;
709 }
710 const float velocityDampingFactor = (1.0f - uniforms_.m_velocityDamping);
711
712 // Calculate motion delta for temporal emission gap handling using temporal buffer
713 const math::vec3 currentPos = effectivePosition;
714 math::vec3 prevPos = uniforms_.m_prevTransform.get_position();
715
716 // Use temporal buffer to get a better previous position estimate
717 // This helps when physics doesn't update every frame
718 if(temporal_position_buffer_.size() >= 2)
719 {
720 // Use the position from 2 frames ago for better interpolation
721 const size_t prevIndex = temporal_position_buffer_.size() - 2;
722 prevPos = temporal_position_buffer_[prevIndex];
723 }
724
725 const math::vec3 up = math::vec3(0.0f, 1.0f, 0.0f);
726
727 // Emit particles with temporal interpolation
728 for(uint32_t ii = 0; ii < actualEmitCount; ++ii)
729 {
730 // Calculate emission phase for temporal motion interpolation
731 // Distribute particles evenly across the frame, scaled by temporal motion factor
732 const float baseEmissionPhase = float(ii) / float(actualEmitCount);
733 const float emissionPhase = baseEmissionPhase * uniforms_.m_temporalMotion;
734
735 // Find next available particle slot
736 Particle* particle = &particles_[num_particles_];
739
740
741 math::vec3 pos;
743 {
744 // Surface spawning
745 switch(shape_)
746 {
747 default:
749 {
750 // Use sphericalRand for surface of sphere
751 pos = glm::sphericalRand(1.0f);
752 }
753 break;
754
756 {
757 // Use sphericalRand and ensure Y >= 0 for hemisphere surface
758 math::vec3 spherePos = glm::sphericalRand(1.0f);
759 if(spherePos.y < 0.0f)
760 spherePos.y = -spherePos.y;
761 pos = spherePos;
762 }
763 break;
764
766 {
767 // Use circularRand for circle perimeter
768 math::vec2 circlePos = glm::circularRand(1.0f);
769 pos = math::vec3(circlePos.x, 0.0f, circlePos.y);
770 }
771 break;
772
774 {
775 // Spawn on surface of box - randomly select a face and position on that face
776 const float face = bx::frnd(&rng_) * 6.0f; // 0-5 for 6 faces
777 const int faceIndex = static_cast<int>(face);
778 const float u = bx::frnd(&rng_) * 2.0f - 1.0f; // -1 to 1
779 const float v = bx::frnd(&rng_) * 2.0f - 1.0f; // -1 to 1
780
781 switch(faceIndex)
782 {
783 case 0: // +X face
784 pos = math::vec3(1.0f, u, v);
785 break;
786 case 1: // -X face
787 pos = math::vec3(-1.0f, u, v);
788 break;
789 case 2: // +Y face
790 pos = math::vec3(u, 1.0f, v);
791 break;
792 case 3: // -Y face
793 pos = math::vec3(u, -1.0f, v);
794 break;
795 case 4: // +Z face
796 pos = math::vec3(u, v, 1.0f);
797 break;
798 case 5: // -Z face
799 pos = math::vec3(u, v, -1.0f);
800 break;
801 default:
802 pos = math::vec3(1.0f, u, v);
803 break;
804 }
805 }
806 break;
807
809 {
810 // Spawn on perimeter of rectangle - randomly select an edge
811 const float edge = bx::frnd(&rng_) * 4.0f; // 0-3 for 4 edges
812 const int edgeIndex = static_cast<int>(edge);
813 const float t = bx::frnd(&rng_) * 2.0f - 1.0f; // -1 to 1 along edge
814
815 switch(edgeIndex)
816 {
817 case 0: // Top edge (+Z)
818 pos = math::vec3(t, 0.0f, 1.0f);
819 break;
820 case 1: // Right edge (+X)
821 pos = math::vec3(1.0f, 0.0f, t);
822 break;
823 case 2: // Bottom edge (-Z)
824 pos = math::vec3(t, 0.0f, -1.0f);
825 break;
826 case 3: // Left edge (-X)
827 pos = math::vec3(-1.0f, 0.0f, t);
828 break;
829 default:
830 pos = math::vec3(t, 0.0f, 1.0f);
831 break;
832 }
833 }
834 break;
835 }
836 }
837 else
838 {
839 // Inside spawning (current behavior)
840 switch(shape_)
841 {
842 default:
844 pos = math::ballRand(1.0f);
845 break;
846
848 {
849 math::vec3 spherePos = math::ballRand(1.0f);
850 if(math::dot(spherePos, up) < 0.0f)
851 spherePos = -spherePos;
852 pos = spherePos;
853 }
854 break;
855
857 {
858 math::vec2 circlePos = math::diskRand(1.0f);
859 pos = math::vec3(circlePos.x, 0.0f, circlePos.y);
860 }
861 break;
862
864 pos = math::vec3(math::linearRand(-1.0f, 1.0f),
865 math::linearRand(-1.0f, 1.0f),
866 math::linearRand(-1.0f, 1.0f));
867 break;
868
870 pos = math::vec3(math::linearRand(-1.0f, 1.0f), 0.0f, math::linearRand(-1.0f, 1.0f));
871 break;
872 }
873 }
874
875 // Apply emission shape scale (use pre-calculated value)
876 pos = (uniforms_.m_emissionShapePosition + pos) * emissionShapeScale;
877
878
879 math::vec3 dir;
880 switch(direction_)
881 {
882 default:
884 dir = up;
885 break;
886
888 {
889 const float len_sq = math::dot(pos, pos);
890 dir = (len_sq > k_emit_dir_zero_len_sq) ? math::normalize(pos) : up;
891 }
892 break;
893
895 {
896 const float len_sq = math::dot(pos, pos);
897 dir = (len_sq > k_emit_dir_zero_len_sq) ? math::normalize(pos) : up;
898 }
899 break;
900 }
901
902 // Use pre-calculated system scale for better performance
903 math::vec3 start = pos;
904
905 // Sample velocity range from gradient at particle end (t=1)
906 const frange_t endVelocityRange = uniforms_.m_velocityGradient.sample(1.0f);
907 const float endVelocity = math::mix(endVelocityRange.min, endVelocityRange.max, bx::frnd(&rng_));
908 const math::vec3 scaledDir = systemScale * dir;
909 const math::vec3 tmp1 = dir * endVelocity;
910 math::vec3 end = tmp1 + start;
911
913 {
914 std::swap(start, end);
915 dir *= -1.0f;
916 }
917
918 particle->lifeSpan = math::max(lifeSpan, k_min_particle_lifespan);
919 particle->life = 0.0f;
920
921 // Fast-forward life so the particle starts at the time it would have been
922 // emitted within this frame. This keeps temporal interpolation smooth
923 // even when lifetime is scaled by emitter speed.
924 // const float spawnTimeOffset = emissionPhase * _dt;
925 // const float invLifeSpan = particle->lifeSpan > 0.0f ? 1.0f / particle->lifeSpan : 0.0f;
926 // particle->life = bx::clamp(spawnTimeOffset * invLifeSpan, 0.0f, 1.0f);
927 // Calculate interpolated emitter position for temporal emission gap handling
928 math::vec3 interpolatedEmitterPos = math::mix(prevPos, currentPos, emissionPhase);
929
931 {
932 // For local simulation, store particles in local space (no transform applied)
933 // The transform will be applied during rendering
934 particle->start = start; // Local space position
935 particle->end[0] = end; // Local space end position
936 }
937 else
938 {
939 // For world simulation, apply rotation and translation as before
940 particle->start = rotationMatrix * start + interpolatedEmitterPos;
941 particle->end[0] = rotationMatrix * end + interpolatedEmitterPos;
942 }
943
944 // Apply damping to the velocity (use pre-calculated damping factor)
945 if(uniforms_.m_velocityDamping > 0.0f)
946 {
947 const math::vec3 velocity = particle->end[0] - particle->start;
948 const math::vec3 dampedVelocity = velocity * velocityDampingFactor;
949 particle->end[0] = particle->start + dampedVelocity;
950 }
951
952 // Use pre-calculated force vectors
953 const math::vec3 totalForce = gravityVector + forceOverLifetimeVector;
954 particle->end[1] = particle->end[0] + totalForce;
955
956 // Color will be sampled from gradient during rendering - no need to copy here
957
958 // Sample scale range from gradient at particle spawn (t=0) and end (t=1)
959 const frange_t startScaleRange = uniforms_.m_scaleGradient.sample(0.0f);
960 const frange_t endScaleRange = uniforms_.m_scaleGradient.sample(1.0f);
961 particle->scale_start = math::mix(startScaleRange.min, startScaleRange.max, bx::frnd(&rng_));
962 particle->scale_end = math::mix(endScaleRange.min, endScaleRange.max, bx::frnd(&rng_));
963
964 // Initialize texture sheet animation random offset
965 particle->texsheet_random_offset = bx::frnd(&rng_);
966
967 // Calculate properties immediately for new particles
968 updateParticleProperties(uniforms_, *particle, avgSystemScale, easePos, hasColorBySpeed, hasSizeBySpeed, effectiveTransform);
969
970 // Add particle position with bounds that account for rotation and pivot
971 const math::vec3 local_half_extents = particle_scale_3d_ * particle->scale * 0.5f;
972 math::vec3 aabb_min, aabb_max;
973 calculateRotatedAABBBounds(particle->rotation, local_half_extents, pivot_, aabb_min, aabb_max);
974
975 aabb.add_point(particle->position + aabb_min);
976 aabb.add_point(particle->position + aabb_max);
977 }
978
979 }
980
983
984 float dt_;
985 bx::RngMwc rng_;
986
988
993
995 bool loop_;
996 float start_delay_elapsed_; // Elapsed time since emitter started playing (for start delay)
997
998 bool first_update_; // Track if this is the first update to avoid interpolation
999
1000 // Temporal position buffer for smooth emitter speed calculation
1001 std::vector<math::vec3> temporal_position_buffer_;
1002 std::vector<float> temporal_time_buffer_;
1003
1004 // Cached texture mode for rendering (determines which shader to use)
1006
1007 // Cached render mode for rendering
1009
1010 // Cached blend mode for rendering
1012
1013 // Cached billboard vectors (calculated from render mode and camera)
1015 math::vec3 billboard_up_;
1016
1017 // Cached 3D particle scale (from uniforms, applied to all particles)
1019
1020 // Cached pivot point (from uniforms, applied to all particles)
1021 math::vec2 pivot_;
1022};
1023
1025{
1026 void init(uint16_t _maxEmitters, bx::AllocatorI* _allocator)
1027 {
1028 m_allocator = _allocator;
1029
1030 if(nullptr == _allocator)
1031 {
1032 static bx::DefaultAllocator allocator;
1033 m_allocator = &allocator;
1034 }
1035
1036 m_emitterAlloc = bx::createHandleAlloc(m_allocator, _maxEmitters);
1037 m_emitter.resize(_maxEmitters);
1038
1039 // Initialize vertex layouts
1041
1042 // Create static quad geometry for instanced rendering
1043 m_quadVBH = bgfx::createVertexBuffer(
1044 bgfx::makeRef(s_quadVertices, sizeof(s_quadVertices)),
1046 );
1047
1048 m_quadIBH = bgfx::createIndexBuffer(
1049 bgfx::makeRef(s_quadIndices, sizeof(s_quadIndices))
1050 );
1051
1052 s_texColor = bgfx::createUniform("s_texColor", bgfx::UniformType::Sampler);
1053 u_viewCamera = bgfx::createUniform("u_viewCamera", bgfx::UniformType::Mat4);
1054 u_eyePos = bgfx::createUniform("u_eyePos", bgfx::UniformType::Vec4);
1055 }
1056
1058 {
1059 bgfx::destroy(s_texColor);
1060 bgfx::destroy(u_viewCamera);
1061 bgfx::destroy(u_eyePos);
1062 bgfx::destroy(m_quadVBH);
1063 bgfx::destroy(m_quadIBH);
1064
1065 bx::destroyHandleAlloc(m_allocator, m_emitterAlloc);
1066 // bx::free(m_allocator, m_emitter);
1067
1068 m_allocator = nullptr;
1069 }
1070
1071 // Batch rendering support structures and functions
1073 {
1074 float dist; // Squared distance from camera for sorting
1075 uint32_t emitter_idx; // Which emitter this particle belongs to
1076 uint32_t particle_idx; // Index within that emitter's particle array
1077 };
1078
1079 uint32_t renderEmitterBatch(const EmitterHandle* _handles,
1080 uint32_t _count,
1081 uint8_t _view,
1082 bgfx::ProgramHandle _program,
1083 const float* _mtxView,
1084 const math::vec3& _eye,
1085 bgfx::TextureHandle _texture,
1086 uint64_t _blend_state,
1087 bool _sort_by_depth)
1088 {
1089 if(_count == 0 || !bgfx::isValid(_texture))
1090 {
1091 return 0;
1092 }
1093 APP_SCOPE_PERF("Rendering/Particle Pass/Render Batched Emitters");
1094 return renderEmitterBatchByMode(_handles, _count, _view, _program, _mtxView, _eye, _texture, _blend_state,
1095 _sort_by_depth);
1096 }
1097
1098 static void writeParticleInstanceRow(uint8_t* row, const Emitter& emitter, const Particle& particle)
1099 {
1100 float* pos = reinterpret_cast<float*>(row);
1101 pos[0] = particle.position.x;
1102 pos[1] = particle.position.y;
1103 pos[2] = particle.position.z;
1104 pos[3] = emitter.pivot_.x;
1105 float* rot = reinterpret_cast<float*>(row + 16);
1106 rot[0] = particle.rotation.x;
1107 rot[1] = particle.rotation.y;
1108 rot[2] = particle.rotation.z;
1109 rot[3] = particle.rotation.w;
1110 float* scale3d = reinterpret_cast<float*>(row + 32);
1111 scale3d[0] = particle.scale * emitter.particle_scale_3d_.x;
1112 scale3d[1] = particle.scale * emitter.particle_scale_3d_.y;
1113 scale3d[2] = particle.scale * emitter.particle_scale_3d_.z;
1114 scale3d[3] = emitter.pivot_.y;
1115 float* uvData = reinterpret_cast<float*>(row + 48);
1116 uvData[0] = particle.uv_offset.x;
1117 uvData[1] = particle.uv_offset.y;
1118 uvData[2] = particle.uv_scale.x;
1119 uvData[3] = particle.uv_scale.y;
1120 float* color = reinterpret_cast<float*>(row + 64);
1121 color[0] = particle.color.value.r;
1122 color[1] = particle.color.value.g;
1123 color[2] = particle.color.value.b;
1124 color[3] = particle.color.value.a;
1125 float* facing = reinterpret_cast<float*>(row + 80);
1126 facing[0] = float(emitter.render_mode_);
1127 facing[1] = 0.0f;
1128 facing[2] = 0.0f;
1129 facing[3] = 0.0f;
1130 }
1131
1132 uint32_t buildEmitterBatchPrefixes(const EmitterHandle* _handles, uint32_t _count)
1133 {
1134 emitter_batch_prefix_scratch_.resize(_count + 1);
1136 for(uint32_t emitterIdx = 0; emitterIdx < _count; ++emitterIdx)
1137 {
1138 uint32_t n = 0;
1139 if(isValid(_handles[emitterIdx]))
1140 {
1141 n = m_emitter[_handles[emitterIdx].idx].num_particles_;
1142 }
1143 emitter_batch_prefix_scratch_[emitterIdx + 1] = emitter_batch_prefix_scratch_[emitterIdx] + n;
1144 }
1145 return emitter_batch_prefix_scratch_[_count];
1146 }
1147
1148 uint32_t renderEmitterBatchByMode(const EmitterHandle* _handles, uint32_t _count,
1149 uint8_t _view, bgfx::ProgramHandle _program,
1150 const float* _mtxView, const math::vec3& _eye,
1151 bgfx::TextureHandle _texture, uint64_t _blendState,
1152 bool _sort_by_depth)
1153 {
1154 const uint32_t totalParticles = buildEmitterBatchPrefixes(_handles, _count);
1155 if(totalParticles == 0)
1156 {
1157 return 0;
1158 }
1159 const uint16_t instanceStride = 96;
1160
1161 if(_sort_by_depth)
1162 {
1163 buildSortedBatchedParticles(_handles, _count, _eye, totalParticles);
1164 if(batched_particles_scratch_.empty())
1165 {
1166 return 0;
1167 }
1168 }
1169
1170 {
1171 APP_SCOPE_PERF("Rendering/Particle Pass/Set Common Uniforms");
1172 float viewCamera[16];
1173 viewCamera[0] = _mtxView[0];
1174 viewCamera[1] = _mtxView[4];
1175 viewCamera[2] = _mtxView[8];
1176 viewCamera[3] = 0.0f;
1177 viewCamera[4] = _mtxView[1];
1178 viewCamera[5] = _mtxView[5];
1179 viewCamera[6] = _mtxView[9];
1180 viewCamera[7] = 0.0f;
1181 viewCamera[8] = _mtxView[2];
1182 viewCamera[9] = _mtxView[6];
1183 viewCamera[10] = _mtxView[10];
1184 viewCamera[11] = 0.0f;
1185 viewCamera[12] = 0.0f;
1186 viewCamera[13] = 0.0f;
1187 viewCamera[14] = 0.0f;
1188 viewCamera[15] = 1.0f;
1189 float eyePosVec4[4] = { _eye.x, _eye.y, _eye.z, 0.0f };
1190
1191 bgfx::setVertexBuffer(0, m_quadVBH);
1192 bgfx::setIndexBuffer(m_quadIBH);
1193 bgfx::setState(0 | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A |
1194 BGFX_STATE_DEPTH_TEST_LESS | BGFX_STATE_CULL_CW |
1195 _blendState);
1196 bgfx::setTexture(0, s_texColor, _texture);
1197 bgfx::setUniform(u_viewCamera, viewCamera);
1198 bgfx::setUniform(u_eyePos, eyePosVec4);
1199 }
1200
1201 const auto writeChunk = [&](uint8_t* data, uint32_t start, uint32_t count)
1202 {
1203 if(_sort_by_depth)
1204 {
1205 writeBatchedInstanceChunk(data, start, count, _handles, instanceStride);
1206 }
1207 else
1208 {
1209 writeDirectInstanceChunk(data, start, count, _handles, _count, instanceStride);
1210 }
1211 };
1212
1213 const uint32_t availAll = bgfx::getAvailInstanceDataBuffer(totalParticles, instanceStride);
1214 if(availAll >= totalParticles)
1215 {
1216 APP_SCOPE_PERF("Rendering/Particle Pass/Allocate Instance Data Buffer (single batch)");
1217 bgfx::InstanceDataBuffer idb{};
1218 bgfx::allocInstanceDataBuffer(&idb, totalParticles, instanceStride);
1219 writeChunk(idb.data, 0, totalParticles);
1220 bgfx::setInstanceDataBuffer(&idb);
1221 {
1222 APP_SCOPE_PERF("Rendering/Particle Pass/Submit Batch");
1223 bgfx::submit(_view, _program);
1224 }
1225 return totalParticles;
1226 }
1227
1228 uint32_t renderedTotal = 0;
1229 uint32_t offset = 0;
1230 while(offset < totalParticles)
1231 {
1232 APP_SCOPE_PERF("Rendering/Particle Pass/Allocate Instance Data Buffer");
1233
1234 const uint32_t remaining = totalParticles - offset;
1235 uint32_t chunk = bgfx::getAvailInstanceDataBuffer(remaining, instanceStride);
1236 if(chunk == 0)
1237 {
1238 BX_WARN(false, "No instance buffer space available for batch rendering.");
1239 break;
1240 }
1241 chunk = math::min(chunk, remaining);
1242 bgfx::InstanceDataBuffer idb{};
1243 bgfx::allocInstanceDataBuffer(&idb, chunk, instanceStride);
1244 writeChunk(idb.data, offset, chunk);
1245 bgfx::setInstanceDataBuffer(&idb);
1246 {
1247 APP_SCOPE_PERF("Rendering/Particle Pass/Submit Batch");
1248 bgfx::submit(_view, _program);
1249 }
1250
1251 renderedTotal += chunk;
1252 offset += chunk;
1253 }
1254 return renderedTotal;
1255 }
1256
1258 uint32_t _count,
1259 const math::vec3& _eye,
1260 uint32_t totalParticles)
1261 {
1262 APP_SCOPE_PERF("Rendering/Particle Pass/Build Sorted Batched Particles");
1263
1265 if(_count == 0 || totalParticles == 0)
1266 {
1267 return;
1268 }
1269
1270 batched_particles_scratch_.resize(totalParticles);
1271
1272 {
1273 APP_SCOPE_PERF("Rendering/Particle Pass/Particle Sort Keys");
1274
1275 const auto fillEmitterKeys = [&](uint32_t emitterIdx)
1276 {
1277 const uint32_t start = emitter_batch_prefix_scratch_[emitterIdx];
1278 const uint32_t end = emitter_batch_prefix_scratch_[emitterIdx + 1];
1279 const uint32_t n = end - start;
1280 if(n == 0)
1281 {
1282 return;
1283 }
1284 const Emitter& emitter = m_emitter[_handles[emitterIdx].idx];
1285 for(uint32_t p = 0; p < n; ++p)
1286 {
1287 const Particle& particle = emitter.particles_[p];
1288 const math::vec3 tmp0 = _eye - particle.position;
1289 const float distSquared = math::dot(tmp0, tmp0);
1290 batched_particles_scratch_[start + p] = BatchedParticle{distSquared, emitterIdx, p};
1291 }
1292 };
1293
1294 // One poolstl task per emitter-index range (not per emitter) to match writeBatchedInstanceChunk grain.
1295 constexpr uint32_t kParallelSortKeysEmitterThreshold = 16;
1296 constexpr uint32_t kMinEmittersPerParallelJob = 16;
1297 if(_count < kParallelSortKeysEmitterThreshold)
1298 {
1299 for(uint32_t emitterIdx = 0; emitterIdx < _count; ++emitterIdx)
1300 {
1301 fillEmitterKeys(emitterIdx);
1302 }
1303 }
1304 else
1305 {
1306 const uint32_t numJobs = (_count + kMinEmittersPerParallelJob - 1) / kMinEmittersPerParallelJob;
1307 if(numJobs <= 1u)
1308 {
1309 for(uint32_t emitterIdx = 0; emitterIdx < _count; ++emitterIdx)
1310 {
1311 fillEmitterKeys(emitterIdx);
1312 }
1313 }
1314 else
1315 {
1316 std::for_each(poolstl::par,
1317 poolstl::iota_iter<uint32_t>(0),
1318 poolstl::iota_iter<uint32_t>(numJobs),
1319 [&](uint32_t job)
1320 {
1321 const uint32_t emitBegin = job * kMinEmittersPerParallelJob;
1322 const uint32_t emitEnd = math::min(emitBegin + kMinEmittersPerParallelJob, _count);
1323 for(uint32_t emitterIdx = emitBegin; emitterIdx < emitEnd; ++emitterIdx)
1324 {
1325 fillEmitterKeys(emitterIdx);
1326 }
1327 });
1328 }
1329 }
1330 }
1331
1332 // Tiny batches: sequential sort avoids poolSTL fork/join Wait on the main thread.
1333 constexpr uint32_t kParallelSortParticleThreshold = 2048;
1334 const auto by_distance_desc = [](const BatchedParticle& a, const BatchedParticle& b)
1335 {
1336 return a.dist > b.dist;
1337 };
1338
1339 if(totalParticles < kParallelSortParticleThreshold)
1340 {
1341 APP_SCOPE_PERF("Rendering/Particle Pass/Particle Sort");
1342 std::sort(batched_particles_scratch_.begin(), batched_particles_scratch_.end(), by_distance_desc);
1343 }
1344 else
1345 {
1346 APP_SCOPE_PERF("Rendering/Particle Pass/Particle Sort Parallel");
1347 std::sort(poolstl::par,
1350 by_distance_desc);
1351 }
1352 }
1353
1354 void writeDirectInstanceChunk(uint8_t* data,
1355 uint32_t globalStart,
1356 uint32_t count,
1357 const EmitterHandle* _handles,
1358 uint32_t _emitterCount,
1359 uint16_t instanceStride)
1360 {
1361 APP_SCOPE_PERF("Rendering/Particle Pass/Write Direct Instance Chunk");
1362
1363 // Emission order: write contiguous particle runs from each emitter into the instance buffer.
1364 // Parallelize by emitter so each job streams one emitter's particle array (no gather).
1365 constexpr uint32_t kParallelEmitterThreshold = 16;
1366 constexpr uint32_t kMinEmittersPerParallelJob = 16;
1367
1368 const auto writeEmitterRange = [&](uint32_t emitBegin, uint32_t emitEnd)
1369 {
1370 for(uint32_t emitterIdx = emitBegin; emitterIdx < emitEnd; ++emitterIdx)
1371 {
1372 const uint32_t emitBeginGlobal = emitter_batch_prefix_scratch_[emitterIdx];
1373 const uint32_t emitEndGlobal = emitter_batch_prefix_scratch_[emitterIdx + 1];
1374 const uint32_t rangeBegin = math::max(emitBeginGlobal, globalStart);
1375 const uint32_t rangeEnd = math::min(emitEndGlobal, globalStart + count);
1376 if(rangeBegin >= rangeEnd)
1377 {
1378 continue;
1379 }
1380
1381 const Emitter& emitter = m_emitter[_handles[emitterIdx].idx];
1382 for(uint32_t g = rangeBegin; g < rangeEnd; ++g)
1383 {
1384 const uint32_t particleIdx = g - emitBeginGlobal;
1385 const uint32_t outIdx = g - globalStart;
1386 writeParticleInstanceRow(data + static_cast<size_t>(outIdx) * instanceStride,
1387 emitter,
1388 emitter.particles_[particleIdx]);
1389 }
1390 }
1391 };
1392
1393 if(_emitterCount < kParallelEmitterThreshold)
1394 {
1395 writeEmitterRange(0, _emitterCount);
1396 return;
1397 }
1398
1399 const uint32_t numJobs = (_emitterCount + kMinEmittersPerParallelJob - 1) / kMinEmittersPerParallelJob;
1400 if(numJobs <= 1u)
1401 {
1402 writeEmitterRange(0, _emitterCount);
1403 return;
1404 }
1405
1406 std::for_each(poolstl::par,
1407 poolstl::iota_iter<uint32_t>(0),
1408 poolstl::iota_iter<uint32_t>(numJobs),
1409 [&](uint32_t job)
1410 {
1411 const uint32_t emitBegin = job * kMinEmittersPerParallelJob;
1412 const uint32_t emitEnd = math::min(emitBegin + kMinEmittersPerParallelJob, _emitterCount);
1413 writeEmitterRange(emitBegin, emitEnd);
1414 });
1415 }
1416
1417 void writeBatchedInstanceChunk(uint8_t* data, uint32_t sortedStart, uint32_t sortedCount,
1418 const EmitterHandle* _handles, uint16_t instanceStride)
1419 {
1420 APP_SCOPE_PERF("Rendering/Particle Pass/Write Batched Instance Chunk Parallel");
1421
1422 const auto writeRow = [&](uint32_t i)
1423 {
1424 const BatchedParticle& batchedParticle = batched_particles_scratch_[sortedStart + i];
1425 const Emitter& emitter = m_emitter[_handles[batchedParticle.emitter_idx].idx];
1426 const Particle& particle = emitter.particles_[batchedParticle.particle_idx];
1427 writeParticleInstanceRow(data + static_cast<size_t>(i) * instanceStride, emitter, particle);
1428 };
1429
1430 // Disjoint row writes. One poolstl task per contiguous row range (not per particle) to amortize scheduling.
1431 constexpr uint32_t kParallelWriteThreshold = 128;
1432 constexpr uint32_t kMinRowsPerParallelJob = 128;
1433
1434 if(sortedCount < kParallelWriteThreshold)
1435 {
1436 for(uint32_t i = 0; i < sortedCount; ++i)
1437 {
1438 writeRow(i);
1439 }
1440 return;
1441 }
1442 const uint32_t numJobs = (sortedCount + kMinRowsPerParallelJob - 1) / kMinRowsPerParallelJob;
1443 if(numJobs <= 1u)
1444 {
1445 for(uint32_t i = 0; i < sortedCount; ++i)
1446 {
1447 writeRow(i);
1448 }
1449 return;
1450 }
1451
1452 std::for_each(poolstl::par,
1453 poolstl::iota_iter<uint32_t>(0),
1454 poolstl::iota_iter<uint32_t>(numJobs),
1455 [&](uint32_t job)
1456 {
1457 const uint32_t rowBegin = job * kMinRowsPerParallelJob;
1458 const uint32_t rowEnd = math::min(rowBegin + kMinRowsPerParallelJob, sortedCount);
1459 for(uint32_t i = rowBegin; i < rowEnd; ++i)
1460 {
1461 writeRow(i);
1462 }
1463 });
1464 }
1465
1466 EmitterHandle createEmitter(EmitterShape::Enum _shape, EmitterDirection::Enum _direction, uint32_t _maxParticles)
1467 {
1468 EmitterHandle handle = {m_emitterAlloc->alloc()};
1469
1470 if(UINT16_MAX != handle.idx)
1471 {
1472 m_emitter[handle.idx].create(_shape, _direction, _maxParticles);
1473 }
1474
1475 return handle;
1476 }
1477
1478 void updateEmitter(EmitterHandle _handle, float _dt, EmitterUniforms* _uniforms)
1479 {
1480 BX_ASSERT(isValid(_handle), "destroyEmitter handle %d is not valid.", _handle.idx);
1481
1482 Emitter& emitter = m_emitter[_handle.idx];
1483
1484 if(nullptr == _uniforms)
1485 {
1486 emitter.reset();
1487 }
1488 else
1489 {
1490
1491 emitter.update(_uniforms, _dt);
1492 }
1493 }
1494
1495 void getAabb(EmitterHandle _handle, math::bbox& _outAabb)
1496 {
1497 BX_ASSERT(isValid(_handle), "getAabb handle %d is not valid.", _handle.idx);
1498 _outAabb = m_emitter[_handle.idx].aabb_;
1499 }
1501 {
1502 BX_ASSERT(isValid(_handle), "getNumParticles handle %d is not valid.", _handle.idx);
1503 return m_emitter[_handle.idx].num_particles_;
1504 }
1505
1507 {
1508 BX_ASSERT(isValid(_handle), "hasUpdated handle %d is not valid.", _handle.idx);
1509 return !m_emitter[_handle.idx].first_update_;
1510 }
1511
1513 {
1514 BX_ASSERT(isValid(_handle), "destroyEmitter handle %d is not valid.", _handle.idx);
1515
1516 m_emitter[_handle.idx].destroy();
1517 m_emitterAlloc->free(_handle.idx);
1518 }
1519
1520 bx::AllocatorI* m_allocator;
1521
1522 bx::HandleAlloc* m_emitterAlloc;
1523 std::vector<Emitter> m_emitter;
1524 std::vector<BatchedParticle> batched_particles_scratch_;
1525 std::vector<uint32_t> emitter_batch_prefix_scratch_;
1526
1527 // Static geometry for instanced rendering
1528 bgfx::VertexBufferHandle m_quadVBH;
1529 bgfx::IndexBufferHandle m_quadIBH;
1530
1531 bgfx::UniformHandle s_texColor;
1532 bgfx::UniformHandle u_viewCamera;
1533 bgfx::UniformHandle u_eyePos;
1534};
1535
1537
1538void Emitter::create(EmitterShape::Enum _shape, EmitterDirection::Enum _direction, uint32_t _maxParticles)
1539{
1540 reset();
1541
1542 shape_ = _shape;
1543 direction_ = _direction;
1544 max_particles_ = _maxParticles;
1545 particles_ = (Particle*)bx::alloc(s_ctx.m_allocator, max_particles_ * sizeof(Particle));
1546}
1547
1549{
1550 bx::free(s_ctx.m_allocator, particles_);
1551 particles_ = nullptr;
1552}
1553
1554} // namespace ps
1555
1556using namespace ps;
1557
1558void psInit(uint16_t _maxEmitters, bx::AllocatorI* _allocator)
1559{
1560 s_ctx.init(_maxEmitters, _allocator);
1561}
1562
1564{
1565 s_ctx.shutdown();
1566}
1567
1568// Sprite functions removed - use bgfx::TextureHandle directly in EmitterUniforms
1569
1571{
1572 return s_ctx.createEmitter(_shape, _direction, _maxParticles);
1573}
1574
1575void psUpdateEmitter(EmitterHandle _handle, float _dt, EmitterUniforms* _uniforms)
1576{
1577 s_ctx.updateEmitter(_handle, _dt, _uniforms);
1578}
1579
1581{
1582 BX_ASSERT(isValid(_handle), "psResetEmitter handle %d is not valid.", _handle.idx);
1583
1584 s_ctx.m_emitter[_handle.idx].reset();
1585}
1586
1587void psGetAabb(EmitterHandle _handle, math::bbox& _outAabb)
1588{
1589 s_ctx.getAabb(_handle, _outAabb);
1590}
1591
1593{
1594 return s_ctx.getNumParticles(_handle);
1595}
1596
1598{
1599 return s_ctx.hasUpdated(_handle);
1600}
1601
1603{
1604 s_ctx.destroyEmitter(_handle);
1605}
1606
1607uint32_t psRenderEmitterBatch(const EmitterHandle* _handles,
1608 uint32_t _count,
1609 uint8_t _view,
1610 bgfx::ProgramHandle _program,
1611 const float* _mtxView,
1612 const math::vec3& _eye,
1613 bgfx::TextureHandle _texture,
1614 uint64_t _blend_state,
1615 bool _sort_by_depth)
1616{
1617 return s_ctx.renderEmitterBatch(_handles, _count, _view, _program, _mtxView, _eye, _texture, _blend_state,
1618 _sort_by_depth);
1619}
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.
auto get_rotation() const noexcept -> const quat_t &
Get the rotation component.
float y
float x
float z
range< float > frange_t
math::vec3 position
Definition defaults.cpp:52
auto look_rotation(const glm::vec3 &forward, const glm::vec3 &upwards) -> glm::quat
transform_t< float > transform
static ParticleSystem s_ctx
std::vector< math::color > color
uint32_t count
std::vector< float > scale
std::vector< math::quat > rotation
std::vector< math::vec3 > start
void psResetEmitter(EmitterHandle _handle)
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, uint64_t _blend_state, bool _sort_by_depth)
bool psHasUpdated(EmitterHandle _handle)
static const uint16_t s_quadIndices[6]
static ParticleVertex s_quadVertices[4]
EmitterHandle psCreateEmitter(EmitterShape::Enum _shape, EmitterDirection::Enum _direction, uint32_t _maxParticles)
void psShutdown()
uint32_t psGetNumParticles(EmitterHandle _handle)
void psGetAabb(EmitterHandle _handle, math::bbox &_outAabb)
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_literal)
Create a scoped performance timer that records to the timeline profiler. Only accepts string literals...
Definition profiler.h:675
math::vec2 m_texSheetTiles
math::gradient< math::color > m_colorGradient
math::transform m_transform
frange_t m_sizeBySpeedRange
math::transform m_prevTransform
math::vec3 m_initialScale3D
math::vec3 m_billboardUp
BlendMode::Enum m_blendMode
bx::Easing::Enum m_easePos
math::vec3 m_billboardRight
RenderMode::Enum m_renderMode
frange_t m_lifetimeByEmitterSpeedRange
math::vec3 m_emissionShapePosition
math::gradient< frange_t > m_scaleGradient
math::gradient< frange_t > m_velocityGradient
TextureMode::Enum m_textureMode
math::gradient< float > m_lifetimeByEmitterSpeedGradient
frange_t m_sizeBySpeedVelocityRange
EmitterSpawnLocation::Enum m_spawnLocation
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
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
math::vec3 particle_scale_3d_
uint32_t max_particles_
float calculateParticleSpeed(const Particle &particle, float ttPos) const
RenderMode::Enum render_mode_
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)
BlendMode::Enum blend_mode_
void updateParticleProperties(EmitterUniforms &uniforms_, Particle &particle, float avgSystemScale, bx::EaseFn easePos, bool hasColorBySpeed, bool hasSizeBySpeed, const math::mat4 &effectiveTransform)
math::vec3 billboard_up_
uint32_t total_particles_spawned_
void update(EmitterUniforms *_uniforms, float _dt)
void calculateRotatedAABBBounds(const math::quat &rotation, const math::vec3 &half_extents, const math::vec2 &pivot, math::vec3 &out_min, math::vec3 &out_max) const
EmitterShape::Enum shape_
std::vector< math::vec3 > temporal_position_buffer_
float calculate_smoothed_emitter_speed(float max_speed) const
static constexpr size_t TEMPORAL_BUFFER_SIZE
TextureMode::Enum texture_mode_
void update_temporal_buffer(const math::vec3 &position, float dt)
uint32_t num_particles_
math::vec3 billboard_right_
EmitterDirection::Enum direction_
Particle * particles_
std::vector< float > temporal_time_buffer_
math::vec3 end[2]
math::vec3 position
math::quat rotation
math::vec2 uv_scale
math::vec2 uv_offset
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)
std::vector< uint32_t > emitter_batch_prefix_scratch_
bgfx::UniformHandle u_viewCamera
bgfx::VertexBufferHandle m_quadVBH
void updateEmitter(EmitterHandle _handle, float _dt, EmitterUniforms *_uniforms)
bgfx::UniformHandle u_eyePos
std::vector< BatchedParticle > batched_particles_scratch_
uint32_t buildEmitterBatchPrefixes(const EmitterHandle *_handles, uint32_t _count)
static void writeParticleInstanceRow(uint8_t *row, const Emitter &emitter, const Particle &particle)
bx::AllocatorI * m_allocator
void writeDirectInstanceChunk(uint8_t *data, uint32_t globalStart, uint32_t count, const EmitterHandle *_handles, uint32_t _emitterCount, uint16_t instanceStride)
uint32_t renderEmitterBatchByMode(const EmitterHandle *_handles, uint32_t _count, uint8_t _view, bgfx::ProgramHandle _program, const float *_mtxView, const math::vec3 &_eye, bgfx::TextureHandle _texture, uint64_t _blendState, bool _sort_by_depth)
void init(uint16_t _maxEmitters, bx::AllocatorI *_allocator)
void destroyEmitter(EmitterHandle _handle)
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, uint64_t _blend_state, bool _sort_by_depth)
bx::HandleAlloc * m_emitterAlloc
void writeBatchedInstanceChunk(uint8_t *data, uint32_t sortedStart, uint32_t sortedCount, const EmitterHandle *_handles, uint16_t instanceStride)
bgfx::IndexBufferHandle m_quadIBH
void buildSortedBatchedParticles(const EmitterHandle *_handles, uint32_t _count, const math::vec3 &_eye, uint32_t totalParticles)
std::vector< Emitter > m_emitter
bgfx::UniformHandle s_texColor
gfx::uniform_handle handle
Definition uniform.cpp:9
static const uint16_t s_quadIndices[6]
static DebugShapeVertex s_quadVertices[4]
bool isValid(SpriteHandle _handle)
Definition debugdraw.h:34