Vue + babylonjs experiment

There is already a popular npm for vue + babylonjs integration vue-babylonjs, so my intention is not to build a replacement integration (even though that library has no updates in > 2 years), but I did want to learn how a vue integration works, so I used (ie: shameless copied many parts of) the existing troij.js (three + vue) as a starting point and ported to babylon a proof of concept.

I have been working in Vue 3 at work for about 18 months and love :heart_eyes: some things about Vue over other libraries like React. Instead of using Vuex or any state management library I write my own stores using computed and refs, which can be built much like React hooks, but have a better data binding and developer experience IMO. I have written custom Vue components that emit events and there are some other excellent parts of Vue that I find it hard to work without. I also really like how troij.js with vite works seamlessly with HMR. In my sample screen capture below I am editing the :beta and :radius camera properties in my vue template and it was reflected in the scene, so I could see how the developer experience would be what I was after as well.

I only spent about 2.5 hours on it and have only 1 camera and 1 geometry working.
:grinning_face_with_smiling_eyes:

End user code looks like this:

<template>
  <Engine ref="engine" antialias resize="window">
    <Scene>
      <ArcRotateCamera :beta="Math.PI / 8" />
      <HemisphericLight />
      <Box :size="1" ref="box"/>
    </Scene>
  </Engine>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { Box, Engine, MeshPublicInterface, EnginePublicInterface, Scene } from './export'

export default defineComponent({
  components: { Box, Engine, Scene },
  mounted() {
    const engineSetup = this.$refs.engine as EnginePublicInterface
    const mesh = (this.$refs.box as MeshPublicInterface).mesh
    if (engineSetup && mesh) {
      engineSetup.engine.onBeginFrameObservable.add(() => {
        mesh.rotation.x += 0.01
      })
    }
  },
})
</script>

You can see HMR in progress here:
vite-babylonjs-hmr

So, not much to it to build a reusable library. Including creating this post I am at something like 2.5 hours total time. There is obviously a looooong way to go with increasing Babylon API support either dynamically or manually/code generation. Not sure if the experiment will end here as I have so many other projects on the go, but wanted to share this.

Also, if you only need a few parts of babylonjs then you could fork this and add just what you need to the plugins and it should tree-shake down really well to a small static site. If you run yarn dev it will run the website above with a spinning cube.

oh. and the link: brianzinn/vite-babylonjs: learning experiment porting trois.js (github.com)

6 Likes

This is cooooool !!! Hope ppl will contribute :wink:

love it!

We have quite a lot of questions regarding Vue so it will be helpful for sure

1 Like

Hi Brian, idk if you saw my post here The BabylonJS Scene Explorer and Inspector in realtime in 3D with two way event binding to @roland , but he’s probably interested in this too.

One very important thing though, check this out… Its the next version of trois, yours is based on the vue2 version before vue3’s createRenderer existed.
.trois-renderer/index.ts at main · troisjs/trois-renderer · GitHub

createRenderer works much like react reconciler. Using this strategy instead of the old vue 2 strategy has actual hot scene reloading like react three fiber. For ex. in the example above, the rotation state probably wouldn’t reset.

I REALLY want to get this working in babylon itsself though. It’s possible, and probably really easy, but webpack makes everything so fucking needlessly hard and complicated.

Interested in helping, especially on a babylon centric implementation.

So far, i’ve thought two approaches to implement it in babylon.

Approach 1) instead of doing HMR how webpack/everyone else does it ie: (user code → running app) , you invert it and do (change running app → post form data to user code). Or said another way, just save the changes you make on the client to the server. This would also be ideal for the babylon electron editor. Also required to send an editor to the client, but thats not a big deal for dev, and would work well with all the existing babylonjs tooling

