If you haven’t noticed, there has been a major change to scripting in Spark AR on V85. If all your filters don’t include any form of scripting, you are in luck, you can skip this article and go on with your life. But if you have used some scripting such as Native UI and sliders like me, this change will make you question the reality of life itself (ok, maybe just the reality of coding in Javascript). Fear not though! At first glance, it might make you want to give up scripting in Spark AR but it is actually not that complicated once you force yourself to understand how the new changes work.
Disclaimer: This article is written for creators that used only some scripting such as Native UI, Sliders, getting face feature position etc in their filters. If you have been using script mostly for your filters, this article probably won’t help you in updating your advance scripting filters.
So What Are The Changes?
The main change in V85 is the transition to scripting in Promise. According to this great article written by Eric Elliot, a promise is an object that may produce a single value sometime in the future: either a resolved value or a reason that it’s not resolved (e.g., a network error occurred). If this still doesn’t make any sense to you (It’s normal, trust me), you can think of promise as a deal you can make with your friends. For example, only when your buddy gives you some money, then you will help him to buy his dinner. In Javascript case, finding an object is equivalent to your buddy giving you money and executing the code is you buying the dinner. Only when an object you want is found, then the code which involves the object will be executed.
Promise.all([
Scene.root.findFirst('plane0'),
]).then(function(results){
const plane = results[0];
plane.transform.x = 0.5;
})
As shown in the code above, only when “plane0” is found in Spark AR, then the plane’s X position will be assigned as 0.5.
This way of coding is supposed to help coders write cleaner code, provide better performance for event-based programs and also better at handling error. Without going too deep into what it means, let’s get on to how you can update your effects to fulfil the promise.
Finding Object in Spark AR in Scripting
//Old Way Of Finding Object
const plane = Scene.root.find('plane0')
//New Way of Finding Object
Promise.all([
Scene.root.findFirst('plane0'),
]).then(function(results){
const plane = results[0];
plane.transform.x = 0.5;
})
Using the same example, let’s compare how to find an object you created in Spark AR and assign it to a constant name in script. The old way of doing it is very simple, you just need to locate ‘plane0’ from the Scene and assign it to constant ‘plane’ in 1 line.
To write it in Promise, you would need to find ‘plane0’ with .findFirst( ) instead of find() without assigning it to anything. Once the promise is fulfilled (Object found), then the function in .then( ) will be executed. You can then assign what the promise found, which is ‘results’ in this case, to constant ‘plane’. Note that I am using results[0] here, which means the first object found in the promise because counting always starts from 0 in coding. If you have more object to find from Spark AR, you just need to add more lines of .findFirst( ) and assign it accordingly in the same order.
Promise.all([
// Loading Textures for the buttons
Textures.findFirst('1'), //Texture : 0
Textures.findFirst('2'), //Texture : 1
Textures.findFirst('3'), //Texture : 2
// Loading the Materials we are switching on the plane
Materials.findFirst('red'), //Material : 3
Materials.findFirst('green'),//Material : 4
Materials.findFirst('blue'), //Material : 5
// Loading the plane
Scene.root.findFirst('plane0'), //Object : 6
// Now, we wait for a "go ahead" from the script to let us know when
// we can start using our assets and creating the NativeUI Picker
]).then(function(results){
// Assets are loaded, so let's set them all so we can use them later in the script.
// The assets are all returned in an object called "results" in the order that we
// loaded them. Remember, the index starts at 0 so the first asset is always results[0],
// the next is results[1], etc.
// First, we set the buttons for the NativeUI Picker
const button1 = results[0]; //Texture 0
const button2 = results[1]; //Texture 1
const button3 = results[2]; //Texture 2
// Next, we set the materials for the plane
const red = results[3]; //Material : 3
const green = results[4]; //Material : 4
const blue = results[5]; //Material : 5
// Finally, we set the plane
const plane = results[6]; //Object : 6
Taking the Native UI code written for V85 by the amazing Luke Hurd as an example, you can count the order which Textures/Materials/Objects are found and use them as result[ i ] in .then( ), with “i” as the order of the Textures/Materials/Objects found starting from 0. Once you have assigned the Textures/Materials/Object to a constant, you can use them as per normal as before, as long as you keep all the code within the { } of the function.
const configuration = {
// This index controls where the NativeUI Picker starts.
// Let's keep things simple for now and start on the first
// button, so we keep it at 0. Remember most things start at 0, not 1.
selectedIndex: 0,
// These are the image textures to use as the buttons in the NativeUI Picker
items: [
{image_texture: button1},
{image_texture: button2},
{image_texture: button3}
],
// These are the materials we are switching between on the plane
mats: [
{material: red},
{material: green},
{material: blue}
]
};
// Create the NativeUI Picker
const picker = NativeUI.picker;
// Load our configuration
picker.configure(configuration);
// Show the NativeUI Picker
picker.visible = true;
// This is a monitor that watches for the picker to be used.
picker.selectedIndex.monitor().subscribe(function(val) {
// When a button is selected, we select the corresponding material.
// When they pick the first button then the first material loads, etc
plane.material = configuration.mats[val.newValue].material
});
});
Taking Native UI as an example again, you can clearly see that after assigning the results[ i ] to a constant, the codes are pretty much the same as before.
Script To Patch Bridge
Passing values or signal from patch editor to script or vice versa is a very useful trick, especially when you want to do scoring for your game filter. To set a value/signal from script to patch on V85, you need to use Patches.inputs.setScalar( ) :
var mynum = 10;
//Old
Patches.setScalarValue("num",mynum);
//New
Patches.inputs.setScalar("num",mynum);
To get a value/signal from patch to script is a little different and require more lines of code than before:
Promise.all([
Scene.root.findFirst('2DText0')
//Finding object
]).then(function(results){
//Only when object is found, then the following code will be executed
const scoretext = results[0];
//Old way of getting value from patch to script to use as text
scoretext.text = Patches.getScalarValue('numnew').toString();
//New way of getting value from patch to script to use as text
Patches.outputs.getScalar('numnew').then(val=>{
val.monitor().subscribe(({newValue}) => {
scoretext.text = newValue.toString();
})
})
})
The example above is also how you can get number from patch editor and convert to 2D text in Spark AR through scripting.
LookAt API
Adam Ferriss has provided a great example of how we can use LookAT API to create billboarding effect in Spark AR. To use that in V85, all you need is to make some slight changes to find the object in Spark AR with. findFirst( ).
const Scene = require("Scene");
const R = require("Reactive");
const Time = require("Time");
Promise.all([
Scene.root.findFirst('plane0'),
Scene.root.findFirst('followNull'),
Scene.root.findFirst('targetNull'),
]).then(function(results){
// Finds an element in the scene
// - Parameters:
// e: The object to find
//const find = e => S.root.find(e); *NOT NEEDED FOR V85*
// Gets the position of an object as an R.point
// - Parameters:
// e: The object to get the transform from
const getPosition = e => R.point(e.transform.x, e.transform.y, e.transform.z);
// Sets the rotation based on a transform
// - Parameters:
// e: The object to rotate
// p: The transform to use
const setRotation = (e, p) => {
e.transform.rotationX = p.rotationX;
e.transform.rotationY = p.rotationY;
e.transform.rotationZ = p.rotationZ;
};
// Look at utility function.
// Because of reactive stupidness, we can't actually apply the lookat directly to the looker itself
// We get around this by nesting the looker object inside a null with no values applied to it's transform
//
// - Parameters:
// _target: The object in the scene you want to face
// _lookerParent: The parent object of the object you want to rotate. Should have no transform applied to it
// _looker: The object that you want to rotate towards the target
const lookAt = (_target, _lookerParent, _looker) => {
const ptToLookAt = getPosition(_target);
const lookAtTransform = _lookerParent.transform.lookAt(ptToLookAt);
setRotation(_looker, lookAtTransform);
};
const plane = results[0];
const followNull = results[1];
const targetNull = results[2];
// Random animation
const scl = R.val(0.1);
targetNull.transform.x = R.sin(Time.ms.mul(R.val(0.001))).mul(scl);
targetNull.transform.y = R.cos(Time.ms.mul(R.val(0.0007))).mul(scl);
targetNull.transform.z = R.sin(Time.ms.mul(R.val(0.0005))).mul(scl);
// Do the look at
lookAt(targetNull, followNull, plane);
})
Getting 2D Face Position from Script
Using Josh Beckwith’s Vignettes and 2d Face Tracking tutorial as an example, we just need to add some modification to how we find objects with .findFirst( ) and we will be able to extract 2D face value from script to patch editor in V85.
const Scene = require('Scene')
const Patches = require('Patches')
const R = require('Reactive')
const FaceTracking = require('FaceTracking')
const face = FaceTracking.face(0)
const camCoords = face.cameraTransform.applyTo(face.nose.tip)
Promise.all([
Scene.root.findFirst("Camera"),
]).then(function(results){
const cam = results[0];
const focalPlane = cam.focalPlane
const u = R.add(R.div(R.div(camCoords.x, R.div(focalPlane.width, 2)), 2), .5)
var v = R.add(R.div(R.div(camCoords.y, R.div(focalPlane.height, 2)), 2), .5)
v = R.sub(1, v)
Patches.inputs.setPoint(`face_2d`, R.point(u, v, 0));
})
Summary
In summary, the main changes of V85 promise scripting are how we tell the code to find the object we created in Spark AR using .findFirst( ). Only when objects are found as a promise, then the codes to modify the object will be executed. Hence, most of what you will do to update your codes are:
Find all objects in your code with .findFirst( ) in Promise.all ( [ ] )
Assign the object to a constant in .then( function ( results ) { } )
Write the rest of your codes within .then( function ( results ) { } ) as per normal
I hope this will help to ease your pain in how Promise are coded in V85, which is actually less scary as it first looks when you read more into it!
Find more useful tips for making Instagram & Facebook filters at GOWAAA!
Follow us on Instagram to see our filters!
תגובות