beginDirectAnimation doesn't exist

Having previously used with BabylonViewer this function I’ve seen recommended multiple times:

BabylonViewer.BABYLON.ArcRotateCamera.prototype.spinTo = function (whichprop, targetval, speed) {
    var ease = new BabylonViewer.BABYLON.CubicEase();
    ease.setEasingMode(BabylonViewer.BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
    BabylonViewer.BABYLON.Animation.CreateAndStartAnimation('at4', this, whichprop, speed, 120, this[whichprop], targetval, 0, ease);
}

I’m now trying to replicate this camera “spinTo” function with Babylon core rather than the Viewer. So, the following:

import { Engine } from "@babylonjs/core/Engines/engine";
import { Scene } from "@babylonjs/core/scene";
import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera";
import { Animation } from "@babylonjs/core/Animations/animation";
import { CubicEase, EasingFunction } from "@babylonjs/core/Animations/easing";
[...]
ArcRotateCamera.prototype.spinTo = function (whichprop, targetval, speed) {
    var ease = new CubicEase();
    ease.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);
    Animation.CreateAndStartAnimation('at4', this, whichprop, speed, 120, this[whichprop], targetval, 0, ease);
}

Unfortunately this is throwing an error in the CreateAndStartAnimation function:

Uncaught TypeError: scene.beginDirectAnimation is not a function
    at Animation.CreateAndStartAnimation

I’ve checked that this is bound correctly in the spinTo function (it correctly returns the ArcRotateCamera, and this.getScene() - which is what the CreateAndStartAnimation function uses - also returns the scene correctly), but it seems like the Scene prototype itself doesn’t contain the function. I’ve found that beginDirectAnimation seems to be in the Animatable class, but surely I don’t need to explicitly import it as it’s being used by the already-imported Animation class right? Even if I do import it I assume webpack won’t do anything different, as it’s not being called directly from my code.

Any ideas? My assumption was that as a dependency of a function I’m calling it should work automatically with the ES6 npm version of babylon.js…

Here is the answer to similar question, hope it will help - What is the correct way to extend a namespace/module with babylon in TS? - #6 by RaananW

3 Likes

You are missing a side-effect in this case. You will need to add import "@babylonjs/core/Animations/animatable"; , if you are not already doing that, this is where the scene gets this function added. I assume this is a runtime error and not a typescript error, right?

2 Likes

Ah - that solves it. I thought I’d tried that but had only tried this, which didn’t work as Animatable isn’t explicitly used by my code so it wasn’t actually being included:

import { Animatable } from "@babylonjs/core/Animations/animatable"

Thank you and apologies for the basic question!

2 Likes

Guys, this is pretty interesting because I never ran into this issue. I was so curious that I’ve created a small test project and I don’t have to import Animatable to get it working.

babylonjs-animatable-test.zip (31.6 KB)

Unzip to a dir.
cd to_dir
npm i
npm run dev

I implemented two approaches. First one using prototype and the second one by extending the ArcRotateCamera class.

Just change the import in main.ts :

import { AppProto as App } from './AppProto';
// import { AppExtends as App } from './AppExtends';

Both works w/o importing the Animatable class.

@RaananW do you have any idea what is happening then here?

Sorry for mixing ts and js and for the ts errors in the editor (lack of time) :see_no_evil: :crazy_face: :nerd_face:

EDIT: ok laziness LOL and it’s good enough to run :stuck_out_tongue:

1 Like

You are importing from “@babylonjs/core”. this will practically import the entire framework, similar to UMD.

2 Likes

Yes, that’s what I assumed. Importing everything more atomically is “annoying” (in that I end up with quite a lot of imports at the top of each file) but is definitely worth that if it reduces file size. It does lead to the occasional complication like my discovery of this Animatable side-effect though :sweat_smile:

2 Likes

Always learning something new. Thanks!

1 Like

If you set your bundler config to remove the unused modules they shouldn’t be included in the final bundle. I am not sure about webpack, I use vite (and it’s pretty good on removing unused code, it uses Rollup + vite has a cool tree shaking visualizer) but I believe @RaananW could surely give us more precise info how good is wepack performing when it comes to treeshaking.

1 Like

Hmm, good point. I have my webpack project set up for tree shaking (as far as I understand at least, sideEffects = false and in webpack config usedExports: true, minimize: true,), and testing it out I now understand it even less lol. Across my different pages using babylon.js (viewer on 2, core on the third), with most being a common bundle:

Importing the entire library as you mention, I’m getting a bundled size of 4.59 MiB 4.84 MiB 4.6 MiB. It also seems to contain two levels of @babylonjs/core for some reason, although as far as I can tell each of those contains slightly different things.

Now if I use the more specific imports like:

import { Engine } from "@babylonjs/core/Engines/engine";
import { Scene } from "@babylonjs/core/scene";
import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera";
[etc.]

the bundle size actually goes up for some reason. The difference is very small but it is there: 4.64 MiB 4.88 MiB 4.64 MiB

This leaves me with absolutely no clue what’s going on - my assumption was that, with tree shaking supposedly enabled for both, importing the entire library would end up larger than just importing specifically the classes I use, but the opposite (although marginally) seems to be true…

