1.3 引擎封装 vs 裸 WebGL


文档摘要

1.3 引擎封装 vs 裸 WebGL 本节摘要:Three.js、Babylon.js 一类库把 WebGL 状态机包成场景图:网格、材质、相机、灯光是对象,绘制循环由引擎调度。裸 WebGL 则要求你自己绑定缓冲、切程序、设 Uniform、调用绘制。封装省的是样板与矩阵,藏的是全局状态、合批顺序和上下文版本细节。选型应按“要不要自定义管线”来,而不是按“会不会写底层”的自尊来。

1.3 引擎封装 vs 裸 WebGL

本节摘要:Three.js、Babylon.js 一类库把 WebGL 状态机包成场景图:网格、材质、相机、灯光是对象,绘制循环由引擎调度。裸 WebGL 则要求你自己绑定缓冲、切程序、设 Uniform、调用绘制。封装省的是样板与矩阵,藏的是全局状态、合批顺序和上下文版本细节。选型应按“要不要自定义管线”来,而不是按“会不会写底层”的自尊来。

你能学到什么

阅读完本节,你应当能够:

  1. 画出引擎场景图与裸状态机的职责分层,指出绘制命令最终仍落在同一套 GPU API
  2. 对照列出引擎省掉的样板,以及出问题时你失去的可见性
  3. 给出产品展示、教学、自定义后处理、自研渲染器四类场景的封装建议
  4. 识别“引擎外再直接改 gl 状态”这种混用为什么会把画面弄乱
  5. 说明 PixiJS 一类 2D WebGL 引擎与 3D 场景图不是同一层封装

同一块 GPU,两种工作语言

裸 WebGL 的工作语言是状态机:当前程序是谁、当前 ARRAY_BUFFER 是谁、属性指针怎么解释、深度测试开不开。你发出的每一句都立刻改变全局上下文,直到下一句再改。引擎的工作语言是场景图:这个网格用哪份几何和哪份材质、相机在哪、这一帧谁可见。引擎在内部把场景图翻译回状态机调用。GPU 只看见后者。

把这事比成剧场更清楚。裸 API 是你亲自对灯、拉幕、换景片,顺序错了观众立刻看见穿帮。引擎是舞台监督:你告诉他“这场有三把椅子、一盏追光”,他去排灯光表。椅子多的时候监督很值;你要搞实验戏剧、每场改电路,监督反而挡手。

第一个能跑的三角形,裸 WebGL 往往要:创建上下文、编译两段着色器、链程序、建缓冲、写顶点、取 attribute 位置、设指针、enable、清屏、设矩阵、draw。引擎则是:场景、相机、渲染器、一个网格。行数差一个数量级。行数差会诱人得出错误结论——“引擎更高级,裸 API 过时了”。过时的不是状态机,是你重复手写状态机。状态机仍在引擎脚下转动。

你的业务代码 │ ├─ 引擎路径:场景图 → 引擎翻译 → gl 调用 → GPU │ └─ 裸路径:直接 gl 调用 → GPU

对照:省掉什么,藏掉什么

维度 裸 WebGL 典型 3D 引擎 谁该在意
着色器 自己编译链接,失败自己读日志 材质对象生成或缓存程序 自定义公式时两边都要会读日志
缓冲与 VAO 每步绑定显式可见 几何对象上传,绑定被调度器藏起 排查错网格、错 stride 时裸更透明
矩阵 自己乘 MVP 节点世界矩阵 × 相机投影 骨骼、实例化要看引擎是否开放
合批 你不写就不存在 引擎按材质排序尝试减少切状态 透明物体顺序乱了先查引擎策略
上下文版本 你自己申请 webgl 或 webgl2 引擎按能力降级或选渲染器 锁版本时要读引擎文档而不是猜
资源释放 deleteBuffer 等显式 dispose 约定,漏了会漏显存 单页长应用两边都致命
学习曲线 陡,但每条调用可对规范 缓,但抽象泄漏时更陡 教学用裸;交付用引擎更常见

