渲染流水线的工作任务
根据一个三维场景,生成一张二维图像
从一系列的顶点数据、纹理等信息出发,将这些信息转换为一张人眼可见的图像
该工作由CPU和GPU共同完成
渲染流程可从概念上分为3个阶段
应用阶段
在CPU中进行(该阶段由开发者控制)
任务
准备好场景数据
- 摄像机的位置
- 视锥体
- 场景中包含的模型
- 场景中使用的光源
粗粒度剔除
- 剔除不可见的物体(避免在下一阶段时再处理)
- 为了提高渲染性能
设置各模型的渲染状态
- 使用的材质(漫反射颜色、高光反射颜色)
- 使用的纹理
- 使用的Shader
输出
渲染所需的几何信息(渲染图元)
- 点、线、三角面
几何阶段
在GPU中进行
任务
对各渲染图元进行处理,将顶点坐标变换到屏幕空间中
输出
屏幕空间的二维顶点坐标、各顶点的深度值和着色等信息
光栅化阶段
在GPU中进行
任务
决定各渲染图元中哪些像素应被绘制到屏幕上(对各顶点数据进行插值后,逐像素处理)
输出
最终的二维图像
真正用于实现的渲染流水线
应用阶段
将数据加载到显存
所有渲染所需的数据都需从硬盘加载到内存中,然后,网格和纹理等数据需加载到显存(显卡的存储空间)中
因为
- 显卡访问显存比访问内存快
- 大多数显卡无权直接访问内存
需要加载到显存的数据
- 顶点的位置信息、法线方向、颜色
- 纹理坐标
对于加载到显存的数据,如果CPU无需访问(有时CPU需要访问网格数据进行碰撞检测,这种情况不能删除内存中的该数据,因为将数据从硬盘加载到内存很耗时),则可以删掉内存中的数据
设置渲染状态
渲染状态定义了场景中的网格如何(使用哪个着色器,光源属性,材质)被渲染
CPU设置渲染状态来指导GPU渲染
CPU通过调用渲染命令DrawCall来通知GPU
调用Draw Call
该命令由CPU发起,由GPU接收,指向一个需被渲染的图元列表
GPU根据渲染状态和顶点数据进行计算,输出成在屏幕上显示的像素
该计算过程就是GPU流水线(几何阶段和光栅化阶段)
几何阶段
顶点着色器
完全可编程
输入进来的每个顶点都会调用一次顶点着色器
顶点着色器无法创建或销毁顶点,无法得到顶点间关系(因顶点相互独立,所以可并行处理多个顶点)
可修改顶点坐标和颜色,模拟水面或布料等
顶点着色器必须要将顶点坐标从模型空间转换到齐次裁剪空间(最基本工作)
顶点着色器可将数据经光栅化后交给片元着色器处理,在现代Shader Model中,可将数据发送给曲面细分着色器或几何着色器
曲面细分着色器
非必须执行
细分图元
几何着色器
非必须执行
逐图元地着色或产生更多图元
裁剪
不可编程(是硬件上的固定操作),可配置(自定义裁剪平面来配置裁剪区域,控制裁剪三角图元的正或反面)
将不在摄像机视野内的顶点裁剪掉并剔除某些三角图元的面片
图元与摄像机视野的关系有3种
- 完全在视野内
传递给下一阶段 - 部分在视野内
进行裁剪操作 - 完全在视野外
无需传递给下一阶段(无需被渲染)
屏幕映射
不可配置,不可编程
将各图元坐标(三维坐标)转换到屏幕坐标系(二维坐标)
OpenGL的最小窗口坐标为左下角
DirectX的最小窗口坐标为左上角
光栅化阶段
计算各图元覆盖了哪些像素,计算这些像素的颜色
三角形设置
固定函数阶段
计算三角网格表示数据的过程
根据顶点数据算出边界各像素的数据,使用算得的数据表示三角形边界
判断一个三角网格覆盖了哪些像素,根据顶点信息对覆盖区域的像素进行插值,得到一个片元序列(片元不是像素,包含了许多用于计算像素最终颜色的状态,如坐标、深度、纹理、法线等)
三角形遍历(扫描变换)
固定函数阶段
计算三角网格表示数据的过程
根据顶点数据算出边界各像素的数据,使用算得的数据表示三角形边界
判断一个三角网格覆盖了哪些像素,根据顶点信息对覆盖区域的像素进行插值,得到一个片元序列(片元不是像素,包含了许多用于计算像素最终颜色的状态,如坐标、深度、纹理、法线等)
片元着色器(在DirectX中叫 像素着色器)
完全可编程
逐片元地着色,仅影响单个片元,但可访问导数信息
根据输入的顶点信息进行插值,得到各片元的信息
逐片元操作
不可编程,高度可配置(设置每一步的操作细节)
通过进行深度测试和模板测试等来决定片元的可见性(是否允许其与颜色缓冲区合并)
模板测试
用于限制渲染区域、渲染阴影、轮廓渲染等
读取模板缓冲区中该片元位置的模板值,与读取到的参考值进行比较,根据比较结果修改模板缓冲区,并决定是否舍弃该片元
深度测试
将该片元的深度值与深度缓冲区中的深度值进行比较,决定是否舍弃该片元
未通过测试的片元无权修改深度缓冲区的值,可使用通过测试的片元的深度值覆盖原深度值(也可不覆盖)
可通过Early-Z技术将深度测试提前到片元着色器之前(但有时需在片元着色器阶段判断是否要舍弃片元)
合并
对通过了所有测试的片元,将其颜色值与已存储在颜色缓冲区中的颜色进行合并(混合)
决定是使用本次渲染得到的颜色完全覆盖掉之前的结果还是进行其他处理
屏幕图像
概念
OpenGL/DirectX
直接访问GPU很麻烦,麻烦体现在需要与各种寄存器和显存打交道
为了消除麻烦,在硬件基础上实现一层抽象,即图像编程接口(OpenGL和DirectX)
应用程序向接口发送渲染命令,接口向显卡驱动发送渲染命令,显卡驱动将命令翻译成GPU能理解的代码,进行绘制
应用程序 -> 图像编程接口 -> 显卡驱动 -> 显存和GPU
HLSL/GLSL/CG
GLSL:OpenGL,OpenGL Shading Language,可在多平台工作(原因是OpenGL未提供着色器编译器,由显卡驱动来完成着色器编译工作,因此只要显卡驱动支持该语言的编译即可),依赖硬件,导致编译结果不一致
HLSL:DirectX,High Level Shading Language,微软控制着色器编译,与硬件无关,仅支持微软平台
CG:NVIDIA,C for Graphic,与HLSL极相像,根据平台不同可编译成对应的中间语言(跨平台)
Draw Call
CPU调用图像编程接口,命令GPU进行渲染
CPU和GPU如何并行工作
使用命令缓冲区,使二者并行工作
缓冲区中包含一个命令队列
- CPU添加指令
- GPU读取指令
二者相互独立工作
Draw Call多了影响帧率
渲染速度快于提交命令的速度,过多的Draw Call导致CPU花费大量时间用在提交Draw Call上,造成CPU过载
如何减少Draw Call
批处理(将许多小Draw Call合并为一个大Draw Call)
适用于静态物体
- 避免使用大量小的网格(考虑合并)
- 避免使用过多材质(共用材质)
固定管线渲染
开发者只能进行配置操作(无法完全控制)
将该管线想象成一个控制电路,开发者只能设置电路中各开关的状态,无法控制电路排布
已退出历史舞台(被可编程渲染管线取代)
Shader(着色器)
GPU流水线中一些可高度编程的阶段,由着色器编译出来的最终代码在GPU上运行
有特定类型:顶点着色器,片元着色器
可通过着色器控制流水线的渲染细节
- 顶点着色器 -> 顶点变换和传递数据
- 片元着色器 -> 逐像素渲染