Scripting
Dynamic Instantiation

Dynamic instantiation

With Meta Spark Studio’s dynamic instantiation feature you can create, destroy and reorder objects in the scene dynamically via script. Dynamic creation and destruction of scene objects, 2D text, materials and blocks are all supported via calls to their respective APIs.

This can result in faster load times and effect performance, as content can be loaded only when required instead of up-front along with the rest of the scene’s content. Additionally, content can be unloaded when not in use to free up valuable resources.

The additional control provided by dynamic instantiation also allows for easier creation of long-form interactive experiences as you can choose if and when certain content is displayed during the effect’s runtime. This is especially effective when coupled with downloadable blocks.

Consider a story-driven effect with two endpoints, A and B, each of which is self-contained within a block. If a user’s journey through the effect takes them to endpoint A, you can dynamically instantiate the corresponding block without ever having to spend resources loading the contents of endpoint B.

The example project demonstrates how to dynamically instantiate and destroy an object, though more in-depth code examples are provided in this article.

Using dynamic mode

Dynamic objects are not displayed in the UI by default. Instead, Meta Spark Studio automatically detects when a dynamic API is called within a script and displays a notification prompting you to switch to dynamic mode.

In this mode, dynamic objects are displayed in purple text in the Scene and Asset panels in the hierarchy order defined by the calling script.

With dynamic mode enabled you can easily visualize the current state of your effect as changes to dynamic objects, such as destruction, are reflected in the UI in realtime.


Dynamic objects can’t be edited from within the Meta Spark Studio UI. Their properties are exclusively set via script.


You can revert back to Meta Spark Studio’s normal mode by clicking the bottom status bar, or from the View option in the menu bar. This will hide all dynamic objects from the UI.

Creating dynamic objects

Scene objects can be dynamically created via calls to the create() method exposed by the SceneModule API. Blocks are instantiated via a call to the instantiate() method exposed by the BlocksModule API.


These method calls require the Scripting Dynamic Instantiation capability to be enabled within the project's Properties in Meta Spark Studio.


Dynamic objects must have their properties set from within the script they are instantiated from, as they are not editable from the Meta Spark Studio UI.

To be rendered within the effect, dynamically created scene objects must be added as a child of an object in the Scene hierarchy using the addChild() method exposed by the SceneModule API. This also applies to blocks being instantiated at runtime.

The following example creates instances of each dynamic object supported by dynamic instantiation and adds them as children of the Focal Distance object from the Scene panel.

// Load in the required modules
const Scene = require('Scene');
const Blocks = require('Blocks');
const Reactive = require('Reactive');

// Enables async/await in JS [part 1]
(async function() {

    // Locate the focal distance object in the scene
    const focalDistance = await Scene.root.findFirst("Focal Distance");

    // Create a single instance of each scene object supported by dynamic instantiation
    const [dynamicPlane, dynamicCanvas, dynamicRectangle, dynamicAmbientLight, dynamicDirectionalLight, dynamicPointLight, dynamicSpotLight, dynamicParticleSystem, dynamicNull] = await Promise.all([
        Scene.create("Plane", {
            "name": "Plane",
            "width": 0.1,
            "height": 0.1,
            "y": -0.2,
            "hidden": false,
        }),
        Scene.create("Canvas", {
            "name": "Canvas",
        }),
        Scene.create("PlanarImage", {
            "name": "Rectangle",
        }),
        Scene.create("PlanarText", {
            "name": "Text2D",
        }),
        Scene.create("AmbientLightSource", {
            "name": "AmbientLight",
        }),
        Scene.create("DirectionalLightSource", {
            "name": "DirectionalLight",
        }),
        Scene.create("PointLightSource", {
            "name": "PointLightSource",
        }),
        Scene.create("SpotLightSource", {
            "name": "SpotLightSource",
        }),
        Scene.create("ParticleSystem", {
            "name": "Particle",
        }),
        Scene.create("SceneObject", {
            "name": "NullObject",
        }),
    ]);


    // Add the dynamic objects as children of objects in the Scene panel - they will not be rendered in the effect otherwise
    focalDistance.addChild(dynamicCanvas);
    dynamicCanvas.addChild(dynamicRectangle);
    dynamicCanvas.addChild(dynamicText2D);          
    focalDistance.addChild(dynamicAmbientLight);
    focalDistance.addChild(dynamicDirectionalLight);
    focalDistance.addChild(dynamicPointLight);
    focalDistance.addChild(dynamicSpotLight);
    focalDistance.addChild(dynamicParticleSystem);
    focalDistance.addChild(dynamicNull);


    // Create an instance of a downloadable Block
    // This assumes an existing block in the project called 'block0'
    Blocks.instantiate('block0').then(function(block) {
        focalDistance.addChild(block);
        block.addChild(dynamicPlane);
    });    
              
// Enables async/await in JS [part 2]
})();
            

