Version: 2017.3
脚本运行时升级(Scripting Runtime Upgrade)
旧版主题

可编程渲染管线

注意:这是一项实验性功能。

Unity 商店下载的标准 Unity Editor 安装程序中未提供可编程渲染管线。请从 Unity Technologies GitHub 下载可编程渲染管线并单独安装。

本页包含以下内容:

功能摘要

通过重新构想渲染管线来支持更高的灵活性和透明度。主要的 Unity 渲染管线将由多个基于 C++ 以 C# 开发的“渲染循环”所取代。“渲染循环”的 C# 代码将在 GitHub 上开源,便于用户调查、强化或创建自己的自定义渲染循环。

本功能背后的动机

当前 Unity 的渲染管线在附录 - 当前渲染管线中进行了描述。我们想要做出一些改进,主要的改进内容如下。

需要在现代硬件上具有更好的性能

“每次绘制调用对应一个光源”前向渲染和“每个光源对应模板标记 + 绘制形状”延迟着色都不是完全现代的方法;它们大致适合于 DX9 硬件,但随着计算着色器的出现,我们通常可以提供高得多的性能。前向着色的问题在于过多的绘制调用(CPU + 顶点变换成本)以及重复采样表面纹理和混合所消耗的带宽;而延迟着色的问题在于绘制调用数量、光照剔除不足、每个光源进行模板标记+绘制调用的成本以及重复提取 G 缓冲区数据。此外,在基于区块的 GPU 上,当涉及实时阴影时,会进行太多区块存储和加载,并且不会利用区块存储或帧缓冲提取。

我们希望在 Unity 中提供一种针对现代硬件的开箱即用渲染管线,从而让我们可以依赖 API 和 GPU 功能,如计算着色器、绘制调用实例化、常量缓冲区等。

更容易自定义和扩展,更少“黑匣子”

大多数 Unity 用户可能不会修改内置渲染管线,但是一些更资深的团队确实希望修改或扩展它。所以它必须是可扩展的,并且比现在更不透明。

虽然当前渲染管线有一定程度的可扩展性(用户可以编写自己的着色器,手动控制摄像机渲染,更改设置,使用命令缓冲区扩展渲染管线),但它的可扩展性还不够。此外,它有太多“黑匣子”,虽然文档、会议演示、麻省理工学院 (MIT) 许可的内置着色器源代码和社区知识确实填补了空白,但如果没有 Unity 源代码许可证,一些部分很难理解。我们希望所有高级代码和着色器/计算代码都是 MIT 许可的开源项目,类似于后期处理UI网络的当前状态。

“通用型单一渲染管线”可能会有一些妥协,使其在牺牲性能的情况下提高灵活性。我们可以想象,在许多情况下,这些类型的渲染管线是有意义的:

  • 针对现代 PC/游戏主机(DX11 基线、“高端”图形)进行了优化。

  • 使用帧缓冲提取或其他可用技术针对移动端 GPU 的区块存储进行了优化。

  • 针对 VR 进行了优化(例如,前向着色 + MSAA、单通道渲染、缓存/共享眼睛渲染导致距离、各种视口/分辨率拼接方案)。

  • 针对低端设备(老旧移动设备、老旧 PC)或简单的 2D 游戏进行了优化:简单的单通道光照(有限的光源数量和/或顶点光照)。

这些渲染管线不必是物理上独立的渲染管线,可以是其他一些现有管线中的选项。

更容易处理向后兼容性

对于 Unity 研发部门来说,这是一个棘手的问题,从根本上改变渲染引擎的工作方式非常困难;主要是因为人们确实期望更新到更高的 Unity 版本,并且“一切仍照常工作”。除非他们不这样想,例如他们主动想要新的变化…例如,我们在 Unity 5.3 中将标准着色器从 Blinn-Phong 更改为 GGX 镜面反射;大多数情况下这是一件好事,但对于那些中期制作人来说却不是,现在他们的镜面反射的行为不同了(所以他们可能不得不重新调整他们的光照设置和材质)。

