1计算机图形学原理及实践:基础方法(第3版)

图形系统概述

图形学基础

图形硬件图形API渲染管线

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第1章 图形系统概述(Overview of Graphics Systems)

一、导读

1.1 本章学习目标

计算机图形学是研究如何用计算机生成、处理和显示图像的学科。从你正在阅读的这块屏幕上的每一个像素,到好莱坞大片中的震撼特效,从手机游戏中的精美角色,到建筑师手中的三维模型,计算机图形学无处不在。本章作为全书的开篇,将为你建立一个完整的图形系统认知框架,帮助你理解从代码到屏幕上的图像之间经历了怎样的过程。

本章的核心学习目标包括:

  • 理解计算机图形学的学科范畴:掌握图形学的核心问题域,区分实时渲染与离线渲染、光栅图形与矢量图形的本质差异。
  • 掌握图形硬件的基本架构:了解 GPU 的并行计算架构、显存组织方式、帧缓冲的概念,以及现代显卡如何从简单的像素输出设备演变为通用并行处理器。
  • 理解图形 API 的设计哲学:对比 OpenGL、DirectX、Vulkan、Metal 等主流图形 API 的设计理念,理解为什么 API 会经历从"立即模式"到"显式控制"的演进。
  • 掌握渲染管线的完整流程:从顶点数据输入到最终像素输出,理解每个阶段的功能和数据变换,包括顶点着色、图元装配、光栅化、片元着色、输出合并。
  • 理解图形程序的基本结构:掌握场景图的概念、渲染循环的工作原理、事件驱动模型在图形程序中的应用。
  • 建立对图形学发展历史的认识:从 Ivan Sutherland 的 Sketchpad 到现代实时光线追踪,了解图形学发展的关键里程碑。

1.2 为什么第一章如此重要

很多学习者急于跳入具体的渲染技术细节,觉得第一章"太泛"或"太基础"。这种想法是危险的。计算机图形学是一个高度系统化的学科,渲染管线中的每一个阶段都紧密关联,如果你不理解整个系统的工作流程,后续学习着色器编程、纹理映射、光照计算等具体技术时就会"只见树木,不见森林"。

本章的目标是让你建立一个完整的心理模型:当你编写一行 OpenGL 代码时,你知道这行代码最终会在 GPU 的哪个阶段被处理;当你设置一个纹理参数时,你理解这个参数如何影响最终的像素输出。这种系统级的理解是区分"图形 API 使用者"和"图形工程师"的关键。

此外,图形学是一个理论与实践高度结合的学科。没有理论指导的实践是盲目的——你不知道为什么某种技术有效,也无法在新场景中灵活运用;没有实践支撑的理论是空洞的——图形学的很多概念必须通过实际渲染才能直观理解。本章将为你搭建理论与实践之间的桥梁。

1.3 核心问题引导

在阅读本章时,请带着以下问题思考:

当程序调用 glDrawArrays() 时,从 CPU 到 GPU 到底发生了什么?数据经历了哪些变换才最终变成屏幕上的像素?

为什么现代 GPU 采用数千个小型计算核心的架构,而不是少数几个高性能核心?这种设计与图形渲染有什么内在联系?

OpenGL 的"状态机"设计有什么优缺点?为什么 Vulkan 选择了完全不同的"显式控制"设计理念?

渲染管线中的顶点处理和片元处理有什么本质区别?为什么 GPU 可以高效地并行处理它们?

实时渲染(如游戏)和离线渲染(如电影特效)的核心差异是什么?它们在技术选择上有什么根本不同?

从 Sketchpad 到现代 VR,图形学的发展遵循了什么样的技术演进规律?


二、核心概念详解

2.1 计算机图形学的学科范畴

计算机图形学(Computer Graphics, CG)是研究如何用计算机表示、生成、处理和显示视觉信息的学科。它的研究范围极为广泛,涵盖了从底层硬件设计到高层用户界面的多个层次。

2.1.1 图形学的核心问题域

