Rendering
Tutorial
Render Graph - Polishing Rain Atmosphere Effect
Jun 17, 2025
:image-description:
Read time: 11 min
:center-100:

___
In this article, I improved the rain atmosphere effect I built in the previous article. This effect works by postprocessing a GBuffer in deferred rendering path, just before the light caluclation, where I increased a smoothness and decreased the GI and albedo.
There were two main issues I had to tackle:
High smoothness resulted in flickering when using the bloom post process.
All parameters were hardcoded, so I needed to make them accessible through the material's properties.
:center-px:

:image-description:
Bloom flickers a lot during a small camera movements.
___
Bloom flickering
High smoothness in the GBuffer can create very bright points on the screen in certain light conditions. It makes the bloom postprocess shimmer and flicker. Bloom flicker can be implemented by modifying the bloom effect itself or modifying its inputs. I decided to limit the smoothness value in the GBuffer to fix the issue. In the previous article I used hardware blending for GBuffer modifications, which has limited capabilities and doesn't allow me to limit the maximum value of the target texture. I can fix that by copying the normal-smoothness buffer and sampling the copy to implement a custom blending in the fragment shader. I will keep the smoothness in the reasonable range in the shader that applies the effect.
Current render graph looks like this (Apply Rain Atmosphere was implemented in the previous article):
:center-px:

:image-description:
Current render graph looks like this. Apply Rain Atmosphere was implemented in the previous article.
I can add a custom node between DrawGBuffer and Apply Rain Atmosphere that will create a copy of the GBuffer[2] texture and share it with other passes. I will call this pass Initialize Rain Resources. Data shared between passes will be called RainResourceData. Then Apply Rain Atmosphere will be able access the copy of GBuffer[2] and use it in the shader with custom blending.
:center-px:

:image-description:
This is how I want to modify the nodes in the render graph.
My plan:
Implement Initialize Rain Resources pass.
Create a RainResourceData class to store resources shared between passes.
Copy the GBuffer[2] contents into a custom texture using a custom shader.
Access the copied GBuffer[2] in Apply Rain Atmosphere pass.
Sample the copied GBuffer[2] in the fragment shader to implement custom blending and avoid bloom shimmer.
___
Implement resource initialization pass
My goal is to create a new texture in the Render Graph and copy the content of GBuffer[2] into this texture.
I created an InitializeRainResourcesPass
and modified the RainAtmosphereFeature
to execute it before the main pass. I've added a material I want to use with a custom shader to copy the GBuffer[2] contents.
Shared resources
I started by creating a class that store the shared resources. When using Render Graph API, shared resources must inherit ContextItem
. They must release manually allocated resources in the `Reset()` function. In my case, I only need to store a TextureHandle
- the copy of GBuffer[2].
I created the shared resources object in the Initialize Rain Resources Pass using the frameData.GetOrCreate<>()
API. After using this method, those resources can be accessed in other passes.
Accessing the GBuffer
I checked if GBuffer is initialized to avoid executing this pass in a forward rendering path.
Allocate GBuffer copy texture
I allocated the target texture by using the renderGraph.CreateTexture(textureDescriptior)
method. This method does not allocate the texture immediately, but it allows the Render Graph to manage the lifetime of the texture. Render Graph will ensure that the texture exists when the render function is executed.
Creating render pass
Then, I created a pass in the render graph. I used a raster render pass where I specified:
Render attachment - our freshly allocated texture handle
Dependencies - gBuffer[2] texture
Render function
Render function
Then, I implemented a render function. Raster Graph Context contains object pooling for PropertyBlocks - I used that to set the shader properties. _GBuffer2 property was set to the original texture. The shader used in the material will use that property.
Writing shader
The RenderGraph node is ready, and it's time to implement the shader that copies the contents of the GBuffer. The shader should read the contents of the GBuffer texture and then output those in the fragment shader. I used the fullscreen draw template to achieve that.
Vertex Shader
I modified the vertex shader to output a UV calculated from the clip space. Clip space range is from -1.0 to 1.0, and UV is from 0.0 to 1.0, so I needed to remap those values.
On some platforms, UV starts at the top of the screen. In this case, Y-axis needs to be flipped.
Fragment Shader
I declared Texture2D _GBuffer2
- the texture I assigned in the render function. The UV was used to sample the GBuffer and output its contents into our render attachment. I used an object-oriented HLSL syntax to sample the texture.
Now I could create a material with this shader and assign it to the renderer feature.
___
Access the copied GBuffer in ApplyRainAtmospherePass
After assigning the material to the render feature, there is no `InitializeRainResourcesPass` in the Frame Debugger and Render Graph Viewer. It is not executed at all.
:center-px:

