The Scriptable Render Pipeline (SRP), introduced in 2018.1 beta, is a way of configuring and performing rendering in Unity that is controlled from a C# script. Before writing a custom render pipeline it’s important to understand what exactly we mean when we say render pipeline.
What is a Render Pipeline
“Render Pipeline” is an umbrella term for a number of techniques used to get objects onto the screen. It encompasses, at a very high level:
- Culling
- Rendering Objects
- Post processing
In addition to these high level concepts each responsibility can be broken down further depending on how you want to execute them. For example rendering objects could be performed using:
- Multi-pass rendering
- one pass per object per light
- Single-pass
- one pass per object
- Deferred
- Render surface properties to a g-buffer, perform screen space lighting
When writing a custom SRP, these are the kind of decisions that you need to make. Each technique has a number of tradeoffs that you should consider.
Demo Project
All the features discussed in this post are covered in a demo project located on GitHub
The Rendering entry point
When using SRP you need to define a class that controls rendering; this is the Render Pipeline you will be creating. The entry point is a call to “Render” which takes the render context (described below) and a list of cameras to render.
1 2 3 4 |
public class BasicPipeInstance : RenderPipeline { public override void Render(ScriptableRenderContext context, Camera[] cameras){} } |
The Render Pipeline Context
SRP renders using the concept of delayed execution. As a user you build up a list of commands and then execute them. The object that you use to build up these commands is called the ‘ScriptableRenderContext’. When you have populated the context with operations, then you can call ‘Submit’ to submit all the queued up draw calls.
An example of this is clearing a render target using a command buffer that is executed by the render context:
1 2 3 4 5 6 7 8 9 |
// Create a new command buffer that can be used // to issue commands to the render context var cmd = new CommandBuffer(); // issue a clear render target command cmd.ClearRenderTarget(true, false, Color.green); // queue the command buffer context.ExecuteCommandBuffer(cmd); |
Here’s a complete render pipeline that simply clears the screen.
Culling
Culling is the process of figuring out what to render on the the screen.
In Unity Culling encompasses:
- Frustum culling: Calculating the objects that exist between the camera near and far plane.
- Occlusion culling: Calculating which objects are hidden behind other objects and excluding them from rendering. For more information see the Occlusion Culling docs.
When rendering starts, the first thing that needs to be calculated is what to render. This involves taking the camera and performing a cull operation from the perspective of the camera. The cull operation returns a list of objects and lights that are valid to render for the camera. These object are then used later in the render pipeline.
Culling in SRP
In SRP, you generally perform object rendering from the perspective of a Camera. This is the same camera object that Unity uses for built-in rendering. SRP provides a number of API’s to begin culling with. Generally the flow looks as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Create an structure to hold the culling paramaters ScriptableCullingParameters cullingParams; //Populate the culling paramaters from the camera if (!CullResults.GetCullingParameters(camera, stereoEnabled, out cullingParams)) continue; // if you like you can modify the culling paramaters here cullingParams.isOrthographic = true; // Create a structure to hold the cull results CullResults cullResults = new CullResults(); // Perform the culling operation CullResults.Cull(ref cullingParams, context, ref cullResults); |
The cull results that get populated can now be used to perform rendering.
Drawing
Now that we have a set of cull results, we can render them to the screen.
But there are so many ways that things can be configured, so a number of decisions need to be made up front. Many of these decisions will be driven by:
- The hardware you are targeting the render pipeline to
- The specific look and feel you wish to achieve
- The type of project you are making
For example, think of a mobile 2D sidescroller game vs a PC high end first person game. These games have vastly different constraints so will have vastly different render pipelines. Some concrete examples of real decisions that may be made:
- HDR vs LDR
- Linear vs Gamma
- MSAA vs Post Process AA
- PBR Materials vs Simple Materials
- Lighting vs No Lighting
- Lighting Technique
- Shadowing Technique
Making these decisions when writing a render pipeline will help you determine many of the constraints that are placed when authoring it.
For now, we’re going to demonstrate a simple renderer with no lights that can render some of the objects opaque.
Filtering: Render Buckets and Layers
Generally, when rendering object has a specific classification, they are opaque, transparent, sub surface, or any number of other categories. Unity uses a concept of queues for representing when an object should be rendered, these queues form buckets that objects will be placed into (sourced from the material on the object). When rendering is called from SRP, you specify which range of buckets to use.
In addition to buckets, standard Unity layers can also be used for filtering.
This provides the ability for additional filtering when drawing objects via SRP.
1 2 3 4 5 6 7 8 9 10 11 12 |
// Get the opaque rendering filter settings var opaqueRange = new FilterRenderersSettings(); //Set the range to be the opaque queues opaqueRange.renderQueueRange = new RenderQueueRange() { min = 0, max = (int)UnityEngine.Rendering.RenderQueue.GeometryLast, }; //Include all layers opaqueRange.layerMask = ~0; |
Draw Settings: How things should be drawn
Using filtering and culling determines what should be rendered, but then we need to determine how it should be rendered. SRP provides a variety of options to configure how your objects that pass filtering should be rendered. The structure used to configure this data is the ‘DrawRenderSettings’ structure. This structure allows for a number of things to be configured:
- Sorting – The order in which objects should be rendered, examples include back to front and front to back.
- Per Renderer flags – What ‘built in’ settings should be passed from Unity to the shader, this includes per object light probes, per object light maps, and similar.
- Rendering flags – What algorithm should be used for batching, instancing vs non-instancing.
- Shader Pass – Which shader pass should be used for the current draw call.
1 2 3 4 5 6 7 8 9 10 11 12 |
// Create the draw render settings // note that it takes a shader pass name var drs = new DrawRendererSettings(Camera.current, new ShaderPassName("Opaque")); // enable instancing for the draw call drs.flags = DrawRendererFlags.EnableInstancing; // pass light probe and lightmap data to each renderer drs.rendererConfiguration = RendererConfiguration.PerObjectLightProbe | RendererConfiguration.PerObjectLightmaps; // sort the objects like normal opaque objects drs.sorting.flags = SortFlags.CommonOpaque; |
Drawing
Now we have the three things we need to issue a draw call:
- Cull results
- Filtering rules
- Drawing rules
We can issue a draw call! Like all things in SRP, a draw call is issued as a call into the context. In SRP you normally don’t render individual meshes, instead you issue a call that renders a large number of them in one go. This reduces script execution overhead as well as allows fast jobified execution on the CPU.
To issue a draw call we combine the functions that we have been building up.
1 2 3 4 5 6 7 |
// draw all of the renderers context.DrawRenderers(cullResults.visibleRenderers, ref drs, opaqueRange); // submit the context, this will execute all of the queued up commands. context.Submit(); |
This will draw the objects into the currently bound render target. You can use a command buffer to switch the render target if you so wish.
A renderer that renders opaque objects can be found here:
This example can be further extended to add transparent rendering:
The important thing to note here is that when rendering transparent the rendering order is changed to back to front.
We hope that this post will help you get started writing your own custom SRP. Download the 2018.1 beta to get started and let us know what you think on this forum thread!
Martin
February 2, 2018 at 8:31 amJust out of curiosity: Does SRP allow to share the culling result of one camera with others? Think of mirrors for a vehicle. In a car you have typically 3 mirrors looking into the same direction. All three render from a slightly different position and different parts of the rendered result are obstructed by the car geometry itself but in fact all three cameras do the same culling operations. If it would be possible to use the culling result of one camera for the other two aswell could save some performance in vehicle simulations imho.
Despite of this maybe stupid question this stuff sounds exciting :-)
VVEthan
February 1, 2018 at 7:09 pmAs a long time mobile dev, with minimal experience in how graphics engines actually work, this was an extremely helpful article.
I kept reading about the “Scriptable Rendering Pipeline” and thought: “That sounds great! …I think? Too bad it probably won’t be useful to us.”
As it turns out, I think we have an actual use for it in our current project.
cfree
February 1, 2018 at 2:23 pmGreat article!
Devang
February 1, 2018 at 4:59 amWell done unity Team
daslolo
February 1, 2018 at 2:27 amthis image could be rendered in unity 2.6, use the translucent mushroom that this French guy showed on twitter
Callen
January 31, 2018 at 10:18 pmExcellent info. While I love unity managing all this for me in most cases, I can think of a few projects where I would love to customize a SRP to make novel visual styles. I look forward to playing with all of this soon!
hippocoder
January 31, 2018 at 5:08 pmThanks for the information, Tim. I appreciate these docs (hoping these blogs will make their way to the actual docs too).
—
Also, can people not be rude and splat their wishlists on every blog posting. You guys realise that will never work, right?
Isaac Surfraz
January 31, 2018 at 3:18 pmDead excited for this. Well done unity team!
You usually find me on the forums complaining about stuff that the Unity team has promised for a while but not delivered on (with good reason, there are a few things missing for a while that hold me and many off from subscribing!).
So I am very very happy to see that for once you are prioritizing the importance of the engines fundamentals in a release, instead of adding lots of extra but crappy services and unfinished systems.
BUT PLEASE:
FINISH NESTED PREFABS
UPDATE TERRAIN SYSTEM
ADD SMARTSPRITE
FIX AND DOCUMENT PROPERLY TIMELINE
These 3 things have really been too long waited for now, its sort of a necessity for an engine with over 50% of all developers in the world using it to have an up to date terrain and spline based 2D terrain system.
Keep up the good work!
interpol
January 31, 2018 at 4:40 pmI am with you, man. I am really mad about core engine features being outdated and literally crappy. SRP looks like a real step forward, and I hope they will fix other systems in 2018 as well. Because Unity should be on a high level not just partially.
Vaka
January 31, 2018 at 5:04 pmI will join you too :) Pls, don’t forget about 2d animation (like builtIn anima2d).
sygan
February 1, 2018 at 11:51 amBut Anima2D is a part of Unity now, like Cinemachine or TextMeshPro…
PLEASE
January 31, 2018 at 10:15 pmPLEASE STOP