1 前言
在计算机图形学中,渲染是根据模型描述在显示器上生成图像的过程。3D图形渲染管线输入根据图元顶点(如三角形、点、线和四边形)对3D模型的描述,并为显示器上的像素生成颜色值。
如下图所示的是3D图形渲染管线的流程。
3D图形渲染管线主要包含以下几个主要阶段:
- Vertex Processing:处理顶点,即对每一个顶点做变换;
- Rasterization:光栅化,即将每一个图元转换为片元。片元可以被认为是3D空间中的像素,它与像素网格对齐,具有位置、颜色、法线和纹理等属性;
- Fragment Processing:对每一个片元进行处理;
- Output Merging:将所有在3D空间的片元合并成2D彩色像素以便显示。
在现代GPU中,Vertex Processing和Fragment Processing是可编程的,我们可以编写顶点着色器和片元着色器的程序来执行对顶点和片元的自定义变换。而Rasterization和Output Merging是不可编程的,是通过向GPU发出配置命令进行配置。
2 顶点、图元、片元和像素
先讲一下顶点、图元、片元和像素这四个概念以及之间的关系。
这四者的生成是有先后顺序的:顶点>图元>片元>像素,生成顺序示意图如下图所示。
- 顶点:最小的单位
- 图元:由顶点组成的。一个顶点,一条线段,一个三角形或者多边形都可以成为图元
- 片元:在图元经过光栅化阶段后,被分割成一个个像素大小的基本单位。片元其实已经很接近像素了,但是它还不是像素。片元包含了比RGBA更多的信息,比如可能有深度值,法线,纹理坐标等等信息。片元需要在通过一些测试(如深度测试)后才会最终成为像素。可能会有多个片元竞争同一个像素,而这些测试会最终筛选出一个合适的片元,丢弃法线和纹理坐标等不需要的信息后,成为像素。
- 像素:最终呈现在屏幕上的包含RGBA值的图像最小单元
以上是生成顺序的一个大致流程,大家也对这四个概念之间的关系有了一个初步的了解。现在具体说明一下。
2.1 顶点
在计算机图形学中,顶点具有以下属性:
- 3D 空间中的位置 V=(x, y, z):通常用浮点数表示。
- 颜色:以 RGB(红-绿-蓝)或 RGBA(红-绿-蓝-Alpha)分量表示。 分量值通常归一化到 0.0 到 1.0 的范围(或 0 到 255 之间的 8 位无符号整数),alpha 用于指定透明度,alpha 为 0 表示完全透明,alpha 为 1 表示不透明。
- Vertex-Normal N=(nx, ny, nz):我们熟悉表面法线的概念,法线向量垂直于表面。 然而,在计算机图形学中,我们需要为每个顶点附加一个法线向量,称为顶点法线。 法线用于区分正面和背面,以及用于其他处理,例如照明。 在 OpenGL 中使用右手法则(或逆时针方向)。法线指向外,表示外表面(或正面)。
- 纹理 T=(s, t):在计算机图形学中,我们经常将 2D 图像包裹到物体上,以使其看起来逼真。 一个顶点可以有一个 2D 纹理坐标 (s, t),它提供了一个到 2D 纹理图像的参考点。
2.2 图元
在OpenGL中,它是支持三类几何图元:点、线段和闭合多边形。 它们是通过顶点指定的,每个顶点都与其属性相关联,例如位置、颜色、法线和纹理。 如图所示,OpenGL 提供了 10 个图元。
2.3 片元
在由顶点组成的图元中,其通常以浮点值表示的顶点不一定与显示器的像素网格对齐。因此在光栅化阶段,对由一个或多个顶点定义的每个图元进行光栅扫描,得到一组包含在图元中的片元,片元可以被视为与像素网格对齐的3D像素,从顶点插值的3D片段具有与顶点相同的属性,例如位置、颜色、法线和纹理。
光栅化阶段包括视口变换、裁剪、透视划分、背面剔除和扫描转换。 光栅器不是可编程的,但可以通过指令进行配置。
光栅化阶段完成后,还需要对片元进行处理。
下面分别讲述光栅化阶段中的几个重要步骤和片元处理。
2.3.1 视口变换
视口是应用程序窗口上的一个矩形显示区域,以屏幕坐标(以像素为单位,原点在左上角)测量。 视口定义显示区域的大小和形状,以将相机捕获的投影场景映射到应用程序窗口。 它可能会或可能不会占据整个屏幕。
在OpenGL中,视口默认设置为覆盖整个应用程序窗口,我们可以使用 glViewport() 函数来选择较小的区域(例如,用于分屏或多屏应用程序)。
void glViewport(GLint xTopLeft, GLint yTopLeft, GLsizei width, GLsizei height)
我们还可以通过 glDepthRange() 设置视口的 z 范围
glDepthRange(GLint minZ, GLint maxZ)
视口变换,即将裁剪体积(2x2x1 长方体)映射到 3D 视口,如下图所示。它是由一系列仿射(y 轴)、缩放(x、y 和 z 轴)和平移(从裁剪体积的近平面中心到左上角的原点)组成 的 3D 视口。
如果视口覆盖整个屏幕,则 minX=minY=minZ=0,w=screenWidth 和 h=screenHeight。
Note:如果视口的纵横比和投影平面的纵横比不一样,形状就会发生变形。因此需要重新配置投影平面的纵横比以匹配视口的纵横比。
// Callback when the OpenGL's window is re-sized.
void reshape(GLsizei width, GLsizei height) { // GLsizei for non-negative integer
if (height == 0) height = 1; // To prevent divide by 0
GLfloat aspect = (GLfloat)width / (GLfloat)height; // Compute aspect ratio
// Set the viewport (display area on the window) to cover the whole application window
glViewport(0, 0, width, height);
// Adjust the aspect ratio of projection's clipping volume to match the viewport
glMatrixMode(GL_PROJECTION); // Select Projection matrix
glLoadIdentity(); // Reset the Projection matrix
// Either "perspective projection" or "orthographic projection", NOT both
// 3D Perspective Projection (fovy, aspect, zNear, zFar), relative to camera's eye position
gluPerspective(45.0, aspect, 0.1, 100.0);
// OR
// 3D Orthographic Projection (xLeft, xRight, yBottom, yTop, zNear, zFar),
// relative to camera's eye position.
if (width <= height) {
glOrtho(-1.0, 1.0, -1.0 / aspect, 1.0 / aspect, -1.0, 1.0); // aspect <= 1
} else {
glOrtho(-1.0 * aspect, 1.0 * aspect, -1.0, 1.0, -1.0, 1.0); // aspect > 1
}
// Reset the Model-View matrix
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
2.3.2 背面剔除
视锥剔除丢弃视锥外的对象,而背面剔除则丢弃不面向相机的图元。
可以根据法向量和连接表面和相机的向量来声明背面。
如果对象是透明的并且启用了 alpha 混合,则不应启用背面剔除。
2.3.3 片元处理
光栅化后,每个图元都有一组片元,一个片有一个位置,它与像素网格对齐,具有深度、颜色、法线和纹理坐标,这些坐标是从顶点插值得到的。
片元处理侧重于纹理和光照,这两者对图形的质量影响最大。片元处理器涉及的操作有纹理化、结合原色和二次色应用雾计算。
3 点操作
点操作对应前言部分图片中的Vertex Processing,点操作可细分为3个变换,如下图所示,输入原始的点,经过3次坐标变换后,每个顶点连同它们的顶点法线一起变换并定位在 裁剪体积长方体空间中,其中x和y坐标(在-1到+1的范围内)代表它在屏幕上的位置,z值(在0到1的范围内)代表它的深度,即离近平面有多远;再通过光栅化阶段的视口变换最终得到屏幕空间下的点,显示在显示器上。
以上坐标变换涉及到4个转换:
- 将物体/模型放在世界空间下,即模型变换或世界变换;
- 放置相机,即视图变换;
- 选择相机的镜头,调整焦距和变焦系数以设置相机的视野,即投影变换;
- 光栅化阶段,在指定区域将图显示出来,即视口变换。
3.1 模型变换/世界变换
3D 场景中的每个对象/模型通常在自己的坐标系中绘制,称为模型空间(或局部空间,或对象空间)。当我们组装对象时,我们需要将顶点从它们的局部空间转换到所有对象共有的世界空间,称为世界变换。 世界变换由一系列缩放(缩放对象以匹配世界的维度)、旋转(对齐轴)和平移(移动原点)组成。
旋转和缩放属于一类称为线性变换的变换(根据定义,线性变换保留向量加法和标量乘法)。 线性变换和平移形成所谓的仿射变换, 在仿射变换下,直线仍然是直线并且点之间的距离比率保持不变。
3.2 视图变换
世界变换后,所有的物体都被转换到世界空间中, 我们现在将放置相机以捕捉视图。
定位相机:
在 3D 图形中,我们通过指定三个视图参数将相机定位到世界空间:世界空间中的 EYE、AT 和 UP。
- 点 EYE (ex, ey, ez) 定义了相机的位置。
- 向量 AT (ax, ay, az) 表示相机瞄准的方向,通常是世界或物体的中心。
- 矢量 UP (ux, uy, uz) 粗略地表示相机的向上方向。 UP 通常与世界空间的 y 轴重合。 UP 与 AT 大致正交,但不是必需的。 由于 UP 和 AT 定义了一个平面,我们可以在相机空间中构造一个与 AT 正交的向量。
在OpenGL中,在默认参数下,相机位于原点 (0, 0, 0),对准屏幕(负 z 轴),并面朝上(正 y 轴)。 如果使用默认参数,则必须将对象/物体放置在负 z 值处。
Note:在计算机图形学中,相对于固定相机移动对象(模型变换)和相对于固定对象移动相机(视图变换)产生相同的图像,因此是等价的。 因此,OpenGL 在所谓的模型-视图矩阵上以相同的方式管理模型变换和视图变换,投影变换(后面会讲)通过投影矩阵进行管理。
3.3 投影变换
相机定位和定向后,我们需要决定它可以看到什么(类似于通过调整焦距和缩放系数来选择相机的视野),以及如何将对象/物体投影到屏幕上。 这是通过选择投影模式(透视或正交)并指定查看体积或裁剪体积来完成的。 裁剪体积之外的对象被裁剪出场景并且看不到。
3.3.1 透视投影
在透视图中查看视锥体,相机的视野有限,呈现视锥体(截头金字塔),并由四个参数指定:fovy、aspect、zNear 和 zFar。
- fovy:以度为单位指定总垂直视角。
- aspect:宽度与高度的比率。 对于特定的 z,我们可以从 fovy 获取高度,然后从 aspect 获取宽度。
- zNear; 近平面。
- zFar:远平面。
为方便起见,相机空间 (xc, yc, zc) 被重命名为熟悉的 (x, y, z)。
3.3.2 正交投影
除了常用的透视投影外,还有一种所谓的正交投影(或平行投影),这是一种特殊情况,相机放置在离世界很远的地方(类似于使用望远镜镜头)。 正交投影的视体积是平行六面体(而不是透视投影中的平截头体)。
4 总结
OpenGL渲染实际上就是将3D坐标进行系列转换变为2D坐标,整个流程如下:
除此之外,为了让最终结果和实际物体更接近, 会进行打光和贴纹理等操作。