Unity正式加入了Universal RP(通用渲染管线),这里会记录一些官方文档,并分析管线的代码,文中使用Unity2019.3.0b1,Universal RP 7.0.1。
为了解决仅有一个默认渲染管线,造成的可配置型、可发现性、灵活性等问题。Unity在管线设计的概念上做了转移,决定在C++端保留一个非常小的渲染内核,让C#端可以通过API暴露出更多的选择性,也就是说,Unity会提供一系列的C# API以及内置渲染管线的C#实现;这样一来,一方面可以保证C++端的代码都能严格通过各种白盒测试,另一方面C#端代码就可以在实际项目中调整,有任何问题也可以方便地进行调试。
新的管线对用户而言主要是C# 端的API以及由这些API编写的一系列定制化的内置渲染管线。而在内部实现上,引擎C++端会负责多线程实现性能关键的部分,如上图所示,而C#端负责更高层的渲染指令调度。
可编程渲染管线的使用层设计用户可以直接使用开源的内置管线,或者在内置管线的基础上进行修改,甚至直接编写定制化的管线。具体使用上渲染管线在工程中会生成特定的Asset,如下图所示,这个Asset序列化了这条管线的一些公共设置变量,并负责在运行时创建实际的渲染上下文;当这个Asset的设置变量在运行时发生变化,引擎会销毁当前上下文然后重新创建管线。
可编程渲染管线是URP的基础,通过它我们可以知道unity中怎么实现最基本的渲染,unity对渲染管线API封装程度。
详解可编程脚本渲染管线SRP - Unity Connectconnect.unity.com Scriptable Render Pipeline Overview – Unity Blogblogs.unity3d.comLWRP是URP之前的名称,两者基本没有区别,URP相对LWRP的变化主要是把PostProcessing集成到内部了。
Unity轻量级渲染管线LWRP源码及案例解析(上) - Unity Connectconnect.unity.com Unity轻量级渲染管线LWRP源码及案例解析(下)connect.unity.comUnity轻量级渲染管线LWRP源码及案例解析,讲解了URP的使用方法和拓展方法,里面有对SRP、URP的一些说明,URP与内置管线的对比,URP的源码结构。
在继续看源码细节之前,先看看URP的主要提升(相对于内置管线),和不完善的地方。
提升:
1 开源
可编程渲染管线最大的好处就是开源,对于有能力的团队,可以选择在SRP的基础上写自己的管线。而使用Unity提供的管线模板URP或HDRP,也可以把他们从Package包中提出到Asset中做更改,上面的文章有提到修改的注意事项(其实就是记着把Shader中的include路径,和代码中的Shader.Find之类的路径改一下),但是并不推荐这么做,因为版本更新的维护是噩梦。开源意味着容易定位到问题,对调试非常友好。
2 渲染路径改为单Pass Forward Rendering
内置管线的多Pass Forward Rendering,会在多光源时对额外的光源使用新的FowardAdd Pass计算,Pass数量是影响物体的光源数量,最大值为8。
URP的单Pass Forward Renderering,会将光源一次性传入Forward Pass,但由于单Pass能够传的数据有限,现在最多支持一个直线光外加4个其他光源。
这样做虽然并不完美,但多光源场景中DrawCall数量会大量下降。
3 拓展性(非代码修改)
URP在渲染队列中嵌入了拓展入口,相当于之前的CommandBuffer的可视化操作。上面的文章中有详细的使用方法。
用新的设计取代了GrabPass的结构,在Opaque渲染之后可以截出一张RenderTexture,提供给之后使用。
4 SRP Batcher
提供了一种新的批处理方式,基于Shader的批处理。不过这个技术还不是正式功能,有些局限,不支持Skinned Meshes、Material Property Blocks。
缺点:
1 不支持多相机叠加
这是很重要的功能,如果真用到了,就去改源码吧,要不等之后更新。
2 Defferred Renderring
现在URP里面没有延迟渲染,也没有时域抗锯齿TAA。
我们先看 详解可编程脚本渲染管线SRP - Unity Connect 中的最后一个例子,半透明渲染。
using
渲染入口点为RenderPipeline类中的Render函数,参数为相机列表和渲染上下文,在这个函数的最后提交渲染上下文,完成渲染。填充渲染上下文的过程中,Unity封装好了一些方法,包括剔除操作、CommandBuffer的一系列指令、绘制渲染器等。
推荐先熟悉Universal RP的使用,再去看代码的实现,这样效率会更高。
Universal RP 7.0的文档docs.unity3d.comUniversal RP的依赖Package有两个,CoreRPLibrary是URP和HDRP都用到的一些工具,ShaderGraph是shader的可视化节点编辑器,之前的PostProcessing已经集成到内部了。
Universal RP的主要实现就在Universal RP的Runtime文件夹中,我们从渲染入口点开始看实现细节。
UniversalRenderPipeline类中的Render方法 一个Rendering Loop过程整体结构非常简单,设置GraphicSetting参数,设置每帧Shader中的Global 变量,相机排序,相机遍历,每相机渲染。
首先看一下四个方法BeginCameraRendering、BeginFrameRendering、EndCameraRendering、EndFrameRendering,它们是基类RenderPipeline中的渲染管线回调。
RenderPipelineGraphicsSettings.lightsUseLinearIntensity = (QualitySettings.activeColorSpace == ColorSpace.Linear);
更改颜色空间设置,这个设置位置在Player的OtherSetting中
GraphicsSettings.useScriptableRenderPipelineBatching = asset.useSRPBatcher;
是否开启SRPBatcher(根据shader合并材质),这个功能使用还需要在Shader中将需要用CBuffer保存的变量用一段代码包含。并且有一定的限制。
按材质合并与按Shader合并 SRPBatcher使用注意事项asset是指UniversalRenderPipelineAsset生成的管线资源,上面包含许多可调节设置,如SRPBatcher在倒数第四行,其他设置会在用到时说明。
UniversalRenderPipelineAsset中的设置SetupPerFrameShaderConstants函数
RenderSettings.ambientProbe是环境光的2阶球谐函数表达,Unity也使用SphericalHarmonicsL2存储LightProbe数据。
RenderSettings.ambientProbe在这里设置将系数转到相应颜色空间,把参数-环境光颜色、Subtractive模式下的阴影颜色传递到Shader,这就是每帧的shader常量。
PerFrameBufferPerFrameBuffer类中的参数是每帧需要的数据缓存,这些静态数据在创建管线时(或修改管线参数时)被赋值。
UniversalRenderPipeline的构造函数继续看Render函数中的SortCameras(cameras)
按深度排序接下来是遍历相机,我们先不关心VFX.VFXManager.ProcessCamera(camera)函数,它是为了在管线中集成Visual Effect Graph功能使用的,这里只有一个需要关心的方法RenderSingleCamera(renderContext, camera)。
public
这个方法的过程如下:
1 初始化剔除参数
camera.TryGetCullingParameters(IsStereoEnabled(camera),outvar cullingParameters)用于获取剔除结果,IsStereoEnabled是判断是否是立体相机(VR/AR)。
2 获取UniversalAdditionalCameraData
获取摄像机的额外数据UniversalAdditionalCameraData,我还没搞懂为什么面板是空的,里面的数据能序列化,甚至还有Tooltip,估计是没开发完。
挂着Camera所在GameObject上,面板默认不暴露3 初始化CameraData,InitializeCameraData方法
static
详细的设置大家阅读代码就可以得知。关于if(additionalCameraData !=null)那段设置,我没理解,现在的状态就是不添加UniversalAdditionalCameraData就会不支持后处理等效果,这里猜测这个设计的目的是为了区分两种相机模式,还希望大家能告诉我为什么。defaultOpaqueSortFlags的两种情况只差了一个是否按深度排序,其中camera.opaqueSortMode并没有被设置,在URP中始终未OpaqueSortMode.Default。captureActions只在Editor模式下生效,是录屏用的。cameraTargetDescriptor是RenderTextureDescriptor,在之后的Blit等操作里会用到,MSAA值也保存在这里。
4 设置PerCameraBuffer
SetupPerCameraShaderConstants方法 PerCameraBuffer类这里设置的几个摄像机参数,_ScreenParams的4个向量格式在HLSL或者GLSL都会用类似的方法传递。
读到这里,我对乘法顺序有疑问,于是去找shader中的乘法使用,以对比C#中的参数。
首先注意shader中通常使用矩阵在左向量在右的常规乘法。
com.unity.render-pipelines.universalShaderLibraryInput.hlsl注意红框中,UNITY_MATRIX_MV实际是UNITY_MATRIX_V × UNITY_MATRIX_M。矩阵乘法满足分配率,MVP矩阵乘向量p,与P(V(Mp))是相等的。这也是为什么C#端VP = P×V,只是命名习惯的问题。
5 获取ScriptableRenderer
ScriptableRenderer renderer = (additionalCameraData != null) ? additionalCameraData.scriptableRenderer : settings.scriptableRenderer;
ScriptableRenderer是抽象出的渲染功能实体,是UniversalRP可拓展的最外层,里面还有ScriptableRenderPass可以拓展。目前Unity提供了2个实现ForwardRenderer和Renderer2D,管线默认使用ForwardRenderer,默认资源是UniversalRP/Runtime/Data/ForwardRendererData.asset。可以每相机使用不同的ScriptableRenderer,这个设置方法和版本有关,可以看这个版本使用的addtionalCameraData.scriptableRenderer,有的版本使用camera.scriptableRenderer。
使用Custom管线6 使用ScriptableRenderer继续填充剔除参数和CameraData
ScriptableRenderer.Clear ScriptableRenderer.SetupCullingParameters ForwardRenderer.SetupCullingParameters Renderer2D.SetupCullingParametersClear方法相当于ScriptableRenderer的每相机初始化,SetupCullingParameters是虚方法,在ForwardRenderer和Renderer2D中有不同实现,后面以ForwardRenderer为主进行说明,ForwardRenderer中SetupCullingParameters进一步确定了阴影距离。
7 开始性能采样(Profiler面板)
CommandBufferPool是CommandBuffer对象池,ProfilingSample是封装的性能采样器,这两个类都在Core RP的Package中。
8 编辑器模式下Scene相机额外显示UI
9 剔除
var cullResults = context.Cull(ref cullingParameters),剔除是Unity封装好的一个方法,我们只能通过参数设置进行控制。
剔除参数 剔除结果的内容10 根据管线设置、CameraData、剔除结果,初始化渲染数据RenderingData
static
RenderingData是用于存储每相机渲染所需的所有数据的结构体,这个方法的目的就是完全填充渲染数据,最后交给ScriptableRenderer使用。
RenderingData结构体var visibleLights = cullResults.visibleLights,从剔除结果中获得所有可见光GetMainLightIndex代码如下,可以从代码中看出MainLight的定义。
GetMainLightIndex方法下面一段是MainLight和AdditionalLights是否存在阴影的判断,可以看出URP是不支持AdditionalLights的点光源和直线光源阴影的。CullResults和CameraData直接赋值给RenderingData。LightData、ShadowData和PostProcessingData方法由独立的方法赋值。
InitializeLightData方法 最大AdditionalLight数量是4,每相机最大可见光数量是16 LightData相关设置 InitializeShadowData方法前半段InitializeShadowData方法前半段,设置每光线的Bias值,这里UniversalAdditionalLightData和相机上挂的额外数据类似,不过这个脚本目前只有usePipelineSettings这一个字段,还是默认的true,面板同样是空的。
InitializeShadowData方法后半段InitializeShadowData方法后半段,设置是否支持MainLight和AdditionalLights的阴影,用到了“MainLight和AdditionalLights是否存在阴影”数据,分别设置ShadowMap的尺寸,MainLight的阴影额外设置级联数据。设置是否支持软阴影、设置ShadowMap的DepthBuffer使用16bits数据。
ShadowData相关设置 InitializePostProcessingData方法 PostProcessingData设置supportsDynamicBatching:是否支持动态批处理。
PerObjectData枚举PerObjectData是每Object需要的数据标记,通过下图方法获取。
killAlphaInFinalBlit会在最后的Blit操作前开启ShaderKeyword:_KILL_ALPHA,把前面的数据完全覆盖。
com.unity.render-pipelines.universalShadersUtilsBlit.shader的片元着色器11 使用ScriptableRenderer根据RenderingData,Setup并Excute渲染上下文
在看具体实现之前,先看看ScriptableRender的设计。
ScriptableRender是由一个实体生成的——ScriptableRendererData。
ScriptableRendererData类ScriptableRendererData中需要注意Create方法,它会在新建渲染管线时与管线一同创建,设置变化时重新创建ScriptableRender实例,还可以根据每相机设置来创建。ScriptableRendererData中的ScriptableRendererFeature集合,是URP提供给用户不用改代码就能增加Feature的功能,类似的,ScriptableRendererFeature继承自ScriptableObject,它可以创建出ScriptableRenderPass实例。
ScriptableRender类中定义了ScriptableRenderPass集合List
ScriptableRenderer.Setup是虚方法。
ForwardRender类ForwardRender类中定义了许多Pass结尾的变量,它们就是ScriptableRenderPass的子类。这里还有光照信息,光照在初始化和SetupLights方法中被赋值使用。
ForwardRender.Setup方法使用RenderData数据,来设置好渲染所需要的内容:管理并提交ScriptableRenderPass到m_ActiveRenderPassQueue;提前定义好这个相机要使用的颜色和深度目标,数据格式是RenderTargetIdentifier,是CommandBuffer中RenderTexture的标识;将RendererFeatures生成的ScriptableRenderPass添加到m_ActiveRenderPassQueue中;创建相机渲染目标的临时RenderTexture;设置Backbuffer的格式。
ForwardRender中初始化ScriptableRenderPass的过程可以把渲染理解为绘制过程,这些ScriptableRenderPass子类就代表不同的过程,可以在上图中看到这些过程所处的阶段,即不同过程的绘制顺序。
因为ScriptableRenderPass的子类内容比较多,这里不展开分析Setup方法了,先去看看ScriptableRender.Execute的功能。
ScriptableRender.ExecuteScriptableRenderer.Execute是ScriptableRenderer层的绘制执行方法,过程如下:
时间的设置,官方还会改动,代码中有注释说明。有个block的概念,m_ActiveRenderPassQueue被分为3个block,按照下图节点划分,FillBlockRanges为划分方法,ExecuteBlock为按Block执行。
Block划分依据ExecuteBlock方法中进一步调用了指定Block中的ExecuteRenderPass方法,ExecuteRenderPass方法在确定ScriptableRenderPass对应的渲染目标后,调用ScriptableRenderPass.Execute设置每个渲染模块的上下文。
ScriptableRenderer.ExecuteRenderPass下面通过Unity中的FrameDebug看看渲染目标设置的过程,Game视窗当前分辨率为1600*900。
ExecuteRenderPass方法中有一行ClearFlag clearFlag = GetCameraClearFlag(camera.clearFlags),这个方法获取第一个Clear的ClearFlag,在不同平台上结果有差异,大家一定要注意。
12 结束性能分析
在ProfilingSample执行Dispose时cmd设置为结束性能分析,并立刻被提交到context中。
13 提交渲染上下文
context.Submit();
由于ScriptableRenderPass内容比较多,就不详细列出RenderData设置的参数是怎么作用于不同的ScriptableRenderPass了,可以在使用相应模块时再去了解。
之后会更新Universal RP中的Lit.shader的内容,这个Shader相当于默认渲染管线中的Standard.shader。