Scriptable Render Pipeline
回顾一下渲染管线,渲染时需要决定画哪些物体,画在哪里,然后在什么时候画,物体之间的遮挡关系等等,渲染管线就是一条工作流水线,去安排这一切的操作。unity2018之前有两种内置的渲染管线:Deferred、Froward。但内置的管线的内容都是固定的,我会记录一下自己搭建一条完整的srp的过程,然后实现各种效果。
Pipeline Asset
首先要创建一个Asset,RenderPipelineAsset的主要目的是帮助Unity创建一个渲染管线实例,RenderPipelineAsset自身其实就是存储各渲染管线的配置。通过override RenderPipeline CreatePipeline()方法来创建一个渲染管线实例,因为现在还没定义自己的渲染管线,所以现在先返回Null。
1 2 3 4 5 6 7 8
| [CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")] public class CustomRenderPipelineAsset : RenderPipelineAsset { protected override RenderPipeline CreatePipeline() { return null; } }
|
然后创建我们的asset:
这个时候就会变得一切漆黑,因为此时引擎没有渲染出来任何物体,而且graphics里面的一些选项也不见了。
Render Pipeline Instance
创建pipeline实例,新建一个C#脚本,继承自RenderPipeline,在这里面需要去复写Render方法去创建一个具体的Pipeline,然后在CustomRenderPipelineAsset.createPipeline里去返回这个实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class CustomRenderPipeline : RenderPipeline { protected override void Render(ScriptableRenderContext context, Camera[] cameras) { throw new System.NotImplementedException(); } }
public class CustomRenderPipelineAsset : RenderPipelineAsset { protected override RenderPipeline CreatePipeline() { return new CustomRenderPipeline(); } }
|
这个时候已经有了我们创建的具体的pipeline了,但是它还没有效果,因为他还没有去渲染任何东西。
Rendering
Unity就是使用context和active状态的camera作为参数,调用pipeline中的render函数,进行渲染。这个过程对game 窗口,scene窗口和材质预览窗口都一样。我们现在要做的就是正确的设置好各参数,用正确的顺序绘制出应该被渲染的东西。
因为每个Camera是单独渲染的,再专门为摄像机新建一个类:
1 2 3 4 5 6 7 8 9 10 11 12
| public class CameraRenderer { ScriptableRenderContext context;
Camera _camera;
public void Render(ScriptableRenderContext context, Camera camera) { this.context = context; this._camera = camera; } }
|
然后在CustomRenderPipeline创建出一个pipeline实例时,用它去渲染所有的相机,所以在CustomRenderPipeline的实例里面:
1 2 3 4 5 6 7
| protected override void Render(ScriptableRenderContext context, Camera[] cameras) { foreach (Camera camera in cameras) { renderer.Render(context, camera); } }
|
这个loop在后面可以去实现不同的渲染方式,比如有的采用延迟渲染,有的渲染第一人称视角等等。
srp最重要的也就是这个RenderPipeline实例,它在已经定义好的Render函数里面调用每个摄像机进行渲染,人为控制管线的流程也就是在每个摄像机类里面去实现的。
Draw SkyBox
CameraRenderer.Render里面去画摄像机能看到的物体,我们在Render里面的每个command都是存在buffer里的,它们是排着队的,必须使用submit命令才能执行这个command。要正确的渲染Skybox和整个场景,还需要通过Camera的位置和视角设置MVP矩阵,要将Camera中的配置信息通过SetupCameraProperties函数写入Context中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public void Render(ScriptableRenderContext context, Camera camera) { this.context = context; this.camera = camera;
Setup(); DrawVisibleGeometry(); Submit(); }
void Setup() { context.SetupCameraProperties(camera); }
void DrawVisibleGeometry() { context.DrawSkybox(camera); }
void Submit() { context.Submit(); }
|
Command Buffers
Context 直到Submit才会进行实际的渲染,在这之前,我们需要配置它并且将comands加入其中等之后执行。一些任务比如绘制Skybox有专门的函数实现,但是画其它物体或者做其它操作的时候就没有这种内置函数了,所以要专门的一个buffer。
设置Command Buffer:
1 2 3 4 5
| const string bufferName = "Render Camera"; CommandBuffer buffer = new CommandBuffer { name = bufferName };
|
使用时,需要用到BeginSample,EndSample,这个范围内就属于Render Camera这个Command Buffer的范围。为了得到正确的渲染图像,还需要clearrendertarget:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void Setup() { buffer.ClearRenderTarget(true, true, Color.clear); buffer.BeginSample(bufferName); ExecuteBuffer(); context.SetupCameraProperties(camera); }
void Submit() { buffer.EndSample(bufferName); ExecuteBuffer(); context.Submit(); }
void ExecuteBuffer() { context.ExecuteCommandBuffer(buffer); buffer.Clear(); }
|
可以看到成功清除掉color和depth Buffer以及stencil
Culling
在渲染物体之前,需要先通过摄像机的视椎体对物体进行裁剪剔除,只渲染那些能被看见的物体。
通过ScriptableCullingParameters结构体和 CullResults.GetCullingParameters 函数,可以针对某个Camera得到裁剪的配置信息:
1 2 3 4 5 6 7 8 9 10
| bool Cull() { ScriptableCullingParameters p; if (camera.TryGetCullingParameters(out p)) { cullingResults = context.Cull(ref p); return true; } return false; }
|
因为是剔除,所以在draw之前进行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public void Render(ScriptableRenderContext context, Camera camera) { this.context = context; this.camera = camera;
if (!Cull()) { return; }
Setup(); DrawVisibleGeometry(); Submit(); }
|
Draw Geometry
通过culling得到可视化信息后,就可以用DrawRenderer去进行绘制,它需要三个参数,cullingresult以及drawingSettings、filteringSettings:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit"); void DrawVisibleGeometry() { var sortingSettings = new SortingSettings(camera) { criteria = SortingCriteria.CommonOpaque };
var drawingSettings = new DrawingSettings( unlitShaderTagId, sortingSettings ); var filteringSettings = new FilteringSettings(RenderQueueRange.all);
context.DrawRenderers( cullingResults, ref drawingSettings, ref filteringSettings );
context.DrawSkybox(camera); }
|
这样就可以画出所有Unlit Shader:
可以看到这里还有个问题,透明物体被天空覆盖了,所以要修改一下渲染的顺序。
先渲染不透明的物体,然后是天空盒,最后是透明物体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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);
|
Draw Gizmos
1 2 3 4 5 6 7 8
| partial void DrawGizmos() { if (Handles.ShouldRenderGizmos()) { context.DrawGizmos(camera, GizmoSubset.PreImageEffects); context.DrawGizmos(camera, GizmoSubset.PostImageEffects); } }
|
Draw Unity UI
添加一个UI物件时,它能成功在Game窗口被渲染出来,因为它是独立渲染的,不依靠我们的自定义管线,它的默认Render Mode是Screen Space - OverLay。UI在Scene里面是使用World Space Mode渲染的,所以它会显得很大。现在把它在Scene里面画出来:
1 2 3 4 5 6 7
| partial void PrepareForSceneWindow() { if (camera.cameraType == CameraType.SceneView) { ScriptableRenderContext.EmitWorldGeometryForSceneView(camera); } }
|
Multiple Cameras
多相机的渲染会比较麻烦,需要小心地设置渲染顺序,也就是对ClearFlag的设置。