我们认为,如果渲染代码和所有着色器代码的高级结构都很容易“分叉”和进行版本控制,那么这个问题就会变得更简单。

可编程渲染循环:新的基础

我们认为上面列出的所有或大多数问题都可以相当从容地解决,只需建立一个可靠、正交、高性能的基础,这种方式基本上“能够通过各种过滤条件有效地渲染对象集”。分工如下:

Unity C++ 代码 C#/着色器代码(MIT 开源)
剔除
使用过滤器/排序/参数来渲染对象集
内部图形平台抽象
摄像机设置
光照设置
阴影设置
帧渲染通道结构和逻辑
着色器/计算代码

C ++ 端甚至不会意识到像“摄像机”或“光源”等对象的存在;例如,剔除代码获取边界图元和矩阵/剔除平面的数组作为输入。它不关心剔除的是主视图、反射渲染视图还是阴影贴图视图。

同样,渲染代码表述如下:“根据剔除结果,渲染不透明渲染队列范围内的所有内容,具有这一个色器通道而没有另一个着色器通道,先按材质再按距离排序,按对象设置光照探针常量”。这种情况下有一些约定和内置规则,主要是应该将什么样的数据设置为每个对象的每实例数据(光照探针、反射探针、光照贴图、每对象光源列表等)。

我们正在进行大量底层平台图形抽象更改,目的是能够提供一套强大、高性能和正交的“构建块”来构建可编程渲染循环,但它们大多数都超出了本文档的范围。已进行的一些更改包括:

  • 将“Buffer”公开为 C# 类,它将用于各种缓冲区数据(顶点、索引、uniform、计算数据等)。能够从 C# 端创建和手动更新 uniform/常量缓冲区。

  • 与计算着色器相关的改进,特别是向其传递数据的方式。

  • 取消 TextureFormat 和 RenderTextureFormat 之间的拆分,改用“DataFormat”之类的命令,可用于所有图形相关代码(类似于 D3D 上的 DXGI 格式)。公开比现在更多的格式。

  • GPU 数据异步回读。异步计算。


API 概述

注意:API 不断变化,本文档可能与您正在测试的 Unity 版本不完全相同。

主入口点是 RenderLoop.renderLoopDelegate,它采用以下格式: bool PrepareRenderLoop(Camera[] cameras, RenderLoop outputLoop);

注册渲染循环委托后,所有渲染都进入该函数,并且根本不执行现有的内置渲染循环。

在渲染循环委托内部,通常会对所有摄像机进行剔除(通过新的 CullResults 类),然后对 RenderLoop.DrawRenderers 进行一系列调用并混合 CommandBuffer 调用以设置全局着色器属性、更改渲染目标、分发计算着色器等。

总的来说,设计思路是 C# 渲染循环代码完全控制每个摄像机的逻辑(它将所有摄像机作为输入),以及所有每个光源的逻辑(它将获取所有可见光源作为剔除结果),但通常不会执行每个对象的逻辑。对象以“集合”形式渲染;通过 DrawRenderers 调用来指定要渲染的可见对象的子集、如何对它们进行排序以及要设置的每对象数据的类型。

可能最简单的渲染循环如下所示:

public bool __Render__(Camera[] cameras, RenderLoop renderLoop)
{
    foreach (var camera in cameras)
    {
        // 剔除摄像机
        CullResults cull;
        CullingParameters cullingParams;
        if (!CullResults.GetCullingParameters (camera, out cullingParams))
            continue;
        cull = __CullResults.Cull__ (ref cullingParams, renderLoop);
        renderLoop.SetupCameraProperties (camera);
        // 设置渲染目标并将其清除
        var cmd = new CommandBuffer();
        cmd.SetRenderTarget(BuiltinRenderTextureType.CameraTarget);
        cmd.ClearRenderTarget(true, true, Color.black);
        renderLoop.__ExecuteCommandBuffer__(cmd);
        cmd.Dispose();
        // 使用 ForwardBase 着色器通道绘制所有不透明对象
        var settings = new __DrawRendererSettings__(cull, camera, "ForwardBase");
        settings.sorting.sortOptions = SortOptions.SortByMaterialThenMesh;
        settings.inputFilter.SetQueuesOpaque();
        renderLoop.__DrawRenderers__(ref settings);
        renderLoop.Submit ();
    }
    return true;
}