图形学要解决的核心问题可以归纳为以下几类:

  • 建模(Modeling):用数学方式描述三维物体的形状和外观。包括多边形网格、参数曲面、隐式曲面、体数据等表示方法。
  • 渲染(Rendering):根据场景描述生成二维图像。这是图形学最核心的问题,包括光栅渲染和光线追踪两大范式。
  • 动画(Animation):描述物体随时间的变化。包括关键帧动画、物理模拟、骨骼动画、运动捕捉等。
  • 交互(Interaction):让用户与图形系统进行自然、高效的交互。包括拾取、相机控制、虚拟现实交互等。

2.1.2 实时渲染 vs 离线渲染

图形渲染按时间约束分为两大类:

实时渲染要求在严格的时间限制内(通常每帧 16.6ms 以达到 60fps)完成图像生成。应用场景包括电子游戏、虚拟现实、建筑可视化等。实时渲染必须在图像质量和性能之间做出权衡,通常采用光栅化作为核心渲染技术,辅以各种近似和加速手段。

离线渲染(也称预渲染)没有严格的时间限制,可以花费数分钟甚至数小时来渲染单帧图像。应用场景包括电影特效、产品可视化、建筑效果图等。离线渲染追求物理精确性,通常采用光线追踪或路径追踪等全局光照技术。

cpp
// 实时渲染的典型循环
void renderLoop() {
  while (!shouldClose()) {
    double startTime = glfwGetTime();

    processInput();
    updateScene();
    renderScene();

    glfwSwapBuffers(window);
    glfwPollEvents();

    double frameTime = glfwGetTime() - startTime;
    double targetFrameTime = 1.0 / 60.0;
    if (frameTime < targetFrameTime) {
      glfwWaitTimeout(targetFrameTime - frameTime);
    }
  }
}

2.2 图形硬件架构

2.2.1 GPU 的演进

图形处理器(GPU)的发展经历了几个重要阶段:

  • 固定功能阶段(1990s):GPU 只能执行预定义的渲染操作,程序员无法自定义光照和着色计算。代表产品有 S3 ViRGE、NVIDIA RIVA 128。
  • 可编程着色器阶段(2001-2006):NVIDIA GeForce 3 首次引入可编程顶点着色器,GeForce FX 引入可编程片元着色器。程序员可以用着色器语言自定义渲染效果。
  • 统一着色器架构(2006-至今):NVIDIA GeForce 8000 系列引入统一着色器架构,所有着色器类型共享相同的计算资源,极大地提高了硬件利用率。
  • 通用计算阶段(2007-至今):NVIDIA 推出 CUDA,AMD 推出 Stream,GPU 不再仅用于图形渲染,还成为通用并行计算的重要平台。
  • 实时光线追踪阶段(2018-至今):NVIDIA RTX 系列引入硬件级光线追踪加速,实时渲染和离线渲染的技术边界开始模糊。

2.2.2 GPU 的并行架构

现代 GPU 采用大规模并行的 SIMT(Single Instruction, Multiple Threads)架构。以 NVIDIA 的架构为例:

  • 流式多处理器(SM):GPU 由数十个 SM 组成,每个 SM 包含多个 CUDA 核心。
  • CUDA 核心:基本的浮点运算单元,执行实际的算术运算。
  • 线程束(Warp):32 个线程组成一个线程束,以锁步方式执行相同指令。
  • 显存控制器:管理 GPU 与显存之间的数据传输。

这种架构天然适合图形渲染的并行性:屏幕上的每个像素可以独立计算,数千个像素可以同时处理。

┌─────────────────────────────────────────────┐
│                    GPU                       │
│  ┌──────┐ ┌──────┐ ┌──────┐     ┌──────┐  │
│  │ SM 0 │ │ SM 1 │ │ SM 2 │ ... │ SM N │  │
│  │┌────┐│ │┌────┐│ │┌────┐│     │┌────┐│  │
│  ││Core││ ││Core││ ││Core││     ││Core││  │
│  ││Core││ ││Core││ ││Core││     ││Core││  │
│  ││Core││ ││Core││ ││Core││     ││Core││  │
│  ││Core││ ││Core││ ││Core││     ││Core││  │
│  │└────┘│ │└────┘│ │└────┘│     │└────┘│  │
│  └──────┘ └──────┘ └──────┘     └──────┘  │
│                                             │
│  ┌─────────────────────────────────────┐   │
│  │          显存控制器 (Memory I/F)      │   │
│  └─────────────────────────────────────┘   │
└─────────────────┬───────────────────────────┘
                  │
         ┌────────┴────────┐
         │     GDDR6/6X    │
         │    显存 (VRAM)   │
         └─────────────────┘

