图形学之路2:自定义渲染管线

Posted by 秦浩凯(Haokai Qin) on 2022-03-19
Estimated Reading Time 8 Minutes
Words 2k In Total
Viewed Times

尚未完成的页面。

占位用。

前言

目标

在本页进行的实验中,目的是:

  1. 控制物体是否被渲染

    • 尽管可以通过setActive()修改物体状态,使得物体不被显示;但我们的目的是从渲染角度,减少draw call开销。
    • 更具体一些,我们希望定制渲染流程,以控制物体的剔除/过滤/排序渲染顺序等,并分离透明/不透明物体或通道。
  2. 提供其他视野

    • 我们希望获得其他角度的视角观察物体。

因此,选择自定义渲染管线,以达到目的。
在一些教程中,这被称为SRP(Scripted Render Pipeline),或URP。

必要知识

什么是渲染管线?

渲染管线是把3D场景内的物体投影到2D屏幕上的过程。

什么是材质?

着色器的实例。给一个确定的几何体指定颜色。

什么是着色器?

为GPU指定场景内几何体(顶点)和像素颜色的代码。

参考资料

实验一
https://catlikecoding.com/unity/tutorials/custom-srp/custom-render-pipeline/
https://zhuanlan.zhihu.com/p/133144555
实验二
https://blog.csdn.net/qq_38913715/article/details/122308196

实验一:渲染指定材质的物体

准备工作:创建场景

在Unity中创建一个简单场景,放置数个具有不同材质的物体。

创建自定义渲染管线

1.新建一个脚本,我把它取名为myRenderPipeline , 令其继承RenderPipeline类。
这个类将用于控制场景内的camera,设置其渲染管线参数。

1
2
3
4
5
6
7
8
9
10
11

public class myRenderPipeline : RenderPipeline
{
//...
}

