Skip to content

Commit

Permalink
Merge branch 'develop' into bugfix/add_deadline_to_prerender
Browse files Browse the repository at this point in the history
  • Loading branch information
kalisp committed May 24, 2024
2 parents e946dd5 + a25bda8 commit e446049
Show file tree
Hide file tree
Showing 95 changed files with 600 additions and 104 deletions.
2 changes: 2 additions & 0 deletions client/ayon_core/addon/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
# - this is used to log the missing addon
MOVED_ADDON_MILESTONE_VERSIONS = {
"applications": VersionInfo(0, 2, 0),
"clockify": VersionInfo(0, 2, 0),
"tvpaint": VersionInfo(0, 2, 0),
}

# Inherit from `object` for Python 2 hosts
Expand Down
59 changes: 59 additions & 0 deletions client/ayon_core/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 client/ayon_core/hosts/blender/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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 client/ayon_core/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 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
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -53,10 +53,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
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
90 changes: 90 additions & 0 deletions client/ayon_core/hosts/blender/plugins/publish/extract_usd.py
Original file line number Diff line number Diff line change
@@ -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
141 changes: 141 additions & 0 deletions client/ayon_core/hosts/houdini/plugins/create/create_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating Model product type.
Note:
Currently, This creator plugin is the same as 'create_pointcache.py'
But renaming the product type to 'model'.
It's purpose to support
Maya (load/publish model from maya to/from houdini).
It's considered to support multiple representations in the future.
"""

from ayon_core.hosts.houdini.api import plugin
from ayon_core.lib import BoolDef

import hou



class CreateModel(plugin.HoudiniCreator):
"""Create Model"""
identifier = "io.openpype.creators.houdini.model"
label = "Model"
product_type = "model"
icon = "cube"

def create(self, product_name, instance_data, pre_create_data):
instance_data.pop("active", None)
instance_data.update({"node_type": "alembic"})
creator_attributes = instance_data.setdefault(
"creator_attributes", dict())
creator_attributes["farm"] = pre_create_data["farm"]

instance = super(CreateModel, self).create(
product_name,
instance_data,
pre_create_data)

instance_node = hou.node(instance.get("instance_node"))
parms = {
"use_sop_path": True,
"build_from_path": True,
"path_attrib": "path",
"prim_to_detail_pattern": "cbId",
"format": 2,
"facesets": 0,
"filename": hou.text.expandString(
"$HIP/pyblish/{}.abc".format(product_name))
}

if self.selected_nodes:
selected_node = self.selected_nodes[0]

# Although Houdini allows ObjNode path on `sop_path` for the
# the ROP node we prefer it set to the SopNode path explicitly

# Allow sop level paths (e.g. /obj/geo1/box1)
if isinstance(selected_node, hou.SopNode):
parms["sop_path"] = selected_node.path()
self.log.debug(
"Valid SopNode selection, 'SOP Path' in ROP will be set to '%s'."
% selected_node.path()
)

# Allow object level paths to Geometry nodes (e.g. /obj/geo1)
# but do not allow other object level nodes types like cameras, etc.
elif isinstance(selected_node, hou.ObjNode) and \
selected_node.type().name() in ["geo"]:

# get the output node with the minimum
# 'outputidx' or the node with display flag
sop_path = self.get_obj_output(selected_node)

if sop_path:
parms["sop_path"] = sop_path.path()
self.log.debug(
"Valid ObjNode selection, 'SOP Path' in ROP will be set to "
"the child path '%s'."
% sop_path.path()
)

if not parms.get("sop_path", None):
self.log.debug(
"Selection isn't valid. 'SOP Path' in ROP will be empty."
)
else:
self.log.debug(
"No Selection. 'SOP Path' in ROP will be empty."
)

instance_node.setParms(parms)
instance_node.parm("trange").set(1)

# Explicitly set f1 and f2 to frame start.
# Which forces the rop node to export one frame.
instance_node.parmTuple('f').deleteAllKeyframes()
fstart = int(hou.hscriptExpression("$FSTART"))
instance_node.parmTuple('f').set((fstart, fstart, 1))

# Lock any parameters in this list
to_lock = ["prim_to_detail_pattern"]
self.lock_parameters(instance_node, to_lock)

def get_network_categories(self):
return [
hou.ropNodeTypeCategory(),
hou.sopNodeTypeCategory()
]

def get_obj_output(self, obj_node):
"""Find output node with the smallest 'outputidx'."""

outputs = obj_node.subnetOutputs()

# if obj_node is empty
if not outputs:
return

# if obj_node has one output child whether its
# sop output node or a node with the render flag
elif len(outputs) == 1:
return outputs[0]

# if there are more than one, then it have multiple output nodes
# return the one with the minimum 'outputidx'
else:
return min(outputs,
key=lambda node: node.evalParm('outputidx'))

def get_instance_attr_defs(self):
return [
BoolDef("farm",
label="Submitting to Farm",
default=False)
]

def get_pre_create_attr_defs(self):
attrs = super().get_pre_create_attr_defs()
# Use same attributes as for instance attributes
return attrs + self.get_instance_attr_defs()
Loading

0 comments on commit e446049

Please sign in to comment.