Merge remote-tracking branch 'upstream/develop' into bugfix/maya_burnin_focallength_renders

# Conflicts:
#	openpype/settings/defaults/project_settings/global.json
This commit is contained in:
Roy Nieterau 2023-04-12 17:06:51 +02:00
commit aa20e3d42b
52 changed files with 1200 additions and 217 deletions

View file

@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Running version**
[ex. 3.14.1-nightly.2]
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. windows]
- Host: [e.g. Maya, Nuke, Houdini]
**Additional context**
Add any other context about the problem here.

183
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,183 @@
name: Bug Report
description: File a bug report
title: 'Bug: '
labels:
- 'type: bug'
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: >-
Please search to see if an issue already exists for the bug you
encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: 'Current Behavior:'
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: 'Expected Behavior:'
description: A concise description of what you expected to happen.
validations:
required: false
- type: dropdown
id: _version
attributes:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
- 3.15.4-nightly.3
- 3.15.4-nightly.2
- 3.15.4-nightly.1
- 3.15.3
- 3.15.3-nightly.4
- 3.15.3-nightly.3
- 3.15.3-nightly.2
- 3.15.3-nightly.1
- 3.15.2
- 3.15.2-nightly.6
- 3.15.2-nightly.5
- 3.15.2-nightly.4
- 3.15.2-nightly.3
- 3.15.2-nightly.2
- 3.15.2-nightly.1
- 3.15.1
- 3.15.1-nightly.6
- 3.15.1-nightly.5
- 3.15.1-nightly.4
- 3.15.1-nightly.3
- 3.15.1-nightly.2
- 3.15.1-nightly.1
- 3.15.0
- 3.15.0-nightly.1
- 3.14.11-nightly.4
- 3.14.11-nightly.3
- 3.14.11-nightly.2
- 3.14.11-nightly.1
- 3.14.10
- 3.14.10-nightly.9
- 3.14.10-nightly.8
- 3.14.10-nightly.7
- 3.14.10-nightly.6
- 3.14.10-nightly.5
- 3.14.10-nightly.4
- 3.14.10-nightly.3
- 3.14.10-nightly.2
- 3.14.10-nightly.1
- 3.14.9
- 3.14.9-nightly.5
- 3.14.9-nightly.4
- 3.14.9-nightly.3
- 3.14.9-nightly.2
- 3.14.9-nightly.1
- 3.14.8
- 3.14.8-nightly.4
- 3.14.8-nightly.3
- 3.14.8-nightly.2
- 3.14.8-nightly.1
- 3.14.7
- 3.14.7-nightly.8
- 3.14.7-nightly.7
- 3.14.7-nightly.6
- 3.14.7-nightly.5
- 3.14.7-nightly.4
- 3.14.7-nightly.3
- 3.14.7-nightly.2
- 3.14.7-nightly.1
- 3.14.6
- 3.14.6-nightly.3
- 3.14.6-nightly.2
- 3.14.6-nightly.1
- 3.14.5
- 3.14.5-nightly.3
- 3.14.5-nightly.2
- 3.14.5-nightly.1
- 3.14.4
- 3.14.4-nightly.4
- 3.14.4-nightly.3
- 3.14.4-nightly.2
- 3.14.4-nightly.1
- 3.14.3
- 3.14.3-nightly.7
- 3.14.3-nightly.6
- 3.14.3-nightly.5
- 3.14.3-nightly.4
- 3.14.3-nightly.3
- 3.14.3-nightly.2
- 3.14.3-nightly.1
- 3.14.2
- 3.14.2-nightly.5
- 3.14.2-nightly.4
- 3.14.2-nightly.3
- 3.14.2-nightly.2
- 3.14.2-nightly.1
- 3.14.1
- 3.14.1-nightly.4
- 3.14.1-nightly.3
- 3.14.1-nightly.2
- 3.14.1-nightly.1
- 3.14.0
- 3.14.0-nightly.1
- 3.13.1-nightly.3
- 3.13.1-nightly.2
- 3.13.1-nightly.1
- 3.13.0
- 3.13.0-nightly.1
- 3.12.3-nightly.3
- 3.12.3-nightly.2
- 3.12.3-nightly.1
validations:
required: true
- type: dropdown
validations:
required: true
attributes:
label: What platform you are running OpenPype on?
description: |
Please specify the operating systems you are running OpenPype with.
multiple: true
options:
- Windows
- Linux / Centos
- Linux / Ubuntu
- Linux / RedHat
- MacOS
- type: textarea
id: to-reproduce
attributes:
label: 'Steps To Reproduce:'
description: Steps to reproduce the behavior.
placeholder: |
1. How did the configuration look like
2. What type of action was made
validations:
required: true
- type: checkboxes
attributes:
label: Are there any labels you wish to add?
description: Please search labels and identify those related to your bug.
options:
- label: I have added the relevant labels to the bug report.
required: true
- type: textarea
id: logs
attributes:
label: 'Relevant log output:'
description: >-
Please copy and paste any relevant log output. This will be
automatically formatted into code, so no need for backticks.
render: shell
- type: textarea
id: additional-context
attributes:
label: 'Additional context:'
description: Add any other context about the problem here.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Ynput Discord Server
url: https://discord.gg/ynput
about: For community quick chats.

View file

@ -0,0 +1,52 @@
name: Enhancement Request
description: Create a report to help us enhance a particular feature
title: "Enhancement: "
labels:
- "type: enhancement"
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this enhancement request report!
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues.
required: true
- type: textarea
id: related-feature
attributes:
label: Please describe the feature you have in mind and explain what the current shortcomings are?
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
id: enhancement-proposal
attributes:
label: How would you imagine the implementation of the feature?
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: checkboxes
attributes:
label: Are there any labels you wish to add?
description: Please search labels and identify those related to your enhancement.
options:
- label: I have added the relevant labels to the enhancement request.
required: true
- type: textarea
id: alternatives
attributes:
label: "Describe alternatives you've considered:"
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: "Additional context:"
description: Add any other context or screenshots about the enhancement request here.
validations:
required: false

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,4 +1,4 @@
name: documentation
name: 📜 Documentation
on:
pull_request:

View file

@ -1,4 +1,4 @@
name: Milestone - assign to PRs
name: 👉🏻 Milestone - assign to PRs
on:
pull_request_target:

View file

