返回知识库

渲染

渲染全景图

渲染全景图 封面
WebGPU/WebGLShaderGLSL/WGSL渲染管线

直觉问题

GPU 是如何把几十个顶点描述的数据,最终变成显示器上百万个彩色像素点的? CPU 负责思考“画什么”,GPU 负责执行“怎么画”——这个分工是怎么做到的?

理解 GPU 渲染全景图,就像理解“从图纸到摩天大楼”的全过程:CPU 是建筑师,GPU 是建筑队和装修队的统称。建筑师画了设计图,装修队日夜赶工把图纸变成具体的“像素” reality。


核心概念白话讲:CPU vs GPU 的分工

CPU:总指挥,负责“调度”与“决策”

CPU(中央处理器)像一个指挥大师,它会根据游戏逻辑、物理引擎、UI 事件等,决定下一帧要画什么。但它并不会自己去画每一个像素——那太慢了。

CPU 的工作流程大致是:

  1. 计算物体的世界坐标、旋转、相机位置(** preparation 阶段**)。
  2. 将顶点数据、索引数据、纹理资源上传到 GPU 的显存(VRAM)
  3. 调用图形 API(WebGL / WebGPU / Vulkan / DirectX),发出Draw Call:“GPU 老兄,请帮我渲染这个物体!”
  4. 等待 GPU 完成渲染,然后继续下一帧循环。

GPU:高度并行的“像素流水线”

GPU(Graphics Processing Unit)的本质是一个由成千上万个微小核心(Streaming Processors)组成的阵列。它不像 CPU 那样擅长复杂分支和逻辑判断,但它的核心数极多,可以在一秒内并行处理上亿个微小任务。

GPU 渲染一帧画面的核心流程如下:

  1. Vertex Processing:处理顶点(位置、纹理坐标等)。
  2. Primitive Assembly & Culling:将顶点连接成三角形,剔除掉看不到的面(背面剔除)。
  3. Rasterization:将三角形打调成像素大小的“拼图”。
  4. Fragment Processing:计算每个像素的颜色。
  5. Per-Sample Operations:深度测试、模板测试、混合,最终写入帧缓冲。

你可以把 GPU 想象成一个拥有上千条生产线的现代化工厂,每条生产线只负责一个像素。CPU 只需要把原料(顶点数据)和配方(Shader)发下去,所有生产线就同时开始并行生产。

从顶点数据到屏幕像素的旅程

graph LR
    A[CPU 准备数据
    顶点、索引、纹理、Uniform] --> B{GPU 接收指令
    Draw Call};
    B --> C[Vertex Shader
    顶点处理 & 变换];
    C --> D[Primitive Assembly
    图元装配 & 背面剔除];
    D --> E[Rasterization
    光栅化:三角形 -> 像素拼图];
    E --> F[Fragment Shader
    片元着色 & 纹理采样];
    F --> G[Per-Sample Ops
    深度/模板/混合];
    G --> H[Framebuffer
    屏幕或纹理];

GPU 渲染管线硬件流程

graph TB
    subgraph CPU[CPU 端]
        A1[准备数据
    顶点/索引/纹理]
        A2[调用 Draw Call]
    end
    
    subgraph GPU[GPU 端]
        B1[Vertex Input
    从 Buffer 读取数据]
        B2[Vertex Shader
    MVP 矩阵变换]
        B3[Primitive Assembly
    三角形组装]
        B4[Primitive Culling
    背面剔除]
        B5[Rasterization
    生成 Fragment]
        B6[Fragment Shader
    计算颜色]
        B7[Per-Sample Ops
    深度测试/混合]
        B8[Framebuffer
    写入最终颜色]
    end
    
    A1 --> A2 --> B1 --> B2 --> B3 --> B4 --> B5 --> B6 --> B7 --> B8

原理与机制:GPU 渲染管线详解

1. 顶点输入阶段 (Vertex Input)