The 2D text capability is usually enabled automatically when you add a 2D text object. To create 2D text dynamically, you'll need to first add the capability from the Properties menu.

Dynamic creation of materials is also supported via a call to the create() method exposed by the MaterialsModule API. As with dynamic scene objects, the properties of dynamic materials must be set from within the script.

However, unlike dynamic scene objects, materials do not need to be made a child of an existing object in the Scene panel.

While the example below only dynamically creates a default material and a blended material, all of the following types are supported:

  • DefaultMaterial
  • BlendedMaterial
  • ColorPaintMaterial
  • ComposedMaterial
  • CustomMaterial
  • MetallicRoughnessPbrMaterial
  • RetouchingMaterial
// Load in the required modules
const Scene = require('Scene');
const Materials = require('Materials');
const Textures = require('Textures');
              
// Enable the Touch Gestures > Tap Gesture capability 
// in the project's properties
const TouchGestures = require('TouchGestures');

// Enables async/await in JS [part 1]
(async function() {

    // Locate the focal distant object in the Scene and the two textures in the Assets panel
    const [focalDistance, textureOne, textureTwo] = await Promise.all([
        Scene.root.findFirst('Focal Distance'),
        Textures.findFirst('texture0'),
        Textures.findFirst('texture1'),
    ]);

    // Dynamically instantiate a plane and two materials
    const [dynamicPlane, defaultMaterial, blendedMaterial] = await Promise.all([

        Scene.create("Plane", {
            "name": "Plane",
            "width": 0.1,
            "height": 0.1,
            "y": -0.2,
            "hidden": false,
        }),

        Materials.create("DefaultMaterial", {
            "name": "Default Material",
            "blendMode": "ALPHA",
            "opacity": 1.0,
            "diffuse": textureOne,
        }),

        Materials.create("BlendedMaterial", {
            "name": "Blended Material",
            "opacity": 0.8,
            "diffuse": textureTwo,
        }),
    ]);


    // Assign the first of the dynamic materials to the created plane.
    dynamicPlane.material = defaultMaterial;

    // Add the Dynamic Plane as a child object of the Focal Distance object in the Scene panel so that it is rendered in the effect
    focalDistance.addChild(dynamicPlane);


    // Switch the plane's material between the two previously created dynamic materials, when the plane is tapped
    let isUsingMaterialOne = true;

    TouchGestures.onTap(dynamicPlane).subscribe(() => {
        if(isUsingMaterialOne){
            dynamicPlane.material = blendedMaterial;
            isUsingMaterialOne = false;
        } else {
            dynamicPlane.material = defaultMaterial;
            isUsingMaterialOne = true;
        }
    });
              
// Enables async/await in JS [part 2]
})();             
            

Destroying dynamic objects

In order to free up resources, it’s considered best practice to destroy objects that aren’t in use and are no longer required by the effect. This is done via a call to the destroy() method exposed by the SceneModule and MaterialsModule APIs.


The destroy() method call requires the Scripting Dynamic Instantiation capability to be enabled within the project's Properties in Meta Spark Studio.


Destroying a scene object or material will automatically unbind any bound properties, with scene objects also being removed from any parent.

Only dynamic objects can be destroyed with a call to the destroy() method. Attempting to call the method on an object added via the Meta Spark Studio UI or a non-existent object will fail the Promise.

In the example below, a dynamic plane and material are instantiated when the effect is run and are destroyed when the plane is tapped, removing them from the Scene and Asset panels respectively.

 // Load in the required modules
const Scene = require('Scene');
const Materials = require('Materials');
const Textures = require('Textures');
              
