Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Feature: USD Workflow for Maya, Houdini and Blender #5925

Closed
wants to merge 69 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
0c0b580
Merge remote-tracking branch 'upstream/develop' into feature/maya_usd…
BigRoy Sep 11, 2023
017cc75
Merge remote-tracking branch 'upstream/develop' into feature/maya_usd…
BigRoy Sep 15, 2023
e57a0a7
Merge remote-tracking branch 'upstream/develop' into feature/maya_usd…
BigRoy Sep 19, 2023
4cc3ca9
Merge branch 'develop' of https://github.com/pypeclub/OpenPype into f…
BigRoy Oct 10, 2023
997f1b1
Draft prototype for Maya USD pipeline
BigRoy Oct 12, 2023
d36f3b9
Merge remote-tracking branch 'upstream/develop' into feature/maya_usd…
BigRoy Oct 31, 2023
cb84aaa
Merge remote-tracking branch 'upstream/develop' into feature/maya_usd…
BigRoy Nov 1, 2023
8bac8f3
Houdini USD functionality draft cleanup/refactor
BigRoy Nov 2, 2023
f0f473e
Report which Creator was duplicated
BigRoy Nov 2, 2023
d1f6b92
Define `add_transient_instance_data` and `remove_transient_instance_d…
BigRoy Nov 2, 2023
b281bcb
Add Maya show in usdview loader
BigRoy Nov 2, 2023
9cf29b2
Collect resources dir for `usd` family
BigRoy Nov 2, 2023
038e4f2
WIP draft prototyping for Maya USD
BigRoy Nov 2, 2023
faa75ad
Fix issues with instances
BigRoy Nov 6, 2023
9a18e40
Tweak usdlib to support creating asset using both anonymous layers fo…
BigRoy Nov 7, 2023
3ace260
Do not warn on empty instances - that's up to validator to do somethi…
BigRoy Nov 8, 2023
8b1cb12
Allow simple `mayaUsdProxyShape` layer export to usd
BigRoy Nov 8, 2023
8e57b54
Remove (experimental) suffix from Houdini USD extractors
BigRoy Nov 8, 2023
803f457
Remove legacy/unused Houdin USD publish plug-ins
BigRoy Nov 8, 2023
6cd836e
Allow publishing USD ROP node including upstream configured save laye…
BigRoy Nov 8, 2023
b9b5d3f
Cleanup + fix label
BigRoy Nov 8, 2023
0c0ab1a
WIP draft for Maya USD exports to "USD Assets" with a Python API that…
BigRoy Nov 8, 2023
05754bb
Improve docstring
BigRoy Nov 8, 2023
3503514
Add comments
BigRoy Nov 8, 2023
5f85209
Merge remote-tracking branch 'upstream/develop' into feature/maya_usd…
BigRoy Nov 8, 2023
c38b3e9
Cleanup + add very simple AYON uri resolving functionality
BigRoy Nov 9, 2023
293cf1b
Remove debug print
BigRoy Nov 9, 2023
cac9c0e
Add simple Blender USD creator + extractor
BigRoy Nov 9, 2023
3970193
Support short-hand `ayon://` entity URI
BigRoy Nov 9, 2023
be65cf1
Cleanup usdlib + start adding tests + improve docstrings
BigRoy Nov 10, 2023
4bc2541
Use dataclass since it's more explicit
BigRoy Nov 10, 2023
b6403e4
Expand tests
BigRoy Nov 10, 2023
35e1394
Tweak logging message for clarity
BigRoy Nov 11, 2023
65ab8a1
Replace old usd bootstrap with new (global) layer contribution system
BigRoy Nov 11, 2023
c95fb7d
Add shading mode option, default to exporting no shaders
BigRoy Nov 11, 2023
5094728
Cleanup
BigRoy Nov 11, 2023
26841a0
Add todo
BigRoy Nov 11, 2023
258e721
Houdini: Allow defining multiple families from Creator
BigRoy Nov 12, 2023
e82f9b3
Houdini: Fix USD Layer Contribution
BigRoy Nov 12, 2023
b878743
Cosmetics + add todo
BigRoy Nov 12, 2023
8bc989b
Fix sublayer versions not continuously appending versions
BigRoy Nov 12, 2023
af3a7f0
Add tests and fix implementation for `add_ordered_sublayer`
BigRoy Nov 12, 2023
7a809b0
Fix ordering (higher order is stronger opinion) + make tests more exp…
BigRoy Nov 13, 2023
02f7d1a
Allow to initialize as either `asset` or `shot` target USD file if no…
BigRoy Nov 13, 2023
b983779
Implement validations in the test that data is as we expect
BigRoy Nov 13, 2023
7086adb
Remove unused `tmp_path` feature
BigRoy Nov 13, 2023
32f1791
Re-enable `BUILD_INTO_LAST_VERSIONS`
BigRoy Nov 13, 2023
c1922f6
Reorder imports (cosmetics)
BigRoy Nov 13, 2023
774c089
Tweak label for readability
BigRoy Nov 13, 2023
53dc0f3
Make asset contribution create just a regular usd instance
BigRoy Nov 13, 2023
5a406c3
Merge remote-tracking branch 'upstream/develop' into feature/maya_usd…
BigRoy Nov 13, 2023
50ec57d
Merge branch 'feature/maya_usd_native_support' into enhancement/blend…
BigRoy Nov 14, 2023
4e0d733
Fix USD export for draft new publisher
BigRoy Nov 14, 2023
c858eef
Lib cleanup
BigRoy Nov 14, 2023
5227f88
Merge remote-tracking branch 'upstream/develop' into feature/usd_work…
BigRoy Nov 15, 2023
2c57123
Merge branch 'enhancement/blender_usd' into feature/usd_workflow
BigRoy Nov 15, 2023
3c28f52
Merge remote-tracking branch 'upstream/develop' into feature/usd_work…
BigRoy Nov 15, 2023
547a4ef
Re-use duplicated functionality from lib instead + improve docstrings
BigRoy Nov 15, 2023
c47777b
Hound + cosmetics
BigRoy Nov 15, 2023
4af412e
Improve docstring + error message
BigRoy Nov 15, 2023
e4fc6a8
Add draft USD importing to Blender
BigRoy Nov 15, 2023
885429c
Hound
BigRoy Nov 15, 2023
9b56fe4
Hound
BigRoy Nov 15, 2023
b2a0014
Remove todo that has been implemented - even if rudimentary
BigRoy Nov 15, 2023
4fc3bf4
Hound
BigRoy Nov 15, 2023
856e654
Do not hide unshareable proxies
BigRoy Nov 15, 2023
65da9c6
Refactor filenames to be easier to find
BigRoy Nov 17, 2023
6385126
Add tooltips
BigRoy Nov 19, 2023
dc43a4b
Merge branch 'develop' of https://github.com/pypeclub/OpenPype into f…
BigRoy Dec 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions openpype/hosts/blender/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
3 changes: 2 additions & 1 deletion openpype/hosts/blender/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
)
from .lib import imprint

VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"]
VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx",
".usd", ".usdc", ".usda"]


def prepare_scene_name(
Expand Down
30 changes: 30 additions & 0 deletions openpype/hosts/blender/plugins/create/create_usd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Create a USD Export."""

from openpype.hosts.blender.api import plugin, lib


class CreateUSD(plugin.BaseCreator):
"""Create USD Export"""

identifier = "io.openpype.creators.blender.usd"
name = "usdMain"
label = "USD"
family = "usd"
icon = "gears"

def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
# Run parent create method
collection = super().create(
subset_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
26 changes: 19 additions & 7 deletions openpype/hosts/blender/plugins/load/load_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ class CacheModelLoader(plugin.AssetLoader):
Note:
At least for now it only supports Alembic files.
"""
families = ["model", "pointcache", "animation"]
representations = ["abc"]
families = ["model", "pointcache", "animation", "usd"]
representations = ["abc", "usd"]

label = "Load Alembic"
# TODO: Should USD loader be a separate loader instead?
label = "Load Alembic/USD"
icon = "code-fork"
color = "orange"

Expand All @@ -53,10 +54,21 @@ def _process(self, libpath, asset_group, group_name):
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()

Expand Down
2 changes: 1 addition & 1 deletion openpype/hosts/blender/plugins/publish/collect_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
43 changes: 9 additions & 34 deletions openpype/hosts/blender/plugins/publish/extract_fbx_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,10 @@

from openpype.pipeline import publish
from openpype.hosts.blender.api import plugin
from openpype.hosts.blender.api.lib import get_highest_root
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY


def get_all_parents(obj):
"""Get all recursive 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 that is also in the collection
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)

minimum_parent = min(num_parents_to_obj)
return num_parents_to_obj[minimum_parent]


class ExtractAnimationFBX(
publish.Extractor,
publish.OptionalPyblishPluginMixin,
Expand Down Expand Up @@ -68,19 +38,24 @@ def process(self, instance):
# and for those objects include the children hierarchy
# TODO: Would it make more sense for the Collect Instance collector
# to also always retrieve all the children?
objects = set(asset_group.objects)

# From the direct children of the collection find the 'root' node
# that we want to export - it is the 'highest' node in a hierarchy
root = get_highest_root(objects)
root = get_highest_root(asset_group.objects)
if not root:
raise publish.KnownPublishError(
f"No root object found in instance: {asset_group.name}"
f"No objects in asset group: {asset_group.name}"
)

objects = set(asset_group.objects)
for obj in list(objects):
objects.update(obj.children_recursive)

# Find all armatures among the objects, assume to find only one
armatures = [obj for obj in objects if obj.type == "ARMATURE"]
if not armatures:
raise RuntimeError(
raise publish.KnownPublishError(
f"Unable to find ARMATURE in collection: "
f"{asset_group.name}"
)
Expand Down
79 changes: 79 additions & 0 deletions openpype/hosts/blender/plugins/publish/extract_usd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import os

import bpy

from openpype.pipeline import publish
from openpype.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
bpy.ops.wm.usd_export(
context,
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)
24 changes: 24 additions & 0 deletions openpype/hosts/houdini/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,30 @@ def maintained_selection():
node.setSelected(on=True)


@contextmanager
def parm_values(overrides):
"""Override Parameter values during the context.

Arguments:
overrides (List[Tuple[hou.Parm, Any]]): The overrides per parm
that should be applied during context.

"""

originals = []
try:
for parm, value in overrides:
originals.append((parm, parm.eval()))
parm.set(value)
yield
finally:
for parm, value in originals:
# Parameter might not exist anymore so first
# check whether it's still valid
if hou.parm(parm.path()):
parm.set(value)


def reset_framerange():
"""Set frame range and FPS to current asset"""

Expand Down
19 changes: 19 additions & 0 deletions openpype/hosts/houdini/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def create(self, subset_name, instance_data, pre_create_data):

instance_data["instance_node"] = instance_node.path()
instance_data["instance_id"] = instance_node.path()
instance_data["families"] = self.get_publish_families()
instance = CreatedInstance(
self.family,
subset_name,
Expand Down Expand Up @@ -245,6 +246,7 @@ def collect_instances(self):
node_path = instance.path()
node_data["instance_id"] = node_path
node_data["instance_node"] = node_path
node_data["families"] = self.get_publish_families()

created_instance = CreatedInstance.from_existing(
node_data, self
Expand All @@ -270,6 +272,7 @@ def imprint(self, node, values, update=False):
# from the node's path
values.pop("instance_node", None)
values.pop("instance_id", None)
values.pop("families", None)
imprint(node, values, update=update)

def remove_instances(self, instances):
Expand Down Expand Up @@ -311,6 +314,22 @@ def customize_node_look(
node.setUserData('nodeshape', shape)
node.setColor(color)

def get_publish_families(self):
"""Return families for the instances of this creator.

Allow a Creator to define multiple families so that a creator can
e.g. specify `usd` and `usdrop`.

There is no need to override this method if you only have the
primary family defined by the `family` property as that will always
be set.

Returns:
list: families for instances of this creator

"""
return []

def get_network_categories(self):
"""Return in which network view type this creator should show.

Expand Down
Loading
Loading