Search Unity

Scriptable Render Pipeline Overview

January 31, 2018 in Technology | 7 min. read
Share

Is this article helpful for you?

Thank you for your feedback!

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.

This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.

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.

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:

// 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);
A not very exciting example of a render pipeline :)

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:

// 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.

// 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.
// 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.

// 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:

https://github.com/stramit/SRPBlog/blob/master/SRP-Demo/Assets/SRP-Demo/2-OpaqueAssetPipe/OpaqueAssetPipe.cs

This example can be further extended to add transparent rendering:

https://github.com/stramit/SRPBlog/blob/master/SRP-Demo/Assets/SRP-Demo/3-TransparentAssetPipe/TransparentAssetPipe.cs

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!

January 31, 2018 in Technology | 7 min. read

Is this article helpful for you?

Thank you for your feedback!