从现代视角审视统一内存架构(UMA)—— (3) PC 架构的演进 - 续
概述:所谓 3D 渲染,本质上就是将一份存储在
.obj文件(或任何二进制资产)里的三维坐标矢量数组,通过一轮由线性代数刮起的数学大风暴,强行拍扁成你屏幕上那层二维像素格的物理过程。在现代商业引擎和图形 API 动辄将底层逻辑重重包裹的今天,我们往往忘记了 3D 渲染最原始的面貌。为了彻底扒掉这些华丽的伪装,看清在“硬件加速”诞生前夕,PC 架构是如何在算力与传输的夹缝中艰难进化的,本期文章将彻底抛弃任何第三方黑盒。我们将直接把一台 1990 年代末的 PC 硬件撕开一扇物理切口,用纯粹的 C++ 源码、三角函数和透视降维公式,手撸一个最原始的 3D 线框渲染流水线,并在总线级别对其进行一场冷酷的底层审计。你会看到,在这场看似流畅的画面背后,高性能 CPU 是如何沦为跨区搬运像素的“物流卡车”,而整根主板总线又是如何沦为惨烈的“数据绞肉机”的。⚠️ 极客硬核警告:
接下来的篇幅由于涉及直视底层的物理内存映射、定点/浮点变换、以及高频的透视除法运算,内容将极度烧脑。我们要求你必须具备扎实的 C/C++ 语言核心基础与基本的线性代数常识。如果你只习惯于在高级引擎里点击“运行”按钮,这篇充满冷酷机器逻辑的文章可能会让你感到极其不适;但如果你渴望在字节级看清 3D 图形诞生的第一条工业流水线,那么请端起你的 C++ 编译器,随我一起走进这场纯软件渲染的代数现场。
- 纯软渲染时代(原版雷神之锤 1):CPU 是个苦力。它不仅要算上面的数学公式,还要用
while循环去计算屏幕上几十万个像素点的死活,逐个字节去擦写内存。 - 3D 加速卡时代(经典的 Voodoo 巫毒卡时代 / 雷神之锤 2):Voodoo 卡只干一件事——硬件光栅化。CPU 负责算好顶点的 2D 坐标后,直接丢给 Voodoo 卡,显卡用硬件电路去暴力填色。
- 现代 GPU 时代(GeForce 256 至今):显卡演变成了 GPU。不仅光栅化,连顶点旋转、投影、灯光数学计算,全被显卡芯片硬件化了。
为了把上述考古理论具象化,我用 Qt C++ 搭建了一个完全不依赖任何第三方 3D 库(如 OpenGL/Direct3D)的极简软几何引擎。它从最原始的 OBJ 3D 数据开始,只用 CPU 跑完了以下三层最纯粹的流水线。
首先要知道 .OBJ 文件本质上是一大堆的顶点数据,我需要加载这个文件的数据,代码如下:
// 手工构建正方体的顶点和边,这在现代就是 OBJ 文件的底表数据 void MyCubeWidget::loadCubeModel() { // 8 个顶点的 3D 空间坐标 X, Y, Z m_vertices.append(QVector3D(-1.0, -1.0, 1.0)); // ... 省略其他顶点 ... // 12 条边(存储的是顶点数组的索引,从 0 开始) m_edges.append({0, 1}); m_edges.append({1, 2}); // ... }
这里为了简化讲解,读取文件被我阉割掉了,我先写死在代码中,之后需要我把OBJ文件加载的内容扩充进来的话,请在留言区给我发消息。
怎么让这个死板的数据在屏幕上转起来?秘密全在 paintEvent 每一帧的数学公式里。我没有使用现代 3D 的复合矩阵,而是把卡马克当年的底层单步计算拆开,让读者能一眼看懂几何拉扯的本质:
- 缩放 (Scale):把 (X,Y,Z) 乘以系数,改变物体体积。
看看代码的实现:
QVector3D MyCubeWidget::scale(const QVector3D &p, double s) { return QVector3D(p.x() * s, p.y() * s, p.z() * s); }
解析:改变物体在虚拟 3D 世界中的原始体积就把 3D 顶点的 \(X, Y, Z\) 坐标同时乘以一个倍数 s,导演觉得正方体太大了,命令道具组:“把这个正方体整体缩小到原来的 0.08
rotateXZ—— 道具原地旋转(旋转)
观察rotateXZ函数是如何写的:
// 绕 Y 轴旋转的底层数学实现,利用三角函数重新计算 X 和 Z QVector3D MyCubeWidget::rotateXZ(const QVector3D &p, double angle) { double c = std::cos(angle); double s = std::sin(angle); return QVector3D(p.x() * c - p.z() * s, p.y(), p.x() * s + p.z() * c); }
解析: 它让顶点绕着 (Y) 轴(垂直轴)旋转一个角度。它通过三角函数 sin 和 cos 去重新计算顶点在 x 轴和 z 轴(前后左右)上的新位置,而 y 轴(上下)保持不变。就像一个道具被放在了一个旋转舞台的底盘上。随着时间推移,底盘不停转动,正方体的各个角也跟着在空中画圈。所以它赋予物体动态的生命,是动画效果的来源。
- 平移 (Translate):在 (Z) 轴加上一个位移量(如 (+2.5)),把物体从镜头贴脸处往舞台深处推开。如果不推开,除以 (Z) 就会触发除以 0 的系统崩溃!
接下来看看Translate_y和Translate_z代码的作用,他们看起来很简单:
QVector3D MyCubeWidget::translateZ(const QVector3D &p, double dz) { return QVector3D(p.x(), p.y(), p.z() + dz); } QVector3D MyCubeWidget::translateY(const QVector3D &p, double dy) { return QVector3D(p.x(), p.y() + dy, p.z()); }
解析: 在图形学中 Z 轴通常代表深度,即离相机的远近,这两个函数通过改变 Y和Z轴的坐标来让物体上下和前后移动。translateY 是把道具往舞台上方吊起一点,或者往地板沉下一极。translateZ(p, 2.5) 是至关重要的一步——把道具从你的相机镜头贴脸上,往舞台深处往后推 2.5 个单位。如果不推开,道具就在相机镜头内部,你什么也拍不到(除以 Z 就会除以 0 导致崩溃)。
- 最终拍板 project:它实现近大远小的视觉魔法。它的核心操作是:用顶点的 X 和 Y 坐标分别除以它当前的 Z 坐标(距离),但代码的实现却极其简单:
// 核心:透视投影(近大远小) QVector3D MyCubeWidget::project(const QVector3D &p) { // 避免除以 0 导致程序崩溃 double z = (p.z() != 0.0) ? p.z() : 0.001; return QVector3D(p.x() / z, p.y() / z, p.z()); }
解析: 这是 3D 变 2D 的惊天一剪。经过这个函数后,虽然返回的依然是
QVector3D,但它的 X 和Y 已经缩放到了数学上的 [-1, 1] 虚拟视口范围内,具备了真实的立体透视感。就像你拿起相机按下快门。根据物理光学,离你近的角 Z 变小,除以 Z 后的数字就变大,在照片上显得大;离你远的角 Z 变大,除以 Z 后的数字就变小,在照片上显得小。
CPU 从文件里读出正方体 8 个顶点的原始 3D 坐标 X, Y, Z)。每当定时器刷新,CPU 会在内存里对这 8 个点进行疯狂的数学大清洗,在渲染流程中发生了几件重要的事情,如下图:
为了让大家最直观地看清上面这套极简软几何引擎在每一帧(每 16 毫秒)渲染时发生的底层数据流动,我们用一张极简的流水线图来做个复盘:
painter.drawLine。在没有显卡 API 参与的纯软件渲染时代,这个函数在底层其实是一个 CPU 亲自数格子的死循环。CPU 必须在系统内存(RAM)里开辟的一块缓冲区中,把线条经过的所有像素点,一个字节一个字节地涂满颜色。现在,让我们开启上帝视角,进行一次机器级别的底层审计:在这套纯软件流水线中,CPU 既要当计算坐标的“数学家”,又要当逐个像素刷墙的“泥瓦匠”,最后还要当跨区搬运像素的“物流大卡车”。我们现在只画了一个包含 8 个顶点、12 条边的简单正方体,总线尚且能靠每秒搬运几十兆的数据苟延残喘。但请极客们发挥想象力:如果是拥有上千个多边形、并且需要引入法线光照(Normal Lighting)进行逐像素复杂光影对撞、或者需要让角色展现出骨骼形变的商业大作呢?如果继续沿用这种“CPU 算全屏像素,总线运全屏像素”的土豪方案,随着分辨率和模型精度的提升,主板上的物理总线会瞬间像早高峰的单行道一样被彻底卡死。于是,整个图形学行业愤怒了。为了砸碎这堵无形的总线之墙,1990 年代末,OpenGL 硬件加速时代应运而生。行业做出了一个违背祖宗的决定:彻底没收 CPU 刷墙填色的权力!显卡接管了流水线中的第 3 步(硬件光栅化)。CPU 删掉了所有“画线、刷像素”的臃肿代码,它只需要在内存里算完顶点的最新 \((X, Y, Z)\) 坐标,然后通过总线仅仅传输这几个点的float标量数组。数据吞吐量瞬间从几百 KB 的像素死尸,暴跌到了几十个字节的顶点坐标!剩下的填色和光栅化,全部由显卡(如经典的 Voodoo 巫毒卡)在本地显存里自己完成。这一场“只传顶点、不传像素”的权力移交,让 PC 终于迎来了 3D 渲染的第一个辉煌时代。然而,历史的戏剧性就在于,当多边形阵营还没来得及欢呼,随着骨骼动画(Skeletal Animation)对顶点数量的指数级暴增,这条看似解脱的顶点传输公路,很快又将迎来它的第二次噩梦……
下一期,我们将正式剥开《半条命》的‘虚拟白骨’,看看 CPU 是如何在浮点矩阵的暴政下沦为显卡长途喂饭的囚徒的。 顺便,如果大家对《DOOM》如何在 486 时代用一棵冷酷的 BSP 树算死宇宙因果、强行榨干带宽感兴趣,扣个 1,我后面单独为这棵‘树’写篇硬核专题
posted on 2026-05-18 17:10 P_P_thoughts 阅读(0) 评论(0) 收藏 举报
浙公网安备 33010602011771号