Merge pull request #5698 from ynput/bugfix/OP-6806_Houdini-wrong-frame-calculation-with-handles

This commit is contained in:
Milan Kolar 2023-10-26 00:25:37 +02:00 committed by GitHub
commit 46ee9cf7af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 302 additions and 125 deletions

View file

@ -568,29 +568,64 @@ def get_template_from_value(key, value):
return parm
def get_frame_data(node):
"""Get the frame data: start frame, end frame and steps.
def get_frame_data(node, handle_start=0, handle_end=0, log=None):
"""Get the frame data: start frame, end frame, steps,
start frame with start handle and end frame with end handle.
This function uses Houdini node's `trange`, `t1, `t2` and `t3`
parameters as the source of truth for the full inclusive frame
range to render, as such these are considered as the frame
range including the handles.
The non-inclusive frame start and frame end without handles
are computed by subtracting the handles from the inclusive
frame range.
Args:
node(hou.Node)
node (hou.Node): ROP node to retrieve frame range from,
the frame range is assumed to be the frame range
*including* the start and end handles.
handle_start (int): Start handles.
handle_end (int): End handles.
log (logging.Logger): Logger to log to.
Returns:
dict: frame data for star, end and steps.
dict: frame data for start, end, steps,
start with handle and end with handle
"""
if log is None:
log = self.log
data = {}
if node.parm("trange") is None:
log.debug(
"Node has no 'trange' parameter: {}".format(node.path())
)
return data
if node.evalParm("trange") == 0:
self.log.debug("trange is 0")
return data
data["frameStartHandle"] = hou.intFrame()
data["frameEndHandle"] = hou.intFrame()
data["byFrameStep"] = 1.0
data["frameStart"] = node.evalParm("f1")
data["frameEnd"] = node.evalParm("f2")
data["steps"] = node.evalParm("f3")
log.info(
"Node '{}' has 'Render current frame' set.\n"
"Asset Handles are ignored.\n"
"frameStart and frameEnd are set to the "
"current frame.".format(node.path())
)
else:
data["frameStartHandle"] = int(node.evalParm("f1"))
data["frameEndHandle"] = int(node.evalParm("f2"))
data["byFrameStep"] = node.evalParm("f3")
data["handleStart"] = handle_start
data["handleEnd"] = handle_end
data["frameStart"] = data["frameStartHandle"] + data["handleStart"]
data["frameEnd"] = data["frameEndHandle"] - data["handleEnd"]
return data

View file

@ -20,7 +20,9 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin):
"""
label = "Arnold ROP Render Products"
order = pyblish.api.CollectorOrder + 0.4
# This specific order value is used so that
# this plugin runs after CollectRopFrameRange
order = pyblish.api.CollectorOrder + 0.4999
hosts = ["houdini"]
families = ["arnold_rop"]
@ -126,8 +128,9 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin):
return path
expected_files = []
start = instance.data["frameStart"]
end = instance.data["frameEnd"]
start = instance.data["frameStartHandle"]
end = instance.data["frameEndHandle"]
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))

View file

@ -1,56 +0,0 @@
import hou
import pyblish.api
class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin):
"""Collect time range frame data for the instance node."""
order = pyblish.api.CollectorOrder + 0.001
label = "Instance Node Frame Range"
hosts = ["houdini"]
def process(self, instance):
node_path = instance.data.get("instance_node")
node = hou.node(node_path) if node_path else None
if not node_path or not node:
self.log.debug("No instance node found for instance: "
"{}".format(instance))
return
frame_data = self.get_frame_data(node)
if not frame_data:
return
self.log.info("Collected time data: {}".format(frame_data))
instance.data.update(frame_data)
def get_frame_data(self, node):
"""Get the frame data: start frame, end frame and steps
Args:
node(hou.Node)
Returns:
dict
"""
data = {}
if node.parm("trange") is None:
self.log.debug("Node has no 'trange' parameter: "
"{}".format(node.path()))
return data
if node.evalParm("trange") == 0:
# Ignore 'render current frame'
self.log.debug("Node '{}' has 'Render current frame' set. "
"Time range data ignored.".format(node.path()))
return data
data["frameStart"] = node.evalParm("f1")
data["frameEnd"] = node.evalParm("f2")
data["byFrameStep"] = node.evalParm("f3")
return data

