Skip to content

Commit

Permalink
Merge branch 'develop' into enhancement/collect-render-layer-name-fro…
Browse files Browse the repository at this point in the history
…m-rop
  • Loading branch information
MustafaJafar committed Sep 19, 2024
2 parents d324d23 + 3c4ab39 commit 54aadd0
Show file tree
Hide file tree
Showing 38 changed files with 870 additions and 342 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/release_trigger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: 🚀 Release Trigger

on:
workflow_dispatch:

jobs:
call-release-trigger:
uses: ynput/automation/.github/workflows/release_trigger.yml@main
secrets:
token: ${{ secrets.YNPUT_BOT_TOKEN }}
email: ${{ secrets.CI_EMAIL }}
user: ${{ secrets.CI_USER }}
68 changes: 32 additions & 36 deletions client/ayon_houdini/api/colorspace.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,55 @@
from typing import List

import attr
import hou
from ayon_houdini.api.lib import get_color_management_preferences
from ayon_core.pipeline.colorspace import get_display_view_colorspace_name
from ayon_core.pipeline.colorspace import (
get_display_view_colorspace_name,
get_ocio_config_colorspaces
)


@attr.s
class LayerMetadata(object):
"""Data class for Render Layer metadata."""
frameStart = attr.ib()
frameEnd = attr.ib()
products: "List[RenderProduct]" = attr.ib()


@attr.s
class RenderProduct(object):
"""Getting Colorspace as
Specific Render Product Parameter for submitting
publish job.
"""
"""Specific Render Product Parameter for submitting."""
colorspace = attr.ib() # colorspace
view = attr.ib()
productName = attr.ib(default=None)


class ARenderProduct(object):
"""This is the minimal data structure required to get
`ayon_core.pipeline.farm.pyblish_functions.create_instances_for_aov` to
work with deadline addon's job submissions."""
# TODO: The exact data structure should actually be defined in core for all
# addons to align.
def __init__(self, aov_names: List[str]):
colorspace = get_scene_linear_colorspace()
products = [
RenderProduct(colorspace=colorspace, productName=aov_name)
for aov_name in aov_names
]
self.layer_data = LayerMetadata(products=products)

def __init__(self):
"""Constructor."""
# Initialize
self.layer_data = self._get_layer_data()
self.layer_data.products = self.get_colorspace_data()

def _get_layer_data(self):
return LayerMetadata(
frameStart=int(hou.playbar.frameRange()[0]),
frameEnd=int(hou.playbar.frameRange()[1]),
)

def get_colorspace_data(self):
"""To be implemented by renderer class.

This should return a list of RenderProducts.
def get_scene_linear_colorspace():
"""Return colorspace name for Houdini's OCIO config scene linear role.
Returns:
list: List of RenderProduct
By default, renderers in Houdini render output images in the scene linear
role colorspace.
"""
data = get_color_management_preferences()
colorspace_data = [
RenderProduct(
colorspace=data["display"],
view=data["view"],
productName=""
)
]
return colorspace_data
Returns:
Optional[str]: The colorspace name for the 'scene_linear' role in
the OCIO config Houdini is currently set to.
"""
ocio_config_path = hou.Color.ocio_configPath()
colorspaces = get_ocio_config_colorspaces(ocio_config_path)
return colorspaces["roles"].get("scene_linear", {}).get("colorspace")


def get_default_display_view_colorspace():
Expand Down
26 changes: 19 additions & 7 deletions client/ayon_houdini/api/hda_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from ayon_core.style import load_stylesheet

from ayon_houdini.api import lib
from .usd import get_ayon_entity_uri_from_representation_context

from qtpy import QtCore, QtWidgets, QtGui
import hou
Expand Down Expand Up @@ -179,11 +180,19 @@ def set_representation(node, representation_id: str):

context = get_representation_context(project_name, repre_entity)
update_info(node, context)
path = get_representation_path_from_context(context)
# Load fails on UNC paths with backslashes and also
# fails to resolve @sourcename var with backslashed
# paths correctly. So we force forward slashes
path = path.replace("\\", "/")

