跳至主要內容

《UnityShadow》

LincZero大约 15 分钟

《UnityShadow》

目录

渲染流水线(书顺序)

综述

什么是流水线

  • 优点:可以提高单位时间的生产量
  • 吐槽:书上的说法有点误导,用来和现实情况类比是不恰当的。这应该是硬件功能的单一性所决定的

图2.1 真实生活中的流水线

![流水线.png-83.4kB](1-02. 渲染流水线.assets/流水线.png)

什么是渲染流水线

渲染流程:《Real-Time Rendering,Third Edition》一书将渲染流程分成下面三个阶段(概念流水线,而非GPU流水线)

图2.2 渲染流水线中的三个概念阶段

![概念流水线.png-16.9kB](1-02. 渲染流水线.assets/概念流水线.png)

概念补充

  • 渲染图元:可以是点、线、三角面等

应用阶段(Application Stage)(CPU实现)

三个主要任务

  • (1) 准备好场景数据(例如:摄像机位置、视锥体、场景中包含哪些模型、使用了哪些光源等等)

  • (2) 为了提高渲染性能,需要作一个粗粒度剔除(culling)工作,剔除不可见物体不提交给集合阶段

    (注:这里的不可见是指被隐藏而非被遮挡而不可见)

  • (3) 需要设置好每个模型的渲染状态(例如:使用的材质(漫反射颜色、高光反射颜色)、纹理、Shader等等)

几何阶段(Geometry Stage)(GPU实现)

负责和每个渲染图元打交道,进行逐顶点、逐多边形操作。可细分成更小的流水线阶段

光栅化阶段(Rasterizer Stage)(GPU实现)

产生屏幕上的像素,并渲染出最终的图像。可细分成更小的流水线阶段

有两个重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色

CPU和GPU通信

CPU和GPU通信的过程大致分为三个阶段

把数据加载到到显存中

  • 目的
    • 显卡对显存的访问速度更快
    • 大多数显卡对RAM没有直接访问权利
  • 过程补充
    • 之后RAM中的数据就可以移除了。但对于一些数据来说CPU仍需要访问,不移除该数据
    • 例如:访问网格数据来进行碰撞检测

图2.3 渲染所需的数据(两张纹理以及3个网格)从硬盘最终加载到显存中。在渲染时,GPU可以快速访问这些数据

![CopyDataToGPU.png-86.5kB](1-02. 渲染流水线.assets/CopyDataToGPU.png)

设置渲染状态

例如:使用哪个顶点着色器/片元着色器、光源属性、材质等等

如果没有更改渲染状态,那么所有网格都使用同一种渲染状态

图2.4 在同一状态下渲染三个网格。由于没有更改渲染状态,因此三个网格的外观看起来像是同一种材质的物体

![SetRenderState.png-157.1kB](1-02. 渲染流水线.assets/SetRenderState.png)

调用Draw Call

  • Draw Call:是一个命令。发起方CPU,接收方GPU,参数为需要被渲染的图元(primitives)列表

图2.5 CPU通过调用Draw Call来告诉GPU开始进行一个渲染过程。一个Draw Call会指向本次调用需要渲染的图元列表

![DrawCall.png-59.1kB](1-02. 渲染流水线.assets/DrawCall.png)

GPU流水线

概述

图2.6 GPU的渲染流水线实现。颜色表示了不同阶段的可配置性或可编程性:

  • 绿色表示该流水线阶段是完全可编程控制的
  • 黄色表示该流水线阶段可以配置但不是可编程的
  • 蓝色表示该流水线阶段是由GPU固定实现的,开发者没有任何控制权
  • 实线表示该shader必须由开发者编程实现
  • 虚线表示该Shader是可选的

![GPU流水线.png-82.2kB](1-02. 渲染流水线.assets/GPU流水线.png)

整理

步骤作用是否能变成控制输入(上一阶段的输出)
顶点数据————————
顶点着色器坐标变换、逐顶点光照可编程控制图元列表
曲面细分着色器(可选)细分图元可编程控制
几何着色器(可选)执行逐图元的着色操作,或者被用于产生更多的图元可编程控制
裁剪将不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片可配置不可编程带归一化的设备坐标的图元
屏幕映射负责把每个图元的坐标转换到屏幕坐标系不可配置不可编程裁剪过的图元
三角形设置计算光栅化一个三角网格所需的信息不可配置不可编程z轴缩放过的图元
三角形遍历检查每个像素是否被一个三角网格所覆盖不可配置不可编程三角网格表示的数据
片元着色器(可选)完成一些渲染技术可编程控制片元序列、顶点的插值信息
逐片元操作逐片元的着色操作可配置不可编程一个或多个颜色值
屏幕图像————————真正的像素颜色

顶点着色器(Vertex Shader)(可编程)

  • 主要工作:坐标变换、逐顶点光照

  • 坐标变换:把顶点坐标从模型空间转换到齐次裁剪空间,然后在由硬件做透视除法,得到归一化的设备坐标(Normalized Device Coordinates,NDC)

    (具体细节第四章还会讲,但我猜测是三维坐标变成了指定透视参数(摄像机位置和视锥体)的透视图坐标)

  • 补充 - OpenGL与DirectX区别

    • OpenGL:NDC的z分量范围为[-1,1],也是Unity使用的NDC
    • DirectX:NDC的z分量范围为[0,1]

图2.7 GPU在每个输入的网格顶点上都会调用顶点着色器。顶点着色器必须进行顶点的坐标变换,需要时还可以计算和输出顶点的颜色。例如,我们可能需要进行逐顶点的光照

![VertexShaderProcess.png-43kB](1-02. 渲染流水线.assets/VertexShaderProcess.png)

图2.8 顶点着色器会将模型顶点的位置变换到齐次裁剪坐标空间下,进行输出后再由硬件做透视除法得到NDC下的坐标

![Vertex Shader.png-34.9kB](1-02. 渲染流水线.assets/Vertex Shader.png)

曲面细分着色器(Tessellation Shader)(可编程、可选)

  • 作用:细分图元

几何着色器(Geometry Shader)(可编程、可选)

  • 作用:执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元

裁剪(Clipping)(可配置)

  • 作用:将不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片
  • 可配置:自定义裁剪平面、控制裁剪三角图元的正面还是背面
  • 原理详解
    • 完全在视野内:继续传递
    • 完全在视野外:不向下传递
    • 部分在视野内:裁剪(例如一条线在交界处被插入一个新的顶点)

图2.9 只有在单位立方体的图元才需要被继续处理。因此,完全在单位立方体外部的图元(红色三角形)被舍弃,完全在单位立方体内部的图元(绿色三角形)将被保留。和单位立方体相交的图元(黄色三角形)会被裁剪,新的顶点会被生成,原来在外部的顶点会被舍弃

![Clipping.png-25.5kB](1-02. 渲染流水线.assets/Clipping.png)

屏幕映射(Screen Mapping)(固定)

  • 作用:负责把每个图元的坐标转换到屏幕坐标系(Screen Coordinates)中
  • 实现细则:把场景渲染到一个窗口上,这是一个缩放的过程。其中z坐标不做任何处理
  • 坐标系:屏幕坐标系Screen Coordinates)与z坐标系一起构成窗口坐标系Window Coordinates

图2.10 屏幕映射将x、y坐标从(-1, 1)范围转换到屏幕坐标系中

![ScreenMapping.png-22.6kB](1-02. 渲染流水线.assets/ScreenMapping.png)

  • OpenGL和DirectX的屏幕坐标差异,如下

    产生这种差异的原因:微软的窗口都使用了DirectX的坐标系统(比如QT开发),这也与我们的阅读方式是一致的:从左到右,从上到下,并且很多图像文件也是按这样的格式存储的

图2.11 OpenGL和DirectX的屏幕坐标系差异。对于一张512*512大小的图像,在OpenGL中其(0, 0)点在左下角,而在DirectX中其(0, 0)点在左上角

![Screen Mapping_OpenGL_DirectX.png-26.9kB](1-02. 渲染流水线.assets/Screen Mapping_OpenGL_DirectX.png)

三角形设置(Triangle Setup)(固定)

  • 作用:计算光栅化一个三角网格所需的信息(在此之前处理的都是图元中的点)

