
实测环境Babylon.js9.30.0、Vite 8.3.4、TypeScript 5.9.3、Windows 11、Chrome。本章所有体积/警告数字都来自本机当次运行日志build.log、dev.log复现命令在 3.8。本章代码src/chapters/ch03-toolchain.ts 对照实验页public/toolchain-umd.html。学完这一章你拥有的不是知道有几个工具而是一条闭环工作流看到一个例子 → 判断它属于哪条路线 → 搬到本地 → 跑起来 → 用 Inspector 看它在做什么 → 量出代价后面 45 章你每天都会走这条链至少一次。3.1 本章要做成什么样本章画面上是一个蓝灰立方体左下方两个按钮① 调试面板Inspector ② 打开 UMD 对照页全局 BABYLON 对象下方面板打印着本章的核心结论——包体积实测表数字见 3.8。点 ① 右侧滑出属性树点 ② 新标签页打开一个不用import、不用 TS、不用构建的橙色方块页面第一屏明显更慢。【图 3-1Inspector 打开状态截图人工验收时补】【图 3-2UMD 对照页截图人工验收时补】3.2 本书的工程结构先看清地形babylonjs/ ├─ index.html 实验台外壳左侧章目录 画布 HUD 本章信息栏 ├─ vite.config.ts 端口 18000 / 0.0.0.0 / strictPort ├─ vite.bench.config.ts 体积基准专用构建3.8 ├─ tsconfig.json strict 系列开关第 2 章 2.3.5 ├─ src/ │ ├─ hub.ts Engine 单例 渲染循环 章切换与销毁 HUD │ ├─ core/chapter.ts 章节契约meta / create / ChapterHandle │ └─ chapters/chNN-*.ts 每章一个文件本章是 ch03-toolchain.ts ├─ bench/min.ts 第 4 章十行代码的独立副本只用于测体积 ├─ public/ │ ├─ toolchain-umd.html 本章的全局脚本对照页 │ └─ vendor/babylon.max.js UMD 包的静态副本8.22 MiB ├─ tools/new-chapter.mjs 生成新章骨架 ├─ 手稿/chNN-*.md 正文 └─ docs/ 大纲、写作规范、坑总表、体积基准三个设计决定值得你知道为什么一个页面跑全书章节而不是每章一份工程。48 份重复的package.json只会让读者在装依赖上浪费生命。代价是章节必须遵守统一契约。章节契约是metacreatesrc/core/chapter.ts。create收到{ engine, canvas, stage, overlay, describe }返回{ scene, dispose }。hub.ts用 Vite 的import.meta.glob(./chapters/*.ts)自动发现它们——你新增一个文件左侧目录就自动出现一章。Engine全局只有一个每章新建Scene并在离开时dispose()。这正是第 4 章 4.7 讲的一个页面一台引擎多个场景的工程实现。新增一章只要一条命令npmrun new-chapter12pbr-intuition从顶点到像素渲染管线直觉3.3 开发环境三件套dev/build/typechecknpmrun dev# 起本地服务器改代码即时生效npmrun build# 先 tsc --noEmit 再做生产构建产物在 dist/npmrun typecheck# 只查类型最快dev和build检查的东西不一样这是新手最容易忽略的一点vite dev用 esbuild/oxc 之类做转译基本不做类型检查——类型错了照样能跑。vite build才产出真实体积也才会暴露 tree-shaking、动态导入分块的结果。tsc --noEmit只管类型不管能不能跑。本书的纪律是每章交付前三个都跑并且build的产物大小要记进docs/体积基准.md第 41 章的性能承诺靠这份数据不靠感觉。顺便vite.config.ts里strictPort: true是故意的。server:{host:0.0.0.0,port:18000,strictPort:true}host: 0.0.0.0让同网段的手机能连进来第 43、44 章必须用它。端口选 18000 是因为这台机器上 Windows 保留了大片区间。实测输出netsh int ipv4 show excludedportrange protocoltcp2180–2779、6203–6302、50000–50059等。落在保留区里的端口listen报的是EACCES权限不是EADDRINUSE占用——不写清楚你会去查防火墙。strictPort: true很重要默认 Vite 端口被占会静默换端口你在手机上输的地址就对不上了。宁可启动失败也不要偷偷换端口。3.4 在线工具Playground、Sandbox、以及版本这个陷阱Babylon 官方提供几个在线工具本书把它们当验证工具而不是学习主战场。用法要点界面细节以你打开时为准本书只对 API 行为负责Playground左边写代码、右边即时出画面能分享链接。它最适合我只想验证某一个 API 的一个参数。第 9 章调MeshBuilder参数、第 14 章试贴图 UV 时比在本地起工程快得多。Sandbox把一个.glb拖进浏览器立刻看到模型、材质、动画、包围盒。第 32 章用它做导出体检——模型在 Sandbox 里就是黑的/朝向不对那是资产问题不是你的代码问题。这个区分能帮你省掉几小时。陷阱在线工具的引擎版本和你本地的不一定一致。Playground 这类站点通常可以选版本有的默认最新预览版。你在 Playground 里验证成功的一段代码回到本地9.30.0可能报错反之亦然。本书的规矩从网上搬代码先看它用的写法在本地tsc下报不报错——编译器是最便宜的版本检测器。搬进本地后跑一次再和 Playground 的表现对照一次不一致就记进docs/坑总表.md第 4 章的 4-01/4-02 就是这条流程抓出来的。不要假设在线示例 最新版。Babylon 大约每周一版第 1 章 1.3.1 的 npm 时间戳实测示例落后半年是常态。3.5 Inspector本书最重要的老师Inspector 是运行时的场景检查器。它对你的价值不是炫而是把抽象变可见场景树谁是谁的父节点、谁被隐藏了第 8 章选中一个网格 → 看它的position / rotation / scaling / 包围盒 / 材质还能当场拖动数值看画面变化材质面板贴图通道、PBR 参数第 13–17 章的每个参数都在这里试灯光与阴影设置第 19 章帧统计、纹理与显存信息第 41 章v9 的入口源码babylonjs/inspector/lib/inspector.d.tsexportdeclarefunctionShowInspector(scene:Scene,options?:PartialInspectorOptions):InspectorTokenInspectorOptions里本书常用一项layoutMode?:inline|overlay// 默认 overlayinline 会把 canvas 一起嵌进面板容器返回值InspectorToken{dispose():Promisevoid,isDisposed:boolean,onDisposed:IReadOnlyObservablevoid}本章代码的完整用法const{ShowInspector}awaitimport(babylonjs/inspector)// 懒加载见 3.7consttokenShowInspector(scene)awaittoken.dispose()// dispose 是异步的为什么await import()而不是文件顶部import因为 Inspector 是 React 实现的包大。本章的实测分块npm run build产物见 3.8能说明代价懒加载时它不在首屏路径上写进主入口就要背进初始包。同包还导出两个对你有用的东西我在lib/index.d.ts里核实的export{AttachDebugLayer,DetachDebugLayer}from./legacy/debugLayerAttachDebugLayer(scene)会在画面左上贴一块引擎自带的调试层帧率、绘制统计等。它比 Inspector 轻适合手机上快速看一眼。注意它在legacy/路径下——本书会用它但如果哪天它消失第一反应是换 Inspector 的统计面板第 41 章实测。3.5.1 Inspector 的五件具体用途新手常把 Inspector 当炫技面板实际上它是你写代码时的第二双眼睛。下面五件事是你几乎每章都会做的① 当场改数值看画面变化。选中一个网格 → 右侧属性栏里position / rotation / scaling都能直接拖滑块或输入数字。比如你不确定rotation.y Math.PI / 4到底转了多少度先在 Inspector 里手动拖到 45°看到效果后再回代码里写死。这省掉改一行→刷新页面→找角度的循环。② 确认材质通道是否真的生效。第 14–17 章的 PBR 参数albedo/metallic/roughness/clearcoat/anisotropy在 Inspector 的材质面板里有实时预览贴图有没有挂上、通道值对不对、法线贴图方向反没反。很多时候画面看起来不对不是算法问题而是贴图 UV 错了或通道名拼错——Inspector 一眼看出。③ 数一数场景里到底有多少东西。Scene 标签页显示 meshes/lights/cameras/textures 的数量Stats 标签页显示 draw calls、三角形数、顶点数。第 41 章的性能优化起点就是这里先知道瓶颈在哪再决定砍什么。④ 验证光照与阴影设置。灯光列表能看到每个光源的类型、强度、颜色、范围ShadowGenerator 面板能看到分辨率、过滤类型、级联设置。第 19 章调阴影锯齿时你会反复在这里切过滤模式看效果。⑤ 读纹理与显存信息。Textures 标签页列出所有加载的贴图包括尺寸、格式、是否压缩、占用显存。第 15 章的 KTX2/DDS 压缩收益实测就靠它——肉眼看不出区别但显存差几倍。这五件事的共同点它们都是运行时真相不是代码里的期望。你写的代码说我挂了这张贴图Inspector 告诉你挂了但 UV 全挤在左下角——这就是它的价值。3.6 DevTools三处真正常用的Console本章所有报错的第一现场。Babylon 的报错多数会说清楚No camera defined、WebGL not supported别跳过读原文。Network看模型/贴图到底请求了什么、返回码是什么、多大、多久。拖模型显示不出来八成先在这里看到 404 或 4 MB 的 PNG第 33 章。Performance录制一段帧看 JavaScript 侧的长任务出现在哪一帧GPU 侧占用在多数浏览器里看不全所以本书主要靠自己的数据getDeltaTime()分布 SceneInstrumentation第 41 章。一个具体的用法示范把第 1 章的体检表和 DevTools 对照——你在页面看到的平均帧 16.6ms与 Performance 面板里requestAnimationFrame之间的间隔应当吻合如果差很多说明瓶颈在 GPU 侧或有人在你看不到的地方做了重活。3.7 两条导入路线以及实测体积路线 A模块化 TypeScript本书主线import{Engine,Scene,MeshBuilder}frombabylonjs/core路线 B全局脚本 UMD老教程、快速速写scriptsrc/vendor/babylon.max.js/scriptscriptconstenginenewBABYLON.Engine(canvas,true)constscenenewBABYLON.Scene(engine)constboxBABYLON.MeshBuilder.CreateBox(box,{size:2},scene)// 老签名第一个参数现在是被忽略的兼容位第 4 章坑 4-02camera.attachControl(canvas,true)/script对照页public/toolchain-umd.html就是路线 B 的完整可运行版本。BABYLON.MeshBuilder确实存在我核实的类型声明babylon.d.ts:58445 export var MeshBuilder: {…}所以老代码里的这一句在 v9 仍然可用但同文件里Mesh.CreateBox之类的旧入口被标了deprecated写着Please use MeshBuilder instead——看到Mesh.CreateXxx一律改MeshBuilder.CreateXxx。实测数字2026-10-11 本机路线产物rawgzipUMDbabylon.max.js单文件8.22 MiB1.77 MiBUMDbabylon.js8.22 MiB与 max字节数完全相同均 8,619,713 B—模块化顶层导入babylonjs/coredist-bench/全部 JS76 个 chunk6.76 MiB1.49 MiB复现命令npminstall--save-dev babylonjs9.30.0 npx vite build--configvite.bench.config.tsbench/min.ts是第 4 章那十行立方体代码的独立副本不含 Inspector、不含实验台外壳。这三行结论是本章最该带走的东西只比 UMD 少约 18%raw/16%gzip。网上tree-shaking 能砍一半以上的说法在顶层导入下不成立——babylonjs/core的根入口几乎把整个引擎拉了进来。想真省体积要走深路径导入 按需动态加载。本书不在这里给具体数字因为路径写法需要在 9.30.0 上逐条验证留到第 41 章实测。选 Babylon 的理由是功能齐不是包小。如果你的项目只有 10 KB 的预算这本书帮不了你——这不是缺点是定位。附带发现一条会浪费你一小时的坑在vite dev下直接写script src/node_modules/babylonjs/babylon.max.js实测下载量是48,581,628 字节≈46 MiBVite 把它当模块处理并挂上 sourcemap。正确做法是复制成静态资源mkdir-ppublic/vendorcpnode_modules/babylonjs/babylon.max.js public/vendor/实测/vendor/babylon.max.js回到8,619,713 字节与磁盘文件一致。本章对照页用的就是这个路径。3.7.1 新增一章时运行时发生了什么当你执行npm run new-chapter 05 my-feature 我的功能并写完src/chapters/ch05-my-feature.ts后运行npm run dev实验台会发生这些事Vite 启动读取vite.config.ts在端口 18000 监听strictPort: true见第 3 章坑 3-04。src/hub.ts被打包其中这一行是关键constloadersimport.meta.glob(./chapters/*.ts,{eager:false})Vite 在构建时扫描src/chapters/目录把所有.ts文件的路径和懒加载函数收集进loaders对象。这不是运行时行为是编译期静态分析——所以新文件只要放在这个目录下就会被自动发现不需要修改hub.ts。左侧章节列表渲染hub.ts遍历loaders用正则/ch(\d)-[^/]*$/从路径提取章号如./chapters/ch05-my-feature.ts→05按章号排序后生成导航链接。用户点击某个章节如果当前有活跃章节调用它的dispose()解绑观察者、清定时器、销毁 Scene。await entry.load()懒加载目标章节模块。校验meta.id是否与文件名一致不一致则报错见挑战 2。调用create(args)传入共享的engine、canvas、stage、overlay和describe函数。拿到返回的{ scene, dispose }存入全局变量current。如果这是第一次渲染启动engine.runRenderLoop(() current.scene.render())。HUD 每 500ms 刷新读取scene.meshes.length、scene.lights.length等加上engine.getFps()更新到页面上方。这条链路解释了三个设计选择为什么create()只解构需要的参数因为 hub 传给你的就是这些多解构未使用的会触发 TS6133 报错noUnusedParameters。为什么不能销毁 EngineEngine 由 hub 创建和管理章节只管自己的 Scene。销毁 Engine 会让整个应用停摆。为什么meta.id必须与文件名一致因为导航以文件名为准正则提取meta.id只是辅助校验。两者不一致说明你复制粘贴时忘了改 id会导致导航与实际内容错位。理解这条链路你就理解了本书工程的核心模块化 ≠ 多项目而是单项目内的动态加载与隔离。3.8 装哪些包一张决策表你想做装什么本书哪章任何 3Dbabylonjs/core全程读 glb/gltf/obj 模型babylonjs/loaders32、33界面控件babylonjs/gui35、36网格地面/压印/液体等特殊材质babylonjs/materials17、34调试面板与节点编辑器babylonjs/inspector3、4 起常驻物理babylonjs/havokWASM较大第 30 章才装30、31老教程式script速写babylonjs本工程装成 devDependency 只用于体积对照3版本必须齐步走。上面所有包都在同一个仓库里发版混用不同大版本是编译期与运行期双重灾难。本书统一9.30.0package.json里写^9.30.0升级时整批升升完先跑第 1 章体检章 npm run typecheck。顺便记录两条不用管的构建警告你会在dev.log/build.log里看到别去修Sourcemap for griffel/core/src/devtools/...js points to a source file outside its package [MODULE_LEVEL_DIRECTIVE] The semantics of the module level directive use client in fluentui/... may not be preserved它们来自 Inspector 依赖的 Fluent UI / Griffel 包自身的 sourcemap 与指令写法实测不影响渲染。3.9 预期输出与自查你看到说明点 ① 右侧出现场景树里面能看到box/camera/hemiInspector 通了本章主要工具就绪再点 ① 面板消失token.dispose()正常工作异步所以有轻微延迟点 ② 新标签出现橙色方块 顶部说明文字UMD 路线能跑typeof BABYLON显示object② 的第一屏明显比 ① 慢8.22 MiB 未压缩脚本代价看得见面板打印了 5 行体积实测数据与docs/体积基准.md一致不一致说明你的 Vite/Babylon 版本不同把差异记下来Inspector 打开后画面区域变小你用了layoutMode: inline它会重挂 canvas默认overlay不会章节左侧目录没有第 3 章文件名没被 glob 匹配到必须是src/chapters/chNN-*.ts且导出了meta与create3.10 本章的坑3-01dev 模式用script引/node_modules下的 UMD → ≈46 MiB。Vite 会加工它并附 sourcemap。→ 复制到public/vendor/当静态资源实测 8.22 MiB。3-02babylon.js与babylon.max.js在 9.30.0 字节数相同。老书上一个压缩一个未压缩的说明对 v9 不再成立压缩交给 CDN 与 gzip。3-03Inspector 静态导入会把几 MiB 的编辑器拖进首屏路径。实测分块里节点几何编辑器单块 5.4 MB、NME 1.9 MB、GUI 编辑器 1.8 MB。→ 一律await import()且只在用户点击时。3-04Vite 端口被 Windows 保留区间占用时报EACCES。不是权限问题也不是占用别去关防火墙。→strictPort: true 避开保留区间本机实测 2180–2779、6203–6302 被保留。3-05dev通过不代表build通过反之也一样。转译不查类型构建才暴露分块与体积。→ 三个命令都跑。3-06Mesh.CreateBox这类旧入口已标deprecated。搬老代码时优先全局替换成MeshBuilder.CreateBox。3.10.1 工程故障排查表现象第一怀疑对象检查步骤页面白屏控制台无报错runRenderLoop没启动 / Scene 为空确认create()返回了scene在create末尾加console.log(scene created, scene)页面白屏控制台报No camera defined场景里没有激活相机确认scene.activeCamera不为空或用camera.attachControl()绑定控制模型不显示控制台无报错相机位置/朝向不对 / 模型太大超出视锥在 Inspector 里看包围盒把相机拉远camera.radius * 10再试模型不显示Network 报 404glTF/贴图路径错误确认SceneLoader.Append的路径相对于public/用绝对路径/assets/model.glb测试帧率个位数场景太重 / 循环里有重活打开 Inspector Stats 看 draw calls注释掉一半网格再测定位重负载源切章节后帧率越来越差上一章节的 Observable 没解绑确认dispose()里调用了scene.onAfterRenderObservable.remove(observer)查第 2 章坑 2-02切章节后体检表重复打印定时器没清除确认dispose()里调用了clearInterval(timer)查第 1 章坑 1-07npm run build失败报Cannot resolve module导入路径错误 / 包未安装确认package.json里有对应依赖路径用babylonjs/core而非相对路径手机端打不开http://IP:18000防火墙拦截 / IP 不对确认电脑 IPipconfig看 IPv4Windows 防火墙放行 Node.js手机和电脑在同一 WiFi页面能开但 canvas 黑屏WebGL 不支持 / 硬件加速关闭看第 1 章体检表的webGLVersionChrome 设置里搜索硬件加速并开启这张表覆盖了你前 10 章会遇到的 90% 工程问题。遇到不在表里的情况第一反应永远是看控制台原文别猜。3.11 章节挑战把public/toolchain-umd.html里的MeshBuilder.CreateBox换成Mesh.CreateBox看它是否还能跑并到node_modules/babylonjs/babylon.d.ts里找到那条deprecated注释。把src/chapters/ch03-toolchain.ts的meta.id改成3刷新页面。画面照常但错误栏会出现一行章号不一致。解释三件事为什么章号以文件名为准hub.ts用正则从文件名取 id、为什么npm run typecheck抓不到这个不一致meta声明为as const类型是字面量但没有跨文件约束、要让它在编译期就报错该怎么改src/core/chapter.ts的契约。用AttachDebugLayer(scene)替换本章的 Inspector 按钮比较两者在手机上的可用性与信息密度第 43 章会用得上这个结论。跑一次npm run build把dist/assets/里最大的三个 chunk 记进docs/体积基准.md。这将是你在第 41 章的优化基线。3.12 本章小结工作流比工具名重要看到例子 → 判路线 → 搬本地 → 跑 → 用 Inspector 看 → 量代价。本书用一个页面 章节契约的实验台meta/create/disposeimport.meta.glob自动发现章节全局一台Engine、每章一个Scene。dev只转译、build才见体积、typecheck只见类型——三者都跑是交付纪律。Inspector 的 v9 入口是ShowInspector返回可dispose()的 token用它请懒加载。两条导入路线的体积差实测只有 16–18%因为顶层导入没给 tree-shaking 留空间Babylon 的定位是功能齐而非包小。所有babylonjs/*包必须齐步升版升完先跑第 1 章体检。3.13 术语卡术语英文一句话转译transpile把 TS/新语法变成浏览器能跑的 JS不做类型检查生产构建production build压缩 分块 tree-shaking 后的产物分块chunk构建把代码切成的可延迟加载文件懒加载lazy loading用到时才import()不进首屏摇树tree-shaking把没被用到的代码从产物里剔除UMDUniversal Module Definition挂全局对象的旧式打包形式调试层debug layer引擎自带的画面内统计浮层保留端口区间excluded port range系统预留、程序不可绑的端口段