什么是渲染流水线
渲染流水线的工作任务在于由一个三维场景出发、生成(或者说渲染)一张二维图像。换句话说,计算机需要从一系列的顶点数据、纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像。这个工作通常是由CPU和GPU共同完成的。(unity shader入门精要 冯乐乐 p6)
图出处Kerry佬
为什么要用渲染流水线
可以提高单位时间的生产量,提高生产率。
CPU端具体实现
渲染流水线的起点是CPU,即应用阶段。应用阶段大致可分为下面三个阶段:
- 把数据加载到显存中。
- 设置渲染状态。
- 调用DrawCall
把数据加载到显存中
我们渲染物体往往需要很多信息,这些数据需要从硬盘中加载到系统内存,然后,网格和纹理等信息又被加载到显存中,这样做的原因是因为大部分显卡对于内存没有直接的访问权利,且显卡对于显存的访问更快。 那么这么多数据,是不是所有的数据都是我们需要的呢? 我们知道,我们看到的图像,是在摄像机视锥体范围内的。如下图我们只能看到立方体,看不到猴头。
从这里看,猴头,也就是视锥体外的数据就可以不处理,这样子可以节省性能。 这步叫做视锥体剔除。
那么我们如何判断物体有没有在视锥体内呢?
我们需要判断物体有没有和视锥体相交,可是物体形状千奇百怪,还有些复杂的,那计算量就会很大,此时,往往采用物体的包围盒(bounding box),最常见的包围盒算法有AABB包围盒(Axis-aligned bounding box),包围球(Sphere), 方向包围盒OBB(Oriented bounding box)。
包围盒怎么求呢?
常用的是取所有点中最小的和最大的x,y,z作为包围盒的边界。如下是求三角形的平面包围盒,取得三角形三个点的x,y的最小值和最大值。
c++
// Bounding Box
int min_x = std::min(v[0].x(), std::min(v[1].x(), v[2].x())), min_y = std::min(v[0].y(), std::min(v[1].y(), v[2].y())), max_x = std::max(v[0].x(), std::max(v[1].x(), v[2].x())), max_y = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));
这块三维引擎往往会帮你处理好。
除了视锥体剔除,还有层级剔除等,层级剔除通过设置摄像机和物体的layer用来控制物体是否剔除。 THREEJS中的层级剔除
现代化图形api比如DX12,vulkan,WebGPU等还会有一个渲染队列,用来控制渲染顺序,来处理透明,不透明,半透明物体的渲染,Opengl的话没有自带,threejs中需要自己设置渲染顺序来达到类似效果。.renderOrder
渲染状态
渲染状态:定义了场景中的网格是怎样被渲染的。 比如顶点着色器,片元着色器,光源属性,材质等。 个人理解是把数据怎么组合。比如给三个mesh,用同一组材质。
调用DrawCall
Draw Call就是一个命令,它的发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元列表。给定了一个Draw Call时,GPU就会根据渲染状态和所有输入的顶点数据来进行计算,计算过程就是GPU流水线。
观察视锥体剔除和Drawcall
对比上述两张图,当两个立方体在视锥体内时,drawcall为4,当一个立方体移出视锥体外时,drawcall为3,可见视锥体剔除确实节省了性能。 github链接