GPU 从**顶点缓冲(Vertex Buffer)**读取数据。顶点不仅包含位置(x, y, z),还可以包含颜色(color)、法线(normal)、纹理坐标(uv)等。

对数据格式有要求吗?WGSL 中通过 @location(index) 来匹配顶点数据布局。


2. 顶点处理阶段:Vertex Shader

Vertex Shader 是渲染管线的第一个可编程阶段。

  • 输入:单个顶点(位置、颜色、UV 等)。
  • 核心任务:将顶点从模型局部空间转换到裁剪空间(Clip Space)。通常做法是使用 MVP 矩阵(Model-View-Projection)进行变换。
  • 输出:顶点在裁剪空间中的坐标(gl_PositionclipPosition),以及传递给后续阶段的数据(颜色、纹理坐标等)。

GLSL 示例:基础 Vertex Shader

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;

uniform mat4 uMVPMatrix;

out vec2 TexCoord;

void main() {
  gl_Position = uMVPMatrix * vec4(aPos, 1.0);
  TexCoord = aTexCoord;
}

WGSL 示例:基础 Vertex Shader

struct VertexInput {
  @location(0) aPos: vec3<f32>,
  @location(1) aTexCoord: vec2<f32>,
};

struct VertexOutput {
  @builtin(position) clipPosition: vec4<f32>,
  @location(0) TexCoord: vec2<f32>,
};

@group(0) @binding(0)
var<uniform> uMVPMatrix: mat4x4<f32>;

@vertex
fn main(input: VertexInput) -> VertexOutput {
  var output: VertexOutput;
  output.clipPosition = uMVPMatrix * vec4<f32>(input.aPos, 1.0);
  output.TexCoord = input.aTexCoord;
  return output;
}

数学原理:空间变换链 如同在 01 中提到的 MVP 概念,实际数学模型是:

Vclip=PVMVmodelV_{clip} = P \cdot V \cdot M \cdot V_{model}

其中:

  • VmodelV_{model}:顶点在模型本身的局部坐标系中的坐标
  • MM:模型变换(Model),包含位移、旋转、缩放
  • VV:视图变换(View),相机将世界坐标转换到相机坐标系
  • PP:投影变换(Projection),将相机空间的点映射到裁剪空间(齐次坐标 [w,w][-w, w] 范围)
  • VclipV_{clip}:裁剪空间坐标

3. 图元装配与剔除 (Primitive Assembly & Culling)

顶点经过处理后,GPU 将它们连接成图元(Primitive)。最常见的图元是三角形

  • 图元装配:将顶点按顺序(或索引)连接成三角形。
  • 背面剔除(Back-face Culling):摄像机背后(法线朝向背面)的三角形将被剔除,不做进一步处理。例如,一个内部面完全看不到,可以直接跳过。

渲染管线硬件图示意:

/-------------------------------------------\
|  CPU: Draw Call + 资源上传                  |
|-------------------------------------------|
|  GPU 渲染管线                                |
|  [Vertex Input] -> [Vertex Shader]          |
|       |                                      |
|       v                                      |
|  [Primitive Assembly]                       |
|       |                                      |
|       v                                      |
|  [Rasterization] -> [Fragment Shader] <- [纹理采样]
|       |                                      |
|       v                                      |
|  [Per-Sample Ops: 深度测试/模板测试/混合]      |
|       |                                      |
|       v                                      |
|  [Framebuffer: 色彩/深度/模板]               |
\-------------------------------------------/

4. 光栅化 (Rasterization)

这是 GPU 渲染管线中最“物理”的一个阶段。它负责将连续的三角形幻化为离散的像素。

  • 任务:判断一个三角形覆盖了屏幕上的哪些像素。
  • 结果:生成片元(Fragment)。片元包含坐标、颜色候选、深度值等信息。它不是最终的“像素”,而是等待 Fragment Shader 计算的“候选像素”。

光栅化的核心算法是扫描线填充(Scanline Fill)边缘填充(Edge Equation)。GPU 硬件有专门的 Rasterizer 电路来加速这个过程。

5. 片元处理阶段:Fragment Shader

