Files
QuaternionEngine/docs/ParticleSystem.md

367 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Particle System
The particle system provides GPU-accelerated particle simulation and rendering with support for flipbook animation, soft particles, and alpha/additive blending.
## Architecture Overview
The system is implemented across multiple components:
- **ParticlePass** (`src/render/passes/particles.h/.cpp`) — Render pass managing particle pools, compute pipelines, and graphics pipelines
- **GameAPI** (`src/core/game_api.h`) — High-level API for creating and controlling particle systems
- **Shaders** — Compute and graphics shaders for simulation and rendering
- `shaders/particles_update.comp` — Per-particle physics simulation
- `shaders/particles_sort_blocks.comp` — Block-level depth sorting for alpha blending
- `shaders/particles_build_indices.comp` — Build draw indices from sorted blocks
- `shaders/particles.vert/.frag` — Vertex/fragment shaders for rendering
## Key Features
- **Global particle pool**: Up to 128K particles (`k_max_particles = 128 * 1024`) shared across all systems
- **GPU simulation**: Fully GPU-driven via compute shaders (no CPU readback)
- **Flipbook animation**: Supports sprite sheet animation with configurable atlas layout and FPS
- **Soft particles**: Depth-aware fading near opaque geometry
- **Blend modes**: Additive (fire, sparks) and Alpha (smoke, debris) with automatic depth sorting
- **Noise distortion**: Optional UV distortion for organic motion
- **Floating-origin stable**: Automatically adjusts particle positions when world origin shifts
## Particle Data Layout
Each particle is represented as 64 bytes (4 × vec4) on the GPU:
```glsl
struct Particle
{
vec4 pos_age; // xyz = local position, w = remaining life (seconds)
vec4 vel_life; // xyz = local velocity, w = total lifetime (seconds)
vec4 color; // rgba
vec4 misc; // x=size, y=random seed, z/w=unused
};
```
## Creating Particle Systems
### Via GameAPI
```cpp
#include "core/game_api.h"
GameAPI::Engine api(&engine);
// Create a particle system with 1024 particles
uint32_t systemId = api.create_particle_system(1024);
// Configure parameters
GameAPI::ParticleSystem sys = api.get_particle_system(systemId);
sys.enabled = true;
sys.reset = true; // Respawn all particles immediately
sys.blendMode = GameAPI::ParticleBlendMode::Additive;
// Emitter settings
sys.params.emitterPosLocal = glm::vec3(0.0f, 0.0f, 0.0f);
sys.params.spawnRadius = 0.1f;
sys.params.emitterDirLocal = glm::vec3(0.0f, 1.0f, 0.0f); // Upward
sys.params.coneAngleDegrees = 20.0f;
// Particle properties
sys.params.minSpeed = 2.0f;
sys.params.maxSpeed = 8.0f;
sys.params.minLife = 0.5f;
sys.params.maxLife = 1.5f;
sys.params.minSize = 0.05f;
sys.params.maxSize = 0.15f;
// Physics
sys.params.drag = 1.0f;
sys.params.gravity = 0.0f; // Positive pulls down -Y in local space
// Appearance
sys.params.color = glm::vec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange
// Flipbook animation (16×4 atlas, 30 FPS)
sys.flipbookTexture = "vfx/flame.ktx2";
sys.params.flipbookCols = 16;
sys.params.flipbookRows = 4;
sys.params.flipbookFps = 30.0f;
sys.params.flipbookIntensity = 1.0f;
// Noise distortion
sys.noiseTexture = "vfx/simplex.ktx2";
sys.params.noiseScale = 6.0f;
sys.params.noiseStrength = 0.05f;
sys.params.noiseScroll = glm::vec2(0.0f, 0.0f);
// Soft particles
sys.params.softDepthDistance = 0.15f; // Fade particles within 0.15 units of geometry
api.set_particle_system(systemId, sys);
```
### Direct API
```cpp
ParticlePass* particlePass = /* obtain from RenderPassManager */;
// Create system
uint32_t systemId = particlePass->create_system(1024);
// Access and modify
auto& systems = particlePass->systems();
for (auto& sys : systems)
{
if (sys.id == systemId)
{
sys.enabled = true;
sys.params.color = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
break;
}
}
```
## Simulation Details
### Update Pipeline (Compute)
The `particles_update.comp` shader runs once per frame for each active system:
1. **Floating-origin correction**: `p.pos_age.xyz -= origin_delta` keeps particles stable when the world origin shifts
2. **Respawn check**: Dead particles (`age <= 0`) or reset flag respawns particles with randomized properties
3. **Physics integration**:
- Apply gravity: `vel += vec3(0, -gravity, 0) * dt`
- Apply drag: `vel *= exp(-drag * dt)`
- Integrate position: `pos += vel * dt`
4. **Age decrement**: `age -= dt`
Random number generation uses a per-particle seed (`misc.y`) combined with system time to ensure deterministic but varied behavior.
### Cone Emission
When `coneAngleDegrees > 0`, particles are emitted within a cone:
- Cone axis is `emitterDirLocal`
- Particles are randomly distributed within the cone solid angle
- `coneAngleDegrees = 0` emits in a single direction
- `coneAngleDegrees < 0` emits in all directions (sphere)
### Spawn Radius
Particles spawn at `emitterPosLocal ± random_in_sphere(spawnRadius)`.
## Rendering Pipeline
### Blend Modes
**Additive** (`BlendMode::Additive`):
- Source: `VK_BLEND_FACTOR_SRC_ALPHA`
- Dest: `VK_BLEND_FACTOR_ONE`
- No depth sorting required
- Ideal for fire, sparks, energy effects
**Alpha** (`BlendMode::Alpha`):
- Source: `VK_BLEND_FACTOR_SRC_ALPHA`
- Dest: `VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA`
- Block-level depth sorting (256 particles per block)
- Better for smoke, debris, leaves
### Alpha Sorting
For alpha-blended systems:
1. **Block sorting** (`particles_sort_blocks.comp`): Divides particles into 256-particle blocks, computes average depth per block, sorts blocks back-to-front
2. **Index building** (`particles_build_indices.comp`): Writes sorted particle indices into `_draw_indices` buffer
3. **Rendering**: Vertex shader reads particles via indirection: `Particle p = pool.particles[indices[gl_InstanceIndex]]`
This provides coarse-grained sorting with minimal compute overhead (512 blocks max).
### Soft Particles
Fragment shader samples G-buffer depth (`gbufferPosition.w`) and fades particle alpha near intersections:
```glsl
float sceneDepth = texture(posTex, screenUV).w;
float particleDepth = /* compute from world pos */;
float depthDiff = sceneDepth - particleDepth;
float softFactor = smoothstep(0.0, softDepthDistance, depthDiff);
outColor.a *= softFactor;
```
Set `softDepthDistance = 0` to disable.
### Flipbook Animation
The fragment shader samples an animated sprite sheet:
1. Compute frame index: `frameIndex = int(time_sec * flipbookFps) % (flipbookCols * flipbookRows)`
2. Map frame to UV rect: `(col, row) = (frameIndex % cols, frameIndex / cols)`
3. Sample texture: `color = texture(flipbookTex, baseUV * cellSize + cellOffset)`
### Noise Distortion
Optional UV distortion using a noise texture:
```glsl
vec2 noiseUV = uv * noiseScale + noiseScroll * time_sec;
vec2 distortion = (texture(noiseTex, noiseUV).rg - 0.5) * 2.0 * noiseStrength;
vec2 finalUV = uv + distortion;
```
## Memory Management
### Particle Pool Allocation
The global pool is pre-allocated (128K particles × 64 bytes = 8 MB) and subdivided into ranges:
- `create_system(count)`: Allocates a contiguous range from `_free_ranges`
- `destroy_system(id)`: Returns range to free list and merges adjacent ranges
- `resize_system(id, new_count)`: Reallocates (may move particles)
Allocation uses a simple first-fit strategy with automatic coalescing.
### Texture Caching
VFX textures (flipbook/noise) are loaded on-demand and cached in `_vfx_textures`:
- `preload_vfx_texture(assetName)`: Explicitly load texture (safe to call from UI)
- `preload_needed_textures()`: Load all textures referenced by active systems (call before ResourceUploads pass)
- Fallback 1×1 textures (`_fallback_flipbook`, `_fallback_noise`) are used when load fails
## Render Graph Integration
The particle pass registers into the render graph after lighting and SSR:
```cpp
void ParticlePass::register_graph(RenderGraph* graph,
RGImageHandle hdrTarget,
RGImageHandle depthHandle,
RGImageHandle gbufferPosition)
{
graph->add_pass("Particles", RGPassType::Graphics,
[=](RGPassBuilder& b, EngineContext*) {
b.write_color(hdrTarget); // Composite onto HDR
b.read_depth(depthHandle); // Depth test
b.sample_image(gbufferPosition); // Soft particles
},
[this](VkCommandBuffer cmd, const RGPassResources& res, EngineContext* ctx) {
// 1. Run compute update for each system
// 2. For alpha systems: sort blocks + build indices
// 3. Render all systems (additive first, then alpha)
}
);
}
```
## Performance Considerations
- **Particle count**: 128K global limit; budget carefully across systems
- **Overdraw**: Additive blending is fill-rate intensive; keep particle size and count moderate
- **Sorting cost**: Alpha systems incur compute overhead for block sorting (~512 blocks × 256 particles)
- **Texture bandwidth**: Flipbook textures should be compressed (KTX2) and atlased (16×4 common)
- **Soft particles**: G-buffer read adds bandwidth; disable if depth fading isn't visible
## Common Presets
### Fire
```cpp
sys.blendMode = Additive;
sys.params.color = glm::vec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange
sys.params.gravity = 0.0f;
sys.params.minSpeed = 1.0f; sys.params.maxSpeed = 3.0f;
sys.params.drag = 0.5f;
sys.flipbookTexture = "vfx/flame.ktx2";
```
### Smoke
```cpp
sys.blendMode = Alpha;
sys.params.color = glm::vec4(0.3f, 0.3f, 0.3f, 0.5f); // Gray, semi-transparent
sys.params.gravity = -2.0f; // Rise upward (negative gravity)
sys.params.drag = 1.5f; // Slow down quickly
sys.params.minSpeed = 0.5f; sys.params.maxSpeed = 2.0f;
sys.noiseTexture = "vfx/simplex.ktx2";
sys.params.noiseStrength = 0.2f; // Strong distortion
```
### Sparks
```cpp
sys.blendMode = Additive;
sys.params.color = glm::vec4(1.0f, 0.8f, 0.2f, 1.0f); // Bright yellow
sys.params.gravity = 9.8f; // Fall downward
sys.params.drag = 0.1f;
sys.params.minSpeed = 5.0f; sys.params.maxSpeed = 15.0f;
sys.params.minSize = 0.01f; sys.params.maxSize = 0.03f; // Small
sys.flipbookTexture = ""; // Disable flipbook (procedural sprite)
```
## Troubleshooting
**Particles not visible**:
- Ensure `enabled = true` and `particleCount > 0`
- Check `color.a > 0` (fully transparent particles are invisible)
- Verify system is allocated: `api.get_particle_systems()` should list the ID
**Particles flickering or popping**:
- Set `reset = false` after first frame (reset respawns all particles immediately)
- Increase `minLife`/`maxLife` to prevent frequent respawning
**Performance issues**:
- Reduce total particle count (check `allocated_particles()`)
- Use additive blend for most systems (cheaper than alpha)
- Reduce flipbook texture resolution or mip levels
**Textures missing**:
- Call `preload_vfx_texture("vfx/texture.ktx2")` before first frame
- Or call `preload_needed_textures()` in engine setup
- Check AssetManager can resolve path: `assetPath("vfx/texture.ktx2")`
## API Reference
### ParticlePass
```cpp
class ParticlePass : public IRenderPass
{
// System management
uint32_t create_system(uint32_t count);
bool destroy_system(uint32_t id);
bool resize_system(uint32_t id, uint32_t new_count);
std::vector<System>& systems();
const std::vector<System>& systems() const;
// Pool stats
uint32_t allocated_particles() const;
uint32_t free_particles() const;
// Texture preloading
void preload_vfx_texture(const std::string& assetName);
void preload_needed_textures();
};
```
### GameAPI::Engine Particle Methods
```cpp
// System creation/destruction
uint32_t create_particle_system(uint32_t particle_count);
bool destroy_particle_system(uint32_t system_id);
// System control
void set_particle_system(uint32_t system_id, const ParticleSystem& sys);
ParticleSystem get_particle_system(uint32_t system_id) const;
std::vector<ParticleSystem> get_particle_systems() const;
// Pool stats
uint32_t get_particle_pool_allocated() const;
uint32_t get_particle_pool_free() const;
// Texture preloading
void preload_particle_texture(const std::string& asset_path);
```
## See Also
- `docs/RenderGraph.md` — Render graph integration details
- `docs/RenderPasses.md` — Pass execution and pipeline management
- `docs/GameAPI.md` — High-level game API
- `docs/TextureLoading.md` — Asset loading and streaming