一句话描述,我们可以把SRP分解成两个部分,分别是SRP Asset,SRP Instance。
SRP Asset
SRP Asset是一个Unity Asset文件,用来存储渲染管线的特定配置信息,包含的信息有:游戏物体是否应该投射阴影;使用什么样的着色器质量级别;影子距离;默认材质配置。
此外还可以保存所有像在配置中保存和控制的信息和unity需要序列化的数据。SRP Asset定义了SRP的类型和所有配置数据的设置。
SRP Instance
SRP Instance是实际执行渲染的类,当unity发现工程使用SRP的时候,就会寻找当前使用的SRP Asset并请求一个渲染实例,该实例必须包含一个Render函数,
渲染实例表示管道配置。在渲染调用中,Unity可以执行如下操作:
- 清理framebuffer。
- 执行场景清理。
- 渲染游戏对象集。
- 从一个帧缓冲区到另一个帧缓冲区执行位块传输;
- 渲染阴影。
- 应用后处理效果。
第一个SRP Asset案例
SRP Asset实际是一个继承RenderPipelineAsset接口的类,需要实现CreatePipeline接口。当unity首次执行渲染命令的时候,则会调用这个接口并返回一个可用的渲染实例。
下面所展示的就是一个简单的SRP Asset类。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{protected override RenderPipeline CreatePipeline(){return new CustomRenderPipeline();}
}
SRP Instance
在上面的叙述中,CustomRenderPipelineAsset类重写了CreatePipeline方法,并返回了一个CustomRenderPipeline实例对象,该对象则是unity SRP渲染的入口。CustomRenderPipeline是一个继承RenderPipeline的类,而且必须实现虚函数Render,该函数包含了两个参数:
ScriptableRenderContext context:是一个command buffer对象,可以向其输入您想执行的渲染指令。
Camera[] cameras:一系列用于渲染的相机。
先介绍一个简单的例子:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;public class CustomRenderPipeline : RenderPipeline
{public CameraRender cameraRender = new CameraRender();public CustomRenderPipeline(){}protected override void Render(ScriptableRenderContext context, Camera[] cameras){foreach(var camera in cameras){cameraRender.Render(context, camera);}}
}
如上所示,我们把渲染的处理逻辑封装到CameraRender类里,对每一个场景中的活动相机进行单独处理。
下一步先定义CameraRender类:
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;}
}
然后正式定义Render函数,实现我们的自定义渲染管线。
- 绘制天空盒
在Render函数中是要绘制所有Camera可见的几何体,我们声明一个函数DrawVisibleGeometry用于处理一些特殊的工作,绘制天空盒也是在这个方法中处理,不过我们还另外声明了一个方法来完成这个工作。代码如下:
public void Render (ScriptableRenderContext context, Camera camera) {this.context = context;this.camera = camera;DrawVisibleGeometry();}void DrawVisibleGeometry () {context.DrawSkybox(camera);}
然而这样仍不能显示天空盒,这是因为我们调用DrawSkybox只是把命令添加到context的缓存队列里,还需要调用context.Submit()方法才会正式执行缓存队列里面的命令。
public void Render (ScriptableRenderContext context, Camera camera) {this.context = context;this.camera = camera;DrawVisibleGeometry();Submit();}void Submit () {context.Submit();}
最后我们可以看到下面的效果。
但是,这个渲染结果还是有问题,当我们调整相机的角度或位置时,渲染结果并没有发生变化,这是因为我们并没有将视角投影矩阵传递给Command Buffer。这个变换矩阵在Unity Shader中是unity_MatrixVP。当我们打开frame debugger面板,选择一个draw call,这个变量就会显示在右侧的监视板上。当前,不管我们怎么调整相机的姿态,这个变量都会保持不变,通过调用SetupCameraProperties方法可以构建这个矩阵。如下所示
但是,这个渲染结果还是有问题,当我们调整相机的角度或位置时,渲染结果并没有发生变化,这是因为我们并没有将视角投影矩阵传递给Command Buffer。这个变换矩阵在Unity Shader中是unity_MatrixVP。当我们打开frame debugger面板,选择一个draw call,这个变量就会显示在右侧的监视板上。当前,不管我们怎么调整相机的姿态,这个变量都会保持不变,通过调用SetupCameraProperties方法可以构建这个矩阵。如下所示
public void Render (ScriptableRenderContext context, Camera camera) {this.context = context;this.camera = camera;Setup();DrawVisibleGeometry();Submit();
}void Setup () {context.SetupCameraProperties(camera);
}
2. Command buffer
前面已经提到,context只有在调用Submit方法时才会执行渲染指令,在这之前都是将指令添加到渲染队列中。像天空盒可以通过专业的方法添加指令,但是像其他任务则不能通过这样添加,需要通过一个单独的command buffer间接添加。因此我们需要另建一个buffer用来绘制其他的几何对象。
const string bufferName = "Render Camera";CommandBuffer buffer = new CommandBuffer {name = bufferName
};
在项目开发中,profiler是一个非常强大和有用的工具,这里我们可以把我们的Command Buffer加入到profiler样例中,这个样例可以同时显示在profiler和frame debugger面板,方法是在适当的位置调用BeginSample和EndSample方法,在我们的案例中将会分别放在Setup()方法和Submit()方法的开头位置。BeginSample和EndSample方法必须使用相同的字符串参数作为样例的名字,这里使用的是buffer的名字。
void Setup () {buffer.BeginSample(bufferName);context.SetupCameraProperties(camera);
}void Submit () {buffer.EndSample(bufferName);context.Submit();
}
执行Command Buffer中的命令只需要调用context的ExecuteCommandBuffer方法,并把buffer作为参数传递进去。
void Setup () {buffer.BeginSample(bufferName);ExecuteBuffer();context.SetupCameraProperties(camera);
}void Submit () {buffer.EndSample(bufferName);ExecuteBuffer();context.Submit();
}void ExecuteBuffer () {context.ExecuteCommandBuffer(buffer);buffer.Clear();
}
这时候打开frame debugger我们就可以看到渲染的案例如下:
3. 清除渲染目标
不管我们想要绘制什么东西,最终都会渲染到相机的渲染目标中。相机的渲染目标一般是帧缓存区,也可以是渲染纹理。渲染目标一般不会自动清除,也就是说之前渲染的结果会一直保存在渲染目标中,这样会干扰我们渲染新的图像。为了保证正确的渲染,我们必须清除渲染目标,以摆脱其旧的内容。通过调用ClearRenderTarget方法可以实现这个目的。
ClearRenderTarget函数需要至少三个参数,前两个参数指定是否清理颜色缓存和深度缓存,一般都设为true,第三个参数指定清理缓存的颜色,这里使用Color.clear。
void Setup () {buffer.BeginSample(bufferName);buffer.ClearRenderTarget(true, true, Color.clear);ExecuteBuffer();context.SetupCameraProperties(camera);
}
打开frame debugger,可以看到清理渲染目标活动对应了一个叫DrawGL的样例。而且这个案例被镶嵌在另一个Render Camera样例中,这是因为 ClearRenderTarget将清除任务包装在了一个使用Command buffer的名字命名的样例中。为了避免这种情况,可以调整一下清除目标的代码的位置。
void Setup () {context.SetupCameraProperties(camera);buffer.ClearRenderTarget(true, true, Color.clear);buffer.BeginSample(bufferName);ExecuteBuffer();//context.SetupCameraProperties(camera);
}
现在我们可以看到Clear(color+Z+stencil),这表明颜色跟深度缓冲都没清除了,stencil数据是同一缓存区的一部分。