Merge branch 'develop' into feature/better_instance_changes

# Conflicts:
#	openpype/pipeline/create/context.py
This commit is contained in:
Jakub Trllo 2023-02-02 10:36:20 +01:00
commit dbda359b9d
90 changed files with 1481 additions and 347 deletions

View file

@ -9,4 +9,4 @@ repos:
- id: check-yaml
- id: check-added-large-files
- id: no-commit-to-branch
args: [ '--pattern', '^(?!((enhancement|feature|bugfix|documentation|tests|local|chore)\/[a-zA-Z0-9\-]+)$).*' ]
args: [ '--pattern', '^(?!((release|enhancement|feature|bugfix|documentation|tests|local|chore)\/[a-zA-Z0-9\-]+)$).*' ]

View file

@ -1,3 +1,4 @@
import os
import logging
import contextlib
from abc import ABCMeta, abstractproperty
@ -100,6 +101,30 @@ class HostBase(object):
pass
def get_current_project_name(self):
"""
Returns:
Union[str, None]: Current project name.
"""
return os.environ.get("AVALON_PROJECT")
def get_current_asset_name(self):
"""
Returns:
Union[str, None]: Current asset name.
"""
return os.environ.get("AVALON_ASSET")
def get_current_task_name(self):
"""
Returns:
Union[str, None]: Current task name.
"""
return os.environ.get("AVALON_ASSET")
def get_current_context(self):
"""Get current context information.
@ -111,19 +136,14 @@ class HostBase(object):
Default implementation returns values from 'legacy_io.Session'.
Returns:
dict: Context with 3 keys 'project_name', 'asset_name' and
'task_name'. All of them can be 'None'.
Dict[str, Union[str, None]]: Context with 3 keys 'project_name',
'asset_name' and 'task_name'. All of them can be 'None'.
"""
from openpype.pipeline import legacy_io
if legacy_io.is_installed():
legacy_io.install()
return {
"project_name": legacy_io.Session["AVALON_PROJECT"],
"asset_name": legacy_io.Session["AVALON_ASSET"],
"task_name": legacy_io.Session["AVALON_TASK"]
"project_name": self.get_current_project_name(),
"asset_name": self.get_current_asset_name(),
"task_name": self.get_current_task_name()
}
def get_context_title(self):

View file

@ -1,6 +1,6 @@
import json
import pyblish.api
from openpype.hosts.aftereffects.api import list_instances
from openpype.hosts.aftereffects.api import AfterEffectsHost
class PreCollectRender(pyblish.api.ContextPlugin):
@ -25,7 +25,7 @@ class PreCollectRender(pyblish.api.ContextPlugin):
self.log.debug("Not applicable for New Publisher, skip")
return
for inst in list_instances():
for inst in AfterEffectsHost().list_instances():
if inst.get("creator_attributes"):
raise ValueError("Instance created in New publisher, "
"cannot be published in Pyblish.\n"

View file

@ -19,7 +19,6 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
hosts = ["blender"]
families = ["camera"]
version = (0, 1, 0)
label = "Zero Keyframe"
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]

View file

@ -14,7 +14,6 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
hosts = ["blender"]
families = ["model"]
category = "geometry"
label = "Mesh Has UV's"
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
optional = True

View file

@ -14,7 +14,6 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator):
order = ValidateContentsOrder
hosts = ["blender"]
families = ["model"]
category = "geometry"
label = "Mesh No Negative Scale"
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]

View file

@ -19,7 +19,6 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
hosts = ["blender"]
families = ["model", "rig"]
version = (0, 1, 0)
label = "No Colons in names"
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]

View file

@ -21,7 +21,6 @@ class ValidateTransformZero(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
hosts = ["blender"]
families = ["model"]
version = (0, 1, 0)
label = "Transform Zero"
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]

View file

@ -1,5 +1,4 @@
import pyblish.api
import argparse
import sys
from pprint import pformat
@ -11,20 +10,40 @@ class CollectCelactionCliKwargs(pyblish.api.Collector):
order = pyblish.api.Collector.order - 0.1
def process(self, context):
parser = argparse.ArgumentParser(prog="celaction")
parser.add_argument("--currentFile",
help="Pass file to Context as `currentFile`")
parser.add_argument("--chunk",
help=("Render chanks on farm"))
parser.add_argument("--frameStart",
help=("Start of frame range"))
parser.add_argument("--frameEnd",
help=("End of frame range"))
parser.add_argument("--resolutionWidth",
help=("Width of resolution"))
parser.add_argument("--resolutionHeight",
help=("Height of resolution"))
passing_kwargs = parser.parse_args(sys.argv[1:]).__dict__
args = list(sys.argv[1:])
self.log.info(str(args))
missing_kwargs = []
passing_kwargs = {}
for key in (
"chunk",
"frameStart",
"frameEnd",
"resolutionWidth",
"resolutionHeight",
"currentFile",
):
arg_key = f"--{key}"
if arg_key not in args:
missing_kwargs.append(key)
continue
arg_idx = args.index(arg_key)
args.pop(arg_idx)
if key != "currentFile":
value = args.pop(arg_idx)
else:
path_parts = []
while arg_idx < len(args):
path_parts.append(args.pop(arg_idx))
value = " ".join(path_parts).strip('"')
passing_kwargs[key] = value
if missing_kwargs:
raise RuntimeError("Missing arguments {}".format(
", ".join(
[f'"{key}"' for key in missing_kwargs]
)
))
self.log.info("Storing kwargs ...")
self.log.debug("_ passing_kwargs: {}".format(pformat(passing_kwargs)))

View file

@ -144,13 +144,20 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
"""
obj_network = hou.node("/obj")
op_ctx = obj_network.createNode(
"null", node_name="OpenPypeContext")
op_ctx = obj_network.createNode("null", node_name="OpenPypeContext")
# A null in houdini by default comes with content inside to visualize
# the null. However since we explicitly want to hide the node lets
# remove the content and disable the display flag of the node
for node in op_ctx.children():
node.destroy()
op_ctx.moveToGoodPosition()
op_ctx.setBuiltExplicitly(False)
op_ctx.setCreatorState("OpenPype")
op_ctx.setComment("OpenPype node to hold context metadata")
op_ctx.setColor(hou.Color((0.081, 0.798, 0.810)))
op_ctx.setDisplayFlag(False)
op_ctx.hide(True)
return op_ctx

View file

@ -103,9 +103,8 @@ class HoudiniCreatorBase(object):
fill it with all collected instances from the scene under its
respective creator identifiers.
If legacy instances are detected in the scene, create
`houdini_cached_legacy_subsets` there and fill it with
all legacy subsets under family as a key.
Create `houdini_cached_legacy_subsets` key for any legacy instances
detected in the scene as instances per family.
Args:
Dict[str, Any]: Shared data.
@ -115,29 +114,30 @@ class HoudiniCreatorBase(object):
"""
if shared_data.get("houdini_cached_subsets") is None:
shared_data["houdini_cached_subsets"] = {}
if shared_data.get("houdini_cached_legacy_subsets") is None:
shared_data["houdini_cached_legacy_subsets"] = {}
cached_instances = lsattr("id", "pyblish.avalon.instance")
for i in cached_instances:
if not i.parm("creator_identifier"):
# we have legacy instance
family = i.parm("family").eval()
if family not in shared_data[
"houdini_cached_legacy_subsets"]:
shared_data["houdini_cached_legacy_subsets"][
family] = [i]
else:
shared_data[
"houdini_cached_legacy_subsets"][family].append(i)
continue
cache = dict()
cache_legacy = dict()
for node in lsattr("id", "pyblish.avalon.instance"):
creator_identifier_parm = node.parm("creator_identifier")
if creator_identifier_parm:
# creator instance
creator_id = creator_identifier_parm.eval()
cache.setdefault(creator_id, []).append(node)
creator_id = i.parm("creator_identifier").eval()
if creator_id not in shared_data["houdini_cached_subsets"]:
shared_data["houdini_cached_subsets"][creator_id] = [i]
else:
shared_data[
"houdini_cached_subsets"][creator_id].append(i) # noqa
# legacy instance
family_parm = node.parm("family")
if not family_parm:
# must be a broken instance
continue
family = family_parm.eval()
cache_legacy.setdefault(family, []).append(node)
shared_data["houdini_cached_subsets"] = cache
shared_data["houdini_cached_legacy_subsets"] = cache_legacy
return shared_data
@staticmethod

View file

@ -14,3 +14,10 @@ class MaxAddon(OpenPypeModule, IHostAddon):
def get_workfile_extensions(self):
return [".max"]
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []
return [
os.path.join(MAX_HOST_DIR, "hooks")
]

View file

@ -119,7 +119,7 @@ class OpenPypeMenu(object):
def manage_callback(self):
"""Callback to show Scene Manager/Inventory tool."""
host_tools.show_subset_manager(parent=self.main_widget)
host_tools.show_scene_inventory(parent=self.main_widget)
def library_callback(self):
"""Callback to show Library Loader tool."""

View file

