mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
Merge branch 'develop' into enhancement/maya_template
# Conflicts: # openpype/hosts/maya/api/lib.py
This commit is contained in:
commit
828a71ee17
39 changed files with 471 additions and 306 deletions
|
|
@ -52,7 +52,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n
|
|||
|
||||
# we need to build our own patchelf
|
||||
WORKDIR /temp-patchelf
|
||||
RUN git clone https://github.com/NixOS/patchelf.git . \
|
||||
RUN git clone -b 0.17.0 --single-branch https://github.com/NixOS/patchelf.git . \
|
||||
&& source scl_source enable devtoolset-7 \
|
||||
&& ./bootstrap.sh \
|
||||
&& ./configure \
|
||||
|
|
|
|||
|
|
@ -104,3 +104,6 @@ class AbcLoader(load.LoaderPlugin):
|
|||
|
||||
node = container["node"]
|
||||
node.destroy()
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -73,3 +73,6 @@ class AbcArchiveLoader(load.LoaderPlugin):
|
|||
|
||||
node = container["node"]
|
||||
node.destroy()
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -106,3 +106,6 @@ class BgeoLoader(load.LoaderPlugin):
|
|||
|
||||
node = container["node"]
|
||||
node.destroy()
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -192,3 +192,6 @@ class CameraLoader(load.LoaderPlugin):
|
|||
|
||||
new_node.moveToGoodPosition()
|
||||
return new_node
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -125,3 +125,6 @@ class ImageLoader(load.LoaderPlugin):
|
|||
prefix, padding, suffix = first_fname.rsplit(".", 2)
|
||||
fname = ".".join([prefix, "$F{}".format(len(padding)), suffix])
|
||||
return os.path.join(root, fname).replace("\\", "/")
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -79,3 +79,6 @@ class USDSublayerLoader(load.LoaderPlugin):
|
|||
|
||||
node = container["node"]
|
||||
node.destroy()
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -79,3 +79,6 @@ class USDReferenceLoader(load.LoaderPlugin):
|
|||
|
||||
node = container["node"]
|
||||
node.destroy()
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -102,3 +102,6 @@ class VdbLoader(load.LoaderPlugin):
|
|||
|
||||
node = container["node"]
|
||||
node.destroy()
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -128,14 +128,14 @@ class AvalonURIOutputProcessor(base.OutputProcessorBase):
|
|||
if not asset_doc:
|
||||
raise RuntimeError("Invalid asset name: '%s'" % asset)
|
||||
|
||||
formatted_anatomy = anatomy.format({
|
||||
template_obj = anatomy.templates_obj["publish"]["path"]
|
||||
path = template_obj.format_strict({
|
||||
"project": PROJECT,
|
||||
"asset": asset_doc["name"],
|
||||
"subset": subset,
|
||||
"representation": ext,
|
||||
"version": 0 # stub version zero
|
||||
})
|
||||
path = formatted_anatomy["publish"]["path"]
|
||||
|
||||
# Remove the version folder
|
||||
subset_folder = os.path.dirname(os.path.dirname(path))
|
||||
|
|
|
|||
24
openpype/hosts/max/hooks/force_startup_script.py
Normal file
24
openpype/hosts/max/hooks/force_startup_script.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pre-launch to force 3ds max startup script."""
|
||||
from openpype.lib import PreLaunchHook
|
||||
import os
|
||||
|
||||
|
||||
class ForceStartupScript(PreLaunchHook):
|
||||
"""Inject OpenPype environment to 3ds max.
|
||||
|
||||
Note that this works in combination whit 3dsmax startup script that
|
||||
is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH
|
||||
environment.
|
||||
|
||||
Hook `GlobalHostDataHook` must be executed before this hook.
|
||||
"""
|
||||
app_groups = ["3dsmax"]
|
||||
order = 11
|
||||
|
||||
def execute(self):
|
||||
startup_args = [
|
||||
"-U",
|
||||
"MAXScript",
|
||||
f"{os.getenv('OPENPYPE_ROOT')}\\openpype\\hosts\\max\\startup\\startup.ms"] # noqa
|
||||
self.launch_context.launch_args.append(startup_args)
|
||||
|
|
@ -32,6 +32,10 @@ from openpype.pipeline import (
|
|||
load_container,
|
||||
registered_host,
|
||||
)
|
||||
from openpype.pipeline.create import (
|
||||
legacy_create,
|
||||
get_legacy_creator_by_name,
|
||||
)
|
||||
from openpype.pipeline.context_tools import (
|
||||
get_current_asset_name,
|
||||
get_current_project_asset,
|
||||
|
|
@ -2153,17 +2157,23 @@ def set_scene_resolution(width, height, pixelAspect):
|
|||
cmds.setAttr("%s.pixelAspect" % control_node, pixelAspect)
|
||||
|
||||
|
||||
def get_frame_range():
|
||||
"""Get the current assets frame range and handles."""
|
||||
def get_frame_range(include_animation_range=False):
|
||||
"""Get the current assets frame range and handles.
|
||||
|
||||
Args:
|
||||
include_animation_range (bool, optional): Whether to include
|
||||
`animationStart` and `animationEnd` keys to define the outer
|
||||
range of the timeline. It is excluded by default.
|
||||
|
||||
Returns:
|
||||
dict: Asset's expected frame range values.
|
||||
|
||||
"""
|
||||
|
||||
# Set frame start/end
|
||||
project_name = get_current_project_name()
|
||||
task_name = get_current_task_name()
|
||||
asset_name = get_current_asset_name()
|
||||
asset = get_asset_by_name(project_name, asset_name)
|
||||
settings = get_project_settings(project_name)
|
||||
include_handles_settings = settings["maya"]["include_handles"]
|
||||
current_task = asset.get("data").get("tasks").get(task_name)
|
||||
|
||||
frame_start = asset["data"].get("frameStart")
|
||||
frame_end = asset["data"].get("frameEnd")
|
||||
|
|
@ -2175,32 +2185,39 @@ def get_frame_range():
|
|||
handle_start = asset["data"].get("handleStart") or 0
|
||||
handle_end = asset["data"].get("handleEnd") or 0
|
||||
|
||||
animation_start = frame_start
|
||||
animation_end = frame_end
|
||||
|
||||
include_handles = include_handles_settings["include_handles_default"]
|
||||
for item in include_handles_settings["per_task_type"]:
|
||||
if current_task["type"] in item["task_type"]:
|
||||
include_handles = item["include_handles"]
|
||||
break
|
||||
if include_handles:
|
||||
animation_start -= int(handle_start)
|
||||
animation_end += int(handle_end)
|
||||
|
||||
cmds.playbackOptions(
|
||||
minTime=frame_start,
|
||||
maxTime=frame_end,
|
||||
animationStartTime=animation_start,
|
||||
animationEndTime=animation_end
|
||||
)
|
||||
cmds.currentTime(frame_start)
|
||||
|
||||
return {
|
||||
frame_range = {
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end
|
||||
}
|
||||
if include_animation_range:
|
||||
# The animation range values are only included to define whether
|
||||
# the Maya time slider should include the handles or not.
|
||||
# Some usages of this function use the full dictionary to define
|
||||
# instance attributes for which we want to exclude the animation
|
||||
# keys. That is why these are excluded by default.
|
||||
task_name = get_current_task_name()
|
||||
settings = get_project_settings(project_name)
|
||||
include_handles_settings = settings["maya"]["include_handles"]
|
||||
current_task = asset.get("data").get("tasks").get(task_name)
|
||||
|
||||
animation_start = frame_start
|
||||
animation_end = frame_end
|
||||
|
||||
include_handles = include_handles_settings["include_handles_default"]
|
||||
for item in include_handles_settings["per_task_type"]:
|
||||
if current_task["type"] in item["task_type"]:
|
||||
include_handles = item["include_handles"]
|
||||
break
|
||||
if include_handles:
|
||||
animation_start -= int(handle_start)
|
||||
animation_end += int(handle_end)
|
||||
|
||||
frame_range["animationStart"] = animation_start
|
||||
frame_range["animationEnd"] = animation_end
|
||||
|
||||
return frame_range
|
||||
|
||||
|
||||
def reset_frame_range(playback=True, render=True, fps=True):
|
||||
|
|
@ -2219,18 +2236,23 @@ def reset_frame_range(playback=True, render=True, fps=True):
|
|||
)
|
||||
set_scene_fps(fps)
|
||||
|
||||
frame_range = get_frame_range()
|
||||
frame_range = get_frame_range(include_animation_range=True)
|
||||
if not frame_range:
|
||||
# No frame range data found for asset
|
||||
return
|
||||
|
||||
frame_start = frame_range["frameStart"] - int(frame_range["handleStart"])
|
||||
frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"])
|
||||
frame_start = frame_range["frameStart"]
|
||||
frame_end = frame_range["frameEnd"]
|
||||
animation_start = frame_range["animationStart"]
|
||||
animation_end = frame_range["animationEnd"]
|
||||
|
||||
if playback:
|
||||
cmds.playbackOptions(minTime=frame_start)
|
||||
cmds.playbackOptions(maxTime=frame_end)
|
||||
cmds.playbackOptions(animationStartTime=frame_start)
|
||||
cmds.playbackOptions(animationEndTime=frame_end)
|
||||
cmds.playbackOptions(minTime=frame_start)
|
||||
cmds.playbackOptions(maxTime=frame_end)
|
||||
cmds.playbackOptions(
|
||||
minTime=frame_start,
|
||||
maxTime=frame_end,
|
||||
animationStartTime=animation_start,
|
||||
animationEndTime=animation_end
|
||||
)
|
||||
cmds.currentTime(frame_start)
|
||||
|
||||
if render:
|
||||
|
|
@ -3978,3 +4000,53 @@ def get_reference_node_parents(ref):
|
|||
referenceNode=True,
|
||||
parent=True)
|
||||
return parents
|
||||
|
||||
|
||||
def create_rig_animation_instance(nodes, context, namespace, log=None):
|
||||
"""Create an animation publish instance for loaded rigs.
|
||||
|
||||
See the RecreateRigAnimationInstance inventory action on how to use this
|
||||
for loaded rig containers.
|
||||
|
||||
Arguments:
|
||||
nodes (list): Member nodes of the rig instance.
|
||||
context (dict): Representation context of the rig container
|
||||
namespace (str): Namespace of the rig container
|
||||
log (logging.Logger, optional): Logger to log to if provided
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
output = next((node for node in nodes if
|
||||
node.endswith("out_SET")), None)
|
||||
controls = next((node for node in nodes if
|
||||
node.endswith("controls_SET")), None)
|
||||
|
||||
assert output, "No out_SET in rig, this is a bug."
|
||||
assert controls, "No controls_SET in rig, this is a bug."
|
||||
|
||||
# Find the roots amongst the loaded nodes
|
||||
roots = (
|
||||
cmds.ls(nodes, assemblies=True, long=True) or
|
||||
get_highest_in_hierarchy(nodes)
|
||||
)
|
||||
assert roots, "No root nodes in rig, this is a bug."
|
||||
|
||||
asset = legacy_io.Session["AVALON_ASSET"]
|
||||
dependency = str(context["representation"]["_id"])
|
||||
|
||||
if log:
|
||||
log.info("Creating subset: {}".format(namespace))
|
||||
|
||||
# Create the animation instance
|
||||
creator_plugin = get_legacy_creator_by_name("CreateAnimation")
|
||||
with maintained_selection():
|
||||
cmds.select([output, controls] + roots, noExpand=True)
|
||||
legacy_create(
|
||||
creator_plugin,
|
||||
name=namespace,
|
||||
asset=asset,
|
||||
options={"useSelection": True},
|
||||
data={"dependencies": dependency}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ from openpype.hosts.maya.api import (
|
|||
class CreateAnimation(plugin.Creator):
|
||||
"""Animation output for character rigs"""
|
||||
|
||||
# We hide the animation creator from the UI since the creation of it
|
||||
# is automated upon loading a rig. There's an inventory action to recreate
|
||||
# it for loaded rigs if by chance someone deleted the animation instance.
|
||||
# Note: This setting is actually applied from project settings
|
||||
enabled = False
|
||||
|
||||
name = "animationDefault"
|
||||
label = "Animation"
|
||||
family = "animation"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
from openpype.pipeline import (
|
||||
InventoryAction,
|
||||
get_representation_context
|
||||
)
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
create_rig_animation_instance,
|
||||
get_container_members,
|
||||
)
|
||||
|
||||
|
||||
class RecreateRigAnimationInstance(InventoryAction):
|
||||
"""Recreate animation publish instance for loaded rigs"""
|
||||
|
||||
label = "Recreate rig animation instance"
|
||||
icon = "wrench"
|
||||
color = "#888888"
|
||||
|
||||
@staticmethod
|
||||
def is_compatible(container):
|
||||
return (
|
||||
container.get("loader") == "ReferenceLoader"
|
||||
and container.get("name", "").startswith("rig")
|
||||
)
|
||||
|
||||
def process(self, containers):
|
||||
|
||||
for container in containers:
|
||||
# todo: delete an existing entry if it exist or skip creation
|
||||
|
||||
namespace = container["namespace"]
|
||||
representation_id = container["representation"]
|
||||
context = get_representation_context(representation_id)
|
||||
nodes = get_container_members(container)
|
||||
|
||||
create_rig_animation_instance(nodes, context, namespace)
|
||||
|
|
@ -4,16 +4,12 @@ import contextlib
|
|||
from maya import cmds
|
||||
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline.create import (
|
||||
legacy_create,
|
||||
get_legacy_creator_by_name,
|
||||
)
|
||||
import openpype.hosts.maya.api.plugin
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
maintained_selection,
|
||||
get_container_members,
|
||||
parent_nodes
|
||||
parent_nodes,
|
||||
create_rig_animation_instance
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -114,9 +110,6 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
# Name of creator class that will be used to create animation instance
|
||||
animation_creator_name = "CreateAnimation"
|
||||
|
||||
def process_reference(self, context, name, namespace, options):
|
||||
import maya.cmds as cmds
|
||||
|
||||
|
|
@ -220,37 +213,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
self._lock_camera_transforms(members)
|
||||
|
||||
def _post_process_rig(self, name, namespace, context, options):
|
||||
|
||||
output = next((node for node in self if
|
||||
node.endswith("out_SET")), None)
|
||||
controls = next((node for node in self if
|
||||
node.endswith("controls_SET")), None)
|
||||
|
||||
assert output, "No out_SET in rig, this is a bug."
|
||||
assert controls, "No controls_SET in rig, this is a bug."
|
||||
|
||||
# Find the roots amongst the loaded nodes
|
||||
roots = cmds.ls(self[:], assemblies=True, long=True)
|
||||
assert roots, "No root nodes in rig, this is a bug."
|
||||
|
||||
asset = legacy_io.Session["AVALON_ASSET"]
|
||||
dependency = str(context["representation"]["_id"])
|
||||
|
||||
self.log.info("Creating subset: {}".format(namespace))
|
||||
|
||||
# Create the animation instance
|
||||
creator_plugin = get_legacy_creator_by_name(
|
||||
self.animation_creator_name
|
||||
nodes = self[:]
|
||||
create_rig_animation_instance(
|
||||
nodes, context, namespace, log=self.log
|
||||
)
|
||||
with maintained_selection():
|
||||
cmds.select([output, controls] + roots, noExpand=True)
|
||||
legacy_create(
|
||||
creator_plugin,
|
||||
name=namespace,
|
||||
asset=asset,
|
||||
options={"useSelection": True},
|
||||
data={"dependencies": dependency}
|
||||
)
|
||||
|
||||
def _lock_camera_transforms(self, nodes):
|
||||
cameras = cmds.ls(nodes, type="camera")
|
||||
|
|
|
|||
|
|
@ -36,6 +36,30 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
context = instance.context
|
||||
objectset = context.data['objectsets']
|
||||
|
||||
# Convert enum attribute index to string for Display Lights.
|
||||
index = instance.data.get("displayLights", 0)
|
||||
display_lights = lib.DISPLAY_LIGHTS_VALUES[index]
|
||||
if display_lights == "project_settings":
|
||||
settings = instance.context.data["project_settings"]
|
||||
settings = settings["maya"]["publish"]["ExtractPlayblast"]
|
||||
settings = settings["capture_preset"]["Viewport Options"]
|
||||
display_lights = settings["displayLights"]
|
||||
|
||||
# Collect camera focal length.
|
||||
burninDataMembers = instance.data.get("burninDataMembers", {})
|
||||
if camera is not None:
|
||||
attr = camera + ".focalLength"
|
||||
if lib.get_attribute_input(attr):
|
||||
start = instance.data["frameStart"]
|
||||
end = instance.data["frameEnd"] + 1
|
||||
time_range = range(int(start), int(end))
|
||||
focal_length = [cmds.getAttr(attr, time=t) for t in time_range]
|
||||
else:
|
||||
focal_length = cmds.getAttr(attr)
|
||||
|
||||
burninDataMembers["focalLength"] = focal_length
|
||||
|
||||
# Account for nested instances like model.
|
||||
reviewable_subsets = list(set(members) & set(objectset))
|
||||
if reviewable_subsets:
|
||||
if len(reviewable_subsets) > 1:
|
||||
|
|
@ -68,6 +92,8 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
data['frameEndFtrack'] = instance.data["frameEndHandle"]
|
||||
data['frameStartHandle'] = instance.data["frameStartHandle"]
|
||||
data['frameEndHandle'] = instance.data["frameEndHandle"]
|
||||
data['handleStart'] = instance.data["handleStart"]
|
||||
data['handleEnd'] = instance.data["handleEnd"]
|
||||
data["frameStart"] = instance.data["frameStart"]
|
||||
data["frameEnd"] = instance.data["frameEnd"]
|
||||
data['step'] = instance.data['step']
|
||||
|
|
@ -77,6 +103,8 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
data["isolate"] = instance.data["isolate"]
|
||||
data["panZoom"] = instance.data.get("panZoom", False)
|
||||
data["panel"] = instance.data["panel"]
|
||||
data["displayLights"] = display_lights
|
||||
data["burninDataMembers"] = burninDataMembers
|
||||
|
||||
# The review instance must be active
|
||||
cmds.setAttr(str(instance) + '.active', 1)
|
||||
|
|
@ -103,6 +131,8 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
instance.data["frameStartHandle"]
|
||||
instance.data['frameEndFtrack'] = \
|
||||
instance.data["frameEndHandle"]
|
||||
instance.data["displayLights"] = display_lights
|
||||
instance.data["burninDataMembers"] = burninDataMembers
|
||||
|
||||
# make ftrack publishable
|
||||
instance.data.setdefault("families", []).append('ftrack')
|
||||
|
|
@ -144,33 +174,3 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
audio_data.append(get_audio_node_data(node))
|
||||
|
||||
instance.data["audio"] = audio_data
|
||||
|
||||
# Convert enum attribute index to string.
|
||||
index = instance.data.get("displayLights", 0)
|
||||
display_lights = lib.DISPLAY_LIGHTS_VALUES[index]
|
||||
if display_lights == "project_settings":
|
||||
settings = instance.context.data["project_settings"]
|
||||
settings = settings["maya"]["publish"]["ExtractPlayblast"]
|
||||
settings = settings["capture_preset"]["Viewport Options"]
|
||||
display_lights = settings["displayLights"]
|
||||
instance.data["displayLights"] = display_lights
|
||||
|
||||
# Collect focal length.
|
||||
if camera is None:
|
||||
return
|
||||
|
||||
attr = camera + ".focalLength"
|
||||
if lib.get_attribute_input(attr):
|
||||
start = instance.data["frameStart"]
|
||||
end = instance.data["frameEnd"] + 1
|
||||
focal_length = [
|
||||
cmds.getAttr(attr, time=t) for t in range(int(start), int(end))
|
||||
]
|
||||
else:
|
||||
focal_length = cmds.getAttr(attr)
|
||||
|
||||
key = "focalLength"
|
||||
try:
|
||||
instance.data["burninDataMembers"][key] = focal_length
|
||||
except KeyError:
|
||||
instance.data["burninDataMembers"] = {key: focal_length}
|
||||
|
|
|
|||
97
openpype/hosts/maya/tools/mayalookassigner/alembic.py
Normal file
97
openpype/hosts/maya/tools/mayalookassigner/alembic.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Tools for loading looks to vray proxies."""
|
||||
import os
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
|
||||
import six
|
||||
|
||||
import alembic.Abc
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_alembic_paths_by_property(filename, attr, verbose=False):
|
||||
# type: (str, str, bool) -> dict
|
||||
"""Return attribute value per objects in the Alembic file.
|
||||
|
||||
Reads an Alembic archive hierarchy and retrieves the
|
||||
value from the `attr` properties on the objects.
|
||||
|
||||
Args:
|
||||
filename (str): Full path to Alembic archive to read.
|
||||
attr (str): Id attribute.
|
||||
verbose (bool): Whether to verbosely log missing attributes.
|
||||
|
||||
Returns:
|
||||
dict: Mapping of node full path with its id
|
||||
|
||||
"""
|
||||
# Normalize alembic path
|
||||
filename = os.path.normpath(filename)
|
||||
filename = filename.replace("\\", "/")
|
||||
filename = str(filename) # path must be string
|
||||
|
||||
try:
|
||||
archive = alembic.Abc.IArchive(filename)
|
||||
except RuntimeError:
|
||||
# invalid alembic file - probably vrmesh
|
||||
log.warning("{} is not an alembic file".format(filename))
|
||||
return {}
|
||||
root = archive.getTop()
|
||||
|
||||
iterator = list(root.children)
|
||||
obj_ids = {}
|
||||
|
||||
for obj in iterator:
|
||||
name = obj.getFullName()
|
||||
|
||||
# include children for coming iterations
|
||||
iterator.extend(obj.children)
|
||||
|
||||
props = obj.getProperties()
|
||||
if props.getNumProperties() == 0:
|
||||
# Skip those without properties, e.g. '/materials' in a gpuCache
|
||||
continue
|
||||
|
||||
# THe custom attribute is under the properties' first container under
|
||||
# the ".arbGeomParams"
|
||||
prop = props.getProperty(0) # get base property
|
||||
|
||||
_property = None
|
||||
try:
|
||||
geo_params = prop.getProperty('.arbGeomParams')
|
||||
_property = geo_params.getProperty(attr)
|
||||
except KeyError:
|
||||
if verbose:
|
||||
log.debug("Missing attr on: {0}".format(name))
|
||||
continue
|
||||
|
||||
if not _property.isConstant():
|
||||
log.warning("Id not constant on: {0}".format(name))
|
||||
|
||||
# Get first value sample
|
||||
value = _property.getValue()[0]
|
||||
|
||||
obj_ids[name] = value
|
||||
|
||||
return obj_ids
|
||||
|
||||
|
||||
def get_alembic_ids_cache(path):
|
||||
# type: (str) -> dict
|
||||
"""Build a id to node mapping in Alembic file.
|
||||
|
||||
Nodes without IDs are ignored.
|
||||
|
||||
Returns:
|
||||
dict: Mapping of id to nodes in the Alembic.
|
||||
|
||||
"""
|
||||
node_ids = get_alembic_paths_by_property(path, attr="cbId")
|
||||
id_nodes = defaultdict(list)
|
||||
for node, _id in six.iteritems(node_ids):
|
||||
id_nodes[_id].append(node)
|
||||
|
||||
return dict(six.iteritems(id_nodes))
|
||||
|
|
@ -9,6 +9,7 @@ from openpype.pipeline import legacy_io
|
|||
from openpype.client import get_last_version_by_subset_name
|
||||
from openpype.hosts.maya import api
|
||||
from . import lib
|
||||
from .alembic import get_alembic_ids_cache
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -68,6 +69,11 @@ def get_nodes_by_id(standin):
|
|||
(dict): Dictionary with node full name/path and id.
|
||||
"""
|
||||
path = cmds.getAttr(standin + ".dso")
|
||||
|
||||
if path.endswith(".abc"):
|
||||
# Support alembic files directly
|
||||
return get_alembic_ids_cache(path)
|
||||
|
||||
json_path = None
|
||||
for f in os.listdir(os.path.dirname(path)):
|
||||
if f.endswith(".json"):
|
||||
|
|
|
|||
|
|
@ -1,108 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Tools for loading looks to vray proxies."""
|
||||
import os
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
|
||||
import six
|
||||
|
||||
import alembic.Abc
|
||||
from maya import cmds
|
||||
|
||||
from openpype.client import get_last_version_by_subset_name
|
||||
from openpype.pipeline import legacy_io
|
||||
import openpype.hosts.maya.lib as maya_lib
|
||||
from . import lib
|
||||
from .alembic import get_alembic_ids_cache
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_alembic_paths_by_property(filename, attr, verbose=False):
|
||||
# type: (str, str, bool) -> dict
|
||||
"""Return attribute value per objects in the Alembic file.
|
||||
|
||||
Reads an Alembic archive hierarchy and retrieves the
|
||||
value from the `attr` properties on the objects.
|
||||
|
||||
Args:
|
||||
filename (str): Full path to Alembic archive to read.
|
||||
attr (str): Id attribute.
|
||||
verbose (bool): Whether to verbosely log missing attributes.
|
||||
|
||||
Returns:
|
||||
dict: Mapping of node full path with its id
|
||||
|
||||
"""
|
||||
# Normalize alembic path
|
||||
filename = os.path.normpath(filename)
|
||||
filename = filename.replace("\\", "/")
|
||||
filename = str(filename) # path must be string
|
||||
|
||||
try:
|
||||
archive = alembic.Abc.IArchive(filename)
|
||||
except RuntimeError:
|
||||
# invalid alembic file - probably vrmesh
|
||||
log.warning("{} is not an alembic file".format(filename))
|
||||
return {}
|
||||
root = archive.getTop()
|
||||
|
||||
iterator = list(root.children)
|
||||
obj_ids = {}
|
||||
|
||||
for obj in iterator:
|
||||
name = obj.getFullName()
|
||||
|
||||
# include children for coming iterations
|
||||
iterator.extend(obj.children)
|
||||
|
||||
props = obj.getProperties()
|
||||
if props.getNumProperties() == 0:
|
||||
# Skip those without properties, e.g. '/materials' in a gpuCache
|
||||
continue
|
||||
|
||||
# THe custom attribute is under the properties' first container under
|
||||
# the ".arbGeomParams"
|
||||
prop = props.getProperty(0) # get base property
|
||||
|
||||
_property = None
|
||||
try:
|
||||
geo_params = prop.getProperty('.arbGeomParams')
|
||||
_property = geo_params.getProperty(attr)
|
||||
except KeyError:
|
||||
if verbose:
|
||||
log.debug("Missing attr on: {0}".format(name))
|
||||
continue
|
||||
|
||||
if not _property.isConstant():
|
||||
log.warning("Id not constant on: {0}".format(name))
|
||||
|
||||
# Get first value sample
|
||||
value = _property.getValue()[0]
|
||||
|
||||
obj_ids[name] = value
|
||||
|
||||
return obj_ids
|
||||
|
||||
|
||||
def get_alembic_ids_cache(path):
|
||||
# type: (str) -> dict
|
||||
"""Build a id to node mapping in Alembic file.
|
||||
|
||||
Nodes without IDs are ignored.
|
||||
|
||||
Returns:
|
||||
dict: Mapping of id to nodes in the Alembic.
|
||||
|
||||
"""
|
||||
node_ids = get_alembic_paths_by_property(path, attr="cbId")
|
||||
id_nodes = defaultdict(list)
|
||||
for node, _id in six.iteritems(node_ids):
|
||||
id_nodes[_id].append(node)
|
||||
|
||||
return dict(six.iteritems(id_nodes))
|
||||
|
||||
|
||||
def assign_vrayproxy_shaders(vrayproxy, assignments):
|
||||
# type: (str, dict) -> None
|
||||
"""Assign shaders to content of Vray Proxy.
|
||||
|
|
|
|||
|
|
@ -27,11 +27,12 @@ class ExtractWorkfileUrl(pyblish.api.ContextPlugin):
|
|||
rep_name = instance.data.get("representations")[0].get("name")
|
||||
template_data["representation"] = rep_name
|
||||
template_data["ext"] = rep_name
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled["publish"]["path"]
|
||||
template_obj = anatomy.templates_obj["publish"]["path"]
|
||||
template_filled = template_obj.format_strict(template_data)
|
||||
filepath = os.path.normpath(template_filled)
|
||||
self.log.info("Using published scene for render {}".format(
|
||||
filepath))
|
||||
break
|
||||
|
||||
if not filepath:
|
||||
self.log.info("Texture batch doesn't contain workfile.")
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ class UnrealPrelaunchHook(PreLaunchHook):
|
|||
project_name=project_doc["name"]
|
||||
)
|
||||
# Fill templates
|
||||
filled_anatomy = anatomy.format(workdir_data)
|
||||
template_obj = anatomy.templates_obj[workfile_template_key]["file"]
|
||||
|
||||
# Return filename
|
||||
return filled_anatomy[workfile_template_key]["file"]
|
||||
return template_obj.format_strict(workdir_data)
|
||||
|
||||
def exec_plugin_install(self, engine_path: Path, env: dict = None):
|
||||
# set up the QThread and worker with necessary signals
|
||||
|
|
|
|||
|
|
@ -327,7 +327,8 @@ def get_usd_master_path(asset, subset, representation):
|
|||
else:
|
||||
asset_doc = get_asset_by_name(project_name, asset, fields=["name"])
|
||||
|
||||
formatted_result = anatomy.format(
|
||||
template_obj = anatomy.templates_obj["publish"]["path"]
|
||||
path = template_obj.format_strict(
|
||||
{
|
||||
"project": {
|
||||
"name": project_name,
|
||||
|
|
@ -340,7 +341,6 @@ def get_usd_master_path(asset, subset, representation):
|
|||
}
|
||||
)
|
||||
|
||||
path = formatted_result["publish"]["path"]
|
||||
# Remove the version folder
|
||||
subset_folder = os.path.dirname(os.path.dirname(path))
|
||||
master_folder = os.path.join(subset_folder, "master")
|
||||
|
|
|
|||
|
|
@ -534,8 +534,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
template_data["comment"] = None
|
||||
|
||||
anatomy = instance.context.data['anatomy']
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled["publish"]["path"]
|
||||
template_obj = anatomy.templates_obj["publish"]["path"]
|
||||
template_filled = template_obj.format_strict(template_data)
|
||||
file_path = os.path.normpath(template_filled)
|
||||
|
||||
self.log.info("Using published scene for render {}".format(file_path))
|
||||
|
|
|
|||
|
|
@ -1202,10 +1202,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
template_data["family"] = "render"
|
||||
template_data["version"] = version
|
||||
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
|
||||
if "folder" in anatomy.templates["render"]:
|
||||
publish_folder = anatomy_filled["render"]["folder"]
|
||||
render_templates = anatomy.templates_obj["render"]
|
||||
if "folder" in render_templates:
|
||||
publish_folder = render_templates["folder"].format_strict(
|
||||
template_data
|
||||
)
|
||||
else:
|
||||
# solve deprecated situation when `folder` key is not underneath
|
||||
# `publish` anatomy
|
||||
|
|
@ -1215,8 +1216,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
" key underneath `publish` (in global of for project `{}`)."
|
||||
).format(project_name))
|
||||
|
||||
file_path = anatomy_filled["render"]["path"]
|
||||
# Directory
|
||||
file_path = render_templates["path"].format_strict(template_data)
|
||||
publish_folder = os.path.dirname(file_path)
|
||||
|
||||
return publish_folder
|
||||
|
|
|
|||
|
|
@ -463,9 +463,7 @@ def get_workdir_from_session(session=None, template_key=None):
|
|||
session = legacy_io.Session
|
||||
project_name = session["AVALON_PROJECT"]
|
||||
host_name = session["AVALON_APP"]
|
||||
anatomy = Anatomy(project_name)
|
||||
template_data = get_template_data_from_session(session)
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
|
||||
if not template_key:
|
||||
task_type = template_data["task"]["type"]
|
||||
|
|
@ -474,7 +472,10 @@ def get_workdir_from_session(session=None, template_key=None):
|
|||
host_name,
|
||||
project_name=project_name
|
||||
)
|
||||
path = anatomy_filled[template_key]["folder"]
|
||||
|
||||
anatomy = Anatomy(project_name)
|
||||
template_obj = anatomy.templates_obj[template_key]["folder"]
|
||||
path = template_obj.format_strict(template_data)
|
||||
if path:
|
||||
path = os.path.normpath(path)
|
||||
return path
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Functions useful for delivery of published representations."""
|
||||
import os
|
||||
import copy
|
||||
import shutil
|
||||
import glob
|
||||
import clique
|
||||
|
|
@ -146,12 +147,11 @@ def deliver_single_file(
|
|||
report_items["Source file was not found"].append(msg)
|
||||
return report_items, 0
|
||||
|
||||
anatomy_filled = anatomy.format(anatomy_data)
|
||||
if format_dict:
|
||||
template_result = anatomy_filled["delivery"][template_name]
|
||||
delivery_path = template_result.rootless.format(**format_dict)
|
||||
else:
|
||||
delivery_path = anatomy_filled["delivery"][template_name]
|
||||
anatomy_data = copy.deepcopy(anatomy_data)
|
||||
anatomy_data["root"] = format_dict["root"]
|
||||
template_obj = anatomy.templates_obj["delivery"][template_name]
|
||||
delivery_path = template_obj.format_strict(anatomy_data)
|
||||
|
||||
# Backwards compatibility when extension contained `.`
|
||||
delivery_path = delivery_path.replace("..", ".")
|
||||
|
|
@ -269,14 +269,12 @@ def deliver_sequence(
|
|||
|
||||
frame_indicator = "@####@"
|
||||
|
||||
anatomy_data = copy.deepcopy(anatomy_data)
|
||||
anatomy_data["frame"] = frame_indicator
|
||||
anatomy_filled = anatomy.format(anatomy_data)
|
||||
|
||||
if format_dict:
|
||||
template_result = anatomy_filled["delivery"][template_name]
|
||||
delivery_path = template_result.rootless.format(**format_dict)
|
||||
else:
|
||||
delivery_path = anatomy_filled["delivery"][template_name]
|
||||
anatomy_data["root"] = format_dict["root"]
|
||||
template_obj = anatomy.templates_obj["delivery"][template_name]
|
||||
delivery_path = template_obj.format_strict(anatomy_data)
|
||||
|
||||
delivery_path = os.path.normpath(delivery_path.replace("\\", "/"))
|
||||
delivery_folder = os.path.dirname(delivery_path)
|
||||
|
|
|
|||
|
|
@ -132,9 +132,9 @@ def get_workdir_with_workdir_data(
|
|||
project_settings
|
||||
)
|
||||
|
||||
anatomy_filled = anatomy.format(workdir_data)
|
||||
template_obj = anatomy.templates_obj[template_key]["folder"]
|
||||
# Output is TemplateResult object which contain useful data
|
||||
output = anatomy_filled[template_key]["folder"]
|
||||
output = template_obj.format_strict(workdir_data)
|
||||
if output:
|
||||
return output.normalized()
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -83,10 +83,11 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
|
|||
"hierarchy": instance.data["hierarchy"]
|
||||
})
|
||||
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
|
||||
if "folder" in anatomy.templates["publish"]:
|
||||
publish_folder = anatomy_filled["publish"]["folder"]
|
||||
publish_templates = anatomy.templates_obj["publish"]
|
||||
if "folder" in publish_templates:
|
||||
publish_folder = publish_templates["folder"].format_strict(
|
||||
template_data
|
||||
)
|
||||
else:
|
||||
# solve deprecated situation when `folder` key is not underneath
|
||||
# `publish` anatomy
|
||||
|
|
@ -95,8 +96,7 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
|
|||
" key underneath `publish` (in global of for project `{}`)."
|
||||
).format(anatomy.project_name))
|
||||
|
||||
file_path = anatomy_filled["publish"]["path"]
|
||||
# Directory
|
||||
file_path = publish_templates["path"].format_strict(template_data)
|
||||
publish_folder = os.path.dirname(file_path)
|
||||
|
||||
publish_folder = os.path.normpath(publish_folder)
|
||||
|
|
|
|||
|
|
@ -665,8 +665,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
# - template_data (Dict[str, Any]): source data used to fill template
|
||||
# - to add required data to 'repre_context' not used for
|
||||
# formatting
|
||||
# - anatomy_filled (Dict[str, Any]): filled anatomy of last file
|
||||
# - to fill 'publishDir' on instance.data -> not ideal
|
||||
path_template_obj = anatomy.templates_obj[template_name]["path"]
|
||||
|
||||
# Treat template with 'orignalBasename' in special way
|
||||
if "{originalBasename}" in template:
|
||||
|
|
@ -700,8 +699,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
template_data["originalBasename"], _ = os.path.splitext(
|
||||
src_file_name)
|
||||
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
dst = anatomy_filled[template_name]["path"]
|
||||
dst = path_template_obj.format_strict(template_data)
|
||||
src = os.path.join(stagingdir, src_file_name)
|
||||
transfers.append((src, dst))
|
||||
if repre_context is None:
|
||||
|
|
@ -761,8 +759,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
template_data["udim"] = index
|
||||
else:
|
||||
template_data["frame"] = index
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled[template_name]["path"]
|
||||
template_filled = path_template_obj.format_strict(
|
||||
template_data
|
||||
)
|
||||
dst_filepaths.append(template_filled)
|
||||
if repre_context is None:
|
||||
self.log.debug(
|
||||
|
|
@ -798,8 +797,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
if is_udim:
|
||||
template_data["udim"] = repre["udim"][0]
|
||||
# Construct destination filepath from template
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled[template_name]["path"]
|
||||
template_filled = path_template_obj.format_strict(template_data)
|
||||
repre_context = template_filled.used_values
|
||||
dst = os.path.normpath(template_filled)
|
||||
|
||||
|
|
@ -810,11 +808,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
# todo: Are we sure the assumption each representation
|
||||
# ends up in the same folder is valid?
|
||||
if not instance.data.get("publishDir"):
|
||||
instance.data["publishDir"] = (
|
||||
anatomy_filled
|
||||
[template_name]
|
||||
["folder"]
|
||||
)
|
||||
template_obj = anatomy.templates_obj[template_name]["folder"]
|
||||
template_filled = template_obj.format_strict(template_data)
|
||||
instance.data["publishDir"] = template_filled
|
||||
|
||||
for key in self.db_representation_context_keys:
|
||||
# Also add these values to the context even if not used by the
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
|||
))
|
||||
try:
|
||||
src_to_dst_file_paths = []
|
||||
path_template_obj = anatomy.templates_obj[template_key]["path"]
|
||||
for repre_info in published_repres.values():
|
||||
|
||||
# Skip if new repre does not have published repre files
|
||||
|
|
@ -303,9 +304,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
|||
anatomy_data.pop("version", None)
|
||||
|
||||
# Get filled path to repre context
|
||||
anatomy_filled = anatomy.format(anatomy_data)
|
||||
template_filled = anatomy_filled[template_key]["path"]
|
||||
|
||||
template_filled = path_template_obj.format_strict(anatomy_data)
|
||||
repre_data = {
|
||||
"path": str(template_filled),
|
||||
"template": hero_template
|
||||
|
|
@ -343,8 +342,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
|||
# Get head and tail for collection
|
||||
frame_splitter = "_-_FRAME_SPLIT_-_"
|
||||
anatomy_data["frame"] = frame_splitter
|
||||
_anatomy_filled = anatomy.format(anatomy_data)
|
||||
_template_filled = _anatomy_filled[template_key]["path"]
|
||||
_template_filled = path_template_obj.format_strict(
|
||||
anatomy_data
|
||||
)
|
||||
head, tail = _template_filled.split(frame_splitter)
|
||||
padding = int(
|
||||
anatomy.templates[template_key]["frame_padding"]
|
||||
|
|
@ -520,24 +520,24 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
|||
})
|
||||
|
||||
if "folder" in anatomy.templates[template_key]:
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
publish_folder = anatomy_filled[template_key]["folder"]
|
||||
template_obj = anatomy.templates_obj[template_key]["folder"]
|
||||
publish_folder = template_obj.format_strict(template_data)
|
||||
else:
|
||||
# This is for cases of Deprecated anatomy without `folder`
|
||||
# TODO remove when all clients have solved this issue
|
||||
template_data.update({
|
||||
"frame": "FRAME_TEMP",
|
||||
"representation": "TEMP"
|
||||
})
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
# solve deprecated situation when `folder` key is not underneath
|
||||
# `publish` anatomy
|
||||
self.log.warning((
|
||||
"Deprecation warning: Anatomy does not have set `folder`"
|
||||
" key underneath `publish` (in global of for project `{}`)."
|
||||
).format(anatomy.project_name))
|
||||
# solve deprecated situation when `folder` key is not underneath
|
||||
# `publish` anatomy
|
||||
template_data.update({
|
||||
"frame": "FRAME_TEMP",
|
||||
"representation": "TEMP"
|
||||
})
|
||||
template_obj = anatomy.templates_obj[template_key]["path"]
|
||||
file_path = template_obj.format_strict(template_data)
|
||||
|
||||
file_path = anatomy_filled[template_key]["path"]
|
||||
# Directory
|
||||
publish_folder = os.path.dirname(file_path)
|
||||
|
||||
|
|
|
|||
|
|
@ -480,8 +480,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
else:
|
||||
template_data["udim"] = src_padding_exp % i
|
||||
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled[template_name]["path"]
|
||||
template_obj = anatomy.templates_obj[template_name]["path"]
|
||||
template_filled = template_obj.format_strict(template_data)
|
||||
if repre_context is None:
|
||||
repre_context = template_filled.used_values
|
||||
test_dest_files.append(
|
||||
|
|
@ -587,8 +587,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
if repre.get("udim"):
|
||||
template_data["udim"] = repre["udim"][0]
|
||||
src = os.path.join(stagingdir, fname)
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled[template_name]["path"]
|
||||
template_obj = anatomy.templates_obj[template_name]["path"]
|
||||
template_filled = template_obj.format_strict(template_data)
|
||||
repre_context = template_filled.used_values
|
||||
dst = os.path.normpath(template_filled)
|
||||
|
||||
|
|
@ -600,9 +600,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
|
||||
if not instance.data.get("publishDir"):
|
||||
instance.data["publishDir"] = (
|
||||
anatomy_filled
|
||||
[template_name]
|
||||
["folder"]
|
||||
anatomy.templates_obj[template_name]["folder"]
|
||||
.format_strict(template_data)
|
||||
)
|
||||
if repre.get("udim"):
|
||||
repre_context["udim"] = repre.get("udim") # store list
|
||||
|
|
|
|||
|
|
@ -271,9 +271,9 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin):
|
|||
"thumbnail_type": "thumbnail"
|
||||
})
|
||||
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
thumbnail_template = anatomy.templates["publish"]["thumbnail"]
|
||||
template_filled = anatomy_filled["publish"]["thumbnail"]
|
||||
template_obj = anatomy.templates_obj["publish"]["thumbnail"]
|
||||
template_filled = template_obj.format_strict(template_data)
|
||||
thumbnail_template = template_filled.template
|
||||
|
||||
dst_full_path = os.path.normpath(str(template_filled))
|
||||
self.log.debug("Copying file .. {} -> {}".format(
|
||||
|
|
|
|||
|
|
@ -554,7 +554,7 @@
|
|||
"publish_mip_map": true
|
||||
},
|
||||
"CreateAnimation": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"write_color_sets": false,
|
||||
"write_face_sets": false,
|
||||
"include_parent_hierarchy": false,
|
||||
|
|
@ -1459,7 +1459,7 @@
|
|||
]
|
||||
},
|
||||
"reference_loader": {
|
||||
"namespace": "{asset_name}_{subset}_##",
|
||||
"namespace": "{asset_name}_{subset}_##_",
|
||||
"group_name": "_GRP"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -119,9 +119,7 @@
|
|||
"label": "3ds max",
|
||||
"icon": "{}/app_icons/3dsmax.png",
|
||||
"host_name": "max",
|
||||
"environment": {
|
||||
"ADSK_3DSMAX_STARTUPSCRIPTS_ADDON_DIR": "{OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup"
|
||||
},
|
||||
"environment": {},
|
||||
"variants": {
|
||||
"2023": {
|
||||
"use_python_2": false,
|
||||
|
|
@ -133,9 +131,7 @@
|
|||
"linux": []
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [
|
||||
"-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms"
|
||||
],
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1050,8 +1050,8 @@ class ProjectPushItemProcess:
|
|||
repre_format_data["ext"] = ext[1:]
|
||||
break
|
||||
|
||||
tmp_result = anatomy.format(formatting_data)
|
||||
folder_path = tmp_result[template_name]["folder"]
|
||||
template_obj = anatomy.templates_obj[template_name]["folder"]
|
||||
folder_path = template_obj.format_strict(formatting_data)
|
||||
repre_context = folder_path.used_values
|
||||
folder_path_rootless = folder_path.rootless
|
||||
repre_filepaths = []
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ class TextureCopy:
|
|||
"hierarchy": hierarchy
|
||||
}
|
||||
anatomy = Anatomy(project_name)
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
return anatomy_filled['texture']['path']
|
||||
template_obj = anatomy.templates_obj["texture"]["path"]
|
||||
return template_obj.format_strict(template_data)
|
||||
|
||||
def _get_version(self, path):
|
||||
versions = [0]
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ class CommentMatcher(object):
|
|||
temp_data["version"] = "<<version>>"
|
||||
temp_data["ext"] = "<<ext>>"
|
||||
|
||||
formatted = anatomy.format(temp_data)
|
||||
fname_pattern = formatted[template_key]["file"]
|
||||
template_obj = anatomy.templates_obj[template_key]["file"]
|
||||
fname_pattern = template_obj.format_strict(temp_data)
|
||||
fname_pattern = re.escape(fname_pattern)
|
||||
|
||||
# Replace comment and version with something we can match with regex
|
||||
|
|
@ -375,8 +375,8 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
|
||||
data["ext"] = data["ext"].lstrip(".")
|
||||
|
||||
anatomy_filled = self.anatomy.format(data)
|
||||
return anatomy_filled[self.template_key]["file"]
|
||||
template_obj = self.anatomy.templates_obj[self.template_key]["file"]
|
||||
return template_obj.format_strict(data)
|
||||
|
||||
def refresh(self):
|
||||
extensions = list(self._extensions)
|
||||
|
|
|
|||
|
|
@ -238,12 +238,12 @@ For resolution and frame range, use **OpenPype → Set Frame Range** and
|
|||
|
||||
Creating and publishing rigs with OpenPype follows similar workflow as with
|
||||
other data types. Create your rig and mark parts of your hierarchy in sets to
|
||||
help OpenPype validators and extractors to check it and publish it.
|
||||
help OpenPype validators and extractors to check and publish it.
|
||||
|
||||
### Preparing rig for publish
|
||||
|
||||
When creating rigs, it is recommended (and it is in fact enforced by validators)
|
||||
to separate bones or driving objects, their controllers and geometry so they are
|
||||
to separate bones or driven objects, their controllers and geometry so they are
|
||||
easily managed. Currently OpenPype doesn't allow to publish model at the same time as
|
||||
its rig so for demonstration purposes, I'll first create simple model for robotic
|
||||
arm, just made out of simple boxes and I'll publish it.
|
||||
|
|
@ -252,41 +252,48 @@ arm, just made out of simple boxes and I'll publish it.
|
|||
|
||||
For more information about publishing models, see [Publishing models](artist_hosts_maya.md#publishing-models).
|
||||
|
||||
Now lets start with empty scene. Load your model - **OpenPype → Load...**, right
|
||||
Now let's start with empty scene. Load your model - **OpenPype → Load...**, right
|
||||
click on it and select **Reference (abc)**.
|
||||
|
||||
I've created few bones and their controllers in two separate
|
||||
groups - `rig_GRP` and `controls_GRP`. Naming is not important - just adhere to
|
||||
your naming conventions.
|
||||
I've created a few bones in `rig_GRP`, their controllers in `controls_GRP` and
|
||||
placed the rig's output geometry in `geometry_GRP`. Naming of the groups is not important - just adhere to
|
||||
your naming conventions. Then I parented everything into a single top group named `arm_rig`.
|
||||
|
||||
Then I've put everything into `arm_rig` group.
|
||||
|
||||
When you've prepared your hierarchy, it's time to create *Rig instance* in OpenPype.
|
||||
Select your whole rig hierarchy and go **OpenPype → Create...**. Select **Rig**.
|
||||
Set is created in your scene to mark rig parts for export. Notice that it has
|
||||
two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET`
|
||||
With the prepared hierarchy it is time to create a *Rig instance* in OpenPype.
|
||||
Select the top group of your rig and go to **OpenPype → Create...**. Select **Rig**.
|
||||
A publish set for your rig is created in your scene to mark rig parts for export.
|
||||
Notice that it has two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET`
|
||||
and geometry to `out_SET`. You should end up with something like this:
|
||||
|
||||

|
||||
|
||||
:::note controls_SET and out_SET contents
|
||||
It is totally allowed to put the `geometry_GRP` in the `out_SET` as opposed to
|
||||
the individual meshes - it's even **recommended**. However, the `controls_SET`
|
||||
requires the individual controls in it that the artist is supposed to animate
|
||||
and manipulate so the publish validators can accurately check the rig's
|
||||
controls.
|
||||
:::
|
||||
|
||||
### Publishing rigs
|
||||
|
||||
Publishing rig is done in same way as publishing everything else. Save your scene
|
||||
and go **OpenPype → Publish**. When you run validation you'll mostly run at first into
|
||||
few issues. Although number of them will seem to be intimidating at first, you'll
|
||||
find out they are mostly minor things easily fixed.
|
||||
Publishing rigs is done in a same way as publishing everything else. Save your scene
|
||||
and go **OpenPype → Publish**. When you run validation you'll most likely run into
|
||||
a few issues at first. Although a number of them will seem to be intimidating you
|
||||
will find out they are mostly minor things, easily fixed and are there to optimize
|
||||
your rig for consistency and safe usage by the artist.
|
||||
|
||||
* **Non Duplicate Instance Members (ID)** - This will most likely fail because when
|
||||
- **Non Duplicate Instance Members (ID)** - This will most likely fail because when
|
||||
creating rigs, we usually duplicate few parts of it to reuse them. But duplication
|
||||
will duplicate also ID of original object and OpenPype needs every object to have
|
||||
unique ID. This is easily fixed by **Repair** action next to validator name. click
|
||||
on little up arrow on right side of validator name and select **Repair** form menu.
|
||||
|
||||
* **Joints Hidden** - This is enforcing joints (bones) to be hidden for user as
|
||||
- **Joints Hidden** - This is enforcing joints (bones) to be hidden for user as
|
||||
animator usually doesn't need to see them and they clutter his viewports. So
|
||||
well behaving rig should have them hidden. **Repair** action will help here also.
|
||||
|
||||
* **Rig Controllers** will check if there are no transforms on unlocked attributes
|
||||
- **Rig Controllers** will check if there are no transforms on unlocked attributes
|
||||
of controllers. This is needed because animator should have ease way to reset rig
|
||||
to it's default position. It also check that those attributes doesn't have any
|
||||
incoming connections from other parts of scene to ensure that published rig doesn't
|
||||
|
|
@ -297,6 +304,19 @@ have any missing dependencies.
|
|||
You can load rig with [Loader](artist_tools_loader). Go **OpenPype → Load...**,
|
||||
select your rig, right click on it and **Reference** it.
|
||||
|
||||
### Animation instances
|
||||
|
||||
Whenever you load a rig an animation publish instance is automatically created
|
||||
for it. This means that if you load a rig you don't need to create a pointcache
|
||||
instance yourself to publish the geometry. This is all cleanly prepared for you
|
||||
when loading a published rig.
|
||||
|
||||
:::tip Missing animation instance for your loaded rig?
|
||||
Did you accidentally delete the animation instance for a loaded rig? You can
|
||||
recreate it using the [**Recreate rig animation instance**](artist_hosts_maya.md#recreate-rig-animation-instance)
|
||||
inventory action.
|
||||
:::
|
||||
|
||||
## Point caches
|
||||
OpenPype is using Alembic format for point caches. Workflow is very similar as
|
||||
other data types.
|
||||
|
|
@ -646,3 +666,15 @@ Select 1 container of type `animation` or `pointcache`, then 1+ container of any
|
|||
The action searches the selected containers for 1 animation container of type `animation` or `pointcache`. This animation container will be connected to the rest of the selected containers. Matching geometries between containers is done by comparing the attribute `cbId`.
|
||||
|
||||
The connection between geometries is done with a live blendshape.
|
||||
|
||||
### Recreate rig animation instance
|
||||
|
||||
This action can regenerate an animation instance for a loaded rig, for example
|
||||
for when it was accidentally deleted by the user.
|
||||
|
||||

|
||||
|
||||
#### Usage
|
||||
|
||||
Select 1 or more container of type `rig` for which you want to recreate the
|
||||
animation instance.
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Loading…
Add table
Add a link
Reference in a new issue