2.2.3 帧缓冲(Framebuffer)

帧缓冲是 GPU 中用于存储最终图像数据的内存区域。一个完整的帧缓冲包含多个附件:

  • 颜色缓冲(Color Buffer):存储每个像素的颜色值,通常是 RGBA 格式。双缓冲机制使用前台缓冲和后台缓冲交替显示,避免画面撕裂。
  • 深度缓冲(Depth Buffer / Z-Buffer):存储每个像素的深度值,用于深度测试,解决遮挡问题。
  • 模板缓冲(Stencil Buffer):存储整数值,用于遮罩操作,如镜面反射、阴影体等高级效果。
cpp
// 创建帧缓冲对象(FBO)
GLuint fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);

// 创建颜色附件
GLuint colorTexture;
glGenTextures(1, &colorTexture);
glBindTexture(GL_TEXTURE_2D, colorTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0,
             GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                       GL_TEXTURE_2D, colorTexture, 0);

// 创建深度附件
GLuint depthBuffer;
glGenRenderbuffers(1, &depthBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24,
                      width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                          GL_RENDERBUFFER, depthBuffer);

// 检查帧缓冲完整性
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
  // 帧缓冲配置错误
}

2.3 图形 API 的设计哲学

2.3.1 OpenGL:跨平台的经典

OpenGL(Open Graphics Library)是最早的跨平台图形 API 之一,由 Silicon Graphics(SGI)于 1992 年发布。它的设计哲学是提供一个与硬件无关的抽象接口,屏蔽不同 GPU 厂商的硬件差异。

OpenGL 的核心设计特征是状态机模型:程序维护一组状态变量(如当前颜色、当前矩阵、当前纹理等),后续的绘制命令使用这些状态。这种设计简单易用,但也带来了问题——状态切换开销不透明,容易导致性能问题。

cpp
// OpenGL 状态机示例
glBindVertexArray(vao);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);

// 所有状态已设置,执行绘制
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, nullptr);

OpenGL 的版本演进:

  • OpenGL 1.x:立即模式(glBegin/glEnd),固定管线
  • OpenGL 2.x:引入可编程着色器,但保留固定管线
  • OpenGL 3.x/4.x:废弃固定管线,全面转向可编程管线,引入核心模式(Core Profile)

2.3.2 Vulkan:显式控制的现代 API

Vulkan 于 2016 年由 Khronos Group 发布,是 OpenGL 的"精神继承者"。它的设计哲学与 OpenGL 截然不同:将控制权完全交给程序员。

Vulkan 的核心设计理念:

  • 显式资源管理:程序员必须手动管理内存、同步和命令缓冲,API 不做任何隐式操作。
  • 多线程友好:命令缓冲可以在多个线程中并行录制,充分利用多核 CPU。
  • 减少驱动开销:API 层极薄,驱动程序几乎不做验证和优化,性能更可预测。
  • 跨平台:支持 Windows、Linux、Android,但不支持 macOS/iOS(Apple 推出了自己的 Metal API)。
cpp
// Vulkan 命令录制示例(对比 OpenGL 的简洁性)
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
vkBeginCommandBuffer(commandBuffer, &beginInfo);

vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
                  graphicsPipeline);

VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = (float)swapChainExtent.width;
viewport.height = (float)swapChainExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);

VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);

VkBuffer vertexBuffers[] = {vertexBuffer};
VkDeviceSize offsets[] = {0};
vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0,
                     VK_INDEX_TYPE_UINT32);

vkCmdDrawIndexed(commandBuffer, indexCount, 1, 0, 0, 0);
vkEndCommandBuffer(commandBuffer);

2.3.3 图形 API 对比

特性OpenGLDirectX 12VulkanMetal
平台跨平台Windows/Xbox跨平台Apple
驱动开销高低低低
多线程支持差好优秀优秀
学习曲线低中高中
显式内存管理否是是是
计算着色器4.3+11+1.0+1.0+

