Rendering Essentials in Unity, for Software Engineers
October 25, 2020
The TV room in 2013 game "Gone Home", made with Unity. CC BY-SA 3.0
While most Software Engineers interested in game development will be most excited about the programming aspect of making a game, you’ll need some familiarity with graphics, animation, and sound to be successful. This is especially true if you’re trying to work solo. While a wealth of assets and resources are available in the Asset Store (especially when it comes to reasonably-priced paid assets), there’s a decent amount you still need to know to execute well when putting these resources together. In this installment, we’ll discuss Rendering, Render Pipelines, and Lighting in Unity.
This is Unity for Software Engineers, a series for those seeking an accelerated introduction to game development in Unity. More is coming over the next few weeks, so consider subscribing for updates.
Shaders & Materials
Shaders are programs that help transform a mesh (made out of vertices and faces) into a 3D rendered image. Shaders are parameterized with any number of properties, such as a texture image representing the surface’s appearance, normal maps representing finer bumps on the mesh, and much more. The shader code you write determines what kind of properties it receives if any.
A Physically-based rendering (PBR) shader will typically ask for a number of property textures representing the albedo (roughly: color appearance), normal map (bumps & dents), metallicness, reflectiveness, and emissiveness, along with others. Since each additional texture carries a cost, certain textures might decide to expect optimized inputs that pack multiple of these textures in one image. Since an RGBA texture has four color channels, a shader can pack various property values in each channel.
In Unity, each Rendering Pipeline (discussed below) comes with its own set of standard shaders. All rendering pipelines include a “Simple Lit” shader as the most common shader, which helps render an object affected by lighting. In all pipelines, the Simple Lit variation takes in a base color or texture, a normal map, a smoothness map, and physically-based rendering inputs like metallic and specular values as properties.1
A Material is effectively an instance of a Shader. It is an Asset that uses a specific shader and defines all relevant input properties on that Shader.

The inspector page of three different materials using the Universal Render Pipeline’s Simple Lit shader.
- The first material uses a custom base texture giving the shape its characteristic color.
- The first and second materials use a custom normal map texture giving the shape a bumpy appearance.
- The second and third materials use a simple white color as their base color.
- The second and third materials use a high smoothness value, this giving their surfaces a shiny, reflective appearance.
- While both have the same smoothness value, the rough normal map on the second material makes it appear significantly less reflective.
Connecting Shaders to Game Objects
For a component to be visible in a game (and in your Scene editor), it needs to have a Renderer component. There are multiple renderer components available, but in 3D game design, you’ll most often be using the Mesh Renderer.

An example of a Cube in a scene hooked up to a custom material.
Objects created from the Game Object > 3D Object > menu, such as Cube, Sphere, Capsule, and others will automatically include:
- A Mesh Renderer component, linked to a default Simple Lit material
- An appropriate Collider component (“Box Collider” for a cube, “Sphere Collider” for a sphere, etc.), sized precisely to the object, and
- A Mesh Filter component, which defines the Mesh the Renderer should use.
A Mesh is effectively Unity’s term for a 3D model. A Mesh is an asset containing a description of an object’s vertices and faces. A mesh defines a “swatch” of materials for each face. This way, you can have meshes (e.g., for an entire building) where all exterior, floor, wall, and window materials are described as separate materials. A Mesh doesn’t specify what the materials are per se, but just that a given collection of faces have the same material.
When a mesh is selected in a Mesh Filter component, the Renderer’s Materials array property can be used to hook up each material. If you mis-order your materials in the above example, you can end up with a glass building and brick windows. Unity’s primitive meshes only use a single material throughout all faces of an object.
Rendering Pipelines
In Unity, a Render Pipeline is the system responsible for the end-to-end rendering of a scene. Historically, Unity came with a single general-purpose system known as the Built-in Render Pipeline. Starting with Unity 2018, Unity introduced the Scriptable Render Pipeline (SRP) technology. Two SRP-based pipelines are included with Unity:
- The Universal Render Pipeline (URP): A lightweight, high-performance pipeline that is easy to use and broadly cross-platform. It is great for any platform, including PC, Mobile, and Nintendo Switch. URP was previously known as the Lightweight Render Pipeline (LWRP).
- The High Definition Render Pipeline (HDRP): A high-quality pipeline for high-end platforms. It is particularly great for highly photorealistic games on beefier PCs and consoles.
URP is intended to replace the Built-in Render Pipeline as Unity’s out-of-the-box default. While URP has graduated out of beta, it’s not quite there yet in terms of 1:1 feature parity. This may or may not matter to you.
What Should I Start Using Today?
The thing to note is that each rendering pipeline comes with its own set of shaders (HDRP’s Lit shaders can be significantly more complex than URP’s, for example). The SRP transition also changed how Unity shaders are written, meaning that custom shaders written for the old Built-in render pipeline aren’t compatible with SRP and must be manually ported to SRP. This means that Unity’s ecosystem of assets became fragmented and individual assets need to put work to support SRP. This is fairly easy for assets that use simple shaders, but assets with custom shaders will need to support URP and HDRP explicitly.
We’re at a point where:
- URP is the future and is easier to use
- The Built-in render pipeline is likely good enough and will give you a more comprehensive selection of compatible assets
- HDRP is hard to use for someone just starting, but probably a good platform to invest time in if you’re interested in graphically striking games
If you’re starting development today because you want to learn and experiment, I suggest you start with URP. Other than limited assets, URP currently has some limitations with grass and trees.
If you’re starting development today because you have a concrete game idea you want to finish within the next year or so, I suggest you start with the Built-in render pipeline. You’ll miss out on learning the ”right way” of doing things in the future, but you’ll likely have a less frustrating experience.
I recommend URP if you’re starting today with the primary goal of learning because:
- SRP usually makes sense if you’re starting from scratch;
- If there’s a way to do something in URP, it’s more likely to be fairly obvious. If you find yourself trying to get something to work that doesn’t seem to, chances are it’s just not yet supported.
This advice is hotly contested, though, so your mileage may vary.
Lighting
When I started with Unity, I expected to spend approximately
0 minutes worrying about lighting. It turns out that I underestimated two things: (a) the difference good lighting makes in what my scene can look like, and (b) how hard it is to have lighting that is both good and performant. By the way, I highly recommend Unity’s Introduction to Lighting section of their user manual.