View file

@ -91,27 +91,3 @@ class CollectInstances(pyblish.api.ContextPlugin):
context[:] = sorted(context, key=sort_by_family)
return context
def get_frame_data(self, node):
"""Get the frame data: start frame, end frame and steps
Args:
node(hou.Node)
Returns:
dict
"""
data = {}
if node.parm("trange") is None:
return data
if node.evalParm("trange") == 0:
return data
data["frameStart"] = node.evalParm("f1")
data["frameEnd"] = node.evalParm("f2")
data["byFrameStep"] = node.evalParm("f3")
return data

View file

@ -24,7 +24,9 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
"""
label = "Karma ROP Render Products"
order = pyblish.api.CollectorOrder + 0.4
# This specific order value is used so that
# this plugin runs after CollectRopFrameRange
order = pyblish.api.CollectorOrder + 0.4999
hosts = ["houdini"]
families = ["karma_rop"]
@ -95,8 +97,9 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
return path
expected_files = []
start = instance.data["frameStart"]
end = instance.data["frameEnd"]
start = instance.data["frameStartHandle"]
end = instance.data["frameEndHandle"]
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))

View file

@ -24,7 +24,9 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
"""
label = "Mantra ROP Render Products"
order = pyblish.api.CollectorOrder + 0.4
# This specific order value is used so that
# this plugin runs after CollectRopFrameRange
order = pyblish.api.CollectorOrder + 0.4999
hosts = ["houdini"]
families = ["mantra_rop"]
@ -118,8 +120,9 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
return path
expected_files = []
start = instance.data["frameStart"]
end = instance.data["frameEnd"]
start = instance.data["frameStartHandle"]
end = instance.data["frameEndHandle"]
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))

View file

