找回密码
 立即注册
查看: 453|回复: 0

[简易教程] Unity SRP2019教程一:自定义渲染管线

[复制链接]
发表于 2020-12-23 12:57 | 显示全部楼层 |阅读模式
本系列主要来自于CatlikeCoding URP2019系列教程,本来想原封不动的做译,但其内容中有不少冗长和与本人实践结果有出入之处,故在原文技术上有增减,精华部分不变。读者可自行取舍。
原系列在此:
准备工作

本文基于Unity版本2019.3.4f1。
1.从unity新建3d project,由于unity2019仍然使用gamma色彩空间,我们需要Project Setting中设置其为Linear.
2.布置场景,这里的场景我们会用到不同的材质,using a mix of standard, unlit opaque and transparent materials. The Unlit/Transparent shader only works with a texture, so here is a UV sphere map for that, 在默认的渲染管线下, 现在的场景是这样的:
注意其中的绿色方块我用的是Unlit shader(绿色方块),而其他的方块我使用的有standard(红色方块)和standard transparent(蓝色球)和unlit transparent(白球)


3.新建我们的自己的RenderPipelineAsset
using UnityEngine;
using UnityEngine.Rendering;

public class CustomRenderPipelineAsset : RenderPipelineAsset {}
创建PipelineAsset的目的是提供一个Unity可以获得Pipeline实例的方法,所以我们需要有一个Create接口来返回一个我们自定义的管线实例。这里我们暂时没有定义出自己的RenderPipeline,所以先返回null.
protected override RenderPipeline CreatePipeline () {
        return null;
    }
4.我们需要向项目提供创建这一pipeline的方法,所以我们提供一个创建方法。注意无论unity2018还是2019都需要自定义创建PipelineAsset的方法。
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset { … }
创建完成之后我们将新创建的Pipeline赋值给当前的ProjectSetting,使用新创建的管线来进行渲染。正常的话,目前应该是一片空白,因为当前管线没有进行任何渲染操作。
5.创建pipeline实例来定义渲染操作。
using UnityEngine;
using UnityEngine.Rendering;

public class CustomRenderPipeline : RenderPipeline {}
RenderPipeline定义受保护的抽象Render方法,我们必须重写该方法才能创建具体的管道。它有两个参数:ScriptableRenderContext和一个Camera阵列。暂时保持该方法为空。该方法每一帧都会会调用,并将所有可用于渲染的相机数组传入,这样我们就可以使用这些相机参数来定义渲染操作。
protected override void Render(
    ScriptableRenderContext context, Camera[] cameras
)
{
}
返回到CumtomRenderPipelineAsset代码中,将刚才返回null的CreatePipeline()接口改写,返回我们新定义的Pipeline实例。现在CustomRenderPipelineAsset长这样:
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
    protected override RenderPipeline CreatePipeline()
    {
        return new CustomRenderPipeline();
    }
}
自定义相机渲染器

为方便每一个相机单独订制渲染方案,我们可以为每一个相机自定义一个相机渲染器类,并定义一个和CustomRenderPipeline中Render类似的接口,区别是这个接口的参数只接收一个相机。
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;
    }
}
在CustomRenderPipeline中迭代遍历执行咱们的CameraRenderer.Render,就可以让我们刚创建的CameraRenderer对所有的相机执行渲染。现在,我们的CustomRenderPipeline长这样:
using UnityEngine;
using UnityEngine.Rendering;

public class CustomRenderPipeline : RenderPipeline {
    CameraRenderer renderer = new CameraRenderer();

    protected override void Render (ScriptableRenderContext context, Camera[] cameras)
    {
        foreach (Camera camera in cameras) {
            renderer.Render(context, camera);
        }
    }
}
渲染天空盒

我们定义的相机渲染器的主要工作是绘制所有相机能看见的几何体。我们先让它具备绘制出天空盒的能力。我们添加一个DrawVisibleGeometry方法。
void DrawVisibleGeometry () {
        context.DrawSkybox(camera);
    }
我们发现现在依旧一篇漆黑。这是因为我们的指令目前只存在缓冲中,我们需要将它提交给设备。
public void Render (ScriptableRenderContext context, Camera camera) {
        this.context = context;
        this.camera = camera;

        DrawVisibleGeometry();
        Submit();
    }

    void Submit () {
        context.Submit();
    }
