BabylonJS / Babylon.js

Babylon.js is a powerful, beautiful, simple, and open game and rendering engine packed into a friendly JavaScript framework.
http://www.babylonjs.com
Apache License 2.0
22.75k stars 3.39k forks source link

Configurable scene loader (e.g. glTF options) #15222

Open ryantrem opened 2 weeks ago

ryantrem commented 2 weeks ago

Currently configuring loader options is cumbersome and error prone. If you wanted to adjust a glTF loader option as well as the MSFT_lod extension options for example, your code would look something like this:

const pluginActivatedObserver = SceneLoader.OnPluginActivatedObservable.add((loader) => {
  if (loader.name === "gltf") {
    const glTFLoader = loader as GLTFFileLoader;
    glTFLoader.skipMaterials = true;
    glTFLoader.onExtensionLoadedObservable.add((extension) => {
      if (extension.name === "MSFT_Lod") {
        const msftLodExtension = extension as MSFT_lod;
        msftLodExtension.maxLODsToLoad = 5;
      };
   });
  }
});

// We want to configure the glTF loaded by this call
await SceneLoader.ImportMeshAsync(...);

// Oops we also configured the glTF loaded by this other call
await SceneLoader.ImportMeshAsync(...);

pluginActivatedObserver.dispose();

This is somewhat difficult, especially the deeper the options you want to set (such as glTF 2.0 extension options). It is also not super type safe as it requires casting plugins and extensions to known types. Also the configuration requires modifying global/static state, which means it is possible to inadvertently configure other imports besides your own.

While thinking about solutions to this problem, it occurred to me that a solution to this problem is more obvious when we think about it in the context of another problem, which is eliminating our dependency on side effects. Currently loader plugins are registered via side effects, glTF version loaders are registered via side effects, and glTF 2.0 extensions are registered via side effects. If we were not relying on side effects, then the user would do something explicit to opt in to the loaders, glTF versions, and glTF extensions they actually want to support. If they are already doing that, then it would present and obvious place to put configuration for those plugin/loader/extensions. Given this, my proposed changes in this PR work towards solving both of these problems by making providing the option of explicitly choosing what plugin/loader/extensions you want, and configuring them while you are at it. The replacement code for the above would then be:

const sceneLoader = new SceneLoader({
  plugins: [
    GLTFFileLoader.Configure({
      skipMaterials: true,
      loaders: {
        ...GLTF2.GLTFLoader.Configure({
          extensions: {
            ...GLTF2.Loader.Extensions.MSFT_lod.Configure({
              maxLODsToLoad: 5,
            }),
          },
        }),
      },
    }),
  ],
});
await sceneLoader.importMeshAsync(...);
sceneLoader.dispose();

The advantages of this are:

  1. Conceptually easier to understand (subjective).
  2. Fully type safe, no casting (you are explicitly interacting with the plugin/loader/extension types and therefore their associated options.
  3. No global state modified, the options you specify apply to your own SceneLoader instance (the Configure calls just return factories that can instantiate the plugin/loader/extension with options).
  4. You opt in to only the stuff you care about (in the example above, only glTF 2.0 and only the MSFT_lod extension).

This approach is also fully backward compatible, as it simply introduces the option to instantiate a SceneLoader and configure it exactly the way you want (leveraging the existing factory function logic). I think this approach is inline with a future CoreScene/CoreSceneLoader/CoreSceneLoaderPlugin as well, so we could likely rework it to be built on top of that layer when it is created.

High level notes on the changes:

Completing this approach will require:

bjsplat commented 2 weeks ago

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s). To prevent this PR from going to the changelog marked it with the "skip changelog" label.

bjsplat commented 2 weeks ago

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s). To prevent this PR from going to the changelog marked it with the "skip changelog" label.

deltakosh commented 2 weeks ago

I do like it! And yes we protect backward compat for now! (as we wont remove side effects)

This is a great idea!

ryantrem commented 2 weeks ago

A side-note - I find the plugin configuration a bit complicated (syntax-wise). Adding it to importMesh would make more sense TBH, but we will probably have an issue with back-compat.

I initially thought about passing options directly to importMesh, but there are a number of complications. Things like how do we route the options through to the right plugin/loader/extension in a type safe and non-error prone way, and what if someone passes through options for a plugin/loader/extension they have not opted in to use?

Configuring the plugin/loader/extensions as part of opting in to using them doesn't face these same problems. Since SceneLoader doesn't actually hold much state, and instance is basically (as you said) a specific configuration of loading logic, so it is quite light weight and creating multiple instances shouldn't be a problem. It also has the advantage that once you setup your desired configuration, you can just make import calls on that instance many times without having to pass through a complex configuration for every call.