mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 21:32:15 +01:00
Merge pull request #3100 from BigRoy/fix3085
This commit is contained in:
commit
f4e6a8a36d
4 changed files with 282 additions and 3 deletions
|
|
@ -1908,7 +1908,7 @@ def iter_parents(node):
|
|||
"""
|
||||
while True:
|
||||
split = node.rsplit("|", 1)
|
||||
if len(split) == 1:
|
||||
if len(split) == 1 or not split[0]:
|
||||
return
|
||||
|
||||
node = split[0]
|
||||
|
|
@ -3213,3 +3213,209 @@ def parent_nodes(nodes, parent=None):
|
|||
node[0].setParent(node[1])
|
||||
if delete_parent:
|
||||
pm.delete(parent_node)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_time():
|
||||
ct = cmds.currentTime(query=True)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cmds.currentTime(ct, edit=True)
|
||||
|
||||
|
||||
def iter_visible_nodes_in_range(nodes, start, end):
|
||||
"""Yield nodes that are visible in start-end frame range.
|
||||
|
||||
- Ignores intermediateObjects completely.
|
||||
- Considers animated visibility attributes + upstream visibilities.
|
||||
|
||||
This is optimized for large scenes where some nodes in the parent
|
||||
hierarchy might have some input connections to the visibilities,
|
||||
e.g. key, driven keys, connections to other attributes, etc.
|
||||
|
||||
This only does a single time step to `start` if current frame is
|
||||
not inside frame range since the assumption is made that changing
|
||||
a frame isn't so slow that it beats querying all visibility
|
||||
plugs through MDGContext on another frame.
|
||||
|
||||
Args:
|
||||
nodes (list): List of node names to consider.
|
||||
start (int, float): Start frame.
|
||||
end (int, float): End frame.
|
||||
|
||||
Returns:
|
||||
list: List of node names. These will be long full path names so
|
||||
might have a longer name than the input nodes.
|
||||
|
||||
"""
|
||||
# States we consider per node
|
||||
VISIBLE = 1 # always visible
|
||||
INVISIBLE = 0 # always invisible
|
||||
ANIMATED = -1 # animated visibility
|
||||
|
||||
# Ensure integers
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
|
||||
# Consider only non-intermediate dag nodes and use the "long" names.
|
||||
nodes = cmds.ls(nodes, long=True, noIntermediate=True, type="dagNode")
|
||||
if not nodes:
|
||||
return
|
||||
|
||||
with maintained_time():
|
||||
# Go to first frame of the range if the current time is outside
|
||||
# the queried range so can directly query all visible nodes on
|
||||
# that frame.
|
||||
current_time = cmds.currentTime(query=True)
|
||||
if not (start <= current_time <= end):
|
||||
cmds.currentTime(start)
|
||||
|
||||
visible = cmds.ls(nodes, long=True, visible=True)
|
||||
for node in visible:
|
||||
yield node
|
||||
if len(visible) == len(nodes) or start == end:
|
||||
# All are visible on frame one, so they are at least visible once
|
||||
# inside the frame range.
|
||||
return
|
||||
|
||||
# For the invisible ones check whether its visibility and/or
|
||||
# any of its parents visibility attributes are animated. If so, it might
|
||||
# get visible on other frames in the range.
|
||||
def memodict(f):
|
||||
"""Memoization decorator for a function taking a single argument.
|
||||
|
||||
See: http://code.activestate.com/recipes/
|
||||
578231-probably-the-fastest-memoization-decorator-in-the-/
|
||||
"""
|
||||
|
||||
class memodict(dict):
|
||||
def __missing__(self, key):
|
||||
ret = self[key] = f(key)
|
||||
return ret
|
||||
|
||||
return memodict().__getitem__
|
||||
|
||||
@memodict
|
||||
def get_state(node):
|
||||
plug = node + ".visibility"
|
||||
connections = cmds.listConnections(plug,
|
||||
source=True,
|
||||
destination=False)
|
||||
if connections:
|
||||
return ANIMATED
|
||||
else:
|
||||
return VISIBLE if cmds.getAttr(plug) else INVISIBLE
|
||||
|
||||
visible = set(visible)
|
||||
invisible = [node for node in nodes if node not in visible]
|
||||
always_invisible = set()
|
||||
# Iterate over the nodes by short to long names to iterate the highest
|
||||
# in hierarchy nodes first. So the collected data can be used from the
|
||||
# cache for parent queries in next iterations.
|
||||
node_dependencies = dict()
|
||||
for node in sorted(invisible, key=len):
|
||||
|
||||
state = get_state(node)
|
||||
if state == INVISIBLE:
|
||||
always_invisible.add(node)
|
||||
continue
|
||||
|
||||
# If not always invisible by itself we should go through and check
|
||||
# the parents to see if any of them are always invisible. For those
|
||||
# that are "ANIMATED" we consider that this node is dependent on
|
||||
# that attribute, we store them as dependency.
|
||||
dependencies = set()
|
||||
if state == ANIMATED:
|
||||
dependencies.add(node)
|
||||
|
||||
traversed_parents = list()
|
||||
for parent in iter_parents(node):
|
||||
|
||||
if parent in always_invisible or get_state(parent) == INVISIBLE:
|
||||
# When parent is always invisible then consider this parent,
|
||||
# this node we started from and any of the parents we
|
||||
# have traversed in-between to be *always invisible*
|
||||
always_invisible.add(parent)
|
||||
always_invisible.add(node)
|
||||
always_invisible.update(traversed_parents)
|
||||
break
|
||||
|
||||
# If we have traversed the parent before and its visibility
|
||||
# was dependent on animated visibilities then we can just extend
|
||||
# its dependencies for to those for this node and break further
|
||||
# iteration upwards.
|
||||
parent_dependencies = node_dependencies.get(parent, None)
|
||||
if parent_dependencies is not None:
|
||||
dependencies.update(parent_dependencies)
|
||||
break
|
||||
|
||||
state = get_state(parent)
|
||||
if state == ANIMATED:
|
||||
dependencies.add(parent)
|
||||
|
||||
traversed_parents.append(parent)
|
||||
|
||||
if node not in always_invisible and dependencies:
|
||||
node_dependencies[node] = dependencies
|
||||
|
||||
if not node_dependencies:
|
||||
return
|
||||
|
||||
# Now we only have to check the visibilities for nodes that have animated
|
||||
# visibility dependencies upstream. The fastest way to check these
|
||||
# visibility attributes across different frames is with Python api 2.0
|
||||
# so we do that.
|
||||
@memodict
|
||||
def get_visibility_mplug(node):
|
||||
"""Return api 2.0 MPlug with cached memoize decorator"""
|
||||
sel = om.MSelectionList()
|
||||
sel.add(node)
|
||||
dag = sel.getDagPath(0)
|
||||
return om.MFnDagNode(dag).findPlug("visibility", True)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def dgcontext(mtime):
|
||||
"""MDGContext context manager"""
|
||||
context = om.MDGContext(mtime)
|
||||
try:
|
||||
previous = context.makeCurrent()
|
||||
yield context
|
||||
finally:
|
||||
previous.makeCurrent()
|
||||
|
||||
# We skip the first frame as we already used that frame to check for
|
||||
# overall visibilities. And end+1 to include the end frame.
|
||||
scene_units = om.MTime.uiUnit()
|
||||
for frame in range(start + 1, end + 1):
|
||||
mtime = om.MTime(frame, unit=scene_units)
|
||||
|
||||
# Build little cache so we don't query the same MPlug's value
|
||||
# again if it was checked on this frame and also is a dependency
|
||||
# for another node
|
||||
frame_visibilities = {}
|
||||
with dgcontext(mtime) as context:
|
||||
for node, dependencies in list(node_dependencies.items()):
|
||||
for dependency in dependencies:
|
||||
dependency_visible = frame_visibilities.get(dependency,
|
||||
None)
|
||||
if dependency_visible is None:
|
||||
mplug = get_visibility_mplug(dependency)
|
||||
dependency_visible = mplug.asBool(context)
|
||||
frame_visibilities[dependency] = dependency_visible
|
||||
|
||||
if not dependency_visible:
|
||||
# One dependency is not visible, thus the
|
||||
# node is not visible.
|
||||
break
|
||||
|
||||
else:
|
||||
# All dependencies are visible.
|
||||
yield node
|
||||
# Remove node with dependencies for next frame iterations
|
||||
# because it was visible at least once.
|
||||
node_dependencies.pop(node)
|
||||
|
||||
# If no more nodes to process break the frame iterations..
|
||||
if not node_dependencies:
|
||||
break
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import openpype.api
|
|||
from openpype.hosts.maya.api.lib import (
|
||||
extract_alembic,
|
||||
suspended_refresh,
|
||||
maintained_selection
|
||||
maintained_selection,
|
||||
iter_visible_nodes_in_range
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -76,6 +77,16 @@ class ExtractAnimation(openpype.api.Extractor):
|
|||
# Since Maya 2017 alembic supports multiple uv sets - write them.
|
||||
options["writeUVSets"] = True
|
||||
|
||||
if instance.data.get("visibleOnly", False):
|
||||
# If we only want to include nodes that are visible in the frame
|
||||
# range then we need to do our own check. Alembic's `visibleOnly`
|
||||
# flag does not filter out those that are only hidden on some
|
||||
# frames as it counts "animated" or "connected" visibilities as
|
||||
# if it's always visible.
|
||||
nodes = list(iter_visible_nodes_in_range(nodes,
|
||||
start=start,
|
||||
end=end))
|
||||
|
||||
with suspended_refresh():
|
||||
with maintained_selection():
|
||||
cmds.select(nodes, noExpand=True)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import openpype.api
|
|||
from openpype.hosts.maya.api.lib import (
|
||||
extract_alembic,
|
||||
suspended_refresh,
|
||||
maintained_selection
|
||||
maintained_selection,
|
||||
iter_visible_nodes_in_range
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -79,6 +80,16 @@ class ExtractAlembic(openpype.api.Extractor):
|
|||
# Since Maya 2017 alembic supports multiple uv sets - write them.
|
||||
options["writeUVSets"] = True
|
||||
|
||||
if instance.data.get("visibleOnly", False):
|
||||
# If we only want to include nodes that are visible in the frame
|
||||
# range then we need to do our own check. Alembic's `visibleOnly`
|
||||
# flag does not filter out those that are only hidden on some
|
||||
# frames as it counts "animated" or "connected" visibilities as
|
||||
# if it's always visible.
|
||||
nodes = list(iter_visible_nodes_in_range(nodes,
|
||||
start=start,
|
||||
end=end))
|
||||
|
||||
with suspended_refresh():
|
||||
with maintained_selection():
|
||||
cmds.select(nodes, noExpand=True)
|
||||
|
|
|
|||
51
openpype/hosts/maya/plugins/publish/validate_visible_only.py
Normal file
51
openpype/hosts/maya/plugins/publish/validate_visible_only.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import pyblish.api
|
||||
|
||||
import openpype.api
|
||||
from openpype.hosts.maya.api.lib import iter_visible_nodes_in_range
|
||||
import openpype.hosts.maya.api.action
|
||||
|
||||
|
||||
class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin):
|
||||
"""Validates at least a single node is visible in frame range.
|
||||
|
||||
This validation only validates if the `visibleOnly` flag is enabled
|
||||
on the instance - otherwise the validation is skipped.
|
||||
|
||||
"""
|
||||
order = openpype.api.ValidateContentsOrder + 0.05
|
||||
label = "Alembic Visible Only"
|
||||
hosts = ["maya"]
|
||||
families = ["pointcache", "animation"]
|
||||
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
if not instance.data.get("visibleOnly", False):
|
||||
self.log.debug("Visible only is disabled. Validation skipped..")
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
start, end = self.get_frame_range(instance)
|
||||
raise RuntimeError("No visible nodes found in "
|
||||
"frame range {}-{}.".format(start, end))
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
if instance.data["family"] == "animation":
|
||||
# Special behavior to use the nodes in out_SET
|
||||
nodes = instance.data["out_hierarchy"]
|
||||
else:
|
||||
nodes = instance[:]
|
||||
|
||||
start, end = cls.get_frame_range(instance)
|
||||
if not any(iter_visible_nodes_in_range(nodes, start, end)):
|
||||
# Return the nodes we have considered so the user can identify
|
||||
# them with the select invalid action
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
def get_frame_range(instance):
|
||||
data = instance.data
|
||||
return data["frameStartHandle"], data["frameEndHandle"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue