:center-100:

: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.
public class RainAtmosphereFeature : ScriptableRendererFeature
{
public Material copyGBufferMaterial;
public Material applyRainMaterial;
private bool HasValidResources => applyRainMaterial != null && copyGBufferMaterial != null;
private InitializeRainResourcesPass initializeResourcesPass;
private ApplyRainAtmospherePass applyRainAtmospherePass;
public override void Create()
{
applyRainAtmospherePass = new ApplyRainAtmospherePass();
initializeResourcesPass = new InitializeRainResourcesPass();
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
bool isGameOrSceneCamera = renderingData.cameraData.cameraType == CameraType.Game || renderingData.cameraData.cameraType == CameraType.SceneView;
if (isGameOrSceneCamera && HasValidResources)
{
initializeResourcesPass.material = copyGBufferMaterial;
applyRainAtmospherePass.material = applyRainMaterial;
renderer.EnqueuePass(initializeResourcesPass);
renderer.EnqueuePass(applyRainAtmospherePass);
}
}
}
public class InitializeRainResourcesPass : ScriptableRenderPass
{
public Material material { get; internal set; }
public InitializeRainResourcesPass()
{
this.renderPassEvent = RenderPassEvent.AfterRenderingGbuffer;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
}
}public class RainAtmosphereFeature : ScriptableRendererFeature
{
public Material copyGBufferMaterial;
public Material applyRainMaterial;
private bool HasValidResources => applyRainMaterial != null && copyGBufferMaterial != null;
private InitializeRainResourcesPass initializeResourcesPass;
private ApplyRainAtmospherePass applyRainAtmospherePass;
public override void Create()
{
applyRainAtmospherePass = new ApplyRainAtmospherePass();
initializeResourcesPass = new InitializeRainResourcesPass();
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
bool isGameOrSceneCamera = renderingData.cameraData.cameraType == CameraType.Game || renderingData.cameraData.cameraType == CameraType.SceneView;
if (isGameOrSceneCamera && HasValidResources)
{
initializeResourcesPass.material = copyGBufferMaterial;
applyRainAtmospherePass.material = applyRainMaterial;
renderer.EnqueuePass(initializeResourcesPass);
renderer.EnqueuePass(applyRainAtmospherePass);
}
}
}
public class InitializeRainResourcesPass : ScriptableRenderPass
{
public Material material { get; internal set; }
public InitializeRainResourcesPass()
{
this.renderPassEvent = RenderPassEvent.AfterRenderingGbuffer;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
}
}public class RainAtmosphereFeature : ScriptableRendererFeature
{
public Material copyGBufferMaterial;
public Material applyRainMaterial;
private bool HasValidResources => applyRainMaterial != null && copyGBufferMaterial != null;
private InitializeRainResourcesPass initializeResourcesPass;
private ApplyRainAtmospherePass applyRainAtmospherePass;
public override void Create()
{
applyRainAtmospherePass = new ApplyRainAtmospherePass();
initializeResourcesPass = new InitializeRainResourcesPass();
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
bool isGameOrSceneCamera = renderingData.cameraData.cameraType == CameraType.Game || renderingData.cameraData.cameraType == CameraType.SceneView;
if (isGameOrSceneCamera && HasValidResources)
{
initializeResourcesPass.material = copyGBufferMaterial;
applyRainAtmospherePass.material = applyRainMaterial;
renderer.EnqueuePass(initializeResourcesPass);
renderer.EnqueuePass(applyRainAtmospherePass);
}
}
}
public class InitializeRainResourcesPass : ScriptableRenderPass
{
public Material material { get; internal set; }
public InitializeRainResourcesPass()
{
this.renderPassEvent = RenderPassEvent.AfterRenderingGbuffer;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
}
}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].
public class RainResourceData : ContextItem
{
public TextureHandle normalSmoothnessTexture { get; internal set; }
public override void Reset()
{
normalSmoothnessTexture = TextureHandle.nullHandle;
}
}
public class RainResourceData : ContextItem
{
public TextureHandle normalSmoothnessTexture { get; internal set; }
public override void Reset()
{
normalSmoothnessTexture = TextureHandle.nullHandle;
}
}
public class RainResourceData : ContextItem
{
public TextureHandle normalSmoothnessTexture { get; internal set; }
public override void Reset()
{
normalSmoothnessTexture = TextureHandle.nullHandle;
}
}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.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
RainResourceData rainResources = frameData.GetOrCreate<RainResourceData>();
...public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
RainResourceData rainResources = frameData.GetOrCreate<RainResourceData>();
...public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
RainResourceData rainResources = frameData.GetOrCreate<RainResourceData>();
...Accessing the GBuffer
I checked if GBuffer is initialized to avoid executing this pass in a forward rendering path.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
RainResourceData rainResources = frameData.GetOrCreate<RainResourceData>();
var urpResources = frameData.Get<UniversalResourceData>();
var gBuffer = urpResources.gBuffer;
if (gBuffer == null || gBuffer.Length == 0 || gBuffer[0].Equals(TextureHandle.nullHandle))
return;public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
RainResourceData rainResources = frameData.GetOrCreate<RainResourceData>();
var urpResources = frameData.Get<UniversalResourceData>();
var gBuffer = urpResources.gBuffer;
if (gBuffer == null || gBuffer.Length == 0 || gBuffer[0].Equals(TextureHandle.nullHandle))
return;public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
RainResourceData rainResources = frameData.GetOrCreate<RainResourceData>();
var urpResources = frameData.Get<UniversalResourceData>();
var gBuffer = urpResources.gBuffer;
if (gBuffer == null || gBuffer.Length == 0 || gBuffer[0].Equals(TextureHandle.nullHandle))
return;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.
...
if (gBuffer == null || gBuffer.Length == 0 || gBuffer[0].Equals(TextureHandle.nullHandle))
return;
rainResources.normalSmoothnessTexture = renderGraph.CreateTexture(gBuffer[2].GetDescriptor(renderGraph));
...
if (gBuffer == null || gBuffer.Length == 0 || gBuffer[0].Equals(TextureHandle.nullHandle))
return;
rainResources.normalSmoothnessTexture = renderGraph.CreateTexture(gBuffer[2].GetDescriptor(renderGraph));
...
if (gBuffer == null || gBuffer.Length == 0 || gBuffer[0].Equals(TextureHandle.nullHandle))
return;
rainResources.normalSmoothnessTexture = renderGraph.CreateTexture(gBuffer[2].GetDescriptor(renderGraph));
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
...
rainResources.normalSmoothnessTexture = renderGraph.CreateTexture(gBuffer[2].GetDescriptor(renderGraph));
using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
{
builder.SetRenderAttachment(rainResources.normalSmoothnessTexture, 0);
builder.UseTexture(gBuffer[2]);
passData.gBuffer_2 = gBuffer[2];
passData.material = material;
builder.SetRenderFunc((PassData passData, RasterGraphContext context) => RenderFunction(passData, context));
}
}
private static void RenderFunction(PassData passData, RasterGraphContext context)
{
}
internal class PassData
{
internal Material material;
internal TextureHandle gBuffer_2;
} ...
rainResources.normalSmoothnessTexture = renderGraph.CreateTexture(gBuffer[2].GetDescriptor(renderGraph));
using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
{
builder.SetRenderAttachment(rainResources.normalSmoothnessTexture, 0);
builder.UseTexture(gBuffer[2]);
passData.gBuffer_2 = gBuffer[2];
passData.material = material;
builder.SetRenderFunc((PassData passData, RasterGraphContext context) => RenderFunction(passData, context));
}
}
private static void RenderFunction(PassData passData, RasterGraphContext context)
{
}
internal class PassData
{
internal Material material;
internal TextureHandle gBuffer_2;
} ...
rainResources.normalSmoothnessTexture = renderGraph.CreateTexture(gBuffer[2].GetDescriptor(renderGraph));
using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
{
builder.SetRenderAttachment(rainResources.normalSmoothnessTexture, 0);
builder.UseTexture(gBuffer[2]);
passData.gBuffer_2 = gBuffer[2];
passData.material = material;
builder.SetRenderFunc((PassData passData, RasterGraphContext context) => RenderFunction(passData, context));
}
}
private static void RenderFunction(PassData passData, RasterGraphContext context)
{
}
internal class PassData
{
internal Material material;
internal TextureHandle gBuffer_2;
}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.
private static void RenderFunction(PassData passData, RasterGraphContext context)
{
MaterialPropertyBlock propertyBlock = context.renderGraphPool.GetTempMaterialPropertyBlock();
propertyBlock.SetTexture(Uniforms._GBuffer2, passData.gBuffer_2);
context.cmd.DrawProcedural(Matrix4x4.identity, passData.material, 0, MeshTopology.Triangles, 6, 1, propertyBlock);
}
public static class Uniforms
{
internal static readonly int _GBuffer2 = Shader.PropertyToID(nameof(_GBuffer2));
}private static void RenderFunction(PassData passData, RasterGraphContext context)
{
MaterialPropertyBlock propertyBlock = context.renderGraphPool.GetTempMaterialPropertyBlock();
propertyBlock.SetTexture(Uniforms._GBuffer2, passData.gBuffer_2);
context.cmd.DrawProcedural(Matrix4x4.identity, passData.material, 0, MeshTopology.Triangles, 6, 1, propertyBlock);
}
public static class Uniforms
{
internal static readonly int _GBuffer2 = Shader.PropertyToID(nameof(_GBuffer2));
}private static void RenderFunction(PassData passData, RasterGraphContext context)
{
MaterialPropertyBlock propertyBlock = context.renderGraphPool.GetTempMaterialPropertyBlock();
propertyBlock.SetTexture(Uniforms._GBuffer2, passData.gBuffer_2);
context.cmd.DrawProcedural(Matrix4x4.identity, passData.material, 0, MeshTopology.Triangles, 6, 1, propertyBlock);
}
public static class Uniforms
{
internal static readonly int _GBuffer2 = Shader.PropertyToID(nameof(_GBuffer2));
}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.
Shader "Hidden/CopyGBuffer2"
{
SubShader
{
Pass
{
Cull Off
ZTest Off
ZWrite Off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
static const float4 verticesCS[] =
{
float4(-1.0, -1.0, 0.0, 1.0),
float4(-1.0, 1.0, 0.0, 1.0),
float4(1.0, 1.0, 0.0, 1.0),
float4(-1.0, -1.0, 0.0, 1.0),
float4(1.0, 1.0, 0.0, 1.0),
float4(1.0, -1.0, 0.0, 1.0)
};
void vert (uint vertexID : SV_VertexID, out float4 OUT_PositionCS : SV_Position)
{
float4 positionCS = verticesCS[vertexID];
}
void frag (out float4 OUT_Color : SV_Target0)
{
}
ENDHLSL
}
}
}Shader "Hidden/CopyGBuffer2"
{
SubShader
{
Pass
{
Cull Off
ZTest Off
ZWrite Off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
static const float4 verticesCS[] =
{
float4(-1.0, -1.0, 0.0, 1.0),
float4(-1.0, 1.0, 0.0, 1.0),
float4(1.0, 1.0, 0.0, 1.0),
float4(-1.0, -1.0, 0.0, 1.0),
float4(1.0, 1.0, 0.0, 1.0),
float4(1.0, -1.0, 0.0, 1.0)
};
void vert (uint vertexID : SV_VertexID, out float4 OUT_PositionCS : SV_Position)
{
float4 positionCS = verticesCS[vertexID];
}
void frag (out float4 OUT_Color : SV_Target0)
{
}
ENDHLSL
}
}
}Shader "Hidden/CopyGBuffer2"
{
SubShader
{
Pass
{
Cull Off
ZTest Off
ZWrite Off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
static const float4 verticesCS[] =
{
float4(-1.0, -1.0, 0.0, 1.0),
float4(-1.0, 1.0, 0.0, 1.0),
float4(1.0, 1.0, 0.0, 1.0),
float4(-1.0, -1.0, 0.0, 1.0),
float4(1.0, 1.0, 0.0, 1.0),
float4(1.0, -1.0, 0.0, 1.0)
};
void vert (uint vertexID : SV_VertexID, out float4 OUT_PositionCS : SV_Position)
{
float4 positionCS = verticesCS[vertexID];
}
void frag (out float4 OUT_Color : SV_Target0)
{
}
ENDHLSL
}
}
}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.
void vert (uint vertexID : SV_VertexID, out float2 OUT_uv : TEXCOORD0, out float4 OUT_PositionCS : SV_Position)
{
float4 positionCS = verticesCS[vertexID];
float2 uv = positionCS.xy * 0.5 + 0.5;
#if UNITY_UV_STARTS_AT_TOP
uv.y = 1.0 - uv.y;
#endif
OUT_PositionCS = verticesCS[vertexID];
OUT_uv = uv;
}
void vert (uint vertexID : SV_VertexID, out float2 OUT_uv : TEXCOORD0, out float4 OUT_PositionCS : SV_Position)
{
float4 positionCS = verticesCS[vertexID];
float2 uv = positionCS.xy * 0.5 + 0.5;
#if UNITY_UV_STARTS_AT_TOP
uv.y = 1.0 - uv.y;
#endif
OUT_PositionCS = verticesCS[vertexID];
OUT_uv = uv;
}
void vert (uint vertexID : SV_VertexID, out float2 OUT_uv : TEXCOORD0, out float4 OUT_PositionCS : SV_Position)
{
float4 positionCS = verticesCS[vertexID];
float2 uv = positionCS.xy * 0.5 + 0.5;
#if UNITY_UV_STARTS_AT_TOP
uv.y = 1.0 - uv.y;
#endif
OUT_PositionCS = verticesCS[vertexID];
OUT_uv = uv;
}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.
SamplerState pointClampSampler;
Texture2D _GBuffer2;
void frag (in float2 IN_uv : TEXCOORD0, out float4 OUT_Color : SV_Target0)
{
OUT_Color = _GBuffer2.SampleLevel(pointClampSampler, IN_uv, 0.0f);
}SamplerState pointClampSampler;
Texture2D _GBuffer2;
void frag (in float2 IN_uv : TEXCOORD0, out float4 OUT_Color : SV_Target0)
{
OUT_Color = _GBuffer2.SampleLevel(pointClampSampler, IN_uv, 0.0f);
}SamplerState pointClampSampler;
Texture2D _GBuffer2;
void frag (in float2 IN_uv : TEXCOORD0, out float4 OUT_Color : SV_Target0)
{
OUT_Color = _GBuffer2.SampleLevel(pointClampSampler, IN_uv, 0.0f);
}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.
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
...
RainResourceData rainResources = frameData.Get<RainResourceData>();
using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
{
builder.UseTexture(rainResources.normalSmoothnessTexture);
...
}
}public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
...
RainResourceData rainResources = frameData.Get<RainResourceData>();
using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
{
builder.UseTexture(rainResources.normalSmoothnessTexture);
...
}
}public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
...
RainResourceData rainResources = frameData.Get<RainResourceData>();
using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
{
builder.UseTexture(rainResources.normalSmoothnessTexture);
...
}
}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.
...
builder.UseTexture(rainResources.normalSmoothnessTexture);
passData.gBuffer2 = rainResources.normalSmoothnessTexture;
passData.material = material;
...
...
builder.UseTexture(rainResources.normalSmoothnessTexture);
passData.gBuffer2 = rainResources.normalSmoothnessTexture;
passData.material = material;
...
...
builder.UseTexture(rainResources.normalSmoothnessTexture);
passData.gBuffer2 = rainResources.normalSmoothnessTexture;
passData.material = material;
...
internal class PassData
{
internal Material material;
internal TextureHandle gBuffer2;
}internal class PassData
{
internal Material material;
internal TextureHandle gBuffer2;
}internal class PassData
{
internal Material material;
internal TextureHandle gBuffer2;
}I used a property block from the render graph pool to set the texture in the shader.
private static void RenderFunction(PassData passData, RasterGraphContext context)
{
MaterialPropertyBlock propertyBlock = context.renderGraphPool.GetTempMaterialPropertyBlock();
propertyBlock.SetTexture(Uniforms._GBuffer2, passData.gBuffer2);
context.cmd.DrawProcedural(Matrix4x4.identity, passData.material, 0, MeshTopology.Triangles, 6, 1, propertyBlock);
}private static void RenderFunction(PassData passData, RasterGraphContext context)
{
MaterialPropertyBlock propertyBlock = context.renderGraphPool.GetTempMaterialPropertyBlock();
propertyBlock.SetTexture(Uniforms._GBuffer2, passData.gBuffer2);
context.cmd.DrawProcedural(Matrix4x4.identity, passData.material, 0, MeshTopology.Triangles, 6, 1, propertyBlock);
}private static void RenderFunction(PassData passData, RasterGraphContext context)
{
MaterialPropertyBlock propertyBlock = context.renderGraphPool.GetTempMaterialPropertyBlock();
propertyBlock.SetTexture(Uniforms._GBuffer2, passData.gBuffer2);
context.cmd.DrawProcedural(Matrix4x4.identity, passData.material, 0, MeshTopology.Triangles, 6, 1, propertyBlock);
}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.
SamplerState pointClampSampler;
Texture2D _GBuffer2;
SamplerState pointClampSampler;
Texture2D _GBuffer2;
SamplerState pointClampSampler;
Texture2D _GBuffer2;
Then, I needed a UV to sample the texture. I copied the vertex shader from the CopyGBuffer.shader.
void vert (uint vertexID : SV_VertexID, out float2 OUT_uv : TEXCOORD0, out float4 OUT_PositionCS : SV_Position)
{
float4 positionCS = verticesCS[vertexID];
float2 uv = positionCS.xy * 0.5 + 0.5;
#if UNITY_UV_STARTS_AT_TOP
uv.y = 1.0 - uv.y;
#endif
OUT_PositionCS = verticesCS[vertexID];
OUT_uv = uv;
}void vert (uint vertexID : SV_VertexID, out float2 OUT_uv : TEXCOORD0, out float4 OUT_PositionCS : SV_Position)
{
float4 positionCS = verticesCS[vertexID];
float2 uv = positionCS.xy * 0.5 + 0.5;
#if UNITY_UV_STARTS_AT_TOP
uv.y = 1.0 - uv.y;
#endif
OUT_PositionCS = verticesCS[vertexID];
OUT_uv = uv;
}void vert (uint vertexID : SV_VertexID, out float2 OUT_uv : TEXCOORD0, out float4 OUT_PositionCS : SV_Position)
{
float4 positionCS = verticesCS[vertexID];
float2 uv = positionCS.xy * 0.5 + 0.5;
#if UNITY_UV_STARTS_AT_TOP
uv.y = 1.0 - uv.y;
#endif
OUT_PositionCS = verticesCS[vertexID];
OUT_uv = uv;
}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:
void frag
(
in float2 IN_uv : TEXCOORD0,
out float4 OUT_GBuffer0 : SV_Target0,
out float4 OUT_GBuffer2 : SV_Target2,
out float4 OUT_GBuffer3 : SV_Target3
)
{
float4 rawGBuffer2 = _GBuffer2.SampleLevel(pointClampSampler, IN_uv, 0.0);
rawGBuffer2.a = min(0.86, rawGBuffer2.a + 0.7);
OUT_GBuffer0 = float4(0.5, 0.5, 0.5, 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4(0.5, 0.5, 0.5, 1.0);
}void frag
(
in float2 IN_uv : TEXCOORD0,
out float4 OUT_GBuffer0 : SV_Target0,
out float4 OUT_GBuffer2 : SV_Target2,
out float4 OUT_GBuffer3 : SV_Target3
)
{
float4 rawGBuffer2 = _GBuffer2.SampleLevel(pointClampSampler, IN_uv, 0.0);
rawGBuffer2.a = min(0.86, rawGBuffer2.a + 0.7);
OUT_GBuffer0 = float4(0.5, 0.5, 0.5, 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4(0.5, 0.5, 0.5, 1.0);
}void frag
(
in float2 IN_uv : TEXCOORD0,
out float4 OUT_GBuffer0 : SV_Target0,
out float4 OUT_GBuffer2 : SV_Target2,
out float4 OUT_GBuffer3 : SV_Target3
)
{
float4 rawGBuffer2 = _GBuffer2.SampleLevel(pointClampSampler, IN_uv, 0.0);
rawGBuffer2.a = min(0.86, rawGBuffer2.a + 0.7);
OUT_GBuffer0 = float4(0.5, 0.5, 0.5, 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4(0.5, 0.5, 0.5, 1.0);
}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.
Shader "Hidden/ApplyRainAtmosphere"
{
Properties
{
_AlbedoMultiplier("Albedo multiplier", Range(0.0, 1.0)) = 0.5
_AmbientLightMultiplier("Ambient multiplier", Range(0.0, 1.0)) = 0.5
_SmoothnessAdd("Added smoothness", Range(0.0, 1.0)) = 0.7
_SmoothnessMax("Maximum smoothness", Range(0.0, 1.0)) = 0.86
_EffectStrength("Overall effect strength", Range(0.0, 1.0)) = 1.0
}
...Shader "Hidden/ApplyRainAtmosphere"
{
Properties
{
_AlbedoMultiplier("Albedo multiplier", Range(0.0, 1.0)) = 0.5
_AmbientLightMultiplier("Ambient multiplier", Range(0.0, 1.0)) = 0.5
_SmoothnessAdd("Added smoothness", Range(0.0, 1.0)) = 0.7
_SmoothnessMax("Maximum smoothness", Range(0.0, 1.0)) = 0.86
_EffectStrength("Overall effect strength", Range(0.0, 1.0)) = 1.0
}
...Shader "Hidden/ApplyRainAtmosphere"
{
Properties
{
_AlbedoMultiplier("Albedo multiplier", Range(0.0, 1.0)) = 0.5
_AmbientLightMultiplier("Ambient multiplier", Range(0.0, 1.0)) = 0.5
_SmoothnessAdd("Added smoothness", Range(0.0, 1.0)) = 0.7
_SmoothnessMax("Maximum smoothness", Range(0.0, 1.0)) = 0.86
_EffectStrength("Overall effect strength", Range(0.0, 1.0)) = 1.0
}
...: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.
...
float _AlbedoMultiplier;
float _AmbientLightMultiplier;
float _SmoothnessAdd;
float _SmoothnessMax;
float _EffectStrength;
void frag
(
in float2 IN_uv : TEXCOORD0,
...
...
float _AlbedoMultiplier;
float _AmbientLightMultiplier;
float _SmoothnessAdd;
float _SmoothnessMax;
float _EffectStrength;
void frag
(
in float2 IN_uv : TEXCOORD0,
...
...
float _AlbedoMultiplier;
float _AmbientLightMultiplier;
float _SmoothnessAdd;
float _SmoothnessMax;
float _EffectStrength;
void frag
(
in float2 IN_uv : TEXCOORD0,
...
Next, I replaced all parameters except _EffectStrength.
...
rawGBuffer2.a = min(_SmoothnessMax, rawGBuffer2.a + _SmoothnessAdd);
OUT_GBuffer0 = float4((float3)_AlbedoMultiplier, 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4((float3)_AmbientLightMultiplier, 1.0);
...
...
rawGBuffer2.a = min(_SmoothnessMax, rawGBuffer2.a + _SmoothnessAdd);
OUT_GBuffer0 = float4((float3)_AlbedoMultiplier, 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4((float3)_AmbientLightMultiplier, 1.0);
...
...
rawGBuffer2.a = min(_SmoothnessMax, rawGBuffer2.a + _SmoothnessAdd);
OUT_GBuffer0 = float4((float3)_AlbedoMultiplier, 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4((float3)_AmbientLightMultiplier, 1.0);
...
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.
...
rawGBuffer2.a = lerp(rawGBuffer2.a, min(_SmoothnessMax, rawGBuffer2.a + _SmoothnessAdd), _EffectStrength);
OUT_GBuffer0 = float4((float3)lerp(1.0, _AlbedoMultiplier, _EffectStrength), 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4((float3)lerp(1.0, _AmbientLightMultiplier, _EffectStrength), 1.0);
...
...
rawGBuffer2.a = lerp(rawGBuffer2.a, min(_SmoothnessMax, rawGBuffer2.a + _SmoothnessAdd), _EffectStrength);
OUT_GBuffer0 = float4((float3)lerp(1.0, _AlbedoMultiplier, _EffectStrength), 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4((float3)lerp(1.0, _AmbientLightMultiplier, _EffectStrength), 1.0);
...
...
rawGBuffer2.a = lerp(rawGBuffer2.a, min(_SmoothnessMax, rawGBuffer2.a + _SmoothnessAdd), _EffectStrength);
OUT_GBuffer0 = float4((float3)lerp(1.0, _AlbedoMultiplier, _EffectStrength), 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4((float3)lerp(1.0, _AmbientLightMultiplier, _EffectStrength), 1.0);
...
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.