Skip to content

Creating custom components

Manuel Martin edited this page Jun 8, 2022 · 8 revisions

Creating custom components

In this article we will go over the structure of a Hubs blender add-on component and we will create a simple component as an example.

Creating a new component

We will go over the most important parts but in case you are curious the full Hubs component structure is documented in the HubsComponent definition in hubs_component.py file.

The first step to create a Hubs component for the Blender add-on is to create a new python source file inside the add-on components/definitions folder. The file name doesn't really matter as long as it conforms the python naming conventions. In case your component requires multiple files you can also create a subfolder and add all the component related code under that folder.

Once you have created the python source file you need to create a class that extends from the base HubsComponent class. The HubsComponent class has a base implementation of all the required methods. If your component only exposes some properties you'll be probably good with just inheriting from the base class and add a component definition. The components registry will traverse the definitions folder and load any classes that inherit from HubsComponent. We will go later over the methods that can be extended to extend the component functionality.

We are going to create an example component to illustrate this process. In our case this is going to be the mirror component so the first step is to create a mirror.py file under the definitions folder.

Adding a component definition

The component needs to define some information:

  • name: The component name. It will be used internally in the components list that every object has when using the Hubs add-on and it's also the name that will be used when the scene is exported as a GLTF file. If the name is not set, the component will fallback to a name in the form: hubs_component_[Classname]
  • display_name: The name that will be displayed in any component related UI. If no display name is specified, the class name will be used.
  • category: The category the component will fall into in the components list menu. If no category is displayed the component won't show in the components list menu. If your component is a hidden component or it's a dependecy of another component that doesn't have a use by itself simply dont' add a category.
  • node_type: The node type where the component will be registered.
  • panel_type: An array of panel types where the component will be shown.
  • deps: An array with all the dependencies that this component requires. All dependencies will be added when this component is added and removed when the component is removed unless some other component requires them.
  • icon: The icon to be displayed in the component list for this component. You can use an image (png, jpeg) or a Blender icon id string. In case you use an icon you will need to add the icon file to the icons folder and set the icon name as the file name.

In the case of our mirror component, this would look like this:

from ..hubs_component import HubsComponent
from ..types import Category, PanelType, NodeType


class Mirror(HubsComponent):
    _definition = {
        'name': 'mirror',
        'display_name': 'Mirror',
        'category': Category.SCENE,
        'node_type': NodeType.NODE,
        'panel_type': [PanelType.OBJECT],
        'icon': 'MIRROR'
    }

If you run Blender now, you should see the component listed in the components list unde the Scene category. At this point that's all, there are no properties or any further functionality but at least the component will be added to the exported GLTF file. You can check that by exporting the scene as a GLTF (GLTF embedded .gltf) and opening it in a text editor. You should see this:

"extensions" : {
    "MOZ_hubs_components" : {
        "mirror" : {
        }
    }
},

Now we need to add some properties to the component.

Adding component properties

The component properties are just regular Blender API properties, you have a list of all of them here. You can add any type of property that Blender supports.

For our mirror component we will need to expose the the tint property. Blender has different property types and some of them have units and subtypes. For showing a Blender color property we need to set it as a FloatVectorProperty property and set the subtype as COLOR.

Our component would look like this:

from bpy.props import FloatVectorProperty
from ..hubs_component import HubsComponent
from ..types import Category, PanelType, NodeType


class Mirror(HubsComponent):
    _definition = {
        'name': 'mirror',
        'display_name': 'Mirror',
        'category': Category.SCENE,
        'node_type': NodeType.NODE,
        'panel_type': [PanelType.OBJECT]
    }

    color: FloatVectorProperty(name="Color",
                               description="Color",
                               subtype='COLOR',
                               default=(1.0, 1.0, 1.0, 1.0),
                               size=4,
                               min=0,
                               max=1)

If you reload the component wou will see that now the componnet shows a color property that show the color picked when clicked.

If you export you should see the serialized color property in the json file:

"extensions" : {
    "MOZ_hubs_components" : {
        "mirror" : {
            "color" : "#ffffff"
        }
    }
},