三角形遍历(Triangle Traversal)(固定)

  • 作用:检查每个像素是否被一个三角网格所覆盖,这个阶段也被称为扫描变换Scan Conversion
  • 输出:片元序列,并不是真正意义上像素,而是包含了很多用于计算像素最终颜色的状态(例如屏幕坐标、深度信息、顶点信息(法线、纹理坐标)等)

图2.12 三角形遍历的过程。根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置。对应像素会生成一个片元,而片元中的状态是对三个顶点的信息进行插值得到的。例如,对图2.12中三个顶点的深度进行插值得到其重心位置对应的片元的深度值为-10.0

![TriangleSetupAndTraversal.png-80kB](1-02. 渲染流水线.assets/TriangleSetupAndTraversal.png)

片元着色器(Fragment Shader)(可编程、可选)这步不太懂

  • 作用:完成一些渲染技术(例如纹理采样)
  • 输入:对顶点信息插值得到的结果
  • 输出:是一个或者多个颜色值
  • 别名:DirectX中称为像素着色器(pixel Shader),但片元着色器Fragment Shader)的名字更佳,因为此时片元还不是像素
  • 局限:仅可以影响单个片元,不可用将自己的任何结果直接发个邻居(导数信息除外)

图2.12 三角形遍历的过程。根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置。对应像素会生成一个片元,而片元中的状态是对三个顶点的信息进行插值得到的。例如,对图2.12中三个顶点的深度进行插值得到其重心位置对应的片元的深度值为-10.0

![FragmentShader.png-42.4kB](1-02. 渲染流水线.assets/FragmentShader.png)

逐片元操作(Per-Fragment Operations)(可配置)

  • 作用:逐片元(Per-Fragment Operations)的着色操作(例如修改颜色、深度缓冲、进行魂环等等)
  • 别名
    • OpenGL:逐片元操作Per-Fragment Operations
    • DirectX:输出合并阶段Output-Merger
  • 实现细节
    • 决定每个片元的可见性。这涉及很多测试工作(例如深度测试、模板测试等等)
    • 如果一个片元通过了所有测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区的颜色进行合并,或者说是混合

图2.14 逐片元操作阶段所做的操作。只有通过了所有的测试后,新生成的片元才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区中

![Per-fragment Operations.png-23.1kB](1-02. 渲染流水线.assets/Per-fragment Operations.png)

模板测试(Stencil Test

  • 模板测试补充:与之相关的是模板缓冲
  • 模板测试高级用法:渲染阴影、轮廓渲染等
  • 深度测试:齐次就是

图2.15 模板测试和深度测试的简化流程图

![Stencil Test_Depth Test.png-93.5kB](1-02. 渲染流水线.assets/Stencil Test_Depth Test.png)

深度测试(Depth Test

  • 深度测试补充:对于被其他物体遮挡的就不需要出现在屏幕上

混合(Blend

  • 混合补充:对于不透明物体,可以关闭混合操作
  • 混合函数:透明度、混合模式等

图2.16 混合操作的简化流程图

![Blending.png-67.6kB](1-02. 渲染流水线.assets/Blending.png)

补充:提前执行技术

  • 提前执行技术:测试顺序并不是唯一的,对于大多数GPU来说,会尽可能在片元着色器之前进行测试,以提前筛掉不需要的片元
  • Early-Z:将深度测试提前执行的技术通常也被称为Early-Z技术
  • 缺陷:有时候提前执行测试会引发冲突,如果有冲突就禁用提前测试,而这会导致性能上的下降
    • 举例:透明度与深度提前测试冲突,即透明度测试会导致性能下降的一个原因

图2.17 图示场景中包含了两个对象:球和长方体,绘制顺序是先绘制球(在屏幕上显示为圆),再绘制长方体(在屏幕上显示为长方形)。如果深度测试在片元着色器之后执行,那么在渲染长方体时,虽然它的大部分区域都被遮挡在球的后面,即它所覆盖的绝大部分片元根本无法通过深度测试,但是我们仍然需要对这些片元执行片元着色器,造成了很大的性能浪费

![why_early_depth_test.png-18.7kB](1-02. 渲染流水线.assets/why_early_depth_test.png)

补充:双重缓冲(Double Buffering)策略

  • 简概:一旦场景被渲染到后置缓冲中,GPU就会交换后置缓冲区Back Buffer)和前置缓冲区Front Buffer)中的内容
  • 优点:前置缓冲区是之前显示在屏幕上的图像,这保证了我们看到的图像总是连续的==(没太懂最后一句话)==

一些容易困惑的地方

什么是OpenGL/DirectX

  • OpenGL和DirectX:是图像应用编程接口,这些接口用于渲染二维或三维图形。是上层应用程序和底层GPU之间的沟通桥梁
  • 显卡驱动作用:接口一次向显卡驱动(Graphics Driver)发送渲染命令,显卡驱动将OpenGL或DirectX的函数调用翻译成GPU能听懂的语言
  • 显存(Video Random Access Meory,VRAM)

图2.18 CPU、OpenGL/DirectX、显卡驱动和GPU之间的关系

![OpenGL和DirectX.png-56.1kB](1-02. 渲染流水线.assets/OpenGL和DirectX.png)

什么是HLSL、GLSL、CG

  • 概念:可编程的着色器阶段能使用高级的着色语言Shading Language)编写

  • 分类与特点

    • 语言优点缺点原因
      DirectX的HLSL
      High Level Shading Language
      不同硬件的编译结果一样
      (版本相同时)
      平台相对有限,几乎都是微软微软控制着色器的编译
      OpenGl的GLSL
      OpenGL Shading Language
      跨平台性不同硬件供应商的编译实现可能不同其着色器编译器由显卡驱动来完成
      NVIDIA的CG
      (C for Graphic)
      真正意义上的跨平台
      可以无缝移植成HLSL
      无法完全发挥出OpenGL的最新特性根据平台不同而编译成相应的中间语言
      与微软的合作

