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.
“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:
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:
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.
All the features discussed in this post are covered in a demo project located on GitHub
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.
public class BasicPipeInstance : RenderPipeline { public override void Render(ScriptableRenderContext context, Camera[] cameras){} }
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:
// 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 is the process of figuring out what to render on the the screen.
In Unity Culling encompasses:
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.
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:
// 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.
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:
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:
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.
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.
// 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;
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:
// 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;
Now we have the three things we need to issue a draw call:
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.
// 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!