Volumetric Light Scattering as a Custom Renderer Feature in URP
Learn how to create your own custom rendering features with Unity’s Universal Render Pipeline by adding some volumetric light scattering to a small animated scene. By Ignacio del Barrio.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Volumetric Light Scattering as a Custom Renderer Feature in URP
30 mins
- Getting Started
- Volumetric Light Scattering
- Using the Screen Space Method
- Creating a Custom Renderer Feature
- Adding the Render Feature to the Forward Renderer
- Implementing a Custom Render Pass
- Setting up the Light Scattering Pass
- Configuring the Occluders Map
- Implementing the Occluders Shader
- Executing the Render Pass
- Drawing the Light Source
- Referencing Unity Default Shaders
- Drawing the Occluders
- Inspecting With the Frame Debbuger
- Refining the Image in Post-Processing
- Implementing the Radial Blur Shader
- Combining the Images
- Adding the Radial Blur Material Instance
- Configuring the Radial Blur Material
- Blurring the Occluders Map
- Where to Go From Here?
Adding the Radial Blur Material Instance
The following steps are similar to what you did for the occluders map. Go back to VolumetricLightScattering.cs and, in LightScatteringPass
, declare a material above the constructor:
private readonly Material radialBlurMaterial;
Then, add this line inside the constructor:
radialBlurMaterial = new Material(
Shader.Find("Hidden/RW/RadialBlur"));
Nothing new so far, you’re just creating the material instance. Make sure that the material isn’t missing by replacing this if
statement:
if (!occludersMaterial) { return; }
With this one:
if (!occludersMaterial || !radialBlurMaterial ) { return; }
Now you’re all set to configure the blur material.
Configuring the Radial Blur Material
The blur shader needs to know the sun’s location so it can use it as the center point. Within Execute()
, replace // TODO: 2
with the following lines:
// 1
Vector3 sunDirectionWorldSpace =
RenderSettings.sun.transform.forward;
// 2
Vector3 cameraPositionWorldSpace =
camera.transform.position;
// 3
Vector3 sunPositionWorldSpace =
cameraPositionWorldSpace + sunDirectionWorldSpace;
// 4
Vector3 sunPositionViewportSpace =
camera.WorldToViewportPoint(sunPositionWorldSpace);
This might seem like some crazy voodoo magic, but it’s actualy quite simple:
- You get a reference to the sun from
RenderSettings
. You need the forward vector of the sun because directional lights don’t have a position in space. - Get the camera position.
- This gives you a unit vector that goes from the camera towards the sun. You’ll use this for the sun’s position.
- The shader expects a viewport space position, but you did your calculations in world space. To fix this, you use
WorldToViewportPoint()
to transform the point-to-camera viewport space.
Good job, you’ve finished the hardest part.
Now, pass the data to the shader with the following code:
radialBlurMaterial.SetVector("_Center", new Vector4(
sunPositionViewportSpace.x, sunPositionViewportSpace.y, 0, 0));
radialBlurMaterial.SetFloat("_Intensity", intensity);
radialBlurMaterial.SetFloat("_BlurWidth", blurWidth);
Keep in mind that you only really need the x and y components of sunPositionViewportSpace
since it represents a pixel position on the screen.
Blurring the Occluders Map
Finally, you need to blur the occluders map.
Add the following line of code to run the shader:
Blit(cmd, occluders.Identifier(), cameraColorTargetIdent,
radialBlurMaterial);
The context provides Blit
, a function that copies a source texture into a destination texture using a shader. It executes your shader with occluders
as the source texture, then stores the output in the camera color target. Because of the blend mode you defined in the shader, it combines both the output and the color target.
To get a reference to the camera color target, add the following line at the top of LightScatteringPass
:
private RenderTargetIdentifier cameraColorTargetIdent;
Add a new function below the constructor to set this variable:
public void SetCameraColorTarget(RenderTargetIdentifier cameraColorTargetIdent) { this.cameraColorTargetIdent = cameraColorTargetIdent; }
In VolumetricLightScattering
, add this line at the end of AddRenderPasses()
:
m_ScriptablePass.SetCameraColorTarget(renderer.cameraColorTarget);
This will pass the camera color target to the render pass, which Blit()
requires.
Save the script and go back to the editor. Congratulations, you completed the effect!
Go to the Scene view and you’ll see your lighting effects at work. Click Play and move around the level to see it in action:
Where to Go From Here?
Download the final project by using the Download Materials button at the top or bottom of this tutorial.
In this tutorial, you learned how to write your own renderer features to extend the Universal Render Pipeline. You also learned a cool post-processing technique to visually enhance your projects.
Feel free to explore the project files and experiment with the effect. To see how everything works together, inspect the render pass with the Frame Debugger.
There’s a small surprise hidden in the scene. Look at the day and night controller attached to the directional light. Activate Auto Increment and enjoy the sunrise. :]
If you want to learn more about the Universal Render Pipeline, it’s helpful to look at the source code. Yes! It’s available to anyone for free, and it’s a great source of learning material. You can find it in Packages/Universal RP.
Also, look at the Boat Attack Demo by Andre McGrail, which uses custom renderer features for water effects and other cool things.
We hope you enjoyed this tutorial! If you have any questions or comments, feel free to join the forum discussion below.