Sampling Environment Maps with DXR Miss Shaders

In DirectX Raytracing (DXR), rays that fail to intersect any geometric primitive in the Top-Level Acceleration Structure invoke the miss shader. This article explains how the miss shader intercepts these missed rays, extracts their world-space direction vectors, and converts that data to sample equirectangular environment textures, sample cubemaps, or calculate mathematical procedural sky gradients directly onto the ray payload.

The Role of the Miss Shader

When a ray is dispatched using the TraceRay() intrinsic function in High-Level Shading Language (HLSL), the hardware traverses the acceleration structure. If the ray does not hit an instance bounding box or primitive within its valid parametric range (\(T_{min}\) to \(T_{max}\)), traversal terminates, and the pipeline executes the miss shader specified by the MissShaderIndex argument.

The primary task of the miss shader is to resolve the background radiance—typically an ambient color, a static skybox, an image-based lighting (IBL) environment map, or a dynamic procedural atmosphere—and write that value back to the ray payload.

Retrieving Ray Direction

To sample any directional background, the miss shader needs to determine the orientation of the missed ray. DXR provides built-in intrinsic functions to inspect ray properties. The miss shader retrieves the normalized world-space direction vector using:

float3 rayDir = WorldRayDirection();

Because environment maps represent infinitely distant backgrounds, the ray's origin (WorldRayOrigin()) is generally ignored unless volumetric atmospheric effects or local spherical projections are being calculated.

Sampling Texture-Based Environment Maps

Environment textures are typically stored as either Cubemaps (TextureCube) or Equirectangular (Latitude-Longitude) maps (Texture2D).

1. Cubemap Sampling

Cubemaps represent the simplest implementation because hardware samplers directly accept 3D directional vectors:

TextureCube<float4> g_EnvironmentCubemap : register(t0);
SamplerState g_LinearSampler : register(s0);

[shader("miss")]
void MissShader(inout RayPayload payload)
{
    float3 dir = WorldRayDirection();
    payload.radiance = g_EnvironmentCubemap.SampleLevel(g_LinearSampler, dir, 0).rgb;
}

Note on Texture Filtering: Miss shaders are executed per-ray rather than in conventional \(2 \times 2\) pixel quads. Because screen-space derivatives (\(ddx\), \(ddy\)) are undefined during ray tracing, standard Sample() calls cannot automatically determine mip levels. Developers must use SampleLevel() with an explicit LOD, or calculate anisotropic footprints manually using ray cone techniques.

2. Equirectangular (Spherical) Mapping

If the environment map is stored as a 2D panorama, the 3D direction vector must be transformed into 2D normalized UV coordinates via spherical trigonometry:

\[u = 0.5 + \frac{\text{atan2}(z, x)}{2\pi}\] \[v = 0.5 - \frac{\arcsin(y)}{\pi}\]

In HLSL:

Texture2D<float4> g_EquirectangularMap : register(t0);
SamplerState g_LinearSampler : register(s0);

#define PI 3.14159265359

[shader("miss")]
void MissShader(inout RayPayload payload)
{
    float3 dir = normalize(WorldRayDirection());

    float2 uv;
    uv.x = 0.5f + (atan2(dir.z, dir.x) / (2.0f * PI));
    uv.y = 0.5f - (asin(dir.y) / PI);

    payload.radiance = g_EquirectangularMap.SampleLevel(g_LinearSampler, uv, 0).rgb;
}

Sampling Procedural Sky Domes

Rather than reading high-resolution textures from memory, miss shaders can calculate sky radiance analytically. This reduces memory bandwidth and allows for dynamic changes such as day-night cycles and weather adjustments.

Simple Directional Gradients

Basic procedural skies blend between horizon and zenith colors using the vertical component (\(y\)) of the direction vector:

cbuffer SkyData : register(b0)
{
    float3 g_ZenithColor;
    float3 g_HorizonColor;
    float3 g_SunDirection;
    float3 g_SunColor;
};

[shader("miss")]
void MissShader(inout RayPayload payload)
{
    float3 dir = WorldRayDirection();
    
    // Smoothstep gradient based on elevation angle
    float elevation = saturate(dir.y);
    float3 sky = lerp(g_HorizonColor, g_ZenithColor, elevation);

    // Analytic sun disc highlight
    float sunDot = dot(dir, normalize(g_SunDirection));
    if (sunDot > 0.9995f) 
    {
        sky += g_SunColor * 10.0f;
    }

    payload.radiance = sky;
}

Atmospheric Scattering Models

For higher physical accuracy, the miss shader can implement models like Preetham or Bruneton. These algorithms evaluate Rayleigh scattering (molecules causing blue daylight) and Mie scattering (particulates causing haze and glare around the sun) on the fly, using dot(WorldRayDirection(), g_SunDirection) as the phase angle input.

Updating the Ray Payload

Once the radiance is either sampled from a texture or mathematically evaluated, the result is assigned directly to the user-defined inout RayPayload structure. The miss shader returns, and control yields back to the original caller (such as a ray generation or closest hit shader), where the sampled radiance is integrated into the final lighting calculation.