Approach 2) I was watching the vids on the babylon youtube channel and i forgot which one it was in but @Deltakosh mentioned that a recent release (v4.2 maybe?) added scene deserialization. So what we could do would be to capture current state in a hot reload callback and re-instantiate the scene tree after reload. Something like module.hot.accept(() => {
var previous = babylon.saveScene();
somehowRestore(previous)
} . Also the module.hot.accept and import.meta.hot is so unnecessary. HMR is just a socket connection that recieves and applies json patches. Imo it’d be much better to just add a listener to either a websocket or SSE socket and do away with the absurd abstractions.

you can serialize a scene with scene.serialize():

	console.log("saving scene");
	var serializedScene = BABYLON.SceneSerializer.Serialize(scene);
	var strScene = JSON.stringify(serializedScene);
	console.log(strScene);
	
	setTimeout(function(){
		console.log("loading scene");
		BABYLON.SceneLoader.Load("", "data:" + strScene, engine, function (newScene) {
		});
	}, 10000);

Yes, the vue createRenderer is a lot like the react-reconciler. The React host config has more to implement I think because of how it handles other environments like react native. I was planning to add that next - I think that will do the HMR for adding/removing components at run time. I would hope that no other changes were needed and the rest would be handled automatically. Also, I believe serializing and loading the scene would not work, since all of the references that Vue has to babylon objects would be lost.

The next step would be to add that createRenderer and remove all of the unneeded code - it was a flurry of copy and paste just to get it running :slight_smile:

edit: Just found an article with Vue 3 rendering to a raspberry pi, so it handles other environments - just has a simpler config to implement! Makes more sense now.

Hmm :thinking: :face_with_raised_eyebrow: :face_with_monocle:

I tested your demo and changed antialias from true to false in the vue template, and the scene reloaded keeping its rotation state! I am fairly certain the three version would reset in this case. hmmm . (responding to your edit) Yea the createRenderer was designed for non-dom targets , there’s a weex implementation in the vue3 repo.

One suggestion i have that came up from trying to make changes. I wouldn’t import anything further than @babylon/core OR babylonjs, It makes it really prone to breaking. It might actually be good to import everything used internally to one file and re-export then just import everything from that file. That would make it a lot easier to support @babylon/core, babylonjs , and global cdn script versions (which a LOT of people seem to use for the fast preview updates). Samesies for react-babylonjs … it’ll help with avoiding breaks from v4 - v5 upgrade also.

I’m actually seeing the opposite personally - with modern jamstack I want to bundle and delay/async load with code splitting and tree shaking to give fastest load experience and only load when/if needed in a SPA. I imagine it is more common for people not using bundling to link to a CDN. It’s very likely that I am missing out on more common workflows.

As for react-babylonjs there are only a couple of breaking changes I’ve found between v4 and v5 and I think I will find time to add those extra exports - it sounded like that was OK to do. It’s nice in React to have the strong typings support, so I suspect most people are using the ES6 for development and then in the bundler configuring the CDN for the peer dependencies. AFAIK there are no changes needed there in that case to support CDN. I have no plans to support the babylonjs-* NPM (only @babylonjs/* - I consider it a legacy NPM for non ES6 development - I am happy to be schooled otherwise.

edit: and when I say configuring CDN for peer dependencies then I think that would be for people wishing to use a CDN and not those taking advantage of other modern techniques like tree-shaking, code splitting, etc.

edit 2: Also, thank-you for mentioning those considerations. 3rd party products should accommodate different common workflows.

Imo, eagerly preloading everything is almost always better. In optimal conditions, it eliminates waterfalls and load times completely for silky smooth native-like experience. In poor reception areas (bus/car/subway) if you lazy load , the user may not have connectivity when they trigger a load and causes your app to stop working. This won’t happen if you eagerly load.

(From my own view as a user) I would much rather load everything while i have connectivity than load a little faster but not be able to use my phone when i get on a train.

That isnt to say splitting is bad, its actually dynamic imports that cause the problem, which is just the common mechanism most people use to tell the bundler how to split chunks. Although, i think it is ineffective and actually causes bad user experience.

Historically, http 1 and chrome used to only allow 5 simultaneous request in-flight at a time. It’s now 50. From my own “play testing” the ideal strategy seems to be to synchronously load everything into namespaces manually chunked where necessary. Kind of like a vendor chunk, but just a little smarter with smaller groups. Also in chrome, specifically 30kb - 50kb sized files are given some fast path, so that is the size to aim for. Given all this, it seems to be an argument for chunking babylon up right? But in practice, I still find it much much better to just namespace import everything up front. Sidenote: es module imports are a post-bundle construct meant for the browser. Import statements are meaningless pre-bundled. Webpack v3 and below , and to some extent still, just had problems tree shaking es modules, because in webpack “modules” are files, whereas in rollup, it actually parses the AST and does codegen. Rollup will extract a single function out of a massive file if configured correctly and the bundle doesnt have side-effecty imports, which sadly, babylon does :cry:. There is a balance between fixing treeshaking and breaking changes though, and I absolutely love and adore that babylon doesnt break things. I’ve thought about it for a while actually, and i think the best fix would be to move to a module structure under babylonjs that is similar to lodash. Nothing would have to break as it is, where you import from babylonjs and you get everything , or you can import from babylon/x and get x, just like you can import from lodash/merge and only get merge. There are existing babel plugins to give cjs bundlers like webpack some help too (although it actually isnt needed in most cases). The nested imports could just re-export from @babylon/namespaces , which should be considered private apis, and do the same for the babylon-x modules, while deprecating (but maintaing) them in favor of just babylon/x. What are your thoughts on that?

1 Like

hi @jeremy-coleman - great ideas there. I think the @babylonjs/core does already support what lodash does, but if you import the file from it’s location instead of root then only what you import from babylon is bundled. ie:

// if you want tree-shaking then don't do this
import { Engine, Scene, Nullable, EngineOptions, SceneOptions, EventState, Observer, Camera } from '@babylonjs/core';
// if you want tree-shaking do this
import { Camera } from '@babylonjs/core/Cameras/camera'
import { Engine } from '@babylonjs/core/Engines/engine'
// ... etc.

And for module libraries you need fully specified imports for webpack 5+. ie:

import { Camera } from '@babylonjs/core/Cameras/camera.js';
import { Engine } from '@babylonjs/core/Engines/engine.js';

The ES6 has a legacy export with everything together.

Also, I think how you are pre-emptively loading everything is absolutely a valid use case. This is how many games would want to load and sites. Some sites have SEO requirements and may not want to require everything loading on some pages. I have not experimented in vue, but have in React with offline-first Progressive Web Apps, which are optimized for offline experiences. You can read more about that here:
Making a Progressive Web App | Create React App (create-react-app.dev)

I have an older PWA repo that I experimented with - it’s 3 years old, but showcases the offline capabilities:
brianzinn/create-react-app-babylonjs-pwa: CRA BabylonJS PWA (github.com)

Also, about changing the structure to something more like lodash - I don’t know if that is necessary with modern tooling. In my react-babylonjs project, for example, I use lodash.camelcase (as a dev dependency) - it’s useful to have those separate NPMs, but I don’t know how that would carry over to babylonjs unless you meant something different (already GUI, procedural-textures, materials, loaders, etc. have their own NPMs). I also like how D3 switched over to modules - it was that transition that made me request an ES6 version for babylon way back! :slight_smile:

I could go on an on - it’s a really interesting discussion area. Those are my main thoughts - I will re-read what you have above there since I probably missed a couple of important points!! I am AFK rest of the day.

Yea, i did mean something different than that, sorry for not being clear. Using babylon-gui and @babylon/gui as an example: My suggestion is that it should be available at babylon/gui and not the main babylonjs bundle. The new babylon/gui should not include any code and should only re-export from “@babylon/gui”. The babylon-gui library should also not include any code and only export from @babylon/gui. This way, there’s only 1 potential copy. After these changes, give guidance that use of babylon-gui is deprecated, @babylon/gui is private and babylon/gui is the public export. This has no breaking changes, has only a single package dependency and mitigates the risk of duplicate copies.

Shifting gears to the export-path stuff.
The full imports are completely unnecessary with rollup and esbuild. If it doesnt work with webpack, idk what to say besides webpack is bad.

Just to confirm, I tested

import { Camera } from '@babylonjs/core/Cameras/camera'
import { Engine } from '@babylonjs/core/Engines/engine'

vs

export { Engine, Camera } from '@babylonjs/core'

Using rollup, the output size for both has exact same line count (unminified), 23,279.
same story for the WebGPU engine doing full export path vs shallow export. TLDR, the full paths actually dont matter .

The full paths can potentially fuck up webpack though, because webpack is bad. Imagine in your application code you have a pretty big app, that in different files uses these imports:

@babylon/core/Engine/WebGPU/WebGPU.js
@babylon/core/Engine/WebGPU (implicit index.js export)
@babylon/core/Engine (implicit index.js export )
@babylon/core (implicit index.js export)

Using webpack, you can end up with 4x copies of the same code assigned to a 4 different variables using the 4 different file names. This is just how webpack’s internal module system works, its essentially a bad version of amd or browserify. This is why imo it’s better to ONLY import it from the root, as doing so actually causes smaller bundle sizes by forcing webpack to resolve only 1 instance of each file. Webpack’s behavior is actually non-deterministic and can lead to incorrect programs with weird runtime bugs for things that have async instantiation. Not directly related but worth sharing, webpack also doesn’t resolve self exports properly either and will often bundle them in the incorrect order. You usually dont get bugs in small apps, but when u start bundling large apps with webpack, it will break your app and your soul.

The shallow-path imports also work better with import maps, because your aliased file can just export all the named modules from all the files and the browser will handle prioritize the modules needed to run the application code. That strategy also leaves room for manual tweaking import paths/bundles without touching application code.

The general guidance I believe is to use either the babylonjs-* or @babylonjs/* and not mixing.

You can read more about babylon tree-shaking as implemented here :

Note about direct index imports:

As a result, it is impossible for Webpack and the other bundlers to determine if imports are safe to be removed when not used so if you import directly from index, all the imports will be followed and included in your packages.

What I found in static deployed size was a reduction of duplicated imports on my webpack bundler analyzer with consistent explicit imports only, but there may have been some configuration to avoid that with other bundlers. For babylon as a peer dependent library with rollup I had configured @babylonjs/core/* as external after switching to explicit imports and that is what had the desired impact on the statically deployed site. If you have suggestions to improve babylonjs it would be best I think to start a new thread in the “Feature Requests” subforum and/or consider updating official docs as more experts can weigh in.