该篇是对Catlike Coding这篇文章的概要总结,本人能力有限,如果有不正确的地方欢迎指正 https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/custom-pipeline/
通过这篇文章,你将学习到
This tutorial is made with Unity 2018.3.0f2. 这篇文章的实现基于Unity 2018.3.0f2版本,是在Unity中实现一个自定义渲染管线教程系列的第一篇。
在Unity2018之前,Unity中有Foward 和 Deferred 两种渲染管线,渲染管线中你可以操控的部分和内容非常少。在Unity2018中,提供了可编程渲染管线(SRP),有了它,我们可以自定义管线中的很多内容,虽然大多数步骤是Unity中已经封装好的功能(比如Culling裁剪)。在2018中,SRP还处于preview阶段,但是目前的版本提供的功能和稳定性足以让我们实现自定义渲染管线。
创建一个标准3D项目,移除掉PackageManager中除Package Manager UI外的所有其他内容。
将Unity2018默认的Gamma空间改成线性空间(Edit / Project Settings / Player -- Color Space in the Other Settingssection to Linear)
创建一些测试用的简单材质资源:
Unity默认使用的是传统的前向渲染管线,如果要使用我们自定义的,需要在Edit / Project Settings / Graphics中设置
想要设置我们自定义的RenderPipelineAsset,我们需要先为我们的自定义渲染管线创建一个RenderPipelineAsset。它继承于RenderPipelineAsset,我们把自定义的渲染管线命名为MyPipline,所以对应的管线资源文件就命名为MyPipelineAsset。我们需要使用UnityEngine.Experimental.Rendering命名空间,因为该功能还在preview阶段,以后如果正式版本发布的话可能需要更改成对应的命名空间
RenderPipelineAsset的主要目的是帮助Unity创建一个渲染管线实例,RenderPipelineAsset自身其实就是存储各渲染管线的配置。我们可以通过overriding the InternalCreatePipeline
方法来创建一个渲染管线实例,因为我们现在还没定义自己的渲染管线,所以现在先返回个Null。
用CreateAssetMenu函数来使得在编辑器中可以创建我们的管线
创建出我们自己的管线资源:
将创建好的管线Asset赋值到SRP Setting
创建一个实现了IRenderPipeline
接口的类,命名为MyPipeline,它将是用于渲染过程的实例
为了方便,我们直接继承RenderPipeline类,它是Unity总已经实现IRenderPipeline
接口基本功能的类
现在,我们将之前InternalCreatePipeline函数中返回Null部分的代码替换成创建我们自定义好的MyPipeline类
pipeline每帧渲染,Unity做的事就是使用context和active状态的camera作为参数,调用pipeline中的render函数,进行渲染。这个过程对game 窗口,scene窗口和材质预览窗口都是一样的。我们现在要做的就是正确的设置好各参数,用正确的顺序绘制出应该被渲染的东西。
RenderPipeline需要实现Render这个方法,它的第一个参数是Context,一个Context是一个ScriptableRenderContext
结构体,作用是对Native code的桥接。该函数的第二个参数是一个camera数组。
基类RenderPipeline的RenderPipeline.Render函数并没有实际绘制任何东西,只是检查了管线中的物体是否合法用于渲染,如果不是的话会抛出异常。我们调用积累中的这个函数来保持这个检查功能。
我们可以调用command是去控制渲染状态和绘制各种东西,其中最简单的一个例子就是绘制天空盒,我们调用DrawSkyBox函数完成这个功能。该函数需要传入一个摄像机作为参数,为求简便我们使用Camera数组中的第一个。
调用了这个函数我们还是看不到任何东西,这是因为我们这是把command传入了buffer,实际起效需要我们通过submit函数submit 这些commands。
这回我们能在game视图中看到skybox了,我们也能在frame debugger中看到。
场景中可能存在多个摄像机需要挨个渲染,我们可以为单个摄像机写一个render函数,在我们目前要实现的渲染管线中,主要关注这个函数就可以了。
想要正确的渲染Skybox和整个场景,我们需要通过Camera的位置和视角设置MVP矩阵,Unity Shader中unity_MatrixVP 就是这个矩阵。我们需要将Camera中的配置信息通过SetupCameraProperties函数写入Context中。
Context 直到我们Submit才会进行实际的渲染,在这之前,我们需要配置它并且将comands加入其中等之后执行。一些任务比如绘制Skybox有专门的函数实现,但是很多commands都是用command buffer来完成。
command buffer在UnityEngine.Rendering命名空间中,它是在srp功能之前就有的功能,我们在绘制Skybox之前创建一个command buffer。
ExecuteCommandBuffer函数将command传入context的内部,等待submit后执行
command buffer会消耗unity native层的内存资源,当我们不再需要它的时候要立刻释放掉。Release函数可以完成这个功能。
执行一个空的command buffer没有意义,我们加入command buffer是为了清除render target去保证之前渲染的东西不会影响到目前。我们向buffer中加入一个clear command通过调用ClearRenderTarget函数,它包括三个参数,第一个参数是否清除depth ,第二个参数是否清除color,第三个参数是将缓冲区清除成什么颜色。
在frame debugger中我肯能够看到command buffer执行的结果,清除了Z缓冲和stencil缓冲。
通过Camera中的clear flags和background color参数,我们可以清除掉这个函数的硬编码,直接使用配置好的信息
给Command Buffer设置一个名字,在这里我们将其设置成camera的name,在frame debugger中就可以看到这个相应信息。
我们目前只渲染了Skybox还没有渲染场景中的物体,在渲染物体之前,我们需要先通过摄像机的视椎体对物体进行裁剪剔除,只渲染那些被我们看见,应该被渲染的物体。
通过ScriptableCullingParameters结构体和 CullResults.GetCullingParameters
函数,我们可以针对某个Camera得到裁剪的配置信息。
优化一下,可以判断一下Camera 设置是否valid,如果不是的话,该函数会返回false,直接不渲染东西return掉。
得到裁剪的配置信息后,通弄过CullResults.Cull 函数可以得到最终的裁剪结果
有了场景中的可视信息后,我们可以通过DrawRenderers函数进行下一步的渲染。该函数用剪裁后的cull.visibleRenderers和
DrawRendererSettings
and FilterRenderersSettings
配置信息进行渲染。
到现在我们还是看不到任何物体,这是因为我们需要设置 FilterRenderersSettings信息,通过置为true,可以渲染everything。·
通过设置DrawRendererSettings信息,我们设置用于渲染的shader pass,在这里我们使用SRPDefaultUnlit Shader Pass。
到这步在场景中我们就可以看到不透明的物体了
在frame debugger中可以看到透明物体也被渲染了,但是在game视图中没看到,这是因为渲染顺序的问题,透明物体渲染时不会写入深度缓冲,应该在最后再渲染。当透明物体被渲染后,再渲染skybox就会导致透明物体渲染不正确。
要解决这个问题,我们需要调整渲染顺序,先只渲染不透明物体。
之后渲染skybox,最后再渲染透明物体
现在不透明物体,透明物体和skybox都可以正常渲染了。
我们先渲染不透明物体,后渲染skybox的原因是因为不透明物体一定在skybox之前,会挡住skybox,这样的话就可以减少over draw。同样的原理,也可以用于不透明物体之间的遮挡,我们可以先对不透明物体进行排序,明确前后关系后按顺序渲染,这样被挡住物体的部分就不会参与渲染,可以增加渲染效率。
通过设置SortFlags.CommonOpaque,可以从前向后的顺序渲染不透明物体
为了渲染透明物体,我们要将渲染顺序改回从后到前,通过设置SortFlags.CommonTransparent,可以实现该功能
现在,我们的自定义渲染管线可以正确的渲染不透明和透明物体了。
能够正确的渲染只是渲染管线一小部分功能,我们还要考虑很多其他东西,比如内存的分配,和Editor是否结合的更好等等问题。
不要每帧都创建CullResults结构体,因为它里面有3个List,都需要分配内存
去掉Camera.name的调用,每次调用这个,都会新生成一个string,消耗内存
缓存command Buffer,不要每帧都分配,使用clear进行重置
3.2 Frame Debugger Sampling
通过BeginSample 和 EndSample,我们可以控制frame debugger中显示的层级,便于调试
因为我们自定义的这个渲染管线只支持unlit shaders,当物体使用其他shader的时候就渲染不出来了。我们可以使用一个Unity的error shader,当物体不能正确渲染的时候可以显示其形状,通过使用DrawDefaultPipeline函数来实现这个功能。
我们使用Unity默认的ForwardBase pass,我们不用考虑剪裁排序这些,因为他们本来就不该被正确的渲染出来
因为我们的管线不支持ForwardBase,所以这些物体渲染的并不正确,我们用Unity中的ErrorShader生成一个material,用于渲染,渲染结果就是粉粉的我们熟悉的那种效果了。
目前是只有使用了ForwardPass的shader会得到这种效果,我们通过设置drawSettings,可以将Unity中其他shader pass加入其中。
3.4 Conditional Code Execution
通过Conditional功能,我们可以控制函数只在development版本和Editor中编译运行,在正式build版本中会被删掉不进行编译
我们不用做任何处理,就可以在game视图中正确显示UI,这些Unity已经为我们做好了。但是想在Scene视图中看到UI,我们需要调用ScriptableRenderContext.EmitWorldGeometryForSceneView函数。但是要注意的是调用这个函数会导致UI在game视图中再渲染一遍,所以我们要根据CameraType.SceneView区分是否是在scene视图中,并且用#if UNITY_EDITOR来保证只在Editor模式下编译这段代码。