@ -24,7 +24,9 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
"""
label = "Redshift ROP Render Products"
order = pyblish.api.CollectorOrder + 0.4
# This specific order value is used so that
# this plugin runs after CollectRopFrameRange
order = pyblish.api.CollectorOrder + 0.4999
hosts = ["houdini"]
families = ["redshift_rop"]
@ -132,8 +134,9 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
return path
expected_files = []
start = instance.data["frameStart"]
end = instance.data["frameEnd"]
start = instance.data["frameStartHandle"]
end = instance.data["frameEndHandle"]
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))

View file

@ -2,40 +2,106 @@
"""Collector plugin for frames data on ROP instances."""
import hou # noqa
import pyblish.api
from openpype.lib import BoolDef
from openpype.hosts.houdini.api import lib
from openpype.pipeline import OpenPypePyblishPluginMixin
class CollectRopFrameRange(pyblish.api.InstancePlugin):
class CollectRopFrameRange(pyblish.api.InstancePlugin,
OpenPypePyblishPluginMixin):
"""Collect all frames which would be saved from the ROP nodes"""
order = pyblish.api.CollectorOrder
hosts = ["houdini"]
# This specific order value is used so that
# this plugin runs after CollectAnatomyInstanceData
order = pyblish.api.CollectorOrder + 0.499
label = "Collect RopNode Frame Range"
use_asset_handles = True
def process(self, instance):
node_path = instance.data.get("instance_node")
if node_path is None:
# Instance without instance node like a workfile instance
self.log.debug(
"No instance node found for instance: {}".format(instance)
)
return
ropnode = hou.node(node_path)
frame_data = lib.get_frame_data(ropnode)
if "frameStart" in frame_data and "frameEnd" in frame_data:
attr_values = self.get_attr_values_from_data(instance.data)
# Log artist friendly message about the collected frame range
message = (
"Frame range {0[frameStart]} - {0[frameEnd]}"
).format(frame_data)
if frame_data.get("step", 1.0) != 1.0:
message += " with step {0[step]}".format(frame_data)
self.log.info(message)
if attr_values.get("use_handles", self.use_asset_handles):
asset_data = instance.data["assetEntity"]["data"]
handle_start = asset_data.get("handleStart", 0)
handle_end = asset_data.get("handleEnd", 0)
else:
handle_start = 0
handle_end = 0
instance.data.update(frame_data)
frame_data = lib.get_frame_data(
ropnode, handle_start, handle_end, self.log
)
# Add frame range to label if the instance has a frame range.
label = instance.data.get("label", instance.data["name"])
instance.data["label"] = (
"{0} [{1[frameStart]} - {1[frameEnd]}]".format(label,
frame_data)
if not frame_data:
return
# Log debug message about the collected frame range
frame_start = frame_data["frameStart"]
frame_end = frame_data["frameEnd"]
if attr_values.get("use_handles", self.use_asset_handles):
self.log.debug(
"Full Frame range with Handles "
"[{frame_start_handle} - {frame_end_handle}]"
.format(
frame_start_handle=frame_data["frameStartHandle"],
frame_end_handle=frame_data["frameEndHandle"]
)
)
else:
self.log.debug(
"Use handles is deactivated for this instance, "
"start and end handles are set to 0."
)
# Log collected frame range to the user
message = "Frame range [{frame_start} - {frame_end}]".format(
frame_start=frame_start,
frame_end=frame_end
)
if handle_start or handle_end:
message += " with handles [{handle_start}]-[{handle_end}]".format(
handle_start=handle_start,
handle_end=handle_end
)
self.log.info(message)
if frame_data.get("byFrameStep", 1.0) != 1.0:
self.log.info("Frame steps {}".format(frame_data["byFrameStep"]))
instance.data.update(frame_data)
# Add frame range to label if the instance has a frame range.
label = instance.data.get("label", instance.data["name"])
instance.data["label"] = (
"{label} [{frame_start} - {frame_end}]"
.format(
label=label,
frame_start=frame_start,
frame_end=frame_end
)
)
@classmethod
def get_attribute_defs(cls):
return [
BoolDef("use_handles",
tooltip="Disable this if you want the publisher to"
" ignore start and end handles specified in the"
" asset data for this publish instance",
default=cls.use_asset_handles,
label="Use asset handles")
]

View file

@ -24,7 +24,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
"""
label = "VRay ROP Render Products"
order = pyblish.api.CollectorOrder + 0.4
# This specific order value is used so that
# this plugin runs after CollectRopFrameRange
order = pyblish.api.CollectorOrder + 0.4999
hosts = ["houdini"]
families = ["vray_rop"]
@ -115,8 +117,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
return path
expected_files = []
start = instance.data["frameStart"]
end = instance.data["frameEnd"]
start = instance.data["frameStartHandle"]
end = instance.data["frameEndHandle"]
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))