if node.parm("use_ayon_entity_uri"):
use_ayon_entity_uri = node.evalParm("use_ayon_entity_uri")
else:
use_ayon_entity_uri = False
if use_ayon_entity_uri:
path = get_ayon_entity_uri_from_representation_context(context)
else:
path = get_representation_path_from_context(context)
# Load fails on UNC paths with backslashes and also
# fails to resolve @sourcename var with backslashed
# paths correctly. So we force forward slashes
path = path.replace("\\", "/")
with _unlocked_parm(file_parm):
file_parm.set(path)

Expand Down Expand Up @@ -255,14 +264,17 @@ def on_representation_id_changed(node):
set_representation(node, repre_id)


def on_representation_parms_changed(node):
def on_representation_parms_changed(node, force=False):
"""
Usually used as callback to the project, folder, product, version and
representation parms which on change - would result in a different
representation id to be resolved.
Args:
node (hou.Node): Node to update.
force (Optional[bool]): Whether to force the callback to retrigger
even if the representation id already matches. For example, when
needing to resolve the filepath in a different way.
"""
project_name = node.evalParm("project_name") or get_current_project_name()
representation_id = get_representation_id(
Expand All @@ -278,7 +290,7 @@ def on_representation_parms_changed(node):
else:
representation_id = str(representation_id)

if node.evalParm("representation") != representation_id:
if force or node.evalParm("representation") != representation_id:
node.parm("representation").set(representation_id)
node.parm("representation").pressButton() # trigger callback

Expand Down
66 changes: 63 additions & 3 deletions client/ayon_houdini/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
from ayon_core.pipeline.create import CreateContext
from ayon_core.pipeline.template_data import get_template_data
from ayon_core.pipeline.context_tools import get_current_task_entity
from ayon_core.pipeline.workfile.workfile_template_builder import (
TemplateProfileNotFound
)
from ayon_core.tools.utils import PopupUpdateKeys, SimplePopup
from ayon_core.tools.utils.host_tools import get_tool_by_name

Expand Down Expand Up @@ -146,11 +149,26 @@ def validate_fps():
return True


def render_rop(ropnode):
def render_rop(ropnode, frame_range=None):
"""Render ROP node utility for Publishing.
This renders a ROP node with the settings we want during Publishing.
Args:
ropnode (hou.RopNode): Node to render
frame_range (tuple): Copied from Houdini's help..
Sequence of 2 or 3 values, overrides the frame range and frame
increment to render. The first two values specify the start and
end frames, and the third value (if given) specifies the frame
increment. If no frame increment is given and the ROP node
doesn't specify a frame increment, then a value of 1 will be
used. If no frame range is given, and the ROP node doesn't
specify a frame range, then the current frame will be rendered.
"""

if frame_range is None:
frame_range = ()

# Print verbose when in batch mode without UI
verbose = not hou.isUIAvailable()

Expand All @@ -161,7 +179,8 @@ def render_rop(ropnode):
output_progress=verbose,
# Render only this node
# (do not render any of its dependencies)
ignore_inputs=True)
ignore_inputs=True,
frame_range=frame_range)
except hou.Error as exc:
# The hou.Error is not inherited from a Python Exception class,
# so we explicitly capture the houdini error, otherwise pyblish
Expand Down Expand Up @@ -579,12 +598,41 @@ def replace(match):

