-
Notifications
You must be signed in to change notification settings - Fork 52
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.
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.
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.
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 toCOLOR
will be exported as an hex string.Arrays that don't specify
unit
andsubtype
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 theSKIP_SAVE
option.
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.
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.
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
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.
- Open Blender and create your model, make sure that is a mesh before exporting it.
- Go to the
Scripting
workspace, create a new file and copy&paste theexport_gizmo.py
that you can find under thescripts
folder. - Click on the
Run script
button and save the file as a[my_gizmo].py
file under thecomponents/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.
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.
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.
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.
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.
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.
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.
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:
- Create a
[component-name].blend
file insidetests/scenes
. The scene should have the new component attached to the corresponding node. - 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"
}
});
});