本文目录 16 个章节
我现在需要深入理解Unity3D工程构建为WebGL的技术框架。特别是:涉及到哪几种语言,语言编译的工具链是什么,最终产物是什么,涉及到哪些模块或者层级,层级之间的调用关系是什么。这样我可以理解Unity3D - WebGL 应用的完整的体系结构与技术底层,乃至于我基本上能在大脑中模拟、认识应用的完整结构
Unity3D WebGL 框架全景

0|WebGL应用介绍
容器
无论是“标准浏览器 WebGL 应用”还是“微信/抖音小程序版 WebGL”,它们都被装在 “JS 引擎(逻辑) + 渲染容器(如WebView) + JSBridge” 这一三层盒子里,而这三层盒子的组合就是广义的 运行时。
| 概念 | 在浏览器里的体现 | 在小程序里的体现 | 说明 |
|---|---|---|---|
| 脚本运行时 (JavaScript Engine) | V8、SpiderMonkey、JavaScriptCore | 同样是 V8 或 QuickJS 的裁剪版;有时用独立进程 | 真正解释 / JIT JS 的地方 |
| 渲染容器 (WebView / Canvas) | <canvas> + GPU 进程 / WebGL context |
宿主 App 内部的轻量级 WebView 或自己实现的 GPU 绑定 | 把像素画到屏幕、接触 GPU |
| 系统调用网关 (JSBridge) | 原生 Web API 由浏览器统一维护 | wx.* / tt.* / my.* … 各家实现 |
让 JS 能用摄像头、支付等特权能力 |
| 安全沙箱 | 同源策略、CSP、Iframe 隔离 | App 级沙箱 + 白名单域名 | 防数据越界、防恶意脚本 |
共性
前端逻辑全部写成 JavaScript/TypeScript
- 无论是在 PC/Mobile 浏览器里跑的标准 WebGL 游戏,还是嵌入微信、抖音、支付宝的小程序——核心代码都是 JS(或编译到 JS 的 WASM/asm.js),靠同一套事件循环、同一套垃圾回收与语法特性。
图形接口都是 WebGL(或 WebGL 封装)
- 浏览器里直接拿
canvas.getContext('webgl')。 - 小程序侧通常把 WebGL API 重新暴露一遍(如
wx.createWebGLRenderingContext、tt.createWebGLRenderingContext),底下仍是 OpenGL ES/Metal/Vulkan 到 GPU 的固定流水线。
- 浏览器里直接拿
运行在沙箱里的单线程事件循环
- 代码只能通过异步回调或
postMessage()与宿主通信,线程安全由宿主保证。 - 内存和文件系统均受限,只能用宿主暴露的 Storage、IndexedDB、临时文件接口。
- 代码只能通过异步回调或
通过“宿主-JSBridge”访问原生能力
- 浏览器的
Web API(Fetch、WebSocket、Audio …)。 - 小程序的
wx.* / tt.* / my.*能力集合(网络、支付、定位)。 - 逻辑上都是把 JS 调用序列化 → 原生线程执行 → 回调结果再序列化回 JS。
- 浏览器的
1 | Emscripten是什么?它和 Clang / LLVM 是什么关系?
| 层级 | 职责 | 关键产物 |
|---|---|---|
| Clang | C/C++ 前端:把源码 ➜ 统一的 LLVM IR | .bc (LLVM bit‑code) |
| LLVM | 通用优化 & 代码生成框架;包含 wasm32/wasm64 目标后端 | 优化后的 IR |
Emscripten(emcc 驱动) |
把 “生成 Web 可运行工件” 所有脏活包揽下来:① 调用 Clang 产出 LLVM IR② 调 wasm-ld / Binaryen 优化、链接成 .wasm ③ 生成 Glue JS (xxx.framework.js),提供 POSIX 运行时、文件系统、gl*/emscripten_* 系统调用④ 包含 emcc, emar, emconfigure 等前端脚本,像一套 “Unix for WebAssembly” SDK |
.wasm + *.js + *.html |
简言之:Clang/LLVM 负责“把 C++ 变 IR→机器码”,Emscripten 负责“把 IR 变 Web 可执行 + 给它配齐生态”。Emscripten 在内部仍使用标准 LLVM 后端,但再包上一层 JS Runtime 和工具链特有库。(Stack Overflow, emscripten.org)
2 | WebAssembly 与传统汇编(二进制机器码)的关系
| 维度 | WebAssembly (.wasm) | 传统汇编/机器码 (x86/ARM) |
|---|---|---|
| 目标对象 | 抽象、虚拟的栈机 ISA;与硬件无关 | 针对单一 CPU 架构;寄存器直接暴露 |
| 格式 | 压缩二进制段 + 结构化控制流 | 二进制(ELF/PE) 或文本 .s |
| 安全 | 强沙盒;无指针运算、无自扩写代码;加载时字节级验证 | 原生权限;可能执行任意指令 |
| 运行流程 | 下载 → 解析/验证 → JIT/AOT 编译 → 执行 | 已是最终机器码,CPU 直接执行 |
| 调试符号 | .wasm.map, DWARF ► DevTools |
.pdb, .dSYM, ELF .symtab |
执行链条
- 浏览器或 Wasm 运行时读取
.wasm,做 字节码验证。 - 使用 Baseline JIT(Chrome Liftoff, Firefox Baseline)快速翻译到本机指令并缓存。
- 热路径再交给 优化 JIT(Chrome TurboFan, Firefox Ion)重编译,得到接近本地 C++ 性能的机器码。(MDN Web Docs, Stack Overflow)
所以 .wasm 是“可移植汇编” → 浏览器即时变成本机汇编;而传统静态编译直接在构建时就生成特定 CPU 的汇编 / 机器码。
3 | Glue JS 是什么?它与普通 JavaScript 有何不同?
| 特征 | Glue JS (xxx.framework.js) |
手写 JavaScript |
|---|---|---|
| 来源 | Emscripten 自动生成 | 开发者撰写 |
| 职责 | - 加载 & 实例化 .wasm (WebAssembly.instantiateStreaming)- 构造全局 Module 对象,导出 ccall/cwrap、FS、GL、Pthreads shim- 把 C/POSIX 调用(open, glDrawArrays, …)映射到浏览器 API 或 TypedArray 操作- 处理字符串/数组在 JS ↔ Wasm 之间的编码、堆管理 |
任意业务逻辑 |
| 语言 | 仍是标准 ES5/ES6 JS;但大量宏化、函数名压缩、TypedArray 操作 | 标准 JS |
| 与 Wasm 交互 | 双向 FFI:• JS 调用 wasm.exports.myFunc 或 cwrap• Wasm 侧通过 EM_ASM 或 dynCall_js 回到 JS |
仅在 JS 世界 |
把它叫“Glue”是因为它 “黏合” 两个运行时:一头连 Wasm 的线性内存/函数导出,另一头连浏览器 Web APIs,让 C/C++ 代码能“像在桌面 POSIX 环境”那样调用文件、线程、OpenGL,却最终落到 JS 方法和 WebGL。(emscripten.org, liveBook)
Unity → WebGL 构建体系结构全景
一句话先行 Unity 把 C# 脚本 + C++ 引擎 经过 IL2CPP → Emscripten 编译成 WebAssembly (.wasm),再配上一组 Glue JS 和资源包,最终在浏览器里由一段 Loader 脚本 拉起运行时,借 WebGL/WebAudio 等 API 驱动 GPU 和外设。
1 涉及的语言与职责
| 语言 | 典型文件 | 在构建链里的角色 |
|---|---|---|
| C# | *.cs(游戏脚本) |
被 IL2CPP 转成 C++ |
| C / C++ | Unity Runtime(物理、渲染核心) | 与脚本一起进 wasm |
| JavaScript | *.loader.js, *.framework.js, *.jslib |
Glue/Boot loader;桥接浏览器 API |
| WebAssembly (binary) | *.wasm |
执行沙盒,承载全部 native 逻辑 |
| HTML | index.html |
页面容器,注入 Loader |
| GLSL ES | (打包入 Asset) | GPU 着色器,由 WebGL 编译 |
2 编译工具链
C# ─► IL2CPP (C# → C++)
│
C/C++ (引擎) ─┤
▼
Clang/LLVM
│ (via Emscripten)
▼
wasm-ld ──► unity.wasm
│
Binaryen/opt (wasm‑opt, DWARF→map)
- IL2CPP:将托管 IL 解码成等价 C++ 源,再交给 C++ 编译器。
- Emscripten:封装 Clang + wasm‑ld;生成
.wasm与 Glue JS (framework.js)。(Unity Docs) - wasm‑opt / wasm‑strip:二进制优化、符号剥离。
- Brotli/GZIP:构建后可选压缩,浏览器端解码。(Unity Discussions)
3 最终产物目录(Unity 2022+ 默认)
Build/
├─ WebGLBuild.loader.js ← Boot & progress bar
├─ WebGLBuild.framework.js← Emscripten glue, Module 对象
├─ WebGLBuild.data ← 资源包 (场景、纹理…)
├─ WebGLBuild.wasm ← C++/C# 编译结果
├─ WebGLBuild.js.symbols ← 堆栈符号 (可选)
└─ WebGLBuild.wasm.br ← Brotli 压缩版 (可选)
index.html ← 模板,可插入脚本/UI
旧版本只有 UnityLoader.js + UnityWebGL.js,2020 LTS 起拆分为 *.loader.js + *.framework.js. (Stack Overflow)
4 运行时层级与调用关系
graph TD
A[index.html] --> B(Loader JS)
B --> C{准备阶段}
C -->|下载 *.data *.wasm| D(Module.configure)
D --> E[WebAssembly.instantiateStreaming]
E --> F{Wasm VM}
F --> G[Unity Runtime & GameLogic]
G -->|JSlib/SendMessage| H(Glue JS calls)
H --> I(Web APIs: WebGL, WebAudio, Input)
I -->|GPU cmds| J(GPU Driver)
Loader JS 把文件名映射到 URL(可带解压),显示进度条。
framework.js 创建
Module对象:- 定义
preloadPlugins,onRuntimeInitialized,cwrap,ccall。
- 定义
WebAssembly.instantiateStreaming把.wasm+内存映射到 VM。Wasm 内部通过 Emscripten syscalls(
gl*,emscripten_*) 回到 JS 层,再调 WebGL/WebAudio。JSlib 插件:用户写的
*.jslib可通过EMSCRIPTEN_BINDINGS让 C# 调浏览器 API。
5 模块职责一览
| 层级 | 主要文件 | 职责 | 调用方向 |
|---|---|---|---|
| Boot 入口 | .loader.js |
找 CDN、本地路径、解压、创建 <script> |
↑ HTML |
| Glue / Shell | .framework.js |
配置 Module、映射 gl* Syscalls、管理 wasm 内存 |
↔ Boot / Wasm / Browser |
| Wasm VM | .wasm |
执行 Unity Runtime、游戏脚本 JIT 后逻辑 | ↑ Glue |
| 浏览器 API 层 | WebGL/WebAudio/DOM | GPU、音频、输入、文件 | n/a |
6 JS ↔ Wasm 互操作示例
C# 调 JS
[DllImport("__Internal")]
private static extern void JsAlert(string msg);
JsAlert("Hello WebGL!");
对应 plugin.jslib
mergeInto(LibraryManager.library, {
JsAlert: function (ptr){
alert(UTF8ToString(ptr));
}
});
JS 调 C#
Module.ccall('OnExternalEvent', null, ['number'], [42]);
7 多线程与文件系统补充
- Pthreads:
Player Settings ▸ Enable Threads→ Emscripten 生成.worker.js;需Cross-Origin-Opener-Policy与SharedArrayBuffer安全头。 - Virtual FS:Emscripten
MEMFS把*.data解包到 wasm 内存;可映射 IndexedDB 做持久化。
8 Unity WebGL vs 微信原生小游戏(复习)
| 方面 | Unity WebGL | 原生小游戏 |
|---|---|---|
| 语言 | C#/Wasm | JS/TS |
| 引擎 | Unity Runtime | WeChat Game Runtime (CocosLite) |
| 加载链 | Loader JS + Wasm | JS 直跑 |
| 包体 | 首包 5‑20 MB | < 4 MB |
| 渲染 | WebGL | WebGL/Canvas2D |
| 线程 | Pthreads(灰度) | 主线程 + Worker |
9 脑内模拟口诀
“Loader 找文件 → Glue 创 Module → Wasm 起 VM → Runtime 写 WebGL”
- 想象
.wasm像 DLL;framework.js像 C 动态链接器;loader.js像 启动脚本。 - 每次
glDrawArrays源自 Wasm 调用,穿过 Glue 映射成浏览器 WebGL 命令。 - 所有 C# 调 JS 的桥都在
.jslib汇合;反向则用ccall/cwrap。
掌握这些层级和调用流,你就能在脑海中精准定位:崩溃发生在哪段、性能瓶颈在何层、要拦截数据应挂在哪边。后续无论补符号映射、注入监控,亦或改 Loader 自定义 CDN 逻辑,都可胸有成竹。