2.4 渲染管线

渲染管线(Rendering Pipeline)是图形系统将三维场景转换为二维图像的核心流程。现代可编程管线的阶段如下:

应用阶段          GPU 光栅阶段           GPU 片元阶段
(CPU)              (固定功能)              (可编程)

┌──────────┐    ┌──────────────┐    ┌──────────────┐
│ 场景数据  │    │  顶点着色器   │    │  片元着色器   │
│ 顶点数组  │───>│  (Vertex      │───>│  (Fragment    │
│ 变换矩阵  │    │   Shader)     │    │   Shader)     │
└──────────┘    └──────┬───────┘    └──────┬───────┘
                       │                    │
                       ▼                    ▼
                ┌──────────────┐    ┌──────────────┐
                │  图元装配     │    │  逐片元操作    │
                │  (Primitive   │    │  (Per-Fragment │
                │  Assembly)    │    │   Operations)  │
                └──────┬───────┘    └──────┬───────┘
                       │                    │
                       ▼                    ▼
                ┌──────────────┐    ┌──────────────┐
                │  光栅化       │    │  帧缓冲       │
                │  (Rasterization)│   │  (Framebuffer)│
                └──────────────┘    └──────────────┘

2.4.1 顶点处理阶段

顶点着色器(Vertex Shader)对每个顶点执行一次,主要功能包括:

  • 模型变换:将顶点从模型空间变换到世界空间
  • 观察变换:将顶点从世界空间变换到相机空间(视图空间)
  • 投影变换:将顶点从相机空间变换到裁剪空间
  • 顶点属性计算:计算和传递法线、纹理坐标等属性
glsl
// 顶点着色器示例
#version 450 core

layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;

uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;

out vec3 vWorldPos;
out vec3 vNormal;
out vec2 vTexCoord;

void main() {
  vec4 worldPos = uModel * vec4(aPosition, 1.0);
  vWorldPos = worldPos.xyz;
  vNormal = mat3(transpose(inverse(uModel))) * aNormal;
  vTexCoord = aTexCoord;
  gl_Position = uProjection * uView * worldPos;
}

2.4.2 图元装配与光栅化

图元装配阶段将顶点组装成三角形、线段或点。光栅化阶段确定每个图元覆盖了屏幕上的哪些像素(片元),并为每个片元插值生成属性值(如颜色、纹理坐标、法线等)。

光栅化的核心是重心坐标插值:对于三角形内部的每个片元,根据其相对于三个顶点的重心坐标,对顶点属性进行透视正确的插值。

2.4.3 片元处理阶段

片元着色器(Fragment Shader)对每个片元执行一次,计算最终的颜色值。这是渲染管线中最灵活的阶段,可以实现各种视觉效果:

glsl
// 片元着色器示例(Phong 光照)
#version 450 core

in vec3 vWorldPos;
in vec3 vNormal;
in vec2 vTexCoord;

out vec4 FragColor;

uniform vec3 uLightPos;
uniform vec3 uViewPos;
uniform sampler2D uTexture;

void main() {
  vec3 norm = normalize(vNormal);
  vec3 lightDir = normalize(uLightPos - vWorldPos);
  vec3 viewDir = normalize(uViewPos - vWorldPos);

  // 环境光
  vec3 ambient = 0.1 * texture(uTexture, vTexCoord).rgb;

  // 漫反射
  float diff = max(dot(norm, lightDir), 0.0);
  vec3 diffuse = diff * vec3(1.0, 1.0, 0.8);

  // 镜面反射
  vec3 reflectDir = reflect(-lightDir, norm);
  float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
  vec3 specular = spec * vec3(0.5);

  FragColor = vec4(ambient + diffuse + specular, 1.0);
}

2.4.4 输出合并阶段

输出合并包括多个逐片元操作:

  • 深度测试(Depth Test):比较片元的深度值与深度缓冲中的值,决定是否更新像素。
  • 模板测试(Stencil Test):根据模板缓冲中的值决定是否丢弃片元。
  • 混合(Blending):将新片元的颜色与帧缓冲中已有的颜色进行混合,实现透明效果。
  • 逻辑操作(Logic Operations):对颜色值执行位级逻辑运算。

2.5 图形程序的基本结构

2.5.1 场景图

场景图(Scene Graph)是组织三维场景数据的常用数据结构。它是一个有向无环图(DAG),每个节点代表一个对象或一组变换,边代表父子关系。

cpp
// 场景图节点的基本结构
class SceneNode {
public:
  glm::mat4 localTransform;
  std::shared_ptr<Mesh> mesh;
  std::shared_ptr<Material> material;
  std::vector<std::shared_ptr<SceneNode>> children;

  void render(const glm::mat4& parentTransform) {
    glm::mat4 worldTransform = parentTransform * localTransform;
    if (mesh && material) {
      material->setUniform("uModel", worldTransform);
      mesh->draw();
    }
    for (auto& child : children) {
      child->render(worldTransform);
    }
  }
};

场景图的优势在于:

  • 层次化变换:子节点自动继承父节点的变换
  • 高效裁剪:可以基于包围体快速剔除不可见子树
  • 状态共享:父节点的状态可以被子节点继承

2.5.2 渲染循环

图形程序的核心是渲染循环,它不断重复"处理输入→更新状态→渲染→交换缓冲"的过程:

cpp
// 完整的渲染循环
class Application {
public:
  void run() {
    init();
    while (!glfwWindowShouldClose(window)) {
      float currentTime = glfwGetTime();
      deltaTime = currentTime - lastTime;
      lastTime = currentTime;

      processInput();
      update(deltaTime);
      render();
    }
    cleanup();
  }

private:
  void processInput() {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
      glfwSetWindowShouldClose(window, true);
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
      camera.processKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
      camera.processKeyboard(BACKWARD, deltaTime);
  }

  void update(float deltaTime) {
    // 更新场景中的动画、物理等
    scene.update(deltaTime);
  }

  void render() {
    glClearColor(0.1f, 0.1f, 0.15f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glm::mat4 projection = glm::perspective(
      glm::radians(camera.Zoom),
      (float)WIDTH / (float)HEIGHT,
      0.1f, 100.0f
    );
    glm::mat4 view = camera.GetViewMatrix();

    scene.render(projection, view);
    glfwSwapBuffers(window);
  }
};

2.6 图形学发展简史

计算机图形学的发展可以追溯到 20 世纪 60 年代:

  • 1963 年:Ivan Sutherland 在 MIT 开发了 Sketchpad,被认为是第一个交互式图形程序,也是 CAD 的先驱。
  • 1970 年代:Bresenham 画线算法(1965)、Bézier 曲面(1972)、Blinn-Phong 光照模型(1977)等基础算法相继提出。
  • 1980 年代:Cook-Torrance 光照模型(1982)、辐射度算法(1984)、第一个光线追踪算法(1979-1986)。OpenGL 的前身 IRIS GL 由 SGI 发布。
  • 1990 年代:OpenGL 1.0 发布(1992)、DirectX 首次发布(1995)、NVIDIA GeForce 256 引入硬件 T&L(1999)。
  • 2000 年代:可编程着色器成为主流、GPU 通用计算兴起、实时渲染技术飞速发展。
  • 2010 年代:基于物理的渲染(PBR)成为行业标准、Vulkan API 发布、实时光线追踪成为现实。
  • 2020 年代:DLSS/FSR 等 AI 超分辨率技术、NeRF 和 3D Gaussian Splatting 等新表示方法、实时路径追踪逐步普及。

三、实战案例

3.1 第一个 OpenGL 程序:绘制三角形

下面是一个完整的 OpenGL 程序,使用现代 OpenGL(Core Profile)绘制一个彩色三角形。

cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

const char* vertexShaderSource = R"(
  #version 450 core
  layout(location = 0) in vec3 aPos;
  layout(location = 1) in vec3 aColor;
  out vec3 vColor;
  void main() {
    gl_Position = vec4(aPos, 1.0);
    vColor = aColor;
  }
)";

const char* fragmentShaderSource = R"(
  #version 450 core
  in vec3 vColor;
  out vec4 FragColor;
  void main() {
    FragColor = vec4(vColor, 1.0);
  }
)";

