您需要 登录 才可以下载或查看,没有账号?注册
x
Hello . 大家好
今天是unity自定义管线系列
本教程使用Unity 2019.2.6f1制作,升级至2022.3.5f1。
本系列中的内容可能看起来与 Unity 2022 略有不同,因为它最开始是使用 Unity 2019 制作的,但是没有影响。而且文章已经指出了一些必要的升级更改。
使用自定义渲染管道进行渲染
1新的渲染管线
为了渲染各种各样的物件,Unity 必须确定必须绘制什么形状、在哪里、何时以及使用什么设置,这可能会非常复杂,具体取决于涉及的效果数量。灯光、阴影、透明度、图像效果、体积效果等都必须以正确的顺序处理才能获得最终图像,这就是渲染管道的作用。
在此之前,Unity 仅支持几种内置的渲染方式。Unity 2018 引入了可编写脚本的渲染管道(简称 RP),使我们可以做任何我们想做的事情,同时仍然能够依赖 Unity 来执行剔除等基本操作。Unity 2018 还添加了使用这种新方法制作的两个实验性 RP:LWRP 和HDRP。LWRP在 Unity 2019.3 中正式更名为URP。
URP 注定会取代当前的旧 RP 作为默认值。现在看来它是一款最适合的 RP,而且也很容易定制。本系列不是自定义 RP,而是从头开始创建整个 RP。
本教程通过使用前向渲染绘制未照亮的形状为最小 RP 奠定了基础。绘制完成以后,我们就可以在后面的教程中扩展我们的管道,添加照明、阴影、不同的渲染方法和更高级的功能。
1.1项目设置
在Unity 2022.3.5f1或更高版本中创建一个新的3D项目。我们将创建自己的管道,因此不要选择任何 RP 项目模板。项目打开后,你可以转到包管理器并删除所有不需要的包。我们只会使用本教程中的Unity UI包用于尝试绘制 UI,所以可以保留这个包。
我们将专门在线性颜色空间中工作,但 Unity 2019.2 仍然使用伽玛空间作为默认值。通过以下方式进入播放器设置Edit / Project Settings进而Player,然后切换Color Space在下面Other Settings节至Linear。
颜色空间设置为线性用standard、unlit的不透明和透明材质来设置默认场景。Unlit/Transparent只有一张贴图,所以用下面的uv球体贴图。
uv球体alpha贴图,黑色背景我在测试场景中放置了一些立方体,它们都是不透明的。红色的使用的材质是Standard,而绿色和黄色的使用材质Unlit/Color。蓝色球体使用Standard并且Rendering Mode设置Transparent,而白色球体则使Unlit/Transparent。
测试场景1.2Pipline Assets
目前,Unity 使用默认渲染管道。要将其替换为自定义渲染管道,我们首先必须为其创建资产类型。这里将使用与 Unity 用于URP 大致相同的文件夹结构。创建一个Custom RP资产文件夹和Runtime子文件夹。在其中放置一个新的 C# 脚本作为该CustomRenderPipelineAsset类型。
文件夹结构资产类型必须从UnityEngine.Rendering namespace的RenderPiplineAsset中继承using System.Collections.Generic;using UnityEngine;using UnityEngine.Rendering;publicclassCustomRenderPipelineAsset : RenderPipelineAsset {}RP 资源的主要目的是为 Unity 提供一种方法来获取负责渲染的管道对象实例。资产本身只是一个句柄和一个存储设置的地方。我们还没有任何设置,所以我们所要做的就是为 Unity 提供一种获取管道对象实例的方法。这是通过重写抽象CreatePipeline方法来完成的,该方法应该返回一个RenderPipeline实例。但我们还没有定义自定义 RP 类型,因此首先返回null.该CreatePipeline方法是使用访问修饰符定义的 protected,这意味着只有定义该方法的类(RenderPipelineAsset也就是扩展该方法的类)才能访问该方法。
protectedoverrideRenderPipeline CreatePipeline () {return null;}现在我们需要将这种类型的资产添加到我们的项目中。[CreateAssetMenu]publicclassCustomRenderPipelineAsset : RenderPipelineAsset { … }这会在Asset/Create菜单下面创建一个空的,让我们整理一下并把它放在一个Rendering子菜单下。通过将menuName属性的属性设置为Rendering/Custom Render Pipeline来做到这一点。
可以在属性类型之后直接在圆括号内设置此属性。
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
publicclassCustomRenderPipelineAsset : RenderPipelineAsset { … }使用新菜单项将资产添加到项目中,然后转到Graphics项目设置并在下面选择它Scriptable Render Pipeline Settings。
选择自定义RP替换默认 RP 后让项目发生了一些变化。首先,许多选项从图形设置中隐藏了,这在信息面板中可以看到。其次,我们禁用了默认 RP,但没有提供有效的替换,因此不再渲染任何内容。游戏窗口、场景窗口和材质预览不再起作用。如果你通过Window / Analysis / Frame Debugger打开帧调试器,你会看到游戏窗口中确实没有绘制任何内容。
1.3渲染管线实例
创建一个CustomRenderPipeline类并将其作为脚本文件CustomRenderPipelineAsset放在相同的文件夹下. 这就是我们的资产返回的 RP 实例所使用的类型,因此它必须扩展RenderPipeline。
using UnityEngine;
using UnityEngine.Rendering;publicclassCustomRenderPipeline : RenderPipeline {}RenderPipeline定义了一个受保护的抽象Render方法,我们必须重写该方法才能创建具体的管道。它有两个参数:一个是ScriptableRenderContex,一个是camera数组,并设置函数为空。
protectedoverridevoid Render (
ScriptableRenderContext context, Camera[] cameras) {}此方法是为自定义 SRP 定义的入口点,但由于相机阵列参数需要为每一帧分配内存,因此引入了具有列表参数的替代方法。
我们可以在 Unity 2022 中使用该版本,但仍然必须保留另一个版本,因为它被声明为抽象的,即使它不会被使用。
注意后面的分析器屏幕截图包括相机阵列的旧分配。protectedoverridevoid Render (ScriptableRenderContext context, List<Camera> cameras) { }让 CustomRenderPipeline : RenderPipeline返回一个新的CustomRenderPipeline实例,这会为我们提供一个有效且实用的管道,尽管它还没有渲染任何东西。
protectedoverrideRenderPipeline CreatePipeline () {
returnnewCustomRenderPipeline();}
2渲染
Unity 在每帧调用渲染管线实例的 Render 方法。它传递一个上下文结构,该结构提供与本机引擎的连接,我们可以使用它来进行渲染。它还传递一系列摄像机,因为场景中可以有多个活动摄像机。RP 负责按照提供的顺序渲染所有这些摄像机。2.2相机渲染器
每个相机都独立渲染。因此,我们不会让CustomRenderPipeline渲染所有相机,而是将这一责任转发给一个专门用于渲染一个相机的新类。为其命名CameraRenderer并为其提供一个Render带有上下文和相机参数的公共方法。为了方便起见,我们将这些参数存储在字段中。
using UnityEngine;
using UnityEngine.Rendering;publicclassCameraRenderer {ScriptableRenderContext context;Camera camera;publicvoid Render (ScriptableRenderContext context, Camera camera) {this.context = context;this.camera = camera;}}在创建渲染器时创建CustomRenderPipeline一个实例,然后使用它循环渲染所有摄像机。
CameraRenderer renderer = newCameraRenderer();
protectedoverridevoid Render (ScriptableRenderContext context, Camera[] cameras) {}protectedoverridevoid Render ( ScriptableRenderContext context, List<Camera> cameras) {for (int i = 0; i < cameras.Count; i++) {renderer.Render(context, cameras);}}我们的相机渲染器大致相当于URP 的可编写脚本的渲染器。这种方法将使未来支持每个摄像机的不同渲染方法变得简单,例如一种用于第一人称视图,一种用于 3D 地图叠加,或者前向渲染与延迟渲染。但现在我们将以相同的方式渲染所有摄像机。
2.3绘制天空盒
CameraRenderer.Render的功能是绘制其相机可以看到的所有几何图形。为了清楚起见,将特定任务隔离在单独的DrawVisibleGeometry方法中。我们首先让它绘制默认的天空盒,这可以通过DrawSkybox以相机作为参数调用上下文来完成。publicvoid Render (ScriptableRenderContext context, Camera camera) {this.context = context;this.camera = camera;DrawVisibleGeometry();}void DrawVisibleGeometry () {context.DrawSkybox(camera);}以上还没有使天空盒成功被绘制。这是因为我们向上下文发出的命令被缓冲了。我们必须通过调用Submit上下文来提交排队的工作以供执行。让我们在一个单独的Submit方法中执行此操作,并在 之后调用DrawVisibleGeometry。
publicvoid Render (ScriptableRenderContext context, Camera camera) {
this.context = context;this.camera = camera;DrawVisibleGeometry();Submit();}void Submit () {context.Submit();}天空盒最终出现在游戏和场景窗口中。启用它后,你还可以在帧调试器中看到它。它被列为Camera.RenderSkybox,其中有一个Draw Mesh在下面,代表实际的绘制调用。这对应于游戏窗口的渲染。帧调试器不会显示其他窗口中的绘图。
天空盒被绘制注意一下,相机的方向当前不会影响天空盒的渲染方式。我们将相机传递给DrawSkybox,但这仅用于确定是否应该绘制天空盒,这是通过相机的清除标志控制的。
为了正确渲染天空盒以及整个场景,我们必须设置视图投影矩阵。该变换矩阵将相机的位置和方向(视图矩阵)与相机的透视或正交投影(投影矩阵)相结合。它在着色器中被称为unity_MatrixVP,绘制几何体时使用的着色器属性之一。您可以在帧调试器中检查该矩阵ShaderProperties选择绘制调用时的部分。
目前,unity_MatrixVP矩阵总是相同的。我们必须通过该方法将相机的属性应用于上下文SetupCameraProperties。这设置了矩阵以及其他一些属性。在调用 之前DrawVisibleGeometry以单独的Setup方法执行此操作。
publicvoid Render (ScriptableRenderContext context, Camera camera) {
this.context = context;this.camera = camera;Setup();DrawVisibleGeometry();Submit();}void Setup () {context.SetupCameraProperties(camera);}
天空盒,正确对齐
2.4命令缓冲区
上下文会延迟实际渲染,直到被提交。在此之前,我们对其进行配置并向其添加命令以供稍后执行。某些任务(例如绘制天空盒)可以通过专用方法发出,但其他命令必须通过单独的命令缓冲区间接发出。我们需要这样的缓冲区来绘制场景中的其他几何体。
为了获得缓冲区,我们必须创建一个新的CommandBuffer对象实例。我们只需要一个缓冲区,因此默认创建一个缓冲区CameraRenderer并将对其的引用存储在字段中。还要为缓冲区命名,以便我们可以在帧调试器中识别它。
conststring bufferName = "Render Camera";
CommandBuffer buffer = newCommandBuffer {name = bufferName};我们可以使用命令缓冲区来注入分析器示例,这些示例将显示在分析器和帧调试器中。这是通过在适当的点调用BeginSample和EndSample来完成的,在我们的例子中,两种方法必须提供相同的示例名称,为此我们将使用缓冲区的名称。
void Setup () {
buffer.BeginSample(bufferName);context.SetupCameraProperties(camera);}void Submit () {buffer.EndSample(bufferName);context.Submit();}要执行缓冲区,请使用ExecuteCommandBuffer缓冲区作为参数调用上下文。这会从缓冲区复制命令但不会清除它,如果我们想重用它,我们必须在之后显式地执行此操作。因为执行和清除总是一起完成,所以添加一个同时执行这两项操作的方法很方便。
void Setup () {
buffer.BeginSample(bufferName);ExecuteBuffer();context.SetupCameraProperties(camera);}void Submit () {buffer.EndSample(bufferName);ExecuteBuffer();context.Submit();}void ExecuteBuffer () {context.ExecuteCommandBuffer(buffer);buffer.Clear();}现在Camera.RenderSkyBox是嵌套在Render Camera里面的。
渲染相机
2.5清除渲染目标
无论我们绘制什么,最终都会渲染到相机的渲染目标,默认情况下是帧缓冲区,但也可以是渲染纹理。之前绘制到该目标的任何内容仍然存在,这可能会干扰我们现在渲染的图像。为了保证正确的渲染,我们必须清除渲染目标以摆脱其旧内容。这是通过调用ClearRenderTarget属于该Setup方法的命令缓冲区来完成的。
CommandBuffer.ClearRenderTarget需要至少三个参数。前两个表示是否应清除深度和颜色数据,两者都是如此。第三个参数是用于清除的颜色,我们将使用Color.clear。
void Setup () {
buffer.BeginSample(bufferName);buffer.ClearRenderTarget(true, true, Color.clear);ExecuteBuffer();context.SetupCameraProperties(camera);}
清除带有嵌套的层帧调试器现在显示Draw GL清除操作的内容,它嵌套在附加级别中Render Camera。发生这种情况是因为ClearRenderTarget用命令缓冲区的名称将清除包装在sample中。我们可以在开始我们自己的sample之前通过清除来摆脱多余的嵌套。这会导致两个相邻的Render Camera示例范围,这些范围被合并。void Setup () {
buffer.ClearRenderTarget(true, true, Color.clear); buffer.BeginSample(bufferName);//buffer.ClearRenderTarget(true, true, Color.clear);ExecuteBuffer();context.SetupCameraProperties(camera);}
清除 不嵌套这Draw GL代表绘制一个全屏四边形Hidden/InternalClear写入渲染目标的着色器,这不是清除它的最有效方法。使用这种方法是因为我们在设置相机属性之前要进行清除。如果我们交换这两个步骤的顺序,我们就可以快速清除。
void Setup () {
context.SetupCameraProperties(camera);buffer.ClearRenderTarget(true, true, Color.clear);buffer.BeginSample(bufferName);ExecuteBuffer();//context.SetupCameraProperties(camera);}
正确清理现在我们看到Clear (color+Z+stencil),这表明颜色和深度缓冲区都被清除。Z 代表深度缓冲区,模板数据是同一缓冲区的一部分。
2.6剔除
我们当前看到的是天空盒,但看不到我们放置在场景中的任何对象。我们不会绘制每个对象,而是只渲染那些对相机可见的对象。为此,我们从场景中具有渲染器组件的所有对象开始,然后剔除那些落在相机视锥体之外的对象。
弄清楚可以剔除的内容需要我们跟踪多个相机设置和矩阵,为此我们可以使用该ScriptableCullingParameters结构体。我们可以在相机上调用TryGetCullingParameters,而不是自己填充它。它返回是否可以成功检索参数,因为对于退化的相机设置可能会失败。为了获取参数数据,我们必须通过out在其前面写入来将其作为输出参数提供。在返回成功或失败的单独cull方法中执行此操作。bool Cull () {
ScriptableCullingParameters pif (camera.TryGetCullingParameters(out p)) {returntrue;}returnfalse;}当用作输出参数时,可以将变量声明内联到参数列表中。bool Cull () {//ScriptableCullingParameters p
if (camera.TryGetCullingParameters(outScriptableCullingParameters p)) {returntrue;}returnfalse;}在设置Render之前插入Cull并且如果失败就丢弃掉
public void Render (ScriptableRenderContext context, Camera camera) {this.context = context;this.camera = camera;if (!Cull()) {return;}Setup();DrawVisibleGeometry();Submit();}实际的剔除是通过调用Cull上下文来完成的,上下文会生成一个CullingResults结构体。如果成功则执行此操作Cull并将结果存储在字段中。在这种情况下,我们必须通过ref在其前面写入来将剔除参数作为引用参数传递。
CullingResults cullingResults;…
bool Cull () {if (camera.TryGetCullingParameters(outScriptableCullingParameters p)) {cullingResults = context.Cull(ref p);returntrue;}returnfalse;}
2.7绘制几何图形
一旦我们知道什么是可见的,我们就可以继续渲染这些东西。这是通过调用DrawRenderers上下文并将剔除结果作为参数来完成的,告诉它要使用哪些渲染器。除此之外,我们还必须提供绘图设置和过滤设置。两者都是结构体,DrawingSettings和FilteringSettings是我们最初将使用的默认构造函数。两者都必须通过引用传递。在绘制天空盒之前,在 DrawVisibleGeometry中执行此操作。
void DrawVisibleGeometry () {
var drawingSettings = newDrawingSettings();var filteringSettings = newFilteringSettings();context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);context.DrawSkybox(camera);}我们还没有看到任何东西,因为我们还必须说明允许哪种类型的着色器pass。由于我们在本教程中仅支持unlit着色器,因此我们必须获取着色器标签 IDSRPDefaultUnlitpass,我们可以执行一次并将其缓存在静态字段中。
staticShaderTagId unlitShaderTagId = newShaderTagId("SRPDefaultUnlit");将其作为 DrawingSettings 构造函数的第一个参数提供,同时还需要提供一个新的 SortingSettings 结构值。将相机传递给 SortingSettings 的构造函数,因为它用于确定是使用正交排序还是基于距离的排序。
void DrawVisibleGeometry () {
var sortingSettings = newSortingSettings(camera);var drawingSettings = newDrawingSettings(unlitShaderTagId, sortingSettings);}除此之外,我们还需要指定允许的渲染队列。将 RenderQueueRange.all 作为参数传递给 FilteringSettings 构造函数,以便我们包含所有内容。
var filteringSettings = newFilteringSettings(RenderQueueRange.all);
绘制unlit的几何图形只有使用unlit的可见对象才会被绘制。所有的绘制调用都会在帧调试器中列出,在 RenderLoop.Draw 下进行分组。透明对象存在一些奇怪的问题,但首先让我们来看一下绘制对象的顺序。在帧调试器中,你可以通过依次选择或使用箭头键来遍历drawcall。
[micxp_wxonkey]wxv_3167933271065509895[/micxp_wxonkey]绘制顺序是随意的。我们可以通过设置排序设置的 criteria 属性来强制指定绘制顺序。我们使用 SortingCriteria.CommonOpaque来进行设置。var sortingSettings = newSortingSettings(camera) {
criteria = SortingCriteria.CommonOpaque}; [micxp_wxonkey]wxv_3167934154201382921[/micxp_wxonkey]
常见的不透明排序现在,对象或多或少是从前到后绘制的,这对于不透明对象来说是理想的选择。如果某些东西最终绘制在其他东西后面,则可以跳过其被遮挡的片段,从而加快渲染速度。常见的不透明排序选项还考虑了一些其他标准,包括渲染队列和材质。
2.8分别绘制不透明和透明几何体
帧调试器显示了透明对象的绘制,但天空盒会覆盖在那些没有出现在不透明对象前面的所有对象上。天空盒在不透明几何体之后绘制,以便可以跳过所有隐藏的片段,但它会覆盖透明的几何体。这是因为透明着色器不会写入深度缓冲区。它们不会隐藏它们后面的任何东西,因为我们可以透过它们看到。解决方法是先绘制不透明对象,然后是天空盒,最后才是透明对象。
我们可以通过切换到 RenderQueueRange.opaque 来在初始的 DrawRenderers 调用中排除透明对象。
var filteringSettings = newFilteringSettings(RenderQueueRange.opaque);然后,在绘制天空盒之后,再次调用 DrawRenderers。但在这样做之前,将渲染队列范围更改为 RenderQueueRange.transparent。同时,还要将排序标准更改为 SortingCriteria.CommonTransparent,并重新设置绘制设置的排序。这将颠倒透明对象的绘制顺序。context.DrawSkybox(camera);sortingSettings.criteria = SortingCriteria.CommonTransparent;drawingSettings.sortingSettings = sortingSettings;filteringSettings.renderQueueRange = RenderQueueRange.transparent;context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);[micxp_wxonkey]wxv_3167934680653643779[/micxp_wxonkey]
不透明,天空盒,然后透明
3编辑器渲染
我们的渲染管线正确地绘制了unlit对象,但是我们可以做一些改进,以提高在Unity编辑器中使用它的体验。
3.1绘制旧版着色器
由于我们的管线只支持unlit着色器pass,因此使用不同pass的对象不会被渲染,所以不可见。虽然理论上这是正确的,但会和场景中绘制错误shader的物件混淆。所以,我们需要分开对他们进行渲染。
如果有人从默认的Unity项目开始,然后切换到我们的渲染管线,那么场景中可能会有使用错误着色器的对象。为了涵盖所有Unity的默认着色器,我们需要使用Always、ForwardBase、PrepassBase、Vertex、VertexLMRGBM和VertexLM的着色器标签ID。我们将这些放在一个静态数组中。static ShaderTagId[] legacyShaderTagIds = {
newShaderTagId("Always"),newShaderTagId("ForwardBase"),newShaderTagId("PrepassBase"),newShaderTagId("Vertex"),newShaderTagId("VertexLMRGBM"),newShaderTagId("VertexLM")};在绘制完可见几何体之后,使用一个单独的方法绘制所有不受支持的着色器,首先绘制第一个pass。由于这些pass是无效的,结果将无论如何都是错误的,因此我们不关心其他设置。我们可以通过 FilteringSettings.defaultValue 属性获取默认的过滤设置。
public void Render (ScriptableRenderContext context, Camera camera) {
Setup();DrawVisibleGeometry();DrawUnsupportedShaders();Submit();}void DrawUnsupportedShaders () {var drawingSettings = newDrawingSettings(legacyShaderTagIds[0], newSortingSettings(camera));var filteringSettings = FilteringSettings.defaultValue;context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);}我们可以通过在绘制设置中使用 draw order 索引和标签作为参数,调用 SetShaderPassName 来绘制多个pass。对于数组中的所有pass,从第二个开始进行此操作,因为在构造绘制设置时已经设置了第一个pass。var drawingSettings = newDrawingSettings(
legacyShaderTagIds[0], newSortingSettings(camera));for (int i = 1; i < legacyShaderTagIds.Length; i++) {drawingSettings.SetShaderPassName(i, legacyShaderTagIds);}
standard shader呈现黑色
3.2错误的material
为了明确指示哪些对象使用了不支持的着色器,我们将使用Unity的错误着色器来绘制它们。通过将 Hidden/InternalErrorShader 字符串作为参数调用 Shader.Find 来查找该着色器。通过静态字段缓存材质,这样我们就不会在每一帧创建一个新的材质。然后将它分配给绘制设置的 overrideMaterial 属性。staticMaterial errorMaterial;
void DrawUnsupportedShaders () {if (errorMaterial == null) {errorMaterial =new Material(Shader.Find("Hidden/InternalErrorShader"));}var drawingSettings = new DrawingSettings(legacyShaderTagIds[0], newSortingSettings(camera)) {overrideMaterial = errorMaterial};}
用紫色来绘制错误的shader现在就可以直观看到所有错误的对象了。
3.3Partial Class
绘制无效对象对于开发很有用,但不适用于发布的应用程序。因此,让我们将相机渲染器中所有仅用于编辑器的代码放在一个单独的部分类文件中。首先,复制原始的相机渲染器脚本资源,并将其重命名为 CameraRenderer.Editor
一个class两个scripts然后,将原始的CameraRenderer转换为Partial Class,并从中删除标记数组、错误材质和 DrawUnsupportedShaders 方法。
publicpartialclassCameraRenderer { … }清理其他的partial class文件只留下我们从其他地方移动过来的文件using UnityEngine;
using UnityEngine.Rendering;partialclassCameraRenderer {static ShaderTagId[] legacyShaderTagIds = {… };staticMaterial errorMaterial;void DrawUnsupportedShaders () { … }}编辑器的Content部分只存在编辑器中,所以给一个宏UNITY_EDITOR
partialclassCameraRenderer {#if UNITY_EDITORstaticShaderTagId[] legacyShaderTagIds = { … }};staticMaterial errorMaterial;void DrawUnsupportedShaders () { … }#endif}然而,此时进行构建将会失败,因为另一部分始终包含对 DrawUnsupportedShaders 的调用,而它现在仅在编辑器中存在。为了解决这个问题,我们也将这个方法设置为部分方法。我们可以在类定义的任何部分中进行声明,就像抽象方法声明一样,在方法签名前面添加 partial 关键字。让我们将其放在编辑器部分。完整的方法声明也必须标记为 partial。
partialvoid DrawUnsupportedShaders ();#if UNITY_EDITOR…partialvoid DrawUnsupportedShaders () { … }#endif现在编译构建成功。编译器将会剥离掉所有没有完整声明的部分方法的调用。
3.4绘制Gizmos
没有Gizmos的场景我们可以通过调用 UnityEditor.Handles.ShouldRenderGizmos 来检查是否应该绘制 Gizmos。如果是,则需要使用相机作为参数,在上下文中调用 DrawGizmos 方法,并传入第二个参数来指示应该绘制哪个 Gizmo 子集。有两个子集,用于图像特效之前和之后。由于我们当前不支持图像特效,我们将同时调用这两个子集。请在一个新的仅限于编辑器的 DrawGizmos 方法中完成这个操作。
using UnityEditor;using UnityEngine;using UnityEngine.Rendering;partialclassCameraRenderer {partialvoid DrawGizmos ();partialvoid DrawUnsupportedShaders ();#if UNITY_EDITOR…partialvoid DrawGizmos () {if (Handles.ShouldRenderGizmos()) {context.DrawGizmos(camera, GizmoSubset.PreImageEffects);context.DrawGizmos(camera, GizmoSubset.PostImageEffects);}}partialvoid DrawUnsupportedShaders () { … }#endif}Gizmo应该在所有物件之后绘制
publicvoid Render (ScriptableRenderContext context, Camera camera) {…Setup();DrawVisibleGeometry();DrawUnsupportedShaders();DrawGizmos();Submit();}
有了gizmos的场景
3.5绘制unity ui
还有一件需要我们关注的事情是Unity的游戏内用户界面。例如,通过GameObject / UI / Button来添加一个简单的按钮来创建一个UI。它会在游戏窗口中显示出来,但不会在场景窗口中显示。
game窗口的ui按钮帧调试器显示ui是单独渲染的,而不是我们的RP渲染的
帧调试器中的ui至少,在画布组件的渲染模式设置为屏幕空间 - 覆盖时是这样的,这也是默认设置。将其更改为屏幕空间 - 摄像机,并使用主摄像机作为其渲染摄像机,将使其成为透明几何体的一部分。
帧调试器中的屏幕空间相机ui当 UI 在场景窗口中渲染时,它始终使用世界空间坐标,这就是为什么它通常会变得非常大的原因。但是虽然我们可以通过场景窗口编辑 UI,但它不会被绘制出来。
ui在场景窗口中不可见当渲染场景窗口时,我们必须通过调用ScriptableRenderContext.EmitWorldGeometryForSceneView 方法将 UI 添加到世界几何体中,并将相机作为参数传入。在一个新的仅限编辑器使用的 PrepareForSceneWindow 方法中进行这个操作。我们在场景摄像机的 cameraType 属性等于 CameraType.SceneView 时进行渲染。partialvoid PrepareForSceneWindow ();
#if UNITY_EDITOR…partialvoid PrepareForSceneWindow () {if (camera.cameraType == CameraType.SceneView) {ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);}}由于这可能会向场景添加几何图形,因此必须在剔除之前完成。PrepareForSceneWindow();if (!Cull()) {return;}
ui在场景窗口中可见
4多个摄像机
场景中可能不止一台摄像机,所以我们需要保证都一起工作。
4.1两个摄像机
每个相机都有一个深度值,对于默认的主相机来说,这个值是 -1。它们按照深度递增的顺序进行渲染。为了验证这一点,可以复制主相机,将其重命名为 Secondary Camera,并将其深度值设置为 0。另外,给它一个不同的标签也可以,因为 MainCamera 应该只被单个相机使用。
两台相机都分组在一个标签类中现在场景被渲染了两次。由于渲染目标在之间被清除,所以得到的图像仍然是相同的。帧调试器显示了这一点,但是由于具有相同名称的相邻采样范围被合并,我们最终得到了一个单独的 Render Camera 范围。
如果每个相机都有自己的范围,那就更清晰了。为了实现这一点,添加一个仅用于编辑器的 PrepareBuffer 方法,使缓冲区的名称与相机的名称相同。
partialvoid PrepareBuffer ();
#if UNITY_EDITOR…partialvoid PrepareBuffer () {buffer.name = camera.name;}#endif在我们准备场景窗口之前调用它。
PrepareBuffer();PrepareForSceneWindow();
每个相机单独标签类
4.2处理更改缓冲区名称
尽管帧调试器现在显示了每个相机的单独采样层次结构,但当我们进入播放模式时,Unity 的控制台会充斥着警告消息,告诉我们 BeginSample 和 EndSample 的计数必须匹配。它会混淆因为我们为采样和缓冲区使用了不同的名称。除此之外,每次访问相机的名称属性时,我们还会分配内存,所以我们不希望在构建中这样做。
为了解决这两个问题,我们将添加一个 SampleName 字符串属性。如果我们在编辑器中,我们会在 PrepareBuffer 方法中设置它,同时设置缓冲区的名称;否则,它只是一个 Render Camera 字符串的常量别名。#if UNITY_EDITOR… string SampleName { get; set; }…partialvoid PrepareBuffer () {buffer.name = SampleName = camera.name;}#elseconststring SampleName = bufferName;#endif为Setup和Submit标签类使用SamplerNamevoid Setup () {
context.SetupCameraProperties(camera);buffer.ClearRenderTarget(true, true, Color.clear);buffer.BeginSample(SampleName);ExecuteBuffer();}void Submit () {buffer.EndSample(SampleName);ExecuteBuffer();context.Submit();}我们可以通过检查性能分析器(通过窗口/分析/性能分析器打开)来看到差异,并首先在编辑器中播放。切换到层级模式并按 GC Alloc 列进行排序。您会看到两个 GC.Alloc 的调用,共分配了 100 字节,这是由于检索相机名称所引起的。在更下面,你将看到这些名称显示为样本:Main Camera 和 Secondary Camera。
具有单独的标签类和100b allocations的分析器接下来,制作一个启用了“Development Build”和“Autoconnect Profiler”的构建。点击build,并确保分析器已连接并记录。在这种情况下,我们不会有100字节的分配,而是只会得到单个的 Render Camera 标签类。
profiling build我们可以通过在一个名为“Editor Only”的性能采样中包装相机名称检索来清楚地表示我们只在编辑器中分配内存,而不在构建中分配内存。在这种情况下,我们需要从 UnityEngine.Profiling 命名空间调用 Profiler.BeginSample 和 Profiler.EndSample。只需要将名称传递给 BeginSample。
using UnityEditor;
using UnityEngine; using UnityEngine.Profiling;using UnityEngine.Rendering;partialclassCameraRenderer {…#if UNITY_EDITOR…partialvoid PrepareBuffer () {Profiler.BeginSample("Editor Only");buffer.name = SampleName = camera.name;Profiler.EndSample();}#elsestring SampleName => bufferName;#endif}
Editor Only
4.3
Layer
相机还可以配置为只能看到特定层上的物体。这是通过调整它们的剔除掩码(Culling Mask)来实现的。为了看到它的效果,我们将使用标准着色器的所有对象移动到“Ignore Raycast”层。
层切换到Ignore Raycast从主摄像机的Culling Mask中取消勾选Ignore Raycast。
剔除掉Ignore Raycast并让他在第二个camera中唯一可见
只显示ignore raycast因为第二个摄像机最后渲染,所以我们只能看到无效的对象。
只渲染ignore raycast
4.4清除标志
我们可以通过调整第二个相机的清除标志(clearFlags)来合并两个相机的结果。它们由 CameraClearFlags 枚举定义,我们可以通过相机的 clearFlags 属性获取它们。在清除之前,在 Setup 中进行此操作。void Setup () {
context.SetupCameraProperties(camera);CameraClearFlags flags = camera.clearFlags;buffer.ClearRenderTarget(true, true, Color.clear);buffer.BeginSample(SampleName);ExecuteBuffer();}CameraClearFlags 枚举定义了四个值。从 1 到 4,它们分别是 Skybox、Color、Depth 和 Nothing。实际上,这些不是独立的标志值,而是表示清除量递减的。在所有情况下(除了最后一个),都必须清除深度缓冲区,因此很多标志值都为 Depth 。buffer.ClearRenderTarget(flags <= CameraClearFlags.Depth, true, Color.clear);当标志设置为 Color 时,我们实际上只需要清除颜色缓冲区,因为在 Skybox 的情况下,我们最终会替换所有先前的颜色数据。buffer.ClearRenderTarget(flags <= CameraClearFlags.Depth,flags == CameraClearFlags.Color,Color.clear);在Unity 2022中,我更改为始终清除颜色,除非明确告知不清除,因为渲染目标可能包含非数字和无穷大的值,这可能会导致混合伪影。此外,帧调试器可能显示随机数据,这会使调试变得困难。flags <= CameraClearFlags.Color如果我们要清除为纯色,我们必须使用相机的背景颜色。但是,因为我们在线性颜色空间中进行渲染,所以我们必须将该颜色转换为线性空间,因此我们最终需要使用 camera.backgroundColor.linear。在所有其他情况下,颜色并不重要,因此我们可以使用 Color.clear。buffer.ClearRenderTarget(
flags <= CameraClearFlags.Depth,flags == CameraClearFlags.Color,flags == CameraClearFlags.Color ?camera.backgroundColor.linear :Color.clear);因为主相机是第一个进行渲染的,所以它的清除标志应设置为 Skybox 或 Color。当启用帧调试器时,我们始终从清除缓冲区开始,但一般情况下不能保证这一点。
第二个相机的清除标志决定了两个相机的渲染结果如何合并。在使用天空盒或颜色进行清除时,先前的结果将完全被替换。当仅清除深度时,次要相机以正常方式进行渲染,只是不绘制天空盒,所以先前的结果会显示为背景。当不进行任何清除时,深度缓冲区将被保留,因此未照亮的对象会遮挡无效对象,就好像它们是由同一相机绘制的一样。然而,由上一个相机绘制的透明对象没有深度信息,因此会被绘制在上面,就像之前的天空盒一样。
依次是清除颜色,depth-only以及都不清除通过调整相机的 Viewport Rect,还可以将渲染区域缩小为整个渲染目标的一部分。渲染目标的其余部分不受影响。在这种情况下,清除操作使用了 Hidden/InternalClear 着色器。模板缓冲区用于限制渲染到视口区域。
缩小第二个相机的视口,清除color注意每帧渲染多个相机意味着需要进行多次裁剪、设置、排序等操作。通常,每个独特视角使用一个相机是最高效的方法。
原文:
https://catlikecoding.com/unity/tutorials/custom-srp/custom-render-pipeline/
- End -
|