本节摘要:环境搭建的目标只有一个——让引擎画出第一帧。本节给出最小可跑工程的"六件套"(画布、引擎、场景、相机、灯光、循环),逐件解释它在帧流水线里的岗位,并用一个"从 CDN 到工程化"的完整案例演示搭建过程与常见翻车点。本节是全册地基,后续每一节都在这个骨架上加零件。
读完后你应当能够:
浏览器原本不会画 3D。它只提供一个矩形绘图区和一个底层的图形接口,剩下的全靠引擎。Babylon.js 做的事,就是在你写的几行 JavaScript 和这块矩形之间架一条流水线:每帧把你的场景描述翻译成几百条图形指令,交给显卡执行,再把结果像素贴回矩形。
所以"环境搭建"不是装软件,而是把流水线的两端接通。一端是画布,另一端是引擎实例。先看最小工程长什么样:
<!-- 页面里只放一块画布:它就是每一帧像素的最终去向 --> <canvas id="renderCanvas"></canvas> <!-- 画布要占满视口,避免出现"半屏渲染"的错觉 --> <style> html, body { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; } #renderCanvas { width: 100%; height: 100%; display: block; touch-action: none; } </style>
<!-- 通过包管理器安装引擎后,用构建工具引入;快速试验时也可用 CDN 全局脚本 --> <script src="https://cdn.babylonjs.com/babylon.js"></script>
画布有了,接通引擎。下面这段脚本就是著名的"三行起步"展开版,注释里标明了每行在流水线上的岗位:
// 1 画布:像素输出的目的地 const canvas = document.getElementById("renderCanvas"); // 2 引擎:接管画布,建立与 WebGL 的连接;第二个参数开启抗锯齿 const engine = new BABYLON.Engine(canvas, true); // 3 场景:所有相机、灯光、网格的容器,帧流水线要遍历的就是它 const scene = new BABYLON.Scene(engine); // 4 相机:决定这一帧"从哪里看"。参数依次是名称、水平角、垂直角、距离、目标点 const camera = new BABYLON.ArcRotateCamera("cam", -Math.PI / 2, Math.PI / 2.4, 8, BABYLON.Vector3.Zero(), scene); camera.attachControl(canvas, true); // 把鼠标拖拽、滚轮缩放交给相机处理 // 5 灯光:决定这一帧"看得见什么"。没有它,多数材质渲染出来是纯黑 const light = new BABYLON.HemisphericLight("sun", new BABYLON.Vector3(0, 1, 0), scene); // 6 循环:每帧调用一次场景渲染,引擎负责按显示器节拍驱动这个循环 engine.runRenderLoop(() => { scene.render(); // 一帧的全部工作都在这行里发生 }); // 窗口尺寸变化时重建画布分辨率,否则拉伸会糊 window.addEventListener("resize", () => engine.resize());
跑起来后往场景里加一个球体,就能看到第一帧以及它后面每秒约 60 次的重复绘制:
// 画第一帧的主角:一个直径 2 的球,放在世界原点 const sphere = BABYLON.MeshBuilder.CreateSphere("ball", { diameter: 2, segments: 24 }, scene); sphere.position.y = 1; // 抬高一点,别陷入地面
六件套里最容易混淆的是引擎与场景。用工厂打比方:引擎是厂房与电力系统——GPU 上下文、纹理上传、着色器编译这些"重资产"都归它管,一个页面通常只有一个;场景是生产线上的工装与物料——相机、灯光、网格这些"每帧要用的东西"都挂在它名下,一个引擎可以带多个场景。判断某个 API 该去哪找,先问它属于"厂房"还是"物料"。
相机与灯光则是一帧的两个输入源:相机产出"从哪看"的矩阵,灯光产出"照多亮"的数据,两者每帧都会被流水线读取。少了相机,引擎不知道往哪个视锥里投影;少了灯光,标准材质没有光可反射,画面不是"暗",是黑。
最后是循环本身。它由浏览器底层的刷新信号驱动,页面切到后台时信号暂停,循环自动休眠——这是引擎替你做的省电设计,也是后文谈帧率时的重要伏笔。
背景:产品组要一个能转动的机械零件展示页。实习生把上面那段演示脚本直接贴进页面,能用,但两周后零件加到 40 个、还要加载 glTF 模型时,主脚本膨胀到八百行,改一处崩三处。
操作:重构只做一件事——按六件套的岗位拆文件。入口模块负责创建引擎与启动循环;场景模块负责装配相机、灯光与模型;业务模块只往场景里加自己的零件。入口处的核心代码收敛成:
// 入口模块:只关心流水线的通电与节拍 import { createEngine } from "./engineSetup.js"; import { buildScene } from "./sceneSetup.js"; const canvas = document.getElementById("renderCanvas"); const engine = createEngine(canvas); // 厂房归它 const scene = buildScene(engine); // 物料装配归它 engine.runRenderLoop(() => scene.render());
结果:零件装配逻辑与引擎配置解耦,帧率问题排查时可以先断开业务模块验证"空场景是否流畅",定位时间从半天缩到几分钟。
解读:六件套不只是入门清单,它是天然的模块边界。引擎层几乎不再改动,场景层随业务膨胀,两者分离后每次改动的影响范围都可预期。
变式:如果是 React 或 Vue 项目,同一套边界对应"组件只持有场景引用、引擎与循环放到应用顶层单例";框架卸载组件时只清理自己加的网格,不动循环。这套思路在第6章综合实战里还会再次出现。
⚠️ 常见坑:全黑画面九成出在六件套缺件。排查顺序固定为:画布尺寸是否为零、相机是否 attach、灯光是否存在、循环是否启动。按清单走一遍,比盯着着色器猜快得多。
第一帧已经画出来了,但那一行场景渲染里到底依次发生了什么?下一节把这一帧切开,画出全册反复使用的流水线八段图。