核心定义
光栅化,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 插值、深度、采样、抗锯齿密切相关。