引擎不是一块,分层也不一样。Three.js 偏场景图加可替换材质;Babylon.js 偏完整运行时,加载、物理、GUI、工具面板都在;PixiJS 偏 2D 精灵与批处理,内部同样吃 WebGL,但没有你以为的那套 3D 相机约定。拿 Pixi 的心智去写 Three 的 Mesh,或反过来,都会觉得“封装怎么这么怪”。封装比较必须先对齐“它在替你管哪一层”。

我更倾向这样切:交付一个有灯光有模型的页面,默认引擎;交付一个渲染器或教学,默认裸;中间地带——只要自定义后处理或特殊合批——先确认引擎的扩展点够不够。Three 的 RawShaderMaterial、Babylon 的自定义过程,是“还在场景图里写着色器”。一旦你开始绕过渲染器直接拿 gl 改混合函数,封装合同就撕了:引擎下一帧可能把状态写回去,你的修改像幽灵一样时灵时不灵。

// 概念:混用是合同撕裂,不是加速技巧 renderer.render(scene, camera); const gl = renderer.getContext(); gl.disable(gl.DEPTH_TEST); // 引擎不知道你改了 renderer.render(scene, camera); // 深度状态可能被引擎改回,也可能没改回

这种代码的危险不在禁止你拿上下文,而在双方都以为自己拥有状态机。要么全程裸,要么只走引擎公开的钩子。调试“偶发闪烁”时,优先怀疑混用,而不是怀疑 GPU 驱动。

图:封装层级与你仍必须理解的层

图:封装层级与你仍必须理解的层

怎么选:按管线控制权,不按身份认同

“我是底层程序员所以必须裸写”和“我赶工期所以必须上引擎”都把决策交给了身份。更好的三问:

第一,管线是否就是产品?渲染器、可视化内核、引擎本身,裸写。管线只是展示手段,引擎。

第二,生命周期是否长过一页活动?活动页用引擎甚至用现成查看器。三年要长的编辑器,要么深度绑定一个引擎的扩展点,要么自研核心、外围再用引擎。

第三,团队有没有人能读 gl 调用?全员只会场景图时,强上裸 WebGL 会把进度锁死在第一张纹理。有人能读调用表时,即使产品用引擎,也应保留“看一帧状态”的能力,否则合批和透明排序出问题只能祈福。

项目类型 建议 理由
营销 3D 看款 引擎或现成查看器 灯光模型加载交互都是重复劳动
课程“三角形为什么是斜的” 裸 WebGL 目标就是看见缓冲和着色器
后处理实验 引擎加自定义材质,或小裸框架 看扩展点够不够切 FBO
自研 Web 渲染器 裸 WebGL 或 WebGPU 场景图会变成你要拆除的墙
2D 大量精灵 2D WebGL 引擎 3D 场景图合批模型不对路

资源加载也常被拿来当选引擎的理由。引擎自带 glTF 加载、纹理解码、压缩纹理回退;裸 WebGL 要从解析二进制开始。这是真差异,但不是不可拆:你可以裸写绘制,加载仍用独立解析库。把“能加载模型”等价于“必须上完整场景图”,会在不需要灯光系统的可视化里拖进一整座运行时。

⚠️ 常见坑:在引擎的渲染循环外偷偷 bindTexture 或改混合,再用引擎截图。截到的画面与屏幕不一致时,优先查状态所有权,而不是查像素格式。

💡 关键直觉:封装比较的是控制权账单。引擎把每帧几百次状态切换做成对象赋值;账单是你调试时必须信任它的翻译。翻译不透明的那一天,你仍要会读状态机。

第一个程序该用哪条路?若目标是理解第 2 章的缓冲与着色器,用裸 API 画一个彩色三角形,把每一句调用标在 CPU 或 GPU 一侧。若目标是下周上线一个可旋转的产品,用引擎,把第 2 章当对照手册而不是当开工方式。本教程后文默认对照裸调用,因为引擎差异太大;读引擎文档时,把网格当 VAO 加绘制,把材质当程序加 Uniform,对照就能接上。

