2、顶点处理详解:顶点着色器、坐标变换、裁剪与背面剔除
好,咱们接着聊顶点处理。这一块是渲染管线的第一道关卡,也是我当年入行时花了不少功夫才啃下来的硬骨头。说白了,顶点处理就是把3D模型里的那些点,变成屏幕上能显示的像素的起点。你想想看,一个模型动辄几万甚至几十万个顶点,每个顶点都要经过一套固定的流程,性能压力可不小。
2.1 顶点着色器:每个顶点的"私人定制"
顶点着色器,你可以把它理解成每个顶点的专属处理函数。GPU会为每个顶点独立调用一次这个程序。我在项目中遇到过不少新手,以为顶点着色器能处理像素级别的操作,其实它只关心顶点本身。
它的核心任务就两个:
- 计算顶点的最终位置(必须做)
- 传递或计算顶点相关的数据(比如颜色、法线、纹理坐标)
来看一个最简单的顶点着色器代码,我用的是类GLSL的写法:
#version 330 core
layout (location = 0) in vec3 aPos; // 顶点位置
layout (location = 1) in vec3 aColor; // 顶点颜色
out vec3 ourColor; // 传递给片段着色器
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
ourColor = aColor;
}
嗯,这里要注意gl_Position这个内置变量。你给这个变量赋值,就等于告诉GPU:"这个顶点在裁剪空间里的坐标就是它了。" 我曾经犯过一个低级错误——忘了把aPos转成vec4,结果编译死活过不去,查了半天才发现是维度不匹配。
2.2 坐标变换:从模型空间到屏幕空间
坐标变换是顶点处理里最绕的部分,但也是最有意思的。我刚开始学的时候,被各种空间搞晕过——模型空间、世界空间、观察空间、裁剪空间、屏幕空间……其实你只要记住一句话:每个空间都是为了解决一个特定问题而存在的。
整个变换流程是这样的:
- 模型变换:把顶点从模型局部坐标,挪到世界坐标。说白了就是"把这个模型放到场景的哪个位置"。
- 视图变换:以摄像机为原点,重新计算所有顶点的位置。相当于"把摄像机移到原点,让世界围着它转"。
- 投影变换:把3D坐标压到裁剪空间里。透视投影会让物体近大远小,正交投影则保持大小不变。
- 视口变换:最后一步,把裁剪坐标映射到屏幕像素坐标。这一步通常是硬件自动完成的。
我建议你把这四个变换理解成"流水线上的四道工序"。每一道工序的输出,就是下一道的输入。我在项目中遇到过一个问题:模型显示出来是扁的。排查了半天,发现是投影矩阵的宽高比设错了,跟窗口的实际宽高比不一致。
关键点:投影矩阵的宽高比必须与视口的宽高比保持一致,否则画面会拉伸变形。
2.3 裁剪:把"看不见"的顶点扔掉
裁剪发生在投影变换之后。GPU会检查每个顶点是否在裁剪空间的可视范围内。这个范围通常是一个立方体,在OpenGL里叫"规范化的设备坐标"(NDC),范围是[-1, 1]。
为什么会需要裁剪?你想想看,一个模型可能有一部分在屏幕外面。如果不裁剪,GPU还得去计算那些根本看不见的像素,纯属浪费性能。裁剪算法最经典的是Sutherland-Hodgman算法,它会逐边地对多边形进行裁剪。
我记得有一次做地形渲染,场景特别大,远处的山体顶点数量惊人。如果不做裁剪,帧率直接掉到个位数。后来我优化了视锥体裁剪,在CPU端就先剔除掉不在视锥体内的物体,GPU的压力一下子就降下来了。
2.4 背面剔除:省掉一半的工作量
背面剔除,说白了就是"不画你看不到的那一面"。一个三角形有正面和背面,如果背面正对着摄像机,那它肯定被正面挡住了,画了也是白画。
GPU怎么判断正面和背面?靠顶点的环绕顺序。如果顶点是按顺时针排列的,就是正面;逆时针就是背面(或者反过来,取决于你的设置)。
来看一个设置背面剔除的代码片段:
// 开启背面剔除
glEnable(GL_CULL_FACE);
// 设置剔除哪一面:GL_BACK 表示剔除背面
glCullFace(GL_BACK);
// 设置正面方向:GL_CCW 表示逆时针为正面
glFrontFace(GL_CCW);
我曾经在项目里遇到过一个诡异的bug:模型有些面消失了,有些面却显示出来了。查了半天,发现是建模软件导出的顶点顺序不一致,有的面是顺时针,有的面是逆时针。解决方案也很简单——要么统一顶点顺序,要么关闭背面剔除。
2.5 总结一下
顶点处理这一套流程,其实就四个步骤:
| 步骤 | 做什么 | 输出空间 |
|---|---|---|
| 顶点着色器 | 计算顶点位置和属性 | 裁剪空间 |
| 坐标变换 | 模型→世界→观察→裁剪 | 裁剪空间 |
| 裁剪 | 剔除可视范围外的部分 | NDC |
| 背面剔除 | 剔除背对摄像机的面 | 屏幕空间 |
嗯,这一章的内容就到这里。顶点处理是整个渲染管线的基石,你把它搞透了,后面的光栅化、片段处理才能顺风顺水。下一章咱们聊聊光栅化,那可是把顶点变成像素的关键一步。