1.2 固定管线记忆 vs 可编程现实 本节摘要:桌面 OpenGL 早期可以用开关打开光照、雾、纹理环境,顶点变换由驱动内置公式完成,这叫固定功能管线。WebGL 从第一天起就没有这条路:必须提供顶点着色器和片元着色器,矩阵、光照、纹理采样都由你写进 GLSL。把固定管线的记忆搬进 canvas,会在“为什么没有灯光函数”处彻底卡住。本节对照两种模型的决策权归属,说明可编程强制项如何既提高门槛,又打开后处理与非真实感渲染。 本节目标 阅读完本节,你应当能够: 用“决策权在驱动还是在着色器”一句话说清固定管线与可编程管线的差别 列出固定管线里常见、WebGL 里不存在的概念:光照开关、纹理环境、内置法线矩阵 解释 WebGL 为什么对齐 ES 2.
本节摘要:桌面 OpenGL 早期可以用开关打开光照、雾、纹理环境,顶点变换由驱动内置公式完成,这叫固定功能管线。WebGL 从第一天起就没有这条路:必须提供顶点着色器和片元着色器,矩阵、光照、纹理采样都由你写进 GLSL。把固定管线的记忆搬进 canvas,会在“为什么没有灯光函数”处彻底卡住。本节对照两种模型的决策权归属,说明可编程强制项如何既提高门槛,又打开后处理与非真实感渲染。
阅读完本节,你应当能够:
很多人第一次碰 WebGL,是带着桌面时代的肌肉记忆来的:创建上下文、设清屏色、打开深度测试、再 glEnable(LIGHT0),然后三角形就会按冯氏模型亮起来。这套记忆在 WebGL 里对应空操作。没有 LIGHT0,没有 glTexEnv,没有 glFog。你能 enable 的是深度测试、面剔除、混合、模板这类仍留在固定阶段的逐片元测试,不是光照本身。
固定管线的本质是:驱动里写死了几条公式,你用状态去选公式、填参数。可编程管线的本质是:公式空缺,你必须提交两段程序。顶点着色器至少要写出裁剪空间位置,片元着色器至少要写出颜色。缺一段,程序链不上,什么都没有。
这不是浏览器“功能少”,这是移动 GPU 在 ES 2.0 阶段已经放弃固定功能后的真实合同。WebGL 对齐的是那份合同。继续在脑子里找灯光开关,等于拿胶片相机的说明书去开无反相机:快门还在,胶卷仓没了。
固定管线: 顶点 --内置变换--> 光栅 --内置着色--> 测试混合 --> 屏 可编程: 顶点 --你的VS--> 光栅 --你的FS--> 测试混合 --> 屏 公式在这 公式在这
仍留在“固定”里的部分,容易造成错觉。深度测试、剪裁、混合、视口、清屏,这些确实还是状态机开关。于是有人会想:“既然深度都能 enable,灯光为什么不能?”因为深度是输出合并阶段的硬件能力,灯光是着色公式。WebGL 把着色公式彻底交给了你,把输出合并留在硬件。两条边界不一样。
| 固定管线做法 | WebGL 可编程对应 | 决策权 |
|---|---|---|
| 打开 LIGHT0 并设位置颜色 | Uniform 传入光方向与颜色,着色器做点积 | 从驱动挪到片元或顶点着色器 |
| 设材质环境光散射高光 | Uniform 传入材质系数,或从纹理采样 | 你决定模型,不限于冯氏 |
glMatrixMode 乘模型视图投影 |
CPU 算好矩阵,Uniform 传 uMVP,VS 里相乘 |
矩阵乘法仍常在 CPU,应用在 GPU |
glTexEnv 调制纹理 |
FS 里 texture 结果与光照相乘或按金属度混合 |
任意公式,不再是几种环境模式 |
| 雾因子按距离插值 | VS 或 FS 按深度自己算雾 | 可换成指数雾、高度雾、体积查找 |
| 自动法线矩阵 | 你自己算法线矩阵或在 VS 里用模型矩阵的逆转置 | 算错了不会有驱动兜底 |
| 顶点颜色数组直接插值 | Attribute 进 VS,varying 到 FS | 形式像,但必须你声明 |
可编程并不禁止你做冯氏光照。它禁止的是“不写公式也有冯氏”。你完全可以在着色器里复刻固定管线那套:环境项加散射项加高光项。很多引擎的默认材质就是这么做的。差别在于,一旦写在着色器里,你就能改:把高光换成 GGX,把环境换成 IBL,把散射换成半兰伯特。固定管线改不了公式,只能调参数。
// 概念:可编程里的“灯光”只是 Uniform 加点积 uniform vec3 uLightDir; uniform vec3 uLightColor; varying vec3 vN; void main() { vec3 n = normalize(vN); float ndl = max(dot(n, normalize(uLightDir)), 0.0); gl_FragColor = vec4(uLightColor * ndl, 1.0); }
对照固定管线:上面这段里没有任何 enable。光方向不进状态表,进的是当前程序的 Uniform 槽。换程序等于换整套灯光模型。这是可编程的代价,也是红利:延迟着色、卡通着色、线稿、假体积,都只是换程序,不用等驱动实现新开关。
还有一块常被忽略的对照:顶点变换。固定管线有模型视图栈,你 translate / rotate 改变“当前矩阵”,驱动用它乘每一个顶点。WebGL 没有矩阵栈。CPU 侧用自己的库算出模型、视图、投影,乘好或分开传到 GPU,顶点着色器写 gl_Position = uP * uV * uM * aPos。谁乘谁、乘几次,没有标准答案,只有你着色器里的那一行。第 3.1 节会把 CPU 乘与 GPU 乘再对照一次;这里先记住:变换也不是开关,是你提交的数学。
固定管线对教学更友好:开关少、公式现成、错了也有一个“看起来像 3D”的默认。可编程对产品更友好:同一套硬件能画 PBR 也能画示意图。WebGL 选了后者,所以入门曲线陡。陡的部分不是 API 函数多,是你必须同时是应用程序员和着色器作者。
工程上怎么降低陡度,而不假装固定管线还在?
第一,允许自己先写“复刻固定管线”的着色器。环境加兰伯特加 Blinn 高光,三行公式,行为可预期。这不是落后,这是用可编程去模拟一条你熟悉的合同,方便对照调试。等法线贴图和金属度进来,再换公式。
第二,把仍存在的固定阶段用明白。深度、剔除、混合、模板、视口、剪裁盒,这些开关影响的是输出合并,不写进着色器。很多人把混合公式硬写进 FS,却忘了 blendFunc 还在外面挡着,结果颜色对不上。可编程不是“一切都在着色器”,是“着色在着色器,合并仍在状态机”。第 3 章专门对照这块。
第三,利用可编程才成立的路线:把场景画到纹理,再画一个全屏三角形做后处理。固定管线几乎没有“用户定义的全屏着色器”这回事。Bloom、色彩映射、SSAO、描边,全是第二次绘制加一张纹理。引擎把这包装成通道图,裸 WebGL 就是切 FBO。没有可编程片元着色器,这条路不存在。
| 任务 | 固定管线思路 | 可编程思路 | 谁更合适 |
|---|---|---|---|
| 一个方向光照立方体 | 开灯光设参数 | VS 变换 + FS 点积 | 两者都能,WebGL 只能后者 |
| 卡通分层着色 | 做不到或靠纹理彩条 hack | FS 里对 ndl 做阶梯 | 只有可编程 |
| 全屏 Bloom | 基本没有正规路径 | 离屏绘制 + 模糊着色器 | 只有可编程 |
| 半透明叠加 | 开混合设函数 | 仍开混合,FS 只出源颜色 | 固定阶段仍关键 |
| 雾 | 开雾设密度 | FS 按深度指数衰减 | 可编程可换模型 |
⚠️ 常见坑:在 WebGL 里寻找
enable灯光或shadeModel。上下文对象上没有这些符号。找到的若是引擎 API,那是引擎用着色器帮你模拟,不是硬件固定功能复活。💡 关键直觉:WebGL 把“怎么着色”全部交给你,把“怎么把片元写进帧缓冲”留下一部分给硬件。搞混这两层,会既写不好着色器,也调不对混合。
环境搭建在这个对照里其实很薄:浏览器加一个 canvas,没有单独的 SDK 要装。真正要“搭建”的是心智模型——接受没有默认灯光。有人把桌面 GL 的兼容上下文经验搬过来,以为 getContext("webgl") 还能要到固定管线,不能。WebGL 规范从一开始就不提供那条合同。教学如果先花一章装环境、再画三角形、最后才说着色器,会强化错误预期。我们把环境问题收成一句:能打开支持 WebGL 的浏览器即可;缺的不是工具链,是对可编程强制项的承认。
能,但那是引擎在替你写。Three.js 的标准材质、Babylon 的默认着色器,本质是一份预制的可编程程序加一堆 Uniform 映射。你没写 GLSL,不代表固定管线回来了。一旦要改公式,你还是得进着色器或引擎的节点材质。把引擎当固定管线用,短期爽,自定义那一天会把欠的账一次还清。下一节专门对照封装层级。
很多人不是不懂可编程,是手里还拿着一张旧开关表。我建议把旧表逐行翻译,翻译完就可以扔掉开关幻想。光照开关变成:有几盏灯、每盏的方向或位置、颜色、衰减,全部变成 Uniform 或存储在 UBO;公式写在 FS 或 VS。雾开关变成:按深度或按高度的一条曲线,参数仍是 Uniform。纹理环境里的 modulate、decal、blend 变成 FS 里乘法、混合或按金属度插值。两点光照模型变成你选兰伯特还是 Blinn,没有 shadeModel 枚举。自动归一化法线变成 VS 或 FS 里一句 normalize,忘了就条带。颜色材质两面变成你是否关剔除、是否写双面公式。
翻译的好处是心理上的:你不是“从零发明 3D”,你是把曾经由驱动代劳的公式收回来。收回来之后可以改,这才是可编程的意义。若你收回来却原样复刻,再抱怨 WebGL 难,那是在用更麻烦的方式过固定管线的日子。允许自己第一周复刻,第二周改一处公式,比如把漫反射改成半兰伯特,立刻能看见可编程的杠杆。杠杆感建立后,后处理、非真实感才不是从天上掉下来的名词。
仍留在固定阶段的测试,不要翻译进着色器。有人在 FS 里用 if 比较深度再 discard,去模仿深度测试,结果早深度优化没了,还与硬件深度打架。混合同理:FS 输出预乘色,硬件按函数合成;你在 FS 里采样“当前屏幕”是另一条河,必须离屏。认清“公式归着色器、合并归状态”,比背 API 列表更能避免重复造轮。
教学顺序我主张先承认强制项,再画三角形。先画三角形却不提为什么必须有两段着色器,学生会把样板当咒语。咒语一旦失效(换 2.0、换精度、换输出变量),他们没有对照语言。本章把强制项提前,就是为了后面缓冲与纹理节不必回头解释“为何这么麻烦”。麻烦来自决策权搬家,不是来自浏览器恶意。
下一节对照引擎封装和裸 WebGL:场景图省掉的样板,以及它藏掉的全局状态。