Implemented camera publishing in Unreal and loading in Blender

This commit is contained in:
Simone Barbieri 2021-09-23 16:26:59 +01:00
parent eae7079b08
commit 8c27675b43
5 changed files with 333 additions and 4 deletions

View file

@ -23,7 +23,7 @@ class BlendCameraLoader(openpype.hosts.blender.api.plugin.AssetLoader):
families = ["camera"]
representations = ["blend"]
label = "Link Camera"
label = "Link Camera (Blend)"
icon = "code-fork"
color = "orange"

View file

@ -0,0 +1,218 @@
"""Load an asset in Blender from an Alembic file."""
from pathlib import Path
from pprint import pformat
from typing import Dict, List, Optional
import bpy
from avalon import api
from avalon.blender import lib
from avalon.blender.pipeline import AVALON_CONTAINERS
from avalon.blender.pipeline import AVALON_CONTAINER_ID
from avalon.blender.pipeline import AVALON_PROPERTY
from openpype.hosts.blender.api import plugin
class FbxCameraLoader(plugin.AssetLoader):
"""Load a camera from FBX.
Stores the imported asset in an empty named after the asset.
"""
families = ["camera"]
representations = ["fbx"]
label = "Load Camera (FBX)"
icon = "code-fork"
color = "orange"
def _remove(self, asset_group):
objects = list(asset_group.children)
for obj in objects:
if obj.type == 'CAMERA':
bpy.data.cameras.remove(obj.data)
elif obj.type == 'EMPTY':
objects.extend(obj.children)
bpy.data.objects.remove(obj)
def _process(self, libpath, asset_group, group_name):
bpy.ops.object.select_all(action='DESELECT')
collection = bpy.context.view_layer.active_layer_collection.collection
bpy.ops.import_scene.fbx(filepath=libpath)
parent = bpy.context.scene.collection
objects = lib.get_selection()
for obj in objects:
obj.parent = asset_group
for obj in objects:
parent.objects.link(obj)
collection.objects.unlink(obj)
for obj in objects:
name = obj.name
obj.name = f"{group_name}:{name}"
if obj.type != 'EMPTY':
name_data = obj.data.name
obj.data.name = f"{group_name}:{name_data}"
if not obj.get(AVALON_PROPERTY):
obj[AVALON_PROPERTY] = dict()
avalon_info = obj[AVALON_PROPERTY]
avalon_info.update({"container_name": group_name})
bpy.ops.object.select_all(action='DESELECT')
return objects
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"]
asset_name = plugin.asset_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
bpy.context.scene.collection.children.link(avalon_container)
asset_group = bpy.data.objects.new(group_name, object_data=None)
avalon_container.objects.link(asset_group)
objects = self._process(libpath, asset_group, group_name)
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)
asset_group[AVALON_PROPERTY] = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": name,
"namespace": namespace or '',
"loader": str(self.__class__.__name__),
"representation": str(context["representation"]["_id"]),
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"objectName": group_name
}
self[:] = objects
return objects
def exec_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!
"""
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(api.get_representation_path(representation))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
)
assert asset_group, (
f"The asset is not loaded: {container['objectName']}"
)
assert libpath, (
"No existing library file found for {container['objectName']}"
)
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)
metadata = asset_group.get(AVALON_PROPERTY)
group_libpath = metadata["libpath"]
normalized_group_libpath = (
str(Path(bpy.path.abspath(group_libpath)).resolve())
)
normalized_libpath = (
str(Path(bpy.path.abspath(str(libpath))).resolve())
)
self.log.debug(
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_group_libpath,
normalized_libpath,
)
if normalized_group_libpath == normalized_libpath:
self.log.info("Library already loaded, not updating...")
return
mat = asset_group.matrix_basis.copy()
self._remove(asset_group)
self._process(str(libpath), asset_group, object_name, action)
asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.
Arguments:
container (openpype: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!
"""
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
if not asset_group:
return False
self._remove(asset_group)
bpy.data.objects.remove(asset_group)
return True

View file

@ -6,10 +6,10 @@ from openpype.hosts.blender.api import plugin
import bpy
class ExtractFBX(api.Extractor):
"""Extract as FBX."""
class ExtractCamera(api.Extractor):
"""Extract as the camera as FBX."""
label = "Extract FBX"
label = "Extract Camera"
hosts = ["blender"]
families = ["camera"]
optional = True

View file

@ -0,0 +1,56 @@
import unreal
from unreal import EditorAssetLibrary as eal
from unreal import EditorLevelLibrary as ell
from openpype.hosts.unreal.api.plugin import Creator
from avalon.unreal import (
instantiate,
)
class CreateCamera(Creator):
"""Layout output for character rigs"""
name = "layoutMain"
label = "Camera"
family = "camera"
icon = "cubes"
root = "/Game/Avalon/Instances"
suffix = "_INS"
def __init__(self, *args, **kwargs):
super(CreateCamera, self).__init__(*args, **kwargs)
def process(self):
data = self.data
name = data["subset"]
# selection = []
# # if (self.options or {}).get("useSelection"):
# # sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
# # selection = [a.get_path_name() for a in sel_objects]
# data["level"] = ell.get_editor_world().get_path_name()
data["level"] = ell.get_editor_world().get_path_name()
# if (self.options or {}).get("useSelection"):
# # Set as members the selected actors
# for actor in ell.get_selected_level_actors():
# data["members"].append("{}.{}".format(
# actor.get_outer().get_name(), actor.get_name()))
if not eal.does_directory_exist(self.root):
eal.make_directory(self.root)
factory = unreal.LevelSequenceFactoryNew()
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset = tools.create_asset(name, f"{self.root}/{name}", None, factory)
asset_name = f"{self.root}/{name}/{name}.{name}"
data["members"] = [asset_name]
instantiate(f"{self.root}", name, data, None, self.suffix)

View file

@ -0,0 +1,55 @@
import os
import unreal
from unreal import EditorAssetLibrary as eal
from unreal import EditorLevelLibrary as ell
import openpype.api
from avalon import io
class ExtractCamera(openpype.api.Extractor):
"""Extract a camera."""
label = "Extract Camera"
hosts = ["unreal"]
families = ["camera"]
optional = True
def process(self, instance):
# Define extract output file path
stagingdir = self.staging_dir(instance)
fbx_filename = "{}.fbx".format(instance.name)
# Perform extraction
self.log.info("Performing extraction..")
# Check if the loaded level is the same of the instance
current_level = ell.get_editor_world().get_path_name()
assert current_level == instance.data.get("level"), \
"Wrong level loaded"
for member in instance[:]:
data = eal.find_asset_data(member)
if data.asset_class == "LevelSequence":
ar = unreal.AssetRegistryHelpers.get_asset_registry()
sequence = ar.get_asset_by_object_path(member).get_asset()
unreal.SequencerTools.export_fbx(
ell.get_editor_world(),
sequence,
sequence.get_bindings(),
unreal.FbxExportOption(),
os.path.join(stagingdir, fbx_filename)
)
break
if "representations" not in instance.data:
instance.data["representations"] = []
fbx_representation = {
'name': 'fbx',
'ext': 'fbx',
'files': fbx_filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(fbx_representation)