现在天空盒创建出来了,但是我们注意到它并没有铺满整个视口,有一部分仍然是黑的。
这是因为我们的设备上下文并没有拿到正确的相机投影矩阵,我们需要通过SetupCameraProperties 函数将相机相关的信息告知渲染设备。
做完这一步你会发现天空盒渲染正常了。
Command Buffers:命令缓冲区

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

    CommandBuffer buffer = new CommandBuffer {
        name = bufferName
    };
若要执行缓冲区指令,我们需要调用ExecuteCommandBuffer,这个接口从缓冲区中复制命令,如果我们想执行,我们需要手动的调用context.Submit();进行提交,一般在完成命令复制后,我们会清除命令缓冲区。这样,拷贝和提交就变成了下面两个接口:
// 提交
void Submit () {
        buffer.EndSample(bufferName);
        buffer.ClearRenderTarget(true, true, Color.clear);
        ExecuteBuffer();
        context.Submit();
    }

// 拷贝
void ExecuteBuffer () {
    context.ExecuteCommandBuffer(buffer);
    buffer.Clear();
}
备注: 我们可以使用buffer.BeginSample(bufferName);/buffer.EndSample(bufferName);来注入我们的分析代码,这样在FrameDebuger中我们就能看到我们想分析渲染代码。
清除渲染目标

如果你学习过OpenGL应该会记得在绘制每一帧之前需要清除帧缓冲,类似的我们的渲染器在开始渲染前需要清除渲染目标:
buffer.ClearRenderTarget(true, true, Color.clear);
Culling剔除

我们需要决定哪些物体需要被渲染。本例中我们用CullingResults存储执行完相机剔除后的结果,你可以理解为要渲染的物体列表。
CullingResults cullingResults;


bool Cull () {
    if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
        cullingResults = context.Cull(ref p);
        return true;
    }
    return false;
}
如果获取不到相机的剔除参数是没有办法进行渲染的,所以在相机渲染器的Render接口中如果没有获得执行相机剔除后的物体列表,我们是无法进行渲染的。
public void Render (ScriptableRenderContext context, Camera camera) {
    this.context = context;
    this.camera = camera;
    if (!Cull()) {
        return;
    }
    Setup();
    DrawVisibleGeometry();
    Submit();
}
绘制可见物体

现在我们有了需要绘制的物体列表,接下来绘制他们,修改DrawVisibleGeometry()
void DrawVisibleGeometry () {
    var drawingSettings = new DrawingSettings();
    var filteringSettings = new FilteringSettings();
    context.DrawRenderers(
        cullingResults, ref drawingSettings, ref filteringSettings
    );
    context.DrawSkybox(camera);
}
目前仍然什么都没有,并不是你的unity坏了,而是我们欠缺执行shader相关的参数,本例中我们只支持unlit shader。 我们定义一个静态变量来指定这个shader tag
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
注意这里的ShaderTagId我们指定的是SRPDefaultUnlit,这是SRP默认的shader tag,应该在cginclude相关位置被定义,这里暂不深究,有知道的朋友欢迎指出。
然后们修改drawingSettings,目的是让我们刚才指定的shadertag生效,另外可指定我们想渲染的队列,这里我们不做限制,让所有渲染队列都渲染(关于渲染队列是什么,请百度RenderQueue),完成这些设定后,DrawVisibleGeometry编程这样:
void DrawVisibleGeometry()
{
    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);
}
现在可以看到使用unlit shader材质的绿色方块被正确显示出来了。
但是我们搭建的场景中的unlit transparent对应的白色透明球体却没有显示出来,我们观察FrameDebugger,发现其实是有进行绘制的,为什么没渲染出来呢?
我们可以通过设置criteria属性的排序设置。让我们用SortingCriteria.CommonOpaque来调整渲染顺序.我们发现这次UnlitTransparent的渲染排序正确了,起码能绘制出一部分透明材质了。
我们来分析下刚才发生了什么,我们调整了sortingSettings让UnilitTransparent在不透明物体绘制完之后再进行绘制所以白色透明球能渲染出来了,但是为什么只有一部分呢?
应为Skybox在所有的物体渲染完之后才进行渲染,这样做的目的是绘制天空盒时可以利用深度缓冲避免绘制天空盒不需要绘制的像素,但是透明材质的物体是不会写入深度缓冲的,所以才导致此白色透明的球体与正方体相交之外的部分会被天空盒渲染的像素覆盖掉。
分别渲染不透明与透明对象