A simple scene constructed with primitive objects and a number of basic materials, shown under two lighting states. Above: Two angles showing a scene with only real-time lighting. Below: The same angles showing a scene with baked indirect lights, in addition to real-time lighting.
In the real world, light travels from its sources and onto objects in the world. Some of it is absorbed, and some of it scatters. An object in the shade will still be visible, even though no direct light shines on it from the sun. Computers can simulate a very realistic rendering using ray tracing, which imitates the physics of light. But rendering an entire scene with ray tracing is quite expensive, and broadly speaking, it can not be done reliably in real-time.
NVIDIA has done a lot of work on real-time ray tracing for its RTX series, and they have some pretty cool examples. Still, it’s not something broadly available on PC and not at all an option on Mobile and Switch.
In the real-time context of a game, there are different strategies to deal with light. Let’s describe a few of these concepts:
Direct and Indirect Lighting
If a light ray leaving its source bounces on an object exactly once and reaches the camera, the light is direct. The part of the object that the light bounced off of is directly lit. This light is easy to compute, given the color, intensity, and distance of the light and the physical properties of the material.
Direct lighting and shadows due to a direct light are reasonably quick to compute. Depending on the rendering pipeline, however, an object lit by multiple lights in real-time will be rendered over multiple passes (one for each light). Unity, for instance, will limit the maximum number of direct lights that can influence an object.
Indirect lighting, on the other hand, is any other light that reaches the camera. This is light that has bounced off any number of surfaces before reaching us. Light coming directly or indirectly from a 3D scene’s skybox, which is somewhat emissive, (imitating the scattered light throughout the sky that illuminates the world) will contribute to indirect lighting.
Indirect lighting is crucial in rendering a realistic scene. Imagine a cube in a daylight 3D scene. The faces of the cube in shadow should not appear completely black.
For example, in the figure above, the left side predominantly shows shaded faces of our 3D objects. Only direct lights are accounted for in the top-left corner, thus making the objects appear excessively dark and hard to tell apart.
Global Illumination
A scene’s global illumination (GI) represents the totality of light from all sources. Unity offers no supported way to compute global illumination in real time 1 , and instead provides a Baked Global Illumination system that uses a few techniques that work together to produce GI:
- Lightmapping (a.k.a baked lightmaps) is the process of precomputing certain portions of global illumination in the scene.
- Light Probes are objects that allow lightmaps to take additional measurements in specific points in the scene for dynamic moving objects to interpolate.
- Reflection Probes are objects that allow reflective objects in their sphere of influence to show a reflection.
The Baked GI system offers a few lighting modes depending on your rendering pipelines. These lighting modes determine what light is included in baked lightmaps and how it should be blended with real-time lighting information to produce the final result.
The baked indirect Backed GI light mode, for example, will only include indirect lights in the lightmap, and that with real-time direct lights.
Other than what parts of GI are captured in a lightmap, another critical question is, what object faces can have their lighting precomputed in a lightmap? Since lightmapping is useless for an object that moves around, Unity provides us with a way of marking objects that are never supposed to move as Static. When an object is marked as Static, then it will be included in the lightmapping process. Your environment (ground, buildings, houses, roads, trees, etc.) should all be marked as static, and therefore it will receive high-quality precomputed lighting information.
A lightmap consists of one or more textures that record the state of the light (indirect or combined, depending on the light mode) on the surfaces of static objects and light probes.

(a) A scene with only real-time direct lights active. Note the dark appearance of faces in the shadow.

(b) A scene with baked indirect light. Direct lights and shadows are still in real-time. Note the smoother colors, for example, the dark area under the leftmost grey cube.

(c) A scene with entirely baked lightmaps. Notice the increased quality of the shadows.