@ -2,6 +2,7 @@
"""Pipeline tools for OpenPype Houdini integration."""
import os
import logging
from operator import attrgetter
import json
@ -141,5 +142,25 @@ def ls() -> list:
if rt.getUserProp(obj, "id") == AVALON_CONTAINER_ID
]
for container in sorted(containers, key=lambda name: container.name):
for container in sorted(containers, key=attrgetter("name")):
yield lib.read(container)
def containerise(name: str, nodes: list, context, loader=None, suffix="_CON"):
data = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": name,
"namespace": "",
"loader": loader,
"representation": context["representation"]["_id"],
}
container_name = f"{name}{suffix}"
container = rt.container(name=container_name)
for node in nodes:
node.Parent = container
if not lib.imprint(container_name, data):
print(f"imprinting of {container_name} failed.")
return container

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""Pre-launch hook to inject python environment."""
from openpype.lib import PreLaunchHook
import os
class InjectPythonPath(PreLaunchHook):
"""Inject OpenPype environment to 3dsmax.
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"]
def execute(self):
self.launch_context.env["MAX_PYTHONPATH"] = os.environ["PYTHONPATH"]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating camera."""
from openpype.hosts.max.api import plugin
from openpype.pipeline import CreatedInstance
class CreateCamera(plugin.MaxCreator):
identifier = "io.openpype.creators.max.camera"
label = "Camera"
family = "camera"
icon = "gear"
def create(self, subset_name, instance_data, pre_create_data):
from pymxs import runtime as rt
sel_obj = list(rt.selection)
instance = super(CreateCamera, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
container = rt.getNodeByName(instance.data.get("instance_node"))
# TODO: Disable "Add to Containers?" Panel
# parent the selected cameras into the container
for obj in sel_obj:
obj.parent = container
# for additional work on the node:
# instance_node = rt.getNodeByName(instance.get("instance_node"))

View file

@ -0,0 +1,49 @@
import os
from openpype.pipeline import (
load
)
class FbxLoader(load.LoaderPlugin):
"""Fbx Loader"""
families = ["camera"]
representations = ["fbx"]
order = -9
icon = "code-fork"
color = "white"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
filepath = os.path.normpath(self.fname)
fbx_import_cmd = (
f"""
FBXImporterSetParam "Animation" true
FBXImporterSetParam "Cameras" true
FBXImporterSetParam "AxisConversionMethod" true
FbxExporterSetParam "UpAxis" "Y"
FbxExporterSetParam "Preserveinstances" true
importFile @"{filepath}" #noPrompt using:FBXIMP
""")
self.log.debug(f"Executing command: {fbx_import_cmd}")
rt.execute(fbx_import_cmd)
container_name = f"{name}_CON"
asset = rt.getNodeByName(f"{name}")
# rename the container with "_CON"
container = rt.container(name=container_name)
asset.Parent = container
return container
def remove(self, container):
from pymxs import runtime as rt
node = container["node"]
rt.delete(node)

View file

@ -0,0 +1,50 @@
import os
from openpype.pipeline import (
load
)
class MaxSceneLoader(load.LoaderPlugin):
"""Max Scene Loader"""
families = ["camera"]
representations = ["max"]
order = -8
icon = "code-fork"
color = "green"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
path = os.path.normpath(self.fname)
# import the max scene by using "merge file"
path = path.replace('\\', '/')
merge_before = {
c for c in rt.rootNode.Children
if rt.classOf(c) == rt.Container
}
rt.mergeMaxFile(path)
merge_after = {
c for c in rt.rootNode.Children
if rt.classOf(c) == rt.Container
}
max_containers = merge_after.difference(merge_before)
if len(max_containers) != 1:
self.log.error("Something failed when loading.")
max_container = max_containers.pop()
container_name = f"{name}_CON"
# rename the container with "_CON"
# get the original container
container = rt.container(name=container_name)
max_container.Parent = container
return container
def remove(self, container):
from pymxs import runtime as rt
node = container["node"]
rt.delete(node)

View file

@ -6,14 +6,19 @@ Because of limited api, alembics can be only loaded, but not easily updated.
"""
import os
from openpype.pipeline import (
load
load, get_representation_path
)
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
class AbcLoader(load.LoaderPlugin):
"""Alembic loader."""
families = ["model", "animation", "pointcache"]
families = ["model",
"camera",
"animation",
"pointcache"]
label = "Load Alembic"
representations = ["abc"]
order = -10
@ -52,14 +57,47 @@ importFile @"{file_path}" #noPrompt
abc_container = abc_containers.pop()
container_name = f"{name}_CON"
container = rt.container(name=container_name)
abc_container.Parent = container
return containerise(
name, [abc_container], context, loader=self.__class__.__name__)
return container
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
alembic_objects = self.get_container_children(node, "AlembicObject")
for alembic_object in alembic_objects:
alembic_object.source = path
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
def switch(self, container, representation):
self.update(container, representation)
def remove(self, container):
from pymxs import runtime as rt
node = container["node"]
rt.delete(node)
@staticmethod
def get_container_children(parent, type_name):
from pymxs import runtime as rt
def list_children(node):
children = []
for c in node.Children:
children.append(c)
children += list_children(c)
return children
filtered = []
for child in list_children(parent):
class_type = str(rt.classOf(child.baseObject))
if class_type == type_name:
filtered.append(child)
return filtered

View file

@ -0,0 +1,75 @@
import os
import pyblish.api
from openpype.pipeline import (
publish,
OptionalPyblishPluginMixin
)
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection,
get_all_children
)
class ExtractCameraAlembic(publish.Extractor,
OptionalPyblishPluginMixin):
"""
Extract Camera with AlembicExport
"""
order = pyblish.api.ExtractorOrder - 0.1
label = "Extract Alembic Camera"
hosts = ["max"]
families = ["camera"]
optional = True
def process(self, instance):
if not self.is_active(instance.data):
return
start = float(instance.data.get("frameStartHandle", 1))
end = float(instance.data.get("frameEndHandle", 1))
container = instance.data["instance_node"]
self.log.info("Extracting Camera ...")
stagingdir = self.staging_dir(instance)
filename = "{name}.abc".format(**instance.data)
path = os.path.join(stagingdir, filename)
# We run the render
self.log.info("Writing alembic '%s' to '%s'" % (filename,
stagingdir))
export_cmd = (
f"""
AlembicExport.ArchiveType = #ogawa
AlembicExport.CoordinateSystem = #maya
AlembicExport.StartFrame = {start}
AlembicExport.EndFrame = {end}
AlembicExport.CustomAttributes = true
exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport
""")
self.log.debug(f"Executing command: {export_cmd}")
with maintained_selection():
# select and export
rt.select(get_all_children(rt.getNodeByName(container)))
rt.execute(export_cmd)
self.log.info("Performing Extraction ...")
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'abc',
'ext': 'abc',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
path))

View file

@ -0,0 +1,75 @@
import os
import pyblish.api
from openpype.pipeline import (
publish,
OptionalPyblishPluginMixin
)
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection,
get_all_children
)
class ExtractCameraFbx(publish.Extractor,
OptionalPyblishPluginMixin):
"""
Extract Camera with FbxExporter
"""
order = pyblish.api.ExtractorOrder - 0.2
label = "Extract Fbx Camera"
hosts = ["max"]
families = ["camera"]
optional = True
def process(self, instance):
if not self.is_active(instance.data):
return
container = instance.data["instance_node"]
self.log.info("Extracting Camera ...")
stagingdir = self.staging_dir(instance)
filename = "{name}.fbx".format(**instance.data)
filepath = os.path.join(stagingdir, filename)
self.log.info("Writing fbx file '%s' to '%s'" % (filename,
filepath))
# Need to export:
# Animation = True
# Cameras = True
# AxisConversionMethod
fbx_export_cmd = (
f"""
FBXExporterSetParam "Animation" true
FBXExporterSetParam "Cameras" true
FBXExporterSetParam "AxisConversionMethod" "Animation"
FbxExporterSetParam "UpAxis" "Y"
FbxExporterSetParam "Preserveinstances" true
exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP
""")
self.log.debug(f"Executing command: {fbx_export_cmd}")
with maintained_selection():
# select and export
rt.select(get_all_children(rt.getNodeByName(container)))
rt.execute(fbx_export_cmd)
self.log.info("Performing Extraction ...")
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'fbx',
'ext': 'fbx',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
filepath))

View file

@ -0,0 +1,60 @@
import os
import pyblish.api
from openpype.pipeline import (
publish,
OptionalPyblishPluginMixin
)
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection,
get_all_children
)
class ExtractMaxSceneRaw(publish.Extractor,
OptionalPyblishPluginMixin):
"""
Extract Raw Max Scene with SaveSelected
"""
order = pyblish.api.ExtractorOrder - 0.2
label = "Extract Max Scene (Raw)"
hosts = ["max"]
families = ["camera"]
optional = True
def process(self, instance):
if not self.is_active(instance.data):
return
container = instance.data["instance_node"]
# publish the raw scene for camera
self.log.info("Extracting Raw Max Scene ...")
stagingdir = self.staging_dir(instance)
filename = "{name}.max".format(**instance.data)
max_path = os.path.join(stagingdir, filename)
self.log.info("Writing max file '%s' to '%s'" % (filename,
max_path))
if "representations" not in instance.data:
instance.data["representations"] = []
# saving max scene
with maintained_selection():
# need to figure out how to select the camera
rt.select(get_all_children(rt.getNodeByName(container)))
rt.execute(f'saveNodes selection "{max_path}" quiet:true')
self.log.info("Performing Extraction ...")
representation = {
'name': 'max',
'ext': 'max',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
max_path))

View file

@ -51,7 +51,7 @@ class ExtractAlembic(publish.Extractor):
order = pyblish.api.ExtractorOrder
label = "Extract Pointcache"
hosts = ["max"]
families = ["pointcache", "camera"]
families = ["pointcache"]
def process(self, instance):
start = float(instance.data.get("frameStartHandle", 1))

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
from pymxs import runtime as rt
class ValidateCameraContent(pyblish.api.InstancePlugin):
"""Validates Camera instance contents.
A Camera instance may only hold a SINGLE camera's transform
"""
order = pyblish.api.ValidatorOrder
families = ["camera"]
hosts = ["max"]
label = "Camera Contents"
camera_type = ["$Free_Camera", "$Target_Camera",
"$Physical_Camera", "$Target"]
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError("Camera instance must only include"
"camera (and camera target)")
def get_invalid(self, instance):
"""
Get invalid nodes if the instance is not camera
"""
invalid = list()
container = instance.data["instance_node"]
self.log.info("Validating look content for "
"{}".format(container))
con = rt.getNodeByName(container)
selection_list = list(con.Children)
for sel in selection_list:
# to avoid Attribute Error from pymxs wrapper
sel_tmp = str(sel)
found = False
for cam in self.camera_type:
if sel_tmp.startswith(cam):
found = True
break
if not found:
self.log.error("Camera not found")
invalid.append(sel)
return invalid

View file

@ -2,8 +2,11 @@
(
local sysPath = dotNetClass "System.IO.Path"
local sysDir = dotNetClass "System.IO.Directory"
local localScript = getThisScriptFilename()
local localScript = getThisScriptFilename()
local startup = sysPath.Combine (sysPath.GetDirectoryName localScript) "startup.py"
local pythonpath = systemTools.getEnvVariable "MAX_PYTHONPATH"
systemTools.setEnvVariable "PYTHONPATH" pythonpath
python.ExecuteFile startup
)

View file

