Babylon.js一帧之旅(番外二):多场景管理——切换、叠加与多画布
2026/9/17 19:20:22 网站建设 项目流程

「Babylon.js 一帧之旅」番外篇之二。第(一)篇确立了"Engine 是节拍器、Scene 是指挥家"的架构认知,但正篇里我们始终只用一个 Engine 配一个 Scene。真实的应用往往不止一个场景:主菜单与游戏关卡、3D 世界与 HUD、主视图与预览窗……本篇系统讲清多场景(乃至多画布)的四种组织形态,以及每种形态的正确姿势。

引言:先记住一个资源事实

做选型之前,必须知道一个硬件层约束:浏览器对同时存活的 WebGL 上下文有数量限制(Chrome 约 16 个),超出后最旧的上下文会被强制回收——表现就是"某个画布突然黑了"。每个 Engine 占一个上下文,这决定了多 Engine 永远是最后的选择。

一个实用的类比:

  • Scene 是"同一舞台上的不同剧目"——换剧目、叠布景都很便宜;
  • Engine 是"新建一座剧院"——昂贵且数量有限;
  • Engine Views 是"舞台上的分屏直播"——一座剧院,多块屏幕。

一、四种形态总览

形态结构上下文数典型用途
场景切换1 Engine + N Scene(分时渲染)1主菜单/关卡/结算
场景叠加1 Engine + N Scene(同帧渲染)13D 世界 + HUD、叠层特效
引擎视图1 Engine + N canvas(registerView)1主视图 + 多角度预览窗
多引擎N Engine + N canvasN完全独立的少量 3D 区域

二、场景切换:分时复用同一舞台

核心手法:渲染指针切换——循环只渲染"当前场景",切场景只是换指针:

