Merge pull request #212 from BigRoy/feature/blender_usd

Blender: Implement USD Extractor and Loader
This commit is contained in:
Libor Batek 2024-05-23 13:22:25 +02:00 committed by GitHub
commit 6dd5f4848e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 210 additions and 10 deletions

View file

@ -365,3 +365,62 @@ def maintained_time():
yield
finally:
bpy.context.scene.frame_current = current_time
def get_all_parents(obj):
"""Get all recursive parents of object.
Arguments:
obj (bpy.types.Object): Object to get all parents for.
Returns:
List[bpy.types.Object]: All parents of object
"""
result = []
while True:
obj = obj.parent
if not obj:
break
result.append(obj)
return result
def get_highest_root(objects):
"""Get the highest object (the least parents) among the objects.
If multiple objects have the same amount of parents (or no parents) the
first object found in the input iterable will be returned.
Note that this will *not* return objects outside of the input list, as
such it will not return the root of node from a child node. It is purely
intended to find the highest object among a list of objects. To instead
get the root from one object use, e.g. `get_all_parents(obj)[-1]`
Arguments:
objects (List[bpy.types.Object]): Objects to find the highest root in.
Returns:
Optional[bpy.types.Object]: First highest root found or None if no
`bpy.types.Object` found in input list.
"""
included_objects = {obj.name_full for obj in objects}
num_parents_to_obj = {}
for obj in objects:
if isinstance(obj, bpy.types.Object):
parents = get_all_parents(obj)
# included parents
parents = [parent for parent in parents if
parent.name_full in included_objects]
if not parents:
# A node without parents must be a highest root
return obj
num_parents_to_obj.setdefault(len(parents), obj)
if not num_parents_to_obj:
return
minimum_parent = min(num_parents_to_obj)
return num_parents_to_obj[minimum_parent]

View file

@ -26,7 +26,8 @@ from .ops import (
)
from .lib import imprint
VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"]
VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx",
".usd", ".usdc", ".usda"]
def prepare_scene_name(

View file

@ -0,0 +1,30 @@
"""Create a USD Export."""
from ayon_core.hosts.blender.api import plugin, lib
class CreateUSD(plugin.BaseCreator):
"""Create USD Export"""
identifier = "io.openpype.creators.blender.usd"
name = "usdMain"
label = "USD"
product_type = "usd"
icon = "gears"
def create(
self, product_name: str, instance_data: dict, pre_create_data: dict
):
# Run parent create method
collection = super().create(
product_name, instance_data, pre_create_data
)
if pre_create_data.get("use_selection"):
objects = lib.get_selection()
for obj in objects:
collection.objects.link(obj)
if obj.type == 'EMPTY':
objects.extend(obj.children)
return collection

View file

@ -26,10 +26,10 @@ class CacheModelLoader(plugin.AssetLoader):
Note:
At least for now it only supports Alembic files.
"""
product_types = {"model", "pointcache", "animation"}
representations = {"abc"}
product_types = {"model", "pointcache", "animation", "usd"}
representations = {"abc", "usd"}
label = "Load Alembic"
label = "Load Cache"
icon = "code-fork"
color = "orange"
@ -53,10 +53,21 @@ class CacheModelLoader(plugin.AssetLoader):
plugin.deselect_all()
relative = bpy.context.preferences.filepaths.use_relative_paths
bpy.ops.wm.alembic_import(
filepath=libpath,
relative_path=relative
)
if any(libpath.lower().endswith(ext)
for ext in [".usd", ".usda", ".usdc"]):
# USD
bpy.ops.wm.usd_import(
filepath=libpath,
relative_path=relative
)
else:
# Alembic
bpy.ops.wm.alembic_import(
filepath=libpath,
relative_path=relative
)
imported = lib.get_selection()

View file

@ -12,7 +12,7 @@ class CollectBlenderInstanceData(pyblish.api.InstancePlugin):
order = pyblish.api.CollectorOrder
hosts = ["blender"]
families = ["model", "pointcache", "animation", "rig", "camera", "layout",
"blendScene"]
"blendScene", "usd"]
label = "Collect Instance"
def process(self, instance):

View file

@ -0,0 +1,90 @@
import os
import bpy
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin, lib
class ExtractUSD(publish.Extractor):
"""Extract as USD."""
label = "Extract USD"
hosts = ["blender"]
families = ["usd"]
def process(self, instance):
# Ignore runtime instances (e.g. USD layers)
# TODO: This is better done via more specific `families`
if not instance.data.get("transientData", {}).get("instance_node"):
return
# Define extract output file path
stagingdir = self.staging_dir(instance)
filename = f"{instance.name}.usd"
filepath = os.path.join(stagingdir, filename)
# Perform extraction
self.log.debug("Performing extraction..")
# Select all members to "export selected"
plugin.deselect_all()
selected = []
for obj in instance:
if isinstance(obj, bpy.types.Object):
obj.select_set(True)
selected.append(obj)
root = lib.get_highest_root(objects=instance[:])
if not root:
instance_node = instance.data["transientData"]["instance_node"]
raise publish.KnownPublishError(
f"No root object found in instance: {instance_node.name}"
)
self.log.debug(f"Exporting using active root: {root.name}")
context = plugin.create_blender_context(
active=root, selected=selected)
# Export USD
with bpy.context.temp_override(**context):
bpy.ops.wm.usd_export(
filepath=filepath,
selected_objects_only=True,
export_textures=False,
relative_paths=False,
export_animation=False,
export_hair=False,
export_uvmaps=True,
# TODO: add for new version of Blender (4+?)
# export_mesh_colors=True,
export_normals=True,
export_materials=True,
use_instancing=True
)
plugin.deselect_all()
# Add representation
representation = {
'name': 'usd',
'ext': 'usd',
'files': filename,
"stagingDir": stagingdir,
}
instance.data.setdefault("representations", []).append(representation)
self.log.debug("Extracted instance '%s' to: %s",
instance.name, representation)
class ExtractModelUSD(ExtractUSD):
"""Extract model as USD."""
label = "Extract USD (Model)"
hosts = ["blender"]
families = ["model"]
# Driven by settings
optional = True

View file

@ -1,3 +1,3 @@
name = "blender"
title = "Blender"
version = "0.1.8"
version = "0.1.9"

View file

@ -151,6 +151,10 @@ class PublishPluginsModel(BaseSettingsModel):
default_factory=ExtractPlayblastModel,
title="Extract Playblast"
)
ExtractModelUSD: ValidatePluginModel = SettingsField(
default_factory=ValidatePluginModel,
title="Extract Model USD"
)
DEFAULT_BLENDER_PUBLISH_SETTINGS = {
@ -348,5 +352,10 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = {
},
indent=4
)
},
"ExtractModelUSD": {
"enabled": True,
"optional": True,
"active": True
}
}