Sky: background, sun and environment radiance
Complete shaders, environment setup, intermediate passes and reflection updates.
A sky shader defines sky color by viewing direction. Its sky() function serves the visible background, the environment radiance map for lighting/reflections and enabled intermediate passes. Changing the radiance update policy does not disable background rendering.
Connect the sky to an environment
- Add a WorldEnvironment and assign an Environment resource.
- Set BackgroundMode = Sky and create a Sky in Environment.Sky.
- Assign a ShaderMaterial to SkyMaterial, with a sky-type Shader.
- Ensure the active Camera3D can see the background. Opaque geometry occludes it.
This complete shader identifies the direction being used: red zenith, green horizon and blue lower hemisphere. These are diagnostic colors, not an atmospheric model.
shader_type sky;
uniform vec3 zenith_color : source_color = vec3(1.0, 0.0, 0.0);
uniform vec3 horizon_color : source_color = vec3(0.0, 1.0, 0.0);
uniform vec3 ground_color : source_color = vec3(0.0, 0.0, 1.0);
void sky() {
float height = clamp(abs(EYEDIR.y) * 2.0, 0.0, 1.0);
vec3 pole = EYEDIR.y >= 0.0 ? zenith_color : ground_color;
COLOR = mix(horizon_color, pole, height);
}For the same resource setup in C#, save SkySetup.cs:
using Reckfell;
public static class SkySetup
{
public static Reckfell.Environment Create(Shader shader)
{
var material = new ShaderMaterial { Shader = shader };
var sky = new Sky
{
SkyMaterial = material,
ProcessMode = Sky.ProcessModeEnum.Quality,
RadianceSize = Sky.RadianceSizeEnum.Size256,
};
return new Reckfell.Environment
{
BackgroundMode = Reckfell.Environment.BGMode.Sky,
Sky = sky,
TonemapMode = Reckfell.Environment.ToneMapper.Linear,
};
}
}Load the shader using ResourceLoader.Load<Shader>("asset://DirectionSky.reckfellshader"), call SkySetup.Create(shader) and assign the result to WorldEnvironment.Environment. Place the WorldEnvironment in the same World3D as the camera; a SubViewport with OwnWorld3D needs its own environment. Linear tonemapping is chosen here for color checks, not as a universal art setting.
The test points the camera up/down and observes red/blue. Translating it without changing orientation does not change this shader because it uses EYEDIR, not POSITION. Change uniforms on the ShaderMaterial: material.SetShaderParameter("zenith_color", Colors.Blue). Passing default(Variant) restores the uniform initializer.
Separate background and lighting radiance
AT_CUBEMAP_PASS identifies radiance-map generation. It is false for the main background pass. You can simplify lighting relative to the visible background, but reflections and background then deliberately differ. The test writes blue into the radiance branch and red on screen; reading the generated panorama confirms the distinction.
Move an expensive static calculation into the radiance branch and read its result for the background. A complete minimal example:
shader_type sky;
void sky() {
if (AT_CUBEMAP_PASS) {
COLOR = vec3(0.0, 0.0, 1.0);
} else {
COLOR = textureLod(RADIANCE, EYEDIR, 0.0).rgb;
}
}Read RADIANCE only outside generation of the map itself: an unfinished result cannot be its own source. textureLod at level 0 explicitly selects base detail. For an image panorama, use a separate sampler2D with SKY_COORDS or PanoramaSkyMaterial; RADIANCE is the processed environment map, not the source image file.
Caching still costs a background sample each frame. Resolution and filtering limit detail; small clouds or stars may disappear. This example proves the path works, not that it accelerates every effect.
Choose an update policy
| Sky.ProcessMode | Purpose |
|---|---|
| Automatic | Renderer selects a policy from shader dependencies and settings; this does not guarantee Realtime. |
| Quality | High-quality updates when needed, intended for static or infrequently changing skies. |
| Incremental | Spreads map filtering over frames; reflection updates can complete after the visible background changes. |
| Realtime | A path for frequent changes, with resolution/filtering quality constraints. |
The current RD renderer uses size 256 for Realtime. Requesting a different RadianceSize causes a warning and internal adjustment. RadianceSize is an enum, not an arbitrary pixel count. The resource stores the requested setting, so its getter does not prove the internally adjusted size. The test uses Realtime with Size256 and also checks Quality with Size64.
Material/parameter changes, used time, camera translation when POSITION is used, used directional lights and some fog settings mark the map for update. Resizing with intermediate passes requires updating their buffers. Remove unnecessary dependencies: even TIME used in a background-only branch is detected by shader analysis. Current RD Automatic selects Realtime for TIME/POSITION at the supported size, Incremental for lights, fog sun scattering or uniform parameters, otherwise Quality; do not assume identical policies across all renderers.
TIME is renderer time affected by Engine.TimeScale, rolling over at rendering/limits/time/time_rollover_secs, default 3600 seconds. Pausing the tree alone does not stop it. For controlled animation, provide your own time and change it only when necessary.
Half- and quarter-resolution passes
use_half_res_pass enables half the width and height; use_quarter_res_pass uses one quarter of each dimension. These are approximately 1/4 and 1/16 of the corresponding full pass pixel count. Additional passes still cost work; enable them only when an appropriate calculation can move to low resolution.
This diagnostic shader writes distinct values into both passes and reads them at full resolution. Half-pass ALPHA becomes the final red channel; green and blue confirm reads from both textures:
shader_type sky;
render_mode use_half_res_pass, use_quarter_res_pass;
void sky() {
if (AT_QUARTER_RES_PASS) {
COLOR = vec3(0.0, 0.0, 1.0);
ALPHA = 0.25;
} else if (AT_HALF_RES_PASS) {
COLOR = vec3(0.0, 1.0, 0.0);
ALPHA = 0.5;
} else {
COLOR = vec3(HALF_RES_COLOR.a, HALF_RES_COLOR.g, QUARTER_RES_COLOR.b);
}
}For clouds, calculate color and coverage at low resolution and explicitly combine them with the sky using mix. Reading HALF_RES_COLOR or QUARTER_RES_COLOR does not automatically blend it. Filtering smooths sampling but cannot recover lost detail. AT_CUBEMAP_PASS can be true at the same time as an intermediate-pass flag, so order conditions deliberately.
ALPHA carries intermediate-pass data. Setting ALPHA=0 in the main sky does not reveal a transparent background. Configure a separate viewport composition path for transparent output rather than relying on sky().
Directional lights
Four slots, LIGHT0…LIGHT3, are available. Check LIGHTX_ENABLED before reading other fields. Eligible DirectionalLight3D objects populate them, not PointLight3D; a slot number is not a stable light ID. LightOnly excludes a light from the sky; SkyOnly keeps it available for the sky.
This complete example forms a light lobe toward the source. It is neither a physical atmosphere nor a disk with an exact angular diameter:
shader_type sky;
void sky() {
COLOR = vec3(0.0);
if (LIGHT0_ENABLED) {
float alignment = max(dot(EYEDIR, LIGHT0_DIRECTION), 0.0);
COLOR = LIGHT0_COLOR * LIGHT0_ENERGY * pow(alignment, 64.0);
}
}In the tested RD path, LIGHTX_DIRECTION points toward the source along the light transform's world +Z. Thus dot(EYEDIR, LIGHTX_DIRECTION) selects the view toward it; do not invert the sign based on the phrase “light direction.” Rotating DirectionalLight3D by π around Y makes the lobe visible to a camera looking along −Z. LIGHTX_COLOR is linear; ENERGY incorporates energy settings and applicable physical units/exposure. LIGHTX_SIZE is angular diameter in radians, not radius: use the cosine of half the diameter for a disk threshold.
Render modes and built-ins
| Mode | Behavior |
|---|---|
| use_half_res_pass | Enables the half-resolution pass. |
| use_quarter_res_pass | Enables the quarter-resolution pass. |
| disable_fog | Disables fog, including custom FOG in the tested RD path. |
| use_debanding | Adds noise to reduce gradient bands; it does not add detail. |
| Type and name | Meaning |
|---|---|
in float TIME | Renderer time; update dependencies are described above. |
in float PI / TAU / E | Mathematical constants. |
in vec3 POSITION | World camera position, not a sky pixel position. |
samplerCube RADIANCE | Read the completed environment map by direction outside its own generation pass. |
in bool AT_CUBEMAP_PASS | Environment map generation, including its intermediate passes. |
in bool AT_HALF_RES_PASS / AT_QUARTER_RES_PASS | Current half-/quarter-resolution pass. |
in bool LIGHTX_ENABLED | Whether slot X from 0 through 3 is available. |
in vec3 LIGHTX_DIRECTION / LIGHTX_COLOR | Direction toward the source / linear color. |
in float LIGHTX_ENERGY / LIGHTX_SIZE | Energy / angular diameter in radians. |
in vec3 EYEDIR | Normalized direction of the current ray. |
in vec2 SCREEN_UV | Current-pass coordinates, not spherical panorama UV. |
in vec4 FRAGCOORD | Fragment coordinates in the current target buffer. |
in vec2 SKY_COORDS | Spherical UV for a panorama texture. |
in vec4 HALF_RES_COLOR / QUARTER_RES_COLOR | Samples from the corresponding enabled passes: color and written ALPHA. |
out vec3 COLOR | Output linear RGB; exposure/tonemapping applies afterward. |
out float ALPHA | Intermediate-pass alpha data, not main-background transparency. |
out vec4 FOG | Custom fog RGB and blend weight in A. |
in is read-only, out is a writable result and a sampler is a texture source. Pass flags, POSITION, RADIANCE, LIGHTX and TIME are global; EYEDIR, SCREEN_UV, SKY_COORDS, FRAGCOORD and outputs belong to sky(). FOG blends RGB using A as its weight; it is not FogVolume density. Current RD disable_fog also disables this contribution.
Troubleshooting
| Symptom | Check |
|---|---|
| Black sky | BackgroundMode, Environment.Sky, SkyMaterial, shader_type sky, active camera and shared World3D. |
| Background changes before reflections | ProcessMode and completion of radiance updates/filtering. |
| Sky is too bright | Shader HDR values, environment energy, camera exposure and tonemapping. |
| ALPHA=0 does not hide the sky | Main-pass ALPHA does not control blending; configure composition separately. |
| Wrong color or lost detail after caching | Background/map branch differences, resolution, RADIANCE LOD and filtering. |
| Sun lobe appears on the opposite side | LIGHTX_DIRECTION sign, light rotation and selected slot. |
| No sun | LIGHTX_ENABLED, DirectionalLight3D.SkyMode, visibility and the four-light limit. |
| Gradient banding | Range and tonemapping; use_debanding when appropriate. |
| Realtime size warning | Use Size256 in current RD or choose another update mode. |
Actual Windows Forward+/Vulkan pixels check directions, uniforms, background/radiance separation, map sampling, intermediate passes, alpha, FOG and directional-light slots. Quality/Incremental/Realtime settings are checked with a static result, not benchmarked filtering latency or performance. Scheduling policy, remaining built-ins, environment rotation/FOV and configuration constraints are source-reviewed. No new editor capture, artistic atmosphere, physical correctness, HDR/XR or all-platform/export acceptance is claimed here.