Loaders and creators now use Blender main thread to operate

This commit is contained in:
Simone Barbieri 2021-06-21 11:40:58 +01:00
parent c8c70cc0a7
commit 6ee64d09ab
7 changed files with 87 additions and 39 deletions

View file

@ -6,10 +6,11 @@ from typing import Dict, List, Optional
import bpy import bpy
from avalon import api, blender from avalon import api, blender
from avalon.blender import ops
from avalon.blender.pipeline import AVALON_CONTAINERS from avalon.blender.pipeline import AVALON_CONTAINERS
from openpype.api import PypeCreatorMixin from openpype.api import PypeCreatorMixin
VALID_EXTENSIONS = [".blend", ".json", ".abc"] VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"]
def asset_name( def asset_name(
@ -161,6 +162,15 @@ class AssetLoader(api.Loader):
raise NotImplementedError("Must be implemented by a sub-class") raise NotImplementedError("Must be implemented by a sub-class")
def load(self, def load(self,
context: dict,
name: Optional[str] = None,
namespace: Optional[str] = None,
options: Optional[Dict] = None) -> Optional[bpy.types.Collection]:
""" Run the loader on Blender main thread"""
mti = ops.MainThreadItem(self._load, context, name, namespace, options)
ops.execute_in_main_thread(mti)
def _load(self,
context: dict, context: dict,
name: Optional[str] = None, name: Optional[str] = None,
namespace: Optional[str] = None, namespace: Optional[str] = None,
@ -216,10 +226,20 @@ class AssetLoader(api.Loader):
return self._get_instance_collection(instance_name, nodes) return self._get_instance_collection(instance_name, nodes)
def exec_update(self, container: Dict, representation: Dict):
"""Must be implemented by a sub-class"""
raise NotImplementedError("Must be implemented by a sub-class")
def update(self, container: Dict, representation: Dict): def update(self, container: Dict, representation: Dict):
""" Run the update on Blender main thread"""
mti = ops.MainThreadItem(self.exec_update, container, representation)
ops.execute_in_main_thread(mti)
def exec_remove(self, container: Dict) -> bool:
"""Must be implemented by a sub-class""" """Must be implemented by a sub-class"""
raise NotImplementedError("Must be implemented by a sub-class") raise NotImplementedError("Must be implemented by a sub-class")
def remove(self, container: Dict) -> bool: def remove(self, container: Dict) -> bool:
"""Must be implemented by a sub-class""" """ Run the remove on Blender main thread"""
raise NotImplementedError("Must be implemented by a sub-class") mti = ops.MainThreadItem(self.exec_remove, container)
ops.execute_in_main_thread(mti)

View file

@ -3,7 +3,7 @@
import bpy import bpy
from avalon import api from avalon import api
from avalon.blender import lib from avalon.blender import lib, ops
from avalon.blender.pipeline import AVALON_INSTANCES from avalon.blender.pipeline import AVALON_INSTANCES
from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api import plugin
@ -17,6 +17,11 @@ class CreateModel(plugin.Creator):
icon = "cube" icon = "cube"
def process(self): def process(self):
""" Run the creator on Blender main thread"""
mti = ops.MainThreadItem(self._process)
ops.execute_in_main_thread(mti)
def _process(self):
# Get Instance Containter or create it if it does not exist # Get Instance Containter or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES) instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances: if not instances:
@ -40,8 +45,6 @@ class CreateModel(plugin.Creator):
for obj in selected: for obj in selected:
obj.select_set(True) obj.select_set(True)
selected.append(asset_group) selected.append(asset_group)
context = plugin.create_blender_context( bpy.ops.object.parent_set(keep_transform=True)
active=asset_group, selected=selected)
bpy.ops.object.parent_set(context, keep_transform=True)
return asset_group return asset_group

View file

@ -3,7 +3,7 @@
import bpy import bpy
from avalon import api from avalon import api
from avalon.blender import lib from avalon.blender import lib, ops
from avalon.blender.pipeline import AVALON_INSTANCES from avalon.blender.pipeline import AVALON_INSTANCES
from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api import plugin
@ -17,6 +17,11 @@ class CreateRig(plugin.Creator):
icon = "wheelchair" icon = "wheelchair"
def process(self): def process(self):
""" Run the creator on Blender main thread"""
mti = ops.MainThreadItem(self._process)
ops.execute_in_main_thread(mti)
def _process(self):
# Get Instance Containter or create it if it does not exist # Get Instance Containter or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES) instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances: if not instances:
@ -40,8 +45,6 @@ class CreateRig(plugin.Creator):
for obj in selected: for obj in selected:
obj.select_set(True) obj.select_set(True)
selected.append(asset_group) selected.append(asset_group)
context = plugin.create_blender_context( bpy.ops.object.parent_set(keep_transform=True)
active=asset_group, selected=selected)
bpy.ops.object.parent_set(context, keep_transform=True)
return asset_group return asset_group

View file

@ -52,9 +52,7 @@ class CacheModelLoader(plugin.AssetLoader):
collection = bpy.context.view_layer.active_layer_collection.collection collection = bpy.context.view_layer.active_layer_collection.collection
relative = bpy.context.preferences.filepaths.use_relative_paths relative = bpy.context.preferences.filepaths.use_relative_paths
context = plugin.create_blender_context()
bpy.ops.wm.alembic_import( bpy.ops.wm.alembic_import(
context,
filepath=libpath, filepath=libpath,
relative_path=relative relative_path=relative
) )
@ -164,7 +162,7 @@ class CacheModelLoader(plugin.AssetLoader):
self[:] = objects self[:] = objects
return objects return objects
def update(self, container: Dict, representation: Dict): def exec_update(self, container: Dict, representation: Dict):
"""Update the loaded asset. """Update the loaded asset.
This will remove all objects of the current collection, load the new This will remove all objects of the current collection, load the new
@ -227,7 +225,7 @@ class CacheModelLoader(plugin.AssetLoader):
metadata["libpath"] = str(libpath) metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"]) metadata["representation"] = str(representation["_id"])
def remove(self, container: Dict) -> bool: def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene. """Remove an existing container from a Blender scene.
Arguments: Arguments:

View file

@ -7,7 +7,7 @@ from typing import Dict, List, Optional
import bpy import bpy
from avalon import api from avalon import api
from avalon.blender import lib from avalon.blender import lib, ops
from avalon.blender.pipeline import AVALON_CONTAINERS from avalon.blender.pipeline import AVALON_CONTAINERS
from avalon.blender.pipeline import AVALON_CONTAINER_ID from avalon.blender.pipeline import AVALON_CONTAINER_ID
from avalon.blender.pipeline import AVALON_PROPERTY from avalon.blender.pipeline import AVALON_PROPERTY
@ -20,7 +20,7 @@ class FbxModelLoader(plugin.AssetLoader):
Stores the imported asset in an empty named after the asset. Stores the imported asset in an empty named after the asset.
""" """
families = ["model"] families = ["model", "rig"]
representations = ["fbx"] representations = ["fbx"]
label = "Load FBX" label = "Load FBX"
@ -29,7 +29,6 @@ class FbxModelLoader(plugin.AssetLoader):
def _remove(self, asset_group): def _remove(self, asset_group):
objects = list(asset_group.children) objects = list(asset_group.children)
empties = []
for obj in objects: for obj in objects:
if obj.type == 'MESH': if obj.type == 'MESH':
@ -37,23 +36,21 @@ class FbxModelLoader(plugin.AssetLoader):
if material_slot.material: if material_slot.material:
bpy.data.materials.remove(material_slot.material) bpy.data.materials.remove(material_slot.material)
bpy.data.meshes.remove(obj.data) bpy.data.meshes.remove(obj.data)
elif obj.type == 'ARMATURE':
objects.extend(obj.children)
bpy.data.armatures.remove(obj.data)
elif obj.type == 'CURVE':
bpy.data.curves.remove(obj.data)
elif obj.type == 'EMPTY': elif obj.type == 'EMPTY':
objects.extend(obj.children) objects.extend(obj.children)
empties.append(obj) bpy.data.objects.remove(obj)
for empty in empties: def _process(self, libpath, asset_group, group_name, action):
bpy.data.objects.remove(empty)
def _process(self, libpath, asset_group, group_name):
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action='DESELECT')
collection = bpy.context.view_layer.active_layer_collection.collection collection = bpy.context.view_layer.active_layer_collection.collection
context = plugin.create_blender_context() bpy.ops.import_scene.fbx(filepath=libpath)
bpy.ops.import_scene.fbx(
context,
filepath=libpath
)
parent = bpy.context.scene.collection parent = bpy.context.scene.collection
@ -97,9 +94,17 @@ class FbxModelLoader(plugin.AssetLoader):
name_data = obj.data.name name_data = obj.data.name
obj.data.name = f"{group_name}:{name_data}" obj.data.name = f"{group_name}:{name_data}"
if obj.type == 'MESH':
for material_slot in obj.material_slots: for material_slot in obj.material_slots:
name_mat = material_slot.material.name name_mat = material_slot.material.name
material_slot.material.name = f"{group_name}:{name_mat}" material_slot.material.name = f"{group_name}:{name_mat}"
elif obj.type == 'ARMATURE':
anim_data = obj.animation_data
if action is not None:
anim_data.action = action
elif anim_data.action is not None:
name_action = anim_data.action.name
anim_data.action.name = f"{group_name}:{name_action}"
if not obj.get(AVALON_PROPERTY): if not obj.get(AVALON_PROPERTY):
obj[AVALON_PROPERTY] = dict() obj[AVALON_PROPERTY] = dict()
@ -122,7 +127,6 @@ class FbxModelLoader(plugin.AssetLoader):
context: Full parenthood of representation to load context: Full parenthood of representation to load
options: Additional settings dictionary options: Additional settings dictionary
""" """
libpath = self.fname libpath = self.fname
asset = context["asset"]["name"] asset = context["asset"]["name"]
subset = context["subset"]["name"] subset = context["subset"]["name"]
@ -140,7 +144,14 @@ class FbxModelLoader(plugin.AssetLoader):
asset_group = bpy.data.objects.new(group_name, object_data=None) asset_group = bpy.data.objects.new(group_name, object_data=None)
avalon_container.objects.link(asset_group) avalon_container.objects.link(asset_group)
objects = self._process(libpath, asset_group, group_name) objects = self._process(libpath, asset_group, group_name, None)
objects = []
nodes = list(asset_group.children)
for obj in nodes:
objects.append(obj)
nodes.extend(list(obj.children))
bpy.context.scene.collection.objects.link(asset_group) bpy.context.scene.collection.objects.link(asset_group)
@ -160,7 +171,7 @@ class FbxModelLoader(plugin.AssetLoader):
self[:] = objects self[:] = objects
return objects return objects
def update(self, container: Dict, representation: Dict): def exec_update(self, container: Dict, representation: Dict):
"""Update the loaded asset. """Update the loaded asset.
This will remove all objects of the current collection, load the new This will remove all objects of the current collection, load the new
@ -214,16 +225,28 @@ class FbxModelLoader(plugin.AssetLoader):
self.log.info("Library already loaded, not updating...") self.log.info("Library already loaded, not updating...")
return return
# Get the armature of the rig
objects = asset_group.children
armatures = [obj for obj in objects if obj.type == 'ARMATURE']
action = None
if armatures:
armature = armatures[0]
if armature.animation_data and armature.animation_data.action:
action = armature.animation_data.action
mat = asset_group.matrix_basis.copy() mat = asset_group.matrix_basis.copy()
self._remove(asset_group) self._remove(asset_group)
self._process(str(libpath), asset_group, object_name) self._process(str(libpath), asset_group, object_name, action)
asset_group.matrix_basis = mat asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath) metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"]) metadata["representation"] = str(representation["_id"])
def remove(self, container: Dict) -> bool: def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene. """Remove an existing container from a Blender scene.
Arguments: Arguments:

View file

@ -145,7 +145,7 @@ class BlendModelLoader(plugin.AssetLoader):
self[:] = objects self[:] = objects
return objects return objects
def update(self, container: Dict, representation: Dict): def exec_update(self, container: Dict, representation: Dict):
"""Update the loaded asset. """Update the loaded asset.
This will remove all objects of the current collection, load the new This will remove all objects of the current collection, load the new
@ -218,7 +218,7 @@ class BlendModelLoader(plugin.AssetLoader):
metadata["libpath"] = str(libpath) metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"]) metadata["representation"] = str(representation["_id"])
def remove(self, container: Dict) -> bool: def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene. """Remove an existing container from a Blender scene.
Arguments: Arguments:

View file

@ -29,7 +29,8 @@ class BlendRigLoader(plugin.AssetLoader):
for obj in objects: for obj in objects:
if obj.type == 'MESH': if obj.type == 'MESH':
for material_slot in list(obj.material_slots): for material_slot in list(obj.material_slots):
bpy.data.materials.remove(material_slot.material) if material_slot.material:
bpy.data.materials.remove(material_slot.material)
bpy.data.meshes.remove(obj.data) bpy.data.meshes.remove(obj.data)
elif obj.type == 'ARMATURE': elif obj.type == 'ARMATURE':
objects.extend(obj.children) objects.extend(obj.children)
@ -178,7 +179,7 @@ class BlendRigLoader(plugin.AssetLoader):
self[:] = objects self[:] = objects
return objects return objects
def update(self, container: Dict, representation: Dict): def exec_update(self, container: Dict, representation: Dict):
"""Update the loaded asset. """Update the loaded asset.
This will remove all children of the asset group, load the new ones This will remove all children of the asset group, load the new ones
@ -232,7 +233,7 @@ class BlendRigLoader(plugin.AssetLoader):
if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath: if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath:
count += 1 count += 1
# # Get the armature of the rig # Get the armature of the rig
objects = asset_group.children objects = asset_group.children
armature = [obj for obj in objects if obj.type == 'ARMATURE'][0] armature = [obj for obj in objects if obj.type == 'ARMATURE'][0]
@ -256,7 +257,7 @@ class BlendRigLoader(plugin.AssetLoader):
metadata["libpath"] = str(libpath) metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"]) metadata["representation"] = str(representation["_id"])
def remove(self, container: Dict) -> bool: def exec_remove(self, container: Dict) -> bool:
"""Remove an existing asset group from a Blender scene. """Remove an existing asset group from a Blender scene.
Arguments: Arguments: