1.1 WebGL1 与 WebGL2 该选哪条 本节摘要:WebGL1 对应 OpenGL ES 2.0 的浏览器接口,WebGL2 对应 ES 3.0。两者不是“新旧皮肤”,而是能力边界:顶点数组对象、多渲染目标、三维与整数纹理、实例化绘制、变换反馈,在 1.0 里要靠扩展或根本没有,在 2.0 里是上下文自带。选型应对赌目标浏览器覆盖面和效果清单,而不是默认追新。 学习目标 阅读完本节,你应当能够: 用一句对照说清 WebGL1 与 WebGL2 各自对齐哪套移动图形标准 列出至少六项 2.0 内置、1.
本节摘要:WebGL1 对应 OpenGL ES 2.0 的浏览器接口,WebGL2 对应 ES 3.0。两者不是“新旧皮肤”,而是能力边界:顶点数组对象、多渲染目标、三维与整数纹理、实例化绘制、变换反馈,在 1.0 里要靠扩展或根本没有,在 2.0 里是上下文自带。选型应对赌目标浏览器覆盖面和效果清单,而不是默认追新。
阅读完本节,你应当能够:
同一块 canvas,getContext("webgl") 和 getContext("webgl2") 返回的不是同一个状态机的两个皮肤。前者合同写的是 ES 2.0:着色器是 GLSL ES 1.00,没有顶点数组对象作为核心对象,没有多渲染目标作为标配,纹理以二维为主。后者合同写的是 ES 3.0:着色器升到 GLSL ES 3.00,VAO、MRT、三维纹理、整数纹理、Uniform Buffer、变换反馈进入核心。
把这事想成两种供电标准,比想成软件升级更准确。插座外形可以长得很像,电压和针脚数不同,插错会直接没电。很多教程让你“检测不到 2 就回退到 1”,听起来稳妥,真正稳妥的前提是:你的渲染路径在两条合同下都能走完。一条依赖 gl.drawArraysInstanced 和多个颜色附件的后处理链,回退到 1.0 不是降画质,是整条链断掉。
我更倾向项目启动时先写效果清单,再决定上下文,而不是先拿得到什么就写什么。产品静态展示、单光源、一张反照率贴图,WebGL1 足够,还多覆盖一批老设备。要做延迟着色、体积雾、GPU 粒子反馈,WebGL2 不是加分项,是入场券。
效果清单 上下文合同 --------- ----------- 单通道前向 webgl 即可 MRT 后处理 必须 webgl2 或一堆扩展 实例化草地 2.0 内置;1.0 看 ANGLE_instanced_arrays 整数索引纹理 2.0;1.0 基本没得谈
覆盖面是产品问题,不是渲染问题。内部工具、新机为主的可视化,可以直接锁 2.0。面向国内广泛设备的营销页,锁 1.0 或提供 2.0 增强档。真正难受的是“主路径按 2.0 写、回退档还想看起来差不多”——那是两套渲染器,工作量接近翻倍。不要用一句 fallback 把翻倍藏起来。
下面这张表只列会改变架构的差异。各家浏览器实现细节会漂,但合同边界稳定。
| 能力 | WebGL1 | WebGL2 |
|---|---|---|
| 着色器语言 | GLSL ES 1.00,attribute 与 varying | GLSL ES 3.00,in 与 out,必须声明版本 |
| 顶点数组对象 VAO | 扩展 OES_vertex_array_object | 核心,应默认使用 |
| 多渲染目标 MRT | 扩展 WEBGL_draw_buffers | 核心 |
| 实例化绘制 | 扩展 ANGLE_instanced_arrays | 核心 |
| 三维纹理 / 二维数组纹理 | 无或极弱 | 核心 |
| 整数纹理与非归一化采样 | 基本无 | 核心 |
| 变换反馈 | 无 | 核心 |
| 统一缓冲 UBO | 无 | 核心 |
| 深度纹理 | 扩展 WEBGL_depth_texture | 核心 |
| 多采样渲染缓冲 | 有限 | 更完整 |
| 循环与纹理采样限制 | 更严,循环常要常量边界 | 宽松许多 |
读这张表时不要把它当“2.0 全能”。WebGL2 仍然是 ES 3.0,不是桌面 GL 4,也不是 WebGPU。没有计算着色器,没有真正的通用存储缓冲那套现代抽象。它相对 1.0 的跃进,主要是把一批事实标准的扩展收编进核心,让你少写扩展检测,多写稳定代码路径。
VAO 值得单独对照。WebGL1 里每次绘制都要重新绑定缓冲、重新 vertexAttribPointer,状态散落在全局。扩展 VAO 能把这组状态打包,但你得检测。WebGL2 里 VAO 是默认公民:创建、绑定、记录属性布局,绘制时切一个对象即可。我见过从 1 迁到 2 却仍每帧指指针的代码,等于把 2.0 当 1.0 用,性能红利从第一天就丢掉一半。
着色器语法也不是换皮。1.0 的 attribute / varying / gl_FragColor 在 2.0 里要改成 in / out 并写 #version 300 es。片元着色器必须自己声明输出。有人把 1.0 字符串塞进 2.0 程序,编译失败日志一长串,根因只是版本指令缺失。这条坑与“对象混用”并列,属于合同没读就开工。
#version 300 es precision mediump float; in vec2 vUv; out vec4 outColor; uniform sampler2D uTex; void main() { outColor = texture(uTex, vUv); }
对照 1.0:没有 version 行,varying 代替 in,texture2D 代替 texture,输出写到 gl_FragColor。两边各写一套工具函数并不丢人,丢人的是用宏把两套语法搅在同一文件里直到无法测试。
选型我用三问,而不是“浏览器支持百分比”这一问。
第一问:效果清单一旦拿掉 MRT、实例化、三维纹理,还剩什么?若后处理是产品卖点,锁 2.0。若只是旋转一下产品模型,1.0 更老实。
第二问:失败策略是“看不到 3D”还是“看到简化的 3D”?前者可以只申请 2.0,给静态图兜底。后者意味着两套着色器和两套资源,预算要单列。
第三问:团队有没有人维护扩展矩阵?WebGL1 靠扩展追 2.0 的部分能力,矩阵会随浏览器漂移。内部工具我几乎总锁 2.0,省掉扩展分支。
| 场景 | 建议上下文 | 理由 | 别做的事 |
|---|---|---|---|
| 电商 3D 看款 | WebGL1 为主 | 覆盖面优先,效果浅 | 为了法线贴图强上 2.0 却无回退 |
| 科学体数据 | WebGL2 | 三维纹理是刚需 | 用二维切片在 1.0 里硬模拟还当正式方案 |
| 自定义后处理链 | WebGL2 | MRT 与浮点缓冲更干净 | 用多次切换 FBO 冒充 MRT 还要求 60 帧 |
| 教学最小三角形 | 两个都写一遍 | 对照学习本身就是目标 | 只教 1.0 却在评论区承诺“和 2.0 一样” |
| 游戏草地与粒子 | WebGL2 | 实例化与变换反馈 | 用 CPU 更新一万个矩阵还怪 GPU |
申请上下文的概念顺序应当是:先决定合同,再拿对象。
function createGL(canvas, prefer2) { if (prefer2) { const gl2 = canvas.getContext("webgl2"); if (gl2) return { gl: gl2, version: 2 }; } const gl1 = canvas.getContext("webgl"); if (gl1) return { gl: gl1, version: 1 }; return { gl: null, version: 0 }; }
这段代码本身没有立场。立场在 prefer2 从哪来:它应来自效果清单,而不是来自“机器上试了一下能拿到 2”。开发机几乎总能拿到 2,用户机不一定。用开发机的成功当产品策略,是 WebGL 项目里最常见的乐观病。
⚠️ 常见坑:在 WebGL1 上下文里调用
createVertexArray,或把 WebGL2 的纹理内部格式拿到 1.0 的texImage2D。报错信息常只说 invalid enum,不会告诉你“你拿错了合同”。💡 关键直觉:版本选择是架构决策。回退不是免费的兼容,而是第二条产品路径。写不起第二条,就别在宣传里承诺第一条会自动降级。
扩展补丁和 2.0 内置的细账放到第 4.5 节。这里只需记住一条:WebGL1 的扩展能补上深度纹理、VAO、实例化、部分浮点纹理,补不上变换反馈和完整的三维纹理工作流。补得上的,仍要面对“扩展名在各浏览器不一致、精度声明不一致”。能锁 2.0 就锁,是我给内部工具的默认建议。
看你是否在用扩展模拟 2.0。若已经依赖 VAO、实例化、深度纹理、draw buffers,迁移的本质是删检测、改着色器版本、改纹理格式枚举,收益是少测几家扩展。若代码仍是每帧 vertexAttribPointer、单渲染目标、CPU 粒子,迁 2.0 几乎不提速,应先改数据路径,再谈版本。版本是合同,不是性能开关。
我做过一次内部工具从 1.0 迁 2.0,真正耗时的不是改 getContext 字符串,而是三处合同裂缝。第一处是着色器:一百多份 1.00 源码,attribute 与 texture2D 散落各处。我们没有写正则替换器,而是按“前向不透明、透明、后处理、调试色块”分成四套 300 es 模板,旧文件对照模板手工搬公式。两周看起来慢,之后再也没有“某台机器编译失败只因漏了 version 行”的夜班。第二处是纹理内部格式。1.0 写 RGBA 对 RGBA 能混过去的地方,2.0 要对齐内部格式、类型、是否 renderable。浮点中间缓冲在 1.0 靠扩展过滤,迁过去后有的机子不能线性过滤半浮点,Bloom 变成马赛克。我们把 HDR 档做成能力开关:过滤失败就退回 8 位加曝光曲线,而不是假设 2.0 等于浮点自由。第三处是 VAO。旧代码每 draw 指一次指针,迁完仍这么写,帧时间几乎没动。直到把布局收进 VAO,CPU 侧绑定时间才掉下来。版本是入场券,习惯才是票根。
对外产品更麻烦。若市场部要求“老设备也能看 3D”,你要承认那是第二条渲染器:可能关掉 MRT 后处理,用前向单通道;可能关掉实例化,草地改成几簇静态网格。工作量按“两个产品”估,不要按“加一个 else”估。我见过 else 里仍调用 drawBuffers,老设备直接上下文丢失。丢失之后默认 FBO 也不一定还在,用户看见白屏,日志在他们手机里。启动时把 version 与关键扩展打进一次遥测,比事后猜有用。
桌面开发机几乎总能拿到 2.0,这会让人产生“合同已经统一”的错觉。真机抽样至少覆盖:一台较新的安卓、一台较旧的安卓、一台 iOS、一台公司标配 Windows。抽样不是为了百分百覆盖,是为了打掉开发机偏见。若四台里有一台只能 1.0,而效果清单依赖变换反馈,清单就必须改,而不是让那一台“尽量试试”。
能,但分支会变成无法测试的森林。优先按能力对象分支:有没有 createVertexArray、有没有浮点渲染、最大颜色附件数。GPU 名称当遥测可以,当控制流会在驱动改名后失效。能力查询是合同语言,型号字符串是八卦。
getContext 的第二参数是另一张小合同:alpha、depth、stencil、antialias、preserveDrawingBuffer、powerPreference。深度缓冲默认通常有,但你可以关掉,关掉后 3.3 节的测试无附件可用。抗锯齿只作用于默认 FBO,用户 FBO 要另走多重采样。preserveDrawingBuffer 为 true 便于截图,可能让实现放弃某些优化。powerPreference 在有的浏览器能申请独显,失败则静默。这些属性在 1.0 与 2.0 都存在,但与版本能力正交。版本决定对象模型,属性决定默认屏的附件与功耗。两者都要在启动时一次选定,运行时改不了。选定后打印出来,避免测试机与生产机属性不一致还怪着色器。
“拿到了 2 却像 1”的清单:不用 VAO、不用实例化、不用 UBO、着色器仍按 1.00 思维写循环展开、纹理仍按 RGBA8 走 HDR。清单上全中,帧时间不会因版本下降。迁移验收应包含:至少一处 VAO、至少一处 300 es 的 in/out、至少一处 2.0 内部格式。没有行为变化的迁移,只是改了字符串,不能写进业绩。
下一节对照固定管线记忆和可编程现实:浏览器里没有“打开灯光”这回事,着色器从第一天就是强制项。