Fog: density, SDF and volumetric fog
Complete shaders, FogVolume setup, coordinates, emission and boundary troubleshooting.
A fog shader defines a FogVolume contribution to volumetric fog. It runs fog() over volumetric-grid cells rather than drawing a transparent surface. Current FogVolume rendering requires Forward+ and Environment.VolumetricFogEnabled. Ordinary Environment.FogEnabled is a separate effect, not a replacement.
First working volume
- Use Forward+ and an active Camera3D.
- Assign an Environment to WorldEnvironment and enable VolumetricFogEnabled. Keep VolumetricFogDensity = 0 when testing a local volume.
- Place a FogVolume in front of the camera, give it a nonzero Size and a ShaderMaterial.
- Assign a Shader containing the complete code below.
This example emits green so it is visible without lights. It is a diagnostic material; for ordinarily lit fog, choose a suitable ALBEDO and remove EMISSION.
shader_type fog;
uniform float density = 0.4;
uniform vec3 glow : source_color = vec3(0.0, 1.0, 0.0);
void fog() {
DENSITY = density;
ALBEDO = vec3(0.0);
EMISSION = glow;
}The complete FogSetup.cs helper creates the checked environment and volume:
using Reckfell;
public static class FogSetup
{
public static Reckfell.Environment CreateEnvironment()
{
return new Reckfell.Environment
{
BackgroundMode = Reckfell.Environment.BGMode.Color,
BackgroundColor = Colors.Black,
TonemapMode = Reckfell.Environment.ToneMapper.Linear,
FogEnabled = false,
VolumetricFogEnabled = true,
VolumetricFogDensity = 0,
VolumetricFogLength = 12,
VolumetricFogTemporalReprojectionEnabled = false,
};
}
public static FogVolume CreateVolume(Shader shader)
{
return new FogVolume
{
Position = new Vector3(0, 0, -4),
Size = new Vector3(4, 4, 4),
Shape = RenderingServer.FogVolumeShape.Box,
Material = new ShaderMaterial { Shader = shader },
};
}
}Assign CreateEnvironment() to WorldEnvironment.Environment. Load the shader with ResourceLoader.Load<Shader>("asset://LocalFog.reckfellshader"), pass it to CreateVolume() and add the volume to the tree. Environment, volume and camera must belong to the same World3D. The helper places the volume at (0,0,-4) for a camera at the origin looking along −Z.
Temporal reprojection is disabled here to inspect individual changes without frame history. In a finished scene it helps reduce noise, but moving-volume trails need inspection. Linear tonemapping and diagnostic emission are not an art-direction recommendation.
Clearing Material does not disable FogVolume: current RD uses a default material with density 1. Hide the volume with Visible=false; a custom ShaderMaterial can also use zero density. The Material getter still returns null rather than the internal renderer material.
Density, color and emission
DENSITY is medium density, not transparent-mesh alpha. Positive values add density, zero removes this shader's contribution, and negative values subtract overlapping density. Explicitly assign DENSITY: an EMISSION-only shader adds nothing in the tested RD path.
ALBEDO controls scattering of incoming light. Black ALBEDO without EMISSION produces an absorbing volume; colored ALBEDO needs lighting to become visible. EMISSION adds the medium's own luminous color, but does not create a Light3D for neighboring models.
Negative density does not erase another volume's emission. RD accumulates density and emission separately. The test shows that a negative volume removes absorption against a white background, while another positive volume's emission remains. For a hole without glow, apply the intended mask to both DENSITY and EMISSION in the original shader or restructure the volumes.
Internal accumulation is quantized and bounded; current RD scales each emission contribution by clamped positive density and clamps it before packing. Extreme values do not replace correct density, lighting and exposure. Overlapping volumes accumulate contributions rather than drawing in transparent-polygon order.
SDF is a scalar and makes you responsible for the boundary
SDF is float: negative inside the shape, positive outside. It is not vec3. The value is calculated in the volume shape's space. Fog shaders run over cells in the bounding AABB, so AABB overlap alone does not reproduce the exact shape.
When the shader does not reference SDF, current RD applies the selected shape mask automatically. Referencing SDF disables that automatic mask: your shader must constrain DENSITY itself. Otherwise an ellipsoid can fill more of its rectangular AABB.
A complete soft interior boundary for a local shape:
shader_type fog;
uniform float density = 0.4;
uniform float edge_width = 0.3;
void fog() {
float weight = 1.0 - smoothstep(-max(edge_width, 0.001), 0.0, SDF);
DENSITY = density * weight;
ALBEDO = vec3(0.0);
EMISSION = vec3(0.0, 1.0, 0.0);
}edge_width uses the same space as SDF; max prevents zero-width smoothstep. This example is checked with Ellipsoid. Do not blindly apply it to World: that global shape has no equivalent local surface, so use an explicit world-space mask.
Local versus world coordinates
For local shapes, UVW contains normalized volume coordinates following its transform. WORLD_POSITION is the world cell position; OBJECT_POSITION is the volume center in world space. Subtracting WORLD_POSITION − OBJECT_POSITION removes translation, not rotation or scale.
This complete shader compares local division with world X:
shader_type fog;
uniform bool world_space = false;
void fog() {
float side = world_space ? WORLD_POSITION.x : UVW.x - 0.5;
DENSITY = 0.4;
ALBEDO = vec3(0.0);
EMISSION = side >= 0.0 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 0.0, 1.0);
}With world_space=false, rotating FogVolume by π around Y swaps its red and blue halves. With true, the boundary stays at world X=0. SIZE is the full local shape size, neither half-extents nor volumetric-grid resolution.
For World shape, UVW belongs to the camera's volumetric grid rather than stable local-volume texture coordinates. Use WORLD_POSITION with an explicit scale for stationary world noise, or UVW for a local 3D texture. A density texture must be Texture3D; its red channel can multiply DENSITY.
Global height fog
Select FogVolume.Shape = World for this complete example. Local Size does not constrain the global shape. Density is constant below base_height and decreases exponentially above it:
shader_type fog;
uniform float density = 0.2;
uniform float base_height = 0.0;
uniform float height_falloff = 1.0;
void fog() {
float height = max(WORLD_POSITION.y - base_height, 0.0);
DENSITY = density * exp(-max(height_falloff, 0.0) * height);
ALBEDO = vec3(0.0);
EMISSION = vec3(0.0, 0.0, 1.0);
}height_falloff acts along world Y regardless of local-volume rotation. This deliberately differs from built-in FogMaterial attenuation relative to OBJECT_POSITION.y. Zero disables height attenuation; this example clamps negative falloff to zero.
Built-ins
TIME and mathematical constants are global; the remaining entries belong to fog(). in is read-only and out is a writable result. Fog has no CanvasItem blend modes.
| Type and name | Meaning |
|---|---|
in float TIME | Renderer time affected by Engine.TimeScale; tree pause alone does not stop it. Rollover is rendering/limits/time/time_rollover_secs, default 3600 seconds. |
in float PI / TAU / E | Mathematical constants. |
in vec3 WORLD_POSITION | World position of the processed cell. |
in vec3 OBJECT_POSITION | World position of the FogVolume center. |
in vec3 UVW | Normalized local-shape coordinates; camera-grid coordinates for World. |
in vec3 SIZE | Full local-shape size; World does not use that local boundary. |
in float SDF | Signed shape distance; referencing it disables the automatic RD shape mask. |
out float DENSITY | Density contribution, including negative subtraction. Assign explicitly. |
out vec3 ALBEDO | Scattering color, effective with incoming light. |
out vec3 EMISSION | Medium self-emission, not a separate Light3D. |
Detail and built-in FogMaterial
A froxel is a camera volumetric-grid cell, not a screen pixel. Sub-grid detail is lost and camera movement changes sampling. Environment.VolumetricFogLength sets coverage: greater range with the same cell count reduces depth detail. Project settings rendering/environment/volumetric_fog/volume_size and volume_depth increase resolution and cost. Set necessary coverage and feature size first, then tune the grid; test-scene numbers are not a ready-made quality preset.
Use FogMaterial when no custom algorithm is needed. It already multiplies Density by height attenuation, the DensityTexture red channel and edge falloff. HeightFalloff and EdgeFade are nonnegative but not capped at one; Density can be negative. A null texture uses the built-in sampler's white fallback. Editing a FogMaterial field does not change a similarly named uniform on an arbitrary ShaderMaterial.
Troubleshooting
| Symptom | Check |
|---|---|
| Invisible volume | Forward+, VolumetricFogEnabled, shared World3D, visibility, fog-type material, position and nonzero Size. |
| Black ALBEDO result | Lighting, density and absorption; temporarily compare with a small EMISSION for diagnosis. |
| EMISSION has no effect | Explicit DENSITY assignment and a nonzero contribution. |
| Shape becomes rectangular after using SDF | Apply your own DENSITY mask from SDF. |
| Negative volume leaves a glow | It subtracts density without guaranteeing subtraction of other emission. |
| Noise rotates with the volume | Local UVW is in use; choose WORLD_POSITION for world alignment. |
| Grainy or banded fog | Feature scale, range, grid resolution and reprojection. |
| Motion leaves trails | Temporal history; compare with reprojection disabled. |
| World fills the scene despite Size | Global shape ignores the local Size boundary; use a world-coordinate mask. |
Four complete shaders and the helper are checked on Windows Forward+/Vulkan: color, omitted DENSITY, zero density, negative overlap with absorption/emission, SDF, rotated coordinates, global height and environment enablement. These are functional pixel readbacks, not published beauty illustrations. TIME, Texture3D, all light/GI combinations, temporal motion, other platforms/exports and performance are not separately accepted in full here; the corresponding contracts are source-reviewed.