GPU自动处理专题4-填充覆盖区域(光栅化)

核心定义

  • 光栅化Rasterization,就是把"几何图元"变成"屏幕上的片元"的过程。
  • 更直白一点说:
    • 前面阶段处理的是点、线、三角形这种几何对象
    • 光栅化处理的是"这个三角形到底覆盖了哪些屏幕位置"
    • 它会把覆盖到的那些位置,生成为一批片元Fragment
  • 所以光栅化是从几何世界进入像素世界的关键一步。

你可以先记一句最核心的话:

  • 光栅化 = 判断图元覆盖了哪些屏幕位置,并为这些位置生成片元

先把它放回整条管线里

在 WebGL 里,一般流程是:

text 复制代码
顶点数据
-> 顶点着色器
-> 图元装配
-> 裁剪
-> 透视除法
-> NDC
-> 视口变换
-> 光栅化
-> 片元着色器
-> 深度/模板/混合
-> 帧缓冲

所以光栅化不是最前面,也不是最后面,它的位置非常明确:

  • 顶点阶段已经结束
  • 顶点已经有了屏幕上的位置
  • 现在 GPU 要决定"哪些像素要参与绘制"
  • 这就是光栅化

光栅化前,GPU手里已经有什么

在进入光栅化之前,GPU 通常已经完成了这些事:

  • 顶点着色器算出了每个顶点的 gl_Position
  • 图元已经组装好了,例如组成了一个三角形
  • 超出可见范围的部分已经被裁剪
  • 顶点已经从 Clip Space 经过透视除法进入 NDC
  • 又经过视口变换,变成了屏幕空间中的位置

也就是说,到光栅化这一步时,GPU 已经知道:

  • 三角形 3 个顶点在屏幕上的大致位置
  • 每个顶点附带的其他属性
  • 例如:
    • 颜色 color
    • 纹理坐标 uv
    • 法线 normal
    • 世界坐标、视图坐标等 varyings

现在要做的就是:

  • 把这个"连续的三角形区域"
  • 离散化成"屏幕上的许多采样点/片元"

为什么叫"光栅化"

"光栅"这个词,本质上和"规则网格"有关。 因为显示器、帧缓冲、像素阵列,本质上都可以看成一个二维网格。

所以光栅化的本质是:

  • 把连续的几何形状
  • 投到一个离散的像素网格上

你可以把它想成:

  • 数学里的三角形边是连续的
  • 但屏幕不是连续平面,而是一格一格的像素
  • GPU 要回答:这个三角形覆盖了哪些格子

这一步,就是光栅化。

最直观的例子:画一个三角形

假设屏幕上有一个三角形:

text 复制代码
      A
     / \
    /   \
   /     \
  B-------C

它经过视口变换后,顶点落在屏幕像素网格上,比如:

  • A = (120, 300)
  • B = (80, 120)
  • C = (260, 140)

GPU 不会直接"把这个数学三角形显示出来",而是要判断:

  • 像素 (100,150) 在不在三角形内部?
  • 像素 (160,180) 在不在三角形内部?
  • 像素 (240,220) 在不在三角形内部?

对于被覆盖的那些位置,就生成片元。

所以你可以把光栅化理解成:

  • 遍历像素网格
  • 做覆盖测试
  • 为通过测试的位置生成片元

片元到底是什么

很多人会把片元像素直接等同,但它们并不完全一样。

像素

  • 是最终屏幕上的一个显示单元

片元

  • 是 GPU 在光栅化阶段生成的一条"候选写入记录"
  • 它包含:
    • 这个位置的屏幕坐标
    • 深度值
    • 由顶点属性插值得到的各种 varying 值
  • 后面还要经过:
    • 片元着色器
    • 深度测试
    • 模板测试
    • 混合
  • 只有通过这些步骤,它才可能真正成为最终像素颜色

所以更准确地说:

  • 光栅化生成的是片元,不是最终像素

光栅化到底做了哪些事

如果拆开说,光栅化通常至少包含这几个关键动作:

  • 确定图元在屏幕上的覆盖区域
  • 判断哪些采样点落在图元内部
  • 为这些采样点生成片元
  • 对顶点输出的 varying 做插值
  • 计算每个片元的深度等附加信息
  • 把这些片元交给片元着色器继续处理

下面逐个讲。

1. 先确定包围范围

GPU 不会傻傻地检查全屏每个像素。 那样太浪费了。

它通常会先给三角形找一个最小包围盒,Bounding Box,比如:

  • 最小 x
  • 最大 x
  • 最小 y
  • 最大 y

这样只需要在这个矩形范围里做判断。

例如三角形顶点是:

  • A = (120, 300)
  • B = (80, 120)
  • C = (260, 140)

那么包围盒大概就是:

  • x ∈ [80, 260]
  • y ∈ [120, 300]

GPU 只要在这块区域内判断像素是否被三角形覆盖即可。