(d) A scene with entirely baked lightmaps in addition to reflection probes. Note the realistic colors around reflective objects.
Baking more of your lights will give you a better appearance and generally also better performance. However, you can only bake the parts of your scene that are static. Baking direct light means that your main light source should also be static (in other words, no day-night cycle for you).
What happens to dynamic objects that move around or animate? The standard strategy here is to place light probes throughout your scene. Your dynamic objects will then blend between their nearest probes to simulate indirect GI, in addition to direct lights. The Brackeys video on light probes is probably the best resource to learn more.
The last piece of the puzzle is reflection probes. Any object with any degree of smoothness will take on some of the colors of its surroundings. Reflection probes are objects that capture a spherical (or cubic) projection from a certain point, and objects within their range are given that projection as a reflection probe texture. The skybox in a 3D game also provides its own reflection probe to reflect off the sky. Reflection probes can be baked or real-time. Similar restrictions apply: a baked probe will only capture static objects, while a real-time probe is slower but captures moving objects. If you’re in a dynamic scene, a mirror might need a dynamic probe, but a subtly shiny part of an object can probably be represented in higher quality with a baked probe. Brackeys also has a video on Reflection probes that I quite like.
In Closing
With this, you should have the resources you need to design beautiful scenes that fit the style of your game. I hope you don’t end up spending hours in the night debugging lights or shadows in your game!
Unity’s Lighting Overview documentation will make an excellent reference as you progress in your journey.
Footnotes
Unity offered a now-deprecated real-time GI system called Enlighten. ↩
Использование универсального конвейера рендеринга
Универсальный конвейер рендеринга (URP) — это готовый конвейер рендеринга с поддержкой сценариев, созданный Unity. URP предлагает удобные для художников рабочие процессы, которые позволяют быстро и легко создавать оптимизированную графику для различных платформ, от мобильных до консолей высокого класса и ПК.

Предыдущая версия URP называлась Lightweight Render Pipeline (LWRP). URP заменяет LWRP.
Custom Render Pipeline
This is the first part of a tutorial series about creating a custom scriptable render pipeline. It covers the initial creation of a bare-bones render pipeline that we will expand in the future.
This series assumes that you’ve worked through at least the Object Management series and the Procedural Grid tutorial.
This tutorial is made with Unity 2019.2.6f1.
What about the other SRP series?
I have another tutorial series covering the scriptable render pipeline, but that one uses the experimental SRP API which only works with Unity 2018. This series is for Unity 2019 and later. This series takes a different and more modern approach but will cover at lot of the same topics. It’s still useful to work through the 2018 series if you don’t want to wait until this one has caught up with it.
Rendering with a custom render pipeline.
A new Render Pipeline
To render anything, Unity has to determine what shapes have to be drawn, where, when, and with what settings. This can get very complex, depending on how many effects are involved. Lights, shadows, transparency, image effects, volumetric effects, and so on all have to be dealt with in the correct order to arrive at the final image. This is what a render pipeline does.
In the past Unity only supported a few built-in ways to render things. Unity 2018 introduced scriptable render pipelines—RPs for short—making it possible to do whatever we want, while still being able to rely on Unity for fundamental steps like culling. Unity 2018 also added two experimental RPs made with this new approach: the Lightweight RP and the High Definition RP. In Unity 2019 the Lightweight RP is no longer experimental and got rebranded to the Universal RP in Unity 2019.3.
The Universal RP is destined to replace the current legacy RP as the default. The idea is that it is a one-size-fits-most RP that will also be fairly easy to customize. Rather than customizing that RP this series will create an entire RP from scratch.
This tutorial lays the foundation with a minimal RP that draws unlit shapes using forward rendering. Once that’s working, we can extend our pipeline in later tutorials, adding lighting, shadows, different rendering methods, and more advanced features.
Project Setup
Create a new 3D project in Unity 2019.2.6 or later. We’ll create our own pipeline, so don’t select one of the RP project templates. Once the project is open you can go to the package manager and remove all packages that you don’t need. We’ll only use the Unity UI package in this tutorial to experiment with drawing the UI, so you can keep that one.
We’re going to exclusively work in linear color space, but Unity 2019.2 still uses gamma space as the default. Go to the player settings via Edit / Project Settings and then Player, then switch Color Space under the Other Settings section to Linear.
Color space set to linear.
Fill the default scene with a few objects, using a mix of standard, unlit opaque and transparent materials. The Unlit/Transparent shader only works with a texture, so here is a UV sphere map for that.
UV sphere alpha map, on black background.
I put a few cubes in my test scene, all of which are opaque. The red ones use a material with the Standard shader while the green and yellow ones use a material with the Unlit/Color shader. The blue spheres use the Standard shader with Rendering Mode set to Transparent, while the white spheres use the Unlit/Transparent shader.
Test scene.
Pipeline Asset
Currently, Unity uses the default render pipeline. To replace it with a custom render pipeline we first have to create an asset type for it. We’ll use roughly the same folder structure that Unity uses for the Universal RP. Create a Custom RP asset folder with a Runtime child folder. Put a new C# script in there for the CustomRenderPipelineAsset type.
Folder structure.
The asset type must extend RenderPipelineAsset from the UnityEngine.Rendering namespace.
The main purpose of the RP asset is to give Unity a way to get a hold of a pipeline object instance that is responsible for rendering. The asset itself is just a handle and a place to store settings. We don’t have any settings yet, so all we have to do is give Unity a way to get our pipeline object instance. That’s done by overriding the abstract CreatePipeline method, which should return a RenderPipeline instance. But we haven’t defined a custom RP type yet, so begin by returning null .
The CreatePipeline method is defined with the protected access modifier, which means that only the class that defined the method—which is RenderPipelineAsset —and those that extend it can access it.
Now we need to add an asset of this type to our project. To make that possible, add a CreateAssetMenu attribute to CustomRenderPipelineAsset .
That puts an entry in the Asset / Create menu. Let’s be tidy and put it in a Rendering submenu. We do that by setting the menuName property of the attribute to Rendering/Custom Render Pipeline. This property can be set directly after the attribute type, within round brackets.
Use the new menu item to add the asset to the project, then go to the Graphics project settings and select it under Scriptable Render Pipeline Settings.
Custom RP selected.
Replacing the default RP changed a few things. First, a lot of options have disappeared from the graphics settings, which is mentioned in an info panel. Second, we’ve disabled the default RP without providing a valid replacement, so nothing gets rendered anymore. The game window, scene window, and material previews are no longer functional. If you open the frame debugger—via Window / Analysis / Frame Debugger—and enable it, you will see that indeed nothing gets drawn in the game window.
Render Pipeline Instance
Create a CustomRenderPipeline class and put its script file in the same folder as CustomRenderPipelineAsset . This will be the type used for the RP instance that our asset returns, thus it must extend RenderPipeline .
RenderPipeline defines a protected abstract Render method that we have to override to create a concrete pipeline. It has two parameters: a ScriptableRenderContext and a Camera array. Leave the method empty for now.
Make CustomRenderPipelineAsset.CreatePipeline return a new instance of CustomRenderPipeline . That will get us a valid and functional pipeline, although it doesn’t render anything yet.
Rendering
Each frame Unity invokes Render on the RP instance. It passes along a context struct that provides a connection to the native engine, which we can use for rendering. It also passes an array of cameras, as there can be multiple active cameras in the scene. It is the RP’s responsibility to render all those cameras in the order that they are provided.
Camera Renderer
Each camera gets rendered independently. So rather than have CustomRenderPipeline render all camera’s we’ll forward that responsibility to a new class dedicated to rendering one camera. Name it CameraRenderer and give it a public Render method with a context and a camera parameter. Let’s store these parameters in fields for convenience.
Have CustomRenderPipeline create an instance of the renderer when it gets created, then use it to render all cameras in a loop.
Our camera renderer is roughly equivalent to the scriptable renderers of the Universal RP. This approach will make it simple to support different rendering approaches per camera in the future, for example one for the first-person view and one for a 3D map overlay, or forward vs. deferred rendering. But for now we’ll render all cameras the same way.
Drawing the Skybox
The job of CameraRenderer.Render is to draw all geometry that its camera can see. Isolate that specific task in a separate DrawVisibleGeometry method for clarity. We’ll begin by having it draw the default the skybox, which can be done by invoking DrawSkybox on the context with the camera as an argument.
This does not yet make the skybox appear. That’s because the commands that we issue to the context are buffered. We have to submit the queued work for execution, by invoking Submit on the context. Let’s do this in a separate Submit method, invoked after DrawVisibleGeometry .
The skybox finally appears in both the game and scene window. You can also see an entry for it in the frame debugger when you enable it. It’s listed as Camera.RenderSkybox, which has a single Draw Mesh item under it, which represents the actual draw call. This corresponds to the rendering of the game window. The frame debugger doesn’t report drawing in other windows.
Skybox gets drawn.
Note that the orientation of the camera currently doesn’t affect how the skybox gets rendered. We pass the camera to DrawSkybox , but that’s only used to determine whether the skybox should be drawn at all, which is controlled via the camera’s clear flags.
To correctly render the skybox—and the entire scene—we have to set up the view-projection matrix. This transformation matrix combines the camera’s position and orientation—the view matrix—with the camera’s perspective or orthographic projection—the projection matrix. It is known in shaders as unity_MatrixVP, one of the shader properties used when geometry gets drawn. You can inspect this matrix in the frame debugger’s ShaderProperties section when a draw call is selected.
At the moment, the unity_MatrixVP matrix is always the same. We have to apply the camera’s properties to the context, via the SetupCameraProperties method. That sets up the matrix as well as some other properties. Do this before invoking DrawVisibleGeometry , in a separate Setup method.
Skybox, correctly aligned.
Command Buffers
The context delays the actual rendering until we submit it. Before that, we configure it and add commands to it for later execution. Some tasks—like drawing the skybox—can be issued via a dedicated method, but other commands have to be issued indirectly, via a separate command buffer. We need such a buffer to draw the other geometry in the scene.
To get a buffer we have to create a new CommandBuffer object instance. We need only one buffer, so create one by default for CameraRenderer and store a reference to it in a field. Also give the buffer a name so we can recognize it in the frame debugger. Render Camera will do.
How does that object initializer syntax work?
It’s as if we’ve written buffer.name = bufferName; as a separate statement after invoking the constructor. But when creating a new object, you can append a code block to the constructor’s invocation. Then you can set the object’s fields and properties in the block without having to reference the object instance explicitly. It makes explicit that the instances should only be used after those fields and properties have been set. Besides that, it makes initialization possible where only a single statement is allowed—for example a field initialization, which we’re using here—without requiring constructors with many parameter variants.
Note that we omitted the empty parameter list of the constructor invocation, which is allowed when object initializer syntax is used.
We can use command buffers to inject profiler samples, which will show up both in the profiler and the frame debugger. This is done by invoking BeginSample and EndSample at the appropriate points, which is at the beginning of Setup and Submit in our case. Both methods must be provided with the same sample name, for which we’ll use the buffer’s name.
To execute the buffer, invoke ExecuteCommandBuffer on the context with the buffer as an argument. That copies the commands from the buffer but doesn’t clear it, we have to do that explicitly afterwards if we want to reuse it. Because execution and clearing is always done together it’s handy to add a method that does both.
The Camera.RenderSkyBox sample now gets nested inside Render Camera.
Render camera sample.
Clearing the Render Target
Whatever we draw ends up getting rendered to the camera’s render target, which is the frame buffer by default but could also be a render texture. Whatever was drawn to that target earlier is still there, which could interfere with the image that we are rendering now. To guarantee proper rendering we have to clear the render target to get rid of its old contents. That’s done by invoking ClearRenderTarget on the command buffer, which belongs in the Setup method.
CommandBuffer.ClearRenderTarget requires at least three arguments. The first two indicate whether the depth and color data should be cleared, which is true for both. The third argument is the color used to clearing, for which we’ll use Color.clear .
Clearing, with nested sample.
The frame debugger now shows a Draw GL entry for the clear action, which shows up nested in an additional level of Render Camera. That happens because ClearRenderTarget wraps the clearing in a sample with the command buffer’s name. We can get rid of the redundant nesting by clearing before beginning our own sample. That results in two adjacent Render Camera sample scopes, which get merged.
Clearing, without nesting.
The Draw GL entry represent drawing a full-screen quad with the Hidden/InternalClear shader that writes to the render target, which isn’t the most efficient way to clear it. This approach is used because we’re clearing before setting up the camera properties. If we swap the order of those two steps we get the quick way to clear.
Correct clearing.
Now we see Clear (color+Z+stencil), which indicates that both the color and depth buffers get cleared. Z represents the depth buffer and the stencil data is part the same buffer.
Culling
We’re currently seeing the skybox, but not any of the objects that we put in the scene. Rather than drawing every object, we’re only going to render those that are visible to the camera. We do that by starting with all objects with renderer components in the scene and then culling those that fall outside of the view frustum of the camera.
Figuring out what can be culled requires us to keep track of multiple camera settings and matrices, for which we can use the ScriptableCullingParameters struct. Instead of filling it ourselves, we can invoke TryGetCullingParameters on the camera. It returns whether the parameters could be successfully retrieved, as it might fail for degenerate camera settings. To get hold of the parameter data we have to supply it as an output argument, by writing out in front of it. Do this in a separate Cull method that returns either success or failure.
Why do we have to write out ?
When a struct parameter is defined as an output parameter it acts like an object reference, pointing to the place on the memory stack where the argument resides. When the method changes the parameter it affects that value, not a copy.
The out keyword tells us that the method is responsible for correctly setting the parameter, replacing the previous value.
Try-get methods are a common way to both indicate success or failure and produce a result.
It is possible to inline the variable declaration inside the argument list when used as an output argument, so let’s do that.
Invoke Cull before Setup in Render and abort if it failed.
Actual culling is done by invoking Cull on the context, which produces a CullingResults struct. Do this in Cull if successful and store the results in a field. In this case we have to pass the culling parameters as a reference argument, by writing ref in front of it.
Why do we have to use ref ?
The ref keyword works just like out , except that the method is not required to assign something to it. Whoever invokes the method is responsible for properly initializing the value first. So it can be used for input and optionally for output.
In this case ref is used as an optimization, to prevent passing a copy of the ScriptableCullingParameters struct, which is quite large. It being a struct instead of an object is another optimization, to prevent memory allocations.
Drawing Geometry
Once we know what is visible we can move on to rendering those things. That is done by invoking DrawRenderers on the context with the culling results as an argument, telling it which renderers to use. Besides that, we have to supply drawing settings and filtering settings. Both are structs— DrawingSettings and FilteringSettings —for which we’ll initially use their default constructors. Both have to be passed by reference. Do this in DrawVisibleGeometry , before drawing the skybox.
We don’t see anything yet because we also have to indicate which kind of shader passes are allowed. As we only support unlit shaders in this tutorial we have to fetch the shader tag ID for the SRPDefaultUnlit pass, which we can do once and cache it in a static field.
Provide it as the first argument of the DrawingSettings constructor, along with a new SortingSettings struct value. Pass the camera to the constructor of the sorting settings, as it’s used to determine whether orthographic or distance-based sorting applies.
Besides that we also have to indicate which render queues are allowed. Pass RenderQueueRange.all to the FilteringSettings constructor so we include everything.
Drawing unlit geometry.
Only the visible objects that use the unlit shader get drawn. All the draw calls are listed in the frame debugger, grouped under RenderLoop.Draw. There’s something weird going on with transparent objects, but let’s first look at the order in which the objects are drawn. That’s shown by the frame debugger and you can step through the draw calls by selecting one after the other or using the arrow keys.
The drawing order is haphazard. We can force a specific draw order by setting the criteria property of the sorting settings. Let’s use SortingCriteria.CommonOpaque .
Common opaque sorting.
Objects now get more-or-less drawn front-to-back, which is ideal for opaque objects. If something ends up drawn behind something else its hidden fragments can be skipped, which speeds up rendering. The common opaque sorting option also takes some other criteria into consideration, including the render queue and materials.
Drawing Opaque and Transparent Geometry Separately
The frame debugger shows us that transparent objects get drawn, but the skybox gets drawn over everything that doesn’t end up in front of an opaque object. The skybox gets drawn after the opaque geometry so all its hidden fragments can get skipped, but it overwrites transparent geometry. That happens because transparent shaders do not write to the depth buffer. They don’t hide whatever’s behind them, because we can see through them. The solution is to first drawn opaque objects, then the skybox, and only then transparent objects.
We can eliminate the transparent objects from the initial DrawRenderers invocation by switching to RenderQueueRange.opaque .
Then after drawing the skybox invoke DrawRenderers again. But before doing so change the render queue range to RenderQueueRange.transparent . Also change the sorting criteria to SortingCriteria.CommonTransparent and again set the sorting of the drawing settings. That reverses the draw order of the transparent objects.
Opaque, then skybox, then transparent.
Why is the draw order reversed?
As transparent objects do not write to the depth buffer sorting them front-to-back has no performance benefit. But when transparent objects end up visually behind each other they have to be drawn back-to-front to correctly blend.
Unfortunately back-to-front sorting does not guarantee correct blending, because sorting is per-object and only based on the object’s position. Intersecting and large transparent objects can still produce incorrect results. This can sometimes be solved by cutting the geometry in smaller parts.
Editor Rendering
Our RP correctly draws unlit objects, but there are a few things that we can do to improve the experience of working with it in the Unity editor.
Drawing Legacy Shaders
Because our pipeline only supports unlit shaders passes, objects that use different passes are not rendered, making them invisible. While this is correct, it hides the fact that some objects in the scene use the wrong shader. So let’s render them anyway, but separately.
If someone were to start with a default Unity project and later switch to our RP then they might have objects with the wrong shader in their scenes. To cover all Unity’s default shaders we have to use shaders tag IDs for the Always, ForwardBase, PrepassBase, Vertex, VertexLMRGBM, and VertexLM passes. Keep track of these in a static array.
Draw all unsupported shaders in a separate method after the visible geometry, starting with just the first pass. As these are invalid passes the results will be wrong anyway so we don’t care about the other settings. We can get default filtering settings via the FilteringSettings.defaultValue property.
We can draw multiple passes by invoking SetShaderPassName on the drawing settings with a draw order index and tag as arguments. Do this for all passes in the array, starting at the second as we already set the first pass when constructing the drawing settings.
Standard shader renders black.
Objects rendered with the standard shader show up, but they’re now solid black because our RP hasn’t set up the required shader properties for them.
Error Material
To clearly indicate which objects use unsupported shaders we’ll draw them with Unity’s error shader. Construct a new material with that shader as an argument, which we can find by invoking Shader.Find with the Hidden/InternalErrorShader string as an argument. Cache the material via a static field so we won’t create a new one each frame. Then assign it to the overrideMaterial property of the drawing settings.
Rendered with magenta error shader.
Now all invalid objects are visible and obviously wrong.
Partial Class
Drawing invalid objects is useful for development but is not meant for released apps. So let’s put all editor-only code for CameraRenderer in a separate partial class file. Begin by duplicating the original CameraRenderer script asset and renaming it to CameraRenderer.Editor.
One class, two script assets.
Then turn the original CameraRenderer into a partial class and remove the tag array, error material, and DrawUnsupportedShaders method from it.
What are partial classes?
It’s a way to split a class—or struct—definition into multiple parts, stored in different files. The only purpose is to organize code. The typical use case is to keep automatically-generated code separate from manually-written code. As far as the compiler is concerned, it’s all part of the same class definition. They were introduced in the Object Management, More Complex Levels tutorial.
Clean the other partial class file so it only contains what we removed from the other.
The content of the editor part only needs to exist in the editor, so make it conditional on UNITY_EDITOR.
However, making a build will fail at this point, because the other part always contains the invocation of DrawUnsupportedShaders , which now only exists while in the editor. To solve this we make that method partial as well. We do that by always declaring the method signature with partial in front of it, similar to an abstract method declaration. We can do that in any part of the class definition, so let’s put it in the editor part. The full method declaration must be marked with partial as well.
Compilation for a build now succeeds. The compiler will strip out the invocation of all partial methods that didn’t end up with a full declaration.
Can we make the invalid objects appear in development builds?
Yes, you can base the conditional compilation on UNITY_EDITOR || DEVELOPMENT_BUILD instead. Then DrawUnsupportedShaders exists in development builds as well and still not in release builds. But I’ll consistently limit everything development-related to the editor only in this series.
Drawing Gizmos
Currently our RP doesn’t draw gizmos, neither in the scene window nor in the game window if they are enabled there.
Scene without gizmos.
We can check whether gizmos should be drawn by invoking UnityEditor.Handles.ShouldRenderGizmos . If so, we have to invoke DrawGizmos on the context with the camera as an argument, plus a second argument to indicate which gizmo subset should be drawn. There are two subsets, for before and after image effects. As we don’t support image effects at this point we’ll invoke both. Do this in a new editor-only DrawGizmos method.
The gizmos should be drawn after everything else.
Scene with gizmos.
Drawing Unity UI
Another thing that requires our attention is Unity’s in-game user interface. For example, create a simple UI by adding a button via GameObject / UI / Button. It will show up in the game window, but not the scene window.
UI button in game window.
Why can’t I create a UI button?
You need to have the Unity UI package in your project.
The frame debugger shows us that the UI is rendered separately, not by our RP.
UI in frame debugger.
At least, that’s the case when the Render Mode of the canvas component is set to Screen Space — Overlay, which is the default. Changing it to Screen Space — Camera and using the main camera as its Render Camera will make it part of the transparent geometry.
Screen-space-camera UI in frame debugger.
The UI always uses the World Space mode when it gets rendered in the scene window, which is why it usually ends up very large. But while we can edit the UI via the scene window it doesn’t get drawn.
UI invisible in scene window.
We have to explicitly add the UI to the world geometry when rendering for the scene window, by invoking ScriptableRenderContext.EmitWorldGeometryForSceneView with the camera as an argument. Do this in a new editor-only PrepareForSceneWindow method. We’re rendering with the scene camera when its cameraType property is equal to CameraType.SceneView .
As that might add geometry to the scene it has to be done before culling.
UI visible in scene window.
Multiple Cameras
It is possible to have more that one active camera in the scene. If so, we have to make sure that they work together.
Two Cameras
Each camera has a Depth value, which is −1 for the default main camera. They get rendered in increasing order of depth. To see this, duplicate the Main Camera, rename it to Secondary Camera, and set its Depth to 0. It’s also a good idea to give it another tag, as MainCamera is supposed to be used by only a single camera.
Both cameras grouped in a single sample scope.
The scene now gets rendered twice. The resulting image is still the same because the render target gets cleared in between. The frame debugger shows this, but because adjacent sample scopes with the same name get merged we end up with a single Render Camera scope.
It’s clearer if each camera gets its own scope. To make that possible, add an editor-only PrepareBuffer method that makes the buffer’s name equal to the camera’s.
Invoke it before we prepare the scene window.
Separate samples per camera.
Dealing with Changing Buffer Names
Although the frame debugger now shows a separate sample hierarchy per camera, when we enter play mode Unity’s console will get filled with messages warning us that BeginSample and EndSample counts must match. It gets confused because we’re using different names for the samples and their buffer. Besides that, we also end up allocating memory each time we access the camera’s name property, so we don’t want to do that in builds.
To tackle both issues we’ll add a SampleName string property. If we’re in the editor we set it in PrepareBuffer along with the buffer’s name, otherwise it’s simply a constant alias for the Render Camera string.
Use SampleName for the sample in Setup and Submit .
We can see the difference by checking the profiler—opened via Window / Analysis / Profiler—and playing in the editor first. Switch to Hierarchy mode and sort by the GC Alloc column. You’ll see an entry for two invocations of GC.Alloc, allocating 100 bytes in total, which is causes by the retrieval of the camera names. Further down you’ll see those names show up as samples: Main Camera and Secondary Camera.
Profiler with separate samples and 100B allocations.
Next, make a build with Development Build and Autoconnect Profiler enabled. Play the build and make sure that the profiler is connected and recording. In this case we don’t get the 100 bytes of allocation and we get the single Render Camera sample instead.
Profiling build.
What are the other 48 bytes allocated for?
It’s for the cameras array, over which we have no control. Its size depends on how many cameras get rendered.
We can make it clear that we’re allocating memory only in the editor and not in builds by wrapping the camera name retrieval in a profiler sample named Editor Only. In this case we need to invoke Profiler.BeginSample and Profiler.EndSample from the UnityEngine.Profiling namespace. Only BeginSample needs to be passed the name.
Editor-only allocations made obvious.
Layers
Cameras can also be configured to only see things on certain layers. This is done by adjusting their Culling Mask. To see this in action let’s move all objects that use the standard shader to the Ignore Raycast layer.
Layer switched to Ignore Raycast.
Exclude that layer from the culling mask of Main Camera.
Culling the Ignore Raycast layer.
And make it the only layer seen by Secondary Camera.
Culling everything but the Ignore Raycast layer.
Because Secondary Camera renders last we end up seeing only the invalid objects.
Only Ignore Raycast layer visible in game window.
Clear Flags
We can combine the results of both cameras by adjusting the clear flags of the second one that gets rendered. They’re defined by a CameraClearFlags enum which we can retrieve via the camera’s clearFlags property. Do this in Setup before clearing.
The CameraClearFlags enum defines four values. From 1 to 4 they are Skybox , Color€ , Depth , and Nothing . These aren’t actually independent flag values but represent a decreasing amount of clearing. The depth buffer has to be cleared in all cases except the last one, so when the flags value is at most Depth .
We only really need to clear the color buffer when flags are set to Color€ , because in the case of Skybox we end up replacing all previous color data anyway.
And if we’re clearing to a solid color we have to use the camera’s background color. But because we’re rendering in linear color space we have to convert that color to linear space, so we end up needing camera.backgroundColor.linear . In all other cases the color doesn’t matter, so we can suffice with Color.clear .
Because Main Camera is the first to render, its Clear Flags should be set to either Skybox or Color€ . When the frame debugger is enabled we always begin with a clear buffer, but this is not guaranteed in general.
The clear flags of Secondary Camera determines how the rendering of both cameras gets combined. In the case of skybox or color the previous results get completely replaced. When only depth is cleared Secondary Camera renders as normal except that it doesn’t draw a skybox, so the previous results show up as the background. When nothing gets cleared the depth buffer is retained, so unlit objects end up occluding invalid objects as if they were drawn by the same camera. However, transparent objects drawn by the previous camera have no depth information, so are drawn over, just like the skybox did earlier.
Clear color, depth-only, and nothing.
By adjusting the camera’s Viewport Rect it is also possible to reduce the rendered area to only a fraction of the entire render target. The rest of the render target remains unaffected. In this case clearing happens with the Hidden/InternalClear shader. The stencil buffer is used to limit rendering to the viewport area.
Reduced viewport of secondary camera, clearing color.
Note that rendering more than one camera per frame means culling, setup, sorting, etc. has to be done multiple times as well. Using one camera per unique point of view is typically the most efficient approach.
Масштабируемая и высокая производительность рендеринга
Universal Render Pipeline от Unity стал мощным решением, сочетающим красоту, скорость и производительность, а также поддержку всех целевых платформ Unity.
The Universal Rendering Pipeline is a scalable multiplatform render pipeline built on top of the Scriptable Render Pipeline (SRP) framework. Thanks to its scalability, customizability, and a rich feature set, URP offers you creative freedom in any type of project, from stylized visuals to physically based rendering.

