开场

图形系统的目标是把应用程序给出的场景描述转换成屏幕上的图像。应用程序通常负责指定物体、材质、观察者和光源;底层系统负责把这些描述一步一步处理成像素。

图形管线(Graphics Pipeline)就是这种处理流程的核心抽象。它把复杂的成像过程拆成多个阶段,每个阶段接收上一阶段的输出,再生成下一阶段需要的数据。现代图形硬件可以把许多阶段放到 GPU 上并行执行,因此它特别适合实时渲染。

图形系统和 API

应用程序不应该直接控制图形硬件的每个细节。更合理的方式是通过应用程序编程接口(Application Programming Interface,API)描述“要画什么”,由图形系统决定“怎么画”。

flowchart LR A[图形应用程序] --> B[图形 API] B --> C[图形管线] C --> D[帧缓冲区] D --> E[屏幕显示]

API 通常提供几类信息的入口。

信息作用
物体指定点、线段、多边形、曲线或曲面
观察者指定相机位置、朝向、视口和投影方式
光源指定光的位置、方向、颜色和强度
材质指定表面对光的吸收、漫反射、镜面反射等性质
输入与系统能力处理鼠标、键盘、显示设备和硬件功能

这种分层让应用开发者专注于场景组织,也让图形系统可以独立优化底层执行方式。

两种成像思路

从物理角度看,可以用光线追踪(Ray Tracing)模拟成像。它从投影中心或相机出发追踪光线,判断光线碰到哪些物体、发生反射还是折射,最后计算颜色。光线追踪适合处理全局效果,例如多次反射、透明物体和复杂阴影,但计算成本较高,而且通常需要在计算时访问完整场景数据。

实时图形系统常用另一种更实用的思路:按照应用程序生成物体的顺序,一个物体接一个物体处理。它通常重点处理局部光照,不完整模拟物体之间的复杂光传输。这个取舍牺牲了一部分物理真实性,但换来了更稳定的实时性能。

管线总览

一个基本图形管线可以概括成以下阶段。

flowchart LR A[顶点数据] --> B[顶点处理] B --> C[裁剪与图元组装] C --> D[光栅化] D --> E[片元处理] E --> F[像素]

每个阶段处理的数据粒度不同。顶点处理关注点的位置和属性;图元组装把点组织成线段、多边形、曲线或曲面;光栅化把几何对象覆盖到像素网格上;片元处理决定每个候选像素最终写入什么颜色。

顶点处理

顶点处理(Vertex Processing)的大量工作是坐标变换。一个点在建模时通常先处在物体自己的局部坐标系中;进入场景后,需要转换到世界坐标系;相机观察时,又需要转换到相机坐标系;最后投影到屏幕坐标系。

flowchart LR A[物体坐标] -->|模型变换| B[世界坐标] B -->|观察变换| C[相机坐标] C -->|投影变换| D[裁剪坐标] D -->|视口变换| E[屏幕坐标]

这些坐标变化通常都可以表示成矩阵变换。矩阵形式的好处是统一、可组合,并且非常适合 GPU 并行计算。顶点处理阶段还可以计算或传递顶点颜色、法向量、纹理坐标等属性。

投影

投影(Projection)把三维观察者和三维物体结合起来,生成二维图像所需的位置关系。常见投影可以分成透视投影(Perspective Projection)和平行投影(Parallel Projection)。

投影方式几何特征常见效果
透视投影投影线汇聚到投影中心近大远小,接近人眼和相机效果
平行投影投影线彼此平行保留平行关系,常用于工程和技术图

透视投影更适合表达真实视觉经验;平行投影更适合强调尺寸和结构关系。两者都不是“更正确”的唯一选择,而是服务于不同任务。

图元组装和裁剪

图元组装(Primitive Assembly)把顶点收集成几何对象。API 传入的顶点本身只是点的位置和属性,只有按照指定规则组合后,系统才知道它们构成线段、三角形、多边形、曲线还是曲面。

裁剪(Clipping)处理的是可见范围问题。真实相机不能看到整个世界,虚拟相机也只能看到视锥体或观察体积内的对象。完全在可见范围外的对象会被移除;部分在范围内的对象需要被切割,只保留可见部分。

flowchart LR A[顶点] --> B[组装成图元] B --> C{是否进入观察体积} C -->|完全在外| D[丢弃] C -->|完全在内| E[保留] C -->|部分相交| F[裁剪后保留]

裁剪的目的不是改变物体本身,而是限制后续阶段只处理相机可能看到的部分,从而避免无意义计算。

光栅化

光栅化(Rasterization)把几何图元转换成片元(Fragment)。片元可以理解为“潜在像素”:它有帧缓冲区中的位置,也带有颜色、深度、纹理坐标等属性,但它还不一定会成为最终像素。

例如,一个三角形投影到屏幕后,会覆盖一片像素位置。光栅化阶段要判断哪些像素位置被这个三角形覆盖,并把顶点属性插值到内部位置。后续学习重心坐标时,会更清楚地看到这种插值是如何计算的。

片元处理和深度缓冲

片元处理(Fragment Processing)负责决定片元最终的颜色。颜色可以来自顶点颜色插值,也可以来自纹理映射(Texture Mapping)和光照计算。

但是,一个片元覆盖某个屏幕位置,并不意味着它会成为最终可见像素。场景中可能有多个物体投影到同一个像素位置,离相机更近的片元应该挡住更远的片元。深度缓冲(Depth Buffer 或 Z-buffer)就是为了解决这个问题。

flowchart TB A[新片元] --> B[读取当前像素深度] B --> C{新片元是否更近} C -->|是| D[更新颜色缓冲和深度缓冲] C -->|否| E[丢弃片元]

深度缓冲通常为每个像素保存一个深度值。片元处理时,系统比较新片元深度和已有深度,只有更接近相机的片元才会更新颜色。

纹理映射

纹理映射(Texture Mapping)把二维图像贴到三维几何表面上。几何模型提供形状,纹理提供表面细节。这样可以在不显著增加几何复杂度的情况下,让物体看起来更丰富。

一个模型可以只有较少的多边形,但通过纹理表现颜色变化、图案、污渍、木纹或鳞片等细节。实际渲染时,顶点会携带纹理坐标,光栅化阶段把纹理坐标插值到片元位置,片元处理阶段再根据纹理坐标读取纹理颜色。

可编程管线

早期图形管线的很多操作是固定的,称为不可编程管线(Non-programmable Pipeline)。这种设计适合硬件资源有限的时代,因为数据通常按简单统一的方式处理,但它限制了开发者表达复杂效果的能力。

现代图形管线把顶点处理器和片元处理器开放给程序控制,形成可编程管线(Programmable Pipeline)。顶点着色器(Vertex Shader)可以改变顶点位置和顶点属性;片元着色器(Fragment Shader)可以按片元计算纹理、光照和材质效果。

flowchart LR A[应用程序数据] --> B[顶点着色器] B --> C[固定功能阶段
组装、裁剪、光栅化] C --> D[片元着色器] D --> E[输出合成] E --> F[屏幕]

可编程管线并没有取消管线结构,而是在关键阶段给开发者更多控制权。实时渲染中的许多视觉效果,例如位移映射、逐片元光照、风格化渲染和后处理,都依赖这种可编程能力。

小结

图形管线把三维场景生成二维图像的过程拆成一组明确阶段。应用程序通过 API 指定对象、相机、光源和材质;管线依次完成坐标变换、投影、图元组装、裁剪、光栅化、片元处理和深度测试。理解这条链路后,后续学习几何、光照、纹理和着色器时,就能知道每个概念在系统中的位置。

返回目录