问题:能不能先裸写一套迷你引擎?

可以,而且很多团队最后都会有一个“自己的薄封装”:程序缓存、VAO 包装、纹理单元分配。那不是 Three 的竞品,是防止全局状态散落的卫生层。薄封装保持调用可见;场景图引擎开始替你决定绘制顺序。两者之间隔着合批和场景管理。需要卫生层不等于需要场景图。

薄封装、场景图、完整运行时,三层不要跳级

团队常从“受不了样板”直接跳到完整引擎,半年后又在引擎里写回状态机。中间其实有一层:自己的 program 缓存、VAO 包装、纹理单元分配器、默认状态表。薄封装大约几百行,调用仍然可见,出问题能打印当前绑定。它解决卫生,不解决场景管理。场景图开始替你排序、剔除、合批,这才是引擎的价值,也才是不透明的来源。完整运行时再把加载、物理、GUI、序列化捆进来。跳级的代价是:你为了少写 bind,买来了物理世界与编辑器,然后在活动页里用不到,却要为包体和概念买单。

反过来,教学如果从第一天用完整运行时,学生会认为 Mesh 是 GPU 原语。等到要做拾取离屏,找不到 FBO 插口,只能抄引擎示例。示例一升级,作业全碎。所以课程用裸加薄封装,产品用场景图,重工具用完整运行时。三层可以在同一公司并存:查看器用引擎,内核实验用裸。禁止的是同一 canvas 上两套所有者抢状态。

包体与启动时间也是封装账单。引擎为了通用,会带一堆你没请求的着色器变体与加载器。营销页只转一个模型,却下载完整运行时,首屏被 JS 解析拖死——这是 CPU 税,第 4.7 节还会碰到。按页面寿命选封装:活三天的活动,现成查看器;活三年的编辑器,值得自研薄层甚至自研场景图。年限比“爱不爱底层”更诚实。

问题:开源引擎源码读不懂,是不是说明必须裸写?

不一定。读不懂源码时,先读一帧调用表:用调试器看 useProgram、bind、draw 的顺序。那是状态机摘要。摘要能对上第 2 章,你就已经能定位合批与透明队列。源码是翻译器实现,调用表是合同现场。现场比实现更好上手。真要改翻译器,才需要源码。多数项目改的是材质与资源,不是翻译器。

何时拆引擎,何时忍

拆引擎的正当理由:合批策略与产品冲突且钩子不够;包体无法接受;许可证或招聘约束;需要的 GPU 路径引擎几年不暴露。不正当理由:讨厌抽象、想在简历写自研、一次透明 bug。自研成本按年计。忍的技术是:用 RawShader 或自定义 pass 插入,而不是 fork 整棵树。fork 之后你吃不到上游修复。对照再一次:要的是插入点还是所有权。插入点用引擎;所有权用裸或薄封装。

多人协作时,封装还决定调试语言。全员说 Mesh 与 Material,沟通快。有人说 VAO 有人说 Mesh,同一 bug 要对译半小时。约定项目语言。即使底层是裸,也可以在薄封装上用网格一词,但文档必须写明映射。映射表就是第 2 章在你们仓库里的投影。

核心回顾

  • 同一 GPU:引擎与裸 API 最终都落到绑定、设状态、draw;差别在谁发出这些调用。
  • 语言不同:场景图说网格与材质,状态机说程序与缓冲;翻译层出 bug 时要用状态机读。
  • 混用撕裂合同:不要在引擎背后改 gl 状态,除非引擎把该钩子写成稳定 API。
  • 2D 引擎不是 3D 引擎:批处理精灵与场景图网格不要混着选型。
  • 按控制权选:管线即产品则裸;展示即产品则引擎;中间看扩展点。
  • 加载可拆:能解析 glTF 不等于必须引入完整运行时。

下一章沿 CPU 准备与 GPU 绘制这条轴,把管线、顶点、缓冲、着色器、纹理逐项对照。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U