@ -1,4 +1,4 @@
name: Milestone - create default
name: Milestone - create default
on:
milestone:

View file

@ -1,4 +1,4 @@
name: Milestone Release [trigger]
name: 🚩 Milestone Release [trigger]
on:
workflow_dispatch:

View file

@ -1,4 +1,4 @@
name: Dev -> Main
name: 🔀 Dev -> Main
on:
schedule:

49
.github/workflows/pr_labels.yml vendored Normal file
View file

@ -0,0 +1,49 @@
name: 🔖 PR labels
on:
pull_request_target:
types: [opened, assigned]
jobs:
size-label:
name: pr_size_label
runs-on: ubuntu-latest
if: github.event.action == 'assigned' || github.event.action == 'opened'
steps:
- name: Add size label
uses: "pascalgn/size-label-action@v0.4.3"
env:
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
IGNORED: ".gitignore\n*.md\n*.json"
with:
sizes: >
{
"0": "XS",
"100": "S",
"500": "M",
"1000": "L",
"1500": "XL",
"2500": "XXL"
}
label_prs_branch:
name: pr_branch_label
runs-on: ubuntu-latest
if: github.event.action == 'assigned' || github.event.action == 'opened'
steps:
- name: Label PRs - Branch name detection
uses: ffittschen/pr-branch-labeler@v1
with:
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
label_prs_globe:
name: pr_globe_label
runs-on: ubuntu-latest
if: github.event.action == 'assigned' || github.event.action == 'opened'
steps:
- name: Label PRs - Globe detection
uses: actions/labeler@v4.0.3
with:
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
configuration-path: ".github/pr-glob-labeler.yml"
sync-labels: false

View file

@ -1,4 +1,4 @@
name: Nightly Prerelease
name: Nightly Prerelease
on:
workflow_dispatch:

View file

@ -1,8 +1,6 @@
name: project-actions
name: 📊 Project task statuses
on:
pull_request_target:
types: [opened, assigned]
pull_request_review:
types: [submitted]
issue_comment:
@ -70,46 +68,3 @@ jobs:
-d '{
"status": "in progress"
}'
size-label:
name: pr_size_label
runs-on: ubuntu-latest
if: github.event.action == 'assigned' || github.event.action == 'opened'
steps:
- name: Add size label
uses: "pascalgn/size-label-action@v0.4.3"
env:
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
IGNORED: ".gitignore\n*.md\n*.json"
with:
sizes: >
{
"0": "XS",
"100": "S",
"500": "M",
"1000": "L",
"1500": "XL",
"2500": "XXL"
}
label_prs_branch:
name: pr_branch_label
runs-on: ubuntu-latest
if: github.event.action == 'assigned' || github.event.action == 'opened'
steps:
- name: Label PRs - Branch name detection
uses: ffittschen/pr-branch-labeler@v1
with:
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
label_prs_globe:
name: pr_globe_label
runs-on: ubuntu-latest
if: github.event.action == 'assigned' || github.event.action == 'opened'
steps:
- name: Label PRs - Globe detection
uses: actions/labeler@v4.0.3
with:
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
configuration-path: ".github/pr-glob-labeler.yml"
sync-labels: false

View file

@ -1,7 +1,7 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Test Build
name: 🏗️ Test Build
on:
pull_request:

25
.github/workflows/update_bug_report.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: 🐞 Update Bug Report
on:
workflow_dispatch:
release:
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release
types: [published]
jobs:
update-bug-report:
runs-on: ubuntu-latest
name: Update bug report
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.release.target_commitish }}
- name: Update version
uses: ynput/gha-populate-form-version@main
with:
github_token: ${{ secrets.YNPUT_BOT_TOKEN }}
registry: github
dropdown: _version
limit_to: 100
form: .github/ISSUE_TEMPLATE/bug_report.yml
commit_message: 'chore(): update bug report / version'

View file

@ -0,0 +1,185 @@
"""Library to register OpenPype Creators for Houdini TAB node search menu.
This can be used to install custom houdini tools for the TAB search
menu which will trigger a publish instance to be created interactively.
The Creators are automatically registered on launch of Houdini through the
Houdini integration's `host.install()` method.
"""
import contextlib
import tempfile
import logging
import os
from openpype.pipeline import registered_host
from openpype.pipeline.create import CreateContext
from openpype.resources import get_openpype_icon_filepath
import hou
log = logging.getLogger(__name__)
CREATE_SCRIPT = """
from openpype.hosts.houdini.api.creator_node_shelves import create_interactive
create_interactive("{identifier}")
"""
def create_interactive(creator_identifier):
"""Create a Creator using its identifier interactively.
This is used by the generated shelf tools as callback when a user selects
the creator from the node tab search menu.
Args:
creator_identifier (str): The creator identifier of the Creator plugin
to create.
Return:
list: The created instances.
"""
# TODO Use Qt instead
result, variant = hou.ui.readInput('Define variant name',
buttons=("Ok", "Cancel"),
initial_contents='Main',
title="Define variant",
help="Set the variant for the "
"publish instance",
close_choice=1)
if result == 1:
# User interrupted
return
variant = variant.strip()
if not variant:
raise RuntimeError("Empty variant value entered.")
host = registered_host()
context = CreateContext(host)
before = context.instances_by_id.copy()
# Create the instance
context.create(
creator_identifier=creator_identifier,
variant=variant,
pre_create_data={"use_selection": True}
)
# For convenience we set the new node as current since that's much more
# familiar to the artist when creating a node interactively
# TODO Allow to disable auto-select in studio settings or user preferences
after = context.instances_by_id
new = set(after) - set(before)
if new:
# Select the new instance
for instance_id in new:
instance = after[instance_id]
node = hou.node(instance.get("instance_node"))
node.setCurrent(True)
return list(new)
@contextlib.contextmanager
def shelves_change_block():
"""Write shelf changes at the end of the context."""
hou.shelves.beginChangeBlock()
try:
yield
finally:
hou.shelves.endChangeBlock()
def install():
"""Install the Creator plug-ins to show in Houdini's TAB node search menu.
This function is re-entrant and can be called again to reinstall and
update the node definitions. For example during development it can be
useful to call it manually:
>>> from openpype.hosts.houdini.api.creator_node_shelves import install
>>> install()
Returns:
list: List of `hou.Tool` instances
"""
host = registered_host()
# Store the filepath on the host
# TODO: Define a less hacky static shelf path for current houdini session
filepath_attr = "_creator_node_shelf_filepath"
filepath = getattr(host, filepath_attr, None)
if filepath is None:
f = tempfile.NamedTemporaryFile(prefix="houdini_creator_nodes_",
suffix=".shelf",
delete=False)
f.close()
filepath = f.name
setattr(host, filepath_attr, filepath)
elif os.path.exists(filepath):
# Remove any existing shelf file so that we can completey regenerate
# and update the tools file if creator identifiers change
os.remove(filepath)
icon = get_openpype_icon_filepath()
# Create context only to get creator plugins, so we don't reset and only
# populate what we need to retrieve the list of creator plugins
create_context = CreateContext(host, reset=False)
create_context.reset_current_context()
create_context._reset_creator_plugins()
log.debug("Writing OpenPype Creator nodes to shelf: {}".format(filepath))
tools = []
with shelves_change_block():
for identifier, creator in create_context.manual_creators.items():
# TODO: Allow the creator plug-in itself to override the categories
# for where they are shown, by e.g. defining
# `Creator.get_network_categories()`
key = "openpype_create.{}".format(identifier)
log.debug(f"Registering {key}")
script = CREATE_SCRIPT.format(identifier=identifier)
data = {
"script": script,
"language": hou.scriptLanguage.Python,
"icon": icon,
"help": "Create OpenPype publish instance for {}".format(
creator.label
),
"help_url": None,
"network_categories": [
hou.ropNodeTypeCategory(),
hou.sopNodeTypeCategory()
],
"viewer_categories": [],
"cop_viewer_categories": [],
"network_op_type": None,
"viewer_op_type": None,
"locations": ["OpenPype"]
}
label = "Create {}".format(creator.label)
tool = hou.shelves.tool(key)
if tool:
tool.setData(**data)
tool.setLabel(label)
else:
tool = hou.shelves.newTool(
file_path=filepath,
name=key,
label=label,
**data
)
tools.append(tool)
# Ensure the shelf is reloaded
hou.shelves.loadFile(filepath)
return tools