:image-description:
My pass is not executed!
Render Graph detects that I don't use the created texture in other passes, so it culls the render node created in the `InitializeRainResourcesPass`. To fix that, I need to declare the usage of the texture in the `ApplyRainAtmospherePass`. I modified the `ApplyRainAtmospherePass` by accessing shared resources with the `frameData.Get<>()` API and declared that the usage of the texture.
And after this change, Frame Debugger and Render Graph Viewer correctly display the InitializeRainResourcesPass
.
:center-px:

:image-description:
My pass is executed! Happy Johny noises!
Right now, when I know that Render Graph properly executes the initialization pass, I can use the copy of the GBuffer in the `ApplyRainAtmospherePass
`. I forwarded the texture handle to the passData, used in our render function.
I used a property block from the render graph pool to set the texture in the shader.
All the data is set right now, so it's time to adjust the shader.
___
Adjusting the rain atmosphere shader
My goal was to reduce the shimmering in the bloom postprocess. I could do that by implementing a custom blending for the smoothness stored in the alpha channel of the second GBuffer target.
The plan was:
Add texture resources to the shader.
Calculate screen UV to sample the GBuffer copy.
Disable hardware blending and implement a custom one.
I declared the texture in the shader.
Then, I needed a UV to sample the texture. I copied the vertex shader from the CopyGBuffer.shader.
It is a good practice to turn off the hardware blending when switching to the software blending - it can save some GPU resources.
The last step was to sample the GBuffer contents in the fragment shader and write a custom blending. I've found that additive blending for smoothness and clamping the result to a max of 0.86 works best in my case, so I hardcoded those values. This is a final fragment shader code:
I could test the results. As you can see, the shimmer was mostly reduced!
:center-px:

:center-px:

:image-description:
Comparison of before and after. Bloom flickering is successfuly reduced. I left a little bit of a flicker, because, as a player, I prefer having a little bit of flicker and aliasing rather then blurry image.
You probably noticed that I needed to write much more code just to copy a texture. In the previous API I would use a `CommandBuffer.Blit` function to achieve that. Here I needed to explicitly create a separate pass. There are some utility methods to *blit* textures in Render Graph, but they still require to create a separate pass, custom shader, and manage the dependencies between passes - no way around that.
___
Shader Parameters
Because I hardcoded all values in the shader, the effect can't be adjusted outside the code. I will add parameters to the material to make the visuals easier to adjust:
Albedo multiplier
Ambient light multiplier
Smoothness
Max Smoothness
Effect strength - controls the strength of the effect. 0 - no effect is applied, 1 - full effect is applied.
Let's start by adding shader properties. I added this at the beginning of the shader.
:center-px:

:image-description:
This section added sliders to the material that renders the effect.
Then, I included all those properties in the shader code, just above the fragment shader.
Next, I replaced all parameters except _EffectStrength
.
To implement the `_EffectStrength` parameter, blending in the shader needs to do nothing when the value is 0. I implemented that using a Lerp function that interpolate all the values to the neutral ones. If the effect strength is set to 0, we must output 1 for the targets with multiply-blend and leave the smoothness as is.
All parameters work nicely.
:center-px:

:image-description:
Effect strength parameter can smoothly interpolate the amosphere of the scene.
___
Performance
My rain atmosphere effect is ready. It takes only 0.44ms to render in 1440p on RTX 3060, increasing frame time from 7.77ms to 8.21ms (5.7% increase) - Measured with NVidia Nsight Graphics. The shader is mostly screen pipe bound - which means that most of the time is spent blending the GBuffer colors using the hardware units - I can't get much faster than that!
:center-px:

:image-description:
Overwiew of GPU hardware units throughput while rendering our feature.