From bcd81fa934bcf13ca5861f212cb6871fa439e78a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 Jun 2020 12:52:35 +0100 Subject: [PATCH] Implemented 'camera' asset for Blender --- pype/plugins/blender/create/create_camera.py | 32 +++ pype/plugins/blender/load/load_camera.py | 241 ++++++++++++++++++ pype/plugins/blender/publish/extract_blend.py | 2 +- 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 pype/plugins/blender/create/create_camera.py create mode 100644 pype/plugins/blender/load/load_camera.py diff --git a/pype/plugins/blender/create/create_camera.py b/pype/plugins/blender/create/create_camera.py new file mode 100644 index 0000000000..5817985053 --- /dev/null +++ b/pype/plugins/blender/create/create_camera.py @@ -0,0 +1,32 @@ +"""Create a camera asset.""" + +import bpy + +from avalon import api +from avalon.blender import Creator, lib +import pype.hosts.blender.plugin + + +class CreateCamera(Creator): + """Polygonal static geometry""" + + name = "cameraMain" + label = "Camera" + family = "camera" + icon = "video-camera" + + def process(self): + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.hosts.blender.plugin.asset_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + collection.objects.link(obj) + + return collection diff --git a/pype/plugins/blender/load/load_camera.py b/pype/plugins/blender/load/load_camera.py new file mode 100644 index 0000000000..a69c01e806 --- /dev/null +++ b/pype/plugins/blender/load/load_camera.py @@ -0,0 +1,241 @@ +"""Load a camera asset in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +from avalon import api, blender +import bpy +import pype.hosts.blender.plugin + +logger = logging.getLogger("pype").getChild("blender").getChild("load_camera") + + +class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader): + """Load a camera from a .blend file. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["camera"] + representations = ["blend"] + + label = "Link Camera" + icon = "code-fork" + color = "orange" + + def _remove(self, objects, lib_container): + + for obj in objects: + bpy.data.cameras.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + def _process(self, libpath, lib_container, container_name, actions): + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + camera_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + for obj in camera_container.objects: + obj = obj.make_local() + obj.data.make_local() + + if not obj.get(blender.pipeline.AVALON_PROPERTY): + obj[blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + + + if actions[0] is not None: + if obj.animation_data is None: + obj.animation_data_create() + obj.animation_data.action = actions[0] + + if actions[1] is not None: + if obj.data.animation_data is None: + obj.data.animation_data_create() + obj.data.animation_data.action = actions[1] + + objects_list.append(obj) + + camera_container.pop(blender.pipeline.AVALON_PROPERTY) + + bpy.ops.object.select_all(action='DESELECT') + + return objects_list + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.hosts.blender.plugin.asset_name(asset, subset) + container_name = pype.hosts.blender.plugin.asset_name( + asset, subset, namespace + ) + + container = bpy.data.collections.new(lib_container) + container.name = container_name + blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + objects_list = self._process( + libpath, lib_container, container_name, (None, None)) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + blender.pipeline.AVALON_PROPERTY) + collection_libpath = collection_metadata["libpath"] + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + + camera = objects[0] + + actions = ( camera.animation_data.action, camera.data.animation_data.action ) + + self._remove(objects, lib_container) + + objects_list = self._process( + str(libpath), lib_container, collection.name, actions) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + self._remove(objects, lib_container) + + bpy.data.collections.remove(collection) + + return True diff --git a/pype/plugins/blender/publish/extract_blend.py b/pype/plugins/blender/publish/extract_blend.py index 0924763f12..a5e76dcf4e 100644 --- a/pype/plugins/blender/publish/extract_blend.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -9,7 +9,7 @@ class ExtractBlend(pype.api.Extractor): label = "Extract Blend" hosts = ["blender"] - families = ["animation", "model", "rig", "action", "layout"] + families = ["model", "camera", "rig", "action", "layout", "animation"] optional = True def process(self, instance):