Fragment Shader(在 OpenGL 中常叫 Pixel Shader,WGSL 沿用 Fragment Shader 术语)是管线的第二个可编程阶段。

  • 输入:从 Vertex Shader 插值(Interpolate)过来的数据(颜色、UV 坐标等),以及 Fragment 的屏幕坐标。
  • 核心任务:根据纹理、光照、材质属性计算最终像素的颜色。
  • 输出:该 Fragment 的颜色(RGBA),以及可选的深度值(Depth)

Fragment Shader 的速度直接决定了像素填充率(Fill Rate)。一个复杂的渲染管线中,它是最易成为瓶颈的阶段。

GLSL 示例:基础 Fragment Shader

#version 330 core
in vec2 TexCoord;
out vec4 FragColor;

uniform sampler2D uTexture;

void main() {
  vec4 texColor = texture(uTexture, TexCoord);
  FragColor = texColor;
}

WGSL 示例:基础 Fragment Shader

@group(0) @binding(1)
var uTexture: texture_2d<f32>;

@group(0) @binding(2)
var uSampler: sampler;

struct FragmentInput {
  @location(0) TexCoord: vec2<f32>,
};

@location(0)
var<out> FragColor: vec4<f32>;

@fragment
fn main(input: FragmentInput) {
  let texColor = textureSample(uTexture, uSampler, input.TexCoord);
  FragColor = texColor;
}

6. 逐样本操作 (Per-Sample Operations)

所有 Fragment 在写入 Framebuffer 之前,都要经过一系列硬件级的像素级测试和混合

测试/操作作用GLSL 控制WGSL 控制
Scissor Test裁剪掉视口外的像素glEnable(GL_SCISSOR_TEST)GPURenderPassDescriptor.scissorRect
Stencil Test通过模板缓冲(Stencil Buffer)执行遮罩或轮廓操作glStencilFunc / glStencilOpGPURenderPipelineDescriptor.stencil
Depth Test深度测试,决定 Fragment 是否可见glDepthFunc(GL_LESS)GPURenderPipelineDescriptor.depthStencil
Blending前后颜色按 Alpha 值进行混合glBlendFunc / glBlendEquationGPUBlendState
Dithering将高精度颜色映射到低精度显示器自动(通常开启)自动(通常开启)

7. 帧缓冲输出 (Framebuffer Write)

最终通过所有测试的 Fragment 颜色会被写入 Framebuffer。现代渲染中,Framebuffer 不一定直接是屏幕,也可能是离屏纹理(Offscreen Texture)多重渲染目标(MRT)。


GPU vs CPU:并行模型的根本差异

特性CPUGPU
核心数4 - 64 个物理核心成百上千个 Streaming Processors
擅长任务复杂逻辑、高分支、串行任务大规模并行、结构重复的简单计算
内存架构通用 RAM + 多级缓存专用 VRAM + 极高带宽(e.g. GDDR6X)
编程模型顺序执行,上下文切换SIMT(单指令多线程),Warp/Wavefront 并行
延迟低延迟(单任务快)高延迟(单任务慢,但任务并行时吞吐量大)

SIMT(Single Instruction Multiple Threads):GPU 的每个“Warp”(NVIDIA,通常为 32 线程)或“Wavefront”(AMD,通常为 64 线程)内的所有线程执行同一指令,但操作不同数据。如果线程之间出现分支(如 if-else),会导致Warp 内分支发散,严重降低 GPU 利用率。


常见误区与陷阱

1. 以为 GPU 是“更快的 CPU”

  • 误区:GPU 核心数多,所以任何计算任务丢给它都超强。
  • 真相:GPU 不适合高分支、频繁内存随机访问的任务。如果 Shader 中有太多 if...else,同一 Warp 内的线程路径不同,会导致Warp Divergence,性能暴跌。

