Previous Post

Forest - A Single Frame

Next Post
Forest - A Single Frame
1 / 4
DESCRIPTION

This post is a continuation of the last Graphics Update post, and gives a quick overview of each stage of the rendering process.

I'm also writing more in-depth posts about each stage, so there's more detail to come!

Shadows

Shadows are rendered first into three separate textures/cascades:

the first cascade is for close-up geometry

the second cascade is for near-far geometry

the third cascade encompasses the entire visible world

The most difficult part was figuring out the camera angles and orthographic projections for each of these cascades. Math isn't my strong point and it has taken me years to fully understand this logic. I'll write a follow-up post for shadows soon.

The shadow map stores how far away each pixel is from the sun, with darker pixels representing closer/smaller values.

To figure out if a pixel is in shadow, we check if it's further away from the sun than what's stored in the shadow texture.

To create smooth shadows, we check 16 times in a small radius around each pixel and average the result. Here's a comparison of 1 vs 16 samples:

Torches only light up nearby geometry, so they don't need cascades. Instead, they render the scene from 6 directions (left, right, up, down, forward, back) and save the result of each to a face of a cube:

Then we can use the direction from each pixel to the light to figure out which face of the cube to sample from.

Main Scene

The main scene is rendered with forward shading, meaning all expensive lighting/shadows/fog/etc are applied upfront. To reduce waste we need to render from front-to-back, so that close-up objects aren't covering far-away objects that we just spent time shading.

It's not perfect, but I'm rendering the scene in this order:

Animated models

Static models (buildings, trees)

Dynamic models (falling items, rolling logs)

Particles (dust, smoke, rain)

Grass

Leaves

Terrain

Skybox

Water Reflection

The world is rendered again, but this time from underneath the water looking up.

The angle of the underwater camera is the opposite of the main view's camera - e.g. if you're looking directly down, the water will reflect the skybox directly above you).

Rendering the entire world again is slow, so we skip shadows and terrain, and render it at half resolution. Since the reflection is distorted, the half resolution isn't noticeable.

This is saved to a texture and used for reflections in the next step.

Water

Water is rendered as a plane in the center of the map, reflecting above-ground geometry and distorting underwater geometry. It's also affected by shadows and fog.

There's a lot to talk about here - reflections, distortion, caustics, murkiness, blending, displacement, fresnel - all of which will be covered in the next post.

Crepuscular Rays

Sun rays are rendered as 2D planes placed on the edge of shadows. It's using the same technique in this video, but with a compute shader doing the shadow test instead of a readback shader.

Bloom

Bloom is achieved by copying the brightest parts of the scene to another framebuffer, then downsampling, blurring and upscaling it, then adding it back onto the scene at 15% opacity.

This mainly affects sun reflections and the skybox:

Composition

The scene is rendered to a 16-bit RGB framebuffer, so we need to squash the colours down to 8-bit RGB for display on the monitor.

To avoid colour banding I use a dither shader to add subtle noise to the 16-bit colour before converting it to 8-bit - I wrote another post about this here.

I also apply ACES tonemapping, which adjusts the hue/saturation of light/dark areas to give it a nicer look. Without it, the colours seem harsher (e.g. sand is very yellow):

Next Post

That's all for this post, thanks for reading! High quality images and videos are available here.

The next post is about water - reflections, displacement, caustics and more. Here's a preview:

vercidium PATREON 2 favs
VIEWS1
FILES4 files
POSTEDFeb 27, 2026
ARCHIVEDFeb 27, 2026