full webpack config because why not
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  entry: {
    home: './src/home.js',
    class: './src/class/class.js',
    'ar-viewer': './src/ar-viewer/ar-viewer.js',
    'model-viewer': './src/model-viewer/model-viewer.js',
    create: './src/create/create.js'
  },
  output: {
    filename: (pathData) => {
      const name = pathData.chunk.name;
      if (name === 'home') return 'home.bundle.js';
      return `${name}/[name].bundle.js`;
    },
    chunkFilename: (pathData) => {
      const name = pathData.chunk.name;
      if (name.startsWith('home')) return '[name].bundle.js';
      return `${name}/[name].bundle.js`;
    },
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  plugins: [
    new BundleAnalyzerPlugin(),
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: './src/index.html',
      chunks: ['home']
    }),
    new HtmlWebpackPlugin({
      filename: 'class/index.html',
      template: './src/class/index.html',
      chunks: ['class']
    }),
    new HtmlWebpackPlugin({
      filename: 'model-viewer/index.html',
      template: './src/model-viewer/index.html',
      chunks: ['model-viewer']
    }),
    new HtmlWebpackPlugin({
      filename: 'ar-viewer/index.html',
      template: './src/ar-viewer/index.html',
      chunks: ['ar-viewer']
    }),
    new HtmlWebpackPlugin({
      filename: 'create/index.html',
      template: './src/create/index.html',
      chunks: ['create']
    }),
    new CopyWebpackPlugin({
      patterns: [
        { from: '**/*.css', to: '[path][name][ext]', context: 'src' },
        { from: 'src/res', to: 'res' }
      ]
    })
  ],
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: false,
        vendors: false,
        common: {
          name: 'common',
          chunks: 'all',
          minChunks: 2,
          priority: 30,
          reuseExistingChunk: true,
          enforce: true,
        },
        homeVendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'homeVendors',
          chunks: (chunk) => chunk.name === 'home',
          priority: 20,
          enforce: true,
        },
        classVendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'classVendors',
          chunks: (chunk) => chunk.name === 'class',
          priority: 20,
          enforce: true,
        },
        modelViewerVendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'model-viewer-vendors',
          chunks: (chunk) => chunk.name === 'model-viewer',
          priority: 20,
          enforce: true,
        },
        arViewerVendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'ar-viewer-vendors',
          chunks: (chunk) => chunk.name === 'ar-viewer',
          priority: 20,
          enforce: true,
        },
        createVendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'create-vendors',
          chunks: (chunk) => chunk.name === 'create',
          priority: 20,
          enforce: true,
        }
      }
    },
    usedExports: true,
    minimize: true,
    minimizer: [
      '...',
      new CssMinimizerPlugin()
    ]
  },
  performance: {
    hints: 'warning',
    maxAssetSize: 512000,
    maxEntrypointSize: 512000,
    assetFilter: function (assetFilename) {
      // Only provide performance hints for JS and CSS files
      return /\.(js|css)$/.test(assetFilename);
    }
  }
};

1 Like

The same confusion here. Logically the bundle size should be smaller because by using the more specific imports you already pre-treeshake the bundle. That’s what I thought but seems not to be true for webpack.

This is what I found online:

Both Webpack and Vite can perform tree shaking, a method which helps eliminate unnecessary code during the build process in order to reduce bundle sizes.

In contrast, Vite uses native ES module system for better tree shaking hence reducing bundles that are smaller and optimized.

1 Like

Yeah, it makes no sense to me :sweat_smile: I’m going to try migrating a version of my project to Vite to see what build sizes look like then - from a quick look it seems like Vite should hopefully not take too long to set up (haven’t used it before), and it will be a very useful test to see which bundles better.

You are using the viewer, and you are probably populating the entire BabylonViewer namespace, so the package will be the full size of the viewer. There is always an explanation to everything.

Webpack does tree-shaking quite well. Vite will not do better in these conditions, and if it will it doesn’t follow the package.json guidelines. Babylon is marked with sideEffect: true, meaning importing any module in the package will result in the entire module loaded, added to the package, and executed (hence - side effects). This prevents any tree-shaking mechanism to work in its full capacity. Any import within a loaded module will behave the same - loaded, executed, included fully in the bundle. This is why we rcommend loading from the files directly and not from @babylonjs/core.

Now, regarding two versions of the framework, make sure that your only babylon core dependency is the one from the viewer and that you don’t install a different version yourself.

Webpack has a lot of problems. Some of them are solved by many other bundlers. But in this case, it’s not webpack :slight_smile:

2 Likes

Oh - thank you very much for the explanation!

What exactly do you mean by “loading from the files directly and not from @babylonjs/core”? The docs seem to recommend doing it like import { Engine, Scene } from "@babylonjs/core", is there a better way to do it (using npm/ES6 version)?

I’m currently switching away from the Viewer for everything - as you say having a mix of the Core from the Viewer and then also using Core stuff without the Viewer might be causing some of my issues!

1 Like

the better way would be to do:

import { Scene } from "@babylonjs/core/scene";
import { Engine } from "@babylonjs/core/Engines/engine";

It is documented here - Babylon.js docs , but I feel like this could be better described in this page. i’ll put it on the todo list to update the page with more detailed information.

2 Likes

Webpack, package.json: Does it make any difference when I set sideEffects: true or when I add all files sideEffects: [file1.js, file2.js, ...etc]? I assume the second option just reduces the build time but the result will be the same. Is that the case?

Well - just switching away from the Viewer from one of the pages using it has slashed the size for that page from 4.6MiB down to 1.72MiB. Seems like the Viewer was my main issues as @ RaananW said! Time to switch over to babylonjs/core for the other remaining page using Viewer as well. It seems obvious now but I naively thought webpack would be able to tree-shake BabylonViewer :sweat_smile:

2 Likes

:nerd_face:

1 Like