Lost In Random by Zoink Games

LEGO® Builder's Journey от Light Brick Studio
Universal Render Pipeline
Упор на производительность
Однопроходный упреждающий рендеринг
Поддержка Shader Graph
Встроенный процесс рендеринга
Универсальность
Поддерживает как упреждающий, так и отложенный рендеринг

Mini Motorways by Dinosaur Polo Club
От 2D и 3D до AR/VR-проектов: Universal Render Pipeline позволяет не тратить время на доработку проекта для выпуска на новом устройстве.
- Mac и iOS
- Android
- Xbox One
- PlayStation 4
- Nintendo Switch
- Ведущие AR- и VR-платформы
- Windows и UWP

Lost In Random от студии Zoink Games
Возможность конфигурации рендеринга в Unity с помощью скриптов на C# позволяет вам:
- оптимизировать производительность для конкретных устройств;
- тонко настраивать процесс рендеринга в соответствии с конкретными требованиями;
- управлять потреблением вычислительных ресурсов.

Circuit Superstars by Original Fire Games
Universal Render Pipeline проще, чем встроенный процесс рендеринга, но улучшает качество графики. Перевод проекта со встроенного процесса рендеринга на Universal Render Pipeline должен обеспечивать аналогичную или улучшенную производительность. Прочтите статью в нашем блоге, чтобы узнать о том, как Universal Render Pipeline повышает частоту кадров без снижения качества графики.
Вы можете воспользоваться преимуществами готовых технологий уже сегодня. Обновите проекты с помощью средств перехода или создайте новый проект на основе нашего шаблона Universal через Unity Hub.

To help with your transition from the Built-in Render Pipeline, URP offers numerous rendering capabilities, from Camera Stacking to Light Cookies and Point Light Shadows, as well as Renderer Features such as Decals and Screen Space Ambient Occlusion (SSAO).
In addition, URP supports the latest node-based tools offered by Unity. Shader Graph allows you to visually author shaders in real-time. With VFX Graph, you can leverage the power of your GPU to create extraordinary VFX.

Thanks to its large platform reach, the URP already powers numerous successful games, such as the relaxing puzzle platformer LEGO Builder’s Journey and action-adventure game Lost in Random.
To help you start creating inspiring experiences in URP, Unity offers learning sample content including the URP Package Samples and the Gigaya living game sample.