@ -254,11 +254,6 @@ def read(node):
return data
def _get_mel_global(name):
"""Return the value of a mel global variable"""
return mel.eval("$%s = $%s;" % (name, name))
def matrix_equals(a, b, tolerance=1e-10):
"""
Compares two matrices with an imperfection tolerance
@ -624,15 +619,15 @@ class delete_after(object):
cmds.delete(self._nodes)
def get_current_renderlayer():
return cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True)
def get_renderer(layer):
with renderlayer(layer):
return cmds.getAttr("defaultRenderGlobals.currentRenderer")
def get_current_renderlayer():
return cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True)
@contextlib.contextmanager
def no_undo(flush=False):
"""Disable the undo queue during the context
@ -1373,27 +1368,6 @@ def set_id(node, unique_id, overwrite=False):
cmds.setAttr(attr, unique_id, type="string")
# endregion ID
def get_reference_node(path):
"""
Get the reference node when the path is found being used in a reference
Args:
path (str): the file path to check
Returns:
node (str): name of the reference node in question
"""
try:
node = cmds.file(path, query=True, referenceNode=True)
except RuntimeError:
log.debug('File is not referenced : "{}"'.format(path))
return
reference_path = cmds.referenceQuery(path, filename=True)
if os.path.normpath(path) == os.path.normpath(reference_path):
return node
def set_attribute(attribute, value, node):
"""Adjust attributes based on the value from the attribute data

View file

@ -1132,6 +1132,7 @@ class RenderProductsRenderman(ARenderProducts):
"""
renderer = "renderman"
unmerged_aovs = {"PxrCryptomatte"}
def get_render_products(self):
"""Get all AOVs.
@ -1181,6 +1182,17 @@ class RenderProductsRenderman(ARenderProducts):
if not display_types.get(display["driverNode"]["type"]):
continue
has_cryptomatte = cmds.ls(type=self.unmerged_aovs)
matte_enabled = False
if has_cryptomatte:
for cryptomatte in has_cryptomatte:
cryptomatte_aov = cryptomatte
matte_name = "cryptomatte"
rman_globals = cmds.listConnections(cryptomatte +
".message")
if rman_globals:
matte_enabled = True
aov_name = name
if aov_name == "rmanDefaultDisplay":
aov_name = "beauty"
@ -1199,6 +1211,15 @@ class RenderProductsRenderman(ARenderProducts):
camera=camera,
multipart=True
)
if has_cryptomatte and matte_enabled:
cryptomatte = RenderProduct(
productName=matte_name,
aov=cryptomatte_aov,
ext=extensions,
camera=camera,
multipart=True
)
else:
# this code should handle the case where no multipart
# capable format is selected. But since it involves
@ -1218,6 +1239,9 @@ class RenderProductsRenderman(ARenderProducts):
products.append(product)
if has_cryptomatte and matte_enabled:
products.append(cryptomatte)
return products
def get_files(self, product):

View file

@ -22,17 +22,25 @@ class RenderSettings(object):
_image_prefix_nodes = {
'vray': 'vraySettings.fileNamePrefix',
'arnold': 'defaultRenderGlobals.imageFilePrefix',
'renderman': 'defaultRenderGlobals.imageFilePrefix',
'renderman': 'rmanGlobals.imageFileFormat',
'redshift': 'defaultRenderGlobals.imageFilePrefix'
}
_image_prefixes = {
'vray': get_current_project_settings()["maya"]["RenderSettings"]["vray_renderer"]["image_prefix"], # noqa
'arnold': get_current_project_settings()["maya"]["RenderSettings"]["arnold_renderer"]["image_prefix"], # noqa
'renderman': '<Scene>/<layer>/<layer>{aov_separator}<aov>',
'renderman': get_current_project_settings()["maya"]["RenderSettings"]["renderman_renderer"]["image_prefix"], # noqa
'redshift': get_current_project_settings()["maya"]["RenderSettings"]["redshift_renderer"]["image_prefix"] # noqa
}
# Renderman only
_image_dir = {
'renderman': get_current_project_settings()["maya"]["RenderSettings"]["renderman_renderer"]["image_dir"], # noqa
'cryptomatte': get_current_project_settings()["maya"]["RenderSettings"]["renderman_renderer"]["cryptomatte_dir"], # noqa
'imageDisplay': get_current_project_settings()["maya"]["RenderSettings"]["renderman_renderer"]["imageDisplay_dir"], # noqa
"watermark": get_current_project_settings()["maya"]["RenderSettings"]["renderman_renderer"]["watermark_dir"] # noqa
}
_aov_chars = {
"dot": ".",
"dash": "-",
@ -81,7 +89,6 @@ class RenderSettings(object):
prefix, type="string") # noqa
else:
print("{0} isn't a supported renderer to autoset settings.".format(renderer)) # noqa
# TODO: handle not having res values in the doc
width = asset_doc["data"].get("resolutionWidth")
height = asset_doc["data"].get("resolutionHeight")
@ -97,6 +104,13 @@ class RenderSettings(object):
self._set_redshift_settings(width, height)
mel.eval("redshiftUpdateActiveAovList")
if renderer == "renderman":
image_dir = self._image_dir["renderman"]
cmds.setAttr("rmanGlobals.imageOutputDir",
image_dir, type="string")
self._set_renderman_settings(width, height,
aov_separator)
def _set_arnold_settings(self, width, height):
"""Sets settings for Arnold."""
from mtoa.core import createOptions # noqa
@ -202,6 +216,66 @@ class RenderSettings(object):
cmds.setAttr("defaultResolution.height", height)
self._additional_attribs_setter(additional_options)
def _set_renderman_settings(self, width, height, aov_separator):
"""Sets settings for Renderman"""
rman_render_presets = (
self._project_settings
["maya"]
["RenderSettings"]
["renderman_renderer"]
)
display_filters = rman_render_presets["display_filters"]
d_filters_number = len(display_filters)
for i in range(d_filters_number):
d_node = cmds.ls(typ=display_filters[i])
if len(d_node) > 0:
filter_nodes = d_node[0]
else:
filter_nodes = cmds.createNode(display_filters[i])
cmds.connectAttr(filter_nodes + ".message",
"rmanGlobals.displayFilters[%i]" % i,
force=True)
if filter_nodes.startswith("PxrImageDisplayFilter"):
imageDisplay_dir = self._image_dir["imageDisplay"]
imageDisplay_dir = imageDisplay_dir.replace("{aov_separator}",
aov_separator)
cmds.setAttr(filter_nodes + ".filename",
imageDisplay_dir, type="string")
sample_filters = rman_render_presets["sample_filters"]
s_filters_number = len(sample_filters)
for n in range(s_filters_number):
s_node = cmds.ls(typ=sample_filters[n])
if len(s_node) > 0:
filter_nodes = s_node[0]
else:
filter_nodes = cmds.createNode(sample_filters[n])
cmds.connectAttr(filter_nodes + ".message",
"rmanGlobals.sampleFilters[%i]" % n,
force=True)
if filter_nodes.startswith("PxrCryptomatte"):
matte_dir = self._image_dir["cryptomatte"]
matte_dir = matte_dir.replace("{aov_separator}",
aov_separator)
cmds.setAttr(filter_nodes + ".filename",
matte_dir, type="string")
elif filter_nodes.startswith("PxrWatermarkFilter"):
watermark_dir = self._image_dir["watermark"]
watermark_dir = watermark_dir.replace("{aov_separator}",
aov_separator)
cmds.setAttr(filter_nodes + ".filename",
watermark_dir, type="string")
additional_options = rman_render_presets["additional_options"]
self._set_global_output_settings()
cmds.setAttr("defaultResolution.width", width)
cmds.setAttr("defaultResolution.height", height)
self._additional_attribs_setter(additional_options)
def _set_vray_settings(self, aov_separator, width, height):
# type: (str, int, int) -> None
"""Sets important settings for Vray."""

View file

@ -50,7 +50,6 @@ def install():
parent="MayaWindow"
)
renderer = cmds.getAttr('defaultRenderGlobals.currentRenderer').lower()
# Create context menu
context_label = "{}, {}".format(
legacy_io.Session["AVALON_ASSET"],

View file

@ -514,6 +514,9 @@ def check_lock_on_current_file():
# add the lock file when opening the file
filepath = current_file()
# Skip if current file is 'untitled'
if not filepath:
return
if is_workfile_locked(filepath):
# add lockfile dialog
@ -680,10 +683,12 @@ def before_workfile_save(event):
def after_workfile_save(event):
workfile_name = event["filename"]
if handle_workfile_locks():
if workfile_name:
if not is_workfile_locked(workfile_name):
create_workfile_lock(workfile_name)
if (
handle_workfile_locks()
and workfile_name
and not is_workfile_locked(workfile_name)
):
create_workfile_lock(workfile_name)
class MayaDirmap(HostDirmap):

View file

@ -12,7 +12,6 @@ class CollectMayaWorkspace(pyblish.api.ContextPlugin):
label = "Maya Workspace"
hosts = ['maya']
version = (0, 1, 0)
def process(self, context):
workspace = cmds.workspace(rootDirectory=True, query=True)

View file

@ -58,23 +58,23 @@ class ValidateAttributes(pyblish.api.ContextPlugin):
# Filter families.
families = [instance.data["family"]]
families += instance.data.get("families", [])
families = list(set(families) & set(self.attributes.keys()))
families = list(set(families) & set(cls.attributes.keys()))
if not families:
continue
# Get all attributes to validate.
attributes = {}
for family in families:
for preset in self.attributes[family]:
for preset in cls.attributes[family]:
[node_name, attribute_name] = preset.split(".")
try:
attributes[node_name].update(
{attribute_name: self.attributes[family][preset]}
{attribute_name: cls.attributes[family][preset]}
)
except KeyError:
attributes.update({
node_name: {
attribute_name: self.attributes[family][preset]
attribute_name: cls.attributes[family][preset]
}
})

View file

@ -19,7 +19,6 @@ class ValidateColorSets(pyblish.api.Validator):
order = ValidateMeshOrder
hosts = ['maya']
families = ['model']
category = 'geometry'
label = 'Mesh ColorSets'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]

View file

@ -11,10 +11,6 @@ from openpype.pipeline.publish import (
)
def float_round(num, places=0, direction=ceil):
return direction(num * (10**places)) / float(10**places)
class ValidateMayaUnits(pyblish.api.ContextPlugin):
"""Check if the Maya units are set correct"""
@ -36,6 +32,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin):
# Collected units
linearunits = context.data.get('linearUnits')
angularunits = context.data.get('angularUnits')
# TODO(antirotor): This is hack as for framerates having multiple
# decimal places. FTrack is ceiling decimal values on
# fps to two decimal places but Maya 2019+ is reporting those fps
@ -43,7 +40,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin):
# rounding, we have to round those numbers coming from Maya.
# NOTE: this must be revisited yet again as it seems that Ftrack is
# now flooring the value?
fps = float_round(context.data.get('fps'), 2, ceil)
fps = mayalib.float_round(context.data.get('fps'), 2, ceil)
# TODO repace query with using 'context.data["assetEntity"]'
asset_doc = get_current_project_asset()

View file

@ -19,7 +19,6 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin):
order = ValidateMeshOrder
hosts = ["maya"]
families = ["model"]
category = "geometry"
label = "Mesh Arnold Attributes"
actions = [
openpype.hosts.maya.api.action.SelectInvalidAction,

View file

@ -48,7 +48,6 @@ class ValidateMeshHasUVs(pyblish.api.InstancePlugin):
order = ValidateMeshOrder
hosts = ['maya']
families = ['model']
category = 'geometry'
label = 'Mesh Has UVs'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
optional = True

View file

@ -15,8 +15,6 @@ class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin):
order = ValidateMeshOrder
hosts = ['maya']
families = ['model']
category = 'geometry'
version = (0, 1, 0)
label = 'Mesh Lamina Faces'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]

View file

@ -19,8 +19,6 @@ class ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin):
order = ValidateMeshOrder
families = ['model']
hosts = ['maya']
category = 'geometry'
version = (0, 1, 0)
label = 'Mesh Edge Length Non Zero'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
optional = True

View file

@ -20,8 +20,6 @@ class ValidateMeshNormalsUnlocked(pyblish.api.Validator):
order = ValidateMeshOrder
hosts = ['maya']
families = ['model']
category = 'geometry'
version = (0, 1, 0)
label = 'Mesh Normals Unlocked'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]

View file

@ -235,7 +235,6 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin):
order = ValidateMeshOrder
hosts = ['maya']
families = ['model']
category = 'geometry'
label = 'Mesh Has Overlapping UVs'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
optional = True

View file

@ -21,9 +21,7 @@ class ValidateMeshSingleUVSet(pyblish.api.InstancePlugin):
order = ValidateMeshOrder
hosts = ['maya']
families = ['model', 'pointcache']
category = 'uv'
optional = True
version = (0, 1, 0)
label = "Mesh Single UV Set"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]

View file

@ -63,7 +63,6 @@ class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin):
order = ValidateMeshOrder
hosts = ['maya']
families = ['model']
category = 'geometry'
label = 'Mesh Vertices Have Edges'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]

View file

@ -16,7 +16,6 @@ class ValidateNoDefaultCameras(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
hosts = ['maya']
families = ['camera']
version = (0, 1, 0)
label = "No Default Cameras"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]

View file

@ -23,8 +23,6 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
hosts = ['maya']
families = ['model']
category = 'cleanup'
version = (0, 1, 0)
label = 'No Namespaces'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]

View file

@ -43,8 +43,6 @@ class ValidateNoNullTransforms(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
hosts = ['maya']
families = ['model']
category = 'cleanup'
version = (0, 1, 0)
label = 'No Empty/Null Transforms'
actions = [RepairAction,
openpype.hosts.maya.api.action.SelectInvalidAction]

View file

@ -24,7 +24,7 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin):
validate_path = (
instance.context.data["project_settings"]["maya"]["publish"]
)
file_attr = validate_path["ValidatePathForPlugin"]["attribute"]
file_attr = validate_path["ValidatePluginPathAttributes"]["attribute"]
if not file_attr:
return invalid
@ -39,7 +39,7 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin):
filepath = cmds.getAttr(file_attr)
if filepath and not os.path.exists(filepath):
self.log.error("File {0} not exists".format(filepath)) # noqa
self.log.error("File {0} not exists".format(filepath)) # noqa
invalid.append(target)
return invalid

View file

@ -24,7 +24,6 @@ class ValidateRigJointsHidden(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
hosts = ['maya']
families = ['rig']
version = (0, 1, 0)
label = "Joints Hidden"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]

View file

@ -31,8 +31,6 @@ class ValidateSceneSetWorkspace(pyblish.api.ContextPlugin):
order = ValidatePipelineOrder
hosts = ['maya']
category = 'scene'
version = (0, 1, 0)
label = 'Maya Workspace Set'
def process(self, context):

View file

@ -38,9 +38,7 @@ class ValidateShapeDefaultNames(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
hosts = ['maya']
families = ['model']
category = 'cleanup'
optional = True
version = (0, 1, 0)
label = "Shape Default Naming"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]

View file

@ -32,9 +32,7 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
hosts = ['maya']
families = ['model']
category = 'cleanup'
optional = True
version = (0, 1, 0)
label = 'Suffix Naming Conventions'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
SUFFIX_NAMING_TABLE = {"mesh": ["_GEO", "_GES", "_GEP", "_OSD"],

View file

@ -18,8 +18,6 @@ class ValidateTransformZero(pyblish.api.Validator):
order = ValidateContentsOrder
hosts = ["maya"]
families = ["model"]
category = "geometry"
version = (0, 1, 0)
label = "Transform Zero (Freeze)"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]

View file

@ -13,7 +13,6 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin):
order = ValidateMeshOrder
hosts = ["maya"]
families = ["staticMesh"]
category = "geometry"
label = "Mesh is Triangulated"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
active = False

View file

@ -1,10 +1,12 @@
import os
import shutil
import pyblish.api
import clique
import nuke
from openpype.pipeline import publish
from openpype.lib import collect_frames
class NukeRenderLocal(publish.ExtractorColormanaged):
@ -13,6 +15,8 @@ class NukeRenderLocal(publish.ExtractorColormanaged):
Extract the result of savers by starting a comp render
This will run the local render of Fusion.
Allows to use last published frames and overwrite only specific ones
(set in instance.data.get("frames_to_fix"))
"""
order = pyblish.api.ExtractorOrder
@ -21,7 +25,6 @@ class NukeRenderLocal(publish.ExtractorColormanaged):
families = ["render.local", "prerender.local", "still.local"]
def process(self, instance):
families = instance.data["families"]
child_nodes = (
instance.data.get("transientData", {}).get("childNodes")
or instance
@ -32,17 +35,16 @@ class NukeRenderLocal(publish.ExtractorColormanaged):
if x.Class() == "Write":
node = x
self.log.debug("instance collected: {}".format(instance.data))
node_subset_name = instance.data.get("name", None)
first_frame = instance.data.get("frameStartHandle", None)
last_frame = instance.data.get("frameEndHandle", None)
node_subset_name = instance.data["subset"]
self.log.info("Starting render")
self.log.info("Start frame: {}".format(first_frame))
self.log.info("End frame: {}".format(last_frame))
filenames = []
node_file = node["file"]
# Collecte expected filepaths for each frame
# Collect expected filepaths for each frame
# - for cases that output is still image is first created set of
# paths which is then sorted and converted to list
expected_paths = list(sorted({
@ -50,22 +52,37 @@ class NukeRenderLocal(publish.ExtractorColormanaged):
for frame in range(first_frame, last_frame + 1)
}))
# Extract only filenames for representation
filenames = [
filenames.extend([
os.path.basename(filepath)
for filepath in expected_paths
]
])
# Ensure output directory exists.
out_dir = os.path.dirname(expected_paths[0])
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# Render frames
nuke.execute(
str(node_subset_name),
int(first_frame),
int(last_frame)
)
frames_to_render = [(first_frame, last_frame)]
frames_to_fix = instance.data.get("frames_to_fix")
if instance.data.get("last_version_published_files") and frames_to_fix:
frames_to_render = self._get_frames_to_render(frames_to_fix)
anatomy = instance.context.data["anatomy"]
self._copy_last_published(anatomy, instance, out_dir,
filenames)
for render_first_frame, render_last_frame in frames_to_render:
self.log.info("Starting render")
self.log.info("Start frame: {}".format(render_first_frame))
self.log.info("End frame: {}".format(render_last_frame))
# Render frames
nuke.execute(
str(node_subset_name),
int(render_first_frame),
int(render_last_frame)
)
ext = node["file_type"].value()
colorspace = node["colorspace"].value()
@ -106,6 +123,7 @@ class NukeRenderLocal(publish.ExtractorColormanaged):
out_dir
))
families = instance.data["families"]
# redefinition of families
if "render.local" in families:
instance.data['family'] = 'render'
@ -133,3 +151,58 @@ class NukeRenderLocal(publish.ExtractorColormanaged):
self.log.info('Finished render')
self.log.debug("_ instance.data: {}".format(instance.data))
def _copy_last_published(self, anatomy, instance, out_dir,
expected_filenames):
"""Copies last published files to temporary out_dir.
These are base of files which will be extended/fixed for specific
frames.
Renames published file to expected file name based on frame, eg.
test_project_test_asset_subset_v005.1001.exr > new_render.1001.exr
"""
last_published = instance.data["last_version_published_files"]
last_published_and_frames = collect_frames(last_published)
expected_and_frames = collect_frames(expected_filenames)
frames_and_expected = {v: k for k, v in expected_and_frames.items()}
for file_path, frame in last_published_and_frames.items():
file_path = anatomy.fill_root(file_path)
if not os.path.exists(file_path):
continue
target_file_name = frames_and_expected.get(frame)
if not target_file_name:
continue
out_path = os.path.join(out_dir, target_file_name)
self.log.debug("Copying '{}' -> '{}'".format(file_path, out_path))
shutil.copy(file_path, out_path)
# TODO shouldn't this be uncommented
# instance.context.data["cleanupFullPaths"].append(out_path)
def _get_frames_to_render(self, frames_to_fix):
"""Return list of frame range tuples to render
Args:
frames_to_fix (str): specific or range of frames to be rerendered
(1005, 1009-1010)
Returns:
(list): [(1005, 1005), (1009-1010)]
"""
frames_to_render = []
for frame_range in frames_to_fix.split(","):
if frame_range.isdigit():
render_first_frame = frame_range
render_last_frame = frame_range
elif '-' in frame_range:
frames = frame_range.split('-')
render_first_frame = int(frames[0])
render_last_frame = int(frames[1])
else:
raise ValueError("Wrong format of frames to fix {}"
.format(frames_to_fix))
frames_to_render.append((render_first_frame,
render_last_frame))
return frames_to_render