GLuint compileShader(GLenum type, const char* source) {
  GLuint shader = glCreateShader(type);
  glShaderSource(shader, 1, &source, nullptr);
  glCompileShader(shader);
  GLint success;
  glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
  if (!success) {
    char infoLog[512];
    glGetShaderInfoLog(shader, 512, nullptr, infoLog);
    std::cerr << "Shader compilation error: " << infoLog << std::endl;
  }
  return shader;
}

int main() {
  glfwInit();
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

  GLFWwindow* window = glfwCreateWindow(800, 600, "Triangle", nullptr, nullptr);
  glfwMakeContextCurrent(window);
  gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
  glViewport(0, 0, 800, 600);

  GLuint vertShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
  GLuint fragShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);

  GLuint program = glCreateProgram();
  glAttachShader(program, vertShader);
  glAttachShader(program, fragShader);
  glLinkProgram(program);

  float vertices[] = {
    -0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,
     0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f
  };

  GLuint VAO, VBO;
  glGenVertexArrays(1, &VAO);
  glGenBuffers(1, &VBO);
  glBindVertexArray(VAO);
  glBindBuffer(GL_ARRAY_BUFFER, VBO);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float),
                        (void*)(3 * sizeof(float)));
  glEnableVertexAttribArray(1);

  while (!glfwWindowShouldClose(window)) {
    glClearColor(0.1f, 0.1f, 0.15f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  glDeleteVertexArrays(1, &VAO);
  glDeleteBuffers(1, &VBO);
  glDeleteProgram(program);
  glfwTerminate();
  return 0;
}

代码解析:

初始化 GLFW 和 GLAD:GLFW 负责创建窗口和管理 OpenGL 上下文,GLAD 负责加载 OpenGL 函数指针。

编写着色器:顶点着色器将顶点位置直接传递到裁剪空间(不做变换),并将颜色传递给片元着色器。片元着色器对颜色进行插值输出。

创建 VAO 和 VBO:VAO(顶点数组对象)记录顶点属性的配置,VBO(顶点缓冲对象)存储顶点数据。

渲染循环:每帧清除帧缓冲、使用着色器程序、绑定 VAO、执行绘制、交换缓冲。

3.2 渲染管线可视化

以下代码展示了渲染管线各阶段的数据变化,帮助理解从顶点到像素的完整流程:

cpp
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <vector>
#include <iostream>

struct Vertex {
  glm::vec3 position;
  glm::vec3 color;
  glm::vec3 normal;
};

// 模拟顶点着色器的变换过程
glm::vec4 vertexShader(const Vertex& v,
                       const glm::mat4& model,
                       const glm::mat4& view,
                       const glm::mat4& projection) {
  glm::vec4 worldPos = model * glm::vec4(v.position, 1.0f);
  glm::vec4 viewPos = view * worldPos;
  glm::vec4 clipPos = projection * viewPos;
  return clipPos;
}

// 模拟透视除法
glm::vec3 perspectiveDivide(glm::vec4 clipPos) {
  return glm::vec3(clipPos) / clipPos.w;
}

// 模拟视口变换(NDC -> 屏幕坐标)
glm::vec3 viewportTransform(glm::vec3 ndcPos, int width, int height) {
  float x = (ndcPos.x + 1.0f) * 0.5f * width;
  float y = (ndcPos.y + 1.0f) * 0.5f * height;
  float z = (ndcPos.z + 1.0f) * 0.5f;
  return glm::vec3(x, y, z);
}

int main() {
  Vertex v = {
    glm::vec3(1.0f, 0.0f, -2.0f),
    glm::vec3(1.0f, 0.0f, 0.0f),
    glm::vec3(0.0f, 1.0f, 0.0f)
  };

  glm::mat4 model = glm::mat4(1.0f);
  glm::mat4 view = glm::lookAt(
    glm::vec3(0.0f, 0.0f, 3.0f),
    glm::vec3(0.0f, 0.0f, 0.0f),
    glm::vec3(0.0f, 1.0f, 0.0f)
  );
  glm::mat4 projection = glm::perspective(
    glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f
  );

  std::cout << "=== 渲染管线数据流 ===" << std::endl;

  std::cout << "模型空间: ("
    << v.position.x << ", " << v.position.y << ", " << v.position.z
    << ")" << std::endl;

  glm::vec4 worldPos = model * glm::vec4(v.position, 1.0f);
  std::cout << "世界空间: ("
    << worldPos.x << ", " << worldPos.y << ", " << worldPos.z
    << ")" << std::endl;

  glm::vec4 viewPos = view * worldPos;
  std::cout << "相机空间: ("
    << viewPos.x << ", " << viewPos.y << ", " << viewPos.z
    << ")" << std::endl;

  glm::vec4 clipPos = projection * viewPos;
  std::cout << "裁剪空间: ("
    << clipPos.x << ", " << clipPos.y << ", " << clipPos.z
    << ", " << clipPos.w << ")" << std::endl;

  glm::vec3 ndcPos = perspectiveDivide(clipPos);
  std::cout << "NDC: ("
    << ndcPos.x << ", " << ndcPos.y << ", " << ndcPos.z
    << ")" << std::endl;

  glm::vec3 screenPos = viewportTransform(ndcPos, 800, 600);
  std::cout << "屏幕坐标: ("
    << screenPos.x << ", " << screenPos.y << ", " << screenPos.z
    << ")" << std::endl;

  return 0;
}

运行结果分析:

这段代码清晰地展示了顶点从模型空间经过一系列变换最终到达屏幕坐标的完整过程。每个阶段的坐标空间都有明确的含义:模型空间是物体自身的局部坐标系;世界空间是所有物体共享的全局坐标系;相机空间是以观察者视角为原点的坐标系;裁剪空间是经过投影变换的齐次坐标空间;NDC(归一化设备坐标)是裁剪后的 [-1,1] 范围内的坐标;屏幕坐标是最终的像素位置。


四、本章小结

本章作为计算机图形学的开篇,建立了从硬件到软件的完整知识框架:

图形学的核心问题包括建模、渲染、动画和交互,其中渲染是最核心的研究主题。

GPU 的并行架构(SIMT 模型)天然适合图形渲染的大规模并行计算需求,数千个核心可以同时处理屏幕上的不同像素。

图形 API 经历了从固定管线到可编程管线、从隐式状态到显式控制的演进。OpenGL 适合入门学习,Vulkan/Metal 代表现代高性能开发方向。

渲染管线是图形系统的核心流程,包括顶点处理、图元装配、光栅化、片元处理和输出合并五个主要阶段。

图形程序通常基于场景图组织数据,通过渲染循环驱动每一帧的更新和绘制。

实时渲染和离线渲染在性能约束、图像质量目标和技术选择上有根本差异,但技术边界正在因实时光线追踪而逐渐模糊。

理解这些基础概念后,你将能够在学习后续章节时,将每个具体技术准确地定位到渲染管线的某个阶段,建立起清晰的知识体系。


五、思考与练习

思考题

为什么 GPU 采用 SIMT(单指令多线程)架构而不是 SIMD(单指令多数据)架构?两者在图形渲染场景中各有什么优劣?

如果渲染管线中的深度测试阶段被禁用,会出现什么视觉效果?在什么场景下你可能需要禁用深度测试?

从 OpenGL 到 Vulkan 的演进,反映了图形 API 设计哲学的什么变化?为什么这种变化是必要的?

双缓冲机制解决了什么问题?如果没有双缓冲,快速运动的物体会出现什么视觉缺陷?

为什么场景图通常是有向无环图(DAG)而不是树?允许环会带来什么问题?

编程练习

基础三角形渲染:修改示例代码中的顶点颜色,实现一个渐变三角形。尝试添加第四个顶点,用两个三角形拼成一个正方形。

变换可视化:扩展渲染管线可视化程序,添加一个绕 Y 轴旋转的模型变换,观察旋转矩阵如何影响顶点在各坐标空间中的位置。

帧缓冲探索:创建一个离屏帧缓冲,将场景渲染到纹理上,然后将该纹理作为贴图渲染到一个全屏四边形上,实现"后处理"效果的基础框架。


1/8图形系统
0%