URP(Universal Render Pipeline)作为一个Unity预先构建好的SRP(Scriptable Render Pipeline),提供了一个对美术友好的工作流,让美术能够方便且快速地跨平台创造优化的图形。官方文档
1. UniversalRenderPipelineAsset类
要使用URP,需要先创建一个URP Asset,并且将它赋给Graphics Settings。URP Asset便是UniversalRenderPipelineAsset这个类的实例。其保存了URP的一些设置,这里我们不去详细介绍。这里要去关注的是CreatePipeline()方法,它继承于父类RenderPipelineAsset。
//
// 摘要:
// Create a IRenderPipeline specific to this asset.
//
// 返回结果:
// Created pipeline.
protected abstract RenderPipeline CreatePipeline();
URP Asset实现了父类的方法CreatePipeline(),在方法中URP Asset创建了管线对象UniversalRenderPipeline。
protected override RenderPipeline CreatePipeline()
{
...
return new UniversalRenderPipeline(this);
}
该方法可以理解为URP管线的总的入口函数。
2. UniversalRenderPipeline类
管线类UniversalRenderPipeline继承自RenderPipeline,其核心方法为Render(…)。
//
// 摘要:
// Defines custom rendering for this RenderPipeline.
//
// 参数:
// context: 可编程渲染的上下文
//
// cameras: 本帧所有需要渲染的相机
protected abstract void Render(ScriptableRenderContext context, Camera[] cameras);
2.1. Render(…)方法
Render(…)方法每帧都会被自动调用,在方法中,会处理本帧需要执行的所有渲染命令,来绘制本帧图像。以下为该方法的主要调用。
protected override void Render(ScriptableRenderContext renderContext, Camera[] cameras)
{
// 1.
BeginFrameRendering(renderContext, cameras);
...
// 2.
SortCameras(cameras);
// 3.
for (int i = 0; i < cameras.Length; ++i)
{
var camera = cameras[i];
if (IsGameCamera(camera))
{
// 4.
RenderCameraStack(renderContext, camera);
}
else
{
// 5.
BeginCameraRendering(renderContext, camera);
UpdateVolumeFramework(camera, null);
RenderSingleCamera(renderContext, camera);
EndCameraRendering(renderContext, camera);
}
}
// 6.
// 7.
EndFrameRendering(renderContext, cameras);
}
-
BeginFrameRendering:表示该帧即将开始渲染。
-
SortCameras:根据所有要渲染的相机的depth值进行排序,depth越小越先渲染。
-
遍历每一个相机。
-
如果当前相机是主相机(也就是cameraType == CameraType.Game且renderType != CameraRenderType.Overlay),则调用RenderCameraStack,这一步在接下来会详细描述。
-
如果当前相机不是游戏相机,比如SceneView相机、预览相机等,则调用相机渲染的通常步骤,BeginCameraRendering → UpdateVolumeFramework → RenderSingleCamera → EndCameraRendering,这些步骤暂且称作“相机渲染常规步骤”,后续再详细描述。
-
遍历相机完成。
-
EndFrameRendering:表示该帧渲染结束,提交后备缓冲区。
2.2. RenderCameraStack(…)方法
如上文所述,Render(…)方法里如果遍历到主相机,就会去调用RenderCameraStack(…)。RenderCameraStack(…)方法主要是去遍历主相机的CameraStack里的每一个Overlay相机,并且把主相机和所有生效的Overlay相机全部渲染出来。以下为该方法的主要调用。
static void RenderCameraStack(ScriptableRenderContext context, Camera baseCamera)
{
// 1.
baseCamera.TryGetComponent<UniversalAdditionalCameraData>(out var baseCameraAdditionalData);
List<Camera> cameraStack = baseCameraAdditionalData.cameraStack;
bool anyPostProcessingEnabled = renderer.supportedRenderingFeatures.cameraStacking;
int lastActiveOverlayCameraIndex = -1;
// 2.
if (cameraStack != null && cameraStack.Count > 0)
{
for (int i = 0; i < cameraStack.Count; ++i)
{
Camera currCamera = cameraStack[i];
//<更新设置anyPostProcessingEnabled和lastActiveOverlayCameraIndex>...</>//
}
}
// 3.
bool isStackedRendering = lastActiveOverlayCameraIndex != -1;
// 4.
BeginCameraRendering(context, baseCamera);
UpdateVolumeFramework(baseCamera, baseCameraAdditionalData);
InitializeCameraData(baseCamera, baseCameraAdditionalData, out var baseCameraData);
RenderSingleCamera(context, baseCameraData, !isStackedRendering, anyPostProcessingEnabled);
EndCameraRendering(context, baseCamera);
// 5.
if (!isStackedRendering)
return;
// 6.
for (int i = 0; i < cameraStack.Count; ++i)
{
var currCamera = cameraStack[i];
if (!currCamera.isActiveAndEnabled)
continue;
currCamera.TryGetComponent<UniversalAdditionalCameraData>(out var currCameraData);
if (currCameraData != null)
{
// 7.
bool lastCamera = i == lastActiveOverlayCameraIndex;
BeginCameraRendering(context, currCamera);
UpdateVolumeFramework(currCamera, currCameraData);
InitializeAdditionalCameraData(currCamera, currCameraData, ref overlayCameraData);
RenderSingleCamera(context, overlayCameraData, lastCamera, anyPostProcessingEnabled);
EndCameraRendering(context, currCamera);
}
}
// 8.
}
-
初始化anyPostProcessingEnabled和lastActiveOverlayCameraIndex,用于当作参数传入接下来所有生效相机的“相机渲染常规步骤”里面。
-
遍历主相机的CameraStack的每一个相机,如果其中任何一个生效的相机需要渲染后期效果(Post-Processing),则anyPostProcessingEnabled为true;将最后一个相机的遍历序号i记录在lastActiveOverlayCameraIndex变量里。遍历每一个相机时候都会去检查它的“生效”性,如果不是“生效”的相机就会提示警告。“生效”的检查范围包括:是否Active,是否Enabled,是否是Overlay相机,是否scriptableRenderer类型和主相机保持一致。
-
如果没有遍历访问到任何Overlay相机,则lastActiveOverlayCameraIndex依然为-1,反之则大于-1。根据lastActiveOverlayCameraIndex的值就可以判断有没有后续生效相机需要渲染,需不需要继续遍历CameraStack的所有相机,将这个判断得出的bool值保存到变量isStackedRendering里。
-
对主相机做“相机渲染常规步骤”,传入参数为“是否是最后一个相机”和“是否需要处理后期效果”。如果isStackedRendering == false则说明主相机已经是最后一个相机了,后面没有可以渲染的相机了;如果anyPostProcessingEnabled == true说明需要处理后期效果。
-
如果CameraStack里没有相机了,就返回吧。
-
开始遍历CameraStack里每一个相机。
-
对CameraStack里每一个相机做“相机渲染常规步骤”,传入参数也是“是否是最后一个相机”和“是否需要处理后期效果”。如果当前相机的遍历序号i等于lastActiveOverlayCameraIndex,那就说明这个相机已经是最后一个相机了,后面没有可以渲染的相机了;如果anyPostProcessingEnabled == true说明需要处理后期效果。
-
结束遍历CameraStack里每一个相机。
2.3. “相机渲染常规步骤”
2.3.1. BeginCameraRendering(…)方法
RenderPipeline的protected方法,表示某一个相机即将开始渲染,渲染相机的固定调用。
2.3.2. UpdateVolumeFramework(…)方法
更新当前相机是否在某一个后期效果的Volume内,如果在Volume内则触发对应的后期效果。
2.3.3. InitializeCameraData(…)方法
首先根据官方文档,在URP里相机上会绑一个叫做UniversalAdditionalCameraData的脚本。该脚本包含的变量,描述了该相机是否有某些渲染特性,比如是否需要渲染DepthTexture,是否需要渲染OpaqueTexture等。InitializeCameraData(…)就是将这些变量值从当前相机的UniversalAdditionalCameraData脚本里提取出来,供相机内的渲染使用。
2.3.4. RenderSingleCamera(…)方法
该方法用于渲染一个相机,其过程主要包括剪裁、设置渲染器、执行渲染器三步。后续会详细描述。
2.3.5. EndCameraRendering(…)方法
RenderPipeline的protected方法,表示某一个相机已经结束渲染,渲染相机的固定调用。
2.4. RenderSingleCamera(…)方法
如上文所述,方法过程主要包括剪裁、设置渲染器、执行渲染器三步。以下为该方法的主要调用。
/// <summary>
/// 渲染一个相机,其过程主要包括剪裁、设置渲染器、执行渲染器三步。
/// </summary>
/// <param name="context">渲染上下文用于记录执行过程中的命令。</param>
/// <param name="cameraData">相机渲染数据,里面可能包含了继承自相机的一些参数</param>
/// <param name="requiresBlitToBackbuffer">如果这是相机渲染栈里的最后一个相机则为true,否则为false</param>
/// <param name="anyPostProcessingEnabled">如果相机需要做后期效果处理则为true,否则为false</param>
static void RenderSingleCamera(ScriptableRenderContext context, CameraData cameraData, bool requiresBlitToBackbuffer, bool anyPostProcessingEnabled)
{
// 1.
var renderer = cameraData.renderer;
// 2.
if (!camera.TryGetCullingParameters(IsStereoEnabled(camera), out var cullingParameters))
return;
// 3.
CommandBuffer cmd = CommandBufferPool.Get(sampler.name);
// 4.
renderer.Clear(cameraData.renderType);
// 5.
renderer.SetupCullingParameters(ref cullingParameters, ref cameraData);
// 6.
context.ExecuteCommandBuffer(cmd);
// 7.
cmd.Clear();
// 8.
var cullResults = context.Cull(ref cullingParameters);
// 9.
InitializeRenderingData(asset, ref cameraData, ref cullResults, requiresBlitToBackbuffer, anyPostProcessingEnabled, out var renderingData);
// 10.
renderer.Setup(context, ref renderingData);
// 11.
renderer.Execute(context, ref renderingData);
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
context.Submit();
}
-
获取当前相机的渲染器renderer,其基类类型是ScriptableRenderer,可以继承以作扩展。
-
获得当前相机的剪裁参数,保存在变量cullingParameters里。
-
申请一个CommandBuffer来执行渲染命令。
-
清空渲染器,也就是重置里面的一些数据。
-
根据相机再去修改一下变量cullingParameters里的信息。
-
执行当前的渲染命令。
-
清空CommandBuffer,以供接下来渲染使用。
-
根据剪裁参数cullingParameters执行相机剪裁,并将剪裁结果储存在cullResults里面。
-
根据当前帧的剪裁结果、灯光状态等每帧可能会改变的数据,来初始化本帧渲染需要用到的渲染数据renderingData。
-
调用渲染器的Setup(…),主要是根据当前渲染数据,去设置本帧渲染需要用到的渲染过程到队列中,这些渲染过程在这里被命名为Pass,其基类类型为ScriptableRenderPass,可以继承扩展。后面会针对前向渲染器(ForwardRenderer)作详细描述。
-
调用渲染器的Execute(…),执行已经在队列中的渲染过程。后面会针对前向渲染器(ForwardRenderer)作详细描述。
3. ForwardRenderer类
ForwardRenderer继承于ScriptableRenderer,这个渲染器被所有支持URP的平台所支持。这个渲染器维护了一个ScriptableRenderPass的列表,每一帧都会往列表里加入Pass,帧中执行Pass得到每一个过程的渲染结果,帧末清空列表,等待下一帧的填充。它渲染的资源被序列化成ScriptableRendererData。
ScriptableRenderer里面最核心的两个方法是Setup(…)和Execute(…),这两个方法在每一帧里都会被执行。Setup(…)会根据渲染数据,将本帧要执行的Pass加入到ScriptableRenderPass的列表中;Execute(…)从ScriptableRenderPass的列表中将Pass按照渲染时序分类(即RenderPassEvent)取出来,并执行这个过程。
3.1. Setup(…)方法
该方法在ScriptableRenderer里面是一个虚方法,任何继承于ScriptableRenderer的子渲染器都需要去实现它。实现它的过程也就是将Pass加入队列的过程,由于队列是FIFO的,所以这个入队的过程也就是本帧内渲染的过程。
以下为该方法的主要调用。
public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData)
{
// 1.
if (cameraData.renderType == CameraRenderType.Base)
{
m_ActiveCameraColorAttachment = (createColorTexture) ? m_CameraColorAttachment : RenderTargetHandle.CameraTarget;
m_ActiveCameraDepthAttachment = (createDepthTexture) ? m_CameraDepthAttachment : RenderTargetHandle.CameraTarget;
...
}
else
{
m_ActiveCameraColorAttachment = m_CameraColorAttachment;
m_ActiveCameraDepthAttachment = m_CameraDepthAttachment;
}
ConfigureCameraTarget(m_ActiveCameraColorAttachment.Identifier(), m_ActiveCameraDepthAttachment.Identifier());
// 2.
for (int i = 0; i < rendererFeatures.Count; ++i)
{
if(rendererFeatures[i].isActive)
rendererFeatures[i].AddRenderPasses(this, ref renderingData);
}
//3.
if (mainLightShadows)
EnqueuePass(m_MainLightShadowCasterPass);
if (additionalLightShadows)
EnqueuePass(m_AdditionalLightsShadowCasterPass);
...
EnqueuePass(m_RenderOpaqueForwardPass);
...
// 如果创建了DepthTexture,我们需要复制它,否则我们可以将它渲染到renderbuffer。
if (!requiresDepthPrepass && renderingData.cameraData.requiresDepthTexture && createDepthTexture)
{
m_CopyDepthPass.Setup(m_ActiveCameraDepthAttachment, m_DepthTexture);
EnqueuePass(m_CopyDepthPass);
}
if (renderingData.cameraData.requiresOpaqueTexture)
{
Downsampling downsamplingMethod = UniversalRenderPipeline.asset.opaqueDownsampling;
m_CopyColorPass.Setup(m_ActiveCameraColorAttachment.Identifier(), m_OpaqueColor, downsamplingMethod);
EnqueuePass(m_CopyColorPass);
}
...
EnqueuePass(m_RenderTransparentForwardPass);
...
// 4.
if (lastCameraInTheStack)
{
// Post-processing将得到最终的渲染目标,不需要final blit pass。
if (applyPostProcessing)
{
m_PostProcessPass.Setup(...);
EnqueuePass(m_PostProcessPass);
}
...
// 执行FXAA或任何其他可能需要在AA之后运行的Post-Processing效果。
if (applyFinalPostProcessing)
{
m_FinalPostProcessPass.SetupFinalPass(sourceForFinalPass);
EnqueuePass(m_FinalPostProcessPass);
}
...
// 我们需要FinalBlitPass来得到最终的屏幕。
if (!cameraTargetResolved)
{
m_FinalBlitPass.Setup(cameraTargetDescriptor, sourceForFinalPass);
EnqueuePass(m_FinalBlitPass);
}
}
else if (applyPostProcessing)
{
m_PostProcessPass.Setup(...);
EnqueuePass(m_PostProcessPass);
}
}
- 如果当前相机是主相机,判断是否需要渲染到DepthTexture,如果需要就设置当前深度缓冲为m_CameraDepthAttachment,否则就渲染到相机默认渲染目标;判断是否需要渲染到ColorTexture,如果需要就设置当前颜色缓冲为m_CameraColorAttachment,否则就渲染到相机默认渲染目标。
需要渲染到ColorTexture的条件包括:打开MSAA、打开RenderScale、打开HDR、打开Post-Processing、打开渲染到OpaqueTexture、添加了自定义ScriptableRendererFeature等。 需要渲染到DepthTexture的条件主要是打开渲染到DepthTexture。
-
将所有自定义的ScriptableRendererFeature加入到ScriptableRenderPass的队列中。
-
将各种通用Pass根据各自条件加入到ScriptableRenderPass的队列中。
- 如果当前相机是本帧最后一个渲染的相机,则将一些需要最后Blit的Pass加入到ScriptableRenderPass的队列中。
3.2. Execute(…)
该方法在ScriptableRenderer里面是一个不用重写的公共方法,由于各个Pass的执行顺序在Setup(…)里已经确定,所以该方法已经没有重写的必要了。不过还是可以看一下Execute(…)里面发生了什么。以下为该方法的主要调用。
public void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
...
// 1.
FillBlockRanges(blockEventLimits, blockRanges);
...
// 2.
ExecuteBlock(RenderPassBlock.BeforeRendering, blockRanges, context, ref renderingData);
...
// Opaque blocks...
ExecuteBlock(RenderPassBlock.MainRenderingOpaque, blockRanges, context, ref renderingData, eyeIndex);
// Transparent blocks...
ExecuteBlock(RenderPassBlock.MainRenderingTransparent, blockRanges, context, ref renderingData, eyeIndex);
// Draw Gizmos...
DrawGizmos(context, camera, GizmoSubset.PreImageEffects);
// In this block after rendering drawing happens, e.g, post processing, video player capture.
ExecuteBlock(RenderPassBlock.AfterRendering, blockRanges, context, ref renderingData, eyeIndex);
}
-
每一个ScriptableRenderPass里都有一个RenderPassEvent字段,FillBlockRanges(…)根据这个字段,将ScriptableRenderPass分配到不同的Block里。也就是给Pass根据渲染阶段进行了一下分类,这样开发者可以比较直观地在某一个阶段插入渲染过程。
-
根据不同的渲染阶段,取出这个阶段所有Pass依次执行其中的渲染过程。
4. 总结
以上所述,即为URP的主体代码,详细代码(比如每一个ScriptableRenderPass,不同的渲染器,细节的渲染过程)可以细读com.unity.render-pipelines.universal里面的代码。
本文主旨是记录并描述URP的主体结构,为定制渲染管线、优化性能等需求提供参考。