Babylon Lite - no DynamicTexture: TypeError: DynamicTexture is not a constructor

Hi Babylon team,

I’m porting a production app (NuminaPRO - a kitchen-furniture 3D configurator, React + Babylon.js 9) to Babylon Lite, motivated by the significant WebGPU performance and smaller bundle.

The Lite engine starts fine (createEngine / createSceneContext / camera / light all OK). The blocker is runtime-updated textures. We use DynamicTexture in 11 places - measurement dimension labels (text drawn on a canvas) and procedural material patterns. Running our real label code on Babylon Lite reproduces:

TypeError: DynamicTexture is not a constructor

because DynamicTexture (and VideoTexture, ProceduralTexture) are not exported - they’re marked “Not yet available” in the feature comparison (Textures).

The pattern we need (works in Babylon.js today):

const dt = new DynamicTexture("label", { width: 256, height: 64 }, scene, false);

const ctx = dt.getContext();

ctx.fillText("600 mm", 128, 40);

dt.update(); // push updated pixels to the GPU

material.diffuseTexture = dt;

I confirmed the low-level path works as a workaround:

import { createTexture2DFromPixels, updateTexture2DFromPixels } from "@babylonjs/lite";

const rgba = new Uint8Array(offscreenCtx.getImageData(0, 0, w, h).data.buffer);

const tex = createTexture2DFromPixels(engine, rgba, w, h, { srgb: true });

// updateTexture2DFromPixels(engine, tex, rgba) on each change

Questions:

  1. Are DynamicTexture / VideoTexture / ProceduralTexture on the roadmap, with any rough timeline?
  2. Is createTexture2DFromPixels + updateTexture2DFromPixels (OffscreenCanvas → getImageData) the intended pattern meanwhile? Caveats around mipmaps, sRGB, premultiplied alpha, per-frame perf?
  3. For procedural content, is rendering into a createRenderTexture2D target the recommended ProceduralTexture substitute?

Thanks - the perf gains are substantial and we’d love to adopt Lite fully.

The GUI part is not supported by Babylon Lite and it seems that he doesn’t have a plan yet. But I read that it was being discussed by the Babylon team. But it won’t be for now I think.

Babylon lite doesn’t support directly DynamicTexture. However, you might want to check the lite-compat package, that simplifies moving from Babylon.js to Babylon Lite. it does have a proxy-implementation of DynamicTexture. It’s canvas-backed and uploads via Lite’s pixel-texture path.

CC @ryantrem who is working on the compat layer.

The compat layer does have an implementation of DynamicTexture, but it does so by adding novel functionality rather than just being a thin layer of existing Lite constructs (which isn’t really what the compat layer should do). Given this, I’ve opened a PR that adds some Lite native APIs for dynamic texures, though it is a little different than BJS - more generalized, but should still feel familiar. You can see the PR here: feat(texture): add createDynamicTexture/updateDynamicTexture to Lite core by ryantrem · Pull Request #432 · BabylonJS/Babylon-Lite