View file

@ -30,7 +30,7 @@ from .vendor_bin_utils import (
)
from .attribute_definitions import (
AbtractAttrDef,
AbstractAttrDef,
UIDef,
UISeparatorDef,
@ -246,7 +246,7 @@ __all__ = [
"get_ffmpeg_tool_path",
"is_oiio_supported",
"AbtractAttrDef",
"AbstractAttrDef",
"UIDef",
"UISeparatorDef",

View file

@ -20,7 +20,7 @@ def register_attr_def_class(cls):
Currently are registered definitions used to deserialize data to objects.
Attrs:
cls (AbtractAttrDef): Non-abstract class to be registered with unique
cls (AbstractAttrDef): Non-abstract class to be registered with unique
'type' attribute.
Raises:
@ -36,7 +36,7 @@ def get_attributes_keys(attribute_definitions):
"""Collect keys from list of attribute definitions.
Args:
attribute_definitions (List[AbtractAttrDef]): Objects of attribute
attribute_definitions (List[AbstractAttrDef]): Objects of attribute
definitions.
Returns:
@ -57,8 +57,8 @@ def get_default_values(attribute_definitions):
"""Receive default values for attribute definitions.
Args:
attribute_definitions (List[AbtractAttrDef]): Attribute definitions for
which default values should be collected.
attribute_definitions (List[AbstractAttrDef]): Attribute definitions
for which default values should be collected.
Returns:
Dict[str, Any]: Default values for passet attribute definitions.
@ -76,15 +76,15 @@ def get_default_values(attribute_definitions):
class AbstractAttrDefMeta(ABCMeta):
"""Meta class to validate existence of 'key' attribute.
"""Metaclass to validate existence of 'key' attribute.
Each object of `AbtractAttrDef` mus have defined 'key' attribute.
Each object of `AbstractAttrDef` mus have defined 'key' attribute.
"""
def __call__(self, *args, **kwargs):
obj = super(AbstractAttrDefMeta, self).__call__(*args, **kwargs)
init_class = getattr(obj, "__init__class__", None)
if init_class is not AbtractAttrDef:
if init_class is not AbstractAttrDef:
raise TypeError("{} super was not called in __init__.".format(
type(obj)
))
@ -92,7 +92,7 @@ class AbstractAttrDefMeta(ABCMeta):
@six.add_metaclass(AbstractAttrDefMeta)
class AbtractAttrDef(object):
class AbstractAttrDef(object):
"""Abstraction of attribute definiton.
Each attribute definition must have implemented validation and
@ -145,7 +145,7 @@ class AbtractAttrDef(object):
self.disabled = disabled
self._id = uuid.uuid4().hex
self.__init__class__ = AbtractAttrDef
self.__init__class__ = AbstractAttrDef
@property
def id(self):
@ -154,7 +154,15 @@ class AbtractAttrDef(object):
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return self.key == other.key
return (
self.key == other.key
and self.hidden == other.hidden
and self.default == other.default
and self.disabled == other.disabled
)
def __ne__(self, other):
return not self.__eq__(other)
@abstractproperty
def type(self):
@ -212,7 +220,7 @@ class AbtractAttrDef(object):
# UI attribute definitoins won't hold value
# -----------------------------------------
class UIDef(AbtractAttrDef):
class UIDef(AbstractAttrDef):
is_value_def = False
def __init__(self, key=None, default=None, *args, **kwargs):
@ -237,7 +245,7 @@ class UILabelDef(UIDef):
# Attribute defintioins should hold value
# ---------------------------------------
class UnknownDef(AbtractAttrDef):
class UnknownDef(AbstractAttrDef):
"""Definition is not known because definition is not available.
This attribute can be used to keep existing data unchanged but does not
@ -254,7 +262,7 @@ class UnknownDef(AbtractAttrDef):
return value
class HiddenDef(AbtractAttrDef):
class HiddenDef(AbstractAttrDef):
"""Hidden value of Any type.
This attribute can be used for UI purposes to pass values related
@ -274,7 +282,7 @@ class HiddenDef(AbtractAttrDef):
return value
class NumberDef(AbtractAttrDef):
class NumberDef(AbstractAttrDef):
"""Number definition.
Number can have defined minimum/maximum value and decimal points. Value
@ -350,7 +358,7 @@ class NumberDef(AbtractAttrDef):
return round(float(value), self.decimals)
class TextDef(AbtractAttrDef):
class TextDef(AbstractAttrDef):
"""Text definition.
Text can have multiline option so endline characters are allowed regex
@ -415,7 +423,7 @@ class TextDef(AbtractAttrDef):
return data
class EnumDef(AbtractAttrDef):
class EnumDef(AbstractAttrDef):
"""Enumeration of single item from items.
Args:
@ -457,7 +465,7 @@ class EnumDef(AbtractAttrDef):
return self.default
def serialize(self):
data = super(TextDef, self).serialize()
data = super(EnumDef, self).serialize()
data["items"] = copy.deepcopy(self.items)
return data
@ -523,7 +531,8 @@ class EnumDef(AbtractAttrDef):
return output
class BoolDef(AbtractAttrDef):
class BoolDef(AbstractAttrDef):
"""Boolean representation.
Args:
@ -768,7 +777,7 @@ class FileDefItem(object):
return output
class FileDef(AbtractAttrDef):
class FileDef(AbstractAttrDef):
"""File definition.
It is possible to define filters of allowed file extensions and if supports
folders.
@ -886,7 +895,7 @@ def serialize_attr_def(attr_def):
"""Serialize attribute definition to data.
Args:
attr_def (AbtractAttrDef): Attribute definition to serialize.
attr_def (AbstractAttrDef): Attribute definition to serialize.
Returns:
Dict[str, Any]: Serialized data.
@ -899,7 +908,7 @@ def serialize_attr_defs(attr_defs):
"""Serialize attribute definitions to data.
Args:
attr_defs (List[AbtractAttrDef]): Attribute definitions to serialize.
attr_defs (List[AbstractAttrDef]): Attribute definitions to serialize.
Returns:
List[Dict[str, Any]]: Serialized data.

View file

@ -189,6 +189,6 @@ class FileTransaction(object):
def _same_paths(self, src, dst):
# handles same paths but with C:/project vs c:/project
if os.path.exists(src) and os.path.exists(dst):
return os.path.samefile(src, dst)
return os.stat(src) == os.stat(dst)
return src == dst

View file

@ -86,6 +86,12 @@ from .context_tools import (
registered_host,
deregister_host,
get_process_id,
get_current_context,
get_current_host_name,
get_current_project_name,
get_current_asset_name,
get_current_task_name,
)
install = install_host
uninstall = uninstall_host
@ -176,6 +182,13 @@ __all__ = (
"register_host",
"registered_host",
"deregister_host",
"get_process_id",
"get_current_context",
"get_current_host_name",
"get_current_project_name",
"get_current_asset_name",
"get_current_task_name",
# Backwards compatible function names
"install",

View file

@ -344,9 +344,9 @@ def get_imageio_config(
imageio_global, imageio_host = _get_imageio_settings(
project_settings, host_name)
config_host = imageio_host["ocio_config"]
config_host = imageio_host.get("ocio_config", {})
if config_host["enabled"]:
if config_host.get("enabled"):
config_data = _get_config_data(
config_host["filepath"], anatomy_data
)
@ -438,13 +438,14 @@ def get_imageio_file_rules(project_name, host_name, project_settings=None):
# get file rules from global and host_name
frules_global = imageio_global["file_rules"]
frules_host = imageio_host["file_rules"]
# host is optional, some might not have any settings
frules_host = imageio_host.get("file_rules", {})
# compile file rules dictionary
file_rules = {}
if frules_global["enabled"]:
file_rules.update(frules_global["rules"])
if frules_host["enabled"]:
if frules_host and frules_host["enabled"]:
file_rules.update(frules_host["rules"])
return file_rules
@ -455,7 +456,7 @@ def _get_imageio_settings(project_settings, host_name):
Args:
project_settings (dict): project settings.
Defaults to None.
Defaults to None.
host_name (str): host name
Returns:
@ -463,6 +464,7 @@ def _get_imageio_settings(project_settings, host_name):
"""
# get image io from global and host_name
imageio_global = project_settings["global"]["imageio"]
imageio_host = project_settings[host_name]["imageio"]
# host is optional, some might not have any settings
imageio_host = project_settings.get(host_name, {}).get("imageio", {})
return imageio_global, imageio_host

View file

@ -11,6 +11,7 @@ import pyblish.api
from pyblish.lib import MessageHandler
import openpype
from openpype.host import HostBase
from openpype.client import (
get_project,
get_asset_by_id,
@ -306,6 +307,58 @@ def debug_host():
return host
def get_current_host_name():
"""Current host name.
Function is based on currently registered host integration or environment
variant 'AVALON_APP'.
Returns:
Union[str, None]: Name of host integration in current process or None.
"""
host = registered_host()
if isinstance(host, HostBase):
return host.name
return os.environ.get("AVALON_APP")
def get_global_context():
return {
"project_name": os.environ.get("AVALON_PROJECT"),
"asset_name": os.environ.get("AVALON_ASSET"),
"task_name": os.environ.get("AVALON_TASK"),
}
def get_current_context():
host = registered_host()
if isinstance(host, HostBase):
return host.get_current_context()
return get_global_context()
def get_current_project_name():
host = registered_host()
if isinstance(host, HostBase):
return host.get_current_project_name()
return get_global_context()["project_name"]
def get_current_asset_name():
host = registered_host()
if isinstance(host, HostBase):
return host.get_current_asset_name()
return get_global_context()["asset_name"]
def get_current_task_name():
host = registered_host()
if isinstance(host, HostBase):
return host.get_current_task_name()
return get_global_context()["task_name"]
def get_current_project(fields=None):
"""Helper function to get project document based on global Session.
@ -316,7 +369,7 @@ def get_current_project(fields=None):
None: Project is not set.
"""
project_name = legacy_io.active_project()
project_name = get_current_project_name()
return get_project(project_name, fields=fields)
@ -341,12 +394,12 @@ def get_current_project_asset(asset_name=None, asset_id=None, fields=None):
None: Asset is not set or not exist.
"""
project_name = legacy_io.active_project()
project_name = get_current_project_name()
if asset_id:
return get_asset_by_id(project_name, asset_id, fields=fields)
if not asset_name:
asset_name = legacy_io.Session.get("AVALON_ASSET")
asset_name = get_current_asset_name()
# Skip if is not set even on context
if not asset_name:
return None
@ -363,7 +416,7 @@ def is_representation_from_latest(representation):
bool: Whether the representation is of latest version.
"""
project_name = legacy_io.active_project()
project_name = get_current_project_name()
return version_is_latest(project_name, representation["parent"])

View file

@ -13,6 +13,11 @@ from openpype.settings import (
get_system_settings,
get_project_settings
)
from openpype.lib.attribute_definitions import (
UnknownDef,
serialize_attr_defs,
deserialize_attr_defs,
)
from openpype.host import IPublishHost
from openpype.pipeline import legacy_io
from openpype.pipeline.mongodb import (
@ -28,6 +33,7 @@ from .creator_plugins import (
CreatorError,
)
# Changes of instances and context are send as tuple of 2 information
UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"])
@ -153,7 +159,7 @@ class CreatorsRemoveFailed(CreatorsOperationFailed):
class CreatorsCreateFailed(CreatorsOperationFailed):
def __init__(self, failed_info):
msg = "Faled to create instances"
msg = "Failed to create instances"
super(CreatorsCreateFailed, self).__init__(
msg, failed_info
)
@ -329,14 +335,12 @@ class AttributeValues(object):
Has dictionary like methods. Not all of them are allowed all the time.
Args:
attr_defs(AbtractAttrDef): Defintions of value type and properties.
attr_defs(AbstractAttrDef): Defintions of value type and properties.
values(dict): Values after possible conversion.
origin_data(dict): Values loaded from host before conversion.
"""
def __init__(self, attr_defs, values, origin_data=None):
from openpype.lib.attribute_definitions import UnknownDef
if origin_data is None:
origin_data = copy.deepcopy(values)
self._origin_data = origin_data
@ -409,15 +413,25 @@ class AttributeValues(object):
@property
def attr_defs(self):
"""Pointer to attribute definitions."""
return self._attr_defs
"""Pointer to attribute definitions.
Returns:
List[AbstractAttrDef]: Attribute definitions.
"""
return list(self._attr_defs)
@property
def origin_data(self):
return copy.deepcopy(self._origin_data)
def data_to_store(self):
"""Create new dictionary with data to store."""
"""Create new dictionary with data to store.
Returns:
Dict[str, Any]: Attribute values that should be stored.
"""
output = {}
for key in self._data:
output[key] = self[key]
@ -427,6 +441,15 @@ class AttributeValues(object):
output[key] = attr_def.default
return output
def get_serialized_attr_defs(self):
"""Serialize attribute definitions to json serializable types.
Returns:
List[Dict[str, Any]]: Serialized attribute definitions.
"""
return serialize_attr_defs(self._attr_defs)
class CreatorAttributeValues(AttributeValues):
"""Creator specific attribute values of an instance.
@ -464,13 +487,14 @@ class PublishAttributes:
"""Wrapper for publish plugin attribute definitions.
Cares about handling attribute definitions of multiple publish plugins.
Keep information about attribute definitions and their values.
Args:
parent(CreatedInstance, CreateContext): Parent for which will be
data stored and from which are data loaded.
origin_data(dict): Loaded data by plugin class name.
attr_plugins(list): List of publish plugins that may have defined
attribute definitions.
attr_plugins(Union[List[pyblish.api.Plugin], None]): List of publish
plugins that may have defined attribute definitions.
"""
def __init__(self, parent, origin_data, attr_plugins=None):
@ -584,6 +608,42 @@ class PublishAttributes:
self, [], value, value
)
def serialize_attributes(self):
return {
"attr_defs": {
plugin_name: attrs_value.get_serialized_attr_defs()
for plugin_name, attrs_value in self._data.items()
},
"plugin_names_order": self._plugin_names_order,
"missing_plugins": self._missing_plugins
}
def deserialize_attributes(self, data):
self._plugin_names_order = data["plugin_names_order"]
self._missing_plugins = data["missing_plugins"]
attr_defs = deserialize_attr_defs(data["attr_defs"])
origin_data = self._origin_data
data = self._data
self._data = {}
added_keys = set()
for plugin_name, attr_defs_data in attr_defs.items():
attr_defs = deserialize_attr_defs(attr_defs_data)
value = data.get(plugin_name) or {}
orig_value = copy.deepcopy(origin_data.get(plugin_name) or {})
self._data[plugin_name] = PublishAttributeValues(
self, attr_defs, value, orig_value
)
for key, value in data.items():
if key not in added_keys:
self._missing_plugins.append(key)
self._data[key] = PublishAttributeValues(
self, [], value, value
)
class CreatedInstance:
"""Instance entity with data that will be stored to workfile.
@ -592,15 +652,22 @@ class CreatedInstance:
about instance like "asset" and "task" and all data used for filling subset
name as creators may have custom data for subset name filling.
Notes:
Object have 2 possible initialization. One using 'creator' object which
is recommended for api usage. Second by passing information about
creator.
Args:
family(str): Name of family that will be created.
subset_name(str): Name of subset that will be created.
data(dict): Data used for filling subset name or override data from
already existing instance.
creator(BaseCreator): Creator responsible for instance.
host(ModuleType): Host implementation loaded with
`openpype.pipeline.registered_host`.
new(bool): Is instance new.
family (str): Name of family that will be created.
subset_name (str): Name of subset that will be created.
data (Dict[str, Any]): Data used for filling subset name or override
data from already existing instance.
creator (Union[BaseCreator, None]): Creator responsible for instance.
creator_identifier (str): Identifier of creator plugin.
creator_label (str): Creator plugin label.
group_label (str): Default group label from creator plugin.
creator_attr_defs (List[AbstractAttrDef]): Attribute definitions from
creator.
"""
# Keys that can't be changed or removed from data after loading using
@ -617,9 +684,24 @@ class CreatedInstance:
)
def __init__(
self, family, subset_name, data, creator, new=True
self,
family,
subset_name,
data,
creator=None,
creator_identifier=None,
creator_label=None,
group_label=None,
creator_attr_defs=None,
):
self.creator = creator
if creator is not None:
creator_identifier = creator.identifier
group_label = creator.get_group_label()
creator_label = creator.label
creator_attr_defs = creator.get_instance_attr_defs()
self._creator_label = creator_label
self._group_label = group_label or creator_identifier
# Instance members may have actions on them
# TODO implement members logic
@ -649,7 +731,7 @@ class CreatedInstance:
self._data["family"] = family
self._data["subset"] = subset_name
self._data["active"] = data.get("active", True)
self._data["creator_identifier"] = creator.identifier
self._data["creator_identifier"] = creator_identifier
# Pop from source data all keys that are defined in `_data` before
# this moment and through their values away
@ -663,10 +745,12 @@ class CreatedInstance:
# Stored creator specific attribute values
# {key: value}
creator_values = copy.deepcopy(orig_creator_attributes)
creator_attr_defs = creator.get_instance_attr_defs()
self._data["creator_attributes"] = CreatorAttributeValues(
self, creator_attr_defs, creator_values, orig_creator_attributes
self,
list(creator_attr_defs),
creator_values,
orig_creator_attributes
)
# Stored publish specific attribute values
@ -751,7 +835,7 @@ class CreatedInstance:
label = self._data.get("group")
if label:
return label
return self.creator.get_group_label()
return self._group_label
@property
def origin_data(self):
@ -759,60 +843,19 @@ class CreatedInstance:
@property
def creator_identifier(self):
return self.creator.identifier
return self._data["creator_identifier"]
@property
def creator_label(self):
return self.creator.label or self.creator_identifier
@property
def create_context(self):
return self.creator.create_context
@property
def host(self):
return self.create_context.host
@property
def has_set_asset(self):
"""Asset name is set in data."""
return "asset" in self._data
@property
def has_set_task(self):
"""Task name is set in data."""
return "task" in self._data
@property
def has_valid_context(self):
"""Context data are valid for publishing."""
return self.has_valid_asset and self.has_valid_task
@property
def has_valid_asset(self):
"""Asset set in context exists in project."""
if not self.has_set_asset:
return False
return self._asset_is_valid
@property
def has_valid_task(self):
"""Task set in context exists in project."""
if not self.has_set_task:
return False
return self._task_is_valid
def set_asset_invalid(self, invalid):
# TODO replace with `set_asset_name`
self._asset_is_valid = not invalid
def set_task_invalid(self, invalid):
# TODO replace with `set_task_name`
self._task_is_valid = not invalid
return self._creator_label or self.creator_identifier
@property
def id(self):
"""Instance identifier."""
"""Instance identifier.
Returns:
str: UUID of instance.
"""
return self._data["instance_id"]
@ -821,6 +864,10 @@ class CreatedInstance:
"""Legacy access to data.
Access to data is needed to modify values.
Returns:
CreatedInstance: Object can be used as dictionary but with
validations of immutable keys.
"""
return self
@ -875,6 +922,12 @@ class CreatedInstance:
@property
def creator_attribute_defs(self):
"""Attribute defintions defined by creator plugin.
Returns:
List[AbstractAttrDef]: Attribute defitions.
"""
return self.creator_attributes.attr_defs
@property
@ -886,7 +939,7 @@ class CreatedInstance:
It is possible to recreate the instance using these data.
Todo:
Todos:
We probably don't need OrderedDict. When data are loaded they
are not ordered anymore.
@ -907,7 +960,15 @@ class CreatedInstance:
@classmethod
def from_existing(cls, instance_data, creator):
"""Convert instance data from workfile to CreatedInstance."""
"""Convert instance data from workfile to CreatedInstance.
Args:
instance_data (Dict[str, Any]): Data in a structure ready for
'CreatedInstance' object.
creator (Creator): Creator plugin which is creating the instance
of for which the instance belong.
"""
instance_data = copy.deepcopy(instance_data)
family = instance_data.get("family", None)
@ -916,26 +977,49 @@ class CreatedInstance:
subset_name = instance_data.get("subset", None)
return cls(
family, subset_name, instance_data, creator, new=False
family, subset_name, instance_data, creator
)
def set_publish_plugins(self, attr_plugins):
"""Set publish plugins with attribute definitions.
This method should be called only from 'CreateContext'.
Args:
attr_plugins (List[pyblish.api.Plugin]): Pyblish plugins which
inherit from 'OpenPypePyblishPluginMixin' and may contain
attribute definitions.
"""
self.publish_attributes.set_publish_plugins(attr_plugins)
def add_members(self, members):
"""Currently unused method."""
for member in members:
if member not in self._members:
self._members.append(member)
def serialize_for_remote(self):
"""Serialize object into data to be possible recreated object.
Returns:
Dict[str, Any]: Serialized data.
"""
creator_attr_defs = self.creator_attributes.get_serialized_attr_defs()
publish_attributes = self.publish_attributes.serialize_attributes()
return {
"data": self.data_to_store(),
"orig_data": copy.deepcopy(self._orig_data)
"orig_data": copy.deepcopy(self._orig_data),
"creator_attr_defs": creator_attr_defs,
"publish_attributes": publish_attributes,
"creator_label": self._creator_label,
"group_label": self._group_label,
}
@classmethod
def deserialize_on_remote(cls, serialized_data, creator_items):
def deserialize_on_remote(cls, serialized_data):
"""Convert instance data to CreatedInstance.
This is fake instance in remote process e.g. in UI process. The creator
@ -945,27 +1029,78 @@ class CreatedInstance:
Args:
serialized_data (Dict[str, Any]): Serialized data for remote
recreating. Should contain 'data' and 'orig_data'.
creator_items (Dict[str, Any]): Mapping of creator identifier and
objects that behave like a creator for most of attribute
access.
"""
instance_data = copy.deepcopy(serialized_data["data"])
creator_identifier = instance_data["creator_identifier"]
creator_item = creator_items[creator_identifier]
family = instance_data.get("family", None)
if family is None:
family = creator_item.family
family = instance_data["family"]
subset_name = instance_data.get("subset", None)
creator_label = serialized_data["creator_label"]
group_label = serialized_data["group_label"]
creator_attr_defs = deserialize_attr_defs(
serialized_data["creator_attr_defs"]
)
publish_attributes = serialized_data["publish_attributes"]
obj = cls(
family, subset_name, instance_data, creator_item, new=False
family,
subset_name,
instance_data,
creator_identifier=creator_identifier,
creator_label=creator_label,
group_label=group_label,
creator_attributes=creator_attr_defs
)
obj._orig_data = serialized_data["orig_data"]
obj.publish_attributes.deserialize_attributes(publish_attributes)
return obj
# Context validation related methods/properties
@property
def has_set_asset(self):
"""Asset name is set in data."""
return "asset" in self._data
@property
def has_set_task(self):
"""Task name is set in data."""
return "task" in self._data
@property
def has_valid_context(self):
"""Context data are valid for publishing."""
return self.has_valid_asset and self.has_valid_task
@property
def has_valid_asset(self):
"""Asset set in context exists in project."""
if not self.has_set_asset:
return False
return self._asset_is_valid
@property
def has_valid_task(self):
"""Task set in context exists in project."""
if not self.has_set_task:
return False
return self._task_is_valid
def set_asset_invalid(self, invalid):
# TODO replace with `set_asset_name`
self._asset_is_valid = not invalid
def set_task_invalid(self, invalid):
# TODO replace with `set_task_name`
self._task_is_valid = not invalid
class ConvertorItem(object):
"""Item representing convertor plugin.
@ -1004,6 +1139,10 @@ class CreateContext:
Context itself also can store data related to whole creation (workfile).
- those are mainly for Context publish plugins
Todos:
Don't use 'AvalonMongoDB'. It's used only to keep track about current
context which should be handled by host.
Args:
host(ModuleType): Host implementation which handles implementation and
global metadata.
@ -1404,7 +1543,7 @@ class CreateContext:
self._instances_by_id[instance.id] = instance
# Prepare publish plugin attributes and set it on instance
attr_plugins = self._get_publish_plugins_with_attr_for_family(
instance.creator.family
instance.family
)
instance.set_publish_plugins(attr_plugins)

View file

@ -425,8 +425,8 @@ class BaseCreator:
keys/values when plugin attributes change.
Returns:
List[AbtractAttrDef]: Attribute definitions that can be tweaked for
created instance.
List[AbstractAttrDef]: Attribute definitions that can be tweaked
for created instance.
"""
return self.instance_attr_defs
@ -563,8 +563,8 @@ class Creator(BaseCreator):
updating keys/values when plugin attributes change.
Returns:
List[AbtractAttrDef]: Attribute definitions that can be tweaked for
created instance.
List[AbstractAttrDef]: Attribute definitions that can be tweaked
for created instance.
"""
return self.pre_create_attr_defs

View file

@ -118,7 +118,7 @@ class OpenPypePyblishPluginMixin:
Attributes available for all families in plugin's `families` attribute.
Returns:
list<AbtractAttrDef>: Attribute definitions for plugin.
list<AbstractAttrDef>: Attribute definitions for plugin.
"""
return []
@ -372,6 +372,12 @@ class ExtractorColormanaged(Extractor):
```
"""
ext = representation["ext"]
# check extension
self.log.debug("__ ext: `{}`".format(ext))
if ext.lower() not in self.allowed_ext:
return
if colorspace_settings is None:
colorspace_settings = self.get_colorspace_settings(context)
@ -386,12 +392,6 @@ class ExtractorColormanaged(Extractor):
self.log.info("Config data is : `{}`".format(
config_data))
ext = representation["ext"]
# check extension
self.log.debug("__ ext: `{}`".format(ext))
if ext.lower() not in self.allowed_ext:
return
project_name = context.data["projectName"]
host_name = context.data["hostName"]
project_settings = context.data["project_settings"]

View file

@ -842,7 +842,8 @@ class PlaceholderPlugin(object):
"""Placeholder options for data showed.
Returns:
List[AbtractAttrDef]: Attribute definitions of placeholder options.
List[AbstractAttrDef]: Attribute definitions of
placeholder options.
"""
return []
@ -1143,7 +1144,7 @@ class PlaceholderLoadMixin(object):
as defaults for attributes.
Returns:
List[AbtractAttrDef]: Attribute definitions common for load
List[AbstractAttrDef]: Attribute definitions common for load
plugins.
"""
@ -1513,7 +1514,7 @@ class PlaceholderCreateMixin(object):
as defaults for attributes.
Returns:
List[AbtractAttrDef]: Attribute definitions common for create
List[AbstractAttrDef]: Attribute definitions common for create
plugins.
"""

View file

@ -0,0 +1,80 @@
import pyblish.api
from openpype.lib.attribute_definitions import (
TextDef,
BoolDef
)
from openpype.pipeline.publish import OpenPypePyblishPluginMixin
from openpype.client.entities import (
get_last_version_by_subset_name,
get_representations
)
class CollectFramesFixDef(
pyblish.api.InstancePlugin,
OpenPypePyblishPluginMixin
):
"""Provides text field to insert frame(s) to be rerendered.
Published files of last version of an instance subset are collected into
instance.data["last_version_published_files"]. All these but frames
mentioned in text field will be reused for new version.
"""
order = pyblish.api.CollectorOrder + 0.495
label = "Collect Frames to Fix"
targets = ["local"]
hosts = ["nuke"]
families = ["render", "prerender"]
enabled = True
def process(self, instance):
attribute_values = self.get_attr_values_from_data(instance.data)
frames_to_fix = attribute_values.get("frames_to_fix")
rewrite_version = attribute_values.get("rewrite_version")
if frames_to_fix:
instance.data["frames_to_fix"] = frames_to_fix
subset_name = instance.data["subset"]
asset_name = instance.data["asset"]
project_entity = instance.data["projectEntity"]
project_name = project_entity["name"]
version = get_last_version_by_subset_name(project_name,
subset_name,
asset_name=asset_name)
if not version:
self.log.warning("No last version found, "
"re-render not possible")
return
representations = get_representations(project_name,
version_ids=[version["_id"]])
published_files = []
for repre in representations:
if repre["context"]["family"] not in self.families:
continue
for file_info in repre.get("files"):
published_files.append(file_info["path"])
instance.data["last_version_published_files"] = published_files
self.log.debug("last_version_published_files::{}".format(
instance.data["last_version_published_files"]))
if rewrite_version:
instance.data["version"] = version["name"]
# limits triggering version validator
instance.data.pop("latestVersion")
@classmethod
def get_attribute_defs(cls):
return [
TextDef("frames_to_fix", label="Frames to fix",
placeholder="5,10-15",
regex="[0-9,-]+"),
BoolDef("rewrite_version", label="Rewrite latest version",
default=False),
]

View file

@ -534,6 +534,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
template_data["representation"] = repre["name"]
template_data["ext"] = repre["ext"]
# allow overwriting existing version
template_data["version"] = version["name"]
# add template data for colorspaceData
if repre.get("colorspaceData"):
colorspace = repre["colorspaceData"]["colorspace"]

View file

@ -93,6 +93,16 @@
"force_combine": true,
"aov_list": [],
"additional_options": []
},
"renderman_renderer": {
"image_prefix": "<layer>{aov_separator}<aov>.<f4>.<ext>",
"image_dir": "<scene>/<layer>",
"display_filters": [],
"imageDisplay_dir": "<imagedir>/<layer>{aov_separator}imageDisplayFilter.<f4>.<ext>",
"sample_filters": [],
"cryptomatte_dir": "<imagedir>/<layer>{aov_separator}cryptomatte.<f4>.<ext>",
"watermark_dir": "<imagedir>/<layer>{aov_separator}watermarkFilter.<f4>.<ext>",
"additional_options": []
}
},
"create": {

View file

@ -423,6 +423,93 @@
}
}
]
}
},
{
"type": "dict",
"collapsible": true,
"key": "renderman_renderer",
"label": "Renderman Renderer",
"is_group": true,
"children": [
{
"key": "image_prefix",
"label": "Image prefix template",
"type": "text"
},
{
"key": "image_dir",
"label": "Image Output Directory",
"type": "text"
},
{
"key": "display_filters",
"label": "Display Filters",
"type": "enum",
"multiselection": true,
"defaults": "empty",
"enum_items": [
{"PxrBackgroundDisplayFilter": "PxrBackgroundDisplayFilter"},
{"PxrCopyAOVDisplayFilter": "PxrCopyAOVDisplayFilter"},
{"PxrEdgeDetect":"PxrEdgeDetect"},
{"PxrFilmicTonemapperDisplayFilter": "PxrFilmicTonemapperDisplayFilter"},
{"PxrGradeDisplayFilter": "PxrGradeDisplayFilter"},
{"PxrHalfBufferErrorFilter": "PxrHalfBufferErrorFilter"},
{"PxrImageDisplayFilter": "PxrImageDisplayFilter"},
{"PxrLightSaturation": "PxrLightSaturation"},
{"PxrShadowDisplayFilter": "PxrShadowDisplayFilter"},
{"PxrStylizedHatching": "PxrStylizedHatching"},
{"PxrStylizedLines": "PxrStylizedLines"},
{"PxrStylizedToon": "PxrStylizedToon"},
{"PxrWhitePointDisplayFilter": "PxrWhitePointDisplayFilter"}
]
},
{
"key": "imageDisplay_dir",
"label": "Image Display Filter Directory",
"type": "text"
},
{
"key": "sample_filters",
"label": "Sample Filters",
"type": "enum",
"multiselection": true,
"defaults": "empty",
"enum_items": [
{"PxrBackgroundSampleFilter": "PxrBackgroundSampleFilter"},
{"PxrCopyAOVSampleFilter": "PxrCopyAOVSampleFilter"},
{"PxrCryptomatte": "PxrCryptomatte"},
{"PxrFilmicTonemapperSampleFilter": "PxrFilmicTonemapperSampleFilter"},
{"PxrGradeSampleFilter": "PxrGradeSampleFilter"},
{"PxrShadowFilter": "PxrShadowFilter"},
{"PxrWatermarkFilter": "PxrWatermarkFilter"},
{"PxrWhitePointSampleFilter": "PxrWhitePointSampleFilter"}
]
},
{
"key": "cryptomatte_dir",
"label": "Cryptomatte Output Directory",
"type": "text"
},
{
"key": "watermark_dir",
"label": "Watermark Filter Directory",
"type": "text"
},
{
"type": "label",
"label": "Add additional options - put attribute and value, like <code>Ci</code>"
},
{
"type": "dict-modifiable",
"store_as_list": true,
"key": "additional_options",
"label": "Additional Renderer Options",
"use_label_wrap": true,
"object_type": {
"type": "text"
}
}
]
}
]
}

View file

@ -4,7 +4,7 @@ import copy
from qtpy import QtWidgets, QtCore
from openpype.lib.attribute_definitions import (
AbtractAttrDef,
AbstractAttrDef,
UnknownDef,
HiddenDef,
NumberDef,
@ -33,9 +33,9 @@ def create_widget_for_attr_def(attr_def, parent=None):
def _create_widget_for_attr_def(attr_def, parent=None):
if not isinstance(attr_def, AbtractAttrDef):
if not isinstance(attr_def, AbstractAttrDef):
raise TypeError("Unexpected type \"{}\" expected \"{}\"".format(
str(type(attr_def)), AbtractAttrDef
str(type(attr_def)), AbstractAttrDef
))
if isinstance(attr_def, NumberDef):

View file

@ -2,7 +2,7 @@ import inspect
from qtpy import QtGui
import qtawesome
from openpype.lib.attribute_definitions import AbtractAttrDef
from openpype.lib.attribute_definitions import AbstractAttrDef
from openpype.tools.attribute_defs import AttributeDefinitionsDialog
from openpype.tools.utils.widgets import (
OptionalAction,
@ -43,7 +43,7 @@ def get_options(action, loader, parent, repre_contexts):
if not getattr(action, "optioned", False) or not loader_options:
return options
if isinstance(loader_options[0], AbtractAttrDef):
if isinstance(loader_options[0], AbstractAttrDef):
qargparse_options = False
dialog = AttributeDefinitionsDialog(loader_options, parent)
else:

View file

@ -826,7 +826,6 @@ class CreatorItem:
label,
group_label,
icon,
instance_attributes_defs,
description,
detailed_description,
default_variant,
@ -847,12 +846,8 @@ class CreatorItem:
self.default_variants = default_variants
self.create_allow_context_change = create_allow_context_change
self.create_allow_thumbnail = create_allow_thumbnail
self.instance_attributes_defs = instance_attributes_defs
self.pre_create_attributes_defs = pre_create_attributes_defs
def get_instance_attr_defs(self):
return self.instance_attributes_defs
def get_group_label(self):
return self.group_label
@ -891,7 +886,6 @@ class CreatorItem:
creator.label or identifier,
creator.get_group_label(),
creator.get_icon(),
creator.get_instance_attr_defs(),
description,
detail_description,
default_variant,
@ -902,15 +896,9 @@ class CreatorItem:
)
def to_data(self):
instance_attributes_defs = None
if self.instance_attributes_defs is not None:
instance_attributes_defs = serialize_attr_defs(
self.instance_attributes_defs
)
pre_create_attributes_defs = None
if self.pre_create_attributes_defs is not None:
instance_attributes_defs = serialize_attr_defs(
pre_create_attributes_defs = serialize_attr_defs(
self.pre_create_attributes_defs
)
@ -927,18 +915,11 @@ class CreatorItem:
"default_variants": self.default_variants,
"create_allow_context_change": self.create_allow_context_change,
"create_allow_thumbnail": self.create_allow_thumbnail,
"instance_attributes_defs": instance_attributes_defs,
"pre_create_attributes_defs": pre_create_attributes_defs,
}
@classmethod
def from_data(cls, data):
instance_attributes_defs = data["instance_attributes_defs"]
if instance_attributes_defs is not None:
data["instance_attributes_defs"] = deserialize_attr_defs(
instance_attributes_defs
)
pre_create_attributes_defs = data["pre_create_attributes_defs"]
if pre_create_attributes_defs is not None:
data["pre_create_attributes_defs"] = deserialize_attr_defs(
@ -1879,12 +1860,12 @@ class PublisherController(BasePublisherController):
which should be attribute definitions returned.
"""
# NOTE it would be great if attrdefs would have hash method implemented
# so they could be used as keys in dictionary
output = []
_attr_defs = {}
for instance in instances:
creator_identifier = instance.creator_identifier
creator_item = self.creator_items[creator_identifier]
for attr_def in creator_item.instance_attributes_defs:
for attr_def in instance.creator_attribute_defs:
found_idx = None
for idx, _attr_def in _attr_defs.items():
if attr_def == _attr_def:

View file

@ -136,10 +136,7 @@ class QtRemotePublishController(BasePublisherController):
created_instances = {}
for serialized_data in serialized_instances:
item = CreatedInstance.deserialize_on_remote(
serialized_data,
self._creator_items
)
item = CreatedInstance.deserialize_on_remote(serialized_data)
created_instances[item.id] = item
self._created_instances = created_instances

View file

@ -1220,7 +1220,8 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
asset_task_combinations = []
for instance in instances:
if instance.creator is None:
# NOTE I'm not sure how this can even happen?
if instance.creator_identifier is None:
editable = False
variants.add(instance.get("variant") or self.unknown_value)

View file

@ -8,7 +8,7 @@ from openpype.style import (
get_objected_colors,
get_style_image_path
)
from openpype.lib.attribute_definitions import AbtractAttrDef
from openpype.lib.attribute_definitions import AbstractAttrDef
log = logging.getLogger(__name__)
@ -406,7 +406,7 @@ class OptionalAction(QtWidgets.QWidgetAction):
def set_option_tip(self, options):
sep = "\n\n"
if not options or not isinstance(options[0], AbtractAttrDef):
if not options or not isinstance(options[0], AbstractAttrDef):
mak = (lambda opt: opt["name"] + " :\n " + opt["help"])
self.option_tip = sep.join(mak(opt) for opt in options)
return

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.15.0"
__version__ = "3.15.1-nightly.2"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.15.0-nightly.1" # OpenPype
version = "3.15.1" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"
@ -146,10 +146,6 @@ hash = "b9950f5d2fa3720b52b8be55bacf5f56d33f9e029d38ee86534995f3d8d253d2"
url = "https://distribute.openpype.io/thirdparty/oiio_tools-2.2.20-linux-centos7.tgz"
hash = "3894dec7e4e521463891a869586850e8605f5fd604858b674c87323bf33e273d"
[openpype.thirdparty.oiio.darwin]
url = "https://distribute.openpype.io/thirdparty/oiio-2.2.0-darwin.tgz"
hash = "sha256:..."
[openpype.thirdparty.ocioconfig]
url = "https://distribute.openpype.io/thirdparty/OpenColorIO-Configs-1.0.2.zip"
hash = "4ac17c1f7de83465e6f51dd352d7117e07e765b66d00443257916c828e35b6ce"

View file

@ -62,7 +62,7 @@ class TestDeadlinePublishInAfterEffects(AEDeadlinePublishTestClass):
failures.append(
DBAssert.count_of_types(dbcon, "representation", 4))
additional_args = {"context.subset": "renderTest_taskMain",
additional_args = {"context.subset": "workfileTest_task",
"context.ext": "aep"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
@ -71,7 +71,7 @@ class TestDeadlinePublishInAfterEffects(AEDeadlinePublishTestClass):
additional_args = {"context.subset": "renderTest_taskMain",
"context.ext": "png"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
DBAssert.count_of_types(dbcon, "representation", 2,
additional_args=additional_args))
additional_args = {"context.subset": "renderTest_taskMain",

View file

@ -47,7 +47,7 @@ class TestDeadlinePublishInAfterEffectsMultiComposition(AEDeadlinePublishTestCla
print("test_db_asserts")
failures = []
failures.append(DBAssert.count_of_types(dbcon, "version", 2))
failures.append(DBAssert.count_of_types(dbcon, "version", 3))
failures.append(
DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1}))
@ -80,7 +80,7 @@ class TestDeadlinePublishInAfterEffectsMultiComposition(AEDeadlinePublishTestCla
additional_args = {"context.subset": "renderTest_taskMain",
"context.ext": "png"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
DBAssert.count_of_types(dbcon, "representation", 2,
additional_args=additional_args))
additional_args = {"context.subset": "renderTest_taskMain",

View file

@ -60,7 +60,7 @@ class TestPublishInAfterEffects(AELocalPublishTestClass):
failures.append(
DBAssert.count_of_types(dbcon, "representation", 4))
additional_args = {"context.subset": "renderTest_taskMain",
additional_args = {"context.subset": "workfileTest_task",
"context.ext": "aep"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
@ -69,7 +69,7 @@ class TestPublishInAfterEffects(AELocalPublishTestClass):
additional_args = {"context.subset": "renderTest_taskMain",
"context.ext": "png"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
DBAssert.count_of_types(dbcon, "representation", 2,
additional_args=additional_args))
additional_args = {"context.subset": "renderTest_taskMain",

View file

@ -47,7 +47,7 @@ class TestPublishInAfterEffects(AELocalPublishTestClass):
failures.append(
DBAssert.count_of_types(dbcon, "representation", 4))
additional_args = {"context.subset": "renderTest_taskMain",
additional_args = {"context.subset": "workfileTest_task",
"context.ext": "aep"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,

View file

@ -130,8 +130,10 @@ def install_thirdparty(pyproject, openpype_root, platform_name):
_print("trying to get universal url for all platforms")
url = v.get("url")
if not url:
_print("cannot get url", 1)
sys.exit(1)
_print("cannot get url for all platforms", 1)
_print((f"Warning: {k} is not installed for current platform "
"and it might be missing in the build"), 1)
continue
else:
url = v.get(platform_name).get("url")
destination_path = destination_path / platform_name

View file

@ -0,0 +1,125 @@
---
id: artist_hosts_3dsmax
title: 3dsmax
sidebar_label: 3dsmax
---
:::note Work in progress
This part of documentation is still work in progress.
:::
<!-- ## OpenPype Global Tools
- [Set Context](artist_tools_context_manager)
- [Work Files](artist_tools_workfiles)
- [Create](artist_tools_creator)
- [Load](artist_tools_loader)
- [Manage (Inventory)](artist_tools_inventory)
- [Publish](artist_tools_publisher)
- [Library Loader](artist_tools_library_loader)
-->
## First Steps With OpenPype
Locate **OpenPype Icon** in the OS tray (if hidden dive in the tray toolbar).
> If you cannot locate the OpenPype icon ...it is not probably running so check [Getting Started](artist_getting_started.md) first.
By clicking the icon ```OpenPype Menu``` rolls out.
Choose ```OpenPype Menu > Launcher``` to open the ```Launcher``` window.
When opened you can **choose** the **project** to work in from the list. Then choose the particular **asset** you want to work on then choose **task**
and finally **run 3dsmax by its icon** in the tools.
![Menu OpenPype](assets/3dsmax_tray_OP.png)
:::note Launcher Content
The list of available projects, assets, tasks and tools will differ according to your Studio and need to be set in advance by supervisor/admin.
:::
## Running in the 3dsmax
If 3dsmax has been launched via OP Launcher there should be **OpenPype Menu** visible in 3dsmax **top header** after start.
This is the core functional area for you as a user. Most of your actions will take place here.
![Menu OpenPype](assets/3dsmax_menu_first_OP.png)
:::note OpenPype Menu
User should use this menu exclusively for **Opening/Saving** when dealing with work files not standard ```File Menu``` even though user still being able perform file operations via this menu but prefferably just performing quick saves during work session not saving actual workfile versions.
:::
## Working With Scene Files
In OpenPype menu first go to ```Work Files``` menu item so **Work Files Window** shows up.
Here you can perform Save / Load actions as you would normally do with ```File Save ``` and ```File Open``` in the standard 3dsmax ```File Menu``` and navigate to different project components like assets, tasks, workfiles etc.
![Menu OpenPype](assets/3dsmax_menu_OP.png)
You first choose particular asset and assigned task and corresponding workfile you would like to open.
If not any workfile present simply hit ```Save As``` and keep ```Subversion``` empty and hit ```Ok```.
![Save As Dialog](assets/3dsmax_SavingFirstFile_OP.png)
OpenPype correctly names it and add version to the workfile. This basically happens whenever user trigger ```Save As``` action. Resulting into incremental version numbers like
```workfileName_v001```
```workfileName_v002```
etc.
Basically meaning user is free of guessing what is the correct naming and other neccessities to keep everthing in order and managed.
> Note: user still has also other options for naming like ```Subversion```, ```Artist's Note``` but we won't dive into those now.
Here you can see resulting work file after ```Save As``` action.
![Save As Dialog](assets/3dsmax_SavingFirstFile2_OP.png)
## Understanding Context
As seen on our example OpenPype created pretty first workfile and named it ```220901_couch_modeling_v001.max``` meaning it sits in the Project ```220901``` being it ```couch``` asset and workfile being ```modeling``` task and obviously ```v001``` telling user its first existing version of this workfile.
It is good to be aware that whenever you as a user choose ```asset``` and ```task``` you happen to be in so called **context** meaning that all user actions are in relation with particular ```asset```. This could be quickly seen in host application header and ```OpenPype Menu``` and its accompanying tools.
![Workfile Context](assets/3dsmax_context.png)
> Whenever you choose different ```asset``` and its ```task``` in **Work Files window** you are basically changing context to the current asset/task you have chosen.
This concludes the basics of working with workfiles in 3dsmax using OpenPype and its tools. Following chapters will cover other aspects like creating multiple assets types and their publishing for later usage in the production.
---
## Creating and Publishing Instances
:::warning Important
Before proceeding further please check [Glossary](artist_concepts.md) and [What Is Publishing?](artist_publish.md) So you have clear idea about terminology.
:::
### Intro
Current OpenPype integration (ver 3.15.0) supports only ```PointCache``` and ```Camera``` families now.
**Pointcache** family being basically any geometry outputted as Alembic cache (.abc) format
**Camera** family being 3dsmax Camera object with/without animation outputted as native .max, FBX, Alembic format
---
:::note Work in progress
This part of documentation is still work in progress.
:::
## ...to be added

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

View file

@ -37,3 +37,8 @@ This functionality cannot deal with all cases and is not error proof, some inter
```bash
openpype_console module kitsu push-to-zou -l me@domain.ext -p my_password
```
## Q&A
### Is it safe to rename an entity from Kitsu?
Absolutely! Entities are linked by their unique IDs between the two databases.
But renaming from the OP's Project Manager won't apply the change to Kitsu, it'll be overriden during the next synchronization.

View file

@ -49,6 +49,7 @@ module.exports = {
],
},
"artist_hosts_blender",
"artist_hosts_3dsmax",
"artist_hosts_harmony",
"artist_hosts_houdini",
"artist_hosts_aftereffects",