unity beautify 阴影_System


一句话描述,我们可以把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函数,实现我们的自定义渲染管线。

  1. 绘制天空盒

在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();
	}


最后我们可以看到下面的效果。


unity beautify 阴影_unity beautify 阴影_02


但是,这个渲染结果还是有问题,当我们调整相机的角度或位置时,渲染结果并没有发生变化,这是因为我们并没有将视角投影矩阵传递给Command Buffer。这个变换矩阵在Unity Shader中是unity_MatrixVP。当我们打开frame debugger面板,选择一个draw call,这个变量就会显示在右侧的监视板上。当前,不管我们怎么调整相机的姿态,这个变量都会保持不变,通过调用SetupCameraProperties方法可以构建这个矩阵。如下所示


unity beautify 阴影_unity beautify 阴影_03


但是,这个渲染结果还是有问题,当我们调整相机的角度或位置时,渲染结果并没有发生变化,这是因为我们并没有将视角投影矩阵传递给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我们就可以看到渲染的案例如下:


unity beautify 阴影_unity 天空盒_04


3. 清除渲染目标

不管我们想要绘制什么东西,最终都会渲染到相机的渲染目标中。相机的渲染目标一般是帧缓存区,也可以是渲染纹理。渲染目标一般不会自动清除,也就是说之前渲染的结果会一直保存在渲染目标中,这样会干扰我们渲染新的图像。为了保证正确的渲染,我们必须清除渲染目标,以摆脱其旧的内容。通过调用ClearRenderTarget方法可以实现这个目的。

ClearRenderTarget函数需要至少三个参数,前两个参数指定是否清理颜色缓存和深度缓存,一般都设为true,第三个参数指定清理缓存的颜色,这里使用Color.clear。


void Setup () {
	buffer.BeginSample(bufferName);
	buffer.ClearRenderTarget(true, true, Color.clear);
	ExecuteBuffer();
	context.SetupCameraProperties(camera);
}


unity beautify 阴影_unity天空盒素材_05


打开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);
}


unity beautify 阴影_unity天空盒素材_06


现在我们可以看到Clear(color+Z+stencil),这表明颜色跟深度缓冲都没清除了,stencil数据是同一缓存区的一部分。