Rendering
Tutorial
Render Graph - Creating Rain Atmosphere
Jun 3, 2025
:center-100:

In this blog I will explain how I created a rain atmosphere effect by altering the URP Gbuffer. This is a second article in the Render Graph series. You can read the previous article here:
Render Graph API - Introduction
:center-100:

:image-description:
This is how my render feature will change the game's visuals.
___
GBuffer
Before diving into the feature, I want to touch on what GBuffer is.
GBuffer is an array of textures containing various surface properties, like albedo, specular, smoothness, ambient occlusion, and emission. When the deferred rendering path is selected in URP, all scene objects are rendered into GBuffer. This means that instead of calculating the lighting at each draw, they output surface properties into GBuffer. Then, lighting is applied in the form of a post-process.
URP GBuffer consists of 4 obligatory textures + some additional ones. According to URP documentation obligatory textures are:
GBuffer[0] - albedo in the RGB channels and material flags in the alpha channel.
GBuffer[1] - specular color in the RGB channels and ambient occlusion in the alpha channel.
GBuffer[2] - normal in the RGB channels and smoothness in the alpha channel
GBuffer[3] - the combined emission, GI, and lighting in the RGB channels.
All those textures are combined within the Render Deferred Lighting pass. It combines all the textures and calculates dynamic lights.
:center-50:

:image-description:
Image illustrating how various GBuffer textures are composed by the Deferred Lighting Pass.
___
How rain atmosphere can be achieved
To achieve the rain atmosphere effect, I need to manipulate GBuffer content before the Render Deferred Lighting pass to make the surface look wet after the lighting is calculated. But how are wet objects different from dry objects? Here's a photo illustrating the difference between dry and wet sand.
:center-px:

:image-description:
Photo illustrating the visual difference between wet and dry sand.
Wet surfaces appear darker and reflected light is sharp and bright. I targeted that by reducing the intensity of albedo color and ambient light stored in the Gbuffer[0].RGB and GBuffer[3].RGB. Sharper reflections can be implemented by increasing the smoothness in the alpha channel in GBuffer[2].
To test the concept I adjusted material properties. I increased the smoothness and darkened the color. The visual impact matched what I hoped for.
:center-px:

:image-description:
Materials with darker albedo color and higher smoothness feel more wet.
___
Implementation
I broke the implementation into 3 steps:
Set up the render feature
Create the Render Graph pass
Write the shader
1. Setting up the render feature
First, I ensured that the project uses a deferred rendering path in the URP renderer settings. I disabled Accurate G-Buffer Normals as this option encodes the normal buffer differently. I want to use standard normal encoding to minimize complexity at this stage.
:center-px:

:image-description:
My URP renderer settings.
Then I started by implementing the Scriptable Renderer Feature. RainAtmosphereFeature
injects the ApplyRainAtmospherePass
into the render pipeline. ApplyRainAtmospherePass
is set to be injected after rendering GBuffer by using a RenderPassEvent.AfterRenderingGbuffer
. I will implement all rendering logic in the RecordRenderGraph
method.
Creating the Render Graph pass
Here I will define a Render Graph pass that directly modifies the GBuffer contents.
GBuffer is a GPU resource that is shared between various passes. It means I can access it using frameData.Get<>()
API. The GBuffer reference is stored in the UniversalResourceData.gBuffer
and is only initialized when using a deferred path. Otherwise, it may not exist or be set to a null handle. I need to check that before adding a custom node to the render graph.
Once I confirmed GBuffer was valid, I proceeded with the node creation. I will modify GBuffer by executing a fullscreen procedural draw call to alter its contents. I created a raster graph node and a PassData class to store resources required in the render function. Then, I set GBuffer textures as render attachments of the node. GBuffer is an array of textures, so I used the `for` loop to bind all of them.
For the render function, I kept it minimal: just a DrawProcedural
call that uses my material. Since I’m rendering two triangles (a fullscreen quad), I drew 6 vertices. Each of the three consecutive vertices creates a triangle - two triangles in this case.
Writing the shader
Now, I need to create a shader that will render into GBuffer. I must write a fragment shader that will output colors to three render targets. I prepared a basic shader that contains an empty vertex and fragment shader. We will work on these in the following steps.
The vertex shader's main role is to define each vertex's position on the screen. The position of the screen is defined in the Clip Space. In short, x, y defines the screen's pixel, where (-1, -1) is one corner of the screen and (1, 1) is a second corner. z defines the depth. The position is stored in float4 in this format: (x\w, y\w, z\w, w).
Notice that when the w component is set to 1, the whole vector is simplified to (x, y, z, 1). We will use that trick in the next step.
In the render function, I declared drawing with six vertices, using two triangles. Each of the three consecutive vertices will form a triangle.
:center-px:

:image-description:
The illustration shows how the fullscreen mesh looks like.
I stored those vertices in the constant array in the shader. I can access the ID of each vertex using the SV_VertexID semantic. This vertex shader outputs a fullscreen triangle. Here is what the code looks like:
Shader will target GBuffer0, GBuffer2, and GBuffer3, corresponding to albedo, normal-smoothness, and ambient light. In Unity shaders, I can specify the blending operation for each target texture using a `Blend` keyword at the beginning of the pass.
Each respective value produced by the fragment shader will affect the GBuffer with its blend operation. This is how I configured the blending for those targets.
GBuffer0 = ShaderOutput0 * GBuffer0
GBuffer2 = ShaderOutput2 + GBuffer2
GBuffer3 = ShaderOutput3 * GBuffer3
Now its a time to make the fragment shader output values into GBuffer. This is done using the SV_Target<ID>
semantics. For example, I reduced the albedo and ambient light color by half and add 0.7 to the smoothness.
Shader is ready. It's time to create a material with it and assign it to the render feature. You can see that my feature is working correctly. I was so used to the old look of the Viking Village, nice to have something fresh!
:center-px:

:image-description:
Altering a GBuffer content is a great way to simulate weather changes in the game.
It is also displayed by the Render Graph Viewer (Window/Analysis/Render Graph Viewer) between Draw GBuffer pass and Render Deferred Lighting.
:center-px:

:image-description:
Render Graph Viewer shows my render pass.
In the next article I will fix the bloom flickering caused by the high smoothness. I will also implement the parameters to control the visuals.
:center-px:

:image-description:
My render feature causes flickering in a bloom postprocess.