Properties
User-editable properties can be added to your custom entities (i.e. your behaviors and components). These make it possible to configure your entity from within the user interface of the 3D editor, thus allowing users to build content using your behavior or component without having to edit its source code.
Types of properties
There are two types of properties; Runtime properties and Constructor properties:
Property type | Description |
---|---|
Runtime properties | Can change while your experience is running. They’re great for most of the configurable parameters of your entity, especially those that users of your entity will want to animate. They’re implemented as member variables on your component/behavior class. Examples include position , scale , and rotation . |
Constructor properties | Have one value for the lifetime of your experience when it’s running on an end-user’s device. For these properties to change, the experience must be reloaded. Constructor properties are great for parameters that result in load times (such as the paths for assets to load). They’re implemented as an object that’s passed into the constructor function of your component/behavior class. |
The table below exemplifies how each property type can be affected:
Property type | Can be changed during the experience | Can be animated with timelines and states | Fixed value for the lifetime of your experience |
---|---|---|---|
Runtime property | ✅ | ✅ | ❌ |
Constructor property | ❌ | ❌ | ✅ |
Using Runtime Properties
Runtime properties are defined by adding a public member variable to your component or behavior class and adding the @zui
annotation. The annotation tells Mattercraft to show your property in the Node or Behavior properties panels of the 3D editor.
Let’s take a look at an example:
/**
* @zcomponent
* @zicon favorite
*/
export class CustomThreeJSComponent extends Group {
/**
* @zui
* @zdefault 0
*/
public metalness: number;
constructor(contextManager: ContextManager, constructorProps: ConstructorProps) {
super(contextManager, constructorProps);
// Construct a material for our sphere
const material = new THREE.MeshStandardMaterial();
material.roughness = 0;
// Construct our sphere, referencing the material
const myObject = new THREE.Mesh(
new THREE.SphereGeometry(),
material,
);
// Add our sphere to this component's Group
this.element.add(myObject);
this.register(useOnBeforeRender(contextManager), dt => {
// Every frame, update the material's metalness to the most recent
// value from our `metalness` prop or '0' if it's not set
material.metalness = this.metalness ?? 0;
});
}
}
Taking a closer look at the constructor, we added the private member variable:
/**
* @zui
* @zdefault 0
*/
public metalness: number;
Then inside the constructor function for our component, we update the material’s metalness from our component’s property in every frame:
this.register(useOnBeforeRender(contextManager), dt => {
// Every frame, update the material's metalness to the most recent
// value from our `metalness` prop or '0' if it's not set
material.metalness = this.metalness ?? 0;
});
The @zdefault 0
annotation tells the Mattercraft 3D editor what value to show as the ‘default’ value in the property input box. Here’s what the 3D editor shows for our custom ‘Metalness’ property:
Using Observables
In our example so far, we update the material’s metalness
in every render frame. This is inefficient, since the value of our metalness
runtime property does not change every frame - it only changes when the user is editing the property in the input box.
To make this more efficient, Mattercraft provides Observable
. It’s an object that keeps track of a property for us and allows us to run some code only when the value of the property changes.
Let’s rewrite our custom component using an observable:
/**
* @zcomponent
* @zicon favorite
*/
export class CustomThreeJSComponent extends Group {
/**
* @zui
* @zdefault 0
*/
public metalness = new Observable(0);
constructor(contextManager: ContextManager, constructorProps: ConstructorProps) {
super(contextManager, constructorProps);
// Construct a material for our sphere
const material = new THREE.MeshStandardMaterial();
material.roughness = 0;
// Construct our sphere, referencing the material
const myObject = new THREE.Mesh(
new THREE.SphereGeometry(),
material,
);
// Add our sphere to this component's Group
this.element.add(myObject);
// Update the material's metalness according
// to the observable's value
this.register(this.metalness, value => {
material.metalness = value;
});
}
}
Taking a closer look, we construct our metalness
prop using an observable, passing in the default value we’d like:
/**
* @zui
* @zdefault 0
*/
public metalness = new Observable(0);
Then, in our constructor, we register a function that’s called every time the value of our Observable changes:
// Update the material's metalness according
// to the observable's value
this.register(this.metalness, value => {
material.metalness = value;
});
Observables are used throughout the components and behaviors included with Mattercraft. If you want to read or change the current value of an Observable, you can use its .value
property, like this:
// Set metalness to 0.5
this. metalness.value = 0.5;
console.log('Current metalness', this.metalness.value);
Observables even work with JavaScript arrays - they deeply observe the arrays and will call registered functions if any of the elements within the array change
Constructor Properties
Constructor properties are passed into the constructor of your component or behavior.
Three.js’s THREE.SphereGeometry
object takes a radius
option in its constructor, so let’s create and pass through a radius
constructor property for our example component.
Since it’s a constructor property, whenever the user edits it in the 3D editor, Mattercraft will reload our scene, constructing our component (and thus our THREE.SphereGeometry
) again with the new radius
value. To keep things concise, we’ve removed the code we added earlier on this page.
interface ConstructorProps {
/**
* @zui
* @zdefault 1
*/
radius: number
}
/**
* @zcomponent
* @zicon favorite
*/
export class CustomThreeJSComponent extends Group {
constructor(contextManager: ContextManager, constructorProps: ConstructorProps) {
super(contextManager, constructorProps);
// Construct our sphere, referencing the material
const myObject = new THREE.Mesh(
new THREE.SphereGeometry(constructorProps.radius ?? 1),
new THREE.MeshBasicMaterial(),
);
// Add our sphere to this component's Group
this.element.add(myObject);
}
}
This first change to notice is that we’ve added the details for our radius
property to the ConstructorProps
interface at the top of our file:
interface ConstructorProps {
/**
* @zui
* @zdefault 1
*/
radius: number
}
As with runtime properties, we’ve indicated to Mattercraft that we want this property to be shown in the 3D editor using the @zui
annotation, and we’ve provided the default value to be shown in the input box.
Then, in our component constructor, we pass our radius
constructor property into the THREE.SphereGeometry
constructor function:
// Construct our sphere, referencing the material
const myObject = new THREE.Mesh(
new THREE.SphereGeometry(constructorProps.radius ?? 1),
new THREE.MeshBasicMaterial(),
);
For most of the configurable parameters of your components and behaviors, it’s best to use runtime properties as they give the most flexibility to the user of the entity in the scene - they can be animated and changed during the experience without a reload.
For parameters that need to be passed to the constructors of objects (such as radius
in our example above), constructor properties ensure the experience runs consistently.
Using Property Annotations
We’ve already made use of two property annotations:
@zui
tells Mattercraft that we’d like a property to be displayed in the 3D editor user interface@zdefault
tells Mattercraft the default value of a property
There are several other annotations that you can use to customize how your component or behavior properties appear in the Mattercraft editor.
Organizing Node Properties
These annotations allow you to collect properties together in groups in the Node Properties Panel. Pass the name of the group along with the @zgroup
annotation, and a number with @zgrouppriority
.
The higher the number, the closer to the top of the Node Properties table the group will appear.
/**
* @zui
* @zdefault 0
* @zgroup Sphere Appearance
* @zgrouppriority 20
*/
public metalness = new Observable(0);
/**
* @zui
* @zdefault 0
* @zgroup Sphere Appearance
* @zgrouppriority 20
*/
public roughness = new Observable(0);
Customing your Node Properties
The @ztype
annotation lets you give Mattercraft a hint about how to represent your property in the Node Properties table.
@ztype proportion
This indicates the property represents a value that goes from 0 to 1. It’s shown in the property table with a draggable slider.
@ztype text-multiline
Shows a multi-line text input field.
@ztype angle-radians
and @ztype angle-degrees
Indicates that the property represents a value that’s either in radians or degrees. Mattercraft will show a switcher that allows the user to enter a value in the units they’re most comfortable with and will automatically convert that into the units required by the property.
@ztype color-*
Indicates that the property represents a color. Mattercraft shows a color panel that allows the user to pick a color from a swatch or list or saved values.
The following options are supported:
Property | Description |
---|---|
color-norm-rgb or color-norm-rgba |
Represents either a 3 or 4 element array of numbers for red, green, blue and (optionally) alpha, that range between 0 and 1. |
color-unnorm-rgb or color-unnorm-rgba |
Represents either a 3 or 4 element array of numbers for red, green, blue and (optionally) alpha, that range between 0 and 255. |
Customizing default Node Properties
The @zvalues
annotation indicates to Mattercraft what values should be shown for this property for autocomplete or user selection.
@zvalues files *.+(jpg|jpeg|png)
Autocompletes a list of files in the project that match the RegExp pattern supplied. At runtime, the value of the property will be a URL that can be used to fetch the file referenced by the property.
@zvalues animations
Autocompletes with the names of the animations present in the in-context 3D file of this component or behavior.
@zvalues morphtargets
Autocompletes with the names of the morph targets (also known as blend shapes) present in the in-context 3D file of this component or behavior.
@zvalues events
Autocompletes with the names of any events in the component instance that a behavior is attached to and that have been annotated with @zui
.
@zvalues nodeids
Autocompletes with the unique IDs of nodes in the scene. The ID stored in the property can be resolved to a component or behavior using the ZComponent’s entityByID
Map:
this.zcomponent.entityByID.get(property);
@zvalues nodelabels
Autocompletes with the labels (i.e. the names) of nodes in the scene. The label stored in the property can be resolved to a component using the ZComponent’s nodeByLabel
Map:
this.zcomponent.nodeByLabel.get(property);
@zvalues layerclipids
Autocompletes with the unique IDs of the layer clips (i.e. timelines or states within a layer) in the scene. The ID stored in the property can be resolved to a layer clip like this:
this.zcomponent.animation.layerClipByID.get(property);
@zvalues layerids
Autocompletes with the unique IDs of the layers in the scene. The ID stored in the property can be resolved to a layer like this:
this.zcomponent.animation.layerByID.get(property);