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:

Custom Metalness Property added to Node Properties Panel

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.

Default value for custom Node properties.

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);

Custom properties added in Node Properties Panel

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.

Draggable scale from 0 to 1 in Node Properties Panel

@ztype text-multiline

Shows a multi-line text input field.

Multi-line input box in Node Properties Panel

@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.

Angle type selector in in Node Properties Panel

@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.

Color Picker in Node Properties Panel

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.

Autocomplete list of Image files in project

@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);
zapcode branded_zapcode i