Skip to content

Commit

Permalink
Merge branch 'Hubs-Foundation:master' into PortToBlender4
Browse files Browse the repository at this point in the history
  • Loading branch information
GottfriedHofmann authored Jun 7, 2024
2 parents 6f09438 + ce74e9f commit 3cad758
Show file tree
Hide file tree
Showing 19 changed files with 810 additions and 145 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,14 @@ jobs:
- name: Update build number
run: |
sed -i'' 's/"dev_build"/${{ github.run_number }}/g' $GITHUB_WORKSPACE/addons/io_hubs_addon/__init__.py
- name: Get version
id: get_version
run: |
VERSION=$(grep '"version"' $GITHUB_WORKSPACE/addons/io_hubs_addon/__init__.py | sed -E 's/.*\(([0-9]+), ([0-9]+), ([0-9]+), ([0-9]+)\).*/\1.\2.\3.\4/')
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Upload addon artifacts
uses: actions/upload-artifact@v3
with:
name: io_hubs_addon
name: io_hubs_addon_${{ steps.get_version.outputs.version }}
path: addons
if-no-files-found: error
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ CMakeCache.txt
Makefile
lib

#Selenium
__hubs_selenium_profile
# Selenium
__hubs_selenium_profile

# Dependencies
addons/io_hubs_addon/.__deps__
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hubs Blender Exporter and Importer

