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: