---
title: "渲染基础——Render-pipeline"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/rendering-pipeline-fundamentals/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/05-游戏图形与运行时/渲染基础/渲染基础——Render-pipeline.md"
content_hash: 8e0378bed7a34d41d9f6a71aa5e9f64fb1a44af1cb586c99a13b4d69cbec2533
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# 渲染基础——Render-pipeline

> 创建时间：2020/8/5 11:02

  * 渲染基础——Render-pipeline
    * 基本概念
      * 渲染管线（流水线）
      * Shader
    * 三个阶段
      * 应用阶段 Application Stage ( CPU )
        * 加载数据到显存
        * 设置渲染状态
        * 调用DrawCall
      * 几何阶段 Geometry Stage ( GPU )
        * 顶点着色器(Vertex Shader)
        * 曲面细分着色器(Tessellation Shader)
        * 几何着色器(Geometry Shader)
        * 裁剪(Clipping)
        * 屏幕映射(Screen Mapping)
      * 光栅化阶段 Rasterizer Stage ( GPU )
        * 三角形设置(Triangle Setup)
        * 三角形遍历(Triangle Traversal)
        * 片元着色器(Fragment Shader)
        * 逐片元操作
    * 其他相关概念
      * 硬件、驱动与图像编程接口
        * High-Level Shading Language
        * 命令缓冲区（Command Buffer）
        * GPU渲染管线理解
    * 数学相关具体处理过程
      * 相关概念
      * 坐标转换过程
        * NDC(Normalized Device Coordinate)归一化设备坐标
        * 屏幕空间（Screen Space）
        * 从ComputeScreenPos 函数看坐标转换
        * 悟一悟：归一化、光栅化与插值
      * Vertex Shader & Fragment Shader
        * 顶点着色器
        * 片元着色器
    * Shader
      * 简单的Shader Demo
      * 接口与实现
    * Ref

## 基本概念

### 渲染管线（流水线）

《Render-Time Rendering Third Edition》一书中将计算机图形渲染的流程划分为3个阶段

![Alt text](/media/9cd9f4e247f028290a6a.png)

定义：也称渲染流水线，是显示芯片内部处理图形信号 **相互独立** 的 **并行** 的处理单位。

> 一个 **流水线** 是一序列可以 **并行** 和 **按固定顺序进行** 的阶段。也就是说每个阶段都是从它的前一阶段输入，然后输出发给随后的阶段。

渲染机理：将图像所具备的图形信息（顶点、纹理、材质、摄像机位置等）经过一系列阶段的处理，最终转换成屏幕上的图像。
实质是输入3D模型，输出2D图片显示出来的过程

**渲染过程中的坐标转换：物体空间- >世界空间->观察空间->裁剪空间->屏幕空间**

### Shader

此处的Shader区别于Unity Shader文件
Shader（着色器），是一种较为短小的程序片段，用于告诉图形硬件如何加速输出图像，过去是有汇编语言来编写。
总而来说，Shader是 **可编程图形管线的算法片段**
分类:

  * Vertex Shader

  * Fragment Shader

![Alt text](/media/962822f43f102114f1e2.png)

物体空间->世界空间->观察空间->裁剪空间->屏幕空间
**Shader和材质、贴图的关系：**
Shader将顶点数据以指定的方式和贴图或者颜色组合起来，将输出数据绘制到屏幕上。
**材质** 就是将 **Shader** 以及 **输入参数（包括贴图）** 打包存储起来，将材质赋予到三维物体上进行渲染
这样就是说材质就是引擎最终使用的商品（每个渲染的物体都需要一个材质）
Shader就是生产这种商品的加工过程或方法，贴图就是商品材料之一。

## 三个阶段

![Alt text](/media/c5a3d56da83397033889.png)

绿色表示完全可编程控制，黄色表示可配置，蓝色表示由GPU固定实现，不可修改。实线表示必须由开发者编程实现，虚线表示该Shader是可选的。

### 应用阶段 Application Stage ( CPU )

**目标：** 准备好场景数据，设置好渲染状态，然后输出渲染图元（rendering primitives），即为下一阶段提供所需的几何信息。

> **图元** 是指渲染的基本图形，通俗来讲图元可以是顶点，线段，三角面等，复杂的图形可以通过渲染多个三角形来实现。

可细分为3个子阶段

#### 加载数据到显存

所有渲染所需的数据都需要从硬盘加载到系统内存中（RAM），然后网格和纹理等数据又被加载到显存（VRAM）。这是因为显卡对于显存的访问速度更快，而且大多数显卡对于RAM没有直接的访问权利。

#### 设置渲染状态

比如设置使用的着色器，材质，纹理，光源属性等。

#### 调用DrawCall

Draw Call就是一个命令，它的发起方是CPU，接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元列表，而不会再包含任何材质信息，这是因为我们已经在上一个阶段设置过了。当给定了一个Draw Call时，GPU就会根据渲染状态和所有输入的顶点数据来进行计算，最终输出成屏幕上显示的那些漂亮的像素。

### 几何阶段 Geometry Stage ( GPU )

**接受数据：** 应用阶段所准备好的 **渲染图源** 信息，也就是顶点数据（模型自身坐标系、顶点颜色、纹理UV等）
**目标：** 与每个渲染图元打交道，进行逐顶点，逐多边形的操作。把顶点坐标变换到屏幕空间中，输出屏幕空间的顶点信息（绘制方法、绘制坐标），再交给光栅化器进行处理。
**基本流程：**

#### 顶点着色器(Vertex Shader)

对每个顶点均调用一次。
通过一系列的 **坐标转换** ，将模型的顶点在摄像机前进行位移，并最终投影到摄像机的投影屏幕上
**本地坐标系- >世界坐标系->观察坐标系->投影坐标系**

顶点处理这个阶段包括顶点的 **坐标变换** 、 **逐顶点雾化** 、 **材质属性** 、 **光照属性** 处理

坐标变换：把顶点坐标从模型空间转换到齐次裁剪空间，接着通常再由硬件做透视除法，最终得到归一化的设备坐标（NDC）。

> 顶点着色器本身不可以创建或者销毁任何顶点，而且无法得到顶点和顶点之间的关系，例如我们无法得知两个顶点是否属于同一个三角网格。但正因为这样的相互独立性，GPU可以利用本身的特性并行化处理每一个顶点，这意味着这一阶段的处理速度会很快。

#### 曲面细分着色器(Tessellation Shader)

细分图元，例如将三角面细分成更小的三角面来添加几何细节。

> In Direct3D 11 pipeline (a part of DirectX 11), the graphics primitive is the patch.[4] The tessellator generates a triangle-based tessellation of the patch according to tessellation parameters such as the TessFactor, which controls the degree of fineness of the mesh.
>  Tessellation: 棋盘型布置，密铺，曲面细分

#### 几何着色器(Geometry Shader)

逐图元着色操作或是生成新的图元(减少CPU负担)。可决定输出的图元类型和个数，当输出的图元减少时，实际上起到了裁剪的作用，当输出的图元增多或类型改变时，起到了产生或改变图元的作用

#### 裁剪(Clipping)

将那些不在摄像机视野内的顶点裁减掉，并剔除某些三角图元的面片（面片通常是由一个一个更小的图元来构成的）。

> 一个图元和摄像机视野的关系有3种：完全在视野内，部分在视野内，完全在视野外。完全在视野内的图元就继续传递给下一个流水线阶段，完全在视野外的图元不会继续向下传递，因为它们不需要被渲染。而那些部分在视野内的图元需要被裁剪。
>
![Alt text](/media/583eb0f957fa359cc8e3.png)

#### 屏幕映射(Screen Mapping)

把每个图元的x和y坐标转换到屏幕坐标系下，这实际上是一个缩放的过程。屏幕坐标系是一个二维坐标系，它和我们用于显示画面的分辨率有很大关系。

![Alt text](/media/4300e608451b39485daf.png)

屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远。 屏幕映射不会对输入的z坐标做任何处理。实际上，屏幕坐标系和z坐标一起构成了 **窗口坐标系** 。这些值会被一起传递到光栅化阶段。

### 光栅化阶段 Rasterizer Stage ( GPU )

> 光栅化：是把顶点数据转换为片元的过程，具有将图转化为一个个栅格组成的图象的作用，特点是每个元素对应帧缓冲区中的一像素。（将以向量为基本结构的面转换为一个个点阵形式的像素。）

