ADD: Docs and shader optim

This commit is contained in:
2025-12-25 22:09:02 +09:00
parent 0172996e12
commit d6216b20fc
16 changed files with 1178 additions and 60 deletions

366
docs/ParticleSystem.md Normal file
View File

@@ -0,0 +1,366 @@
# 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