In the Universal Render Pipeline (URP), "Camera Stacking", which overlaps drawings from multiple cameras, is often used to project UI or special 3D models (characters, weapons, decorative assets, etc.) in front of a 3D scene. However, when using this feature, you often encounter serious drawing quality issues such as severe jaggies (staircase artifacts) at the edges of the overlaid model. In this article, we will explain in an easy-to-understand manner why this bug occurs, its internal rendering mechanism, and the perfect workaround code and configuration steps that should be adopted in practice.
1. Root cause: MSAA timing issue in the rendering pipeline
First, why is MSAA (Multi-Sample Anti-Aliasing) enabled on the Base camera but not applied to objects on the Overlay camera? The answer lies in "When to execute MSAA resolution (Resolve)".
MSAA is a technology that divides one pixel into multiple subpixels (sample points), draws them, and finally averages (resolves) those samples to smooth the edges. In the URP rendering flow, processing proceeds in the following steps.
- Base Camera Drawing: Draws the 3D scene to a multi-sampled color buffer (MSAA buffer).
- Base camera rendering finished (Resolve): Once the scene is fully rendered, the multi-sampled color buffer is converted to a single-sample color buffer (this is where anti-aliasing is done).
- Overlay camera drawing: Directly renders and overwrites objects set in the Overlay camera to a flat color buffer that has already been resolved and MSAA functionality is turned off.
This way, when the Overlay camera starts processing, the color buffer has already been "resolved" and no additional data for multisampling is present in memory. Therefore, the Overlay camera's rendering is forced to normal rendering without MSAA, resulting in a jagged staircase phenomenon at the edges.
2. Blurry problem when using post process together.
Another problem is when Post-processing is enabled on both the Base and Overlay cameras. When applying post-processing to the Overlay camera, URP copies the entire screen to an "intermediate texture" and processes it. During this copying process, the overall sharpness of the screen is lost due to resolution mismatches or sampling method mismatches, causing text and superimposed 3D models to become blurry. Also, even if you assign a separate post-process controller to the Overlay camera side, it will conflict with the render pass of the Base camera side, causing problems where the effects you set (Bloom, Color Grading, etc.) will not blend as expected.
3. Two solution approaches that should be adopted in practice
To solve this problem, it is necessary to change the rendering order and allocation. Depending on your development context, adopt one of the following approaches:
Solution A: Porting to a Render Feature (custom render pass) (recommended)
The most recommended method is to stop using a "second camera" called the Overlay camera and insert objects in the middle of the single render pipeline of the Base camera. This allows you to finish drawing within a pure multisample buffer before MSAA is resolved.
Specific steps:
- Assign the layer of the 3D object displayed on the Overlay camera to a new layer, for example "`Overlay3D`".
- Exclude `Overlay3D` from the Base camera's ``Culling Mask'' (this excludes it from the normal drawing flow).
- Open the URP's "Forward Renderer Data (Universal Renderer Data)" asset and add "Render Objects" from "Add Renderer Feature" at the bottom of the inspector.
- Adjust the added "Render Objects" settings as follows.
- Name: `Overlay3DPass`
- Event: `After Rendering Transparents`
- Filters > Queue: `Opaque` and `Transparent`
- Filters > Layer Mask: Select only `Overlay3D`
- Overrides > Depth: Set 'Write Depth' to `True` and 'Depth Test' to `Less Equal` (optionally `Always` to clear the camera)
With this method, the overhead of the Overlay camera itself (camera update processing, culling calculations, buffer re-allocation, etc.) can be reduced, so it is very advantageous in terms of performance, and the settings (4x/8x) on the Base camera side are applied as is for MSAA.
Solution B: Manual resolution of multi-samples using RenderTexture (for individual display)
If separate drawing using the Overlay camera is absolutely necessary due to the screen configuration (for example, when you want to display a completely independent 3D character in the UI), dynamically create a multi-sample RenderTexture using a C# script and apply MSAA yourself to blend.
The following is an example of a robust control script that performs overlay drawing on RenderTexture with MSAA enabled and reflects it on RawImage etc.
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
[RequireComponent(typeof(Camera))]
public class MsaaOverlayController : MonoBehaviour
{\
public int msaaSamples = 4; // 2, 4, 8
private Camera overlayCamera;
private RenderTexture msaaColorBuffer;
private RenderTexture resolvedColorBuffer;
void Start()
{
overlayCamera = GetComponent();
InitializeBuffers();
}
void InitializeBuffers()
{
if (msaaColorBuffer != null) msaaColorBuffer.Release();
if (resolvedColorBuffer != null) resolvedColorBuffer.Release();
// 1. Create a temporary render texture with MSAA enabled
RenderTextureDescriptor desc = new RenderTextureDescriptor(Screen.width, Screen.height, RenderTextureFormat.ARGB32, 24);
desc.msaaSamples = msaaSamples; // Set the number of MSAA samples
desc.useMipMap = false;
desc.sRGB = true;
msaaColorBuffer = new RenderTexture(desc);
msaaColorBuffer.name = "MsaaOverlay_ColorBuffer";
msaaColorBuffer.Create();
// 2. Create a flat color buffer after resolution (for UI drawing)
RenderTextureDescriptor resolveDesc = desc;
resolvedDesc.msaaSamples = 1; // Set the number of samples to 1
resolvedColorBuffer = new RenderTexture(resolveDesc);
resolvedColorBuffer.name = "MsaaOverlay_ResolvedBuffer";
resolvedColorBuffer.Create();
// Set camera output destination
overlayCamera.targetTexture = msaaColorBuffer;
}
// Resolve MSAA in callback when rendering is complete
void OnPostRender()
{
if (msaaColorBuffer == null || resolvedColorBuffer == null) return;
// Issue "Resolve" command from MSAA texture to non-MSAA texture
Graphics.Blit(msaaColorBuffer, resolvedColorBuffer);
}
public RenderTexture GetResolvedTexture()
{
return resolvedColorBuffer;
}
void OnDestroy()
{
if (msaaColorBuffer != null) msaaColorBuffer.Release();
if (resolvedColorBuffer != null) resolvedColorBuffer.Release();
}
}
By attaching this script to the Overlay camera, capturing the camera's output, and assigning it to a RawImage on the UI, it is possible to render 3D objects drawn within the UI with very smooth contours without jaggies.