Shader
Read the HLSL behind the panorama, sun, and three cloud layers.
Read the shader from start to finish
Start with 01-Directions.hlsl in the archive’s Checkpoints folder. Compare each file with the next, then follow 04-CloudVolume.hlsl through the walkthrough below. In TiXL, select the CustomPixelShader node and click Edit beside Template File to open the active shader. In the finished project, double-click Cloud Sky and then Sky Panorama to reach the shader node. Its file is Assets/weather/volume/CloudVolume.hlsl.
Download the four checkpoints and reference project. See Build From Scratch for the operator wiring and values.
A. What runs for each pixel
TiXL draws a full-screen pass into the 2:1 texture. The vertex pass gives psMain the current pixel’s texture coordinate (UV) and its screen position. The pixel shader runs once for each panorama texel. It uses UV to find the direction to look, evaluates the clear sky in that direction, then samples clouds along that same ray. The output is a linear HDR color, which can contain values brighter than 1.
The four files give you useful stopping points.
| Template | What psMain returns | What to look for |
|---|---|---|
| 01-Directions.hlsl | World direction encoded as RGB | The middle row is the horizon. The left and right edges meet when wrapped. |
| 02-SkyAndSun.hlsl | Atmosphere plus the HDR sun and halo | The sun moves and the horizon color changes as you vary the hour. |
| 03-Cumulus.hlsl | Cumulus light plus the sky seen through it | Cover changes how much of the sky holds clouds. |
| 04-CloudVolume.hlsl | Light from three shells plus the sky behind them | Each blend can isolate its own shape and height. |
B. Where the shader gets its inputs
A constant buffer is a block of small values that every pixel can read. A texture is an image the shader can sample at different coordinates. In the build, SrvFromTexture2d exposes a texture through a shader resource view (SRV), which lets the pixel shader read it. In HLSL, registers named b hold buffers and registers named t hold textures. A resource’s register number is its binding slot, so the graph wiring and the HLSL declarations have to agree.
| HLSL binding | TiXL source | What the shader reads |
|---|---|---|
| b0 · CloudMix | CustomPixelShader built-in fields | Offset is wind travel. A is cover, B is cumulus, C is stratus, and D is cirrus. |
| b2 · CloudView | FloatsToBuffer → Constant Buffers | Twelve floats in the order shown in Build From Scratch. HLSL reads them as three groups of four. |
| t0 · SkyGradient | Image A | Daylight colors from zenith to nadir. |
| t1 · NoiseAtlas | Image B | Repeating 3D detail for stratus and cumulus, stored as a grid of 2D slices. |
| t3 · WeatherMap | First extra Shader Resources connection | Large cloud shapes in red for cumulus and green for stratus. Blue varies the broad coverage of cirrus. |
| s1 · LinearClamp | Stock shader sampler | Interpolates adjacent texels and clamps the lookup at the texture edges. |
The first four floats of CloudView are sun yaw, sun path height, cloud scale, and sun radiance. The next four are the cloud tint’s RGBA channels. The last four are the three base heights and the time of day. The order is why moving a Params cable can make a sun control change the wrong quantity.
C. Turn UV into a ray with DirectionFromUv
U runs across the texture from 0 to 1. Multiply it by 2π to get azimuth, a full turn around the vertical axis. V runs from the top to the bottom. Multiply it by π to get the polar angle, which starts straight up and ends straight down.
Y is up. At V = 0 the ray points straight up. At V = 0.5 its Y component is zero, so the ray points along the horizon. At V = 1 it points straight down. The horizontal components shrink toward the poles because they are multiplied by the sine of the polar angle. U = 0 and U = 1 give the same direction, which is how the panorama wraps.
The first checkpoint maps each component of the ray from −1…1 to 0…1 with ray * 0.5 + 0.5. The false colors let you check direction mapping before adding any weather.
D. Build the atmosphere and sun
SolarDirection subtracts six hours and converts hours into radians. At hour 6 the solar phase is zero and the sun crosses the horizon. At hour 12 its vertical component reaches its peak. At hour 18 it crosses the horizon again. Sun Position X rotates that path around world Y. Sun Position Y changes its peak height.
Atmosphere converts the ray’s Y component back to a polar angle using acos, divides by π, and samples the gradient at that vertical coordinate. A daylight weight based on the sun’s height blends between the daylight palette and the darker sky. A twilight weight grows while the sun is near the horizon. The exponential horizon falloff keeps the warmest color close to that edge, and a dot product in the horizontal plane brightens the part of the horizon facing the sun. The hour also selects different color mixes for morning and evening.
SunLight uses dot(ray, sun). It is 1 when the ray looks directly at the sun. The chord distance sqrt(2 − 2 * cosine) is then zero at the disc center. A narrow smoothstep softens the edge. Two exponential falloffs spread the halo over a wider angular range. psMain fades the disc and halo as the sun sinks below the horizon. The shader halo lives in the panorama. Bloom adds a second, display-space glow after the camera render. Keeping the intermediate textures in float format preserves the brightness needed by both Bloom and SetEnvironment.
Try it. On checkpoint 2, move the hour from 16 toward 18. Then restore 16 and change radiance to zero. The disc and halo disappear, while the background gradient still follows the solar height.
E. Find the part of the ray that passes through clouds
A layer occupies the space between two spherical shells. The inner shell is the layer’s base and the outer shell is its top. ShellExit solves where the ray crosses each shell. IntegrateLayer takes the interval between those two distances, then caps the far end at the layer’s draw distance. A ray looking straight up crosses a short interval. A ray grazing the horizon travels much farther before reaching the same altitude. Using curved shells and distance limits keeps those rays bounded.
| Layer | Tutorial base | Thickness in the shader | Maximum steps |
|---|---|---|---|
| Stratus | 0.65 km | 0.38 km | 36 |
| Cumulus | 1.90 km | 1.08 km | 64 |
| Cirrus | 7.60 km | 0.54 km | 32 |
psMain marches low stratus first, then cumulus and cirrus. It enforces a minimum gap between layers if the exposed heights cross. The step size grows when needed to fit the interval inside the sample budget. A stable offset derived from the panorama pixel shifts each sample away from the center of its step. This breaks up regular bands while keeping the pattern steady between frames.
F. Decide how much cloud is at one sample
- Position and wind. CloudDensity gets a position in kilometres. Its lower-layer path subtracts wind travel from XZ and stretches the coordinates horizontally. Wind is scaled by 0.72 for stratus and 1 for cumulus. The separate cirrus path uses 1.68.
- Noise atlas. VolumeNoise wraps the 3D coordinate with frac and scales it to a 64-cell grid. AtlasSlice maps the integer Z index to one tile in the 8 × 8 atlas. Each 64 × 64 tile has a one-pixel gutter, so the tile stride is 66. The sampler interpolates inside the tile in XY. The code interpolates between two neighboring tiles along Z, wrapping slice 63 back to 0. This gives a continuous volume from a 2D texture.
- Weather map. For stratus and cumulus, the shader folds the horizontal sample coordinates into the texture and looks up green or red. The map gives the lower clouds their broad shapes.
- Height and erosion. A smooth height envelope fades the density in at the base and out at the top. The coverage threshold decides how much of the body survives. Fine noise removes material from edges. For long grazing steps, the shader fades fine detail toward its midpoint so the step samples a steadier density near the horizon.
Cirrus takes a separate path. Follow CirrusDensity in checkpoint 4. First it subtracts wind travel, then rotates into a fixed basis that sets the general direction of the wisps. CirrusHash assigns each world-space cell a stable value. CirrusNoise blends the four neighboring values so the field stays smooth across cell edges. This creates a continuous density field in world space.
A low-frequency sample bends the field across the wind direction. A second sample creates broad cloud patches. A third changes faster across the wind than along it, forming filaments. The shader fades the finer detail when the ray takes long steps. The blue channel modulates coverage gently through a bounded lookup. The shell still has thickness, and the height envelope tapers the sample density at its base and top. Lower extinction and an earlier distance fade keep the high layer translucent.
Try it. With only cumulus enabled, increase A to lower the coverage threshold and let more areas form clouds. Change B to scale how strongly that layer contributes. The three layer blends are independent. Setting one to 1 leaves the other two at whatever values you entered.
G. Light the sample and composite the ray
For each visible sample, IntegrateLayer takes one more density reading a short distance toward the sun. A dense sunward sample reduces direct light, which darkens interiors. Ambient fill lights the rest. A forward glow brightens samples near the line of sight to the sun. Distant samples blend toward the atmospheric haze.
The code accumulates light from the nearest sample to the farthest. Transmittance is the fraction of light that can still get through the samples already processed.
The first line converts the segment’s optical depth into an opacity. Doubling the distance through the same density makes the segment block more light. The next line adds the light from that segment after accounting for clouds in front of it. The last line reduces the fraction available to the remaining samples. If the first segment has opacity 0.25, it contributes 25% of its own color and leaves 75% transmittance. If the next segment has opacity 0.5, it contributes 37.5% of its color and leaves 37.5% for the sky.
The same light and transmittance values carry across all three layers. The loop can stop when almost no light remains. psMain finishes with light + transmittance * sky. Its output alpha is 1 because the clouds and sky have already been composited inside the shader. The HDR result goes to the visible dome and the environment lighting branch.
