最近做的东西要接触SRP了,所以这里跟着Catlike Coding做一做上面的教程,然后记录下来这个过程
参考:https://catlikecoding.com/unity/tutorials/custom-srp/custom-render-pipeline/
创建多种材质和场景物体
这里场景不同的材质,包括Standard Shader的材质、Unlit的透明和不透明Shader,如下图所示,不多说:
创建Scriptable Render Pipeline Aseet
这是默认项目的Graphics设置: 我写了个脚本,可以创建自定义的RP Asset:
using UnityEngine;
using UnityEngine.Rendering;
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
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,整体的代码如下:
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipeline : RenderPipeline
{
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
}
}
using UnityEngine;
using UnityEngine.Rendering;
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
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 cameraRenderer = new CameraRenderer();
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
for (int i = 0; i < cameras.Length; i++)
{
cameraRenderer.Render(context, cameras[i]);
}
}
}
此时仍然没有调用任何Draw Call如果打开Analysis的Frame Debugger窗口,会发现里面没有任何东西: 这里我还额外看了看RenderPipeline里的Render函数每帧都有哪些Camera要渲染,结果发现每帧就一个Camra,取决于自己的鼠标放在哪个Window上,比如放Scene上,就是传的Scene Camera,如果是Game View就是Main Camera,Inspector上就是Preview Camera,但每次都是只传入一个Camera,如下图所示,而且只有鼠标在窗口上移动的时候才会调用RenderPipeline的Render函数,确实挺有意思: 额外看了下Camera的种类,一共五种:
[Flags]
public enum CameraType
{
Game = 1,
SceneView = 2,
Preview = 4,
VR = 8,
Reflection = 16
}
创建CameraRenderer在Render函数里绘制Skybox
代码如下所示,很简单:
public class CameraRenderer
{
ScriptableRenderContext context;
Camera camera;
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矩阵还没有设置,设置的代码如下:
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
};
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
buffer.BeginSample(bufferName);
context.ExecuteCommandBuffer(buffer);
buffer.Clear();
context.SetupCameraProperties(camera);
context.DrawSkybox(camera);
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:
buffer.ClearRenderTarget(true, true, 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
{
...
CullingResults cullingResults;
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
if (!Cull())
return;
context.SetupCameraProperties(camera);
...
context.Submit();
}
bool Cull()
{
if (camera.TryGetCullingParameters(out ScriptableCullingParameters parameters))
{
cullingResults = context.Cull(ref parameters);
return true;
}
return false;
}
}
绘制Geometry
通过上面的Contex.Culling操作,得到了Culling Result,现在就可以知道哪些Geometry是在相机的Frustum里了,相关代码如下:
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
public void Render(ScriptableRenderContext context, Camera camera)
{
...
var sortingSettings = new SortingSettings(camera);
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
var filteringSettings = new FilteringSettings(RenderQueueRange.all);
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的方法的:
[Flags]
public enum SortingCriteria
{
None = 0,
SortingLayer = 1,
RenderQueue = 2,
BackToFront = 4,
QuantizedFrontToBack = 8,
OptimizeStateChanges = 16,
CommonTransparent = 23,
CanvasOrder = 32,
CommonOpaque = 59,
RendererPriority = 64
}
如下所示,是这些Enum的源代码:
[Flags]
public enum SortingCriteria
{
None = 0,
SortingLayer = (1 << 0),
RenderQueue = (1 << 1),
BackToFront = (1 << 2),
QuantizedFrontToBack = (1 << 3),
OptimizeStateChanges = (1 << 4),
CanvasOrder = (1 << 5),
RendererPriority = (1 << 6),
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);
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
context.DrawSkybox(camera);
sortingSettings.criteria = SortingCriteria.CommonTransparent;
drawingSettings.sortingSettings = sortingSettings;
filteringSettings.renderQueueRange = RenderQueueRange.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的变化。
所以这里就进行相关的处理,代码如下:
public void Render(ScriptableRenderContext context, Camera camera)
{
...
var drawingSettings2 = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
);
for (int i = 1; i < legacyShaderTagIds.Length; i++)
{
drawingSettings2.SetShaderPassName(i, legacyShaderTagIds[i]);
}
var filteringSettings2 = FilteringSettings.defaultValue;
context.DrawRenderers(
cullingResults, ref drawingSettings2, ref filteringSettings2
};
...
}
此时之前用的不正确的Shader就也可以显示,不过教程里说的这些物体都呈现黑色,但是我的颜色是正常的,如下图所示:
非Unlit的Shader全部用洋红色表示 虽然不知道为啥上面的显示是正确的,可能是新版本的Unity做了处理吧,但是我并不确定这个Shader是不是真的可用,这里还是做额外的处理,把不支持的Shader全部变为洋红色。代码如下:
static Material errorMaterial;
public void Render(ScriptableRenderContext context, Camera camera)
{
...
if (errorMaterial == null)
{
errorMaterial = new Material(Shader.Find("Hidden/InternalErrorShader"));
}
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 void DrawUnsupportedShaders();
#if UNITY_EDITOR
static Material errorMaterial;
static ShaderTagId[] legacyShaderTagIds = {
new ShaderTagId("Always"),
...
};
partial void DrawUnsupportedShaders()
{
...
}
#endif
}
Drawing Gizmos
根据教程,目前是没gizmos的,如下图所示: 但我发现我是有Gizmos的,但是不完整,比如点选相机时,没有对应的Frustum,也看不到Light的Icon,如下图所示: 这里加一块代码就行了:
public 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);
}
}
...
#endif
}
public void Render(ScriptableRenderContext context, Camera camera)
{
...
...
...
...
DrawUnsupportedShaders();
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的:
partial void PrepareForSceneWindow();
#if UNITY_EDITOR
partial void PrepareForSceneWindow() {
if (camera.cameraType == CameraType.SceneView) {
ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
}
}
...
#endif
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的名字来代替:
public void Render(ScriptableRenderContext context, Camera camera)
{
...
buffer.ClearRenderTarget(true, true, Color.clear);
UpdateBufferName();
buffer.name = camera.name;
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:
public enum CameraClearFlags
{
Skybox = 1,
Color = 2,
SolidColor = 2,
Depth = 3,
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函数,之前都是无脑写死的,绘制的东西完全相同:
buffer.ClearRenderTarget(true, true, Color.clear);
现在会根据Camera的Flag来判断:
CameraClearFlags flags = camera.clearFlags;
buffer.ClearRenderTarget(
flags <= CameraClearFlags.Depth,
flags == CameraClearFlags.Color,
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其实是最高效的做法
|