什么是Draw Call

问题一:CPU和GPU是如何实现并行工作的

  • 解决方法:使用一个命令缓冲区Command Buffer
    • 命令缓冲区包含一个命令队列,由CPU向其中添加命令,而GPU从中读取队列
  • 补充:这也解释了为什么说:Draw Call中造成性能问题的原因往往是CPU

图2.19 命令缓冲区。CPU通过图像编程接口向命令缓冲区中添加命令,而GPU从中读取命令并执行。黄色方框内的命令就是Draw Call,而红色方框内的命令用于改变渲染状态。我们使用红色方框来表示改变渲染状态的命令,
是因为这些命令往往更加耗时

![CommandBuffer.png-49.9kB](1-02. 渲染流水线.assets/CommandBuffer.png)

为什么Draw Call多了会影响帧率

  • 类比:复制10000个1KB文件比复制一个单独的10MB文件慢很多。复制需要很多额外操作,例如分配内存、创建各种元数据等
  • 结论:如果Draw Call数量太多,CPU会把大量时间花费在提交Draw Call上,造成CPU的过载

图2.20 命令缓冲区中的虚线方框表示GPU已经完成的命令。此时,命令缓冲区中没有可以执行的命令了,GPU处于空闲状态,而CPU还没有准备好下一个渲染命令

![SmallCommand.png-107.7kB](1-02. 渲染流水线.assets/SmallCommand.png)

如何减少Draw Call?

  • 方法:有很多,这里仅讨论使用批处理Batching)的方法
    • 批处理
      • 方法:把很多很小的DrawCall合并成一个大的Draw Call,比如在CPU内存中合并网格,合并的过程需要消耗时间
      • 局限:更适合那些静态物体。如果对动态物体进行批处理,会一帧都需要重新合并物体,效率低
    • 开发启示
      • 避免使用大量很小的网格,当不可避免时应当考虑是否合并它们
      • 避免使用过多的材质。尽量在不同的网格之间共用同一个材质

图2.21 利用批处理,CPU在RAM把多个网格合并成一个更大的网格,再发送给GPU,然后在一个Draw Call中渲染它们。但要注意的是,使用批处理合并的网格将会使用同一种渲染状态。也就是说,如果网格之间需要使用不同的渲染状态,那么就无法使用批处理技

![Batching.png-70.3kB](1-02. 渲染流水线.assets/Batching.png)

什么是固定管线渲染

3种图像接口从固定管线向可编程管线进化的版本

3D API最后支持固定管线的版本第一个支持可编程管线的版本
OpenGL1.52.0
OpenGL ES1.12.0
DirectX7.08.0