Rain And Snow Effect With Geometry Shaders In Unity

Andres Gomez
13 min readJul 24, 2020

The completed source code for this tutorial can be found here.

Here’s the end result:

I love rainy weather, and it helps me feel immersed in game worlds when i can experience a wet rain forest or snowy mountain tops.

In order to achieve this in my own environments I’ve often tried going the route of using particle systems to simulate precipitation. Unfortunately the heavier you want the rain or snow, the slower your computer chugs (especially on low end machines).

GEOMETRY SHADERS

Geometry shaders work by procedurally generating vertices on the GPU side before sending them to the fragment shader to fill in.

This is generally used in situations like rendering grass. The geometry shader takes in a mesh whose vertices are the points at which each blade of grass will be, and after the vertex pass in the shader, it builds the actual quads to be drawn at each vertex point.

This eliminates the bottleneck of sending each quad from CPU to GPU, transferring most of the work to the GPU side. This also makes it so all blades of grass are rendered in only one draw call, as opposed to one draw call for each blade of grass (in the worst case scenario).

We’ll use this same concept to render precipitation in a game world. This way we can batch every single rain drop / snowflake into one draw call and ease the workload on our CPU.

PROJECT SETUP

First lets start by creating a new Unity project. I’m using Unity Engine 2019.4.1f1, but any version should work.

Note: If you’re developing on OSX, you’ll have to go into the Player Settings in Unity and Disable ‘Metal’ or remove it from the list of rendering APIs. Otherwise geometry shaders won’t be supported.

I created a new scene and deleted everything except the main camera (this effect doesn’t need lighting). I also changed the skybox to a stormy sky, but that’s not necessary for the tutorial

If you want to see the effect rendered fully with the skybox in the scene view without having to enter play mode, make sure you enable the toggle that says “Toggle skybox and animated effects”

Next we’ll import the 2 texture assets we’ll be using, you can find them here.

The “Noise” and “Precipitation” textures.

Next we’ll want to write a script to be able to move the camera around easily so we can debug what the effect looks like with a non-static camera.

I’ve prepared a script below that will let us do so.

You can move the camera around with the W,A,S,D keys and look around with the arrow keys ( you can easily change this to look with the mouse values, but i find it simpler to just use the keyboard ).

Attach this script to the main camera game object that comes with the scene. Then you can move it around when you enter play mode.

Great! Now we’re ready to start working on the effect!

GRID SYSTEM

In order to save on performance, we only want to render the effect around the camera to within a certain distance. It could be raining all over the game “world”, but we only need to show it where the camera is.

One way we could implement this is to have the effect actually follow the camera around.

This wouldn’t look too good though, as when the camera started moving, it would be obvious as to what is happening, the fact that you could never “reach” the rain drops or snowflakes in front of you would look very uncanny. Being able to move “within” the field of precipitation will make it feel like it’s part of the actual world rather than an effect just added to screen space.

we can do this by implementing a grid system in the world, where we render the rainfall in only the grid coordinate the player is currently in:

Top-down view:

=================================
| | | | |
| 0,2 | 1,2 | 2,2 | 3,2 |
| | | | |
| | | | |
=================================
| |-------| | |
| 0,1 |---P---| 2,1 | 3,1 |
| |-------| | |
| |-------| | |
=================================
| | | | |
| 0,0 | 1,0 | 2,0 | 3,0 |
| | | | |
| | | | |
=================================

This way the rain is coming from a static place, and the player can freely move around within that spot.

This presents a problem when the player is on the edge of a grid though:

=================================
| | | | |
| 0,2 | 1,2 | 2,2 | 3,2 |
| | | | |
| | | | |
=================================
| |-------| | |
| 0,1 |-------P 2,1 | 3,1 |
| |-------| | |
| |-------| | |
=================================
| | | | |
| 0,0 | 1,0 | 2,0 | 3,0 |
| | | | |
| | | | |
=================================

If the player looks towards coordinate [2, 1], they’ll see a clear divide between where the rain ends and stops. That’s no good!

To fix this we can extend this system to render rain in the grids that are also 1 grid away from the player’s current grid coordinate.

We wind up rendering the effect statically in a 3 x 3 grid around the player. This makes it so the player can never be on the “edge” of the effect.

=================================
|-------|-------|-------| |
| 0,2 | 1,2 | 2,2 | 3,2 |
|-------|-------|-------| |
|-------|-------|-------| |
=================================
|-------|-------|-------| |
| 0,1 |-------P 2,1 | 3,1 |
|-------|-------|-------| |
|-------|-------|-------| |
=================================
|-------|-------|-------| |
| 0,0 | 1,0 | 2,0 | 3,0 |
|-------|-------|-------| |
|-------|-------|-------| |
=================================