View file

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
from openpype.pipeline.publish import RepairAction
from openpype.hosts.houdini.api.action import SelectInvalidAction
import hou
class DisableUseAssetHandlesAction(RepairAction):
label = "Disable use asset handles"
icon = "mdi.toggle-switch-off"
class ValidateFrameRange(pyblish.api.InstancePlugin):
"""Validate Frame Range.
Due to the usage of start and end handles,
then Frame Range must be >= (start handle + end handle)
which results that frameEnd be smaller than frameStart
"""
order = pyblish.api.ValidatorOrder - 0.1
hosts = ["houdini"]
label = "Validate Frame Range"
actions = [DisableUseAssetHandlesAction, SelectInvalidAction]
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
title="Invalid Frame Range",
message=(
"Invalid frame range because the instance "
"start frame ({0[frameStart]}) is higher than "
"the end frame ({0[frameEnd]})"
.format(instance.data)
),
description=(
"## Invalid Frame Range\n"
"The frame range for the instance is invalid because "
"the start frame is higher than the end frame.\n\nThis "
"is likely due to asset handles being applied to your "
"instance or the ROP node's start frame "
"is set higher than the end frame.\n\nIf your ROP frame "
"range is correct and you do not want to apply asset "
"handles make sure to disable Use asset handles on the "
"publish instance."
)
)
@classmethod
def get_invalid(cls, instance):
if not instance.data.get("instance_node"):
return
rop_node = hou.node(instance.data["instance_node"])
if instance.data["frameStart"] > instance.data["frameEnd"]:
cls.log.info(
"The ROP node render range is set to "
"{0[frameStartHandle]} - {0[frameEndHandle]} "
"The asset handles applied to the instance are start handle "
"{0[handleStart]} and end handle {0[handleEnd]}"
.format(instance.data)
)
return [rop_node]
@classmethod
def repair(cls, instance):
if not cls.get_invalid(instance):
# Already fixed
return
# Disable use asset handles
context = instance.context
create_context = context.data["create_context"]
instance_id = instance.data.get("instance_id")
if not instance_id:
cls.log.debug("'{}' must have instance id"
.format(instance))
return
created_instance = create_context.get_instance_by_id(instance_id)
if not instance_id:
cls.log.debug("Unable to find instance '{}' by id"
.format(instance))
return
created_instance.publish_attributes["CollectRopFrameRange"]["use_handles"] = False # noqa
create_context.save_changes()
cls.log.debug("use asset handles is turned off for '{}'"
.format(instance))

View file

@ -65,9 +65,11 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline):
job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S")
# Deadline requires integers in frame range
start = instance.data["frameStartHandle"]
end = instance.data["frameEndHandle"]
frames = "{start}-{end}x{step}".format(
start=int(instance.data["frameStart"]),
end=int(instance.data["frameEnd"]),
start=int(start),
end=int(end),
step=int(instance.data["byFrameStep"]),
)
job_info.Frames = frames

View file

@ -107,6 +107,9 @@
}
},
"publish": {
"CollectRopFrameRange": {
"use_asset_handles": true
},
"ValidateWorkfilePaths": {
"enabled": true,
"optional": true,

View file

@ -4,6 +4,27 @@
"key": "publish",
"label": "Publish plugins",
"children": [
{
"type":"label",
"label":"Collectors"
},
{
"type": "dict",
"collapsible": true,
"key": "CollectRopFrameRange",
"label": "Collect Rop Frame Range",
"children": [
{
"type": "label",
"label": "Disable this if you want the publisher to ignore start and end handles specified in the asset data for publish instances"
},
{
"type": "boolean",
"key": "use_asset_handles",
"label": "Use asset handles"
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -151,6 +151,17 @@ class ValidateWorkfilePathsModel(BaseSettingsModel):
)
class CollectRopFrameRangeModel(BaseSettingsModel):
"""Collect Frame Range
Disable this if you want the publisher to
ignore start and end handles specified in the
asset data for publish instances
"""
use_asset_handles: bool = Field(
title="Use asset handles")
class BasicValidateModel(BaseSettingsModel):
enabled: bool = Field(title="Enabled")
optional: bool = Field(title="Optional")
@ -158,6 +169,11 @@ class BasicValidateModel(BaseSettingsModel):
class PublishPluginsModel(BaseSettingsModel):
CollectRopFrameRange: CollectRopFrameRangeModel = Field(
default_factory=CollectRopFrameRangeModel,
title="Collect Rop Frame Range.",
section="Collectors"
)
ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field(
default_factory=ValidateWorkfilePathsModel,
title="Validate workfile paths settings.")
@ -179,6 +195,9 @@ class PublishPluginsModel(BaseSettingsModel):
DEFAULT_HOUDINI_PUBLISH_SETTINGS = {
"CollectRopFrameRange": {
"use_asset_handles": True
},
"ValidateWorkfilePaths": {
"enabled": True,
"optional": True,

View file

@ -1 +1 @@
__version__ = "0.1.5"
__version__ = "0.1.6"