```


2.创建一个myRenderPipeline对象:定义`myRenderPipelineAsset`类,使得可以通过unity编辑器菜单栏创建渲染管线对象。

[CreateAssetMenu(menuName = "Rendering/my Render Pipeline")]
public class myRenderPipelineAsset : RenderPipelineAsset 
{
    protected override RenderPipeline CreatePipeline() 
    {
        return new myRenderPipeline();
    }
}
1
2
3

3.新建一个myCameraRender类。这个类用于实现camera的具体渲染控制方法。

using UnityEngine;
using UnityEngine.Rendering;

public class myCameraRenderer 
{

    ScriptableRenderContext context;

    Camera camera;

    public void Render(ScriptableRenderContext context, Camera camera) 
    {
        this.context = context;
        this.camera = camera;
    }
}
1
2
3
4
5



4.这时pipelinerender类的继承方法render尚未实现。根据这几个类型的关系,我们设置`myPipelineRender`类的render方法为:

myCameraRenderer mCR = new myCameraRenderer();

protected override void Render(ScriptableRenderContext context, Camera[] cameras) 
{
    foreach (Camera camera in cameras) 
    {
        mCR.Render(context, camera);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
5.在project settings->player中,将SRP设置为新建的渲染管线对象。

#### 小结:关于参数,及其意义
+ RenderPipeline.Render()
+ protect方法,具有override标记以保证重写。控制自定义渲染方法。
+ ScriptableRenderContext
+ https://docs.unity3d.com/cn/2019.4/ScriptReference/Rendering.ScriptableRenderContext.html
+ 简单地说,这个类定义(或者说,承载)自定义渲染管线使用的状态和绘制命令,并能控制GPU开始/结束渲染。




### 设置渲染流程
在上一步完成设置后,场景内的物体应该都不可见。这是由于:
1. 我们的渲染管线缺少待渲染物体列表;
2. 未发送渲染指令到GPU。

现在就是上一小节刚提到的SRC的用途了。
注意SRC控制GPU渲染的方式与openGL的操作非常接近,都需要缓冲区处理,以及绘制语句,才能实现渲染。
**接下来在myCameraRender类中添加语句**


1.定义`drawVisibleGeometry()`方法,这个方法用作控制绘制可见几何体的接口。

void DrawVisibleGeometry () 
{
    // 绘制天空盒
    context.DrawSkybox(camera);
}
1
2

2.定义`Submit()`方法,用作向GPU提交指令的接口。
void Submit () 
{
    context.Submit();
}
1
2

3.在`Render()`中设置语句,以实现基础的渲染流程。
public void Render(ScriptableRenderContext context, Camera camera)
{
    this.context = context;
    this.camera = camera;

    DrawVisibleGeometry();
    Submit();
}
1
2
3
4
5
这时天空盒已经可以看到了,但角度似乎不太正常。这是由于未从世界坐标系转换到相机视角坐标系所致。
![set](/Road-To-Graphics-2/unaligned_SKYBOX.PNG)

4. 定义一个`SetUp()`函数以进行渲染前的设置,并修改`render()`方法。

public void Render(ScriptableRenderContext context, Camera camera)
{
    this.context = context;
    this.camera = camera;

    Setup();
    DrawVisibleGeometry();
    Submit();
}

void Setup()
{
    context.SetupCameraProperties(camera);

    // 下一步用到
    buffer.ClearRenderTarget(true, true, Color.clear);
    buffer.BeginSample(bufferName);
    ExecuteBuffer();
}
1
2
3
4
5
6


5.命令缓冲区
> 除了像DrawSkybox这样的接口我们可以通过设备上下文直接调用外,其他命令我们都需要通过单独的命令缓冲区间接发出。为了获得一个缓冲区,我们需要一个CommandBuffer对象。我们暂时只需要一个缓冲区,所以我们将它缓存起来。

在`myCameraRenderPipeline`类内定义一个缓冲区
const string bufferName = "Render Camera";
CommandBuffer buffer = new CommandBuffer 
{
    name = bufferName
};
1
2
并修改submit()

void Submit () 
{
    // 从缓冲区读取,需要与BeginSample()配对
    buffer.EndSample(bufferName);

    // 清理深度缓冲区 (另见:渲染管线知识 一文)
    buffer.ClearRenderTarget(true, true, Color.clear);
    
    // 执行缓冲区命令,这个函数是自己定义的
    ExecuteBuffer();

    // 提交
    context.Submit();
}


void ExecuteBuffer () 
{
    // 执行命令,这个方法是unity提供的
    context.ExecuteCommandBuffer(buffer);

    // 清楚命令缓冲区
    buffer.Clear();
}
1
2
3
4

6. 相机剔除
这一步用于删除不在视野内的物体。

CullingResults cullingResults;

bool Cull () 
{
    if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) 
    {
        cullingResults = context.Cull(ref p);
        return true;
    }
    return false;
}
1
2
3

同时,render()函数也需要调整

public void Render (ScriptableRenderContext context, Camera camera) 
{
    this.context = context;
    this.camera = camera;
    if (!Cull()) 
    {
        return;
    }
    Setup();
    DrawVisibleGeometry();
    Submit();
}
1
2
3
4
5


### 绘制可见物体
这一步修改`drawVisibleGeometry()`

   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.opaque);
    context.DrawRenderers(
        cullingResults, ref drawingSettings, ref filteringSettings
    );


    // 选取透明物体渲染
    filteringSettings = new FilteringSettings(RenderQueueRange.transparent);
    context.DrawRenderers(
        cullingResults, ref drawingSettings, ref filteringSettings
    );

    // 最后绘制天空盒
    context.DrawSkybox(camera);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

![set](/Road-To-Graphics-2/final1-nodeal.PNG)

#### 小结:参数设置及其意义
+ SortingSettings
+ https://docs.unity3d.com/cn/2019.4/ScriptReference/Rendering.SortingSettings.html
+ 渲染对象排序
+ DrawingSettings
+ https://docs.unity3d.com/cn/2019.4/ScriptReference/Rendering.DrawingSettings.html
+ 为排序后的对象设置材质
+ FilteringSettings
+ https://docs.unity3d.com/cn/2019.4/ScriptReference/Rendering.FilteringSettings.html
+ 过滤器,保留渲染其材质渲染器在此范围内排队的对象。
+ DrawRenderers
+ https://docs.unity3d.com/cn/2019.4/ScriptReference/Rendering.ScriptableRenderContext.DrawRenderers.html
+ 注册绘制参数到执行命令表中



### 处理指定材质外的物体
现在,所有Unlit类型材质的物体都应被显示了,其余材质的物体尚未渲染。

我们定义


static Material errorMaterial;

static ShaderTagId[] legacyShaderTagIds = {
    new ShaderTagId("Always"),
    new ShaderTagId("ForwardBase"),
    new ShaderTagId("PrepassBase"),
    new ShaderTagId("Vertex"),
    new ShaderTagId("VertexLMRGBM"),
    new ShaderTagId("VertexLM")
};


void DrawUnsupportedShaders()
{
    if (errorMaterial == null)
    {
        errorMaterial =
            new Material(Shader.Find("Hidden/InternalErrorShader"));
    }
    var drawingSettings = new DrawingSettings(
        legacyShaderTagIds[0], new SortingSettings(camera)
    )
    {
        overrideMaterial = errorMaterial
    };

    //var drawingSettings = new DrawingSettings(
    //    legacyShaderTagIds[0], new SortingSettings(camera)
    //);
    for (int i = 1; i < legacyShaderTagIds.Length; i++)
    {
        drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
    }
    var filteringSettings = FilteringSettings.defaultValue;
    context.DrawRenderers(
        cullingResults, ref drawingSettings, ref filteringSettings
    );
}

处理其他类型的材质。  
`ShaderTagId[] legacyShaderTagIds`定义了可被此方法处理的材质列表。


至此,所有物体都应该显示了。

![set](/Road-To-Graphics-2/final1-noErrMaterial.PNG)
> 如果不设置errorMaterial,非支持材质物体将为黑色,这是由于未正确设置光照导致的。

![set](/Road-To-Graphics-2/final1.PNG)
> 设置errorMaterial后的效果


## 实验二:多相机视角 
到上一步为止,我们添加了自定义渲染管线`myRenderPipeline`,并对所有相机生效。这一节我们将设置多个相机视角。

1. 复制main camera对象,**修改其tag为`untagged`**
(这是为了在hololens 等ar设备中使用,否则多个主摄像机会导致AR设备混乱)。

2. 在project中新建`Render Texture`对象,并将新的相机对象的`render texture`选项设置为新建的RT。

![set](/Road-To-Graphics-2/set22.PNG)

3. 新建一个`Unlit/Texture`材质,设置纹理为刚才新建的RT;

![set](/Road-To-Graphics-2/set21.PNG)

4. 场景中新建一个`Image`物体,设置其`material`参数为刚才设置好的材质;

![set](/Road-To-Graphics-2/set23.PNG)

5. 调整相机和画布位置。现在可以看到多个摄像机画面了。

![set](/Road-To-Graphics-2/final2.PNG)

If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !