核心流程
顶点着色器写出
gl_Position之后,这个值还不是最终屏幕坐标,它此时处在裁剪空间-Clip Space。
- GPU 后续会继续走完整的图形管线,大致顺序是:
图元装配裁剪透视除法视口变换光栅化片元着色各种测试与混合写入帧缓冲
如果把它串成一句话,就是:
text
gl_Position(裁剪空间)
-> 裁剪
-> NDC
-> 屏幕坐标
-> 生成片元
-> 计算颜色
-> 深度/模板/混合
-> 显示到屏幕
1. 图元装配
- 顶点着色器是"逐顶点"执行的,GPU 会把多个顶点按照你的绘制方式组装成图元。
- 例如:
gl.drawArrays(gl.TRIANGLES, ...)会每 3 个顶点组装成一个三角形gl.LINES会组装成线段gl.POINTS会组装成点
- 这一步里,
gl_Position只是每个顶点的位置结果,GPU 还要先知道这些顶点组成了什么图形。
2. 裁剪,Clipping
- GPU 会拿组装好的图元,和
裁剪空间可见范围做比较。 - 在标准裁剪空间里,顶点需要满足类似这样的范围关系:
text
-w <= x <= w
-w <= y <= w
-w <= z <= w
- 如果一个三角形完全在可见范围外,就直接丢掉。
- 如果只有一部分超出范围,GPU 会把它裁掉,只保留可见部分。
- 所以
gl_Position的w很重要,因为裁剪不是简单拿x/y/z和固定值比,而是和w相关。
3. 透视除法,Perspective Divide
- 裁剪之后,GPU 会把裁剪空间坐标变成
NDC,标准化设备坐标。 - 做法就是:
text
x_ndc = x / w
y_ndc = y / w
z_ndc = z / w
- 这一步非常关键,透视投影里的"近大远小"就是在这里真正体现出来的。
- 做完之后,坐标会落到标准范围内:
text
x: [-1, 1]
y: [-1, 1]
z: [-1, 1]
- 这里得到的已经不是
gl_Position原始值,而是透视除法后的标准坐标。
4. 视口变换,Viewport Transform
- NDC 仍然不是屏幕像素坐标,GPU 接下来会把它映射到视口区域。
- 比如:
x = -1映射到屏幕左边x = 1映射到屏幕右边y = -1映射到屏幕下边y = 1映射到屏幕上边
- 这个过程受
gl.viewport(x, y, width, height)影响。 - 也就是说,NDC 会被换算成真正的窗口像素区域里的位置。
5. 面剔除,可选
- 在光栅化前,GPU 还可能做
背面剔除。 - 如果启用了:
js
gl.enable(gl.CULL_FACE);
- 那么 GPU 会根据三角形顶点顺序判断它是正面还是背面。
- 背向摄像机的面可以直接丢弃,不再继续处理。
- 这样能减少后续片元计算量。
6. 光栅化,Rasterization
- 到这一步,GPU 已经有了屏幕上的三角形轮廓。
- 光栅化会把几何图元转换成大量片元,
Fragment。 - 你可以把它理解成:
- 三角形覆盖了哪些像素区域
- 就为这些像素位置生成对应的片元
- 注意:
片元不完全等于最终像素- 它只是"候选像素数据",后面还要经过测试才可能真正写进屏幕
7. 插值,Interpolation
- 光栅化生成片元时,GPU 会把顶点着色器输出的其他变量自动插值到每个片元上。
- 比如你在顶点着色器里输出:
glsl
varying vec2 vUv;
varying vec3 vColor;
varying vec3 vNormal;
- 那么 GPU 会根据三角形内部位置,为每个片元自动算出它自己的:
vUvvColorvNormal
- 这一步非常重要,因为片元着色器通常就是靠这些插值后的数据来计算纹理、光照和颜色的。
8. 片元着色器执行
- 每个片元都会执行一次片元着色器。
- 在这里你通常会做:
- 采样纹理
- 计算光照
- 组合颜色
- 输出最终颜色
例如:
glsl
gl_FragColor = vec4(color, 1.0);
或者在 WebGL2 中:
glsl
out vec4 outColor;
- 这一步决定"这个片元想要显示成什么颜色"。
9. 每片元测试,Per-Fragment Tests
-
片元着色器算出颜色后,还不一定能真正画到屏幕上。
-
GPU 还会做一系列测试,常见包括:
-
深度测试- 判断这个片元是不是被前面的物体挡住了
- 只有更靠近摄像机的片元通常才会保留
-
模板测试- 根据模板缓冲决定哪些区域允许绘制
-
裁剪/裁切规则- 某些片元可能仍会因为状态设置被丢弃
-
Alpha discard- 如果你在片元着色器里主动
discard,这个片元也会被直接丢掉
- 如果你在片元着色器里主动
如果深度测试没过,这个片元即使颜色算出来了,也不会写入最终图像。
10. 混合,Blending
- 如果片元通过测试,且启用了混合:
js
gl.enable(gl.BLEND);
- GPU 会把"新片元颜色"和"帧缓冲里已有颜色"混合。
- 这就是透明效果的基础。
- 常见混合公式例如:
js
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
- 这时最终显示的颜色,可能不是片元着色器直接输出的颜色,而是混合后的结果。
11. 写入帧缓冲
- 最终通过所有测试的片元,会把结果写入
帧缓冲,Framebuffer。 - 帧缓冲里通常包含:
- 颜色缓冲
- 深度缓冲
- 模板缓冲
- 当一帧全部完成后,浏览器再把颜色缓冲内容显示到屏幕上。
重点理解 gl_Position 的地位
gl_Position只负责告诉 GPU:这个顶点在裁剪空间里在哪里。- 它并不直接等于屏幕坐标。
- 它后面还要经历:
- 裁剪
- 除以
w - 映射到视口
- 光栅化成片元
- 片元着色
- 深度/混合处理
- 所以
gl_Position更像是"几何阶段的最终输出",不是整个渲染流程的最终输出。
最容易混淆的几点
gl_Position不是屏幕像素坐标,它是裁剪空间坐标。w不是可有可无,它直接影响裁剪和透视除法。- 顶点着色器结束后,不是直接上屏,而是先从"顶点"变成"图元",再变成"片元"。
- 片元着色器输出颜色后,也不一定会显示,还要过深度测试和混合。
一句话总结
- 计算出
gl_Position之后,GPU 会先把顶点组装成图元,再做裁剪和透视除法,把它们变成屏幕范围内的片元,然后执行片元着色器,最后经过深度测试、混合等步骤,才真正写到屏幕上。
最短记忆版
gl_Position之后的主线就是:
text
图元装配 -> 裁剪 -> 透视除法 -> 视口变换 -> 光栅化 -> 片元着色 -> 深度/混合 -> 帧缓冲