Hi! Following up on the DynamicTexture issue (fixed in PR #432 — thanks, it works great). We went further and tried running our real app (NuminaPRO, a kitchen configurator, ~60 manager files on Babylon.js 9) on Babylon Lite through @babylonjs/lite-compat with the liteCompat() Vite plugin.

Good news: the app boots and runs on Lite — engine starts, all app logic works, our furniture builder assembles cabinets, compat covers most of the API surface. But the viewport stays permanently black, and we traced it to what looks like an architectural limitation:

The problem. Lite’s registerScenebuildScene is one-shot: buildScene drains _deferredBuilders once and sets ctx._built = true (scene-core.ts), and there is no re-registration path. Our app (like most real-world Babylon.js apps) constructs the scene incrementally: engine + camera first, runRenderLoop() is called mid-initialization, then the room, materials, and furniture are created and meshes keep being added/disposed afterwards. With compat, engine._start() runs its flush + registerScene at whatever moment the first runRenderLoop() happens, and everything added after that races the one-shot build. The state we reliably end up in: ctx._built === true, meshes: 150, camera set, but _renderables: 0, lights: 0, and _deferredBuilders: 2 queued forever — the drain loop only runs inside buildScene, which never runs again. Result: 0 draw calls, black canvas, no errors anywhere.

What we’d need: either support for re-registering a scene (re-running buildScene when new deferred builders are queued on an already-_built context), or a live drain of _deferredBuilders after _built (the “join an already-built group” path exists, but builders for new groups queued post-build are never processed). Basically: make addToScene after registration eventually produce renderables in all cases, not only for already-built groups.

Two smaller compat issues we hit and worked around, in case they’re worth fixing too:

  1. UtilityLayerRenderer’s deferred registerUtilityLayer() promise never resolves in our setup, and since it runs in the engine’s late-work phase before startEngine, it deadlocks engine startup entirely.
  2. Camera auto-activation in compat (first created camera becoming activeCamera) doesn’t propagate to scene._lite.camera — the scene registers with no camera unless the app explicitly re-assigns scene.activeCamera afterwards.

Also a docs nit: if /brdf-lut.png is missing from the app’s public root, the PBR pipeline dies with an unhandled InvalidStateError: The source image could not be decoded (dev servers return the SPA HTML fallback with a 200), which silently aborts engine startup — a clearer error or a configurable URL would help a lot.

Thanks for the detailed report — your scene-core.ts trace was spot on and made all four issues easy to confirm. Two PRs cover three of them.

Black canvas — correct diagnosis, including the subtlety that the trigger isn’t “adding after registerScene” but that no mesh of that material group existed at registration time. Fixed in PR #461, which routes those meshes into a runtime build path off the existing material-swap drain, plus a parity scene that fails without it. It also fixes a mid-frame GPU teardown bug in removeFromScene — relevant if you dispose meshes from onBeforeRender.

Note that introducing a material family at runtime is inherently async (module fetch + shader compile), so the mesh appears a few frames after addToScene, not immediately.

The other three are in PR #463:

UtilityLayerRenderer deadlock — confirmed. Late work was drained before startEngine, so a layer waiting on the first frame waited on a loop that hadn’t started. Startup now awaits startEngine first.

/brdf-lut.png — you asked for a clearer error or a configurable URL; you get both, plus the cause removed. Babylon.js embeds its LUT as Base64, so a BJS app has no asset to forget; compat’s fetch is what introduced this on port. The LUT is now embedded, so the default path makes no network request. scene.environmentBRDFTexture (the real BJS API) is supported for overrides. When a URL is used, the error now names it:

BRDF LUT '/brdf-lut.png' is not an image (200 text/html).

covering your SPA fallback, a 404, and any other undecodable response.

Camera auto-activation — couldn’t reproduce. Camera’s constructor calls scene._registerCamera, which assigns _lite.camera when unset, and tests for first-camera-wins and explicit activeCamera override all pass. Likely something more specific in your setup — a camera built before the scene is attached, or one inside a utility layer’s scene. A repro in a separate thread would help.

Thanks for pushing a real production app through compat — this class of bug only shows up under incremental scene construction, which synthetic test scenes don’t do.

Hi Babylon team,

First, the headline: we got our production kitchen configurator running on Babylon Lite, and the performance difference is dramatic — on our scenes it is roughly 10–15× faster than our Babylon.js build. Everything is immediate: no lag when placing or editing cabinets, no waiting on scene rebuilds, camera navigation stays smooth throughout. This is exactly why we want to move to Lite, and we are following the project closely — we’re very much looking forward to further additions and fixes.

Following up on the DynamicTexture thread (thanks again for PR #432, #461 and #463 — all three landed and unblocked us): we have now brought NuminaPRO, a production kitchen-furniture configurator (React + Babylon.js 9, ~60 scene-manager modules), up on Babylon Lite through @babylonjs/lite-compat.

The scene boots, cabinets assemble with edgebanding and hardware, textures apply, ~165 FPS. Getting there took a ~1000-line app-side shim of workarounds, so I’d like to report what we hit. Everything below was verified live in the running app; I can supply a minimal reproduction for any item.

A. Engine-core issues

Following up on the DynamicTexture thread (thanks again for PR #432, #461 and #463 — all three landed and unblocked us): we have now brought NuminaPRO, a production kitchen-furniture configurator (React + Babylon.js 9, ~60 scene-manager modules), up on Babylon Lite through @babylonjs/lite-compat.

It runs. The scene boots, cabinets assemble with edgebanding and hardware, textures apply, ~165 FPS. Getting there took a ~1000-line app-side shim of workarounds, so I’d like to report what we hit. Everything below was verified live in the running app; I can supply a minimal reproduction for any item.

A. Engine-core issues

1. createBox cannot make a non-uniform box. createBox(engine, size?: number) and createBoxData(size?) only accept a scalar, so MeshBuilder.CreateBox in compat does createBox(engine, options.size ?? options.width ?? 1). A 598×18×530 panel becomes a 598³ cube. Measured: bottom bounds were ±0.299 on all axes, leftSide ±0.009 on all axes — our entire cabinet rendered as a solid block. This is the single highest-impact item for us; a non-uniform box is the most-used primitive in Babylon. Workaround: take createBoxData(1), scale positions per axis, upload via VertexData.applyToMesh.

2. glTF/GLB cannot be loaded from a blob: URL. loader-gltf/load-gltf.ts:254 derives a directory base for any string source: new URL(".", new URL(source, location.href)). That is impossible for an opaque blob: URL → TypeError: Failed to construct 'URL': Invalid URL. We fetch private models through an authorized proxy, decrypt them and wrap the bytes in a blob URL, so all hardware (hinges, legs, fasteners) failed to load. fetchGltfAsset already accepts ArrayBuffer/Blob (self-contained, baseUrl = ""), so skipping base derivation for blob:/data: sources should be a small fix. The comment four lines below already claims blob URLs are supported.

3. A mesh’s material group is bound once at addToScene and never recomputed. scene/scene-core.ts:370-401 selects the build group from mesh.material._buildGroup at add time. Our room builder creates walls with a Standard material and assigns the real PBR material a moment later; the mesh stays in the Standard group, so buildStandardMeshRenderables runs writeStdMaterialData on PBR props and dies at standard-pipeline.ts:376 (data[0] = dc[0], diffuseColor undefined). registerScene rejects → _built stays false → startEngine never runs → black viewport with no error anywhere (see item 8). Lite already has the material-swap machinery; re-grouping on material change (or guarding the write) would close this.

4. rebuildSingle reuses a shader context built for a different material. pbr-renderable.ts:253 imports the gamma template only when hasGammaAlbedo is true at first build. If a material gains a gamma-space albedo texture later, every rebuild hits _gammaTemplate!.gammaBaseColor(...) (pbr-template.ts:374) with _gammaTemplate === nullCannot read properties of null (reading 'gammaBaseColor') every frame, crashing the app. Our workaround is to force gammaAlbedo = false for late-assigned materials, which costs us correct colour.

5. Changing shadow casters does not re-run the PCF preload. shadow/pcf-shadow-task-hooks.ts:57-81 lazily imports the “no-color” material views, choosing which ones from the material families of the casters present at preload time. setShadowTaskCasterMeshes doesn’t re-run it, so a caster whose family appeared later crashes the render loop: createPbrNoColorMaterialView is not a function at getNoColorViewshadow-task.executerenderFrame.

6. A shadow generator with zero casters produces an invalid frame. The 4096×4096 Depth32Float map is sampled by receivers but never written, so WebGPU rejects the whole command buffer: “usage (TextureBinding|RenderAttachment) includes writable usage and another usage in the same synchronization scope”Invalid CommandBuffer. This is the normal state of an empty project.

7. BoundingBox world-space values. centerWorld, extendSizeWorld and vectorsWorld are undefined, and minimumWorld/maximumWorld return local values (verified: a mesh at (1,2,3) still reported min (−0.3,−0.3,−0.3)). Any code doing world-space bounds maths breaks.

8. Startup rejections are swallowed. runRenderLoop does void this._start(), so anything that throws inside _startCore leaves a black canvas with an empty console. Items 3, 4 and 6 each cost us hours for this reason alone. Surfacing that rejection (console.error, or an onEngineStartFailed hook) would be a large DX win.

B. Compat APIs our app needed and had to shim

Geometry/meshes: Mesh.setIndices() / getIndices(); setVerticesData cannot create geometry (no-ops unless the mesh already has some); Mesh.MergeMeshes (absent; we use it in 9 places); Mesh.clone() is declared never and throws; bakeTransformIntoVertices / bakeCurrentTransformIntoVertices (absent, 9 call sites); mesh.dispose() does not remove the mesh from the Lite scene; Node.setEnabled() only sets a flag and does not hide.

Materials/textures: Material.clone(name), getScene(), getActiveTextures(), freeze()/unfreeze(); textures have isReady() but no onLoadObservable — our code awaits onLoadObservable with a 15 s backstop, so every module add stalled exactly 15 s until we supplied the signal; texture assignment binds to Lite only inside Material._ensureRenderable, which is called only from addPrimitive, so a material assigned after the mesh was added never binds its textures.

Loading: AssetContainer.meshes returns a stripped LoadedMesh with no transform API, no clone, no isVisible/setEnabled, no bake — post-processing imported meshes is a very common pattern and currently impossible. Returning real Mesh wrappers would remove several of our workarounds at once.

Math/camera: Quaternion.toRotationMatrix(result) (absent — fromRotationMatrix exists); camera.inputs (absent entirely, so camera.inputs.attached.pointers — standard button remapping — throws and killed our init); wheelPrecision / angularSensibility / panningSensibility are not forwarded from the compat wrapper to the Lite camera, and attachControl captures them once, whereas Babylon.js reads them live (we set wheelPrecision right after attachControl, as Babylon allows, and it was silently ignored — zoom ran ~50× too fast).

Also: scene.meshes and scene.textures are never populated, which breaks introspection and tooling.

C. One behavioural difference worth documenting

Lite’s wheel zoom scales the step by the current radius (arc-rotate-controls.ts:286), while Babylon.js uses a constant step. Close to an object each notch becomes microscopic and the camera feels “stuck” — pulling back out takes dozens of scrolls. Not a bug, but it surprises ported apps.

Closing

None of this is a blocker for us any more — we have workarounds for all 23 items. The reason I’m writing is that those workarounds are a large, fragile layer we’d rather not maintain, and items A1–A5 in particular affect any app that builds geometry from primitives or loads assets from object URLs.

Happy to open individual issues with minimal repros for whichever of these you want, and to share NuminaPRO as a real-world test case. The perf and bundle-size wins are substantial — we want to ship on Lite.

Thanks!

lol you just made my day :slight_smile:

ok a lot to unpack here (and I love that:))

  • @thomlucc can you check how we can have a partner page for lite? We should definitely work with @Sambler to get a link and a nice shot of their app
  • @georgie please check the camera zoom issue :wink:
  • @ryantrem for the compat layer

@Sambler give me some time to check all your fantastic report and I will give you a definitive answer (like yeah that’s worth a PR or nah, we need to stay lean :))

Thanks! As I said: this is one of the most useful reports Lite has had!

One correction first: only #432 has landed. #461 and #463 are still open and in review, so on master you’d still hit the black canvas.

Already fixed on master

A5 in the ordinary case; mesh.dispose() now removes the mesh from the Lite scene; Mesh.setEnabled() now hides; bakeCurrentTransformIntoVertices exists.

And one you can act on today: Texture already has whenReadyAsync(). Do await texture.whenReadyAsync() before assigning it to a material and your 15 s backstop can go.

Fixed, but not merged yet

  • A3 (black viewport) → #461. Your case is its “Bug B”: the trigger isn’t “material assigned late”, it’s that no mesh of that material family existed at registerScene time.
  • BRDF LUT + utility-layer startup → #463.

Real gaps — one issue each, all filed

A1 non-uniform createBox #465
A2 blob:/data: glTF #466
A4 gamma template on rebuild #467
A5 remaining preload race #468
A6 zero-caster shadow map #469
A8 swallowed startup rejections #470
A7 world-space BoundingBox #471
setIndices/getIndices #472
Mesh.clone() #473
Mesh.MergeMeshes #474
bakeTransformIntoVertices(matrix) #475
late material → textures never bind #476
Material.getScene/getActiveTextures #477
Material.clone(name) #478
Texture.onLoadObservable #479
AssetContainer.meshes stripped #480
Quaternion.toRotationMatrix #481
scene.meshes incomplete #482
Node.setEnabled descendants #483

A6 is the one I couldn’t reproduce — the PCF pass runs with a clear and its depth target clears to 1, so the map should be written even with zero casters, and sampling happens in a later pass. #469 is open as an investigation; a minimal repro there would help me most.

Two of your findings also turned out to be slightly off-target in a useful way: setVerticesData isn’t actually blocked (an empty new Mesh gets placeholder geometry) — the real blocker is the missing index accessors, which is what #472 covers. And the late-texture problem isn’t _ensureRenderable being called only from addPrimitive; it’s the mesh.material setter never calling it at all (#476).

Philosophically correct (lol, sorry :))

Material.freeze()/unfreeze(). In BJS, freeze() stops per-frame effect re-derivation and uniform rebinding. Lite compiles a material once into a renderable; per-frame it’s a cheap version compare deciding whether to re-upload the UBO. There’s nothing for freeze() to skip, so it would cost nothing and save nothing. Drop the call :slight_smile:

scene.textures. Not a feasibility problem: compat already keeps scene-local arrays for materials, cameras and lights. Such a list would only contain textures built through the compat Texture classes and would silently omit everything the glTF and environment loaders create, since Lite has no complete scene-level texture registry. For introspection, a list that looks complete and isn’t is worse than no list. If it’s blocking something concrete, tell me the use case and I’ll look again.

Thanks again!

PR addressing camera bug! fix(camera): honor arc-rotate wheelPrecision/angularSensibility/panningSensibility set after attachControl by georginahalpern · Pull Request #484 · BabylonJS/Babylon-Lite