1.1 程序员的三大浪漫
- 编译原理、操作系统和图形学
- 学习 C++、C# 这样的
高级语言
时,我们可以在不了解计算机架构的情况下仍然编写出有效的代码,因为它们更符合人类的思维方式 - 但 Shader 更多地是
面向 GPU
的工作方式,所以它的一些语法不并直观 - 建立一个
渲染流程
的整体体系,它是跨越 Shader 学习中层层障碍的重要因素
2.1 综述
- Shader 仅仅是渲染流水线中的一个环节,我们需要知道它在其中扮演怎样的角色
2.1.1 什么是流水线

2.1.2 什么是渲染流水线
任务就是由一个三维场景出发,生成一张二维图像
- 即计算机需要从一系统的顶点数据、纹理等信息出发,把它们转换成一张人眼可见的图像
- 分 3 个阶段(概念流水线,并非 GPU 渲染流水线)
- 应用阶段
- 开发者有绝对控制权
- 3 个主要任务:
- 准备好
场景数据
(摄像机、模型、光源等) - 粗粒度
剔除
(culling) - 设置好每个模型的
渲染状态
- 准备好
- 输出:渲染所需的几何信息,即
渲染图元
- 几何阶段
- 处理所有要绘制的几何相关的事情(绘制什么、怎么绘制、在哪绘制)
- 负责和每个渲染图元打交道,进行逐顶点、逐多边形操作
- 输出:屏幕空间中的二维顶点信息(坐标、深度、着色等)
- 光栅化阶段
- 使用上阶段输出的数据来产生屏幕上的像素,并渲染出最终图像
- 主要任务是决定每个渲染图元中的哪些像素应该被如何绘制在屏幕上(顶点插值)
- 应用阶段
2.2 CPU 和 GPU 之间的通信(应用阶段)
2.2.1 把数据加载到显存中
所有渲染数据都需要从硬盘加载到内存中,然后网格和纹理等数据又被加载到显存中
- 当把数据加载到显存中后,内存中的数据通常可以被移除。某些情况除外(CPU 还需要访问)
- 碰撞检测:网格数据
- 物理模拟:物体的几何数据、顶点信息等
- 动画控制:模型的顶点数据和骨骼信息
- 数据实时更新:与新数据进行整合或作为更新的基础,再重新上传到显存
- 多线程渲染:一个线程负责将数据加载到显存,另一个线程需要访问内存中的数据进行其他处理
2.2.2 设置渲染状态
- 定义了场景中的网格如何被渲染
- 顶点着色器
- 片元着色器
- 光源属性
- 材质
2.2.3 调用 Draw Call
这命令仅仅会指向一个需要被渲染的
图元列表
,而不会再包含任何材质信息
- GPU 在收到命令后,会根据渲染状态和所有输入的顶点数据来进行计算,最终输出图像
2.3 GPU 流水线
2.3.1 概述
- 后两个阶段,开发者无法拥有绝对的控制权,但 GPU 向开发者开放了很多控制权(提供了不同的
可配置性
和可编程性
)- 绿色表示完全可编程控制
- 黄色表示可配置
- 蓝色表示由GPU固定实现,不可修改
- 实线框表示必须由开发者编程实现
- 虚线框表示该Shader是可选的
- 顶点着色器 (完全可编程)
- 空间变换
- 顶点着色
- 曲面细分着色器 (可选)
- 细分图元
- 几何着色器 (可选)
- 逐图元的着色操作
- 产生更多图元
- 裁剪 (可配置)
- 将不在摄像机视野内的顶点裁剪掉
- 屏幕映射 (不可编程和配置)
- 将每个图元的坐标转换到屏幕空间中
- 三角形设置和遍历(不可编程和配置)
- 片元着色器 (完全可编程)
- 逐片元着色
- 逐片元操作 (可配置)
- 修改颜色
- 深度缓冲
- 混合
2.3.2 顶点着色器
- 输入进来的每个顶点都会调用一次顶点着色器
- 任务
- 坐标变换:对顶点坐标进行某种变换
- 必要:将顶点坐标从模型空间转换到齐次裁剪空间
- 改变顶点位置来模拟水面、布料等效果
- 必要:将顶点坐标从模型空间转换到齐次裁剪空间
- 逐顶点光照
- 输出后续阶段所需的数据
- 坐标变换:对顶点坐标进行某种变换
2.3.3 裁剪
对跨越视锥体边界的图元 "切割",只保留可见部分