Note about properties and export

The GLTF exporter handles the Blender properties serialization but the Hubs add-on applies some transforms to certain types to conform to the expected Hubs client input, specifically in the case of arrays. These are the transforms:

  • Arrays with subtype equal to COLOR will be exported as an hex string.

  • Arrays that don't specify unit and subtype will be exported as array.

  • Any other atray will be exported as and { x, y, z, w } object depending on the array length.

Note about properties visibility

The Hubs addon won't display in the component panel any property that includes a HIDDEN option so if you have an internal property that you don't want exposed, simply add that option to the property's options. If you don't need that property to be persisted in the .blend file when saving, you can also add the SKIP_SAVE option.

Adding logic to the component

So far the component is pretty static, there is not logic at all, we are just showing pre defined properties. One simple piece of logic that we can add is showing a warning in case the mirror color is black.

The base add-on implements a draw method. This method is in charge of drawing the content of the component panel once the component is installed. By default this method just looks for the defined component properties and adds them to the panel in a column.

To show the warning we want to override it in our class and add a custom label that will only be displayed in case the color is black.

def draw(self, context, layout):
    super().draw(context, layout)

    cmp = getattr(context.object, self.get_id())
    if cmp.color[:3] == (0.0, 0.0, 0.0):
        layout.label(
            text="You won't see much if the mirror is black", icon='ERROR')

In the the first line we call the base class method as we still want to draw all the component properties (in this case color). Then we get the component property from the current selected object. The base HubsComponent has a method to get the component id. The component id is what it's used by the Hubs addon intenally as component property name that is attached to the nodes. With the line getattr(context.object, self.get_id()) we are getting the component instance that is attached to the current selected object.

Then we just compare if the RGB values of the color property are equal to black and if that's the case we show the warning.

Adding a gizmo to a component

By default the components have no gizmos. For adding a gizmo to a component you will need to override the following methods from the base HubsComponent class:

@classmethod
def create_gizmo(cls, ob, gizmo_group):

@classmethod
def update_gizmo(cls, ob, gizmo):

There are two alternatives for adding gizmos to a component. A builting Blender gizmo or a custom model.

Builtin gizmos

Blender has a set of builtin gizmos that you can use right away. There is not much information about the builtin gizmos in the docs right now but you can find a list here

Let's add a simple plane gizmo to our mirror component. First we need to override the create_gizmo method and return our gizmo:

@classmethod
def create_gizmo(cls, ob, gizmo_group):
    gizmo = gizmo_group.gizmos.new('GIZMO_GT_primitive_3d')
    gizmo.draw_style = ('PLANE')
    gizmo.use_draw_scale = False
    gizmo.line_width = 3
    gizmo.color = (0.8, 0.8, 0.8)
    gizmo.alpha = 0.5
    gizmo.hide = not ob.visible_get()
    gizmo.hide_select = True
    gizmo.scale_basis = 1.0
    gizmo.use_draw_modal = True
    gizmo.color_highlight = (0.8, 0.8, 0.8)
    gizmo.alpha_highlight = 1.0

    return gizmo

Custom gizmos

In case you need a customized gizmo, you can use your own gizmo model.

Note: Keep in mind that the gizmo models are meant to be light so keep the mesh complexity as low as possible.

  1. Open Blender and create your model, make sure that is a mesh before exporting it.
  2. Go to the Scripting workspace, create a new file and copy&paste the export_gizmo.py that you can find under the scripts folder.
  3. Click on the Run script button and save the file as a [my_gizmo].py file under the components/models folder.

The create_gizmo method for a custom model is quite similar to the one for builtin gizmos but specifying the custom gizmo class. The Hubs add-on includes a utility CustomModelGizmo class that implements the basic logic for loading a custom gizmo. You can use that one, extend it or write your own gizmo class. In this case we are going to use the CustomModelGizmo class.