**接受数据：** 几何阶段准备好的顶点信息
**目标：** 对几何阶段传递过来的 **屏幕空间的顶点信息** 进行处理,最终生成 **屏幕像素,** 渲染出图像

需要对上一个阶段得到的逐顶点数据（例如纹理坐标，顶点颜色等）进行插值，然后再进行逐像素处理。
例如对于三角形图元，得到的就是三个顶点的坐标和颜色信息等。而光栅化阶段要做的就是根据这三个顶点，计算出这个三角形 **覆盖了哪些像素** ，并为这些像素通过 **插值** 计算出它们的颜色。

#### 三角形设置(Triangle Setup)

计算光栅化一个三角网格所需的信息。具体来说，上一个阶段输出的都是三角网格的顶点，但如果要得到整个三角网格对像素的覆盖情况，我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息，我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。

#### 三角形遍历(Triangle Traversal)

检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话，就会生成一个片元。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历，这个阶段也被称为扫描变换。
三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素，并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行 **插值** 。 **像素** 和 **片元** 是一一对应的，每个像素都会生成一个片元， **片元中的状态记录了对应像素的信息** ，是对三个顶点的信息进行插值得到的。

![Alt text](/media/3951bdbb1d104e4fd308.png)

> 片元包含了很多状态的集合，这些状态用于计算每个像素的最终颜色。这些状态包括了但不限于它的屏幕坐标，深度信息，以及其他从几何阶段输出的顶点信息，例如法线，纹理坐标等。

#### 片元着色器(Fragment Shader)

又称 **像素着色器(Pixel Shader)** 。逐片元的着色操作(Per-Fragment Operations)，输出是一个或者多个颜色值（即计算该片元对应像素的颜色，但不是最终颜色）。

  1. 纹理采样
这一阶段可以完成很多重要的渲染技术，其中最重要的技术之一就是 **纹理采样** 。为了在片元着色器中进行纹理采样，我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标，然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后，就可以得到其覆盖的片元的纹理坐标了。

  2. 计算光照(阴影、明暗…)

根据上一步插值后的片元信息，片元着色器计算该片元的输出颜色 虽然片元着色器可以完成很多重要效果，但它的局限在于，它 **仅可以影响单个片元** 。也就是说，当执行片元着色器时，它不可以将自己的任何结果直接发送给它的邻居们。当然导数信息例外。

#### 逐片元操作

  1. 决定每个片元的可见性。这涉及了很多测试工作，例如 **深度测试** ， **模板测试** 等。

  2. 如果一个片元通过了所有的测试，就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并，或者所是 **混合** 。

片元-> 模板测试->深度测试->混合->颜色缓冲区

**模板测试（Stencil Test）**
模板测试模板缓冲（Stencil Buffer）
过程：GPU会首先读取（使用读取掩码）模板缓冲区中该片元位置的模板 值，然后将该值和读取（使用读取掩码）到的参考值（reference value） 进行比较，这个 **比较函数可以是由开发者指定** 的，例如小于时 舍弃该片元，或者大于等于时舍弃该片元。可根据模板测试和下面的深度测试结果来修改模板缓冲区，这个 **修改操作也是由开发者指定** 。
应用： 限制渲染的区域，渲染阴影，轮廓渲染等

**深度测试（Depth Test）**
如果开启了深度测试，GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个 **比较函数也是由开发者设置** 的。通常如果这个片元的深度值大于等于当前深度缓冲区中的值，那么就会舍弃它。
如果一个片元没有通过深度测试，它就没有权利更改深度缓冲区中的值。而如果它通过了测试，开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值，这是通过 **开启/关闭深度写入** 来做到的。

**混合（Blend）**
对于不透明物体，开发者可以关闭混合操作。但对于半透明物体，我们就需要使用混合操作来让这个物体看起来是透明的。
如果开启了混合，GPU会取出源颜色和目标颜色，将两种颜色进行混合。源颜色指的是片元着色器得到的颜色值，而目标颜色则是已经存在于颜色缓冲区中的颜色值。之后，就会使用一个 **混合函数** 来进行混合操作。