def get_color_management_preferences():
"""Get default OCIO preferences"""
return {

preferences = {
"config": hou.Color.ocio_configPath(),
"display": hou.Color.ocio_defaultDisplay(),
"view": hou.Color.ocio_defaultView()
}

# Note: For whatever reason they are cases where `view` may be an empty
# string even though a valid default display is set where `PyOpenColorIO`
# does correctly return the values.
# Workaround to get the correct default view
if preferences["config"] and not preferences["view"]:
log.debug(
"Houdini `hou.Color.ocio_defaultView()` returned empty value."
" Falling back to `PyOpenColorIO` to get the default view.")
try:
import PyOpenColorIO
except ImportError:
log.warning(
"Unable to workaround empty return value of "
"`hou.Color.ocio_defaultView()` because `PyOpenColorIO` is "
"not available.")
return preferences

config_path = preferences["config"]
config = PyOpenColorIO.Config.CreateFromFile(config_path)
display = config.getDefaultDisplay()
assert display == preferences["display"], \
"Houdini default OCIO display must match config default display"
view = config.getDefaultView(display)
preferences["display"] = display
preferences["view"] = view

return preferences


def get_obj_node_output(obj_node):
"""Find output node.
Expand Down Expand Up @@ -1360,3 +1408,15 @@ def prompt_reset_context():
update_content_on_context_change()

dialog.deleteLater()


def start_workfile_template_builder():
from .workfile_template_builder import (
build_workfile_template
)

log.info("Starting workfile template builder...")
try:
build_workfile_template(workfile_creation_enabled=True)
except TemplateProfileNotFound:
log.warning("Template profile not found. Skipping...")
11 changes: 5 additions & 6 deletions client/ayon_houdini/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,10 @@ def install(self):

self._has_been_setup = True

# Set folder settings for the empty scene directly after launch of
# Houdini so it initializes into the correct scene FPS,
# Frame Range, etc.
# TODO: make sure this doesn't trigger when
# opening with last workfile.
_set_context_settings()
# Manually call on_new callback as it doesn't get called when AYON
# launches for the first time on a context, only when going to
# File -> New
on_new()

if not IS_HEADLESS:
import hdefereval # noqa, hdefereval is only available in ui mode
Expand Down Expand Up @@ -414,6 +412,7 @@ def _enforce_start_frame():

if hou.isUIAvailable():
import hdefereval
hdefereval.executeDeferred(lib.start_workfile_template_builder)
hdefereval.executeDeferred(_enforce_start_frame)
else:
# Run without execute deferred when no UI is available because
Expand Down
43 changes: 42 additions & 1 deletion client/ayon_houdini/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import six
import hou

import clique
import pyblish.api
from ayon_core.pipeline import (
CreatorError,
Expand All @@ -19,7 +20,8 @@
)
from ayon_core.lib import BoolDef

from .lib import imprint, read, lsattr, add_self_publish_button
from .lib import imprint, read, lsattr, add_self_publish_button, render_rop
from .usd import get_ayon_entity_uri_from_representation_context


SETTINGS_CATEGORY = "houdini"
Expand Down Expand Up @@ -316,6 +318,14 @@ class HoudiniLoader(load.LoaderPlugin):

hosts = ["houdini"]
settings_category = SETTINGS_CATEGORY
use_ayon_entity_uri = False

@classmethod
def filepath_from_context(cls, context):
if cls.use_ayon_entity_uri:
return get_ayon_entity_uri_from_representation_context(context)

return super(HoudiniLoader, cls).filepath_from_context(context)


class HoudiniInstancePlugin(pyblish.api.InstancePlugin):
Expand Down Expand Up @@ -345,3 +355,34 @@ class HoudiniExtractorPlugin(publish.Extractor):

hosts = ["houdini"]
settings_category = SETTINGS_CATEGORY

def render_rop(self, instance: pyblish.api.Instance):
"""Render the ROP node of the instance.
If `instance.data["frames_to_fix"]` is set and is not empty it will
be interpreted as a set of frames that will be rendered instead of the
full rop nodes frame range.
Only `instance.data["instance_node"]` is required.
"""
# Log the start of the render
rop_node = hou.node(instance.data["instance_node"])
self.log.debug(f"Rendering {rop_node.path()}")

frames_to_fix = clique.parse(instance.data.get("frames_to_fix", ""),
"{ranges}")
if len(set(frames_to_fix)) < 2:
render_rop(rop_node)
return

# Render only frames to fix
for frame_range in frames_to_fix.separate():
frame_range = list(frame_range)
first_frame = int(frame_range[0])
last_frame = int(frame_range[-1])
self.log.debug(
f"Rendering frames to fix [{first_frame}, {last_frame}]"
)
# for step to be 1 since clique doesn't support steps.
frame_range = (first_frame, last_frame, 1)
render_rop(rop_node, frame_range=frame_range)
Loading

0 comments on commit 54aadd0

Please sign in to comment.