@classmethod
def create_gizmo(cls, ob, gizmo_group):
    gizmo = gizmo_group.gizmos.new(CustomModelGizmo.bl_idname)
    setattr(gizmo, "hubs_gizmo_shape", mirror.SHAPE)
    gizmo.setup()
    gizmo.use_draw_scale = False
    gizmo.use_draw_modal = True
    gizmo.color = (0.8, 0.8, 0.8)
    gizmo.alpha = 1.0
    gizmo.hide = not ob.visible_get()
    gizmo.scale_basis = 1.0
    gizmo.hide_select = False
    gizmo.color_highlight = (0.8, 0.8, 0.8)
    gizmo.alpha_highlight = 0.5

    return gizmo

You will need to update the hubs_gizmo_shape attribute and set it to your exported shape.

Gizmo update

If you open Blender and attach the mirror component to an object and you move the object around, you will see that the gizmo is updated and it follows the object transformation. That's because the default behaviour for the update_gizmo method is to copy the object transforms.

You will also probaby notice that the gizmo plane is facing +Z but in Hubs the objects use the -Y axis as the forward facing axis. We can make the gizmo to face the right direction overriding the update_gizmo method.

@classmethod
def update_gizmo(cls, ob, gizmo):
    loc, rot, scale = ob.matrix_world.decompose()
    offset = Quaternion((1.0, 0.0, 0.0), radians(90.0))
    new_rot = rot @ offset
    mat_out = Matrix.Translation(
        loc) @ new_rot.normalized().to_matrix().to_4x4() @ Matrix.Diagonal(scale).to_4x4()
    gizmo.matrix_basis = mat_out
    gizmo.hide = not ob.visible_get()

In this case we just copy all the object transforms and rotate the gizmo 90 degrees on the X axis to it faces +Y. Now the gizmo should look fine.

Now you can export the new component and as long as you have also added support for this component in the Hubs client, you will see it in action. If you have folowed this tutorial you should see a mirror object in the Hubs client as that component is already implemented.

What else can I do?

The HubsComponent has some more methods that can be overriden to customize your component behaviour. You can take a look at already existing components to see examples of what can be done. Let's take a look at some other overridable methods.

Export hooks

def pre_export(self, export_settings, object):
def post_export(self, export_settings, object):

The pre_export and post_export methods are called before and after the exporter serializes the scene to a GLTF file. A use case for this would be for example applying the object transforms before exporting and restoring them afterwards.

Migration

def migrate(cls):

The migrate method is called right after a new .blend file is loaded so a component can migrate date from previous addon versions.

Class registration

def register():
def unregister():

The register/unregister methods are called by the Blender runtime and give the component a chance to register classes that it's going to use.

Component polling

def poll(cls, context):

The HubsComponent exposes a poll method that is called for every component when the component list is displayed and when the component panel is drawn. In case of returning True the component will be drawn otherwise it won't. This is the equivalen to the Blender API poll mehod that you can implement in operators to determine if the execution context is valid.

You can use this if for example a component only makes sense for a specific object type like a Camera. The poll method would look like this in that case:

@classmethod
def poll(cls, context):
    return context.object.type == 'CAMERA'

This will only show the component panel in case the object is a Camera.

Adding tests for your component

In case you are planning to contribute your component to the official Hubs Blender add-on repository you will need to provide a unit test. To add a new test you have to:

  1. Create a [component-name].blend file inside tests/scenes. The scene should have the new component attached to the corresponding node.
  2. Add a new test to the test_export.js file.

The test just compares the component GLTF export output with the expected output. In the case of our mirror component, and based on a mirror.blend input file, it would look like this:

it('can export mirror', function () {
  let gltfPath = path.resolve(outDirPath, 'mirror.gltf');
  const asset = JSON.parse(fs.readFileSync(gltfPath));

  assert.strictEqual(asset.extensionsUsed.includes('MOZ_hubs_components'), true);
  assert.strictEqual(utils.checkExtensionAdded(asset, 'MOZ_hubs_components'), true);

  const node = asset.nodes[0];
  assert.strictEqual(utils.checkExtensionAdded(node, 'MOZ_hubs_components'), true);

  const ext = node.extensions['MOZ_hubs_components'];
  assert.deepStrictEqual(ext, {
    "mirror": {
      "color": "#ffffff"
    }
  });
});
Clone this wiki locally