> 大多数GPU会尽可能在执行片元着色器之前就进行测试，避免无谓的计算。
>  将深度测试提前执行的技术通常也被称为Early-Z技术

**透明度测试会导致性能下降** 的原因：
现代的GPU会判断片元着色器中的操作是否和提前测试发生冲突，如果有冲突，就会禁用提前测试。但是，这样也会造成性能上的下降，因为有更多片元需要被处理了。这也是透明度测试会导致性能下降的原因。

## 其他相关概念

### 硬件、驱动与图像编程接口

![Alt text](/media/c090c1d7cd30b27ca227.png)

OpenGL和DirectX就是 **图像应用编程接口** ，用于渲染二维或三维图形。
应用程序向这些接口发送渲染命令，这些接口依次向 **显卡驱动(Graphics Driver)** 发送渲染命令。

> 显卡驱动把OpenGL或者DirectX的函数调用翻译成GPU能够听懂的语言，把纹理等数据转换成GPU所支持的格式。
>  概括 来说， 我们 的 应用 程序 运行 在 CPU 上。 应用 程序 可以 通过 调用 OpenGL 或 DirectX 的 图形 接口 将 渲染 所需 的 数据， 如 顶点 数据、 纹理 数据、 材质 参数 等 数据 存储 在 显 存 中的 特定 区域。 随后， 开发者 可以 通过 图像 编程 接口 发出 渲染 命令， 这些 渲染 命令 也 被称为 Draw Call， 它们 将 会被 显 卡 驱动 翻译 成 GPU 能够 理解 的 代码， 进行 真正 的 绘制。
>  冯乐乐. Unity Shader入门精要（异步图书） (Kindle Locations 641-644). 人民邮电出版社. Kindle Edition.

**显存（VRAM, Video Random Access Memory）** ：图像缓冲、深度缓冲等

#### High-Level Shading Language

  * DirectX - HLSL (High Level Shading Language)

  * OpenGL - GLSL (OpenGL Shading Language)

  * NVIDIA - CG (C for Graphic)

这些语言会被编译成与机器无关的 **汇编语言** ，也被称为中间语言（Intermediate Language，IL）。这些中间语言再交给 **显卡驱动来翻译** 成真正的 **机器语言** ，即GPU可以理解的语言。 ——冯乐乐

#### 命令缓冲区（Command Buffer）

命令缓冲区包含了一个命令队列，由CPU向其中添加命令，而由GPU从中读取命令，添加和读取的过程是互相独立的。命令缓冲区使得CPU和GPU可以相互独立工作。当CPU需要渲染一些对象时，它可以向命令缓冲区中添加命令，而当GPU完成了上一次的渲染任务后，它就可以从命令队列中再取出一个命令并执行它。

#### GPU渲染管线理解

![Alt text](/media/d392d88b6afd1ae11981.jpg)