We also want to extend this system vertically as well, for the same reason as before. We only want to render the rain from its start point slightly above the player, to a point below the player, but only as far as the effect is “effective”.

Without this, if the camera moves up or down enough we can reach the edge of where the effect “starts” or where it stops “falling”.

Lets’s implement this system in a script called GridHandler.cs and visualize it with the on OnDrawGizmos method:

Create this GridHandler component script and attach it to a new game object called PrecipitationSystem. Then assign the MainCamera object to the Player Transform field in the editor.

We can see that we track what “grid” the player is currently in (Green), as well as the grid’s immediate neighbors (Red) .

we can use this information to know where to render the precipitation effect.

MESH CREATION

We need to create the mesh as the base to render, so let’s create a script called PrecipitationManager.cs and attach it to the PrecipitationSystem game object.

So far, the script only has the capabilities of creating the mesh, and contains a custom editor at the bottom that renders a button that can be clicked in the editor to recreate the mesh when any updates to the subdivisions has been made.

It also subscribes itself to the attached GridHandler’s on grid change callback, with a method OnPlayerGridChange: This is where we’ll update the position of the precipitation in the near future.

Note how the UVs created are Vector3’s, when normally UVs are 2 component vectors. This is because we’re going to store another value in there later.

RENDERING:

Let’s work on getting something to actually render. For this step we’ll need to create two shaders (Rain.shader, Snow.shader) and one CGinclude file (Precipitation.cginc).

We’ll mostly be working in the Precipitation.cginc since the behavior of rain and snow are fairly similar, save for a few tweaked values.

The CGinclude file is a way to share code between the two shaders. Anything specific to rain or snow can be specified by checking if RAIN has been defined before including the Precipitation.cginc file (see Rain.shader).

Now that we started our shaders, we can edit our precipitation manager to actually use these to visualize our created mesh:

We’ve actually implemented the logic in OnPlayerGridChange, created a simple function to create our materials from our shaders if they don’t exist, and made a new function (RenderEnvironmentParticles) that renders the rain and snow in the Update method using Unity’s Graphics.DrawMeshInstanced.

With all this set up you should see a bunch of small quads rendering towards the top of each grid cube.

If this doesn’t work immediately, try moving the camera to another grid coordinate to trigger ‘OnPlayerGridChange’.

CULLING:

We don’t want to render ALL of those quads all the time, so we’re going to set up a culling system based on the amount of precipitation, distance from the rendering camera, and if it’s behind the rendering camera.

First let’s update PrecipitationManager.cs to include two instances of a class that will hold settings for each type of precipitation, we’ll populate them with values we’re going to use later just so they start of with some initialization.

We’ve also changed RenderEnvironmentParticles to take in a settings object and send the amount value to the shader, as well as the other settings values while we’re at it.

You might be tempted to just adjust the opacity of the entire effect based on that amount variable, but it would look odd, as if ALL the rainfall is fading away. What we want is to simulate is light rainfall as a spread out “trickle” with heavier rainfall appearing as “denser”. To accomplish this we’ll define a threshold for each vertex when we build the mesh in RebuildPrecipitationMesh, this threshold will be one of four ‘levels’. If the amount is below that threshold, then the vertex won’t render. The threshold will be calculated based on the vertex’s position in the mesh, so that it gives the illusion of the rain becoming denser.

We can use this threshold to cull the vertex in the geometry building function in our shader code ( Precipitation.cginc ).

Back in Unity, if we rebuild the mesh, and set the snow amount to 0, we should be able to see this pattern emerge when fiddling with the rain amount slider.

This is better, but we still want it to seem more “natural”, so let’s add some noise to the threshold, and adjust the opacity for each quad to fade out as it reaches the threshold, so they don’t just pop in and out of existence.

In PrecipitationManager.cs we’ll add a Texture2D field to supply our noise texture, which we’ll pass into the shader via the material (just like we did with the amount variable). Let’s also pass in the main texture we’ll use later as well.

In Precipitation.cginc we’ll use this noise texture to randomize the vertex threshold, and calculate the opacity based on where the amount is in relation to this threshold.

Now raising and lowering the rain amount should have a more ‘natural’ look:

Now let’s cull and fade based on the rendering camera’s distance and forward direction. Luckily we can do this all in the shader, as Unity gives us several built in variables we can use to get the camera’s position and forward.

Now you should see the quads fading in and out as you move the scene view camera:

MOVEMENT:

Now it’s time to work on making the precipitation actually “fall” from the sky. To do this, we’ll animate the Y position of the vertex in the shader. Before we do this though, we need to establish where the rain will “stop” falling. For now this will be after it travels the entire grid coordinate in the y position. In PrecipitationManager.cs we’ll send the _MaxTravelDistance shader variable to the Grid Handler’s grid size.