这一步是光栅化的性能基础之一。

2. 判断一个采样点是否在三角形内部

这是光栅化最核心的数学问题。

对包围盒中的每个候选位置,GPU 要判断:

  • 这个位置是否落在三角形内部
  • 或者是否落在边上
  • 如果是,就生成片元
  • 如果不是,就跳过

底层实现可以有不同细节,但核心思想通常和这些方法有关:

  • 边函数Edge Function
  • 重心坐标Barycentric Coordinates
  • 半平面测试

你不需要立刻掌握所有公式,但要理解本质:

  • 三角形内部的点,满足一组几何条件
  • GPU 用这些条件高效判断"覆盖关系"

3. 生成片元

一旦某个采样点被判定在三角形内部,GPU 就会生成一个片元。

这个片元至少带有:

  • 屏幕位置
  • 深度值
  • 各种插值后的 varying 数据

例如:

glsl 复制代码
varying vec2 vUv;
varying vec3 vColor;
varying vec3 vNormal;

这些不是在光栅化前就"天然存在于每个像素"的。 它们最初只存在于顶点上。

光栅化会把顶点数据传播到片元。

4. 插值,这是光栅化最重要的能力之一

假设一个三角形 3 个顶点分别有颜色:

  • 顶点 A:红色
  • 顶点 B:绿色
  • 顶点 C:蓝色

那么三角形内部某个片元的颜色并不是随便取一个顶点,而是根据它在三角形里的位置,做插值。

结果就是:

  • 靠近 A 更偏红
  • 靠近 B 更偏绿
  • 靠近 C 更偏蓝
  • 中间区域是平滑过渡色

这就是为什么你能看到渐变、纹理映射、平滑法线、光照过渡。

如果没有光栅化插值,片元着色器就拿不到这些逐片元变化的数据。

所以你可以把光栅化插值理解成:

  • 把顶点上的属性,连续地铺满整个三角形内部

重心坐标是怎么参与插值的

这是理解光栅化插值的关键。

如果片元位于三角形内部某点 P,GPU 可以用 3 个权重描述它相对三个顶点的位置:

text 复制代码
P = a * A + b * B + c * C

并且:

text 复制代码
a + b + c = 1

这里的 a b c 就可以理解成重心坐标权重。

那么任何顶点属性都能按同样方式插值:

text 复制代码
value(P) = a * value(A) + b * value(B) + c * value(C)

比如颜色:

text 复制代码
color(P) = a * colorA + b * colorB + c * colorC

比如纹理坐标:

text 复制代码
uv(P) = a * uvA + b * uvB + c * uvC

所以从数学上看,光栅化不只是"找像素",它还负责把几何顶点属性平滑传播到三角形内部。

5. 透视正确插值

这是一层更深入但非常重要的点。

如果只是普通线性插值,在透视投影下会出问题,尤其是:

  • 纹理坐标
  • 世界坐标
  • 深度相关数据

因为屏幕空间里的三角形已经经历了透视变换,不再保持简单线性关系。

所以 GPU 并不是天真地直接在屏幕上做普通线性插值,而是会做透视校正插值Perspective-Correct Interpolation

这件事的效果是:

  • 纹理不会在远近变化时发生明显扭曲
  • 透视场景中的 varying 值仍然正确

你可以先记住结论:

  • 光栅化阶段的插值,不只是线性插值,而是透视正确的插值

这也是硬件图形管线非常强大的地方。

6. 计算片元深度

光栅化时,不仅会插值颜色、UV,也会得到片元的深度值。

这个深度值非常重要,因为后续要做:

  • 深度测试
  • 遮挡判断

比如两个三角形投影到屏幕上重叠了,最终谁显示在前面,不是靠绘制顺序"硬决定",而是通常靠深度测试。

所以光栅化输出的片元里,深度是非常关键的一部分。

7. 交给片元着色器

光栅化本身不负责最终"算颜色公式"。 它负责的是:

  • 确定哪些片元存在
  • 把这些片元需要的数据准备好

然后 GPU 对每个片元执行一次片元着色器:

glsl 复制代码
gl_FragColor = ...

所以两者关系可以理解成:

  • 光栅化:生成片元,准备片元数据
  • 片元着色器:根据这些数据计算片元颜色

一个完整直觉例子:纹理贴图为什么依赖光栅化

假设你有一个贴了图片的矩形,由两个三角形组成。

每个顶点有:

glsl 复制代码
attribute vec3 aPosition;
attribute vec2 aUv;

顶点着色器把 aUv 传给片元着色器:

glsl 复制代码
varying vec2 vUv;

这时候:

  • 顶点上只有 4 个 UV
  • 但屏幕上可能有几万个片元

那每个片元的 UV 从哪来?

答案就是:

  • 光栅化阶段帮你插值得到每个片元的 vUv

然后片元着色器才能:

glsl 复制代码
texture2D(uTexture, vUv)

所以没有光栅化插值,就没有正确的纹理映射。

