Introduction
As I mentioned in my post on god rays, I’ve been focusing my time recently on environmental weather effects. The end goal being a weather system for my game that can dynamically transition between different weather patterns. This could be used for creating transitions in weather between different regions or just base on the time of day.
Today I want to cover an approach to adding shadows for our clouds. What about making actual clouds you might ask? Well since The Far Reaches is a top-down game, the camera is never actually pointed at the clouds.
It doesn’t really make sense to render clouds if we never see them so let’s instead focus on how they affect the world around them. The main way that clouds impact our scene is shadows. Let’s dive into a procedural approach to creating these shadows!
Cloud Silhouette
I think a reasonable start is to first create a silhouette of our clouds. We’re going to achieve this with noise. Luckily Godot provides a noise texture we can sample to create our clouds from.
Caption
Godot’s Perlin Noise
This, unfortunately, looks more like a stormy day with high cloud cover. We want to be able to control how much cloud cover there is. Since the image can be treated as black-and-white pixels in the range of 0 to 1 let’s try setting all pixels above a certain cutoff value to white and the rest to black.
Here There Be Math!
We’re going to write a function that handles sampling so we can re-use it across multiple shaders. All we need to do for now is sample the noise texture and then use the
step()
function to set all values below our cutoff to 0 and above our cutoff to 1.Eventually we’ll probably migrate some of the parameters to global uniforms.
Caption
Varied Cutoff values
Varying that cutoff makes the black take up more or less space in the texture depending. We can still improve this a little. Playing around with some parameters for the built-in noise texture gets us:
Caption
There’s definitely room for improvement in the shape of the clouds but for now we’ll make do with these shapes for our silhouette.
Sampling the Silhouette
Okay now that we have the silhouette of our shadows how do we actually check to see if a point on surface is in our shadow. Let’s first look at a two-dimensional version of this problem:
Caption
Shadow Casting
Here we see three distinct layers:
- Clouds The point above us where our clouds exist. We are treating this as a flat plane.
- Origin Where the world’s y-coordinate is 0. This could potentially be the same as whatever surface we are casting shadows on..
- Surface The surface we are trying to cast shadows on. Shown as flat for simplicity’s sake.
In order to check if a point on the surface layer is shadowed by a cloud we can cast a ray in the opposite direction of the sun direction and see where it intersects with the cloud layer. The point at which it intersects is where we want to sample. Notice that in order for this to work we define a height h above the origin layer we want our cloud layer to be.
Here There Be Math!
Identifying the point on the cloud layer we want to sample is basic linear algebra problem. Let’s model it quickly:
This equation just says that the point on the cloud layer we want is equal to the point minus some value time the the direction of the light where:
- is the resulting point on the cloud layer
- is the point on the surface we want to sampler for
- is the vector representing the direction of our light
- is some distance along the light vector
Expanding this out in three dimensions gives us the following three equations:
Now we if set our cloud layer’s height () to a constant , we can solve for . With known we can solve for our cloud layer’s other two components:
Put into code looks like:
Now that we know how to sample our clouds’ shadows at any point in the scene. We can use this function in our various shaders to actually cast shadows on our objects. The way this works is a bit in the weeds but put simply: when calculating the impact of each light on each pixel of the screen, we also take into account whether or not that pixel is in the clouds’ shadows.
Here There Be Math!
Godot provides the
ATTENUATION
parameter in thelight()
function to help tell our custom lighting code how much a light affects final color. We want to modify this value using our shadow lookup like so:Then we need only call this function before we use our attenuation in any lighting calculations.
We do something similar to the way we modulated the alpha for our god rays to get the clouds to also block our god rays.
See my post on god rays for more.
The result is pretty cool but as always, I think we can do a little better.
Caption
Cloud Shadows!
Softer Edges
Developer Disclaimer
My game’s rendering utilizes a far bit of custom lighting such as dithering between brightness levels. This means that if you’re following along, some of my rendered scenes may appear slightly different from yours.
My first complaint is the hard edges on our shadows. Let’s fix this by creating a region between our 1s and 0s in our noise texture that smoothly gradients between them.
Here There Be Math!
Doing this is fairly straightforward with the use of the the
smoothstep()
function. This operates similar to thestep()
function but instead smoothly gradients if the value is between its two cutoff values.We can determine these two cutoff values using our original cutoff and a new range parameter to determine how large of a range around our cutoff we want our gradient to be.
Now our shadows are looking a lot softer.
Caption
Softer Shadows
Roiling and Moving Clouds
Okay I really just wanted to use the word roil if I’m being honest.
What I’m referring to is the nature of clouds to slowly change shape over time. This turbulent behavior is usually caused by winds pushing on the clouds. And speaking of wind, we should let the clouds move as well.
Roiling
Let’s talk about how we can make the clouds roil (see its a really good word). This seems like it would be really hard to achieve with our two-dimensional noise texture. Good thing there’s a such as three-dimensional noise!
If two-dimensional noise changes in the x and y directions then its safe to assume that three-dimensional noise changes in the x, y and z direction. So let’s just replace our 2D noise with 3D noise and try slowly moving through our noise’s z dimension with time!
Here There Be Math!
Honestly this one is pretty straightforward. We can change our
sampler2D
to asampler3D
to sample from Godot’s NoiseTexture3D. Then just vary the sample point’s z-coordinate with time.I’ve also started defining some constants and uniforms so shaders using the
sample_cloud_shadow()
function don’t have to copy it each time. This could be parameterized easily.
This looks pretty good already!
Caption
Roiling Clouds
Moving
Moving the clouds is as easy as simply defining a direction we want the wind to be blowing our clouds and moving our noise texture’s sample point in that direction.
Here There Be Math!
This is also fairly straightforward. We can introduce a
wind_direction
vector that points in the direction the wind is blowing and who’s magnitude is the speed of the wind. The we need only multiply it by our time and add it to our sample point.
Putting everything we’ve discussed together and we get a pretty neat effect. The clouds affect both the god rays and the surfaces they pass over!
Caption
Full Cloud Shadows
Conclusion
I hope you enjoyed this short excursion into the world of procedural generation. There’s many improvements that could be done to make these clouds seem more realistic. For now I think these stylized clouds will serve The Far Reaches very well. Although in the future I may attempt to improve them.
As always please leave a comment below if anything was confusing or if I got some information wrong so I can clarify / correct any mistakes. Also feel free to let me know what you think, offer advice or tell me what topics you’d like me to try to tackle!
Take care and see you soon!
— Carson
Resources
Final Function