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.