Files
QuaternionEngine/docs/Volumetrics.md

488 lines
15 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.
# Volumetric Cloud System
The volumetric system provides GPU-accelerated voxel-based rendering for clouds, smoke, and flame effects using raymarching and procedural density simulation.
## Architecture Overview
The system is implemented across multiple components:
- **CloudPass** (`src/render/passes/clouds.h/.cpp`) — Render pass managing voxel volumes, compute simulation, and raymarching
- **GameAPI** (`src/core/game_api.h`) — High-level API for configuring volumetric effects
- **Shaders**
- `shaders/cloud_voxel_advect.comp` — Voxel density simulation (advection + injection)
- `shaders/clouds.frag` — Raymarching fragment shader
## Key Features
- **Voxel-based density**: Cubic grids (4-256³ resolution) storing per-voxel density values
- **Three volume types**: Clouds (infinite XZ wrap), Smoke (localized), Flame (emissive)
- **GPU simulation**: Semi-Lagrangian advection with procedural noise injection
- **Raymarching composite**: Beer-Lambert absorption + single-scattering approximation
- **Ping-pong buffers**: Double-buffered voxel grids for temporal stability
- **Camera following**: Volumes can anchor to camera XZ (infinite clouds) or drift in world-space
- **Floating-origin stable**: Automatically adjusts volume positions when world origin shifts
- **Multi-volume support**: Up to 4 independent volumes (`MAX_VOXEL_VOLUMES = 4`)
## Volume Types
### Clouds (Type 0)
- **Behavior**: Continuous XZ wrapping for infinite cloud layers
- **Injection**: Broad slab with height-based shaping (upper/lower bounds)
- **Advection**: Wind wraps in XZ, clamped in Y
- **Typical use**: Sky clouds, atmospheric layers
### Smoke (Type 1)
- **Behavior**: Localized emission with soft dissipation
- **Injection**: Spherical emitter in UVW space with softer noise threshold
- **Advection**: Fully clamped (no wrapping)
- **Typical use**: Smoke columns, steam, fog banks
### Flame (Type 2)
- **Behavior**: Flickering emissive source with strong noise
- **Injection**: Spiky procedural noise, blends toward injected field (avoids fog accumulation)
- **Advection**: Fully clamped (no wrapping)
- **Rendering**: Adds emission term (`emissionColor × emissionStrength`)
- **Typical use**: Fires, torches, explosions
## Creating Volumetric Effects
### Via GameAPI
```cpp
#include "core/game_api.h"
GameAPI::Engine api(&engine);
// Enable volumetrics globally
api.set_volumetrics_enabled(true);
// Configure a cloud volume (index 0)
GameAPI::VoxelVolumeSettings cloud;
cloud.enabled = true;
cloud.type = GameAPI::VoxelVolumeType::Clouds;
// Position: follow camera XZ, offset in Y
cloud.followCameraXZ = true;
cloud.volumeCenterLocal = glm::vec3(0.0f, 50.0f, 0.0f); // 50 units above camera
cloud.volumeHalfExtents = glm::vec3(100.0f, 20.0f, 100.0f); // 200×40×200 box
// Animation: enable voxel advection
cloud.animateVoxels = true;
cloud.windVelocityLocal = glm::vec3(5.0f, 2.0f, 0.0f); // Drift +X, rise +Y
cloud.dissipation = 0.5f; // Slow decay
cloud.noiseStrength = 0.8f;
cloud.noiseScale = 8.0f;
cloud.noiseSpeed = 0.3f;
// Rendering
cloud.densityScale = 1.5f;
cloud.coverage = 0.3f; // Higher = less dense (threshold)
cloud.extinction = 1.0f;
cloud.stepCount = 64; // Raymarch steps (quality vs performance)
cloud.gridResolution = 64; // 64³ voxel grid
// Shading
cloud.albedo = glm::vec3(1.0f, 1.0f, 1.0f); // White clouds
cloud.scatterStrength = 1.2f;
cloud.emissionColor = glm::vec3(0.0f); // No emission
cloud.emissionStrength = 0.0f;
api.set_voxel_volume(0, cloud);
```
### Flame Effect
```cpp
GameAPI::VoxelVolumeSettings flame;
flame.enabled = true;
flame.type = GameAPI::VoxelVolumeType::Flame;
// Position: absolute world location
flame.followCameraXZ = false;
flame.volumeCenterLocal = glm::vec3(0.0f, 1.0f, 0.0f);
flame.volumeHalfExtents = glm::vec3(1.0f, 2.0f, 1.0f); // 2×4×2 box
// Animation
flame.animateVoxels = true;
flame.windVelocityLocal = glm::vec3(0.0f, 8.0f, 0.0f); // Rise upward
flame.dissipation = 2.0f; // Fast decay
flame.noiseStrength = 1.5f;
flame.noiseScale = 10.0f;
flame.noiseSpeed = 2.0f;
// Emitter in UVW space (bottom center)
flame.emitterUVW = glm::vec3(0.5f, 0.05f, 0.5f);
flame.emitterRadius = 0.2f; // 20% of volume size
// Shading
flame.densityScale = 2.0f;
flame.coverage = 0.0f;
flame.extinction = 0.8f;
flame.stepCount = 48;
flame.gridResolution = 48;
flame.albedo = glm::vec3(1.0f, 0.6f, 0.2f); // Orange scatter
flame.scatterStrength = 0.5f;
flame.emissionColor = glm::vec3(1.0f, 0.5f, 0.1f); // Orange-red glow
flame.emissionStrength = 3.0f; // Strong emission
api.set_voxel_volume(1, flame);
```
## Simulation Details
### Voxel Advection (Compute Shader)
The `cloud_voxel_advect.comp` shader updates voxel density each frame:
1. **Semi-Lagrangian advection**: Backtrace along wind velocity
```glsl
vec3 back = uvw - (windVelocityLocal / volumeSize) * dt;
```
- Clouds: Wrap XZ (`fract(back.xz)`), clamp Y
- Smoke/Flame: Clamp all axes
2. **Trilinear sampling**: Sample input density at backtraced position
```glsl
float advected = sample_density_trilinear(back, gridResolution);
```
3. **Dissipation**: Exponential decay
```glsl
advected *= exp(-dissipation * dt);
```
4. **Noise injection**: Procedural density injection using 4-octave FBM
- **Clouds**: Broad slab with height shaping
```glsl
injected = smoothstep(0.55, 0.80, fbm3(uvw * noiseScale + time * noiseSpeed));
low = smoothstep(0.0, 0.18, uvw.y);
high = 1.0 - smoothstep(0.78, 1.0, uvw.y);
injected *= low * high;
```
- **Smoke**: Spherical emitter with softer threshold
```glsl
shape = 1.0 - smoothstep(emitterRadius, emitterRadius * 1.25, distance(uvw, emitterUVW));
injected = smoothstep(0.45, 0.75, fbm3(...)) * shape;
```
- **Flame**: Spiky noise with flickering
```glsl
injected = (fbm3(...) ^ 2) * shape;
out_density = mix(advected, injected, noiseStrength * dt); // Blend toward injected
```
5. **Write output**: Write to ping-pong buffer
```glsl
vox_out.density[idx3(c, gridResolution)] = clamp(out_density, 0.0, 1.0);
```
### Raymarching (Fragment Shader)
The `clouds.frag` shader composites volumes onto the HDR buffer:
1. **Ray setup**:
- Reconstruct world-space ray from screen UV
- Define AABB from `volumeCenterLocal ± volumeHalfExtents`
- Compute ray-AABB intersection (`t0`, `t1`)
2. **Geometry clipping**:
- Sample G-buffer position (`posTex`)
- If opaque geometry exists, clamp `t1` to surface distance
- Prevents clouds rendering behind solid objects
3. **Raymarching loop**:
```glsl
float transmittance = 1.0;
vec3 scattering = vec3(0.0);
for (int i = 0; i < stepCount; ++i) {
vec3 p = camPos + rd * t;
float density = sample_voxel_density(p, bmin, bmax);
// Apply coverage threshold
density = max(density - coverage, 0.0) * densityScale;
// Beer-Lambert absorption
float extinction_coeff = density * extinction;
float step_transmittance = exp(-extinction_coeff * dt);
// In-scattering (single-scattering approximation)
vec3 light_contrib = albedo * scatterStrength * density;
// Flame emission
if (volumeType == 2) {
light_contrib += emissionColor * emissionStrength * density;
}
scattering += transmittance * (1.0 - step_transmittance) * light_contrib;
transmittance *= step_transmittance;
t += dt;
}
```
4. **Composite**:
```glsl
vec3 finalColor = baseColor * transmittance + scattering;
outColor = vec4(finalColor, 1.0);
```
### Floating-Origin Stability
When the world origin shifts (`CloudPass::update_time_and_origin_delta()`):
- Volumes with `followCameraXZ = false` are adjusted: `volumeCenterLocal -= origin_delta`
- Ensures volumes stay in the same world-space location despite coordinate changes
### Volume Drift
For non-camera-following volumes:
```cpp
volumeCenterLocal += volumeVelocityLocal * dt;
```
Allows volumes to drift independently (e.g., moving storm clouds).
## Memory Management
### Voxel Buffers
Each volume maintains two ping-pong buffers (`voxelDensity[2]`):
- **Read buffer**: Input to advection compute shader and raymarch fragment shader
- **Write buffer**: Output of advection compute shader
- Buffers swap each frame (`voxelReadIndex = 1 - voxelReadIndex`)
Buffer size: `gridResolution³ × sizeof(float)` bytes
- Example: 64³ grid = 1 MB per buffer (2 MB total per volume)
- Maximum 4 volumes = 8 MB total (at 64³ resolution)
### Lazy Allocation
Voxel buffers are allocated only when:
- `enabled = true`
- `gridResolution` changes
- Called via `rebuild_voxel_density()`
Initial density is procedurally generated using the same FBM noise as injection.
## Render Graph Integration
The cloud pass registers after lighting/SSR:
```cpp
RGImageHandle CloudPass::register_graph(RenderGraph* graph,
RGImageHandle hdrInput,
RGImageHandle gbufPos)
{
// For each enabled volume:
// 1. Optional: Add compute pass for voxel advection (if animateVoxels == true)
// 2. Add graphics pass for raymarching composite
// Passes read/write ping-pong buffers and sample G-buffer depth
// Returns final HDR image with clouds composited
}
```
**Pass structure** (per volume):
1. **VoxelUpdate** (compute, optional): Read voxel buffer → advect → write voxel buffer
2. **Volumetrics** (graphics): Read HDR input + G-buffer + voxel buffer → raymarch → write HDR output
Volumes are rendered sequentially (volume 0 → 1 → 2 → 3) to allow layered effects.
## Performance Considerations
- **Voxel resolution**: Higher resolution = better detail but 8× memory per doubling (64³ = 1 MB, 128³ = 8 MB)
- **Raymarch steps**: More steps = smoother results but linear fragment cost (48-128 typical)
- **Fill rate**: Volumetrics are fragment-shader intensive; reduce `stepCount` on low-end hardware
- **Advection cost**: Compute cost is `O(resolution³)` but typically <1ms for 64³
- **Multi-volume overhead**: Each active volume adds a full raymarch pass; budget 2-3 volumes max
### Recommended Settings
**High quality (desktop)**:
```cpp
gridResolution = 128;
stepCount = 128;
```
**Medium quality (mid-range)**:
```cpp
gridResolution = 64;
stepCount = 64;
```
**Low quality (mobile/low-end)**:
```cpp
gridResolution = 32;
stepCount = 32;
```
## Parameter Reference
### VoxelVolumeSettings
```cpp
struct VoxelVolumeSettings
{
// Enable/type
bool enabled{false};
VoxelVolumeType type{Clouds}; // Clouds, Smoke, Flame
// Positioning
bool followCameraXZ{false}; // Anchor to camera XZ
bool animateVoxels{true}; // Enable voxel simulation
glm::vec3 volumeCenterLocal{0,2,0};
glm::vec3 volumeHalfExtents{8,8,8};
glm::vec3 volumeVelocityLocal{0}; // Drift velocity (if !followCameraXZ)
// Rendering
float densityScale{1.0}; // Density multiplier
float coverage{0.0}; // 0..1 threshold (higher = less dense)
float extinction{1.0}; // Absorption coefficient
int stepCount{48}; // Raymarch steps (8-256)
uint32_t gridResolution{48}; // Voxel grid resolution (4-256)
// Simulation (advection)
glm::vec3 windVelocityLocal{0,2,0}; // Wind velocity (units/sec)
float dissipation{1.25}; // Density decay (1/sec)
float noiseStrength{1.0}; // Injection rate
float noiseScale{8.0}; // Noise frequency
float noiseSpeed{1.0}; // Time scale
// Emitter (smoke/flame only)
glm::vec3 emitterUVW{0.5,0.05,0.5}; // Normalized (0..1)
float emitterRadius{0.18}; // Normalized (0..1)
// Shading
glm::vec3 albedo{1,1,1}; // Scattering tint
float scatterStrength{1.0};
glm::vec3 emissionColor{1,0.6,0.25};// Flame emission tint
float emissionStrength{0.0}; // Flame emission strength
};
```
## Common Presets
### Stratocumulus Clouds
```cpp
cloud.type = Clouds;
cloud.followCameraXZ = true;
cloud.volumeCenterLocal = glm::vec3(0, 80, 0);
cloud.volumeHalfExtents = glm::vec3(200, 30, 200);
cloud.windVelocityLocal = glm::vec3(3, 1, 0);
cloud.dissipation = 0.3f;
cloud.densityScale = 1.2f;
cloud.coverage = 0.4f;
cloud.gridResolution = 64;
cloud.stepCount = 64;
```
### Torch Flame
```cpp
flame.type = Flame;
flame.followCameraXZ = false;
flame.volumeCenterLocal = glm::vec3(0, 1.5, 0);
flame.volumeHalfExtents = glm::vec3(0.3, 0.8, 0.3);
flame.windVelocityLocal = glm::vec3(0, 6, 0);
flame.dissipation = 2.5f;
flame.noiseStrength = 2.0f;
flame.emitterUVW = glm::vec3(0.5, 0.1, 0.5);
flame.emitterRadius = 0.25f;
flame.emissionColor = glm::vec3(1.0, 0.4, 0.1);
flame.emissionStrength = 4.0f;
flame.gridResolution = 32;
flame.stepCount = 32;
```
### Smoke Plume
```cpp
smoke.type = Smoke;
smoke.followCameraXZ = false;
smoke.volumeCenterLocal = glm::vec3(0, 2, 0);
smoke.volumeHalfExtents = glm::vec3(2, 5, 2);
smoke.windVelocityLocal = glm::vec3(1, 4, 0);
smoke.dissipation = 1.0f;
smoke.noiseStrength = 1.2f;
smoke.emitterUVW = glm::vec3(0.5, 0.05, 0.5);
smoke.emitterRadius = 0.15f;
smoke.albedo = glm::vec3(0.4, 0.4, 0.4);
smoke.scatterStrength = 0.8f;
smoke.gridResolution = 48;
smoke.stepCount = 48;
```
## Troubleshooting
**Volumes not visible**:
- Ensure `enabled = true` and `volumetrics_enabled = true` globally
- Check AABB intersects camera frustum
- Reduce `coverage` (lower = denser)
- Increase `densityScale`
**Blocky/noisy appearance**:
- Increase `gridResolution` (64 → 128)
- Increase `stepCount` (48 → 96)
- Adjust `noiseScale` for finer detail
**Performance issues**:
- Reduce `gridResolution` (64 → 32)
- Reduce `stepCount` (64 → 32)
- Disable `animateVoxels` for static volumes
- Reduce number of active volumes
**Volumes don't animate**:
- Ensure `animateVoxels = true`
- Check `windVelocityLocal` is non-zero
- Verify `noiseStrength > 0` and `noiseSpeed > 0`
**Volumes flicker/pop**:
- Increase `dissipation` to smooth density changes
- Lower `noiseStrength` for subtler injection
- Use higher `gridResolution` for temporal stability
## API Reference
### GameAPI::Engine Volumetric Methods
```cpp
// Global enable/disable
void set_volumetrics_enabled(bool enabled);
bool get_volumetrics_enabled() const;
// Volume configuration (index 0-3)
void set_voxel_volume(int index, const VoxelVolumeSettings& settings);
VoxelVolumeSettings get_voxel_volume(int index) const;
// Retrieve all volumes
std::vector<VoxelVolumeSettings> get_voxel_volumes() const;
```
### CloudPass
```cpp
class CloudPass : public IRenderPass
{
// Render graph registration
RGImageHandle register_graph(RenderGraph* graph,
RGImageHandle hdrInput,
RGImageHandle gbufPos);
// Internal voxel management
void rebuild_voxel_density(uint32_t volume_index,
uint32_t resolution,
const VoxelVolumeSettings& settings);
};
```
## See Also
- `docs/ParticleSystem.md` — GPU particle system documentation
- `docs/RenderGraph.md` — Render graph integration details
- `docs/RenderPasses.md` — Pass execution and pipeline management
- `docs/GameAPI.md` — High-level game API
- `docs/Compute.md` — Compute pipeline details