// Enable the Touch Gestures > Tap Gesture capability 
// in the project's properties
const TouchGestures = require('TouchGestures');

// Enables async/await in JS [part 1]
(async function() {

    // Locate the focal distant object in the Scene and the two textures in the Assets panel
    const [focalDistance, texture] = await Promise.all([
         Scene.root.findFirst('Focal Distance'),
         Textures.findFirst('texture0'),
    ]);
              
    // Dynamically instantiate a plane and a material
    const [dynamicPlane, dynamicMaterial] = await Promise.all([

        Scene.create("Plane", {
            "name": "Plane",
            "width": 0.1,
            "height": 0.1,
            "y": -0.2,
            "hidden": false,
        }),

        Materials.create("DefaultMaterial", {
             "name": "Default Material",
             "blendMode": "ALPHA",
             "opacity": 1.0,
             "diffuse": texture,
        }),
    ]);

    // Set the dynamic material as the plane's material
    dynamicPlane.material = dynamicMaterial;

    // Add the Dynamic Plane as a child object of the Focal Distance object in the Scene panel so that it is rendered in the effect otherwise
    focalDistance.addChild(dynamicPlane);

    
    // Destroy the plane and material when the plane is tapped
    TouchGestures.onTap(dynamicPlane).subscribe(() => {
        Scene.destroy(dynamicPlane);
        Materials.destroy(dynamicMaterial);
    });
              
// Enables async/await in JS [part 2]
})();
            

Cloning materials

Materials can be cloned, making them easier to create and providing the ability to reuse a template material across multiple instances.

Just as you can create a given material via MaterialsModule.create(), the MaterialModule API also exposes a clone() method that accepts an instance of an existing material, as well as an optional initial state.


The clone() method call requires the Scripting Dynamic Instantiation capability to be enabled within the project's Properties in Meta Spark Studio.


There are a few important considerations when cloning a material:

  • New materials are assigned a globally unique name, unless otherwise specified in the initial state.
  • The clone() method’s second argument, initialState, is entirely optional but its use is highly encouraged to set the material’s initial state.
  • All Signal type properties are assigned a ConstSignal with the last value from the previously bound signal. To rebind any property you can supply a signal within the initialState argument.

The following example queries an existing material, clones it, and then sets it as the material of an existing object in the scene.

// Load in the required modules
const Scene = require('Scene');
const Materials = require('Materials');
const Textures = require('Textures');
const Reactive = require('Reactive');
              
// Enable the Touch Gestures > Tap Gesture capability 
// in the project's properties
const TouchGestures = require('TouchGestures');

// Enables async/await in JS [part 1]
(async function() {

    // Locate our objects in the Scene and Assets panels
    // This includes the texture which we will apply to our cloned material
    const [focalDistance, existingMaterial, newTexture] = await Promise.all([
        Scene.root.findFirst('Focal Distance'),
        Materials.findFirst('material0'),
        Textures.findFirst('texture0'),
    ]);

    // Dynamically create a plane and clone a material
    const [dynamicPlane, clonedMaterial] = await Promise.all([

        Scene.create("Plane", {
            "name": "Plane",
            "width": 0.1,
            "height": 0.1,
            "y": -0.2,
            "hidden": false,
        }),

        // Clone the existing material from the Assets panel and alter its initiation state, including specifying a new diffuse texture.
        Materials.clone(existingMaterial, {
            "name": "Cloned Material",
            "blendMode": "ALPHA",
            "opacity": 0.8,
            "diffuse": newTexture,
            "diffuseColorFactor": Reactive.RGBA(255,0,0,1),
        }),
    ]);

    // Assign the existing material to the dynamic plane
    dynamicPlane.material = existingMaterial;

    // Add the Dynamic Plane as a child object of the Focal Distance object in the Scene panel so that it is rendered in the effect
    focalDistance.addChild(dynamicPlane);

    // Switch the plane's material between the existing material and the cloned material when the plane is tapped
    let isUsingExistingMaterial = true;

    TouchGestures.onTap(dynamicPlane).subscribe(() => {
        if(isUsingExistingMaterial){
            dynamicPlane.material = clonedMaterial;
            isUsingExistingMaterial = false;
        } else {
            dynamicPlane.material = existingMaterial;
            isUsingExistingMaterial = true;
        }
    });
              
// Enables async/await in JS [part 2]
})();
                
            