In the shader itself, we’ll animate the Y position of the vertex over time by _FallSpeed and make sure it loops back around after _MaxTravelDistance . The ‘loop around’ point and fall speed are also modified by the noise values, so that all the particles don’t look like they’re falling in unison.

Note how we changed the quad normal to (0, 1, 0), so the quads are upright now

If we change the rain amount to 0, and turn up the snow amount, we can see this “falling” movement at a more manageable speed:

If this isn’t immediately visible, try moving the camera around, since the snow quads are culled on one side

The snow looks like it needs some more natural movement along the X and Z axis, to make it seem like it’s “fluttering” around. Let’s add that to the geometry shader with the falling movement, using the _Flutter prefixed variables.

You should immediately be able to see the results:

already looking noisier!

VISUALS:

Now let’s give the particles some visual fidelity, by rendering the main texture, giving them some color variation based on it’s location / time, and taking into account the size of the quads specified in the settings objects.

Here’s the result as we mess around with the color variation:

Make sure you set the Precipitation texture asset as the main texture.

FINALIZING SNOW:

Since the snow is being rendered as uniformly sized quads that are relatively small, we can billboard them so the face that renders the texture is always facing the camera. This way it won’t disappear like it currently does when we move the camera around. This lets us continue to cull the face that points away from the camera (which saves on performance).

To do this, we calculate certain values for the normal direction and right direction for the quad in the geometry function if the pre-processor directive specifies that RAIN is NOT defined.

Now when we move the camera around in the scene view it doesn’t disappear based on it’s direction from the camera.

FINALIZING RAIN:

The rain quad will be drawn similarly, except the quad will be stretched along its height. Since the rain particle is going to be a non uniform rectangle though, billboarding it will lead to some strange artifacts at certain angles. We’ll have to figure out another way to make it viewable from all angles.

To do this we’ll render 2 quads per rain particle perpendicular to eachother:

SIDE VIEW:          TOP-DOWN VIEW:
______
| |
| | |
| | ---|---
| | |
| |
| |
|____|

We’ll make some changes to the Precipitation.cginc file in the space where RAIN is defined:

Back in the scene view in Unity, if we turn the snow amount down, and the rain amount up we should see this result:

WIND:

No storm is complete without wind! Let’s create some logic so we can define a wind direction on the XZ plane (specified by an angle around the Y axis) and a wind strength that will ‘bend’ the particles towards that wind direction.

We do this by creating a matrix in the PrecipitationManager.cs that contains the rotation of the angle of the ‘bend’ towards the wind, and the wind’s direction itself. This matrix is then supplied to the shader so that it can modify the “fall” direction of the particles.

In Precipitation.cginc we’ll use that _WindRotationMatrix to rotate the position of the vertex. We’ll also temporarily make sure the opacity is set to 1, so we can clearly see the precipitation’s behavior.

In the Unity scene view, if we set wind strength to .5, turn on just the snow, and move the camera so it’s a straight on view of the side of the grid we can see this view:

The snow is falling in the right direction, but the rotation seems have rotated the entire mesh, including where the particle starts from. This can be fixed by calculating the offset of this rotated start position, with the original start position of the particle (before it starts ‘falling’) and subtracting it from the final position (after the ‘falling’ movement).

The result:

Notice the gap towards the bottom of each grid though. If we were to take the camera through this area, the snow would appear to thin out.

This gap happens because the snow only falls for _MaxTravelDistance which is set as the grid size. This works fine when the snow is falling straight down, but when it’s at an angle, it should travel for the length of the hypotenuse of the right triangle it forms with the angle of wind strength, and adjacent side of length grid size. We can calculate this using some trigenomotry:

h = hypotenuse = maxTravelDistance
θ = theta = windStrengthAngle
a = adjacent = gridSize
|\
|θ\
a| \ h
| \
| \
cos(θ) = a / h
h(cos(θ)) = a
h = a / cos(θ)
maxTravelDistance = gridSize / cos(windStrengthAngle)

The result fixes the gap:

But when we switch back to the rain effect we see another issue. The rain drops are still pointing straight up, as opposed to stretching out to the direction they’re falling towards.

This is easily fixed by using the same wind rotation matrix in the shader to rotate the up direction of the raindrop’s quad:

The result of this fix:

CONCLUSION:

As mentioned in the beginning, the completed source code for this tutorial can be found here.

Don’t forget to remove the debug line in the geometry function of the shader where we manually set opacity to 1.

Now we’re ready to enter play mode and see our effect in action. You can easily render a dense rain AND snow storm at the same time without a ton of performance loss. Try implementing this effect with Unity’s built in particle system and compare the performance for yourself.

Here are the settings I used in the video:

--

--