2. 认为 Vertex 和 Fragment 之间可以“直接通信”

  • 误区:Vertex Shader 声明一个 out 变量,Fragment Shader 用 in 接收,感觉像是在两个函数间传参。
  • 真相:这叫做 插值(Interpolation)。GPU 硬件会在三角形的三个顶点之间,按像素位置对数据进行线性插值(Linear Interpolation)。它不是精确的“传参”,而是系统性的“插值重建”过程。这解释了为什么 Vertex Shader 中的数据到 Fragment Shader 中只能在三角形内部进行平滑过渡。

3. 忽略“Draw Call 是 CPU 的瓶颈”

  • 误区:CPU 丢一个 Draw Call 给 GPU 就像点击“发送”一样瞬间完成。
  • 真相:Draw Call 存在巨大的 CPU 开销。CPU 需要设置渲染状态(材质、纹理、Shader)、绑定资源、调用底层驱动。一次 Draw Call 可能要消耗几百到几千个 CPU 周期。现代渲染管线的核心优化方向之一就是减少 Draw Call(如使用 Instancing、合并材质、使用 GPU-Driven Rendering 等)。

4. 混淆 GLSL 与 WGSL 的坐标系规则

  • GLSL / WebGL $y:在 NDC(标准化设备坐标)中,y向上为正(屏幕中心为原点)。
  • WGSL / WebGPU $y:在 NDC 中,y向下为正(左上角为原点)。WebGPU 也遵循 Clip Space 的此规范,等同于 DirectX 的惯例。这意味着在 WebGPU 中,如果你从 WebGL 迁移代码,可能需要反转 y 轴或修改投影矩阵。

5. 认为 GPU 渲染是“连续的过程”

  • 误区:GPU 一帧一帧地连续渲染,像放电影一样。
  • 真相:GPU 渲染过程高度并行化,处理上通常为**双缓冲(Double Buffering)三缓冲(Triple Buffering)**机制。GPU 渲染当前帧(Back Buffer)时,屏幕显示的是上一帧(Front Buffer)。渲染完成后交换两者(Swap Operation)。这避免了画面撕裂(Tearing)问题。

延伸阅读与自测

权威索引

  1. The Rasterization Stage (GPU Gems): NVIDIA GPU Gems - 光栅化硬件流水线与性能优化的深入解读。
  2. A Trip Through the Graphics Pipeline (Fabien Sanglard): fabien-sanglard 的个人博客文章 - 详细介绍了 GPU 内部结构。
  3. WebGPU Specification (Render Pipeline): W3C WebGPU Spec - 浏览 WebGPU 的 RenderPipelineCommandEncoder 部分,了解 API 层面的映射。
  4. OpenGL ES 3.2 Spec - Chapter 9, 10: 描述了光栅化、插值、深度测试和混合的数学公式。通过 WebSearch 检索 “OpenGL ES 3.2 specification” 获取。

进阶思考题

  1. 为什么 GPU 平行执行 Shader 时需要进行“Warp 内分支发散”优化? 请用代码举例说明如何通过重构逻辑减少 Warp Divergence。 2.如果 CPU 渲染一帧需要 16ms,GPU 渲染只需要 4ms,瓶颈在 CPU 还是 GPU?如果 GPU 核心占用率只有 20%,瓶颈又在哪?
  2. 在现代 GPU 上,Fragment Shader 的复杂度和贴图纹理数量对性能的影响哪个更大? 为什么 GPU Profiler 通常会建议你优先检查 Frag Shader 的 ALU(算术逻辑单元)利用率?

总结

GPU 渲染管线是一个从**几何空间(Vertex)像素空间(Pixel)**的“加工厂”。

  • 理解 CPU 与 GPU 的分工,是做好高性能渲染的第一步。
  • 掌握 Vertex → Primitive → Fragment → Output 这四个核心阶段,你就能看懂绝大多数复杂的渲染问题。
  • GLSL 与 WGSL 虽然 API 不同,但它们都遵循同样的 GPU 渲染硬件架构。学习 WGSL/WebGPU 并不会让你抛弃 GLSL 知识,反而会让你对 GPU 的本质看得更清楚。