488 lines
15 KiB
Markdown
488 lines
15 KiB
Markdown
# 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
|