解决方案是首先绘制不透明物体,然后是Skybox,然后才是透明物体。 我们可以从DrawRenderers的filteringSettings通过切换到RenderQueueRange.opaque来先绘制非透明物体。
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
    );
    // 天空盒
    context.DrawSkybox(camera);
    // 透明物体
    filteringSettings = new FilteringSettings(RenderQueueRange.transparent);
    context.DrawRenderers(
        cullingResults, ref drawingSettings, ref filteringSettings
    );
}


现在的渲染顺序是:
笔者注

如果你不在乎Skybox的那点overdraw大可以像项目这样直接待天空盒渲染完成再渲染剩下的物体,效果是一样的。
void DrawVisibleGeometry()
{
    var sortingSettings = new SortingSettings(camera)
    {
        criteria = SortingCriteria.CommonOpaque
    };
    var drawingSettings = new DrawingSettings(
        unlitShaderTagId, sortingSettings
    );
    var filteringSettings = new FilteringSettings(RenderQueueRange.all);
    context.DrawSkybox(camera);
    context.DrawRenderers(
        cullingResults, ref drawingSettings, ref filteringSettings
    );
}
兼容非unlit shader

目前我们的管线只支持渲染unlit shader的pass,为了让使用其他shader pass的物体也显示出来我们需要做一些额外的工作。
如果有项目需要升级到我们的这套渲染管线,可能会有一些错误的shader存在与场景中,要统一所有unity的默认shader我们需要使用ShaderTagId来定义Always, ForwardBase, PrepassBase, Vertex, VertexLMRGBM, 和 VertexLM 这些pass。
static ShaderTagId[] legacyShaderTagIds = {
        new ShaderTagId("Always"),
        new ShaderTagId("ForwardBase"),
        new ShaderTagId("PrepassBase"),
        new ShaderTagId("Vertex"),
        new ShaderTagId("VertexLMRGBM"),
        new ShaderTagId("VertexLM")
    };
我们需要一个单独的方法来渲染这些使用非unlit shader的物体。
我们可以通过调用SetShaderPassName设置drawingSettings,使用绘图顺序索引和标记作为参数。对数组中的所有传递执行此操作。
void DrawUnsupportedShaders()
{
    var drawingSettings = new DrawingSettings(
        legacyShaderTagIds[0], new SortingSettings(camera)
    );
    for (int i = 1; i < legacyShaderTagIds.Length; i++)
    {
        drawingSettings.SetShaderPassName(i, legacyShaderTagIds);
    }
    var filteringSettings = FilteringSettings.defaultValue;
    context.DrawRenderers(
        cullingResults, ref drawingSettings, ref filteringSettings
    );
}
至此,我们场景中的物体全部渲染出来了。
笔者注

在原文中,原作者使用standard shader的物体会渲染不正确,但笔者的场景是正常的,笔者使用的是unity2019.3.4f1,可能是官方修正的缘故,有知情的老哥还望告知。 原作者的效果:
多相机的处理

两个相机

每个相机都有一个深度值,它是默认主摄像机的1。它们是按深度递增的顺序呈现的。
我们将MainCamera复制一份,这样场景中所有渲染将执行两次。
Clear Flags

我们可以通过调整第二个相机的Clear Flags,来将实现将第二个相机渲染的画面与第一个相机的渲染画面融合。 我们修改ClearRenderTarget的参数,让它根据相机的ClearFlag来进行调整。
void Setup()
{
    context.SetupCameraProperties(camra);
    // 修改
    CameraClearFlags flags = camera.clearFlags;
    buffer.ClearRenderTarget(
        flags <= CameraClearFlags.Depth,
        flags == CameraClearFlags.Color,
        flags == CameraClearFlags.Color ?
            camera.backgroundColor.linear : Color.clear
    );
    //
    buffer.BeginSample(bufferName);
    ExecuteBuffer();
}
注意当我们第二个相机的ClearFlag为Depth时,我们第二相机渲染的画面总会在第一相机渲染的后面,因为在第二相机进行渲染时深度缓存已经被清楚了,所以可以自由自在的绘制。而选择ClearFlag为Don'tClear时还会使用上个相机渲染后的深度缓存。
将第二相机的视口缩小,下面是两张对比,你能看出差别吗。
20209 4.19更新
今天重新打开工程发现standard材质确实变黑了,和原教程中的效果一样,我们将在下一篇中解决这个问题。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-6-3 04:31 , Processed in 0.092340 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表