最近做的东西要接触SRP了,所以这里跟着Catlike Coding做一做上面的教程,然后记录下来这个过程
创建多种材质和场景物体
这里场景不同的材质,包括Standard Shader的材质、Unlit的透明和不透明Shader,如下图所示,不多说:
创建Scriptable Render Pipeline Aseet
这是默认项目的Graphics设置:
我写了个脚本,可以创建自定义的RP Asset:
using UnityEngine;
using UnityEngine.Rendering;
// 需要继承于RenderPipelineAsset, 此类代表一个pipeline object instance
// Unity会使用它去做渲染, Asset本质只是一个Handle和一些Settings
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
// 获取Handle
protected override RenderPipeline CreatePipeline()
{
return null;
}
}
菜单栏创建文件后,指派上去,发现可选的设置少了很多,少了Tier Settings、一些Built-in Shader Settings和Camera Settings:
除了一些设置问题, we’ve disabled the default RP without providing a valid replacement, so nothing gets rendered anymore. The game window, scene window, and material previews are no longer functional. If you open the frame debugger—via Window / Analysis / Frame Debugger—and enable it, you will see that indeed nothing gets drawn in the game window.
创建Scriptable Render Pipeline的Runtime Instance
此时再创建一个脚本CustomRenderPipeline.cs
,感觉可以理解为Renderer,整体的代码如下:
// CustomRenderPipeline.cs脚本
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipeline : RenderPipeline
{
// Unity每帧都会调用这个Instance的Render函数, 会给Engine传递一个Render Context
// RP需要按照提供的Camera数组的顺序, 逐个渲染这些Camera
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
}
}
// CustomRenderPipelineAsset.cs脚本
using UnityEngine;
using UnityEngine.Rendering;
// 需要继承于RenderPipelineAsset, 此类代表一个pipeline object instance
// Unity会使用它去做渲染, Asset本质只是一个Handle和一些Settings
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
// 获取Handle
protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline();
}
}
创建CameraRenderer类
这里的每个Camera都会各自被渲染,所以创建一个CameraRenderer.cs
脚本:
using UnityEngine;
using UnityEngine.Rendering;
public class CameraRenderer {
ScriptableRenderContext context;
Camera camera;
public void Render (ScriptableRenderContext context, Camera camera) {
this.context = context;
this.camera = camera;
}
}
原本RP Instance的Renderer函数就可以改写为:
public class CustomRenderPipeline : RenderPipeline
{
// 这里用了统一的CameraRenderer来对相机进行渲染, 有点像URP的方式
// 更高级的可以用不同的CameraRenderer实现不同的渲染
CameraRenderer cameraRenderer = new CameraRenderer();
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
for (int i = 0; i < cameras.Length; i++)
{
cameraRenderer.Render(context, cameras[i]);
//Debug.Log("第" + i + "个:" + cameras[i] + " 总个数:" + cameras.Length);
}
}
}
此时仍然没有调用任何Draw Call如果打开Analysis的Frame Debugger窗口,会发现里面没有任何东西:
这里我还额外看了看RenderPipeline里的Render函数每帧都有哪些Camera要渲染,结果发现每帧就一个Camra,取决于自己的鼠标放在哪个Window上,比如放Scene上,就是传的Scene Camera,如果是Game View就是Main Camera,Inspector上就是Preview Camera,但每次都是只传入一个Camera,如下图所示,而且只有鼠标在窗口上移动的时候才会调用RenderPipeline的Render函数,确实挺有意思:
额外看了下Camera的种类,一共五种:
// Describes different types of camera.
[Flags]
public enum CameraType
{
// Used to indicate a regular in-game camera.
Game = 1,
// Used to indicate that a camera is used for rendering the Scene View in the Editor.
SceneView = 2,
// Used to indicate a camera that is used for rendering previews in the Editor.
Preview = 4,
// Used to indicate that a camera is used for rendering VR (in edit mode) in the Editor.
VR = 8,
// Used to indicate a camera that is used for rendering reflection probes.
Reflection = 16
}
创建CameraRenderer在Render函数里绘制Skybox
代码如下所示,很简单:
public class CameraRenderer
{
ScriptableRenderContext context;
Camera camera;
// 由RenderPipeline的Render函数每帧调用此函数
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
context.DrawSkybox(camera);
context.Submit();
}
}
此时就可以在Game和Scene View下看到天空盒子了,而且Frame Debugger也出现了函数调用,这个数字1应该是代表一帧调用一次的意思(或者是代表其child的个数),但此时的Scene View下的Camera不支持任何的Input操作,不可以移动或者干啥:
设置Camera的VP矩阵
此时的天空盒完全是静态的,这是因为Camera的VP矩阵还没有设置,设置的代码如下:
// 由RenderPipeline的Render函数每帧调用此函数
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
context.SetupCameraProperties(camera);
context.DrawSkybox(camera);
context.Submit();
}
此时就可以在Scene View和Main Camera的Transform里调整Camera的Zoom和Rotation了,不过天空盒永远是远平面的,调整滚轮不会有任何反应
创建CommandBuffer
这里创建了一个CommandBuffer,用于在Frame Debugger里插入profiler samples。然后把它在Frame Debugger里显示出来,感觉有点类似Unity的Profiler.BeginSample操作,代码如下,我感觉其实就是创建了一个函数指针的数组,然后把这些数组Copy到Context的Command Buffer里:
public class CameraRenderer
{
ScriptableRenderContext context;
Camera camera;
const string bufferName = "Render Camera";
CommandBuffer buffer = new CommandBuffer
{
name = bufferName
};
// 由RenderPipeline的Render函数每帧调用此函数
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
// 在每次渲染开始时, 先begin sample
buffer.BeginSample(bufferName);
// 此函数会把Buffer里的Commands复制到context里,应该不会立马执行
context.ExecuteCommandBuffer(buffer);
buffer.Clear();
// Update相机的属性
context.SetupCameraProperties(camera);
// 绘制
context.DrawSkybox(camera);
// 然后结束这次Sample工作
buffer.EndSample(bufferName);
context.ExecuteCommandBuffer(buffer);
buffer.Clear();
context.Submit();
}
}
此时在Frame Debugger里,就会多一层嵌套了:
Clearing the Render Target
绘制的东西,最终都会到Camra对应的Render Target上,本质上,Render Target默认是一块Frame Buffer,但它也可以是一个Render Texture(所以是一个OpenGL里的FBO咯?),每帧渲染时,要把之前的绘制内容情况(感觉类似于OpenGL的glClear函数),所以这里加一行代码,旨在在绘制之前清空old contents:
// 第一个true代表clear depth, 第二个代表clear color, 第三个代表Color清除后的颜色
buffer.ClearRenderTarget(true, true, Color.clear);// Color.clear其实是黑色
// 类比于这两个函数
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glClearColor(color.x, color.y, color.z, color.w);
// 后面的继续...
buffer.BeginSample(bufferName);
context.ExecuteCommandBuffer(buffer);
...
现在的Frame Debugger如下图所示,不过我还是很奇怪,Draw GL为什么会出现在Render Name这个Buffer的嵌套里面:
这里有个问题,就是调用了ClearBuffer的类似操作,为什么在Frame Debugger里展示的名字叫Draw GL?因为此时Camera用于清空Buffer的方式比较特殊,它调用了一个叫做Hidden/InternalClear
的Shader,写入到Render Target上,绘制了一个Full-Screnn Quad。这是因为,Unity检测到:在调用ClearRenderTarget函数后,我又调用了SetCameraProperties来改变Camera的VP矩阵,所以它做了这样的Trick,没有直接清空Buffer。(更深层次的原因Remain,估计是改变了VP矩阵又得Clear一次吧,可能跟架构有关)
但这样效率并不高,所以需要把ClearRenderTarget提前,如下图所示:
此时的Frame Debugger就正常了,注意这里的Z和stencil共享了一个Buffer:
借助Culling来绘制Camera里的对象
目前Camera里除了绘制Skybox,没有任何其他的Scene里的内容,这里做Culling,依据有两个:
- 只绘制挂载了Renderer组件的GameObject
- 只绘制相机Frustum里的东西,对于外部的东西进行Culling
执行Culling操作需要得到Camera的相关参数,代码如下:
using UnityEngine;
using UnityEngine.Rendering;
public class CameraRenderer
{
...// 一些Fields
CullingResults cullingResults;// Field用于记录Culling的结果
// 由RenderPipeline的Render函数每帧调用此函数
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
if (!Cull())
return;
// Update相机的属性
context.SetupCameraProperties(camera);
...
context.Submit();
}
bool Cull()
{
// 从Camera里获取Culling信息, 这里的parameters作用域是Cull整个函数
if (camera.TryGetCullingParameters(out ScriptableCullingParameters parameters))
{
// 在Context执行Culling操作, 并获得结果
cullingResults = context.Cull(ref parameters);
return true;
}
return false;
}// parameters作用域结束
}
绘制Geometry
通过上面的Contex.Culling操作,得到了Culling Result,现在就可以知道哪些Geometry是在相机的Frustum里了,相关代码如下:
// ShaderTagId是一个Struct, 本质就是一个String, 代表Shader的Tag
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
// 由RenderPipeline的Render函数每帧调用此函数
public void Render(ScriptableRenderContext context, Camera camera)
{
...
// 绘制
// 其实就是调用一堆封装好的API而言
// 根据Camera的设置, 判断是使用 orthographic还是distance based sorting.
var sortingSettings = new SortingSettings(camera);
// 指定可以使用哪种类型的Shader Passes, 这里只支持Unlit Shaders
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
// 指定是否指出RenderQueue, 这里表示要Render所有Queue里的东西
var filteringSettings = new FilteringSettings(RenderQueueRange.all);
// 调用context的DrawRenderers函数
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
context.DrawSkybox(camera);
...
}
此时,就可以在GameView和Scene View下看到东西,不过这里的透明物体是看不到的,除非选中它。Frame Debugger里使用的Camera是Game View下的Camera,如下图所示,从上往下一行行的换,可以看到一个个对象逐渐按顺序被画出来,很有意思,而且我移动Main Camera的话,这个Draw Loop列表会随着Frustum进行Culling:
但是这里有个问题,此时的绘制顺序可以说是完全随机的,好像是和GameObject在Hierarchy里的顺序有关,但总归是乱的,如下图所示:
这里可以在代码里规定渲染物体的顺序:
var sortingSettings = new SortingSettings(camera)
{
criteria = SortingCriteria.CommonOpaque
};
这里的SortingCriteria是个枚举,还挺多Sorting的方法的:
// How to sort objects during rendering.
[Flags]
public enum SortingCriteria
{
// Do not sort objects.
None = 0,
// Sort by renderer sorting layer.
SortingLayer = 1,
// Sort by material render queue. 根据材质的render queue排序
RenderQueue = 2,
// Sort objects back to front. 从离相机远的开始绘制
BackToFront = 4,
// Sort objects in rough front-to-back buckets.
QuantizedFrontToBack = 8,
// Sort objects to reduce draw state changes.
OptimizeStateChanges = 16,
// Typical sorting for transparencies.
CommonTransparent = 23,
// Sort renderers taking canvas order into account.
CanvasOrder = 32,
// Typical sorting for opaque objects. 只为非透明的物体排序, 从离相机近的开始绘制, 这样被挡住的就不会画了
// 这种排序其实也包含了the render queue and materials两种排序
CommonOpaque = 59,
// Sorts objects by renderer priority.
RendererPriority = 64
}
如下所示,是这些Enum的源代码:
[Flags]
public enum SortingCriteria
{
None = 0,
SortingLayer = (1 << 0), // by global sorting layer
RenderQueue = (1 << 1), // by material render queue
BackToFront = (1 << 2), // distance back to front, sorting group order, same distance sort priority, material index on renderer
QuantizedFrontToBack = (1 << 3), // front to back by quantized distance
OptimizeStateChanges = (1 << 4), // combination of: static batching, lightmaps, material sort key, geometry ID
CanvasOrder = (1 << 5), // same distance sort priority (used in Canvas)
RendererPriority = (1 << 6), // by renderer priority (if render queues are not equal)
CommonOpaque = SortingLayer | RenderQueue | QuantizedFrontToBack | OptimizeStateChanges | CanvasOrder,
CommonTransparent = SortingLayer | RenderQueue | BackToFront | OptimizeStateChanges,
}
然后此时的带颜色的非透明的Cube就可以按照从近到远的顺序进行绘制了,但是透明物体还是乱的,如下图所示:
额外提一下,这里的距离是根据相机坐标系的,物体的坐标在Z轴上的距离的,所以相机要摆正,比如我之前的相机x轴有角度,就导致渲染不太对,我还查了半天的问题,以为是精度问题。
单独绘制透明和不透明的物体
可以看一下目前的渲染状态,渲染到这里还是正常的,可以看到透明物体,在Loop里是先画不透明物体,再画半透明物体,这个逻辑是对的:
然后再往下,绘制skybox的时候,就发现半透明物体没了,准确的说,是在天空盒部分的半透明物体没了,如下图所示:
这是因为,半透明物体的绘制,不会写入任何Z-buffer(因为它不想阻挡后面东西,因为后面的东西还能看到),而除了不透明物体占据像素的这些位置,写入了Z值,其他的位置,都没有写过任何Z值,所以绘制Skybox的时候,它就直接写入了颜色,导致半透明物体填入的颜色被overwrite了。
所以这里的绘制顺序是不对,应该是先绘制不透明物体=> 再绘制skybox => 再绘制半透明物体,代码变成如下所示:
var sortingSettings = new SortingSettings(camera)
{
criteria = SortingCriteria.CommonOpaque
};
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
// 这里不再是all了, 而是opaque
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
// 1. 绘制opaque
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
// 2. 绘制skybox
context.DrawSkybox(camera);
// 改变排序规则
sortingSettings.criteria = SortingCriteria.CommonTransparent;
drawingSettings.sortingSettings = sortingSettings;
// 改变渲染filter设置
filteringSettings.renderQueueRange = RenderQueueRange.transparent;
// 3. 绘制transparent
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
此时效果就对了,果然渲染还是有意思啊:
这里有个奇怪的现象,就是半透明的物体,渲染顺序跟不透明物体不一样,不透明物体是从近到远的,而半透明物体是从远到近的,这里有两个原因:
- 半透明物体不会写入ZBuffer,所以从近到远绘制没意义,不会有任何效率上的提升
- 基于视觉上的考虑,一个离屏幕最近的半透明物体,应该颜色是其后面的半透明的物体的Blend结果,所以从后往前绘制逻辑是对的
不过还是可能会有问题,因为sorting is per-object and only based on the object’s position,就不深究了
到此为止,一个基本的RP就跑通了,不过目前还只能支持Unlit类型的Shader Pass.
Editor Rendering
现在有个文件,如果一个Material用的Shader不是Unlit Shader Passes,或者本身是个错误的Shader,那它也应该在场景中显示出来,而不是不显示。在Unity里,如果一个材质的shader丢了,那么应该是洋红色的。这其实也是普通Project升级到URP项目时,很多Shader的变化。
所以这里就进行相关的处理,代码如下:
// 由RenderPipeline的Render函数每帧调用此函数
public void Render(ScriptableRenderContext context, Camera camera)
{
...// 绘制前面正常的部分
// 绘制Shader不正常的本不应该可见的Geometry(megenta color)
// 创建新的绘制Settings, DrawingSettings里最少需要一个基本的Pass, 这里传入第一个
var drawingSettings2 = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
);
// 然后添加剩下的Shader Passes
for (int i = 1; i < legacyShaderTagIds.Length; i++)
{
// 添加Shader的名字, 只加一个名字有啥用啊?
drawingSettings2.SetShaderPassName(i, legacyShaderTagIds[i]);
}
// 这里用的默认的filtering, 也就是不做任何filter处理吧
var filteringSettings2 = FilteringSettings.defaultValue;
// cullingResults是整帧都不会变化的数据
context.DrawRenderers(
cullingResults, ref drawingSettings2, ref filteringSettings2
};
...// EndSample再Submmit
}
此时之前用的不正确的Shader就也可以显示,不过教程里说的这些物体都呈现黑色,但是我的颜色是正常的,如下图所示:
非Unlit的Shader全部用洋红色表示
虽然不知道为啥上面的显示是正确的,可能是新版本的Unity做了处理吧,但是我并不确定这个Shader是不是真的可用,这里还是做额外的处理,把不支持的Shader全部变为洋红色。代码如下:
static Material errorMaterial;
// 由RenderPipeline的Render函数每帧调用此函数
public void Render(ScriptableRenderContext context, Camera camera)
{
...// 绘制前面正常的部分
// 绘制Shader不正常的本不应该可见的Geometry(megenta color)
if (errorMaterial == null)
{
// 代码只跑一次, 用Unity默认提供的错误的洋红色Shader给它赋值
errorMaterial = new Material(Shader.Find("Hidden/InternalErrorShader"));
}
// 创建新的绘制Settings, DrawingSettings里最少需要一个基本的Pass, 这里传入第一个
var drawingSettings2 = new DrawingSettings(legacyShaderTagIds[0], new SortingSettings(camera))
{
overrideMaterial = errorMaterial
};
...//其他的不变
}
此时所有不支持的Shader,就都是洋红色了,如下图所示:
Partial Class
这一段代码应该只放在Editor下执行,Release版本不应该出现这些洋红色的东西,这里创建一个CameraRenderer.Editor.cs
文件,把相关Editor的代码放进去,在CameraRenderer.cs
里调用DrawUnsupportedShaders
即可:
public partial class CameraRenderer
{
// 使用partial method将函数的声明和定义分离
partial void DrawUnsupportedShaders();
#if UNITY_EDITOR
static Material errorMaterial;
// 代表所有Unity默认的shaders
static ShaderTagId[] legacyShaderTagIds = {
new ShaderTagId("Always"),
...
};
// 绘制Shader不正常的本不应该可见的Geometry(megenta color)
partial void DrawUnsupportedShaders()
{
...//
}
#endif
}
Drawing Gizmos
根据教程,目前是没gizmos的,如下图所示:
但我发现我是有Gizmos的,但是不完整,比如点选相机时,没有对应的Frustum,也看不到Light的Icon,如下图所示:
这里加一块代码就行了:
// CameraRenderer.Editor.cs文件里
public partial class CameraRenderer
{
partial void DrawGizmos();
partial void DrawUnsupportedShaders();
#if UNITY_EDITOR
...
partial void DrawGizmos()
{
if (Handles.ShouldRenderGizmos())
{
// 目前并不支持image effects, 所以两种Subset都要绘制
context.DrawGizmos(camera, GizmoSubset.PreImageEffects);
context.DrawGizmos(camera, GizmoSubset.PostImageEffects);
}
}
...
#endif
}
// CameraRenderer.cs文件里
// 由RenderPipeline的Render函数每帧调用此函数
public void Render(ScriptableRenderContext context, Camera camera)
{
...
// 画opaque
...
// 画skybox
...
// 画transparent
...
DrawUnsupportedShaders();
// 在绘制完所有场景里的物体之后再绘制Gizmos, 因为gizmos是无视Depth的
DrawGizmos();
...
}
Drawing Unity UI
此时在Hierarchy里,可以右键添加UI Button,此时会在Game View下出现Button,Scene View下会有个Canvas,但是看不到Button。
此时打开Frame Debugger,会发现出现了一个额外的UI节点,说明相关的UI不是用的我们的RP绘制的,这里调用了两个DrawMesh,第一个绘制了Button的白色底板,第二个DrawMesh绘制了Button对应的字:
至于为什么,创建的RP没有用于绘制Canvas上面的UI,这是因为Canvas组件的默认RenderMode(绘制模式)是Screen Space - Overlay,如下图所示:
此时只要把它改为Screen Space - Camera,然后拖进去Hierarchy里的Main Camera,就可以在Frame Debugger里看到,所有的内容都是交给我们的RP来绘制了,而且UI这一块是算在Transparent栏目里的,顺序是UI、再是由远到近的Transparent(因为opaque的UI会挡住所有的Transparent,所以opaque的UI会写入Z值?):
这里的Scene View下的UI仍然是看不到的,而且选中它的时候,会发现相关UI往往都很大,这是因为Scene View下的UI往往是以World Space的Render Mode进行绘制的,所以说屏幕分辨率的1920*1080尺寸,可能UI就有1920米。。。如下图所示:
想要在Scene下看到UI,需要在渲染Scene窗口的时候明确的把UI加入到World Geometry里,相关代码如下,是Editor-Only的:
// CameraRenderer.Editor.cs文件下
partial void PrepareForSceneWindow();
#if UNITY_EDITOR
partial void PrepareForSceneWindow() {
// 如果Camera绘制的是Scene View
if (camera.cameraType == CameraType.SceneView) {
ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
}
}
...
#endif
// CameraRenderer.cs文件下
// 加入到Culling之前, 也就是绘制任何物体之前, 因为Culling还能剔除不需要绘制的UI
PrepareForSceneWindow();
if (!Cull()) {
return;
}
现在就可以看到Scene里面的UI了,美滋滋:
多个Cameras
每个Camera都有一个Depth值,main camera的默认Depth值为-1,渲染时会按照相机的深度递增顺序,依次渲染不同深度的相机,这里复制一个Main Camera,深度值改为0,其Main Camera的Tag换成别的(因为Main Camera的Tag理论上应该只有一个Camera可以挂)
此时Frame Debugger里的可以看到,同样的渲染内容,出现了两次,第二次Camera渲染的时候,会把之前渲染的清空掉,所以画面跟之前没有任何变化:
这里的两个Camera都在一个Sample的Scope里,不太美观,改成两个好了,Scope的名字就用Camera的名字来代替:
// CameraRenderer.cs文件下
public void Render(ScriptableRenderContext context, Camera camera)
{
...
buffer.ClearRenderTarget(true, true, Color.clear);
// 在每次渲染开始时, 先begin sample
UpdateBufferName();
buffer.name = camera.name;
// 这段代码, buffer.name被用于Frame Debugger里的名字,
// 而传入的camera.name则是用于Profiler里的Sample的名字
buffer.BeginSample(camera.name);
...
buffer.EndSample(camera.name);
...
}
此时两个Debugger窗口就都可以看到相关内容了:
Layers
Camera可以通过Layer来实现,Camera只可以看到特定Layer的对象,核心在于改变相机的Culling Mask。
接下来的操作很简单,把使用Standard Shader的GameObject的Layer全部设置为Ignore Raycast,然后让Main Camera的Culling Mask看向所有除了Ignore Raycast的Layer,然后让Second Camera的Culling Mask只看Ignore Raycast,然后就是如下图所示了,因为Second Camera后渲染,所以只绘制了使用标准Shader的物体(不过Scene View好像没有变化):
Clear Flags
如下图所示,相机有个属性叫做Clear Flags,下面一共有四种选项:
这里的ClearFlags类似于OpenGL里的glClear里的选项,OpenGL里有GL_COLOR、GL_DEPTH和GL_STENCIL三个选项,我其实不太懂这些具体的代表什么,Clear Depth Only我还能理解,就是清除Z Buffer,由于Unity里的ZBuffer和Stencil Buffer共用一个Buffer,所以这里的清除Depth-only实际上是同时清除Z和模板缓冲。
对应到脚本里面,是Camera类里的一个枚举属性:CameraClearFlags:
// Values for Camera.clearFlags, determining what to clear when rendering a Camera.
public enum CameraClearFlags
{
// 这里没有Clear Stencil的选项, 原因上面提到了
// Clear with the skybox.
Skybox = 1,
// 这里的Color和SolidColor居然值是相同的
Color = 2,
// Clear with a background color. 不是很清楚具体指的什么
SolidColor = 2,
// Clear only the depth buffer.
Depth = 3,
// Don't clear anything.
Nothing = 4
}
注意,从上到下的枚举之间是包含关系,比如说Skybox的ClearFlag实际上包含了ClearColor和ClearDepth的操作。
为什么ClearFlags里会有Clear Skybox
感觉在OpenGL里并没有这个东西,只有Clear 颜色、模板和深度的操作,这里的Clear Skybox我不太理解,所以额外提一下。
这里的Clear Skybox其实可以理解为,Clear Everything,直到剩下一个Skybox,接下来的内容都会基于这个Skybox上去绘制。
它其实类似于OpenGL的:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );
只不过,它这里的Clear Color的操作好像有点特殊,因为它是重新画了个Skybox,而不是直接去Clear Color,不过也差不多,因为如果Camera里面没有指定Skybox,那么会用Camera.backgroundColor去Clear Color。
Solid Color和Color
Color应该只是Solid Color的别名,作用是一样的,其实就是Clear颜色+深度+模板。
The solid color clear flag means that when the camera renders a new frame, it clears everything from the old frame and it displays the new image on top of a solid color.
目前两个Camera绘制的东西完全相同,没有任何意义。现在想要实现这么个功能,就是让两个相机绘制不同的内容,Main Camera绘制主场景,而Second Camera把不支持的Shader的物体绘制出来,作为一张图片叠起来,类似于两个Frame Buffer渲染组合到一起,如下图所示:
那么现在需要,根据相机的ClearFlag属性,选择性的调用ClearRenderTarget函数,之前都是无脑写死的,绘制的东西完全相同:
// 原来的代码如下, 每个相机都Clear了Depth和Color Buffer
buffer.ClearRenderTarget(true, true, Color.clear);
// 函数签名是:
// public void ClearRenderTarget(bool clearDepth, bool clearColor, Color backgroundColor);
现在会根据Camera的Flag来判断:
// 获取flags
CameraClearFlags flags = camera.clearFlags;
buffer.ClearRenderTarget(
// 小于Depth的Flag都包含了清除Depth功能, 所以用<=, 不过这种写法感觉挺蠢的, 就不能用位运算么
flags <= CameraClearFlags.Depth,
// 这里不用<=是因为, skybox的绘制flag本身就算是ClearColor了, 所以不用再额外Clear
flags == CameraClearFlags.Color,
// 如果清除的是solid color, 那么需要使用camera的background color, 而且要用linearSpace下的背景颜色
// 因为前面在ProjSettings里把Color Space从Gamma改为了Linear
flags == CameraClearFlags.Color ? camera.backgroundColor.linear : Color.clear
);
接下来就可以去改变Second Camera的Inspector里的Clear Flag属性了,如果改为Skybox是这样的,第二次绘制的时候把第一次的内容清空了,然后画了个Skybox:
如果改成Solid Color也差不多,无非是背景颜色改为了Camera.backgroundColor在linear space下的颜色:
如果换成了Depth,那么会保留颜色,但是新绘制的都会基于原本的作为Canvas,可以看到红色全部覆盖了原本的颜色。
而如果改为Not Clear,那么原本的深度信息还在,新的红色可能会被绿色这些opaque物体遮挡,如下图所示:
最后,通过视口变换,调整Second Camera的Viewport Rect,就能得到最终的效果(这一课终于搞完了。。。)
最后提一下,每一帧渲染多个Camera,意味着每帧多次Culing、Setup和Sorting等操作,每个视角只使用一个camera其实是最高效的做法