Version: 5.6
环境阴影 (Environment Shadow)
旧版主题

可编程渲染管线

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

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

本页包含以下内容:

功能摘要

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

本功能背后的动机

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

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

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

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

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

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

While the current rendering pipeline is somewhat extensible (users can write their own shaders, manually control camera rendering, change settings, extend the rendering pipeline with command buffers), it is not extensible enough. Additionally, it is too much of a “black box”, and while the documentation, conference presentations, MIT-licensed built-in shader source code and community knowledge does fill in the gaps, some parts are hard to understand without a Unity source code license. We want all the high level code and shader/compute code to be a MIT-licensed open source project, similar to how Post-Processing, UI or Networking already are.

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

  • 针对现代 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 变量中。




环境阴影 (Environment Shadow)
旧版主题