线、点也会光栅化吗

会,只不过规则和三角形不同。

  • 一个点图元可以光栅化成一个小方块区域
  • 大小常和 gl_PointSize 有关

线

  • 一条线会覆盖一串像素位置
  • GPU 会根据线的栅格化规则生成片元

三角形

  • 最常见
  • 也是最重要的图元类型
  • 因为几乎所有复杂模型最终都会被拆成三角形

所以虽然"光栅化"适用于点线面,但图形学里最重要、最典型的光栅化对象就是三角形。

为什么几乎一切都用三角形

因为三角形有几个天然优势:

  • 3 个点一定共面
  • 数学最稳定
  • 内部插值简单明确
  • 方便硬件高效处理

四边形、多边形最终大多都要拆成三角形。 所以 GPU 的光栅化硬件,本质上就是为三角形极度优化的。

光栅化和"画线算法"有什么关系

如果你学过计算机图形学,可能见过:

  • Bresenham 画线算法
  • 扫描线填充

它们和光栅化在思想上是同类问题:

  • 如何把连续几何图形映射到离散像素格子

现代 GPU 的光栅化实现远比这些经典算法复杂和高效,但本质问题是一样的。

光栅化为什么是图形硬件的核心

因为这是图形硬件最擅长的事之一:

  • 几何图元很多
  • 屏幕采样点更多
  • 需要大量并行判断覆盖关系
  • 需要大量并行插值 varying

GPU 天生适合做这种:

  • 同样规则
  • 海量数据
  • 高并行

的任务。

所以光栅化是整个实时渲染性能的核心环节之一。

多重采样和抗锯齿也和光栅化有关

你看到三角形边缘有锯齿,本质上就是因为:

  • 连续边界被投到离散像素网格上
  • 某些像素只有一部分被覆盖
  • 如果简单按"覆盖/不覆盖"二选一,就会显得生硬

为了解决这个问题,会有:

  • MSAA
  • 多采样覆盖测试
  • 边缘覆盖率估计

这些都发生在光栅化相关阶段。

所以:

  • 锯齿问题,本质上就是光栅化离散化带来的副作用
  • 抗锯齿,本质上就是在改善光栅化采样质量

为什么边缘像素有时看起来发虚

因为一个像素不是数学意义上的"点",而是一个有面积的采样单元。 当三角形边缘穿过一个像素时,会出现:

  • 这个像素只被部分覆盖
  • 不同采样策略会给出不同结果

如果有抗锯齿,会根据覆盖率来混合颜色; 如果没有,可能直接粗暴取整,看起来就会锯齿明显。

这也是光栅化天然会碰到的问题。

光栅化 和 光线追踪 的区别

这是理解现代图形学时常见的对照。

光栅化

  • 从几何出发
  • 判断图元覆盖哪些屏幕位置
  • 非常适合实时渲染
  • WebGL 主流程就是它

光线追踪

  • 从像素出发
  • 发射射线去问"这个像素看到什么"
  • 更适合高真实感,但代价更高

所以在 WebGL 里你学的"标准渲染管线",核心就是光栅化路线。

初学者最容易误解的点

  • 误解 1:光栅化就是"直接上色"
    不是。光栅化先生成片元,真正算颜色通常是片元着色器做的。
  • 误解 2:光栅化输出的就是最终像素
    不是。片元还要经过深度测试、模板测试、混合等流程。
  • 误解 3:顶点属性会自动存在于三角形每个位置
    不是。是光栅化负责插值过去的。
  • 误解 4:透视效果是在光栅化阶段产生的
    严格说核心透视来自投影和透视除法,光栅化负责在这个结果上做正确插值和覆盖判断。
  • 误解 5:光栅化只和三角形覆盖有关
    不止。它还和 varying 插值、深度、采样、抗锯齿密切相关。
相关推荐
悦儿遥遥雨11 小时前
PXE + Kickstart 无人值守批量部署系统
linux·javascript·nginx
清秋2 小时前
全网最全 ECMAScript 攻略( 更新至 ES2026)
前端·javascript·编程语言
Hilaku2 小时前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
不简说5 小时前
拖拽DIY表格插件来了!sv-print 七月更新:这次真的能自己拼表格了
前端·javascript·前端框架
weixin_471383035 小时前
Next.js - 04 - 深入理解next.js渲染原理
前端·javascript·next.js
ssshooter5 小时前
小程序分包页面报 "has not been registered yet" 的隐蔽根因:一个动态 import 拖垮整个分包
前端·javascript·微信小程序
鹏多多5 小时前
Web浏览器后台离线通知方案实现以及兼容指南
前端·javascript·vue.js
yangmu32036 小时前
Vue3 响应式系统深度解析:从 Proxy 到依赖追踪
前端·javascript·vue.js
Asize6 小时前
Node path 与 fs 模块:从回调地狱到 async await 的异步进化
javascript·node.js