最重要的新脚本 API:

// 主入口点
struct RenderLoop
{
    void ExecuteCommandBuffer (CommandBuffer);
    void DrawRenderers (ref DrawRendererSettings);
    void DrawShadows (ref DrawShadowsSettings); // 类似,专用性稍高
    void DrawSkybox (Camera);
    static PrepareRenderLoop renderLoopDelegate;
}
// 设置和控制如何使用 RenderLoop.DrawRenderers 来渲染对象集
struct DrawRendererSettings
{
    DrawRendererSortSettings sorting;
    ShaderPassName shaderPassName;
    InputFilter inputFilter;
    RendererConfiguration rendererConfiguration;
    CullResults cullResults { set };
}
struct DrawRendererSortSettings
{
Matrix4x4 worldToCameraMatrix;
Vector3 cameraPosition;
SortOptions sortOptions;
    bool sortOrthographic;
}
enum SortOptions { None, FrontToBack, BackToFront, SortByMaterialThenMesh, ... };
struct InputFilter
{
    int renderQueueMin, renderQueueMax;
    int layerMask;
};
// 渲染每个对象时应设置的数据类型
[Flags] enum RendererConfiguration
{
    None,
    PerObjectLightProbe,
    PerObjectReflectionProbes,
    PerObjectLightProbeProxyVolume,
    PerObjectLightmaps,
    ProvideLightIndices,
    // ...
};
//剔除和剔除结果
struct CullResults
{
    VisibleLight[] visibleLights;
    VisibleReflectionProbe[] visibleReflectionProbes;
    bool GetCullingParameters(Camera, out CulingParameters);
    static CullResults Cull(ref CullingParameters, RenderLoop renderLoop);
    // 实用函数,比如
// ComputeDirectionalShadowMatricesAndCullingPrimitives 等
}
struct CullingParameters
{
    int isOrthographic;
    LODParameters lodParameters;
    Plane cullingPlanes[10];
    int cullingPlaneCount;
    int cullingMask;
    float layerCullDistances[32];
    Matrix4x4 cullingMatrix;
    Vector3 position;
    float shadowDistance;
ReflectionProbeSortOptions reflectionProbeSortOptions;
Camera camera;
}
struct VisibleLight
{
    LightType lightType;
    Color finalColor;
    Rect screenRect;
    Matrix4x4 localToWorld;
    Matrix4x4 worldToLocal;
    float range;
    float invCosHalfSpotAngle;
    VisibleLightFlags flags;
    Light light { get }
}

struct VisibleReflectionProbe; // similar to VisibleLight…

上面列出的 API 不是最终的!很可能需要更改之处包括:

  • 考虑不使用 RenderLoop 类,而是让 CommandBuffer 包含像 DrawRenderers 等函数,可能还有嵌套的命令缓冲区。

  • 对剔除 API 进行更改以实现更高的性能,即任务化的剔除与其他工作重叠。

  • 可能更多的渲染器过滤选项。

  • 更明确的“渲染通道”控制,取代当前的“设置渲染目标”API。

用法、内部工作原理和性能

一般流程是您自己的渲染循环代码负责剔除和渲染所有内容。包括设置每帧或每渲染通道的着色器 uniform 变量,管理临时渲染目标并进行设置,分发计算着色器等。

可从剔除结果中查询可见光源和探针,例如将它们的信息放入计算着色器缓冲区以进行平铺光照剔除。或者,渲染循环提供了几种为 DX9 风格的前向渲染设置每对象光源列表的方法。

