翻译汇总文章:
HipHopBoy:Unity SRP 系列翻译汇总zhuanlan.zhihu.com
原文链接 :
https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/custom-pipeline/catlikecoding.com
原作者:Jasper Flick
由于水平有限,可能翻译的会有错误,请大家在评论区指出,我会及时更新改正。
Custom PipeLine 自定义管线
- 创建一个管线资源和实例
- 裁剪,过滤,排序,渲染
- 保持内存干净
- 提供一个好的编辑体验
这是一个系列教程的第一部分,涵盖了Unity的脚本渲染管线。这个教程假设你已经学过了Basics 系列和Procedural Grid教程。渲染系列的前几个部分也很有用。 这篇教程使用的Unity版本是2018.3.0f2.
要渲染任何东西,Unity必须确定要绘制什么形状,在哪里,什么时候,用什么设置。这可能变得很复杂,取决于涉及到多少条件被影响。灯光,阴影,透明,图像效果,体积光效果等等,全部按照正确的顺序处理,最后生成我们最后的图像。这个过程叫做渲染管线。
Unity 2017支持两个预定义的渲染管线,一个是前向渲染,一个是延迟渲染。它还支持在Unity 5中引入的更老的延迟渲染。这些管线是灵活的,你可以启用,禁用 或者重新管线的某些固定部分,但是不可能彻底的偏离他们的设计。
Unity 2018 添加支持了scriptable render pipelines(脚本化的渲染管线),使从头设计管线成为可能,尽管你还是要依赖于Unity来完成单独的步骤,例如 Culling,Unity 2018 引进了两种用这个新方法制作的管线,the lightweight pipeline 和 the high-definition pipeline。 虽然这两个管道仍然处于预览阶段而且可编写脚本的render管道API仍然标记为实验技术。但是对于我们来说已经可以说很稳定了,足够支撑我们继续探索并且创建我们自己的渲染管线。
在本教程中,我们将设置一个绘制unlit图形的最小渲染管道。一旦它开始工作,我们可以在以后的教程中扩展管道,添加照明、阴影和更高级的特性。
1.1 Project Setup (工程设置)
打开Unity 2018并创建一个新项目。我正在使用Unity 2018.2.9f1,但任何2018.2或更高版本也应该工作。创建一个标准的3D项目,禁用分析功能。我们将创建自己的管道,因此不要选择管道选项。
项目打开后,通过窗口/包管理器进入包管理器,删除默认包含的所有包,因为我们不需要它们。只保留包管理器UI,不能删除它。
我们将工作在线性的颜色空间,但Unity 2018仍然使用伽玛空间作为默认值。因此,通过Edit / Project Settings / Player 进入Player设置,在其他设置部分切换颜色空间为线性。
我们需要一些简单的材质来测试我们的管道。我创建了四种材质。首先,默认的标准不透明材质,带有红色的反照率。第二,同样的材质,但是渲染模式设置为透明,蓝色反照率,alpha值降低。第三,材质使用Unlit/Color 着色器,颜色设置为黄色。最后一个材质使用了Unlit/Transparent 材质,没有任何变化,所以看起来是纯白色。
利用所有四种材料,创建一下物体放入场景里。
1.2 Pipeline Asset
目前,Unity使用默认的前向渲染管线。要使用自定义管线,我们必须在图形设置中选择一个,它可以通过Edit / Project Settings / Graphics.找到。
要设置自己的管道,我们必须将管道资产分配给可编写脚本的Render pipeline Settings字段。这些资源必须继承RenderPipelineAsset,这是一种脚本对象类型。
为我们的自定义管道资产创建一个新脚本。我们简单地将管道命名为My pipeline。它的资产类型将因此是MyPipelineAsset,它必须继承RenderPipelineAsset,它是在UnityEngine.Experimental命名空间中定义的。
代码语言:javascript复制using UnityEngine;
using UnityEngine.Experimental.Rendering;
public class MyPipelineAsset : RenderPipelineAsset {}
Jetbrains全家桶1年46,售后保障稳定
管道资产的主要目的是为Unity提供一种获取负责渲染的管道对象实例的方法。资产本身只是一个句柄和存储管道设置的地方。我们还没有任何设置,所以我们要做的就是给Unity一个获取管道对象实例的方法。这是通过重写InternalCreatePipeline方法实现的。但是我们还没有定义管道对象类型,所以在这里我们将返回null。
代码语言:javascript复制public class MyPipelineAsset : RenderPipelineAsset {
protected override IRenderPipeline InternalCreatePipeline () {
return null;
}
}
现在我们需要将这种类型的资产添加到我们的项目中。要做到这一点,可以向MyPipelineAsset添加一个CreateAssetMenu属性来实现。
代码语言:javascript复制[CreateAssetMenu]
public class MyPipelineAsset : RenderPipelineAsset {}
这将在Asset / Create菜单中放置一个条目。让我们整理下,把它放在一个渲染子菜单。为此,我们将属性的menuName属性设置为Rendering/My Pipeline。可以在属性类型之后直接在圆括号内设置属性。
代码语言:javascript复制[CreateAssetMenu(menuName = "Rendering/My Pipeline")]
public class MyPipelineAsset : RenderPipelineAsset {}
使用新菜单项将资产添加到项目中,并将其命名为My Pipeline。并把它赋值给 Scriptable Render Pipeline Settings。
我们现在已经替换了默认的管道,它改变了一些事情。首先,很多选项从图形设置中消失了,Unity在信息面板中也提到了这一点。其次,由于我们绕过了默认管道而没有提供有效的替换,所以不再呈现任何内容。游戏窗口、场景窗口和材质预览都不再具有功能,尽管场景窗口仍然显示天空盒。如果你打开FrameDebugger-通过 窗口/分析/帧调试器-并启用它,你会看到实际上什么也没有绘制在游戏窗口。
1.3 Pipeline 实例化
为了创建一个有效的管线,我们必须提供一个实现IRenderPipeline 接口的对象来负责渲染过程,,那么我们创建一个一个类,命名为MyPipeline
代码语言:javascript复制using UnityEngine;
using UnityEngine.Experimental.Rendering;
public class MyPipeline : IRenderPipeline {}
尽管我们可以自己实现IRenderPipeline,但是扩展抽象RenderPipeline类更为方便。该类型已经提供了我们可以建立的IRenderPipeline的基本实现。
代码语言:javascript复制public class MyPipeline : RenderPipeline {}
现在我们可以在InternalCreatePipeline返回一个新的MyPipeline的实例,这意味着我们有了一个有用的管线,虽然什么也没有渲染。
代码语言:javascript复制protected override IRenderPipeline InternalCreatePipeline ()
{
return new MyPipeline();
}
2.Rendering
MyPipeline对象负责渲染每一帧。Unity所做的就是用激活的摄像机和上下文调用管线的Render方法。这不仅仅在游戏窗口工作,而且在材质预览窗口和场景窗口也能工作。搞明白什么是需要被渲染的,并且按正确的顺序渲染所有东西,取决于合理的配置。
2.1 (Context)上下文
RenderPipeline包含一个在IRenderPipeline接口中定义的Render方法的实现。第一个参数是Render Context,它是一个ScriptableRenderContext结构体,作为原生代码的外观。(ps:不知道咋翻译,原文是actint as a facade for native code).第二个参数是一个包含所有需要被渲染的摄像机的数组。
RenderPipeline.Render 不渲染任何东西,只是检测管线对象在渲染时是否是有效的。如果无效,就会抛出一个异常。我们要重新这个方法并且调用基类的实现,来做这个检查。
代码语言:javascript复制public class MyPipeline : RenderPipeline
{
public override void Render (ScriptableRenderContext renderContext,Camera[] cameras)
{
base.Render(renderContext, cameras);
}
}
通过渲染上下文,我们向Unity引擎发出命令来渲染和控制渲染状态。最简单的例子是渲染天空盒,这个可以通过调用DrawSkyBox方法来完成。
代码语言:javascript复制base.Render(renderContext, cameras);
renderContext.DrawSkybox();
DrawSkyBox需要一个相机作为参数,我们先简单的使用数组中的第一个作为参数
代码语言:javascript复制renderContext.DrawSkybox(cameras[0]);
现在我们还是看不到天空盒在game视图,这是因为我们向上下文发布的命令还在缓存中,他真正的生效是在我们执行Submit方法之后。
代码语言:javascript复制renderContext.DrawSkybox(cameras[0]);
renderContext.Submit();
现在再game视图可以看到天空盒了,你也可以在frame debugger窗口看到了。
2.2 Cameras
我们提供了一组摄像机,因为在场景里面我们有多个摄像机需要被渲染。多摄像头设置的例子有分屏多人游戏、小地图和后视镜。每个相机都需要单独处理。
在这里,我们不需要关心管道的多摄像头支持。我们将简单地创建一个替代的渲染方法,作用于一个相机。让它绘制天空框,然后提交。
代码语言:javascript复制void Render (ScriptableRenderContext context, Camera camera) {
context.DrawSkybox(camera);
context.Submit();
}
我们通过循环调用这个方法来对每一个摄像头进行提交。这里我用foreach进行,unity的管线也是用这个方法来进行循环的。
代码语言:javascript复制public override void Render (
ScriptableRenderContext renderContext, Camera[] cameras
) {
base.Render(renderContext, cameras);
//renderContext.DrawSkybox(cameras[0]);
//renderContext.Submit();
foreach (var camera in cameras) {
Render(renderContext, camera);
}
}
注意当前摄像机的方向并不影响天空盒的渲染。我们把相机传给DrawSkybox,但是这只决定是否绘制天空盒,这是由摄像机的Clear flags控制。 为了正确的渲染天空盒和整个场景,我们必须要设置view-projection矩阵,这个变换矩阵将摄像机的位置和方向(视图矩阵)与摄像机的透视或正投影(投影矩阵)相结合。你可以在frame debugger中看到这个矩阵,他就是unity_MatrixVP.我们在绘制中会经常用到这个变量,是shader中的一个属性。
此时,unity_MatrixVP矩阵都是一样的,我们通过SetupCameraProperties这个方法来传递摄像机的属性给上下文,矩阵和其他的属性一起被设置。
代码语言:javascript复制void Render (ScriptableRenderContext context, Camera camera) {
context.SetupCameraProperties(camera);
context.DrawSkybox(camera);
context.Submit();
}
把相机的参数设置好之后,现在Game视图和Scene视图都可以正确的渲染天空盒了。
2.3 Command Buffers
上下文直到我们提交它才会真正的渲染,在这之前,我们需要配置它并且添加它后续所要执行的命令。像绘制天空盒这样的任务我们可以通过特有的方法来控制,但是其他的命令我们不能直接发布,只能通过单独的Command Buffer(命令缓冲区)。
一个Command Buffer可以通过实例化一个新的CommandBuffer对象来创建,它定义在UnityEngine.Rendering的命令空间里面。Command Buffers在scriptable rendering pipeline添加之前就已经存在,所以它不是实验性的,现在我们在绘制skybox之前创建一个commandbuffer对象。
代码语言:javascript复制using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;
public class MyPipeline : RenderPipeline {
…
void Render (ScriptableRenderContext context, Camera camera) {
context.SetupCameraProperties(camera);
var buffer = new CommandBuffer();
context.DrawSkybox(camera);
context.Submit();
}
}
我们通过ExecuteCommandBuffe方法让上下文执行这个buffer,再次说明下,这个命令不会立即执行,他只是把它copy到上下文的内部buffer中。
代码语言:javascript复制var buffer = new CommandBuffer();
context.ExecuteCommandBuffer(buffer);
命令缓冲区会在unity的原生层开辟空间来去存储命令。所以如果我们不再需要这些资源,我们最好马上释放它。我们可以在调用ExecuteCommandBuffer方法之后调用Release方法来释放它。
代码语言:javascript复制var buffer = new CommandBuffer();
context.ExecuteCommandBuffer(buffer);
buffer.Release();
执行一个空的command buffer什么都不会做,我们添加它是为了清空渲染对象,避免受到之前渲染结果的影响。这可以通过命令缓冲区实现,但不能直接通过上下文实现。
我们可以通过调用ClearRenderTarget方法添加一个一个清理命令。它需要三个参数,两个bool值和一个color,第一个表示深度信息是否清除,第二个表示color信息是否清除,第三个是清理的color值,如果使用。例如,让我们清除深度数据,忽略颜色数据,使用Color.clear 作为清除颜色。
代码语言:javascript复制var buffer = new CommandBuffer();
buffer.ClearRenderTarget(true, false, Color.clear);
context.ExecuteCommandBuffer(buffer);
buffer.Release();
Frame debugger 会向我们显示command buffer被执行了,他清除了渲染目标。在这个例子中,它表明了深度缓冲区和模板缓冲区都被清除了。
我们可以通过配置每一个相机的clear flags和background color来得到我们想要清除什么。我们可以用这个来代替硬编码的配置怎么清理每个渲染目标。
代码语言:javascript复制CameraClearFlags clearFlags = camera.clearFlags;
buffer.ClearRenderTarget(
(clearFlags & CameraClearFlags.Depth) != 0,
(clearFlags & CameraClearFlags.Color) != 0,
camera.backgroundColor
);
因为我们没有给命令缓冲区指定一个名称,调试器会显示默认名称,即未命名的命令缓冲区。让我们使用摄像机的名称,通过将其分配给缓冲区的名称属性。我们将使用对象初始化语法来实现这一点。
代码语言:javascript复制var buffer = new CommandBuffer {
name = camera.name
};
2.4 Culling
我们能渲染天空盒,但是现在我们还没有把任何东西放入场景里面。我们通过从场景中的所有renderer开始,然后剔除那些落在摄像机视图截屏之外的渲染器来做到只渲染摄像机看到的,而不是渲染所有的东西。决定什么可以剔除需要我们设置多个相机设置和矩阵,我们可以用ScriptableCullingParameters结构来做到设置。我们可以用CullResults.GetCullingParameters 这个静态方法来填充它,它传入一个摄像机参数然后使用 out 返回填充后的 ScriptableCullingParameters 给我们。
代码语言:javascript复制void Render (ScriptableRenderContext context, Camera camera) {
ScriptableCullingParameters cullingParameters;
CullResults.GetCullingParameters(camera, out cullingParameters);
…
}
GetCullingParameters除了返回out 参数之外,它还返回创建ScriptableCullingParameters是否成功,并不是所有的摄像机设置都是有效的,有一些设置如果用于culling可能会产生退化的结果。所以如果返回失败,我们就退出渲染。
代码语言:javascript复制if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
return;
}
如果我们拿到了culling paraments,我们就可以使用它来裁剪。我们可以通过调用静态方法CullResults.Cull 并且传入裁剪参数和上下文环境作为参数来调用它。它会返回一个包含可见信息 CullResult 结构体。
这里我们要把裁剪参数作为一个引用参数来使用。
代码语言:javascript复制if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
return;
}
CullResults cull = CullResults.Cull(ref cullingParameters, context);
2.5 Drawing
一旦我们知道什么是可见的,我们就可以继续渲染那些形状,我们通过调用在上下文上的Drawrenderers方法,传入cull.visibleRenderers 作为参数,告诉上下文哪些渲染器可以用来继续渲染那些可见的物体。除此之外,我们还必须提供绘制设置和筛选设置。这两个都是结构——drawrenderersettings和filterrendererssettings——我们将首先使用它们的默认值。绘制设置必须作为引用传递。
代码语言:javascript复制 buffer.Release();
var drawSettings = new DrawRendererSettings();
var filterSettings = new FilterRenderersSettings();
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
context.DrawSkybox(camera);
我们还没有看到任何对象,因为默认的过滤器设置什么也不包含,相反,我们可以通过为FilterRenderersSettings构造函数提供true作为参数来包含所有内容。
代码语言:javascript复制var filterSettings = new FilterRenderersSettings(true);
我们还要配置一个Draw setting ,通过再它的构造函数里面传入camera 和shader pass 作为参数。这个摄像机被用于设置排序和裁剪层级,而通道控制哪个着色器通道用于渲染。 shader pass通过一个字符串定义,注意passname不要写错,否则无法渲染对象,它被ShaderPassName结构包含。因为我们的管线只支持Unlit材质,所以我们用unity默认的pass,用SPRDefaultUnlit定义。
代码语言:javascript复制var drawSettings = new DrawRendererSettings(
camera, new ShaderPassName("SRPDefaultUnlit")
);
我们可以看到不透明的物体出现了,但是透明的物体没有显示。然而,frame debugger界面显示unlit物体已经绘制了。
其实透明物体已经绘制了,但是因为透明shader pass没有写入深度缓冲区,所以他们被天空盒覆盖掉了。解决方案是延迟绘制透明渲染器,在绘制skybox之后。
首先,在绘制天空盒之前只绘制不透明物体。这通过设置filtersetting的renderQuueRange来控制,我们把覆盖0到2500的RenderQueueRange.opaque赋值给它。
代码语言:javascript复制var filterSettings = new FilterRenderersSettings(true) {
renderQueueRange = RenderQueueRange.opaque
};
下一步,在渲染天空盒之后把渲染范围改成2501到5000,再次渲染。
代码语言:javascript复制var filterSettings = new FilterRenderersSettings(true) {
renderQueueRange = RenderQueueRange.opaque
};
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
context.DrawSkybox(camera);
filterSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
我们在渲染天空盒之前渲染不透明物体来避免overdraw.因为图形总是在天空盒之前,我们通过先绘制他避免重复工作。这是因为不透明shader通过写入深度缓冲区,用于跳过绘制在它之后的或者更远的东西。
除了覆盖天空的部分,不透明渲染器也会使彼此变得模糊, 理想情况下,对于帧缓冲区的每个片段,只绘制离摄像机最近的一个片段。所以为了尽量减少overdraw(重复绘制),我们应该先画出最近的形状。这可以通过在绘制之前对渲染器进行排序来实现,这是通过排序标志来控制的。
Draw setting 包含DrawRendererSortSettings类型的排序标志的结构体,其中包含排序标志。在绘制不透明形状之前将它设置为SortFlags.CommonOpaque.这表明unity通过距离来对渲染对象排序,从前到后,加上一些其他的规则。
代码语言:javascript复制var drawSettings = new DrawRendererSettings(
camera, new ShaderPassName("SRPDefaultUnlit")
);
drawSettings.sorting.flags = SortFlags.CommonOpaque;
但是,透明物体的渲染时不同的,它混合了正在渲染的和之前渲染的物体的颜色,才能产生透明的效果。这就需要反转绘制顺序,从后向前。我们用SortFlags.CommonTransparent来设置它。
代码语言:javascript复制context.DrawSkybox(camera);
drawSettings.sorting.flags = SortFlags.CommonTransparent;
filterSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
现在我们的管线可以正确的渲染unlit材质的透明物体和不透明物体了。
3 Polising
正确的渲染只是好玩的渲染管线的一部分。还又其他的事情要考虑,比如是否够快,以及是否能不分配额外的对象并且很好的集成到unity编辑器。
3.1 Memory Allocations(内存分配)
让我们检查一下我们的渲染管线在内存管理方面是否表现良好,如果它在每一帧分配内存,这将触发频繁的内存gc.我们可以通过Pfoiler面板里面的CP使用数据Hierarchy模式来观察它。你可以在编辑器下模式运行,也可以打包出development的版本来看性能。
在profiler面板排序GC Alloc选项你会发现在每一帧都分配了内存,有一些是我们无法控制的,但是有一些分配是在我们的Render方法里面的。
这表明了我们的裁剪分配了一部分内存。这是因为虽然CullResults是一个结构体,但是它包含了三个链表,他们是引用对象。每一次我们申请一个新的CullResult,都会在内存里分配新的链表。所以把CullResult作为一个结构体也没有多少好处。
幸运的是,CurllResult有一个可选的Cull方法,可以接受一个结构体作为引用参数,代替每次返回一个新的值。 这就意味着lists可以重用。我们必须在外部申请一个成员变量传入Cull方法,代替直接使用返回值。
代码语言:javascript复制CullResults cull;
…
void Render (ScriptableRenderContext context, Camera camera) {
…
//CullResults cull = CullResults.Cull(ref cullingParameters, context);
CullResults.Cull(ref cullingParameters, context, ref cull);
…
}
另外一个持续分配内存的地方是我们使用摄像机名字属性的地方。每一次我们获取名字,他都会冲原生层拉取一个string,每次都是一个新的对象。所以我们用命名我们的 command buffer 为 Render Camera 这样的一个常量来替代。
代码语言:javascript复制var buffer = new CommandBuffer() {
name = "Render Camera"
};
最后,command buffer自身也是一个对象。幸运的是我们可以只创建一个对象并且复用它。用一个Camerabuffer 字段来替代局部变量。感谢对象初始化语法,我们可以创建一个Command buffer作为它的默认值。唯一一个其他的改变是我们要用clear方法清理command buffer而不是释放它。
代码语言:javascript复制CommandBuffer cameraBuffer = new CommandBuffer {
name = "Render Camera"
};
…
void Render (ScriptableRenderContext context, Camera camera) {
…
//var buffer = new CommandBuffer() {
// name = "Render Camera"
//};
cameraBuffer.ClearRenderTarget(true, false, Color.clear);
context.ExecuteCommandBuffer(cameraBuffer);
//buffer.Release();
cameraBuffer.Clear();
…
}
修改这些之后,我们的管线就不会在每一帧都创建临时内存了。
3.2 Frame Debugger Sampling
我们可以做的另一件事是改善Frame debugger显示的数据.Unity的管线展示了事件嵌套层级,但是我们的都在底层。我们可以通过使用命令缓冲区来开始和结束分析器样本来构建层次结构。
让我们在ClearRenderTarget之前调用BeginSample,紧接着调用EndSample。每个示例都必须有一个开始和一个结束,并且必须提供完全相同的名称。除此之外,我发现最好使用与定义采样的命令缓冲区相同的名称。命令缓冲区的名称经常被使用。
代码语言:javascript复制cameraBuffer.BeginSample("Render Camera");
cameraBuffer.ClearRenderTarget(true, false, Color.clear);
cameraBuffer.EndSample("Render Camera");
context.ExecuteCommandBuffer(cameraBuffer);
cameraBuffer.Clear();
现在,我们看到一个RenderCamera级别嵌套在命令缓冲区的原始Render Camera中,而它包含clear操作。但是可以更进一步,将与摄像机相关的所有其他操作也嵌套在其中。这要求我们将 EndSample延迟到提交上下文之前.所以我们在这里插入额外的ExecuteCommandBuffer ,只包含结束样本的指令。为此使用相同的命令缓冲区,在完成之后再次清除它。
代码语言:javascript复制cameraBuffer.BeginSample("Render Camera");
cameraBuffer.ClearRenderTarget(true, false, Color.clear);
//cameraBuffer.EndSample("Render Camera");
context.ExecuteCommandBuffer(cameraBuffer);
cameraBuffer.Clear();
…
cameraBuffer.EndSample("Render Camera");
context.ExecuteCommandBuffer(cameraBuffer);
cameraBuffer.Clear();
context.Submit();
这看起来不错,除了清除动作嵌套在一个冗余的Render Camera层,其他的操作都是直接显示在根节点。我不确定它为什么发生,但是可以通过在clear之后开始采样来避免它。
代码语言:javascript复制//cameraBuffer.BeginSample("Render Camera");
cameraBuffer.ClearRenderTarget(true, false, Color.clear);
cameraBuffer.BeginSample("Render Camera");
context.ExecuteCommandBuffer(cameraBuffer);
cameraBuffer.Clear();
3.3 Rendering the Default Pipeline(渲染默认管线)
因为我们的管线支持unlit shader,那么其他材质的物体没有被渲染出来。如果他是正确的,那么就代表其他的对象使用了错误的shader。如果我们用Unity的错误着色器来可视化这些对象,那就太好了,那么它们应该呈现出明显不正确的洋红色。让我们为此添加一个专用的DrawDefaultPipeline方法,带有上下文和摄像机参数。我们将在绘制透明形状之后调用它。
代码语言:javascript复制void Render (ScriptableRenderContext context, Camera camera) {
…
drawSettings.sorting.flags = SortFlags.CommonTransparent;
filterSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
DrawDefaultPipeline(context, camera);
cameraBuffer.EndSample("Render Camera");
context.ExecuteCommandBuffer(cameraBuffer);
cameraBuffer.Clear();
context.Submit();
}
void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {}
Unity的默认表面着色器有一个前向渲染基础Pass,它被用作第一个前向渲染通道。我们可以把他作为一个能在默认管线下工作的一个标识。通过新的draw setting 和默认的filter setting 把它设置进去并且使用它渲染,我们不关心透明和不透明排序和分离,因为他们无论如何都不会生效。
代码语言:javascript复制void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
var drawSettings = new DrawRendererSettings(
camera, new ShaderPassName("ForwardBase")
);
var filterSettings = new FilterRenderersSettings(true);
context.DrawRenderers(
cull.visibleRenderers, ref drawSettings, filterSettings
);
}
使用默认着色器的对象现在显示出来。它们在Frame debugger面板中也是可见的。
因为我们的渲染管线不支持前项渲染,所以他们没有正确的渲染。必须的数据没有设置,所有依赖光的东西最后都是黑色的。相反,我们应该用一个错误着色器来渲染它们。为此,我们需要一个错误材料。为错误材质添加一个字段,然后在DrawDefaultPipeline的开始创建它,如果它不存在的话。这是通过Shader检索 Hidden/InternalErrorShader来完成的。找到,然后创建一个新的材质与着色器。同样,将材质的隐藏标记设置为HideFlags。因此它不会显示在项目窗口中,也不会和其他资产一起被保存。
代码语言:javascript复制Material errorMaterial;
…
void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
if (errorMaterial == null) {
Shader errorShader = Shader.Find("Hidden/InternalErrorShader");
errorMaterial = new Material(errorShader) {
hideFlags = HideFlags.HideAndDontSave
};
}
…
}
通过调用DrawRendererSettings的SetOverrideMaterial方法我们可以在渲染时覆盖一个材质。第一个参数是我们使用的材质,它的第二个参数是用于渲染的材质着色器的传递的索引。因为错误的shader只有一个pass,所以我们传0.
代码语言:javascript复制var drawSettings = new DrawRendererSettings(
camera, new ShaderPassName("ForwardBase")
);
drawSettings.SetOverrideMaterial(errorMaterial, 0);
不支持的材质现在也能清楚的显示了。但是这只适用于Unity的默认管道材质,它的材质有一个前向渲染pass。还有一些其他的内建shader,我们可以通过定义不同的通道来区分,特别是 PrepassBase, Always, Vertex, VertexLMRGBM, 和 VertexLM.
幸运的是,可以通过调用SetShaderPassName向绘制设置添加多个Pass。名称是这个方法的第二个参数。它的第一个参数是一个索引,用于控制绘制传递的顺序。我们不关心这个,所以任何顺序都可以.通过构造函数提供的传递始终具有索引0,其他的递增就可以。
代码语言:javascript复制var drawSettings = new DrawRendererSettings(
camera, new ShaderPassName("ForwardBase")
);
drawSettings.SetShaderPassName(1, new ShaderPassName("PrepassBase"));
drawSettings.SetShaderPassName(2, new ShaderPassName("Always"));
drawSettings.SetShaderPassName(3, new ShaderPassName("Vertex"));
drawSettings.SetShaderPassName(4, new ShaderPassName("VertexLMRGBM"));
drawSettings.SetShaderPassName(5, new ShaderPassName("VertexLM"));
drawSettings.SetOverrideMaterial(errorMaterial, 0);
这涵盖了所有由Unity提供的着色器到目前为止,这应该足以帮助指出在创建一个场景时使用了不正确的材质。但是我们只需要在开发过程中做这件事,而不是在发布版本里包含。所以,我们只需要编辑器中调用DrawDefaultPipeline。向方法中添加一个条件属性可以做到这一点。
3.4 Conditional Code Execution
Conditional 属性在System.Diagnostics定义。我们可以使用该名称空间,但不幸的是,它还包含一个与UnityEngine.Debug冲突的Debug类。我们使用别名来避免冲突,我们用一个特殊的类型赋值给他,这里我们定义Conditional作为System.Diagnostics.ConditionalAttribute的别名
代码语言:javascript复制using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;
using Conditional = System.Diagnostics.ConditionalAttribute;
将属性添加到我们的方法上。它需要一个指定符号的字符串参数。如果符号是在编译期间定义的,那么方法调用将被正常调用。如果符号没有被定义,这个方法将被忽略。就像DrawDefaultPipeline(context, camera),编译期间代码不存在。唯一可以调用的是在Unity Editor编译的时候,我们依赖 UNITY_EDITOR 这个符号。
代码语言:javascript复制[Conditional("UNITY_EDITOR")]
void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
…
}
我们可以更进一步,将调用包含在开发构建中,只是将它排除在发布构建之外。为此,使用DEVELOPMENT_BUILD符号添加一个附加条件。
代码语言:javascript复制[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
…
}
3.5 UI in Scene Window
到目前为止我们还没有考虑到的是Unity的游戏内UI。为了测试它,添加一个UI元素到场景中,例如一个按钮,通过GameObject / UI / button。创建一个带有按钮的画布,加上一个事件系统。
原来UI是在游戏窗口中渲染的,我们不需要做任何事情。Unity为我们解决了这个问题。Frame debugger显示UI是单独呈现的,作为一个图层。
至少,当画布被设置为在屏幕空间中呈现时是这样的,当设置为在world space中渲染时,UI和其他透明对象一起被渲染。
虽然UI在游戏窗口中工作,但它不会显示场景窗口,UI总是存在于场景窗口的世界空间中,但是我们必须手动将它注入到场景中。我们通过 ScriptableRenderContext.EmitWorldGeometryForSceneView这个方法来完成,用camera作为参数且必须在裁剪之前调用。
代码语言:javascript复制if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
return;
}
ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
CullResults.Cull(ref cullingParameters, context, ref cull);
但是这也会在game视图再添加一次。为了防止这种情况,我们必须只在渲染场景窗口时绘制UI。当camera的cameraType = cameraType.sceneview时可以做到这一点。
代码语言:javascript复制if (camera.cameraType == CameraType.SceneView) {
ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
}
这是有效的,但只在编辑器中有效。条件编译确保在编译构建时不存在EmitWorldGeometryForSceneView,这意味着我们现在在尝试编译时会得到一个编译器错误。要使它再次工作,我们必须使调用EmitWorldGeometryForSceneView的代码也有条件。这是通过将代码放在#if和#endif语句之间来实现的。与条件属性一样,#if语句需要一个符号。通过使用UNITY_EDITOR,只在编辑器编译时包含要编译的代码。
代码语言:javascript复制void Render (ScriptableRenderContext context, Camera camera) {
ScriptableCullingParameters cullingParameters;
if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
return;
}
#if UNITY_EDITOR
if (camera.cameraType == CameraType.SceneView) {
ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
}
#endif
CullResults.Cull(ref cullingParameters, context, ref cull);
…
}
最终结果
下一篇教程是 Unity Toturial (SPR) 2.自定义shader:
HipHopBoy:Unity Toturial (SPR) 2.自定义shaderzhuanlan.zhihu.com
作者的代码仓库:
Bitbucketbitbucket.org
觉得不方便下载的也可以去我的github仓库下载代码,里面是我的练习代码。地址是
zhudianyu/MyUnityToturialgithub.com
PDFcatlikecoding.com
我们下一篇文章再见。请不要吝啬你的鼓励,给我一个赞吧。谢谢!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/234666.html原文链接:https://javaforall.cn