SRP简单入门 agile Posted on Oct 2 2021 优秀博文 > 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [zhuanlan.zhihu.com](https://zhuanlan.zhihu.com/p/378828898) 概念 -- SRP 全称为:Scriptable Render Pipeline,也就是可编程的渲染管线,它属于一种轻量的 API,允许开发者使用 C# 脚本来设置渲染命令,Unity 将这些命令传递给它的底层图形架构(low-level graphics architecture),它会将这些指令会被发送到 graphics API 当中,最终由 GPU 进行处理。 SRP 的官方介绍如下: [https://docs.unity3d.com/Manual/ScriptableRenderPipeline.html](https://docs.unity3d.com/Manual/ScriptableRenderPipeline.html) 如今常见的 **URP** 以及 **HDRP** 都是在 SRP 的基础上拓展的,当然我们也可以用 SPR 实现自定义的渲染管线。想要实现自定义的渲染管线,必须要有 Render Pipeline Asset 和 Render Pipeline Instance 这两个东西。 Render Pipeline Instance ------------------------ 我们先来看看什么是 Render Pipeline Instance,它实际上就是一个继承了 [RenderPipeline](https://docs.unity3d.com/ScriptReference/Rendering.RenderPipeline.html) 的类,如下: ``` public class CustomRenderPipeline : RenderPipeline { protected override void Render(ScriptableRenderContext context, Camera[] cameras) { } } ``` 然后我们可以通过重写 Render 方法,在里面实现我们自定义的渲染效果。其中我们可以看见有一个 [ScriptableRenderContext](https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html) 对象,它充当着 C# 代码与 Unity 底层图形代码的接口。SRP 的渲染使用的是**延迟**执行,我们可以用 ScriptableRenderContext 构建一系列的渲染命令,然后告诉 Unity 去执行这些命令。 我们通过下面两种方法来设置渲染命令: * 将一系列的 CommandBuffer 传递给 ScriptableRenderContext,然后使用 ScriptableRenderContext.ExecuteCommandBuffer 方法执行这些命令。 * 直接调用 ScriptableRenderContext 的 API,例如 ScriptableRenderContext.Cull 和 ScriptableRenderContext.DrawRenderers。 然后我们可以通过调用 ScriptableRenderContext.Submit 方法来告诉 Unity 去执行我们设置好的命令。在调用 Submit 方法之前,Unity 不会去执行我们前面所设置的命令。 下面例子,我们使用红色来进行清屏: ``` protected override void Render(ScriptableRenderContext context, Camera[] cameras) { var cmd = new CommandBuffer(); cmd.ClearRenderTarget(true, true, Color.red); context.ExecuteCommandBuffer(cmd); cmd.Release(); context.Submit(); } ``` Render Pipeline Asset --------------------- 那么怎么让我们上面的代码生效呢?这里就需要我们的 Render Pipeline Asset 出场了。从 Asset 关键字可以看出,它属于一种资源文件,例如在 URP 中,我们就可以用下面方法来创建一个属于 URP 的 Render Pipeline Asset。 ![](https://pic4.zhimg.com/v2-f710012e17c1f0589e7417423556ee43_r.jpg) 那么我们怎么创建上面自定义的 Render Pipeline Instance 对应的 Asset 文件呢?只需要新建一个脚本继承 [RenderPipelineAsset](https://docs.unity3d.com/ScriptReference/Rendering.RenderPipelineAsset.html) 类,并且重写里面的 **CreatePipeline()** 方法,返回我们的 Render Pipeline Instance 即可,如下: ``` [CreateAssetMenu(menuName = "Rendering/CustomRenderPipelineAsset")] public class CustomRenderPipelineAsset : RenderPipelineAsset { protected override RenderPipeline CreatePipeline() { return new CustomRenderPipeline(); } } ``` 通常情况下,我们会把 Render Pipeline Instance 中需要用于渲染的一些信息存储在 Asset 文件中。例如前面我们用了红色来清屏,我们可以利用 Asset 来配置这个颜色,然后传递给 Instance,代码如下: ``` [CreateAssetMenu(menuName = "Rendering/CustomRenderPipelineAsset")] public class CustomRenderPipelineAsset : RenderPipelineAsset { public Color clearColor; protected override RenderPipeline CreatePipeline() { return new CustomRenderPipeline(this); } } ``` ``` public class CustomRenderPipeline : RenderPipeline { CustomRenderPipelineAsset m_asset; public CustomRenderPipeline(CustomRenderPipelineAsset asset) { m_asset = asset; } protected override void Render(ScriptableRenderContext context, Camera[] cameras) { var cmd = new CommandBuffer(); cmd.ClearRenderTarget(true, true, m_asset.clearColor); context.ExecuteCommandBuffer(cmd); cmd.Release(); context.Submit(); } } ``` 除此之外我们还可以根据不同的设备配置来指定不同的 Asset,来达到不同硬件条件下的适配效果。 接着我们就可以创建我们自定义的 Render Pipeline Asset 了: ![](https://pic1.zhimg.com/v2-94ecb1c0725f0481f0afa7163f66dafc_r.jpg) 在 Asset 的 Inspector 界面就可以设置清屏的颜色,如图我设置一个黄色: ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='290' height='133'></svg>) 最后我们要在 Project Settings 的 Graphics 中,关联上我们的 Asset 文件,如图: ![](https://pic4.zhimg.com/v2-7f43fbd0dde982c13c4e14971cef9d87_r.jpg) 此时我们的画面就变得一片黄色,说明我们 Instance 中的代码生效了: ![](https://pic1.zhimg.com/v2-2f19a5f1ed9275faaa6fdea85b1742b8_r.jpg) 接着我们来想办法在这一片屎黄色的屏幕上加点什么,ScriptableRenderContext 为我们提供了 **DrawRenderers** 方法来绘制可见的物体,具体函数如下: ``` public void DrawRenderers(CullingResults cullingResults,ref DrawingSettings drawingSettings, ref FilteringSettings filteringSettings); ``` 其中参数有 CullingResults,DrawingSettings 和 FilteringSettings 对象,我们依次来了解一下。 CullingResults -------------- CullingResults,顾名思义就是剔除后的结果。在 SRP 的每个**渲染循环**里(Render Loop,每帧执行的所有渲染操作我们称之为一个渲染循环),渲染过程中通常会对每个 Camera 做剔除操作留下可见的物体,然后再渲染它们以及处理可见光。 我们可以通过 ScriptableRenderContext.Cull 方法来执行剔除操作,如下: ``` public CullingResults Cull(ref ScriptableCullingParameters parameters); ``` 其中 [ScriptableCullingParameters](https://docs.unity3d.com/ScriptReference/Rendering.ScriptableCullingParameters.html) 参数决定了剔除的规则,该参数通常从当前渲染的 Camera 中获得,即 Camera.TryGetCullingParameters 方法,如下: ``` public bool TryGetCullingParameters(out ScriptableCullingParameters cullingParameters); ``` 此外我们还可以手动的修改得到的 ScriptableCullingParameters 结构体,来更新剔除的规则,例如: ``` //增加遮挡剔除 cullingParameters.cullingOptions |= CullingOptions.OcclusionCull; //剔除除了default layer之外的layer cullingParameters.cullingMask = 1 << 0; ``` 执行 Cull 方法后,得到的结果即存储在 CullingResults 对象中,并且在每次渲染循环完成后,CullingResults 所占的内存都会被释放掉。 DrawingSettings --------------- DrawingSettings 用来描述可见物体的排序方式,以及绘制它们时使用的 Shader Pass,其构造函数如下: ``` public DrawingSettings(ShaderTagId shaderPassName, SortingSettings sortingSettings); ``` ShaderTagId 用于关联 Shader 中的 Tag id,在 SRP 中,我们可以使用 **LightMode** 这个 Pass 块里的 Tag 来决定我们的绘制方式。在内置的渲染管线中,我们的 LightMode 可以选择诸如 Always,ForwardBase,Deferred 等等值,但是现在我们要自定义渲染管线,因此 LightMode 的值就需要我们自定义,例如我们可以在 Shader 的 Pass 中添加如下代码: ``` Tags { "LightMode" = "CustomLightModeTag"} ``` 那么我们 DrawingSettings 中需要的 ShaderTagId 即为: ``` ShaderTagId shaderTagId = new ShaderTagId("CustomLightModeTag"); ``` 这样就会找到 Shader 中 LightMode 为 CustomLightModeTag 的 Pass 进行渲染。 接着是 SortingSettings 结构图,对应着排序方式,我们可以通过下面方法获得: ``` var sortingSettings = new SortingSettings(camera); ``` 怎么理解排序方式呢?可以参考 [Camera.transparencySortMode](https://docs.unity3d.com/ScriptReference/Camera-transparencySortMode.html)。简单来说,默认情况下正交摄像机的话,排序只考虑物体与摄像机在 Camera.forward 方向上的距离。而透视摄像机直接根据物体到摄像机的距离进行排序。 最终我们的 DrawingSettings 即为: ``` DrawingSettings drawingSettings = new DrawingSettings(shaderTagId, sortingSettings); ``` FilteringSettings ----------------- FilteringSettings 用来描述渲染时如何过滤可见物体,可通过如下方法获得: ``` FilteringSettings filteringSettings = FilteringSettings.defaultValue; filteringSettings.layerMask = 1 << 0; ``` defaultValue 即表示不进行任何的过滤,设置 layerMask,即只显示属于该 layer 的物体。 完整 RenderPipeline 代码 -------------------- 通过上面,DrawRenderers 需要的参数就都可以获得了,我们就可以绘制物体了,完整代码如下: ``` protected override void Render(ScriptableRenderContext context, Camera[] cameras) { var cmd = new CommandBuffer(); //清除上一帧绘制的东西 cmd.ClearRenderTarget(true, true, Color.black); context.ExecuteCommandBuffer(cmd); cmd.Release(); // 会有显示Scene视图的SceneCamera,点击Camera时显示Preview视图的PreviewCamera,以及场景中我们添加的Camera foreach(Camera camera in cameras) { //获取当前相机的剔除规则,进行剔除 camera.TryGetCullingParameters(out var cullingParameters); var cullingResults = context.Cull(ref cullingParameters); //根据当前Camera,更新内置Shader的变量 context.SetupCameraProperties(camera); //生成DrawingSettings ShaderTagId shaderTagId = new ShaderTagId("CustomLightModeTag"); var sortingSettings = new SortingSettings(camera); DrawingSettings drawingSettings = new DrawingSettings(shaderTagId, sortingSettings); //生成FilteringSettings FilteringSettings filteringSettings = FilteringSettings.defaultValue; context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings); if(camera.clearFlags == CameraClearFlags.Skybox && RenderSettings.skybox != null) { //绘制天空盒 context.DrawSkybox(camera); } context.Submit(); } } ``` 自定义 Shader ---------- 别忘了,在自定义管线里,我们用的是 Tags {"LightMode" = "CustomLightModeTag"} 的 Shader,因此我们要新建一个 Shader,然后 Pass 里添加我们指定的 Tag。 一个简单的无光照 Shader 如下: ``` Shader "Custom/UnlitColor" { SubShader { Pass { Tags { "LightMode" = "CustomLightModeTag"} HLSLPROGRAM #pragma vertex vert #pragma fragment frag float4x4 unity_MatrixVP; float4x4 unity_ObjectToWorld; struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; float4 worldPos = mul(unity_ObjectToWorld, v.vertex); o.vertex = mul(unity_MatrixVP, worldPos); return o; } float4 frag (v2f i) : SV_TARGET { return float4(0.5,1,0.5,1); } ENDHLSL } } } ``` 最后我们只需要创建一个 Material 来关联这个 Shader,并且在场景中创建 Cube,Sphere 等 GameObject,并且使用我们的 Material 即可。 得到的效果如下: ![](https://pic1.zhimg.com/v2-165a0b3e3eefc0a8fa8d0bfa2621fa7c_r.jpg) 本文是一个最简单的例子,如果我们要在 SRP 里添加更多复杂的功能,可以在 Package Manager 中安装 [Core RP Liberary](https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@11.0/manual/index.html)。它里面包含有一些核心的 shader 库,使用它们可以让我们的 shader 兼容 SRP Batcher,此外还有很多工具函数适用于一些常见操作。 菲涅尔方程(Fresnel Equation) Unity中使用ComputeShader做视锥剔除(View Frustum Culling)