This addon extends the glTF 2.0 exporter to support the `MOZ_hubs_components` and `MOZ_lightmap` extensions allowing you to add behavior to glTF assets for [Mozilla Hubs](https://hubs.mozilla.com).
This addon extends the glTF 2.0 exporter to support the `MOZ_hubs_components` and `MOZ_lightmap` extensions allowing you to add behavior to glTF assets for Hubs.

[![Test](https://github.com/MozillaReality/hubs-blender-exporter/actions/workflows/test.yml/badge.svg)](https://github.com/MozillaReality/hubs-blender-exporter/actions/workflows/test.yml)
[![Publish](https://github.com/MozillaReality/hubs-blender-exporter/actions/workflows/publish.yml/badge.svg)](https://github.com/MozillaReality/hubs-blender-exporter/actions/workflows/publish.yml)
Expand Down Expand Up @@ -46,7 +46,7 @@ This addon works in conjunction with the official glTF add-on, so exporting is d

# Import into Hubs

The easiest way to use your scene file is through the Spoke [project creation page](https://hubs.mozilla.com/spoke/projects/create) and selecting _Import From Blender_:
The easiest way to use your scene file is through the Spoke project creation page and selecting _Import From Blender_:

<img width="710" alt="Screenshot 2021-10-31 at 14 05 21" src="https://user-images.githubusercontent.com/303516/139588457-8d9d7835-6101-4cfc-886b-ad3e86c37846.png">

Expand Down
4 changes: 1 addition & 3 deletions addons/io_hubs_addon/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from .utils import create_prefs_dir
from .utils import get_user_python_path
import sys
import bpy
from .io import gltf_exporter, gltf_importer, panels
Expand All @@ -11,7 +10,7 @@
bl_info = {
"name": "Hubs Blender Addon",
"author": "Mozilla Hubs",
"description": "Tools for developing glTF assets for Mozilla Hubs",
"description": "Tools for developing glTF assets for Hubs",
"blender": (3, 1, 2),
"version": (1, 5, 0, "dev_build"),
"location": "",
Expand All @@ -22,7 +21,6 @@
"category": "Generic"
}

sys.path.insert(0, get_user_python_path())

create_prefs_dir()

Expand Down
70 changes: 70 additions & 0 deletions addons/io_hubs_addon/components/components_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,43 @@ def get_components_in_dir(dir):
return sorted(components)


def get_user_component_names():
component_names = []
from ..preferences import get_addon_pref
addon_prefs = get_addon_pref(bpy.context)
for entry in addon_prefs.user_components_paths:
if entry.path and os.path.isdir(entry.path):
component_names.append(get_components_in_dir(entry.path))
return component_names


def get_user_component_paths():
component_paths = []
from ..preferences import get_addon_pref
addon_prefs = get_addon_pref(bpy.context)
for entry in addon_prefs.user_components_paths:
if entry.path and os.path.isdir(entry.path):
components = get_components_in_dir(entry.path)
for component in components:
component_paths.append(os.path.join(entry.path, component + ".py"))
return component_paths


def get_user_component_definitions():
modules = []
component_paths = get_user_component_paths()
for component_path in component_paths:
try:
spec = importlib.util.spec_from_file_location(component_path, component_path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
modules.append(mod)

except Exception as e:
print(f'Failed import of component {component_path}', e)
return modules


def get_component_definitions():
components_dir = join(dirname(realpath(__file__)), "definitions")
component_module_names = get_components_in_dir(components_dir)
Expand Down Expand Up @@ -113,6 +150,32 @@ def unregister_component(component_class):
print(f"Component unregistered: {component_class.get_name()}")


def load_user_components():
global __components_registry
for module in get_user_component_definitions():
for _, member in inspect.getmembers(module):
if inspect.isclass(member) and issubclass(member, HubsComponent) and module.__name__ == member.__module__:
try:
if hasattr(module, 'register_module'):
module.register_module()
register_component(member)
__components_registry[member.get_name()] = member
except Exception:
import traceback
traceback.print_exc()


def unload_user_components():
global __components_registry
for _, component_class in __components_registry.items():
for module_name in get_user_component_names():
if module_name == component_class.get_name():
unregister_component(component_class)
for module in get_user_component_definitions():
if hasattr(module, 'unregister_module'):
module.unregister_module()


def load_components_registry():
"""Recurse in the components directory to build the components registry"""
global __components_registry
Expand All @@ -125,6 +188,13 @@ def load_components_registry():
register_component(member)
__components_registry[member.get_name()] = member

# When running Blender in factory startup mode and specifying an addon, that addon's register function is called.
# As preferences are not available until the addon is enabled, the user component load fails when accessing them.
# This happens when running tests and this guard avoids crashing in that scenario.
from ..utils import is_addon_enabled
if is_addon_enabled():
load_user_components()


def unload_components_registry():
"""Recurse in the components directory to unload the registered components"""
Expand Down
2 changes: 1 addition & 1 deletion addons/io_hubs_addon/components/definitions/ammo_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
class AmmoShape(HubsComponent):
_definition = {
'name': 'ammo-shape',
'display_name': 'Ammo Shape',
'display_name': 'Ammo Shape (deprecated)',
'category': Category.OBJECT,
'node_type': NodeType.NODE,
'panel_type': [PanelType.OBJECT, PanelType.BONE],
Expand Down
44 changes: 44 additions & 0 deletions addons/io_hubs_addon/components/definitions/grabbable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from bpy.props import BoolProperty
from ..hubs_component import HubsComponent
from ..types import NodeType, PanelType, Category
from ..utils import remove_component, add_component
from .networked_transform import NetworkedTransform


class Grabbable(HubsComponent):
_definition = {
'name': 'grabbable',
'display_name': 'Grabbable',
'category': Category.OBJECT,
'node_type': NodeType.NODE,
'panel_type': [PanelType.OBJECT],
'icon': 'VIEW_PAN',
'deps': ['rigidbody', 'networked-transform'],
'version': (1, 0, 1)
}

cursor: BoolProperty(
name="By Cursor", description="Can be grabbed by a cursor", default=True)

hand: BoolProperty(
name="By Hand", description="Can be grabbed by VR hands", default=True)

@classmethod
def init(cls, obj):
obj.hubs_component_list.items.get('rigidbody').isDependency = True

def migrate(self, migration_type, panel_type, instance_version, host, migration_report, ob=None):
migration_occurred = False
if instance_version <= (1, 0, 0):
migration_occurred = True

# This was a component that has disappeared but it was usually added together with grababble so we try to remove those instances.
if "capturable" in host.hubs_component_list.items:
remove_component(host, "capturable")

if "networked-object-properties" in host.hubs_component_list.items:
remove_component(host, "networked-object-properties")

add_component(host, NetworkedTransform.get_name())

return migration_occurred
14 changes: 14 additions & 0 deletions addons/io_hubs_addon/components/definitions/networked_transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from io_hubs_addon.components.hubs_component import HubsComponent
from io_hubs_addon.components.types import NodeType, PanelType


class NetworkedTransform(HubsComponent):
_definition = {
'name': 'networked-transform',
'display_name': 'Networked Transform',
'node_type': NodeType.NODE,
'panel_type': [PanelType.OBJECT],
'icon': 'EMPTY_AXIS',
'deps': ['networked'],
'version': (1, 0, 0)
}
134 changes: 134 additions & 0 deletions addons/io_hubs_addon/components/definitions/physics_shape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from bpy.props import BoolProperty, FloatProperty, EnumProperty, FloatVectorProperty
from ..hubs_component import HubsComponent
from ..types import NodeType, PanelType, Category
from mathutils import Vector
from ...io.utils import import_component, assign_property


class PhysicsShape(HubsComponent):
_definition = {
'name': 'physics-shape',
'display_name': 'Physics Shape',
'category': Category.OBJECT,
'node_type': NodeType.NODE,
'panel_type': [PanelType.OBJECT, PanelType.BONE],
'icon': 'SCENE_DATA',
'version': (1, 0, 1)
}

type: EnumProperty(
name="Type", description="Type",
items=[("box", "Box Collider", "A box-shaped primitive collision shape"),
("sphere", "Sphere Collider", "A primitive collision shape which represents a sphere"),
("hull", "Convex Hull",
"A convex hull wrapped around the object's vertices. A good analogy for a convex hull is an elastic membrane or balloon under pressure which is placed around a given set of vertices. When released the membrane will assume the shape of the convex hull"),
("mesh", "Mesh Collider",
"A shape made of the actual vertices of the object. This can be expensive for large meshes")],
default="hull")

fit: EnumProperty(
name="Fit Mode",
description="Shape fitting mode",
items=[("all", "Automatic fit all", "Automatically match the shape to fit the object's vertices"),
("manual", "Manual", "Use the manually specified dimensions to define the shape, ignoring the object's vertices")],
default="all")

halfExtents: FloatVectorProperty(
name="Half Extents",
description="Half dimensions of the collider. (Only used when fit is set to \"manual\" and type is set to \"box\")",
unit='LENGTH',
subtype="XYZ",
default=(0.5, 0.5, 0.5))

minHalfExtent: FloatProperty(
name="Min Half Extent",
description="The minimum size to use when automatically generating half extents. (Only used when fit is set to \"all\" and type is set to \"box\")",
unit="LENGTH",
default=0.0)

maxHalfExtent: FloatProperty(
name="Max Half Extent",
description="The maximum size to use when automatically generating half extents. (Only used when fit is set to \"all\" and type is set to \"box\")",
unit="LENGTH",
default=1000.0)

sphereRadius: FloatProperty(
name="Sphere Radius",
description="Radius of the sphere collider. (Only used when fit is set to \"manual\" and type is set to \"sphere\")",
unit="LENGTH", default=0.5)

offset: FloatVectorProperty(
name="Offset", description="An offset to apply to the collider relative to the object's origin",
unit='LENGTH',
subtype="XYZ",
default=(0.0, 0.0, 0.0))

includeInvisible: BoolProperty(
name="Include Invisible",
description="Include invisible objects when generating a collider. (Only used if \"fit\" is set to \"all\")",
default=False)

def draw(self, context, layout, panel):
layout.prop(self, "type")
layout.prop(self, "fit")
if self.fit == "manual":
if self.type == "box":
layout.prop(self, "halfExtents")
elif self.type == "sphere":
layout.prop(self, "sphereRadius")
else:
if self.type == "box":
layout.prop(self, "minHalfExtent")
layout.prop(self, "maxHalfExtent")
layout.prop(self, "includeInvisible")
layout.prop(self, "offset")

if self.fit == "manual" and (self.type == "mesh" or self.type == "hull"):
col = layout.column()
col.alert = True
col.label(
text="'Hull' and 'Mesh' do not support 'manual' fit mode", icon='ERROR')

def gather(self, export_settings, object):
props = super().gather(export_settings, object)
props['offset'] = {
'x': self.offset.x,
'y': self.offset.z if export_settings['gltf_yup'] else self.offset.y,
'z': self.offset.y if export_settings['gltf_yup'] else self.offset.z,
}
props['halfExtents'] = {
'x': self.halfExtents.x,
'y': self.halfExtents.z if export_settings['gltf_yup'] else self.halfExtents.y,
'z': self.halfExtents.y if export_settings['gltf_yup'] else self.halfExtents.z,
}
return props

def migrate(self, migration_type, panel_type, instance_version, host, migration_report, ob=None):
migration_occurred = False
if instance_version <= (1, 0, 0):
migration_occurred = True

offset = self.offset.copy()
offset = Vector((offset.x, offset.z, offset.y))
self.offset = offset

halfExtents = self.halfExtents.copy()
halfExtents = Vector((halfExtents.x, halfExtents.z, halfExtents.y))
self.halfExtents = halfExtents

return migration_occurred

@classmethod
def gather_import(cls, gltf, blender_host, component_name, component_value, import_report, blender_ob=None):
gltf_yup = gltf.import_settings.get('gltf_yup', True)

blender_component = import_component(component_name, blender_host)
for property_name, property_value in component_value.items():
if property_name == 'offset' and gltf_yup:
property_value['y'], property_value['z'] = property_value['z'], property_value['y']

elif property_name == 'halfExtents' and gltf_yup:
property_value['y'], property_value['z'] = property_value['z'], property_value['y']

assign_property(gltf.vnodes, blender_component,
property_name, property_value)
Loading

0 comments on commit 3cad758

Please sign in to comment.