Scripting
Dynamic Instantiation

Dynamic Instantiation

With Spark AR 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, 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.

Using dynamic mode

Dynamic objects are not displayed in the UI by default. Instead, Spark AR 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 Spark AR Studio UI. Their properties are exclusively set via script.

You can revert back to Spark AR 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.

Dynamic objects must have their properties set from within the script they are instantiated from, as they are not editable from the Spark AR 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.

With dynamic mode enabled, these objects will be visible within Spark AR Studio’s UI as entries in purple text.

// 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 material and texture in the Assets
    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("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
    // Once added as a child, these dynamic objects will be displayed when Dynamic Mode is enabled in Spark AR Studio
    focalDistance.addChild(dynamicCanvas);
    dynamicCanvas.addChild(dynamicRectangle);
    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);
    });    
})();
            

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');
 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",
             "blendMode": "ALPHA",
             "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((gesture) => {
         if(isUsingMaterialOne){
             dynamicPlane.material = blendedMaterial;
             isUsingMaterialOne = false;
         } else {
             dynamicPlane.material = defaultMaterial;
             isUsingMaterialOne = true;
         }
     });
 })();              
            

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.

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 Spark AR 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');
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,
        }),
    ]);


    // 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
    // Once added as a child, these dynamic objects will be displayed when Dynamic Mode is enabled in Spark AR Studio
    focalDistance.addChild(dynamicPlane);

    
    // Destroy the plane and material when the plane is tapped
    TouchGestures.onTap(dynamicPlane).subscribe((gesture) => {
        Scene.destroy(dynamicPlane);
        Materials.destroy(dynamicMaterial);
    });
})();
            

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.

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 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.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,
         }),
     ]);
                
     // 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 to the cloned material when the plane is tapped
     TouchGestures.onTap(dynamicPlane).subscribe((gesture) => {
         dynamicPlane.material = clonedMaterial;
     });
 })();
                
            

Reparenting objects

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

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');
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": "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((gesture) => {
        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((gesture) => {
	dynamicPlane.removeFromParent();
    });
})();
            
Was this article helpful?