letcurrentScene=menuScene;engine.runRenderLoop(()=>currentScene.render());asyncfunctionswitchLevel(){// 1. 后台场景预加载(就绪机制照常工作,见正篇第(二)篇)constnextScene=newBABYLON.Scene(engine);awaitBABYLON.SceneLoader.AppendAsync("./assets/","level2.glb",nextScene);awaitnextScene.whenReadyAsync();// 含着色器编译,切换零等待// 2. 切换渲染指针constoldScene=currentScene;currentScene=nextScene;// 3. 释放旧场景(见正篇第(九)篇的销毁纪律)oldScene.dispose();}

要点:

  • 不渲染的场景零开销——不调render()就没有任何帧成本;
  • 预加载模式让切换"无缝":新场景完全就绪后才换指针,用户看不到加载过程;
  • 输入控制记得随迁:旧场景相机detachControl、新场景相机attachControl(正篇第(四)篇)。

三、场景叠加:一帧画两条管线

核心手法:同帧依次渲染多个场景,上层场景关闭清屏

worldScene.autoClear=true;// 底层:正常清屏(颜色+深度)hudScene.autoClear=false;// 上层:保留下层画面// 关键细节:深度缓冲也要保住,否则上层场景会被"清空后的深度"误导hudScene.autoClearDepthAndStencil=false;engine.runRenderLoop(()=>{worldScene.render();hudScene.render();});

3.1 深度缓冲的两种处理哲学

叠加时深度缓冲怎么处理,决定上层场景和下层场景是什么关系:

上层场景设置效果适用
autoClearDepthAndStencil = false上层共享下层的深度——上层物体仍能被"下层世界的墙"遮挡上下层是同一个世界的两部分(如分层加载的场景)
autoClearDepthAndStencil = true上层开始前清深度——上层完全无视下层遮挡,永远置顶HUD、告警标记、独立于世界的内容

3.2 叠加方案的官方封装:UtilityLayerRenderer

手写双场景叠加时,你要自己同步相机、处理输入路由、管理生命周期。对于"辅助工具/交互控件永远置顶"这个最常见诉求,Babylon 直接提供了封装:UtilityLayerRenderer——官方 Gizmo 系统(GizmoManager)就是基于它实现的。

constutilLayer=newBABYLON.UtilityLayerRenderer(scene);// utilityLayerScene 就是内部维护的叠层场景,渲染在主场景之上consthandle=BABYLON.MeshBuilder.CreateTorus("handle",{diameter:1},utilLayer.utilityLayerScene);// 叠层场景的相机默认跟随主场景相机,无需手动同步

什么时候手写双场景,什么时候用 UtilityLayer?做自定义 Gizmo、选中框、操作手柄等交互辅助层——用 UtilityLayer(输入、相机同步、清理都帮你处理了);做内容级的叠层(一套独立于主世界的 HUD 场景,有自己的相机构图)——手写双场景,自由度更高。

3.3 同场景内其实也能"置顶"——别急着开第二个场景

在决定开叠层场景之前,先确认你要的效果是不是同一场景内就能解决。 Babylon 提供三个层级的同场景手段:

// 层级一:渲染组置顶(最省事)// 原理:Babylon 默认在每个渲染组开始时清空深度缓冲,// 高组号内容天然无视低组号的遮挡marker.renderingGroupId=3;// 想让组间保留深度关系时:scene.setRenderingAutoClearDepthStencil(3,false);// 层级二:深度函数穿透(透视/X光)pipe.onBeforeRenderObservable.add(()=>{engine.setDepthFunction(BABYLON.Constants.ALWAYS);// 深度测试永远通过});pipe.onAfterRenderObservable.add(()=>{engine.setDepthFunction(BABYLON.Constants.LEQUAL);// 画完恢复});// 层级三:双趟渲染实现"墙内幽灵"效果(楼宇管道透视的标准做法)// 第一趟正常渲染(被墙挡);第二趟半透明 + ALWAYS,墙内部分以幽灵轮廓透出

选择原则:只是"显示在最上"→ 渲染组;要"穿透遮挡的透视感"→ 深度函数/双趟;内容是交互工具且要独立输入管理 → UtilityLayer;内容是完全独立的一套世界(独立相机、独立光照)→ 才轮到双场景叠加。

四、多画布:Engine Views 与多 Engine

4.1 Engine Views:一个引擎,多块画布(推荐)

需求是"主视图 + 几个不同角度的预览窗"时,registerView是正解——一座剧院,多块屏幕

// 把小画布注册为主场景某相机的输出视口constpreviewCam=newBABYLON.ArcRotateCamera("preview",0,Math.PI/2.5,15,BABYLON.Vector3.Zero(),scene);engine.registerView(previewCanvas,previewCam);// 渲染循环照常只有一句 scene.render()// 引擎会自动把主画布渲染给主相机、把 previewCanvas 渲染给 previewCamengine.runRenderLoop(()=>scene.render());// 移除视图engine.unRegisterView(previewCanvas);

每个 view 可以有自己的相机,共享同一个场景、同一份资源、同一个 WebGL 上下文。注意:view 的渲染是增量开销(每个 view 多走一遍该相机的渲染流程),但与多 Engine 相比,省下了重复的资源加载和着色器编译。

4.2 多 Engine:最后的选择

仅当"多个完全独立、互不相干的 3D 画布"且数量很少时才用。代价清单:

  • 上下文数量天花板(约 16 个),超限回收旧上下文;
  • 同一模型在每个 Engine 里重复解析、重复编译着色器、重复占显存;
  • 多个 rAF 循环各自为政,低端设备集体掉帧。

特殊例外——Web Worker + OffscreenCanvas:渲染重到阻塞主线程时,把 Engine 放进 Worker(BABYLON.Engine支持 OffscreenCanvas)。此时必然是新 Engine,但这是有意的架构选择,不是资源浪费。

4.3 决策速查

需求方案
同画布,不同时刻不同内容场景切换(渲染指针)
同画布,内容叠加场景叠加(autoClear 组合)/ UtilityLayer
多画布,内容相关(同场景多视角)engine.registerView
多画布,完全独立,数量 ≤ 个位数多 Engine
渲染阻塞主线程OffscreenCanvas + Worker

五、实战:产品展示页——主视图 + 缩略预览

组合本篇手法:一个主画布交互查看产品,右下角一个小画布固定俯视角度:

constengine=newBABYLON.Engine(mainCanvas,true);constscene=newBABYLON.Scene(engine);// 主相机:用户交互constmainCam=newBABYLON.ArcRotateCamera("main",Math.PI/4,Math.PI/3,8,BABYLON.Vector3.Zero(),scene);mainCam.attachControl(mainCanvas,true);scene.activeCamera=mainCam;// 俯视预览相机:挂在 view 上,不占额外汇编/资源consttopCam=newBABYLON.ArcRotateCamera("top",0,0.01,10,BABYLON.Vector3.Zero(),scene);engine.registerView(thumbCanvas,topCam);// 选中部件高亮:同场景渲染组置顶即可,无需叠层functionhighlightPart(mesh:BABYLON.AbstractMesh){mesh.renderingGroupId=3;// 组间默认清深度,永远可见consthl=newBABYLON.HighlightLayer("hl",scene);hl.addMesh(mesh,BABYLON.Color3.Green());}engine.runRenderLoop(()=>scene.render());// 页面卸载:一个引擎,一套清理(正篇第(九)篇)window.addEventListener("beforeunload",()=>{engine.stopRenderLoop();engine.unRegisterView(thumbCanvas);scene.dispose();engine.dispose();});

整个页面只用了1 个上下文、1 条渲染循环,实现了双画布 + 置顶高亮——这就是本篇选型的价值。

六、常见误区

误区一:叠加场景只设autoClear = false,忘了autoClearDepthAndStencil
颜色缓冲保住了,深度缓冲却被清掉,上层物体的遮挡关系全乱。两个开关要成对考虑。

误区二:为"置顶显示"就开一个新场景。
同场景的渲染组(默认组间清深度)或 UtilityLayer 就能解决的事,开新场景意味着独立的相机同步、输入管理和资源开销。粒度够用就别升级方案。

误区三:registerView之后以为每个 view 是免费的。
每个 view 都是一遍额外的相机渲染流程(剔除 + 绘制)。view 多了同样掉帧——只是比多 Engine 便宜得多。

误区四:多 Engine 做无限滚动的 3D 列表。
WebGL 上下文约 16 个就到天花板,之后旧画布被系统回收变黑。列表类需求应该用"少量 view 复用"(滚出视口的 item 回收其 view 给新 item)或干脆截图静态化。

误区五:切换场景后忘了旧场景的输入监听。
detachControl不调用,旧相机还在响应拖拽(正篇第(四)篇的坑在多场景下会放大)。

七、小结

  • 多内容组织的四种形态按成本递增:场景切换 → 场景叠加 → Engine Views → 多 Engine,能用便宜方案就别升级;
  • 场景叠加的核心开关是autoClear/autoClearDepthAndStencil的组合,深度缓冲是否共享决定上下层"是不是同一个世界";
  • "永远在最上"有三个同场景层级:渲染组(默认组间清深度)、深度函数穿透、UtilityLayer 叠层——开新场景是最后才需要的手段;
  • 多画布首选registerView(一个上下文多块屏幕),多 Engine 只留给真正独立的少量场景或 Worker 隔离。

本篇为「Babylon.js 一帧之旅」番外篇之二,与正篇第(一)(四)(七)(九)篇互为补充。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询