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 坐标变换:从模型空间到屏幕空间

坐标变换是顶点处理里最绕的部分,但也是最有意思的。我刚开始学的时候,被各种空间搞晕过——模型空间、世界空间、观察空间、裁剪空间、屏幕空间……其实你只要记住一句话:每个空间都是为了解决一个特定问题而存在的

整个变换流程是这样的:

  1. 模型变换:把顶点从模型局部坐标,挪到世界坐标。说白了就是"把这个模型放到场景的哪个位置"。
  2. 视图变换:以摄像机为原点,重新计算所有顶点的位置。相当于"把摄像机移到原点,让世界围着它转"。
  3. 投影变换:把3D坐标压到裁剪空间里。透视投影会让物体近大远小,正交投影则保持大小不变。
  4. 视口变换:最后一步,把裁剪坐标映射到屏幕像素坐标。这一步通常是硬件自动完成的。

我建议你把这四个变换理解成"流水线上的四道工序"。每一道工序的输出,就是下一道的输入。我在项目中遇到过一个问题:模型显示出来是扁的。排查了半天,发现是投影矩阵的宽高比设错了,跟窗口的实际宽高比不一致。

关键点:投影矩阵的宽高比必须与视口的宽高比保持一致,否则画面会拉伸变形。

2.3 裁剪:把"看不见"的顶点扔掉

裁剪发生在投影变换之后。GPU会检查每个顶点是否在裁剪空间的可视范围内。这个范围通常是一个立方体,在OpenGL里叫"规范化的设备坐标"(NDC),范围是[-1, 1]。

为什么会需要裁剪?你想想看,一个模型可能有一部分在屏幕外面。如果不裁剪,GPU还得去计算那些根本看不见的像素,纯属浪费性能。裁剪算法最经典的是Sutherland-Hodgman算法,它会逐边地对多边形进行裁剪。

我记得有一次做地形渲染,场景特别大,远处的山体顶点数量惊人。如果不做裁剪,帧率直接掉到个位数。后来我优化了视锥体裁剪,在CPU端就先剔除掉不在视锥体内的物体,GPU的压力一下子就降下来了。

注意:裁剪是在顶点处理阶段完成的,但裁剪后的新顶点需要重新插值属性(比如颜色、纹理坐标)。这个插值过程由GPU自动完成,但如果你在顶点着色器里做了特殊计算,要确保插值结果正确。

2.4 背面剔除:省掉一半的工作量

背面剔除,说白了就是"不画你看不到的那一面"。一个三角形有正面和背面,如果背面正对着摄像机,那它肯定被正面挡住了,画了也是白画。

GPU怎么判断正面和背面?靠顶点的环绕顺序。如果顶点是按顺时针排列的,就是正面;逆时针就是背面(或者反过来,取决于你的设置)。

来看一个设置背面剔除的代码片段:

// 开启背面剔除
glEnable(GL_CULL_FACE);

// 设置剔除哪一面:GL_BACK 表示剔除背面
glCullFace(GL_BACK);

// 设置正面方向:GL_CCW 表示逆时针为正面
glFrontFace(GL_CCW);

我曾经在项目里遇到过一个诡异的bug:模型有些面消失了,有些面却显示出来了。查了半天,发现是建模软件导出的顶点顺序不一致,有的面是顺时针,有的面是逆时针。解决方案也很简单——要么统一顶点顺序,要么关闭背面剔除。

避坑指南:如果你在调试时发现模型"漏了"或者"穿了",先检查背面剔除是不是开错了。我一般会在开发阶段先关掉背面剔除,等模型显示正常了再打开。

2.5 总结一下

顶点处理这一套流程,其实就四个步骤:

步骤 做什么 输出空间
顶点着色器 计算顶点位置和属性 裁剪空间
坐标变换 模型→世界→观察→裁剪 裁剪空间
裁剪 剔除可视范围外的部分 NDC
背面剔除 剔除背对摄像机的面 屏幕空间

嗯,这一章的内容就到这里。顶点处理是整个渲染管线的基石,你把它搞透了,后面的光栅化、片段处理才能顺风顺水。下一章咱们聊聊光栅化,那可是把顶点变成像素的关键一步。