View file

@ -127,6 +127,8 @@ def get_output_parameter(node):
return node.parm("filename")
elif node_type == "comp":
return node.parm("copoutput")
elif node_type == "opengl":
return node.parm("picture")
elif node_type == "arnold":
if node.evalParm("ar_ass_export_enable"):
return node.parm("ar_ass_file")

View file

@ -18,7 +18,7 @@ from openpype.pipeline import (
)
from openpype.pipeline.load import any_outdated_containers
from openpype.hosts.houdini import HOUDINI_HOST_DIR
from openpype.hosts.houdini.api import lib, shelves
from openpype.hosts.houdini.api import lib, shelves, creator_node_shelves
from openpype.lib import (
register_event_callback,
@ -83,6 +83,10 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
_set_context_settings()
shelves.generate_shelves()
if not IS_HEADLESS:
import hdefereval # noqa, hdefereval is only available in ui mode
hdefereval.executeDeferred(creator_node_shelves.install)
def has_unsaved_changes(self):
return hou.hipFile.hasUnsavedChanges()

View file

@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating openGL reviews."""
from openpype.hosts.houdini.api import plugin
from openpype.lib import EnumDef, BoolDef, NumberDef
class CreateReview(plugin.HoudiniCreator):
"""Review with OpenGL ROP"""
identifier = "io.openpype.creators.houdini.review"
label = "Review"
family = "review"
icon = "video-camera"
def create(self, subset_name, instance_data, pre_create_data):
import hou
instance_data.pop("active", None)
instance_data.update({"node_type": "opengl"})
instance_data["imageFormat"] = pre_create_data.get("imageFormat")
instance_data["keepImages"] = pre_create_data.get("keepImages")
instance = super(CreateReview, self).create(
subset_name,
instance_data,
pre_create_data)
instance_node = hou.node(instance.get("instance_node"))
frame_range = hou.playbar.frameRange()
filepath = "{root}/{subset}/{subset}.$F4.{ext}".format(
root=hou.text.expandString("$HIP/pyblish"),
subset="`chs(\"subset\")`", # keep dynamic link to subset
ext=pre_create_data.get("image_format") or "png"
)
parms = {
"picture": filepath,
"trange": 1,
# Unlike many other ROP nodes the opengl node does not default
# to expression of $FSTART and $FEND so we preserve that behavior
# but do set the range to the frame range of the playbar
"f1": frame_range[0],
"f2": frame_range[1],
}
override_resolution = pre_create_data.get("override_resolution")
if override_resolution:
parms.update({
"tres": override_resolution,
"res1": pre_create_data.get("resx"),
"res2": pre_create_data.get("resy"),
"aspect": pre_create_data.get("aspect"),
})
if self.selected_nodes:
# The first camera found in selection we will use as camera
# Other node types we set in force objects
camera = None
force_objects = []
for node in self.selected_nodes:
path = node.path()
if node.type().name() == "cam":
if camera:
continue
camera = path
else:
force_objects.append(path)
if not camera:
self.log.warning("No camera found in selection.")
parms.update({
"camera": camera or "",
"scenepath": "/obj",
"forceobjects": " ".join(force_objects),
"vobjects": "" # clear candidate objects from '*' value
})
instance_node.setParms(parms)
to_lock = ["id", "family"]
self.lock_parameters(instance_node, to_lock)
def get_pre_create_attr_defs(self):
attrs = super(CreateReview, self).get_pre_create_attr_defs()
image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
]
return attrs + [
BoolDef("keepImages",
label="Keep Image Sequences",
default=False),
EnumDef("imageFormat",
image_format_enum,
default="png",
label="Image Format Options"),
BoolDef("override_resolution",
label="Override resolution",
tooltip="When disabled the resolution set on the camera "
"is used instead.",
default=True),
NumberDef("resx",
label="Resolution Width",
default=1280,
minimum=2,
decimals=0),
NumberDef("resy",
label="Resolution Height",
default=720,
minimum=2,
decimals=0),
NumberDef("aspect",
label="Aspect Ratio",
default=1.0,
minimum=0.0001,
decimals=3)
]

View file

@ -14,7 +14,7 @@ class CollectFrames(pyblish.api.InstancePlugin):
order = pyblish.api.CollectorOrder
label = "Collect Frames"
families = ["vdbcache", "imagesequence", "ass", "redshiftproxy"]
families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "review"]
def process(self, instance):

View file

@ -0,0 +1,52 @@
import hou
import pyblish.api
class CollectHoudiniReviewData(pyblish.api.InstancePlugin):
"""Collect Review Data."""
label = "Collect Review Data"
order = pyblish.api.CollectorOrder + 0.1
hosts = ["houdini"]
families = ["review"]
def process(self, instance):
# This fixes the burnin having the incorrect start/end timestamps
# because without this it would take it from the context instead
# which isn't the actual frame range that this instance renders.
instance.data["handleStart"] = 0
instance.data["handleEnd"] = 0
# Get the camera from the rop node to collect the focal length
ropnode_path = instance.data["instance_node"]
ropnode = hou.node(ropnode_path)
camera_path = ropnode.parm("camera").eval()
camera_node = hou.node(camera_path)
if not camera_node:
raise RuntimeError("No valid camera node found on review node: "
"{}".format(camera_path))
# Collect focal length.
focal_length_parm = camera_node.parm("focal")
if not focal_length_parm:
self.log.warning("No 'focal' (focal length) parameter found on "
"camera: {}".format(camera_path))
return
if focal_length_parm.isTimeDependent():
start = instance.data["frameStart"]
end = instance.data["frameEnd"] + 1
focal_length = [
focal_length_parm.evalAsFloatAtFrame(t)
for t in range(int(start), int(end))
]
else:
focal_length = focal_length_parm.evalAsFloat()
# Store focal length in `burninDataMembers`
burnin_members = instance.data.setdefault("burninDataMembers", {})
burnin_members["focalLength"] = focal_length
instance.data.setdefault("families", []).append('ftrack')

View file

@ -0,0 +1,58 @@
import os
import pyblish.api
from openpype.pipeline import (
publish,
OptionalPyblishPluginMixin
)
from openpype.hosts.houdini.api.lib import render_rop
import hou
class ExtractOpenGL(publish.Extractor,
OptionalPyblishPluginMixin):
order = pyblish.api.ExtractorOrder - 0.01
label = "Extract OpenGL"
families = ["review"]
hosts = ["houdini"]
optional = True
def process(self, instance):
if not self.is_active(instance.data):
return
ropnode = hou.node(instance.data.get("instance_node"))
output = ropnode.evalParm("picture")
staging_dir = os.path.normpath(os.path.dirname(output))
instance.data["stagingDir"] = staging_dir
file_name = os.path.basename(output)
self.log.info("Extracting '%s' to '%s'" % (file_name,
staging_dir))
render_rop(ropnode)
output = instance.data["frames"]
tags = ["review"]
if not instance.data.get("keepImages"):
tags.append("delete")
representation = {
"name": instance.data["imageFormat"],
"ext": instance.data["imageFormat"],
"files": output,
"stagingDir": staging_dir,
"frameStart": instance.data["frameStart"],
"frameEnd": instance.data["frameEnd"],
"tags": tags,
"preview": True,
"camera_name": instance.data.get("review_camera")
}
if "representations" not in instance.data:
instance.data["representations"] = []
instance.data["representations"].append(representation)

View file

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
import hou
class ValidateSceneReview(pyblish.api.InstancePlugin):
"""Validator Some Scene Settings before publishing the review
1. Scene Path
2. Resolution
"""
order = pyblish.api.ValidatorOrder
families = ["review"]
hosts = ["houdini"]
label = "Scene Setting for review"
def process(self, instance):
invalid = self.get_invalid_scene_path(instance)
report = []
if invalid:
report.append(
"Scene path does not exist: '%s'" % invalid[0],
)
invalid = self.get_invalid_resolution(instance)
if invalid:
report.extend(invalid)
if report:
raise PublishValidationError(
"\n\n".join(report),
title=self.label)
def get_invalid_scene_path(self, instance):
node = hou.node(instance.data.get("instance_node"))
scene_path_parm = node.parm("scenepath")
scene_path_node = scene_path_parm.evalAsNode()
if not scene_path_node:
return [scene_path_parm.evalAsString()]
def get_invalid_resolution(self, instance):
node = hou.node(instance.data.get("instance_node"))
# The resolution setting is only used when Override Camera Resolution
# is enabled. So we skip validation if it is disabled.
override = node.parm("tres").eval()
if not override:
return
invalid = []
res_width = node.parm("res1").eval()
res_height = node.parm("res2").eval()
if res_width == 0:
invalid.append("Override Resolution width is set to zero.")
if res_height == 0:
invalid.append("Override Resolution height is set to zero")
return invalid

View file

@ -3655,7 +3655,17 @@ def get_color_management_preferences():
# Split view and display from view_transform. view_transform comes in
# format of "{view} ({display})".
regex = re.compile(r"^(?P<view>.+) \((?P<display>.+)\)$")
if int(cmds.about(version=True)) <= 2020:
# view_transform comes in format of "{view} {display}" in 2020.
regex = re.compile(r"^(?P<view>.+) (?P<display>.+)$")
match = regex.match(data["view_transform"])
if not match:
raise ValueError(
"Unable to parse view and display from Maya view transform: '{}' "
"using regex '{}'".format(data["view_transform"], regex.pattern)
)
data.update({
"display": match.group("display"),
"view": match.group("view")

View file

@ -1,4 +1,5 @@
import os
import re
from maya import cmds
@ -12,6 +13,7 @@ from openpype.pipeline import (
AVALON_CONTAINER_ID,
Anatomy,
)
from openpype.pipeline.load import LoadError
from openpype.settings import get_project_settings
from .pipeline import containerise
from . import lib
@ -82,6 +84,44 @@ def get_reference_node_parents(ref):
return parents
def get_custom_namespace(custom_namespace):
"""Return unique namespace.
The input namespace can contain a single group
of '#' number tokens to indicate where the namespace's
unique index should go. The amount of tokens defines
the zero padding of the number, e.g ### turns into 001.
Warning: Note that a namespace will always be
prefixed with a _ if it starts with a digit
Example:
>>> get_custom_namespace("myspace_##_")
# myspace_01_
>>> get_custom_namespace("##_myspace")
# _01_myspace
>>> get_custom_namespace("myspace##")
# myspace01
"""
split = re.split("([#]+)", custom_namespace, 1)
if len(split) == 3:
base, padding, suffix = split
padding = "%0{}d".format(len(padding))
else:
base = split[0]
padding = "%02d" # default padding
suffix = ""
return lib.unique_namespace(
base,
format=padding,
prefix="_" if not base or base[0].isdigit() else "",
suffix=suffix
)
class Creator(LegacyCreator):
defaults = ['Main']
@ -143,15 +183,46 @@ class ReferenceLoader(Loader):
assert os.path.exists(self.fname), "%s does not exist." % self.fname
asset = context['asset']
subset = context['subset']
settings = get_project_settings(context['project']['name'])
custom_naming = settings['maya']['load']['reference_loader']
loaded_containers = []
count = options.get("count") or 1
for c in range(0, count):
namespace = namespace or lib.unique_namespace(
"{}_{}_".format(asset["name"], context["subset"]["name"]),
prefix="_" if asset["name"][0].isdigit() else "",
suffix="_",
if not custom_naming['namespace']:
raise LoadError("No namespace specified in "
"Maya ReferenceLoader settings")
elif not custom_naming['group_name']:
raise LoadError("No group name specified in "
"Maya ReferenceLoader settings")
formatting_data = {
"asset_name": asset['name'],
"asset_type": asset['type'],
"subset": subset['name'],
"family": (
subset['data'].get('family') or
subset['data']['families'][0]
)
}
custom_namespace = custom_naming['namespace'].format(
**formatting_data
)
custom_group_name = custom_naming['group_name'].format(
**formatting_data
)
count = options.get("count") or 1
for c in range(0, count):
namespace = get_custom_namespace(custom_namespace)
group_name = "{}:{}".format(
namespace,
custom_group_name
)
options['group_name'] = group_name
# Offset loaded subset
if "offset" in options:
@ -187,7 +258,7 @@ class ReferenceLoader(Loader):
return loaded_containers
def process_reference(self, context, name, namespace, data):
def process_reference(self, context, name, namespace, options):
"""To be implemented by subclass"""
raise NotImplementedError("Must be implemented by subclass")

View file

@ -14,7 +14,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
icon = "code-fork"
color = "orange"
def process_reference(self, context, name, namespace, data):
def process_reference(self, context, name, namespace, options):
import maya.cmds as cmds
from openpype.hosts.maya.api.lib import unique_namespace
@ -41,7 +41,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
namespace=namespace,
sharedReferenceFile=False,
groupReference=True,
groupName="{}:{}".format(namespace, name),
groupName=options['group_name'],
reference=True,
returnNewNodes=True)

View file

@ -125,14 +125,15 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
except ValueError:
family = "model"
group_name = "{}:_GRP".format(namespace)
# True by default to keep legacy behaviours
attach_to_root = options.get("attach_to_root", True)
group_name = options["group_name"]
with maintained_selection():
cmds.loadPlugin("AbcImport.mll", quiet=True)
file_url = self.prepare_root_value(self.fname,
context["project"]["name"])
nodes = cmds.file(file_url,
namespace=namespace,
sharedReferenceFile=False,

View file

@ -19,8 +19,7 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
def process_reference(
self, context, name=None, namespace=None, options=None
):
group_name = "{}:{}".format(namespace, name)
group_name = options['group_name']
with lib.maintained_selection():
file_url = self.prepare_root_value(
self.fname, context["project"]["name"]

View file

@ -26,7 +26,7 @@ HARDLINK = 2
@attr.s
class TextureResult:
class TextureResult(object):
"""The resulting texture of a processed file for a resource"""
# Path to the file
path = attr.ib()

View file

@ -9,9 +9,9 @@ import openpype.hosts.nuke.api.lib as nlib
from openpype.pipeline.publish import (
ValidateContentsOrder,
PublishXmlValidationError,
OptionalPyblishPluginMixin
)
class SelectInvalidInstances(pyblish.api.Action):
"""Select invalid instances in Outliner."""
@ -92,7 +92,10 @@ class RepairSelectInvalidInstances(pyblish.api.Action):
nlib.set_node_data(node, nlib.INSTANCE_DATA_KNOB, node_data)
class ValidateCorrectAssetName(pyblish.api.InstancePlugin):
class ValidateCorrectAssetName(
pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin
):
"""Validator to check if instance asset match context asset.
When working in per-shot style you always publish data in context of
@ -111,6 +114,9 @@ class ValidateCorrectAssetName(pyblish.api.InstancePlugin):
optional = True
def process(self, instance):
if not self.is_active(instance.data):
return
asset = instance.data.get("asset")
context_asset = instance.context.data["assetEntity"]["name"]
node = instance.data["transientData"]["node"]

View file

@ -1,8 +1,12 @@
import nuke
import pyblish
from openpype.hosts.nuke import api as napi
from openpype.pipeline import PublishXmlValidationError
from openpype.pipeline.publish import (
ValidateContentsOrder,
PublishXmlValidationError,
OptionalPyblishPluginMixin
)
class SelectCenterInNodeGraph(pyblish.api.Action):
"""
@ -46,12 +50,15 @@ class SelectCenterInNodeGraph(pyblish.api.Action):
nuke.zoom(2, [min(all_xC), min(all_yC)])
class ValidateBackdrop(pyblish.api.InstancePlugin):
class ValidateBackdrop(
pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin
):
""" Validate amount of nodes on backdrop node in case user
forgotten to add nodes above the publishing backdrop node.
"""
order = pyblish.api.ValidatorOrder
order = ValidateContentsOrder
optional = True
families = ["nukenodes"]
label = "Validate Backdrop"
@ -59,6 +66,9 @@ class ValidateBackdrop(pyblish.api.InstancePlugin):
actions = [SelectCenterInNodeGraph]
def process(self, instance):
if not self.is_active(instance.data):
return
child_nodes = instance.data["transientData"]["childNodes"]
connections_out = instance.data["transientData"]["nodeConnectionsOut"]

View file

@ -18,7 +18,7 @@ class ValidateScriptAttributes(
order = pyblish.api.ValidatorOrder + 0.1
families = ["workfile"]
label = "Validatte script attributes"
label = "Validate script attributes"
hosts = ["nuke"]
optional = True
actions = [RepairAction]

View file

@ -354,6 +354,61 @@ def publish_plugins_discover(paths=None):
return result
def _get_plugin_settings(host_name, project_settings, plugin, log):
"""Get plugin settings based on host name and plugin name.
Args:
host_name (str): Name of host.
project_settings (dict[str, Any]): Project settings.
plugin (pyliblish.Plugin): Plugin where settings are applied.
log (logging.Logger): Logger to log messages.
Returns:
dict[str, Any]: Plugin settings {'attribute': 'value'}.
"""
# Use project settings from host name category when available
try:
return (
project_settings
[host_name]
["publish"]
[plugin.__name__]
)
except KeyError:
pass
# Settings category determined from path
# - usually path is './<category>/plugins/publish/<plugin file>'
# - category can be host name of addon name ('maya', 'deadline', ...)
filepath = os.path.normpath(inspect.getsourcefile(plugin))
split_path = filepath.rsplit(os.path.sep, 5)
if len(split_path) < 4:
log.warning(
'plugin path too short to extract host {}'.format(filepath)
)
return {}
category_from_file = split_path[-4]
plugin_kind = split_path[-2]
# TODO: change after all plugins are moved one level up
if category_from_file == "openpype":
category_from_file = "global"
try:
return (
project_settings
[category_from_file]
[plugin_kind]
[plugin.__name__]
)
except KeyError:
pass
return {}
def filter_pyblish_plugins(plugins):
"""Pyblish plugin filter which applies OpenPype settings.
@ -372,21 +427,21 @@ def filter_pyblish_plugins(plugins):
# TODO: Don't use host from 'pyblish.api' but from defined host by us.
# - kept becau on farm is probably used host 'shell' which propably
# affect how settings are applied there
host = pyblish.api.current_host()
host_name = pyblish.api.current_host()
project_name = os.environ.get("AVALON_PROJECT")
project_setting = get_project_settings(project_name)
project_settings = get_project_settings(project_name)
system_settings = get_system_settings()
# iterate over plugins
for plugin in plugins[:]:
# Apply settings to plugins
if hasattr(plugin, "apply_settings"):
# Use classmethod 'apply_settings'
# - can be used to target settings from custom settings place
# - skip default behavior when successful
try:
# Use classmethod 'apply_settings'
# - can be used to target settings from custom settings place
# - skip default behavior when successful
plugin.apply_settings(project_setting, system_settings)
continue
plugin.apply_settings(project_settings, system_settings)
except Exception:
log.warning(
@ -395,53 +450,20 @@ def filter_pyblish_plugins(plugins):
).format(plugin.__name__),
exc_info=True
)
try:
config_data = (
project_setting
[host]
["publish"]
[plugin.__name__]
else:
# Automated
plugin_settins = _get_plugin_settings(
host_name, project_settings, plugin, log
)
except KeyError:
# host determined from path
file = os.path.normpath(inspect.getsourcefile(plugin))
file = os.path.normpath(file)
split_path = file.split(os.path.sep)
if len(split_path) < 4:
log.warning(
'plugin path too short to extract host {}'.format(file)
)
continue
host_from_file = split_path[-4]
plugin_kind = split_path[-2]
# TODO: change after all plugins are moved one level up
if host_from_file == "openpype":
host_from_file = "global"
try:
config_data = (
project_setting
[host_from_file]
[plugin_kind]
[plugin.__name__]
)
except KeyError:
continue
for option, value in config_data.items():
if option == "enabled" and value is False:
log.info('removing plugin {}'.format(plugin.__name__))
plugins.remove(plugin)
else:
log.info('setting {}:{} on plugin {}'.format(
for option, value in plugin_settins.items():
log.info("setting {}:{} on plugin {}".format(
option, value, plugin.__name__))
setattr(plugin, option, value)
# Remove disabled plugins
if getattr(plugin, "enabled", True) is False:
plugins.remove(plugin)
def find_close_plugin(close_plugin_name, log):
if close_plugin_name:

View file

@ -49,7 +49,8 @@ class ExtractBurnin(publish.Extractor):
"webpublisher",
"aftereffects",
"photoshop",
"flame"
"flame",
"houdini"
# "resolve"
]
@ -78,9 +79,10 @@ class ExtractBurnin(publish.Extractor):
self.log.warning("No profiles present for create burnin")
return
# QUESTION what is this for and should we raise an exception?
if "representations" not in instance.data:
raise RuntimeError("Burnin needs already created mov to work on.")
if not instance.data.get("representations"):
self.log.info(
"Instance does not have filled representations. Skipping")
return
self.main_process(instance)

View file

@ -44,6 +44,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
"nuke",
"maya",
"blender",
"houdini",
"shell",
"hiero",
"premiere",

View file

@ -253,13 +253,14 @@
{
"families": ["review"],
"hosts": [
"maya"
"maya",
"houdini"
],
"task_types": [],
"task_names": [],
"subsets": [],
"burnins": {
"maya_review_burnin": {
"focal_length_burnin": {
"TOP_LEFT": "{yy}-{mm}-{dd}",
"TOP_CENTERED": "{focalLength:.2f} mm",
"TOP_RIGHT": "{anatomy[version]}",

View file

@ -21,7 +21,8 @@
"floatLut": "linear",
"logLut": "Cineon",
"viewerLut": "sRGB",
"thumbnailLut": "sRGB"
"thumbnailLut": "sRGB",
"monitorOutLut": "sRGB"
},
"regexInputs": {
"inputs": [

View file

@ -1047,6 +1047,10 @@
125,
255
]
},
"reference_loader": {
"namespace": "{asset_name}_{subset}_##",
"group_name": "_GRP"
}
},
"workfile_build": {

View file

@ -363,6 +363,11 @@
"optional": true,
"active": true
},
"ValidateBackdrop": {
"enabled": true,
"optional": true,
"active": true
},
"ValidateScript": {
"enabled": true,
"optional": true,

View file

@ -42,10 +42,19 @@
"nuke-default": "nuke-default"
},
{
"aces_1.0.3": "aces_1.0.3"
"aces_1.0.3": "aces_1.0.3 (12)"
},
{
"aces_1.1": "aces_1.1"
"aces_1.1": "aces_1.1 (12, 13)"
},
{
"aces_1.2": "aces_1.2 (13, 14)"
},
{
"studio-config-v1.0.0_aces-v1.3_ocio-v2.1": "studio-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)"
},
{
"cg-config-v1.0.0_aces-v1.3_ocio-v2.1": "cg-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)"
},
{
"custom": "custom"
@ -93,6 +102,11 @@
"type": "text",
"key": "thumbnailLut",
"label": "Thumbnails"
},
{
"type": "text",
"key": "monitorOutLut",
"label": "Monitor"
}
]
}

View file

@ -91,6 +91,28 @@
"key": "yetiRig"
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "reference_loader",
"label": "Reference Loader",
"children": [
{
"type": "text",
"label": "Namespace",
"key": "namespace"
},
{
"type": "text",
"label": "Group name",
"key": "group_name"
},
{
"type": "label",
"label": "Here's a link to the doc where you can find explanations about customing the naming of referenced assets: https://openpype.io/docs/admin_hosts_maya#load-plugins"
}
]
}
]
}

View file

@ -74,28 +74,34 @@
"nuke-default": "nuke-default"
},
{
"spi-vfx": "spi-vfx"
"spi-vfx": "spi-vfx (11)"
},
{
"spi-anim": "spi-anim"
"spi-anim": "spi-anim (11)"
},
{
"aces_0.1.1": "aces_0.1.1"
"aces_0.1.1": "aces_0.1.1 (11)"
},
{
"aces_0.7.1": "aces_0.7.1"
"aces_0.7.1": "aces_0.7.1 (11)"
},
{
"aces_1.0.1": "aces_1.0.1"
"aces_1.0.1": "aces_1.0.1 (11)"
},
{
"aces_1.0.3": "aces_1.0.3"
"aces_1.0.3": "aces_1.0.3 (11, 12)"
},
{
"aces_1.1": "aces_1.1"
"aces_1.1": "aces_1.1 (12, 13)"
},
{
"aces_1.2": "aces_1.2"
"aces_1.2": "aces_1.2 (13, 14)"
},
{
"studio-config-v1.0.0_aces-v1.3_ocio-v2.1": "studio-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)"
},
{
"cg-config-v1.0.0_aces-v1.3_ocio-v2.1": "cg-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)"
},
{
"custom": "custom"
@ -257,4 +263,4 @@
]
}
]
}
}