Reparenting objects

Objects instantiated dynamically can be reparented at runtime using the addChild(), removeChild() and removeFromParent() methods exposed by the SceneModule and SceneObjectBase APIs.


These method calls require the Scripting Dynamic Instantiation capability to be enabled within the project's Properties in Meta Spark Studio.


Dynamic objects without a parent in the Scene panel will not be rendered until they are added back as a child of an object in the scene.

Removing a dynamic object from a parent does not automatically dispose of it. Destruction of dynamic objects must be performed via explicit calls to the destroy() method.

In the example below, a plane is reparented between two groups in the Scene panel when the plane is tapped. When the plane is long pressed the dynamic plane is removed from its parent, regardless of which of the two groups it was parented under.

// Load in the required modules
const Scene = require('Scene');
              
// Enable the Touch Gestures > Tap Gesture capability 
// in the project's properties
const TouchGestures = require('TouchGestures');

// Enables async/await in JS [part 1]
(async function() {

    // Locate the focal distant object in the Scene
    // Then dynamically instantiate two objects to use as groups and a plane
    const [focalDistance, groupOne, groupTwo, dynamicPlane] = await Promise.all([
        Scene.root.findFirst('Focal Distance'),

        Scene.create("SceneObject", {
            "name": "group0",
        }),

        Scene.create("SceneObject", {
            "name": "group1",
        }),

        Scene.create("Plane", {
            "name": "Dynamic Plane",
            "width": 0.1,
            "height": 0.1,
            "y": -0.2,
            "hidden": false,
        }),
    ]);


    // Add the groups as children of the Focal Distance object in the Scene panel, then add the plane as a child of the first group
    focalDistance.addChild(groupOne);
    focalDistance.addChild(groupTwo);
    groupOne.addChild(dynamicPlane);


    // Create a variable to track which of the two groups the plane belongs to
    let isAChildOfGroupOne = true;

    // Reparent the dynamic plane between the two groups when the plane is tapped
    TouchGestures.onTap(dynamicPlane).subscribe(() => {
        if(isAChildOfGroupOne){
            groupTwo.addChild(dynamicPlane);
            isAChildOfGroupOne = false;
        } else {
            groupOne.addChild(dynamicPlane);
            isAChildOfGroupOne = true;
        }
    });
    
    // Remove the dynamic plane from its parent when the plane is long pressed
    TouchGestures.onLongPress(dynamicPlane).subscribe(() => {
        dynamicPlane.removeFromParent();
    });
              
// Enables async/await in JS [part 2]
})();
            

Downloadable blocks

Any block that is included in your effect via dynamic instantiation can be made Downloadable. This means that rather than the block being downloaded to a user’s device when they first open the effect (as is the default for all other assets), it will instead be downloaded when it is first instantiated.

You may find this useful if you are creating a long-form AR experience or an experience with many possible outcomes. For example, imagine an AR game you are creating displays a 3D model of a bronze, silver or gold trophy depending on the outcome of the game. By using downloadable blocks, you could avoid downloading the trophy models until the outcome is nearly decided. Alternatively, you could download only the single trophy model that is being shown to the user, removing the need to download all three.

To make a block downloadable, it must be instantiated via a call to the instantiate() method exposed by the BlocksModule API. It can then be included in your scene using the he addChild() method exposed by the SceneModule API. Once the block is no longer needed in the scene, it can be destroyed using the destroy() method exposed by the SceneModule and MaterialsModule APIs.

To make a block in your effect downloadable:

  1. Select your block in the Assets panel.
  2. Check the box next to Downloadable.
The block is made downloadable by checking a box in the Inspector.

By default, a block that is set as downloadable will start loading as soon as it is instantiated. But, in some instances, you may want to load the contents of a block slightly before it will be used in the effect. Blocks can be downloaded before they are first instantiated using the download method found in the blocksModule class.

Note: Downloadable blocks are still counted in your effects total file size, and are not a method for creating effects that exceed the file size requirements. However, using dynamically instantiated, downloadable blocks does allow for optimization of load times and performance.