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.
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.
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:
// 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] })();
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] })();
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:
clone()
method’s second argument, initialState
, is entirely optional but its use is highly encouraged to set the material’s initial state.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] })();
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] })();
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:
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.