2.3.4 屏幕映射
把每个图元的坐标(仅 x 和 y)转换到屏幕坐标系下(3D -> 2D)
- z 坐标暂时不处理,它与屏幕坐标系构成了一个坐标系(窗口坐标系)。它们被传送到光栅化阶段
2.3.5 三角形设置
计算光栅化一个三角网格所需的信息(每条边上的像素坐标)
2.3.6 三角形遍历
检查
每个像素是否被一个三角网格所覆盖
。若被覆盖,就会生成一个片元
- 使用三角网格 3 个顶点信息对整个覆盖区域的像素进行
插值
- 输出得到一个
片元序列
- 一个
片元
并不是真正意义上的像素,而是包含了很多状态的集合
,用于计算最终颜色 - 状态包括:屏幕坐标、深度,顶点信息(纹理坐标、颜色、法线)等
- 一个

2.3.7 片元着色器
负责对每个片元
计算其颜色值
,并可添加各种高级视觉效果
- 通常只会影响单个片元(例外就是片元着色器可访问到导数信息 gradient)
2.3.8 逐片元操作
对片元进行最终筛选、处理和输出,确保正确的像素最终显示在屏幕上
主要任务:
- 决定每个片元的
可见性
(测试:深度、模板) - 若一个片元通过所有测试,就把它的颜色值和颜色缓冲区中的颜色进行
合并
(混合)
模板测试

- GPU 首先读取模板缓冲区中该片元位置的模板值,然后将它和读取到的参考值进行比较(可由开发者指定)。若片元没通过测试,就会被舍弃
- 不管有没通过测试,我们都可以根据模板测试和深度测试结果来修改模板缓冲区(由开发者指定)
深度测试

- 深度比较函数可由开发者指定
- 与模板测试不同,若片元没通过测试,它就没权利更改深度缓冲区中的值
合并

- 使用混合函数进行混合(可由开发者指定)
- 通常和透明通道息息相关(相加、相减、相乘等)
补充
- 测试顺序并不是唯一
- 大多数 GPU 会尽可能在执行片元着色器之前就进行这些测试,以提升性能
- Unity 中的
Early-Z
技术:在片元着色器之前进行深度测试 - 透明度测试提前会导致一系列问题,所以它会导致性能下降
- Unity 中的
- 为了避免我们看到正在进行光栅化的图元(未处理完成),GPU 会使用
双重缓冲
。即对场景的渲染是在幕后发生的
2.4 一些容易困惑的地方
2.4.1 什么是 OpenGL/DirectX
都是图像编程接口,架起了上层应用程序和底层 GPU 的沟通桥梁,以便开发者操作 GPU

- 开发者直接访问 GPU 是一件非常麻烦的事,而图像编程接口在这些硬件的基础上实现了一层抽象,提供了一系列操作 GPU 的接口
- 对显卡来说,它只需要和显卡驱动打交道就可以了
- 一个显卡厂商若想同时和 OpenGL、DirectX 合作,就必须提供两种接口的驱动
2.4.2 什么是 HLSL、GLSL、CG
都是着色器编程语言
- 会被编译为与机器无关的汇编语言(中间语言 IL),再交给显卡驱动翻译为真正的机器语言
- GLSL 跨平台,但没提供着色器编译器,得依靠显卡驱动来完成编译,风险在于各显卡厂商的编译结果不一致
- HLSL 由微软控制着色器的编译,在不同硬件上的编译结果是一样的。所以支持 HLSL 的平台相对较少,因为其他平台上没有编译器
- CG 是真正意义上的跨平台(与微软合作),缺点是可能无法完全发挥出 OpenGL 的最新特性
2.4.3 什么是 Draw Call
就是 CPU 调用图像编程接口,以命令 GPU 进行渲染操作
- Draw Call 造成性能问题的元凶其实是 CPU
问题一:CPU 和 GPU 如何实现并行工作?
- 命令缓冲区。CPU 往里面添加,GPU 从中提取,互相独立工作
- 命令类型:
- Draw Call 是其中一种
- 改变渲染状态
- 资源绑定
- 其他
- 清理缓冲区
- 执行计算着色器
- 同步(CPU 与 GPU)
- 复制数据(GPU 内部)
问题二:为什么 Draw Call 多了会影响帧率
- 每次 Draw Call 前,CPU 需要
- 向 GPU 发送内容:数据、状态和命令等
- 工作:检查渲染状态等
- GPU 渲染能力很强,速度往往快于 CPU 提交命令的速度
问题三:如何减少 Draw Call?
- 批处理(Batching)
- 把很多小的 Draw Call 合并成一个大的
- 更加适合合并静态物体
- 处理动态物体的话,每帧都需要重新合并,对空间和时间都会造成影响
- 注意
- 避免使用大量很小的网格。若需要使用,考虑合并它们
- 避免使用过多的材质。尽量共用一个材质