渲染管线是一系列渲染操作的集合,在传统渲染管线一文中我简单介绍了渲染管线中的通用概念,本文将对Unity的通用渲染管线(Universal Render Pipeline, URP)进行介绍。
Unity提供了内置渲染管线(Built-In)和可编程渲染管线(SRP)两类渲染管线。内置渲染管线是Unity的默认渲染管线,其自定义选项有限。而可编程渲染管线可以通过脚本代码定制化管线,允许用户更多地控制渲染流程。搭建一个完整的渲染管线工作量巨大,为了简化用户的自定义开发流程,Unity提供了URP和HDRP两种SRP的模板。三种管线均可在不同平台上进行开发,其适用平台对比如下:
1 URP新增功能
内置管线和URP各有优势,两者的对比详细见Unity官方文档。下面对集中URP中的新增功能进行简介。
1.1 合批
SRP Batcher是Unity提供的一种合批技术,合批的对象是具有相同着色器变体的物体,而不要求材质球相同(传统合批的做法:相同材质,合并网格)。SRP Batcher 是一个低级渲染循环,使材质数据持久保留在 GPU 内存中。如果材质内容不变,SRP Batcher 不需要设置缓冲区并将缓冲区上传到 GPU。实际上,SRP Batcher 会使用专用的代码路径来快速更新大型 GPU 缓冲区中的 Unity 引擎属性
Unity官方文档中关于SRP Batcher 和Standard Batch的区别可见下图。
个人理解:无非就是把按照材质球合批变成了着色器变体合批。Unity以往渲染完就丢掉着色器变体内容和着色器参数,SRP Batch保存了着色器变体的Buffer,因此后续需要使用该着色器变体时直接上传渲染参数即可。
1.2 光照
1.2.1 Pass数量
URP与内置管线一样支持前向渲染路径和延迟渲染路径。内置渲染管线前向渲染路径是多pass的,当场景中有多个光源时,会产生多个pass来渲染光照,在移动端如果有多个光源的话,将会产生巨大的性能消耗。而URP使用单个pass,这个物体收到的所有光照都只会产生一个pass,所有的光源处理都可以在一个DrawCall中完成,对于性能的消耗就小得多。URP的缺点是光源的数量有限制,URP14.04中为9。
前向渲染就是在渲染物体受到多个光源光照的时候,分别对每个光源对该物体产生的影响进行计算,最后将所有光的渲染结果相加得到最终物体的颜色。见传统渲染管线的介绍。
1.2.2 光源
内置管线仅能调整外圈角度,而URP可以调整内外圈角度。此外URP支持2D光源,内置管线不支持。
1.3 渲染逻辑
URP中支持对图层进行排序,支持双面(前侧,背部)的渲染。两者在渲染管线上最大的区别见下图,这张图主要体现了不同管线扩展渲染逻辑的能力。内置管线主要是立即的渲染,且管线功能的扩展依赖于重写Monobehaviour中的虚函数,这些虚函数会被引擎在不同渲染时刻被自动调用。而URP的自定义逻辑从管道本身着手,具有更大的自由度。
通过C#脚本的顺序
2 URP创建
2.1 GUI操作
Unity提供了GUI界面创建URP管线,如下图所示。点击URP Asset(with 2D Rendererer) 或 URP Asset(with Universal Rendererer)。点击后URP Asset(with Universal Rendererer)后出现如下两个资源文件,分别为UniversalRenderPipelineAsset和UniversalRendererData的实例化。在下面的源码分析中进一步介绍。图中的Renderer List只有一个UniversalRendererData,实际上可以包含多个UniversalRendererData。多个UniversalRendererData对应着不同的渲染操作,用户可以通过指定camera的renderer属性来在这些渲染操作中进行切换。
2.2 源码分析
在编写SRP管线时,必须创建和自定义两个关键元素:渲染管线资源和渲染管线实例。实例是最终渲染时遵循的渲染流程,资源则是用于实例中部分数据的序列化文件。
2.2.1 渲染管线资源
渲染管线资源存储有关SRP配置数据的Unity资源。URP的渲染管线资源通过类UniversalRenderPipelineAsset创建,该类继承自所有SRP渲染管线的公共基类RenderPipelineAsset。RenderPipelineAsset具有两个特点:(1)抽象类,定义了其子类应实现的功能;(2)继承自Unity提供的支持用户自定义的数据资源文件ScriptableObject,因此可以实例化为磁盘文件。通过如下所示的RenderPipelineAsset接口信息可以看出,子类必须提供创建RenderPipeline的接口,RenderPipeline即SRP管线的另一个关键元素:渲染管线实例。
public abstract class RenderPipelineAsset : ScriptableObject
{
protected RenderPipelineAsset();
// Return the detail grass Shader for this pipeline.
public virtual Shader terrainDetailGrassShader { get; }
// Return the default SpeedTree v7 Shader for this pipeline.
public virtual Shader defaultSpeedTree7Shader { get; }
// Return the default Shader for this pipeline.
public virtual Shader defaultShader { get; }
// Gets the default 2D Mask Material used by Sprite Masks in Universal Render Pipeline.
public virtual Material default2DMaskMaterial { get; }
// Return the default 2D Material for this pipeline
public virtual Material default2DMaterial { get; }
// Return the default UI ETC1 Material for this pipeline.
public virtual Material defaultUIETC1SupportedMaterial { get; }
// Return the default UI overdraw Material for this pipeline
public virtual Material defaultUIOverdrawMaterial { get; }
// Return the default UI Material for this pipeline.
public virtual Material defaultUIMaterial { get; }
// Return the default Terrain Material for this pipeline.
public virtual Material defaultTerrainMaterial { get; }
// Return the default Line Material for this pipeline.
public virtual Material defaultLineMaterial { get; }
// Return the default particle Material for this pipeline.
public virtual Material defaultParticleMaterial { get; }
// Return the detail grass billboard Shader for this pipeline.
public virtual Shader terrainDetailGrassBillboardShader { get; }
// Returns the Shader Tag value for the render pipeline that is described by this asset
public virtual string renderPipelineShaderTag { get; }
// Return the detail lit Shader for this pipeline.
public virtual Shader terrainDetailLitShader { get; }
// Retrieves the default Autodesk Interactive masked Shader for this pipeline
public virtual Shader autodeskInteractiveMaskedShader { get; }
// Retrieves the default Autodesk Interactive transparent Shader for this pipeline.
public virtual Shader autodeskInteractiveTransparentShader { get; }
// Retrieves the default Autodesk Interactive Shader for this pipeline.
public virtual Shader autodeskInteractiveShader { get; }
// Return the default Material for this pipeline.
public virtual Material defaultMaterial { get; }
// Returns the names of the Rendering Layer Masks for this pipeline, with each name
// prefixed by a unique numerical ID.
public virtual string[] prefixedRenderingLayerMaskNames { get; }
// Returns the names of the Rendering Layer Masks for this pipeline
public virtual string[] renderingLayerMaskNames { get; }
// The render index for the terrain brush in the edito
public virtual int terrainBrushPassIndex { get; }
// Return the default SpeedTree v8 Shader for this pipeline.
public virtual Shader defaultSpeedTree8Shader { get; }
// Create a IRenderPipeline specific to this asset
protected abstract RenderPipeline CreatePipeline();
// Default implementation of OnDisable for RenderPipelineAsset. See ScriptableObject.OnDisable
protected virtual void OnDisable();
// Default implementation of OnValidate for RenderPipelineAsset. See MonoBehaviour.OnValidate
protected virtual void OnValidate();
UniversalRenderPipelineAsset部分代码如下所示,该类具有两个重要的成员m_Renderers和m_RendererDataList。其中m_RendererDataList(ScriptableRendererData )放置构造m_Renderers(ScriptableRenderer)的资源。
在CreatePipeline函数中,我们可以看到有以下两个主要的操作。这两个操作的本质都是通过资源创建实例。
- CreateRenderers:使用m_RendererDataList来创建对应的一组m_Renderers。
- 使用UniversalRenderPipelineAsse创建UniversalRenderPipeline。
class UniversalRenderPipelineAsset
{
ScriptableRenderer[] m_Renderers = new ScriptableRenderer[1];
[SerializeField] internal ScriptableRendererData m_RendererDataList= null;
internal int m_DefaultRendererIndex = 0;
protected override RenderPipeline CreatePipeline()
{
if (m_RendererDataList == null)
m_RendererDataList = new ScriptableRendererData[1];
// If no default data we can't create pipeline instance
if (m_RendererDataList[m_DefaultRendererIndex] == null)
if (k_AssetPreviousVersion != k_AssetVersion)
return null;
if (m_RendererDataList[m_DefaultRendererIndex].GetType().ToString()
.Contains("Universal.ForwardRendererData"))
return null;
Debug.LogError(
$"Default Renderer is missing, make sure there is a Renderer assigned as the default on the current Universal RP asset:{UniversalRenderPipeline.asset.name}",this);
return null;
}
DestroyRenderers();
var pipeline = new UniversalRenderPipeline(this);
CreateRenderers();
foreach (var data in m_RendererDataList)
{
if (data is UniversalRendererData universalData)
{
Blitter.Initialize(universalData.shaders.coreBlitPS, universalData.shaders.coreBlitColorAndDepthPS);
break;
}
}
return pipeline;
}
}
2.2.2 渲染管线实例
渲染管线实例定义了SRP渲染管线的基本流程,管理各种渲染管线资源和渲染任务的导入和执行。所有的自定义渲染管线实例应继承自抽象类RenderPipeline。RenderPipeline封装的接口如下所示。其中BeginContextRendering是BeginFrameRendering的升级,可以避免不必要的堆分配和垃圾收集。此外,子类必须实现Render函数的功能,这是管线真正的渲染过程。具体UniversalRenderPipeline的内容自行查看源码,下一章节会介绍其中核心的渲染逻辑。
public abstract class RenderPipeline
{
protected RenderPipeline();
// Returns true when the RenderPipeline is invalid or destroyed.
public bool disposed { get; }
public virtual RenderPipelineGlobalSettings defaultSettings { get; }
public static void SubmitRenderRequest<RequestData>(Camera camera, RequestData data);
public static bool SupportsRenderRequest<RequestData>(Camera camera, RequestData data);
protected static void BeginCameraRendering(ScriptableRenderContext context, Camera camera);
protected static void EndCameraRendering(ScriptableRenderContext context, Camera camera);
protected static void BeginContextRendering(ScriptableRenderContext context, List<Camera> cameras);
protected static void EndContextRendering(ScriptableRenderContext context, List<Camera> cameras);
protected static void BeginFrameRendering(ScriptableRenderContext context, Camera[] cameras);
protected static void EndFrameRendering(ScriptableRenderContext context, Camera[] cameras);
protected virtual void Dispose(bool disposing);
protected virtual void ProcessRenderRequests<RequestData>(ScriptableRenderContext context, Camera camera, RequestData renderRequest);
// Entry point method that defines custom rendering for this RenderPipeline.
protected abstract void Render(ScriptableRenderContext context, Camera[] cameras);
protected virtual void Render(ScriptableRenderContext context, List<Camera> cameras);
protected internal virtual bool IsRenderRequestSupported<RequestData>(Camera camera, RequestData data);
...//其他
}
3 渲染流程
URP Universal Renderer中每帧的渲染流程如图所示,更为详细的流程见下文中的伪代码。不同版本间可能存在稍许差异,此伪代码为URP14.04版本。此伪代码是源代码的简略版,实际的过程远远比此复杂。
BeginFrameRendering #每帧绘制前的命令,通过事件订阅
GraphicsSetting.xxx = xx #这里的xxx是GraphicsSetting内的属性,如lightsUseLinearIntensity,useScriptableRenderPipelineBatching等
SetupPerFrameShaderConstants #shader中的全局变量设置
setup parameter #图形参数设置,每帧中所需要的shader常量:环境光,纹理等
sort Cameras #按深度排序
for loop camera: render camera #遍历相机(遍历前会进行排序),对每个相机执行绘制逻辑。
BeginCameraRendering #相机绘制前的命令,通过事件订阅
update volume framework #初始化附加属性前,需要更新volumeframework
RenderSingleCameraInternal #封装一个相机所有渲染流程的函数
camera.gameObject.TryGetComponent(out additionalCameraData) #获取additionalCameraData
InitializeCameraData #生成并初始化CameraData
InitializeStackedCameraData #函数
renderer = additionalCameraData?.scriptableRenderer
InitializeAdditionalCameraData #初始化CameraData, 每个相机不同的参数
CameraData.Renderer = asset.ScriptableRenderer//ScriptableRenderer对于urp是UniversalRenderer
//UniversalRenderer在别的章节细说,这里需要知道renderer中包含了许多内置的Pass,如天空盒的绘制,不透,半透图元的绘制
//等
RenderSingleCamera: #一个封装的函数名
TryGetCullingParameters #初始化剔除参数
renderer.OnPreCullRenderPasses:
for loop rendererFeature : rendererFeature list #遍历
rendererFeatures.OnCameraPreCull #抽象函数,具体实现交给ScriptableRendererFeature的子类
Renderer.setupCullingParameters #使用cameradata进一步配置剔除参数
Cull #利用剔除参数获得剔除结果CullingResults类
Init Rendering Data #通过pipelineAsset,cameraData和cullResults,commandbuffer等构造renderingData,还包含光源,阴影,后处理等
Renderer.AddRenderPasses: #构造render pass list到renderer中
for loop rendererFeature : rendererFeature list #遍历
rendererFeatures.AddRenderingPasses #抽象函数,具体实现交给ScriptableRendererFeature的子类
Renderer.Setup: #配置待渲染的render pass
if(isOffscreenDepthTexture)
Renderer.SetupRenderPasses:
for loop rendererFeature : rendererFeature list #遍历
rendererFeature.SetupRenderPasses #虚函数,具体实现交给ScriptableRendererFeature的子类。
EnqueuePass(m_RenderOpaqueForwardPass)
EnqueuePass(m_RenderOpaqueForwardPass)
//添加阴影计算Pass
if (mainLightShadows)
EnqueuePass(m_MainLightShadowCasterPass);
if (additionalLightShadows)
EnqueuePass(m_AdditionalLightsShadowCasterPass);
if(this.renderingModeActual == RenderingMode.Deferred)//如果是延迟渲染
EnqueueDeferred:
EnqueuePass(m_GBufferPass);
EnqueuePass(m_DeferredPass);;
EnqueuePass(m_RenderOpaqueForwardOnlyPass);
else
EnqueuePass(renderOpaqueForwardPass);
if(DwawSkybox)
EnqueuePass(m_DrawSkyboxPass);
if(depthTextureNeedToCopy)
EnqueuePass(m_CopyDepthPass);
if(copyColorPass)
EnqueuePass(m_CopyColorPass);
if(needTransparencyPass)
EnqueuePass(m_RenderTransparentForwardPass);
Renderer.Execute: #按照renderer pass执行绘制命令
Renderer.SetupRenderPasses:
for loop rendererFeature : rendererFeature list #遍历
rendererFeature.SetupRenderPasses #虚函数(空实现),具体实现交给ScriptableRendererFeature的子类。
Renderer.InternalStartRendering:
for loop rendererPass : active renderPass list #遍历当前活跃pass的队列
rendererPass.OnCameraSetup #每个renderer pass的OnCameraSetup 逻辑
SortStable #依据evt值,将renderpasses排序
for loop renderPass : active renderPass list
renderPass.configure #每个renderer pass的Configure逻辑
Renderer.Execute Block #依据event,执行每个阶段的绘制逻辑
Renderer. InternalFinishRendering:
for loop rendererPass : active renderPass list #遍历
rendererPass.FrameCleanup(12.1.1中标记过时,后者取代)/OnCameraCleanup #每个renderer pass的FrameCleanup逻辑
if last Camera #在使用camera stack下起效
m_ActiveRenderPassQueue.OnFinishCameraStackRendering
m_ActiveRenderPassQueue.clear #清空当前活跃pass的队列
context.submit #执行
ScriptableRenderer.current = null
EndCameraRendering #相机绘制结束后的命令,通过事件订阅
EndFrameRendering #每帧绘制结束的命令,通过事件订阅
3.1 通过rendererPass和rendererFeature扩展URP的渲染功能
URP中渲染功能被放置在了rendererPass当中。UniversalRenderer成员变量中包含许多内置的rendererPass,这些内置pass在URP管线创建时被自动创建。同时这些pass将在伪代码中Renderer.Setup过程中,根据当前的配置和场景设置有选择地、自动地被添加到rendererPass队列中。内置的pass的源代码所在程序集如下图所示。此外,用户可以编写自己的rendererPass和rendererFeature实现定制化的显示需求。
3.1.1 rendererFeature
自定义的rendererFeature需要继承自scriptableRendererFeature,用于管理rendererPass的类,负责创建、配置、添加和销毁rendererPass。scriptableRendererFeature中规定的重要的接口如下所示。
函数名 | 解释 |
---|---|
Create | 创建、初始化此类中将使用的资源 |
Dispose | renderer在调用其Dispose的时候将遍历每个rendererFeature,调用其将Dispose清除资源。这里将调用虚函数的Dispose(bool)。开始和结束时调用,中间渲染过程不调用。 |
Dispose(bool) | 清除在RendererFeature中申请的资源 |
OnCameraPreCull | CUll前需要完成的操作在此函数中进行。如收集camera在自定义rendererPass中需要渲染的物体 |
AddRenderPasses | 根据当前的场景,指定pass的插入 |
SetupRenderPasses | 配置pass,这里主要是指定不同属性(颜色,深度等)的渲染目标(RTHandle),以及需要使用的buffer等 |
SetActive | 设置rendererFeature的激活,未激活的feature,不会参与渲染 |
namespace UnityEngine.Rendering.Universal
{
[ExcludeFromPreset]
public abstract class ScriptableRendererFeature : ScriptableObject, IDisposable
{
protected ScriptableRendererFeature();
public bool isActive { get; }
public abstract void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData);
public abstract void Create();
public void Dispose();
public virtual void OnCameraPreCull(ScriptableRenderer renderer, in CameraData cameraData);
public void SetActive(bool active);
public virtual void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData);
protected virtual void Dispose(bool disposing);
}
}
在UniversalRendererData资源文件的GUI界面中点击Add Renderer Feature,选择自定义的rendererFeature,即可将其添加到管线资源中。
3.1.2 rendererPass
所有自定义的rendererPass必须继承自ScriptableRenderPass,ScriptableRenderPass的接口和调用顺序如下所示。注意Configure和OnCameraSetup的区别,伪代码中可见OnCameraSetup执行在Configure前。
函数名 | 解释 |
---|---|
Configure | 在执行前配置rendererPass,如果需要配置渲染目标及其清除状态,以及创建临时渲染目标纹理,请重写此方法,否则将渲染到相机的渲染目标 |
ConfigureClear | 用于在Configure内调用,用于清除状态 |
ConfigureTarget | 用于在Configure内调用,用于配置渲染目标 |
Execute | 执行具体的渲染逻辑 |
OnCameraSetup | 在渲染一个相机前调用,用于临时渲染目标纹理的创建、指定渲染目标,状态清除等。如果没有指定渲染目标,则默认渲染到当前激活相机的渲染目标上。 |
OnCameraCleanup | 用于清除在renderpass中创建的资源,在这个相机完成渲染后 |
FrameCleanup | 清除在renderpass中创建的资源,在rendererpass完成后就调用。12.1.1中标记过时,使用OnCameraCleanup替代。对于URP管线实际就是每个相机完成渲染后调用 |
OnFinishCameraStackRendering | 当camera stack完全选然后再,清除资源 |
namespace UnityEngine.Rendering.Universal
{
public abstract class ScriptableRenderPass
{
public static RTHandle k_CameraTarget;
public ScriptableRenderPass();
public ClearFlag clearFlag { get; }
public ScriptableRenderPassInput input { get; }
public RenderBufferStoreAction depthStoreAction { get; }
public RenderBufferStoreAction[] colorStoreActions { get; }
public RTHandle depthAttachmentHandle { get; }
public RTHandle colorAttachmentHandle { get; }
public RTHandle[] colorAttachmentHandles { get; }
[Obsolete("Use depthAttachmentHandle")]
public RenderTargetIdentifier depthAttachment { get; }
[Obsolete("Use colorAttachmentHandle")]
public RenderTargetIdentifier colorAttachment { get; }
[Obsolete("Use colorAttachmentHandles")]
public RenderTargetIdentifier[] colorAttachments { get; }
public RenderPassEvent renderPassEvent { get; set; }
public Color clearColor { get; }
protected internal ProfilingSampler profilingSampler { get; set; }
public void Blit(CommandBuffer cmd, ref RenderingData data, Material material, int passIndex = 0);
[Obsolete("Use RTHandles for source and destination")]
public void Blit(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, Material material = null, int passIndex = 0);
public void Blit(CommandBuffer cmd, RTHandle source, RTHandle destination, Material material = null, int passIndex = 0);
public void Blit(CommandBuffer cmd, ref RenderingData data, RTHandle source, Material material, int passIndex = 0);
public virtual void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor);
public void ConfigureClear(ClearFlag clearFlag, Color clearColor);
public void ConfigureColorStoreAction(RenderBufferStoreAction storeAction, uint attachmentIndex = 0);
public void ConfigureColorStoreActions(RenderBufferStoreAction[] storeActions);
public void ConfigureDepthStoreAction(RenderBufferStoreAction storeAction);
public void ConfigureInput(ScriptableRenderPassInput passInput);
[Obsolete("Use RTHandles for colorAttachments and depthAttachment")]
public void ConfigureTarget(RenderTargetIdentifier[] colorAttachments, RenderTargetIdentifier depthAttachment);
public void ConfigureTarget(RTHandle[] colorAttachments, RTHandle depthAttachment);
[Obsolete("Use RTHandle for colorAttachment")]
public void ConfigureTarget(RenderTargetIdentifier colorAttachment);
public void ConfigureTarget(RTHandle[] colorAttachments);
public void ConfigureTarget(RTHandle colorAttachment, RTHandle depthAttachment);
public void ConfigureTarget(RTHandle colorAttachment);
[Obsolete("Use RTHandles for colorAttachments")]
public void ConfigureTarget(RenderTargetIdentifier[] colorAttachments);
[Obsolete("Use RTHandles for colorAttachment and depthAttachment")]
public void ConfigureTarget(RenderTargetIdentifier colorAttachment, RenderTargetIdentifier depthAttachment);
public DrawingSettings CreateDrawingSettings(List<ShaderTagId> shaderTagIdList, ref RenderingData renderingData, SortingCriteria sortingCriteria);
public DrawingSettings CreateDrawingSettings(ShaderTagId shaderTagId, ref RenderingData renderingData, SortingCriteria sortingCriteria);
public abstract void Execute(ScriptableRenderContext context, ref RenderingData renderingData);
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual void FrameCleanup(CommandBuffer cmd);
public virtual void OnCameraCleanup(CommandBuffer cmd);
public virtual void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData);
public virtual void OnFinishCameraStackRendering(CommandBuffer cmd);
public void ResetTarget();
public static bool operator <(ScriptableRenderPass lhs, ScriptableRenderPass rhs);
public static bool operator >(ScriptableRenderPass lhs, ScriptableRenderPass rhs);
}
}
3.2 物体在相机中的可见性
内置管线中物体在相机中的可见性遵循以下的原则:
- 位于视锥体内的渲染对象才会进行渲染
- 相机的CullingMask属性包含的Layer的游戏对象才会被渲染。为了更灵活地管理游戏对象。Unity为每个游戏对象增加了一个Layer属性,同时为相机增加了一个CullingMask的属性。这样具有相同layer的游戏对象可以视为一组,通过切换相机的CullingMask实现一组物体的滤除。
URP中的可见性滤除操作位于伪代码中的Cull部分,这里调用了ScriptableRenderContext.Cull函数。ScriptableCullingParameters中的滤除参数从相机属性中传递而来,常见的参数如相机远近平面,CullingMask等。
public CullingResults Cull(ref ScriptableCullingParameters parameters);
URP管线在内置管线基础上增加了以下规则,这里的源码解释见3.3。其中UniversalRendererData资源文件的界面见2.1 GUI操作,Opaque Layer Mask和Transparent Layer Mask默认显示所有类。
- 不透物体是否被绘制取决于UniversalRendererData资源文件中的Opaque Layer Mask
- 半透物体是否被绘制取决于UniversalRendererData资源文件中的Transparent Layer Mask
3.3 渲染顺序
3.3.1 相机的渲染顺序
由上面伪代码可以看到,在进行单个相机的渲染前,将会根据相机深度对相机进行排序。排序后,深度值较低的相机先渲染。这里的相机深度指的是Camera.depth属性。在URP模式下,Unity的相机inspector界面中可通过priority设置。
3.3.2 物体的渲染顺序
Unity默认的渲染顺序如下,优先级从上向下递减:
- 不透物体(renderqueue<2500)先于半透物体(renderqueue>2500)的渲染。
- 不透物体遵循:(1)按照Sorting Layer,值越小越优先。若无此属性,等同于 Sorting Layer=default;(2)按照Order in Layer,值越小越优先。若无此属性Order in Layer=0 参与排序;(3)RenderQueue 越小越优先;(4)RenderQueue 相等,由近到远排序优先。
- 半透物体遵循规则和不透物体相同,除了(4) RenderQueue 相等,由远到近排序优先。
内置的DrawObjectsPass是URP管线完成各类游戏对象渲染的主要模块。DrawObjectsPass的构造函数接口如下所示,其构造参数中包含是否透明,渲染序列范围,LayerMask等设置。这些参数将在DrawObjectsPass运行ExecutePass函数时传递给渲染命令DrawRenderers。
public DrawObjectsPass(string profilerTag, ShaderTagId[] shaderTagIds, bool opaque, RenderPassEvent evt, RenderQueueRange renderQueueRange, LayerMask layerMask, StencilState stencilState, int stencilReference)
DrawRenderers的原型如下所示,其中CullingResults 是位于相机视锥体内的物体;drawingSettings是一些绘制的设置;filteringSettings则是一些过滤参数,内部利用这些参数和待渲染物体的材质属性对待渲染物体进行过滤,保证该指令只绘制某一类物体。
public void DrawRenderers(CullingResults cullingResults, ref DrawingSettings drawingSettings, ref FilteringSettings filteringSettings, ref RenderStateBlock stateBlock);
drawingSettings包含中包含渲染材质相关的设置和排序的设置,其属性中SortingSettings包含
一个SortingCriteria变量。这个变量指定了物体的渲染顺序的规则,如下表所示。
SortingCriteria | 解释 |
---|---|
None | 不对对象进行排序 |
SortingLayer | 按照渲染器排序图层排序 |
RenderQueue | 按材质渲染队列排序 |
BackToFront | 从后到前对对象进行排序。 |
QuantizedFrontToBack | 在大致从前到后的存储桶中对对象进行排序 |
OptimizeStateChanges | 对对象进行排序,以减少绘制状态更改的次数 |
CanvasOrder | 根据画布顺序对渲染器进行排序 |
RendererPriority | 按渲染器优先级对对象排序 |
CommonOpaque | 不透明对象的典型排序,即按照如下组合SortingLayer、RenderQueue、QuantizedFrontToBack、OptimizeStateChanges、CanvasOrder |
CommonTransparent | 透明对象的典型排序,即按照如下的组合SortingLayer、RenderQueue、BackToFront、OptimizeStateChanges |
FilteringSettings包含的属性如下所示,主要包含RenderQueueRange,LayerMask和renderingLayerMask等参数。Unity渲染对象存在与之对应的参数,这些参数可以通过脚本代码设置,也可以直接使用GUI界面设置,如下图cube对象的RenderQueue,LayerMask和Render Layer Mask。
namespace UnityEngine.Rendering
{
public struct FilteringSettings : IEquatable<FilteringSettings>
{
public FilteringSettings([DefaultValue("RenderQueueRange.all")] RenderQueueRange? renderQueueRange = null, int layerMask = -1, uint renderingLayerMask = uint.MaxValue, int excludeMotionVectorObjects = 0);
public static FilteringSettings defaultValue { get; }
public RenderQueueRange renderQueueRange { get; set; }
public int layerMask { get; set; }
public uint renderingLayerMask { get; set; }
public bool excludeMotionVectorObjects { get; set; }
public SortingLayerRange sortingLayerRange { get; set; }
}
}
UniversalRenderer存在多个DrawObjectsPass变量,正是过多个DrawObjectsPass变量的组合,Unity才形成了上述的渲染顺序。如m_RenderOpaqueForwardPass中渲染序列范围为opaque,因此该Pass只会渲染材质的RenderQueue属性位于此范围内的物体,同时默认情况下其渲染顺序为CommonOpaque。类似地,m_RenderTransparentForwardPass则只会渲染半透物体,默认渲染顺序则为CommonTransparent。
4 URP管线 扩展渲染方案示例
4.1 URP管线正确支持半透
众所周知,Unity这种基于物体距离排序进行绘制的方法是不能正确支持半透绘制的。
Unity中不透绘制先于半透绘制,且不透绘制时开启深度写入和深度比较,而半透绘制时开启深度比较,关闭深度比较。因此通过深度测试的半透片元一定位于半透片元前。可以将通过深度测试的所有半透网格的片元保存到指定缓存中,然后单独使用一个rendererPass来对这些片元进行排序和绘制,并与不透结果进行混合。
4.2 渲染ComputeBuffer类型的网格数据
Unity提供了直接渲染该类数据的命令CommandBuffer.DrawProceduralIndirect和Graphics.DrawProceduralIndirect。后者立即完成渲染,而前者将该指令增加到渲染命令池中,等待后续的执行命令。
CommandBuffer.DrawProceduralIndirect(后续简称DrawProceduralIndirect)的原型如下,其参数的函数见下表。下面使用一个存有网格的顶点和法向量CommandBuffer(后文称为GPUMesh)进行渲染。
public void DrawProceduralIndirect (Matrix4x4 matrix, Material material, int shaderPass, MeshTopology topology, ComputeBuffer bufferWithArgs, int argsOffset= 0, MaterialPropertyBlock properties= null);
public void DrawProceduralIndirect(Matrix4x4 matrix, Material material, int shaderPass, MeshTopology topology, GraphicsBuffer bufferWithArgs, int argsOffset);
参数 | 解释 |
---|---|
matrix | 从模型坐标系到世界坐标系的转换矩阵 |
material | 使用的材质 |
shaderPass | 使用shader中的哪一个Pass |
topology | 拓扑结构,指示如何组织顶点着色后输出的顶点 |
bufferWithArgs | 绘制数据的参数 |
argsOffset | Byte offset where in the buffer the draw arguments are |
properties | 其他希望使用的参数,比如真正的渲染数据 |
(1)着色器
由于GPUMesh的数据与传统的网格不同,因此使用的着色器对象中的顶点着色器与普通网格的顶点着色器的输入存在区别。可以考虑单独编写支持GPUMesh的着色器,也可以在普通网格的着色器对象中增加一个关键字,该关键字对应的分支用于处理GPUMesh。这样一个着色器对象可以支持多种数据输入了。关于GPUMesh的顶点着色器写法,Github上有示例项目,下面也给出了一个示例代码。
struct Varyings
{
float3 normalWS : TEXCOORD2;
float4 positionCS : SV_POSITION;
};
struct Triangle
{
float3 vertices[3];
float3 vFaceNormal;
};
StructuredBuffer<Triangle> _TriangleData;
Varyings LitPassVertex(uint vertexID : SV_VertexID)
{
uint pid = vertexID / 3;
uint vid = vertexID % 3;
Varyings output = (Varyings) 0;
output.positionCS = TransformObjectToHClip(_TriangleData[pid].vertices[vid]);
output.normalWS = TransformObjectToWorldNormal(_TriangleData[pid].vFaceNormal);
return output;
}
(2)ComputeBuffer的管理
参照Unity使用MeshFilter组件管理普通网格的模式,我们新建一个GPUMeshFilter脚本来管理GPUMesh。同时仍然使用Transform组件管理GPUMesh的位置
(3)RendererPass
增加一个自定义GPUMeshRendererPass,使用该Pass完成对绘制指令参数的最终收集和绘制命令的调用。同时参照普通的DrawObjectPass对渲染物体进行过滤的操作,判断每个带渲染物体的材质的renderqueue是否与当前RendererPass的RenderPassEvent相匹配。只有匹配的,才会进行渲染操作。
- RenderPassEvent为AfterRenderingOpaques时,绘制renderqueue<2500的不透物体;
- RenderPassEvent为BeforeRenderingTransparents时,绘制renderqueue>2500的半透物体;
(4)RendererFeature
增加一个GPUMeshRendererFeature,管理GPUMeshRendererPass的插入。在GPUMeshRendererFeature中建立两个GPUMeshRendererPass,且其RenderPassEvent属性分别设置为AfterRenderingOpaques和BeforeRenderingTransparents。
(5)收集、过滤ComputeBuffer的对象并在RendererPass中使用
RendererFeature和RendererPass中各个环节均获取到当前的相机。因此可以考虑增加一个相机的脚本用于收集、存放需要渲染的游戏对象。在GPUMeshRendererFeature的OnCameraPreCull中调用该脚本完成收集后,在GPUMeshRendererPass的Execute内获取到待渲染的游戏对象。
4.3 渲染Texture3D形式的距离场数据
距离场是表征特定形状的数据,其渲染本质是绘制形状的表面。因此可以理解成是另一种网格,该类数据允许支持半透和不透绘制,其渲染思路和上述的操作基本一致。(1)编写着色器。绘制一个与距离场真实大小一致的长方体网格,在材质的片元着色器中使用Raycast算法渲染距离场。在Raycast算法步进时,距离场数据作为三维纹理进行采样,作为前进和停止的依据;(2)增加一个TextureFilter管理Texture3D形式的数据,及所有相关联的渲染数据、参数。同时仍然使用Transform组件管理数据的位置。(3)增加一个CameraTextureFilter收集和存放待渲染的游戏对象。(4)rendererPass收集所有渲染信息,调用渲染命令进行绘制,同时过滤不透和半透;(5)rendererFeature管理rendererPass的插入,并调用CameraTextureFilter收集游戏对象。
4.4 渲染Texture3D形式的医学数据
距离场渲染片元着色时,每个片元只会找到一个对应的形状表面点。然后对该点进行着色作为片元输出和,因此每个片元只对应一个颜色和深度。而如果是医学的体数据,每个纹素值代表了一定的医学意义(如CT值,PET值)。在Raycast步进时,往往需要叠加一系列采样点的颜色,这种情况下每个片元对应多个颜色和深度。因此这一类数据的渲染,一定放在最后的阶段,与此前的片元结果进行混合。之后会新开一篇博客详细介绍医学体数据的渲染。
上边我们展示了重写rendererPass和rendererFeature扩展URP的例子,实际上开发者也可以重写渲染器本身(本小白做不到)。
该文档很粗糙,后续再优化吧。