ADD: Docs and shader optim
This commit is contained in:
487
docs/Volumetrics.md
Normal file
487
docs/Volumetrics.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user