3.Editor Rendering
3.1Drawing Legacy Shaders
因为我们的管线只支持无光照的着色过程,使用其他不同的着色过程的对象是不能被渲染的,他们被标记为不可见。尽管这是正确的,但是它还是隐藏了场景中一些使用错误着色器的对象。所以让我们来渲染它们吧,但是要和无光照分开。
为了能够兼容所有unity默认的着色器我们必须使用着色器标记ID(ShaderTagId ),把Always,ForwardBase,PrepassBase,Vertex,VertexLMRGBM ,和VertexLM放到一个静态数组里。
static ShaderTagId[] legacyShaderTagIds = {
new ShaderTagId("Always"),
new ShaderTagId("ForwardBase"),
new ShaderTagId("PrepassBase"),
new ShaderTagId("Vertex"),
new ShaderTagId("VertexLMRGBM"),
new ShaderTagId("VertexLM")
};
在DrawVisibleGeometry 函数之后我们使用一个单独的方法绘制所有不支持的着色器。因为它们是错误的着色过程所以无论如何渲染结果都是错误的,所以我们不用关心其他设置。我们可以使用默认的过滤设置FilteringSetting.defaultValue属性。
我们可以多此调用DrawingSetting 的SetShaderName方法,使用序列和标记作为参数。因为在构造函数中已经加入了第一个Tag,所有我们对数组的操作从第二个开始。
void DrawUnsupportedShaders()
{
var drawingSettings = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
);
for (int i = 1; i < legacyShaderTagIds.Length; i++)
{
drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
}
var filteringSettings = FilteringSettings.defaultValue;
context.DrawRenderers(
cullingResults, ref drawingSettings, ref filteringSettings
);
}
3.2Error Material
为了明确的指明哪些对象使用了不支持的着色器我们用Unity错误的着色器来绘制它们。我们通过Shader.Find 函数,使用Hidden/InternalErrorShader 作为参数来构造一个全新的材质。我们将这个材质用静态字段缓存,这样我们就不用每帧都创建它了。然后将它赋值给DrawingSetting 的overrideMaterial属性。
static Material errorMaterial;
...
void DrawUnsupportedShaders () {
if (errorMaterial == null) {
errorMaterial =
new Material(Shader.Find("Hidden/InternalErrorShader"));
}
var drawingSettings = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
) {
overrideMaterial = errorMaterial
};
...
}
显示结果:
现在所有使用其他着色器的都变成粉色了
3.3Partial Class
绘制无效的对象对开发模式是有用的,但是对于正式版是没有意义的。所以我们把所有的只在编辑器模式有用的代码放在一个分开的局部类文件CameraRenderer 中。首先复制原始的CameraRenderer 文件,并将其命名为CamreaRenderer.Editor。
然后将原始的CameraRenderer转换为局部类,并从中删除ShaderTagId 数组,错误的材质,和DrawUnsupportedShaders方法。
什么是局部类?
这是一种拆分类-结构体的方法,把他们放入不同的部分,存储在不同的文件中。这样做的目的是为了更好的组织代码。典型的用例是将自动生成的代码与手动编写的代码分开。就编译器而言,它们都是同一类定义的一部分。他们在*Object Management, More Complex Levels*教程中有介绍。
清理另一个局部类文件只包含上个文件我们之前删除的那部分。
这个编辑器部分的内容只需要在编辑器中执行,所以我们用条件宏来标记。
但是,现在发布模式运行还是会失败,因为另一个局部类中包含DrawUnsupportedShaders函数的调用,它应该只能存在于编辑器模式。为了解决这个问题,我们需要让这个方法正常执行。为此我们需要在方法前面用partial进行声明,类似于抽象方法声明。我们可以在类定义的任何部分中执行此操作,所以让我们把它放在编辑器部分。完整的方法声明也必须标记为partial。
public partial class CameraRenderer : MonoBehaviour
{
partial void DrawUnsupportedShaders();
#if UNITY_EDITOR
static Material errorMaterial;
static ShaderTagId[] legacyShaderTagIds = {
new ShaderTagId("Always"),
new ShaderTagId("ForwardBase"),
new ShaderTagId("PrepassBase"),
new ShaderTagId("Vertex"),
new ShaderTagId("VertexLMRGBM"),
new ShaderTagId("VertexLM")
};
partial void DrawUnsupportedShaders()
{
if (errorMaterial == null)
{
errorMaterial =
new Material(Shader.Find("Hidden/InternalErrorShader"));
}
var drawingSettings = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
)
{
overrideMaterial = errorMaterial
};
for (int i = 1; i < legacyShaderTagIds.Length; i++)
{
drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
}
var filteringSettings = FilteringSettings.defaultValue;
context.DrawRenderers(
cullingResults, ref drawingSettings, ref filteringSettings
);
}
#endif
}
3.4Drawing Gizmos
现在我们的管线不能绘制辅助图标(Gizmos),无论是场景视图还是游戏视图激活的时候都不会显示。
我们可以检查辅助图标是否应该被绘制通过执行UnityEditor.Handles.ShouldRenderGizmos函数。如果需要显示,我们必须调用context的DrawGizmos方法,并传递camera作为第一个参数,传入第二个参数来确定辅助图标的哪些子集需要被绘制。这里有两个子集,在ImageEffects(屏幕后效)之前和之后。因为我们现在不支持ImageEffects因此两个方法我们都会调用。我们在一个只有Editor模型运行的方法DrawGizmos。
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
partial class CameraRenderer {
partial void DrawGizmos ();
partial void DrawUnsupportedShaders ();
#if UNITY_EDITOR
...
partial void DrawGizmos () {
if (Handles.ShouldRenderGizmos()) {
context.DrawGizmos(camera, GizmoSubset.PreImageEffects);
context.DrawGizmos(camera, GizmoSubset.PostImageEffects);
}
}
partial void DrawUnsupportedShaders () { ... }
#endif
}
辅助图标将被绘制在所有对象绘制之后。
3.5Drawing Unity UI
另一个需要我们注意的事情是Unity的用户界面系统。例如,我们通过GameObject/UI/Button 添加一个按钮来创建一个简单的UI。它将显示在game 窗口,但是不会显示在scene窗口。
帧调试器显示UI是通过新增的UGUI.Rendering.RenderOverlays绘制的,并不是通过我们的渲染管线绘制的
这是因为UI的默认绘制模式是 ScreenSpace-Overlay
当我们把UI的渲染模式修改成ScreenSpace-Camera 模式。使用主摄像机作为RenderCamera的参数,可以使UI的渲染成为透明几何体的一部分。
在渲染scene窗口的时候我们可以通过执行ScriptableRenderContext.EmitWorldGeometryForSceneView 函数和camera 参数来将UI添加到世界的几何体中。在一个只有编辑器模式允许的新函数PrepareForSceneWindow 中执行这些逻辑。当Camera 的cameraType 属性是CameraType.SceneView的时候我们渲染这个场景摄像机。
partial void PrepareForSceneWindow ();
#if UNITY_EDITOR
...
partial void PrepareForSceneWindow () {
if (camera.cameraType == CameraType.SceneView) {
ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
}
}
因为我们要把几何体添加到场景中,所有我们必须在Culling剔除之前执行这个函数。
执行完这一步后UI就会被绘制到场景视图中了。
4.Multiple Cameras
在场景中有可能同时存在多个已激活的摄像机,如果是这样我们必须保证他们同时工作。
4.1Two Cameras
每一个摄像机都有一个深度(Depth )值,默认的主摄像机的值为-1。摄像机按深度增加的顺序进行渲染。为了看到这些,复制主摄像机,改它改名为SecondaryCamera ,并把深度值设置为0。同时我们给它另一个tag值,因为MainCamera的tag只能被一个摄像机使用。
现在场景被渲染了两次。渲染结果的表现仍然是一样的,因为渲染过程中执行了清楚操作。FrameDebugger 向我们展示了这个过程,但是因为相邻的具有相同名字的采样被合并成我们最终得到的一个Render Camera列表。
如果每个摄像机都有自己的范围,就更清楚了。为了使之成为可能,添加一个只在编辑器模式运行的函数PrepareBuffer方法,使得缓冲区的名字和相机的名字一样。
partial void PrepareBuffer ();
#if UNITY_EDITOR
...
partial void PrepareBuffer () {
buffer.name = camera.name;
}
#endif
现在看就清晰很多了
4.2Dealing with Changing Buffer Names
尽管现在FrameDebugger能够分开显示每个相机的采样信息,但是当我们进入运行模式时Unity的控制台将充满警告并告诉我们BeginSample 和EndSample的计数必须要匹配(但是这里我没有报错不知道为啥)。因为我们对采用和他们的缓冲区使用了不同的名字,所以匹配混淆了。除此之外,我们每次访问camera的name属性也会造成内存的分配,所以我们不想在创建的时候这样做。
为了解决这两个问题我们添加一个SampleName 字符串属性。在编辑器模式我们应该在PrepareBuffer 函数中设置它和缓冲区的名字,运行模式它就是RenderCamera类的一个字符串常量值。
#if UNITY_EDITOR
...
string SampleName { get; set; }
...
partial void PrepareBuffer () {
buffer.name = SampleName = camera.name;
}
#else
const string SampleName = bufferName;
#endif
然后在开始采样和结束采样的时候都使用SampleName
我们可以看到不同通过查看 profiler-打开 Window/Analgsis/Profiler。 切换到Hierarchy模式然后按GC数据进行排序。可以看到有一个GC.Alloc,是96B。这部分就是在编辑器模式下产生的GC,在打包以后这部分GC就会消失。教程里后面摄像机数组的48bit不知道为啥没有出现。
4.3Layers
摄像机可以被配置成只看到某些层级的物体。这是通过调整摄像机的Culling Mask 实现的。为了看到这个效果我们把所有使用标准着色器的对象设置为IgnoreRaycast层级
然后从主摄像机的Culling Mask中排除这个层级。
将SecondaryCamera 的Culling Mask 设置为只有IgnoreRaycast
因为SecondaryCamera最后渲染,所以我们只能看到无效的对象。
4.4Clear Flags
我们可以通过设置CameraClearFlags来让第二个摄像机在第一个摄像机的渲染结果之上进行渲染(在之前,渲染第二个摄像机时我们会清除所有缓冲)。
void Setup () {
context.SetupCameraProperties(camera);
CameraClearFlags flags = camera.clearFlags;
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(SampleName);
ExecuteBuffer();
}
CameraClearFlags枚举定义了Skybox
,Color
,Depth
, andNothing
四个值,除了 Nothing,其他情况都需要清理深度缓冲区。观看源代码还有一个SolidColor
我们只需要在标记设置为Color时清除颜色缓冲区,因为在Skybox中,我们必然都会替换之前所有的颜色数据。
如果想要清除为纯色,我们就必须使用摄像机的背景色。但是因为我们在线性空间进行渲染,我们必须把颜色转换为线性空间,所以我们使用camera.backgroundColor.linear。在其他情况下,颜色并不重要,所以我们可以用Color.clear。
SecondaryCamera的清理标记定义如何组合两个摄影机的渲染。在skybox或color的情况下,先前的结果将被完全替换。如果仅清除深度,则辅助摄影机将正常渲染,但不会绘制天空盒,因此以前的结果显示为背景。当什么都没有清除时,深度缓冲区将保留,因此未照亮的对象最终将遮挡无效对象,就像它们是由同一台摄像机绘制的一样。但是,前一个摄像机绘制的透明对象没有深度信息,因此像 skybox之前所做的那样被绘制。
通过调整相机的 Viewport Rect ,也可以将渲染区域缩小到整个渲染目标的一小部分。其余渲染目标保持不受影响。在这种情况下,将使用Hidden / InternalClear着色器进行清除。模板缓冲区用于将渲染限制在视口区域。这里将第二个相机渲染到右半屏幕。
请注意,如果每帧渲染一台以上的摄像机就必须同时进行多次剔除,设置,分类等操作这是很耗的。一种有效的方式是让每一个摄像机都有自己的渲染视角。