在 CPU 性能方面,根据该 API 的构建方式,通常不会进行每对象的操作;代码的 C# 端与场景复杂性无关。它通常在摄像机上循环,并在可见光源上进行某种迭代,从而渲染阴影或者打包光源数据以供着色器使用。用 C# 编写的其余代码用于设置渲染过程/渲染纹理,并发出“绘制这个可见对象子集”命令。

代码的 C++ 部分(culling、DrawRenderers 和 DrawShadows)是以高性能风格编写的,通常只是检查紧密打包的数据数组,并采用内部多线程。我们当前的实验表明,通过这种拆分(C# 的高级帧设置,C++ 中的剔除/渲染),我们可以获得与之前的渲染循环实现方式相同甚至更好的性能。

C# 端看起来会产生大量垃圾回收的对象;我们正在研究如何将“本机”(C++ 端)数据直接公开给 C#,而无需额外的往返;在 C# 中,看起来非常类似于直接写入本机端内存的数组。这是一个有点不同的主题,我们将单独探讨。


新的内置“HD 渲染循环”

我们计划提供针对现代(有计算能力的)平台的内置“HD 渲染循环”。目前在开发过程中考虑了 PC 和 PS4/XB1 游戏主机,但我们也将考虑针对高端移动平台进行优化。对于移动平台,特别感兴趣的是如何在区块存储/帧缓冲提取和其他节省带宽的技术方面进行优化。

在内部,根据着色器的编写原则,对每个可以想象的旋钮较少依赖于单独的着色器变体,而更多使用“静态”(基于 uniform)分支,并且仅当根据现代 GPU 的着色器分析/性能分析结果认为合理的情况下,才使用专门的着色器变体。

目前正在 github ScriptableRenderLoop 下开发新的 HDRenderLoop(在任何方面都可能有点混乱,除非您现在特别好奇,否则不建议使用)。

光照功能

  • 使用计算着色器进行平铺光照剔除:

    • 对延迟着色不透明对象使用精细修剪平铺光照 (FPTL)。

    • 对前向渲染对象和透明度使用集群平铺光照。

    • 渲染可在延迟和前向模式之间切换,具体取决于为项目带来的优势。

  • 光源:

    • 常规准时光源(点光源/聚光灯)和方向光。

    • 面光源(多边形光源和线光源)。

    • 正确的线性光照和 PBR。

    • 物理光源单元、IES 光源。

    • (稍后)视椎体光源(即有界方向光)。

  • 阴影:

    • 所有实时阴影都是从单个图集细分出来的。

    • 直观控制阴影内存预算和进行每个光源的分辨率覆盖。

    • 更好的 PCF 过滤,尤其适用于聚光灯/点光源。

    • 半透明物体上的阴影。

  • GI:

    • 正确的 HDR。

    • 与直接光照的一致性。

  • (稍后)改善的阴影

    • 指数阴影贴图 (ESM/EVSM)。

    • 面光源改善的阴影。

  • (稍后)体积光照

    • 天空/雾大气散射模型。

    • 局部雾效。

材质功能

  • 支持金属和镜面反射参数化的 GGX,类似于当前的标准着色器。

  • 各向异性 GGX(金属参数化)

  • 次表面散射和透射

  • 透明涂层

  • 双面支持

  • 良好的镜面反射遮挡

  • 分层材质(其他材质的混合和遮罩输入,最多 4 层)

  • 通过视差或位移曲面细分实现的高度贴图

  • (稍后)内置 LOD 交叉淡入淡出/抖动

  • (稍后)头发、眼睛、布料着色模型

摄像机功能

  • 基于物理的摄像机参数

  • 支持 Unity 的后期处理栈

  • 失真

  • 速度缓冲区(用于运动模糊/时间 AA)

  • (稍后)二分之一/四分之一分辨率渲染(例如,对于粒子)和合成。

工作流程/调试功能

  • 着色器输入视图(反照率、法线等)

  • 渲染的所有中间缓冲区的视图(光照、运动矢量等)

  • 通过调试菜单控制各种通道的渲染


附录 - Unity 中的当前渲染管线

目前(Unity 5.5 及更早版本),Unity 支持两个用于场景的渲染管线(前向渲染和延迟着色),以及一种渲染实时阴影的方法。以下进一步详细介绍了当前管线:

阴影

无论使用前向渲染还是延迟着色,着色系统的工作方式都大致相同。

  • 每个启用了阴影的实时光源都会获得单独的阴影贴图。

  • 阴影贴图是传统的深度纹理贴图,采用 PCF 过滤采样的着色器(无 VSM/EVSM 等阴影)。

  • 方向光可以使用级联阴影贴图(2 个或 4 个级联);阴影贴图空间分为级联,就像在图集内一样。

  • 聚光灯总是使用简单的 2D 阴影贴图;点光源使用立方体贴图。

  • 阴影贴图大小是根据质量设置、屏幕分辨率和光源在屏幕上的投影大小计算出来的;或者可以由游戏开发者通过脚本为每个光源进行显式控制。

  • 级联阴影贴图应用于“屏幕空间”:有一个单独的“收集和执行 PCF 过滤”步骤产生屏幕空间阴影遮罩纹理;稍后在常规对象渲染时只会将一个样本放入此纹理中。

  • 不支持在半透明对象上接受阴影。

前向渲染

默认工作模式主要是 DX9 风格的“每个光源进行一次绘制调用并进行附加混合”。游戏的质量设置决定了每个对象将实时渲染多少个光源;其余部分折叠成球谐函数 (SH) 表示,并与其他环境光照一起渲染。

*(可选)在主场景渲染之前,使用一个“深度纹理”渲染通道。如果脚本需要它,或者其他功能(例如实时级联阴影)需要它,则会启动。从概念上讲,这类似于 Z 预通道;使用场景深度缓冲区来生成纹理。

*(可选)在主场景渲染之前,使用一个“运动矢量”渲染通道。如果脚本(例如运动模糊和时间 AA)需要它,则会启动。为需要它们的对象渲染速度向量纹理。

  • 在主场景渲染之前渲染实时阴影贴图;所有阴影同时存在于内存中。

  • 实际场景渲染通道专门用于两个着色器集:“ForwardBase”(环境/探针 + 光照贴图 + 来自主方向光的光照/阴影),然后是附加混合“ForwardAdd”(一次进行一个光源的实时光照)。

延迟着色

这是“传统”DX9 风格的延迟着色:G 缓冲区渲染通道,接着是“逐个渲染光源形状”通道,其中每个都读取 G 缓冲区数据、计算光照并将其添加到光照缓冲区。

  • 与前向渲染类似,在 G 缓冲区之前有一个可选的运动矢量通道。

  • 通过渲染盒体形状并将反射添加到纹理中,逐个渲染反射探针,类似于光源。

  • 通过渲染光源形状(全屏四边形、球体或锥体)并将反射添加到纹理中,逐个渲染光源。

  • 在渲染每个光源之前渲染光源的阴影贴图,通常在完成之后立即丢弃光源。

  • 对光源和反射探针使用模板标记,以限制实际计算的像素数量。

  • 不支持延迟着色的对象和所有半透明对象都使用前向渲染进行渲染。

自定义

可在某种程度上自定义上述行为,但可自定义的程度不高。例如,Valve 的 The Lab Renderer(位于 Asset Store 中)取代了内置行为(完全采用 C# + 着色器):

  • 实现自定义阴影系统,其中所有阴影都打包到一个图集内。

  • 自定义前向渲染系统,其中所有光源都在一个通道中渲染;光源信息设置到自定义着色器 uniform 变量中。




  • 2017–05–26 页面已发布并只进行了有限的编辑审查

  • 5.6 中的新功能

脚本运行时升级(Scripting Runtime Upgrade)
旧版主题