View file

@ -62,7 +62,7 @@
"template_data": [
{
"key": "ValidateCorrectAssetName",
"label": "Validate Correct Asset name"
"label": "Validate Correct Asset Name"
}
]
},
@ -72,7 +72,7 @@
"template_data": [
{
"key": "ValidateContainers",
"label": "ValidateContainers"
"label": "Validate Containers"
}
]
},
@ -81,7 +81,7 @@
"collapsible": true,
"checkbox_key": "enabled",
"key": "ValidateKnobs",
"label": "ValidateKnobs",
"label": "Validate Knobs",
"is_group": true,
"children": [
{
@ -104,6 +104,10 @@
"key": "ValidateOutputResolution",
"label": "Validate Output Resolution"
},
{
"key": "ValidateBackdrop",
"label": "Validate Backdrop"
},
{
"key": "ValidateGizmo",
"label": "Validate Gizmo (Group)"

View file

@ -6,6 +6,7 @@ import collections
import uuid
import tempfile
import shutil
import inspect
from abc import ABCMeta, abstractmethod
import six
@ -26,8 +27,8 @@ from openpype.pipeline import (
PublishValidationError,
KnownPublishError,
registered_host,
legacy_io,
get_process_id,
OptionalPyblishPluginMixin,
)
from openpype.pipeline.create import (
CreateContext,
@ -2307,6 +2308,37 @@ class PublisherController(BasePublisherController):
def _process_main_thread_item(self, item):
item()
def _is_publish_plugin_active(self, plugin):
"""Decide if publish plugin is active.
This is hack because 'active' is mis-used in mixin
'OptionalPyblishPluginMixin' where 'active' is used for default value
of optional plugins. Because of that is 'active' state of plugin
which inherit from 'OptionalPyblishPluginMixin' ignored. That affects
headless publishing inside host, potentially remote publishing.
We have to change that to match pyblish base, but we can do that
only when all hosts use Publisher because the change requires
change of settings schemas.
Args:
plugin (pyblish.Plugin): Plugin which should be checked if is
active.
Returns:
bool: Is plugin active.
"""
if plugin.active:
return True
if not plugin.optional:
return False
if OptionalPyblishPluginMixin in inspect.getmro(plugin):
return True
return False
def _publish_iterator(self):
"""Main logic center of publishing.
@ -2315,11 +2347,9 @@ class PublisherController(BasePublisherController):
states of currently processed publish plugin and instance. Also
change state of processed orders like validation order has passed etc.
Also stops publishing if should stop on validation.
QUESTION:
Does validate button still make sense?
Also stops publishing, if should stop on validation.
"""
for idx, plugin in enumerate(self._publish_plugins):
self._publish_progress = idx
@ -2344,6 +2374,11 @@ class PublisherController(BasePublisherController):
# Add plugin to publish report
self._publish_report.add_plugin_iter(plugin, self._publish_context)
# WARNING This is hack fix for optional plugins
if not self._is_publish_plugin_active(plugin):
self._publish_report.set_plugin_skipped()
continue
# Trigger callback that new plugin is going to be processed
plugin_label = plugin.__name__
if hasattr(plugin, "label") and plugin.label:
@ -2450,7 +2485,11 @@ def collect_families_from_instances(instances, only_active=False):
instances(list<pyblish.api.Instance>): List of publish instances from
which are families collected.
only_active(bool): Return families only for active instances.
Returns:
list[str]: Families available on instances.
"""
all_families = set()
for instance in instances:
if only_active:

View file

@ -162,7 +162,8 @@ class PluginsModel(QtGui.QStandardItemModel):
items = []
for plugin_item in plugin_items:
item = QtGui.QStandardItem(plugin_item.label)
label = plugin_item.label or plugin_item.name
item = QtGui.QStandardItem(label)
item.setData(False, ITEM_IS_GROUP_ROLE)
item.setData(plugin_item.label, ITEM_LABEL_ROLE)
item.setData(plugin_item.id, ITEM_ID_ROLE)

View file

@ -4,7 +4,6 @@ from qtpy import QtWidgets, QtCore, QtGui
import qtawesome
from openpype.client import get_projects
from openpype.pipeline import AvalonMongoDB
from openpype.style import get_objected_colors
from openpype.tools.utils.widgets import ImageButton
from openpype.tools.utils.lib import paint_image_with_color
@ -97,6 +96,7 @@ class CompleterView(QtWidgets.QListView):
# Open the widget unactivated
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)
self.setAttribute(QtCore.Qt.WA_NoMouseReplay)
delegate = QtWidgets.QStyledItemDelegate()
self.setItemDelegate(delegate)
@ -241,6 +241,18 @@ class SettingsLineEdit(PlaceholderLineEdit):
if self._completer is not None:
self._completer.set_text_filter(text)
def _completer_should_be_visible(self):
return (
self.isVisible()
and (self.hasFocus() or self._completer.hasFocus())
)
def _show_completer(self):
if self._completer_should_be_visible():
self._focus_timer.start()
self._completer.show()
self._update_completer()
def _update_completer(self):
if self._completer is None or not self._completer.isVisible():
return
@ -249,7 +261,7 @@ class SettingsLineEdit(PlaceholderLineEdit):
self._completer.move(new_point)
def _on_focus_timer(self):
if not self.hasFocus() and not self._completer.hasFocus():
if not self._completer_should_be_visible():
self._completer.hide()
self._focus_timer.stop()
@ -258,9 +270,7 @@ class SettingsLineEdit(PlaceholderLineEdit):
self.focused_in.emit()
if self._completer is not None:
self._focus_timer.start()
self._completer.show()
self._update_completer()
self._show_completer()
def paintEvent(self, event):
super(SettingsLineEdit, self).paintEvent(event)

View file

@ -106,6 +106,37 @@ or Deadlines **Draft Tile Assembler**.
This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation.
`Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending.
## Load Plugins
### Reference Loader
#### Namespace and Group Name
Here you can create your own custom naming for the reference loader.
The custom naming is split into two parts: namespace and group name. If you don't set the namespace or the group name, an error will occur.
Here's the different variables you can use:
<div class="row markdown">
<div class="col col--5 markdown">
| Token | Description |
|---|---|
|`{asset_name}` | Asset name |
|`{asset_type}` | Asset type |
|`{subset}` | Subset name |
|`{family}` | Subset family |
</div>
</div>
The namespace field can contain a single group of '#' number tokens to indicate where the namespace's unique index should go. The amount of tokens defines the zero padding of the number, e.g ### turns into 001.
Warning: Note that a namespace will always be prefixed with a _ if it starts with a digit.
Example:
![Namespace and Group Name](assets/maya-admin_custom_namespace.png)
### Extract GPU Cache
![Maya GPU Cache](assets/maya-admin_gpu_cache.png)
@ -170,6 +201,7 @@ These options are set on the camera shape when publishing the review. They corre
![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings_camera_options.png)
## Custom Menu
You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**.
![Custom menu definition](assets/maya-admin_scriptsmenu.png)

View file

@ -14,20 +14,29 @@ sidebar_label: Houdini
- [Library Loader](artist_tools_library-loader)
## Publishing Alembic Cameras
You can publish baked camera in Alembic format. Select your camera and go **OpenPype -> Create** and select **Camera (abc)**.
You can publish baked camera in Alembic format.
Select your camera and go **OpenPype -> Create** and select **Camera (abc)**.
This will create Alembic ROP in **out** with path and frame range already set. This node will have a name you've
assigned in the **Creator** menu. For example if you name the subset `Default`, output Alembic Driver will be named
`cameraDefault`. After that, you can **OpenPype -> Publish** and after some validations your camera will be published
to `abc` file.
## Publishing Composites - Image Sequences
You can publish image sequence directly from Houdini. You can use any `cop` network you have and publish image
sequence generated from it. For example I've created simple **cop** graph to generate some noise:
You can publish image sequences directly from Houdini's image COP networks.
You can use any COP node and publish the image sequence generated from it. For example this simple graph to generate some noise:
![Noise COP](assets/houdini_imagesequence_cop.png)
If I want to publish it, I'll select node I like - in this case `radialblur1` and go **OpenPype -> Create** and
select **Composite (Image Sequence)**. This will create `/out/imagesequenceNoise` Composite ROP (I've named my subset
*Noise*) with frame range set. When you hit **Publish** it will render image sequence from selected node.
To publish the output of the `radialblur1` go to **OpenPype -> Create** and
select **Composite (Image Sequence)**. If you name the variant *Noise* this will create the `/out/imagesequenceNoise` Composite ROP with the frame range set.
When you hit **Publish** it will render image sequence from selected node.
:::info Use selection
With *Use selection* is enabled on create the node you have selected when creating will be the node used for published. (It set the Composite ROP node's COP path to it). If you don't do this you'll have to manually set the path as needed on e.g. `/out/imagesequenceNoise` to ensure it outputs what you want.
:::
## Publishing Point Caches (alembic)
Publishing point caches in alembic format is pretty straightforward, but it is by default enforcing better compatibility
@ -46,6 +55,16 @@ you handle `path` attribute is up to you, this is just an example.*
Now select the `output0` node and go **OpenPype -> Create** and select **Point Cache**. It will create
Alembic ROP `/out/pointcacheStrange`
## Publishing Reviews (OpenGL)
To generate a review output from Houdini you need to create a **review** instance.
Go to **OpenPype -> Create** and select **Review**.
![Houdini Create Review](assets/houdini_review_create_attrs.png)
On create, with the **Use Selection** checkbox enabled it will set up the first
camera found in your selection as the camera for the OpenGL ROP node and other
non-cameras are set in **Force Objects**. It will then render those even if
their display flag is disabled in your scene.
## Redshift
:::note Work in progress

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -255,7 +255,7 @@ suffix is **"client"** then the final suffix is **"h264_client"**.
| resolution_height | Resolution height. |
| fps | Fps of an output. |
| timecode | Timecode by frame start and fps. |
| focalLength | **Only available in Maya**<br /><br />Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |
| focalLength | **Only available in Maya and Houdini**<br /><br />Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |
:::warning
`timecode` is a specific key that can be **only at the end of content**. (`"BOTTOM_RIGHT": "TC: {timecode}"`)

View file

@ -304,7 +304,7 @@ If source representation has suffix **"h264"** and burnin suffix is **"client"**
| resolution_height | Resolution height. |
| fps | Fps of an output. |
| timecode | Timecode by frame start and fps. |
| focalLength | **Only available in Maya**<br /><br />Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |
| focalLength | **Only available in Maya and Houdini**<br /><br />Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |
:::warning
`timecode` is specific key that can be **only at the end of content**. (`"BOTTOM_RIGHT": "TC: {timecode}"`)