[深入GPU硬件架构及运行机制](https://www.cnblogs.com/timlly/p/11471507.html)

## 数学相关具体处理过程

### 相关概念

视锥体（view frustum）
frustum(截头锥体)

### 坐标转换过程

物体空间(object space)->世界空间(World Space)->观察空间(View Space)->裁剪空间(clip space)->屏幕空间(Screen Space)

#### NDC(Normalized Device Coordinate)归一化设备坐标

在这一步会进行一个叫齐次除法的步骤，说白了就是各个点（x,y,z,w）会除以w的值（注：计算机图形学中经常使用四元数代表一个点，叫齐次空间，齐次点等 ）

![Alt text](/media/712423a6eb5a103abb0e.png)

![Alt text](/media/ec8fa66e95d7eda55a4e.png)

#### 屏幕空间（Screen Space）

pixelWidth:屏幕横向分辨率
pixelHeight:屏幕纵向分辨率
OpenGL规范

![OpenGL规范](/media/c36e480bc02a5b9ded5d.png)

DirectX规范

![DirectX规范](/media/7a79e5e5179e774e758a.png)

screenx={clipx*pixelWidth/(2*clipw)}+pixelWidth/2
screeny={clipy*pixelHeight/(2*clipw)}+pixelHeight/2

#### 从ComputeScreenPos 函数看坐标转换

ComputScreenPos() just remap [-w,w] to [0,w].

example code of ComputeScreenPos()’s usage

```cpp
string s = String.Empty;

```
```cpp
string s = String.Empty;

```

ComputeScreenPos字面意思是计算屏幕空间的坐标，但该函数得到的坐标的xy值除以w分量，才是真正地视口空间下的坐标。

这是因为该函数是在顶点函数中计算，得到的结果还要在光栅化阶段进行插值。如果在顶点函数中就除以w，那么就会破坏插值的结果。我们不可以在投影空间进行插值。除以w之后，各个分量的关系就不是线性的了，而插值是线性的过程。

#### 悟一悟：归一化、光栅化与插值

![Alt text](/media/86b1c858ecf2256015c6.png)

光栅化与插值是两个过程，光栅化并不是插值。

先求得归一化设备坐标，在NDC空间中进行光栅化——光栅化是将一个面，由三个顶点的坐标，生成很多个像素点，而这一过程，无所谓是在裁切坐标空间还是在NDC坐标空间中进行的，无所谓线性不线性。最终要的结果，就是NDC空间中的一个面上面的一堆点。

对通道值进行插值，必须先插值再归一化，可以从不同的角度理解：

  1. 插值是线性插值 ，必须在线性的坐标空间中进行，不能对归一化后的结果进行线性插值

  2. 插值本质上是由原始的面的顶点的通道上的值，计算光栅化后的像素点的通道上的值。本质上是要把线性空间中，插值得到的合理的结果，映射到光栅化后的像素上。先插值，再归一化，插值的时候，使用的是原始的线性空间下的信息。

ComputeScreenPos的例子中，本质是要把顶点的坐标信息传到片元函数中，用代码人工计算屏幕坐标。也就是要把原始空间点的坐标，转换成屏幕坐标，映射给相应的像素（片元），要求插值的结果保证映射关系是正确的。如果插值使用的是线性空间下的数据，进行线性插值，得到的结果就是正确的。试想，如果线性空间下的两个点中点是A，映射到NDC空间下的点a，a在NDC空间下并不是这两个点的中点，而a点的所有通道的结果，应该对他的两个端点取均值。如果使用NDC空间下的值进行插值，取均值的结果却是NDC空间下的中点b，得到的结果，是b在线性空间下的点B应当获取的结果。

总之，光栅化阶段后，片元函数前，会进行点的插值，而这插值方法是线性插值，传递给该阶段的通道中的数据，应当是线性空间下的。插值完了，再归一化。

> 三角形内插值

### Vertex Shader & Fragment Shader

#### 顶点着色器

将物体从 **物体空间** -> **世界空间** -> **观察空间** -> **裁剪空间** 就是顶点着色器的工作。

>   1. 将物体空间的数据（点）作为顶点着色器的输入
>
>   2. 将所有在自己范围中的点全部遍历一遍，就是每个点都会算进行加工
>
>   3. 高度可编程配置！
>
>

#### 片元着色器

将裁剪空间中的点从 **裁剪空间** -> **屏幕空间** 就是片元着色器的工作。
片元着色器会返回一个四元数-（r,g,b,a）->分别为（red<红>，green<绿>，blue<蓝>，alpha<透明度>）

>   1. 将裁减空间的数据（点）作为片元着色器的输入
>
>   2. 将所有在自己范围中的像素全部遍历一遍（三角遍历Rasterizer—Triangle Traversal），就是每个片元（像素）都会运算进行加工。
>
>   3. 高度可编程配置！
>
>

## Shader

### 简单的Shader Demo

```cpp
string s = String.Empty;

```

### 接口与实现

UnityObjectToClipPos()

```cpp
string s = String.Empty;

```

unity_ObjectToWorld是物体空间到世界空间的转换矩阵mul()矩阵乘法内置函数。
UNITY_MATRIX_VP（观察空间和裁剪空间合一起了），乘完后将从世界空间变换到裁剪空间。

完全手动自定义计算的话，这么写：

```cpp
string s = String.Empty;

```

## Ref

<https://www.cnblogs.com/SouthBegonia/p/11564407.html>
<https://blog.csdn.net/AvatarForTest/article/details/80438344>
<http://m.elecfans.com/article/713834.html>
<https://juejin.im/post/6844903975712489485>
《Unity Shader入门精要》
