This commit is contained in:
Roy Nieterau 2017-06-27 16:07:59 +02:00
commit 06a7864a6c
12 changed files with 319 additions and 175 deletions

View file

@ -12,6 +12,8 @@ from .plugin import (
# temporary fix, might
from .action import (
get_errored_instances_from_context,
SelectInvalidAction,
GenerateUUIDsOnInvalidAction,
RepairAction,
@ -24,7 +26,8 @@ all = [
"ValidateContentsOrder",
"ValidateSceneOrder",
"ValidateMeshOrder",
# action
"get_errored_instances_from_context",
"SelectInvalidAction",
"GenerateUUIDsOnInvalidAction",
"RepairAction"

View file

@ -242,3 +242,63 @@ def collect_animation_data():
def get_current_renderlayer():
return cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True)
def is_visible(node,
displayLayer=True,
intermediateObject=True,
parentHidden=True,
visibility=True):
"""Is `node` visible?
Returns whether a node is hidden by one of the following methods:
- The node exists (always checked)
- The node must be a dagNode (always checked)
- The node's visibility is off.
- The node is set as intermediate Object.
- The node is in a disabled displayLayer.
- Whether any of its parent nodes is hidden.
Roughly based on: http://ewertb.soundlinker.com/mel/mel.098.php
Returns:
bool: Whether the node is visible in the scene
"""
# Only existing objects can be visible
if not cmds.objExists(node):
return False
# Only dagNodes can be visible
if not cmds.objectType(node, isAType='dagNode'):
return False
if visibility:
if not cmds.getAttr('{0}.visibility'.format(node)):
return False
if intermediateObject and cmds.objectType(node, isAType='shape'):
if cmds.getAttr('{0}.intermediateObject'.format(node)):
return False
if displayLayer:
# Display layers set overrideEnabled and overrideVisibility on members
if cmds.attributeQuery('overrideEnabled', node=node, exists=True):
override_enabled = cmds.getAttr('{}.overrideEnabled'.format(node))
override_visibility = cmds.getAttr('{}.overrideVisibility'.format(node))
if override_enabled and override_visibility:
return False
if parentHidden:
parents = cmds.listRelatives(node, parent=True, fullPath=True)
if parents:
parent = parents[0]
if not is_visible(parent,
displayLayer=displayLayer,
intermediateObject=False,
parentHidden=parentHidden,
visibility=visibility):
return False
return True

View file

@ -7,6 +7,11 @@ class AbcLoader(api.Loader):
families = ["colorbleed.animation", "colorbleed.camera"]
representations = ["abc"]
label = "Reference animation"
order = -10
icon = "code-fork"
color = "orange"
def process(self, name, namespace, context):
from maya import cmds
@ -31,6 +36,10 @@ class CurvesLoader(api.Loader):
families = ["colorbleed.animation"]
representations = ["curves"]
label = "Import curves"
order = -1
icon = "question"
def process(self, name, namespace, context):
from maya import cmds
from avalon import maya

View file

@ -8,6 +8,11 @@ class HistoryLookLoader(api.Loader):
families = ["colorbleed.historyLookdev"]
representations = ["ma"]
label = "Reference look history"
order = -10
icon = "code-fork"
color = "orange"
def process(self, name, namespace, context):
from avalon import maya
with maya.maintained_selection():

View file

@ -11,6 +11,11 @@ class LookLoader(api.Loader):
families = ["colorbleed.lookdev"]
representations = ["ma"]
label = "Reference look"
order = -10
icon = "code-fork"
color = "orange"
def process(self, name, namespace, context):
from avalon import maya
try:

View file

@ -5,15 +5,16 @@ from avalon import maya
class ModelLoader(api.Loader):
"""Load models
Stores the imported asset in a container named after the asset.
"""
"""Load the model"""
families = ["colorbleed.model"]
representations = ["ma"]
label = "Reference model"
order = -10
icon = "code-fork"
color = "orange"
def process(self, name, namespace, context):
with maya.maintained_selection():
@ -31,3 +32,46 @@ class ModelLoader(api.Loader):
cmds.sets(meshes, forceElement="initialShadingGroup")
self[:] = nodes
class ModelGPUCacheLoader(api.Loader):
"""Import a GPU Cache"""
families = ["colorbleed.model"]
representations = ["abc"]
label = "Import GPU Cache"
order = -1
icon = "download"
def process(self, name, namespace, context):
from maya import cmds
# todo: This will likely not be entirely safe with "containerize"
# also this cannot work in the manager because it only works
# on references at the moment!
# especially in cases of duplicating the gpu cache node this will
# mess up the "containered" workflow in the avalon core for maya
print("WARNING: Importing gpuCaches isn't fully tested yet")
path = self.fname
cmds.loadPlugin("gpuCache", quiet=True)
# Create transform with shape
transform = cmds.createNode("transform",
name=name)
cache = cmds.createNode("gpuCache",
parent=transform,
name="{0}Shape".format(name))
# Set the cache filepath
cmds.setAttr(cache + '.cacheFileName', path, type="string")
cmds.setAttr(cache + '.cacheGeomPath', "|", type="string") # root
# Select the transform
cmds.select(transform, r=1)
# Store the created nodes
self[:] = [transform, cache]

View file

@ -12,6 +12,11 @@ class RigLoader(api.Loader):
families = ["colorbleed.rig"]
representations = ["ma"]
label = "Reference rig"
order = -10
icon = "code-fork"
color = "orange"
def process(self, name, namespace, context):
nodes = cmds.file(self.fname,
namespace=namespace,

View file

@ -1,6 +1,7 @@
import pyblish.api
import maya.cmds as cmds
import cb.utils.maya.dag as dag
import pyblish.api
import colorbleed.maya.lib as lib
class ValidateInstancerContent(pyblish.api.InstancePlugin):
@ -15,7 +16,7 @@ class ValidateInstancerContent(pyblish.api.InstancePlugin):
def process(self, instance):
invalid = False
error = False
members = instance.data['setMembers']
export_members = instance.data['exactExportMembers']
@ -23,42 +24,22 @@ class ValidateInstancerContent(pyblish.api.InstancePlugin):
if not len(members) == len(cmds.ls(members, type="instancer")):
self.log.error("Instancer can only contain instancers")
invalid = True
error = True
# TODO: Implement better check for particles are cached
if not cmds.ls(export_members, type="nucleus"):
self.log.error("Instancer must have a connected nucleus")
invalid = True
error = True
if not cmds.ls(export_members, type="cacheFile"):
self.log.error("Instancer must be cached")
invalid = True
error = True
# Ensure all instanced geometry is hidden
shapes = cmds.ls(export_members,
dag=True, shapes=True,
noIntermediate=True)
meshes = cmds.ls(shapes, type="mesh")
def invalidate(node):
"""Whether mesh is in a valid state
Arguments:
node (str): The node to check
Returns:
bool: Whether it is in a valid state.
"""
return dag.is_visible(node,
displayLayer=False,
intermediateObject=False)
visible = [node for node in meshes if invalidate(node)]
if visible:
hidden = self.check_geometry_hidden(export_members)
if not hidden:
error = True
self.log.error("Instancer input geometry must be hidden "
"the scene. Invalid: {0}".format(visible))
invalid = True
"the scene. Invalid: {0}".format(hidden))
# Ensure all in one group
parents = cmds.listRelatives(members,
@ -68,7 +49,26 @@ class ValidateInstancerContent(pyblish.api.InstancePlugin):
if len(roots) > 1:
self.log.error("Instancer should all be contained in a single "
"group. Current roots: {0}".format(roots))
invalid = True
error = True
if invalid:
if error:
raise RuntimeError("Instancer Content is invalid. See log.")
def check_geometry_hidden(self, export_members):
# Ensure all instanced geometry is hidden
shapes = cmds.ls(export_members,
dag=True,
shapes=True,
noIntermediate=True)
meshes = cmds.ls(shapes, type="mesh")
visible = [node for node in meshes
if lib.is_visible(node,
displayLayer=False,
intermediateObject=False)]
if visible:
return False
return True

View file

@ -1,3 +1,5 @@
import os
import re
import pyblish.api
VERBOSE = False
@ -10,6 +12,27 @@ def is_cache_resource(resource):
return required.issubset(tags)
def valdidate_files(files):
for f in files:
assert os.path.exists(f)
assert f.endswith(".mcx") or f.endswith(".mcc")
return True
def filter_ticks(files):
tick_files = set()
ticks = set()
for path in files:
match = re.match(".+Tick([0-9]+).mcx$", os.path.basename(path))
if match:
tick_files.add(path)
num = match.group(1)
ticks.add(int(num))
return tick_files, ticks
class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin):
"""Validates all instancer particle systems are cached correctly.
@ -26,7 +49,6 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin):
@classmethod
def get_invalid(cls, instance):
import os
import pyseq
start_frame = instance.data.get("startFrame", 0)
@ -42,7 +64,6 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin):
node = resource['node']
all_files = resource['files'][:]
all_lookup = set(all_files)
# The first file is usually the .xml description file.
@ -54,28 +75,21 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin):
# Ensure all files exist (including ticks)
# The remainder file paths should be the .mcx or .mcc files
for f in all_files:
assert os.path.exists(f)
assert f.endswith(".mcx") or f.endswith(".mcc")
valdidate_files(all_files)
# Maya particle caches support substeps by saving out additional files
# that end with a Tick60.mcx, Tick120.mcx, etc. suffix. To avoid `pyseq`
# getting confused we filter those out and then for each file (except
# the last frame) check that at least all ticks exist.
tick_files = set()
ticks = set()
for path in all_files:
import re
match = re.match(".+Tick([0-9]+).mcx$", os.path.basename(path))
# Maya particle caches support substeps by saving out additional
# files that end with a Tick60.mcx, Tick120.mcx, etc. suffix.
# To avoid `pyseq` getting confused we filter those out and then
# for each file (except the last frame) check that at least all
# ticks exist.
if match:
tick_files.add(path)
num = match.group(1)
ticks.add(int(num))
tick_files, ticks = filter_ticks(all_files)
if tick_files:
files = [f for f in all_files if f not in tick_files]
else:
files = all_files
files = [f for f in all_files if f not in tick_files] if tick_files else all_files
sequences = pyseq.get_sequences(files)
if len(sequences) != 1:
invalid.append(node)
cls.log.warning("More than one sequence found? "
@ -112,7 +126,8 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin):
# for the frames required by the time range.
if ticks:
ticks = list(sorted(ticks))
cls.log.info("Found ticks: {0} (substeps: {1})".format(ticks, len(ticks)))
cls.log.info("Found ticks: {0} "
"(substeps: {1})".format(ticks, len(ticks)))
# Check all frames except the last since we don't
# require subframes after our time range.
@ -123,7 +138,8 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin):
frame = item.frame
if not frame:
invalid.append(node)
cls.log.error("Path is not a frame in sequence: {0}".format(item))
cls.log.error("Path is not a frame in sequence: "
"{0}".format(item))
continue
# Not required for our time range
@ -137,7 +153,8 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin):
if tick_file not in all_lookup:
invalid.append(node)
cls.log.warning("Tick file found that is not "
"in cache query filenames: {0}".format(tick_file))
"in cache query filenames: "
"{0}".format(tick_file))
return invalid
@ -148,4 +165,4 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin):
if invalid:
self.log.error("Invalid nodes: {0}".format(invalid))
raise RuntimeError("Invalid particle caches in instance. "
"See logs for details.")
"See logs for details.")

View file

@ -2,66 +2,7 @@ from maya import cmds
import pyblish.api
import colorbleed.api
def is_visible(node,
displayLayer=True,
intermediateObject=True,
parentHidden=True,
visibility=True):
"""Is `node` visible?
Returns whether a node is hidden by one of the following methods:
- The node exists (always checked)
- The node must be a dagNode (always checked)
- The node's visibility is off.
- The node is set as intermediate Object.
- The node is in a disabled displayLayer.
- Whether any of its parent nodes is hidden.
Roughly based on: http://ewertb.soundlinker.com/mel/mel.098.php
Returns:
bool: Whether the node is visible in the scene
"""
# Only existing objects can be visible
if not cmds.objExists(node):
return False
# Only dagNodes can be visible
if not cmds.objectType(node, isAType='dagNode'):
return False
if visibility:
if not cmds.getAttr('{0}.visibility'.format(node)):
return False
if intermediateObject and cmds.objectType(node, isAType='shape'):
if cmds.getAttr('{0}.intermediateObject'.format(node)):
return False
if displayLayer:
# Display layers set overrideEnabled and overrideVisibility on members
if cmds.attributeQuery('overrideEnabled', node=node, exists=True):
override_enabled = cmds.getAttr('{}.overrideEnabled'.format(node))
override_visibility = cmds.getAttr('{}.overrideVisibility'.format(node))
if override_enabled and override_visibility:
return False
if parentHidden:
parents = cmds.listRelatives(node, parent=True, fullPath=True)
if parents:
parent = parents[0]
if not is_visible(parent,
displayLayer=displayLayer,
intermediateObject=False,
parentHidden=parentHidden,
visibility=visibility):
return False
return True
import colorbleed.maya.lib as lib
class ValidateJointsHidden(pyblish.api.InstancePlugin):
@ -87,7 +28,7 @@ class ValidateJointsHidden(pyblish.api.InstancePlugin):
@staticmethod
def get_invalid(instance):
joints = cmds.ls(instance, type='joint', long=True)
return [j for j in joints if is_visible(j, displayLayer=True)]
return [j for j in joints if lib.is_visible(j, displayLayer=True)]
def process(self, instance):
"""Process all the nodes in the instance 'objectSet'"""

View file

@ -2,7 +2,6 @@ from maya import cmds
import pyblish.api
import colorbleed.api
from colorbleed.api import get_errored_instances_from_context
from cbra.utils.maya.node_uuid import get_id, add_ids
@ -71,10 +70,10 @@ class CopyUUIDsFromHistory(pyblish.api.Action):
# Get the errored instances
self.log.info("Finding failed instances..")
errored_instances = get_errored_instances_from_context(context)
errored = colorbleed.api.get_errored_instances_from_context(context)
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
instances = pyblish.api.instances_by_plugin(errored, plugin)
ids_map = dict()
for instance in instances:

View file

@ -22,93 +22,149 @@ class ValidateRigContents(pyblish.api.InstancePlugin):
accepted_controllers = ["transform"]
ignore_nodes = []
invalid_hierarchy = []
invalid_controls = []
invalid_geometry = []
def process(self, instance):
error = False
objectsets = ("controls_SET", "out_SET")
missing = [obj for obj in objectsets if obj not in instance]
assert not missing, ("%s is missing %s" % (instance, missing))
# Ensure there are at least some transforms or dag nodes
# in the rig instance
set_members = self.check_set_members(instance)
self.log.info("Evaluating contents of object sets..")
# Ensure contents in sets and retrieve long path for all objects
output_content = cmds.sets("out_SET", query=True) or []
assert output_content, "Must have members in rig out_SET"
controls_content = cmds.set("controls_SET", query=True) or []
assert controls_content, "Must have members in rig controls_SET"
root_node = cmds.ls(set_members, assemblies=True)
hierarchy = cmds.listRelatives(root_node, allDescendents=True,
fullPath=True)
self.invalid_geometry = self.validate_geometry(output_content,
hierarchy)
self.invalid_controls = self.validate_controls(controls_content,
hierarchy)
if self.invalid_hierachy:
self.log.error("Found nodes which reside outside of root group "
"while they are set up for publishing."
"\n%s" % self.invalid_hierachy)
error = True
if self.not_transforms:
self.log.error("Only transforms can be part of the controls_SET."
"\n%s" % self.not_transforms)
error = True
if self.invalid_geometry:
self.log.error("Only meshes can be part of the out_SET\n%s"
% self.invalid_geometry)
error = True
if error:
raise RuntimeError("Invalid rig content. See log for details.")
def check_set_members(self, instance):
"""Check if the instance has any dagNodes
Args:
instance: the instance which needs to be published
Returns:
set_members (list): all dagNodes from instance
"""
set_members = instance.data['setMembers']
if not cmds.ls(set_members, type="dagNode", long=True):
raise RuntimeError("No dag nodes in the pointcache instance. "
"(Empty instance?)")
return set_members
self.log.info("Evaluating contents of object sets..")
def validate_hierarchy(self, hierarchy, nodes):
"""Collect all nodes which are NOT within the hierarchy
Args:
hierarchy (list): nodes within the root node
nodes (list): nodes to check
not_meshes = list()
not_transforms = list()
invalid_hierachy = list()
Returns:
errors (list): list of nodes
"""
errors = []
for node in nodes:
if node not in hierarchy:
errors.append(node)
return errors
error = False
def validate_geometry(self, set_members, hierarchy):
"""Check if the out set passes the validations
# Ensure contents in sets and retrieve long path for all objects
out_members = cmds.sets("out_SET", query=True) or []
assert out_members, "Must have members in rig out_SET"
out_members = cmds.ls(out_members, long=True)
Checks if all its set members are within the hierarchy of the root
Checks if the node types of the set members valid
controls_members = cmds.sets("controls_SET", query=True) or []
controls_members = cmds.ls(controls_members, long=True)
assert controls_members, "Must have controls in rig control_SET"
Args:
set_members: list of nodes of the controls_set
hierarchy: list of nodes which reside under the root node
root_node = cmds.ls(set_members, assemblies=True)
root_content = cmds.listRelatives(root_node,
allDescendents=True,
fullPath=True)
Returns:
errors (list)
"""
errors = []
# Validate the contents further
shapes = cmds.listRelatives(out_members,
shapes = cmds.listRelatives(set_members,
allDescendents=True,
shapes=True,
fullPath=True) or []
# The user can add the shape node to the out_set, this will result
# in none when querying allDescendents
out_shapes = out_members + shapes
all_shapes = set_members + shapes
# geometry
for shape in out_shapes:
invalid_shapes = self.validate_hierarchy(hierarchy, all_shapes)
self.invalid_hierachy.extend(invalid_shapes)
for shape in all_shapes:
nodetype = cmds.nodeType(shape)
if nodetype in self.ignore_nodes:
continue
if nodetype not in self.accepted_output:
not_meshes.append(shape)
errors.append(shape)
# check if controllers are in the root group
if shape not in root_content:
invalid_hierachy.append(shape)
return errors
# curves
for node in controls_members:
def validate_controls(self, set_members, hierarchy):
"""Check if the controller set passes the validations
Checks if all its set members are within the hierarchy of the root
Checks if the node types of the set members valid
Args:
set_members: list of nodes of the controls_set
hierarchy: list of nodes which reside under the root node
Returns:
errors (list)
"""
errors = []
invalid_controllers = self.validate_hierarchy(hierarchy, set_members)
self.invalid_hierachy.extend(invalid_controllers)
for node in set_members:
nodetype = cmds.nodeType(node)
if nodetype in self.ignore_nodes:
continue
if nodetype not in self.accepted_controllers:
not_transforms.append(node)
errors.append(node)
# check if controllers are in the root group
if node not in root_content:
invalid_hierachy.append(node)
if invalid_hierachy:
self.log.error("Found nodes which reside outside of root group "
"while they are set up for publishing."
"\n%s" % invalid_hierachy)
error = True
if not_transforms:
self.log.error("Only transforms can be part of the controls_SET."
"\n%s" % not_transforms)
error = True
if not_meshes:
self.log.error("Only meshes can be part of the out_SET\n%s"
% not_meshes)
error = True
if error:
raise RuntimeError("Invalid rig content. See log for details.")
return errors