mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into feature/OP-3933_RR-support
This commit is contained in:
commit
226b088871
43 changed files with 837 additions and 285 deletions
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,8 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.15.7
|
||||
- 3.15.7-nightly.3
|
||||
- 3.15.7-nightly.2
|
||||
- 3.15.7-nightly.1
|
||||
- 3.15.6
|
||||
|
|
@ -133,8 +135,6 @@ body:
|
|||
- 3.14.1
|
||||
- 3.14.1-nightly.4
|
||||
- 3.14.1-nightly.3
|
||||
- 3.14.1-nightly.2
|
||||
- 3.14.1-nightly.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -112,3 +112,9 @@ tools/run_eventserver.*
|
|||
tools/dev_*
|
||||
|
||||
.github_changelog_generator
|
||||
|
||||
|
||||
# Addons
|
||||
########
|
||||
/openpype/addons/*
|
||||
!/openpype/addons/README.md
|
||||
|
|
|
|||
297
CHANGELOG.md
297
CHANGELOG.md
|
|
@ -1,6 +1,303 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## [3.15.7](https://github.com/ynput/OpenPype/tree/3.15.7)
|
||||
|
||||
|
||||
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.6...3.15.7)
|
||||
|
||||
### **🆕 New features**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Addons directory <a href="https://github.com/ynput/OpenPype/pull/4893">#4893</a></summary>
|
||||
|
||||
This adds a directory for Addons, for easier distribution of studio specific code.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Kitsu - Add "image", "online" and "plate" to review families <a href="https://github.com/ynput/OpenPype/pull/4923">#4923</a></summary>
|
||||
|
||||
This PR adds "image", "online" and "plate" to the review families so they also can be uploaded to Kitsu.It also adds the `Add review to Kitsu` tag to the default png review. Without it the user would manually need to add it for single image uploads to Kitsu and might confuse users (it confused me first for a while as movies did work).
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Feature/remove and load inv action <a href="https://github.com/ynput/OpenPype/pull/4930">#4930</a></summary>
|
||||
|
||||
Added the ability to remove and load a container, as a way to reset it.This can be useful in cases where a container breaks in a way that can be fixed by removing it, then reloading it.Also added the ability to add `InventoryAction` plugins by placing them in `openpype/plugins/inventory`.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🚀 Enhancements**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Load Rig References - Change Rig to Animation in Animation instance <a href="https://github.com/ynput/OpenPype/pull/4877">#4877</a></summary>
|
||||
|
||||
We are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya template builder - preserve all references when importing a template <a href="https://github.com/ynput/OpenPype/pull/4797">#4797</a></summary>
|
||||
|
||||
When building a template with Maya template builder, we import the template and also the references inside the template file. This causes some problems:
|
||||
- We cannot use the references to version assets imported by the template.
|
||||
- When we import the file, the internal reference files are also imported. As a side effect, Maya complains about a reference that no longer exists.`// Error: file: /xxx/maya/2023.3/linux/scripts/AETemplates/AEtransformRelated.mel line 58: Reference node 'turntable_mayaSceneMain_01_RN' is not associated with a reference file.`
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: Renaming the integration plugin to Ayon. <a href="https://github.com/ynput/OpenPype/pull/4646">#4646</a></summary>
|
||||
|
||||
Renamed the .h, and .cpp files to Ayon. Also renamed the classes to with the Ayon keyword.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>3dsMax: render dialogue needs to be closed <a href="https://github.com/ynput/OpenPype/pull/4729">#4729</a></summary>
|
||||
|
||||
Make sure the render setup dialog is in a closed state for the update of resolution and other render settings
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya Template Builder - Remove default cameras from renderable cameras <a href="https://github.com/ynput/OpenPype/pull/4815">#4815</a></summary>
|
||||
|
||||
When we build an asset workfile with build workfile from template inside Maya, we load our turntable camera. But then we end up with 2 renderables camera : **persp** the one imported from the template.We need to remove the **persp** camera (or any other default camera) from renderable cameras when building the work file.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Validators for Frame Range in Max <a href="https://github.com/ynput/OpenPype/pull/4914">#4914</a></summary>
|
||||
|
||||
Switch Render Frame Range Type to 3 for specific ranges (initial setup for the range type is 4)Reset Frame Range will also set the frame range for render settingsRender Collector won't take the frame range from context data but take the range directly from render settingAdd validators for render frame range type and frame range respectively with repair action
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: Saver creator settings <a href="https://github.com/ynput/OpenPype/pull/4943">#4943</a></summary>
|
||||
|
||||
Adding Saver creator settings and enhanced rendering path with template.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>General: Project Anatomy on creators <a href="https://github.com/ynput/OpenPype/pull/4962">#4962</a></summary>
|
||||
|
||||
Anatomy object of current project is available on `CreateContext` and create plugins.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🐛 Bug fixes**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Validate shader name - OP-5903 <a href="https://github.com/ynput/OpenPype/pull/4971">#4971</a></summary>
|
||||
|
||||
Running the plugin would error with:
|
||||
```
|
||||
// TypeError: 'str' object cannot be interpreted as an integer
|
||||
```Fixed and added setting `active`.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Houdini: Fix slow Houdini launch due to shelves generation <a href="https://github.com/ynput/OpenPype/pull/4829">#4829</a></summary>
|
||||
|
||||
Shelf generation during Houdini startup would add an insane amount of delay for the Houdini UI to launch correctly. By deferring the shelf generation this takes away the 5+ minutes of delay for the Houdini UI to launch.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion - Fixed "optional validation" <a href="https://github.com/ynput/OpenPype/pull/4912">#4912</a></summary>
|
||||
|
||||
Added OptionalPyblishPluginMixin and is_active checks for all publish tools that should be optional
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bug: add missing `pyblish.util` import <a href="https://github.com/ynput/OpenPype/pull/4937">#4937</a></summary>
|
||||
|
||||
remote publishing was missing import of `remote_publish`. This is adding it back.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: Fix missing 'object_path' property <a href="https://github.com/ynput/OpenPype/pull/4938">#4938</a></summary>
|
||||
|
||||
Epic removed the `object_path` property from `AssetData`. This PR fixes usages of that property.Fixes #4936
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Remove obsolete global validator <a href="https://github.com/ynput/OpenPype/pull/4939">#4939</a></summary>
|
||||
|
||||
Removing `Validate Sequence Frames` validator from global plugins as it wasn't handling correctly many things and was by mistake enabled, breaking functionality on Deadline.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>General: fix build_workfile get_linked_assets missing project_name arg <a href="https://github.com/ynput/OpenPype/pull/4940">#4940</a></summary>
|
||||
|
||||
Linked assets collection don't work within `build_workfile` because `get_linked_assets` function call has a missing `project_name`argument.
|
||||
- Added the `project_name` arg to the `get_linked_assets` function call.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>General: fix Scene Inventory switch version error dialog missing parent arg on init <a href="https://github.com/ynput/OpenPype/pull/4941">#4941</a></summary>
|
||||
|
||||
QuickFix for the switch version error dialog to set inventory widget as parent.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: Fix camera frame range <a href="https://github.com/ynput/OpenPype/pull/4956">#4956</a></summary>
|
||||
|
||||
Fix the frame range of the level sequence for the Camera in Unreal.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: Fix missing parameter when updating Alembic StaticMesh <a href="https://github.com/ynput/OpenPype/pull/4957">#4957</a></summary>
|
||||
|
||||
Fix an error when updating an Alembic StaticMesh in Unreal, due to a missing parameter in a function call.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: Fix render extraction <a href="https://github.com/ynput/OpenPype/pull/4963">#4963</a></summary>
|
||||
|
||||
Fix a problem with the extraction of renders in Unreal.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: Remove Python 3.8 syntax from addon <a href="https://github.com/ynput/OpenPype/pull/4965">#4965</a></summary>
|
||||
|
||||
Removed Python 3.8 syntax from addon.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Ftrack: Fix editorial task creation <a href="https://github.com/ynput/OpenPype/pull/4966">#4966</a></summary>
|
||||
|
||||
Fix key assignment on instance data during editorial publishing in ftrack hierarchy integration.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **Merged pull requests**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Add "shortcut" to Scripts Menu Definition <a href="https://github.com/ynput/OpenPype/pull/4927">#4927</a></summary>
|
||||
|
||||
Add the possibility to associate a shorcut for an entry in the script menu definition with the key "shortcut"
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.15.6](https://github.com/ynput/OpenPype/tree/3.15.6)
|
||||
|
||||
|
||||
|
|
|
|||
3
openpype/addons/README.md
Normal file
3
openpype/addons/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
This directory is for storing external addons that needs to be included in the pipeline when distributed.
|
||||
|
||||
The directory is ignored by Git, but included in the zip and installation files.
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
from copy import deepcopy
|
||||
import os
|
||||
|
||||
from openpype.hosts.fusion.api import (
|
||||
|
|
@ -11,15 +12,13 @@ from openpype.lib import (
|
|||
)
|
||||
from openpype.pipeline import (
|
||||
legacy_io,
|
||||
Creator,
|
||||
Creator as NewCreator,
|
||||
CreatedInstance,
|
||||
)
|
||||
from openpype.client import (
|
||||
get_asset_by_name,
|
||||
Anatomy
|
||||
)
|
||||
|
||||
|
||||
class CreateSaver(Creator):
|
||||
class CreateSaver(NewCreator):
|
||||
identifier = "io.openpype.creators.fusion.saver"
|
||||
label = "Render (saver)"
|
||||
name = "render"
|
||||
|
|
@ -28,9 +27,24 @@ class CreateSaver(Creator):
|
|||
description = "Fusion Saver to generate image sequence"
|
||||
icon = "fa5.eye"
|
||||
|
||||
instance_attributes = ["reviewable"]
|
||||
instance_attributes = [
|
||||
"reviewable"
|
||||
]
|
||||
default_variants = [
|
||||
"Main",
|
||||
"Mask"
|
||||
]
|
||||
|
||||
# TODO: This should be renamed together with Nuke so it is aligned
|
||||
temp_rendering_path_template = (
|
||||
"{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}")
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
instance_data.update({
|
||||
"id": "pyblish.avalon.instance",
|
||||
"subset": subset_name
|
||||
})
|
||||
|
||||
# TODO: Add pre_create attributes to choose file format?
|
||||
file_format = "OpenEXRFormat"
|
||||
|
||||
|
|
@ -39,7 +53,6 @@ class CreateSaver(Creator):
|
|||
args = (-32768, -32768) # Magical position numbers
|
||||
saver = comp.AddTool("Saver", *args)
|
||||
|
||||
instance_data["subset"] = subset_name
|
||||
self._update_tool_with_data(saver, data=instance_data)
|
||||
|
||||
saver["OutputFormat"] = file_format
|
||||
|
|
@ -78,7 +91,7 @@ class CreateSaver(Creator):
|
|||
for tool in tools:
|
||||
data = self.get_managed_tool_data(tool)
|
||||
if not data:
|
||||
data = self._collect_unmanaged_saver(tool)
|
||||
continue
|
||||
|
||||
# Add instance
|
||||
created_instance = CreatedInstance.from_existing(data, self)
|
||||
|
|
@ -125,60 +138,35 @@ class CreateSaver(Creator):
|
|||
original_subset = tool.GetData("openpype.subset")
|
||||
subset = data["subset"]
|
||||
if original_subset != subset:
|
||||
# Subset change detected
|
||||
# Update output filepath
|
||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||
filename = f"{subset}..exr"
|
||||
filepath = os.path.join(workdir, "render", subset, filename)
|
||||
tool["Clip"] = filepath
|
||||
self._configure_saver_tool(data, tool, subset)
|
||||
|
||||
# Rename tool
|
||||
if tool.Name != subset:
|
||||
print(f"Renaming {tool.Name} -> {subset}")
|
||||
tool.SetAttrs({"TOOLS_Name": subset})
|
||||
def _configure_saver_tool(self, data, tool, subset):
|
||||
formatting_data = deepcopy(data)
|
||||
|
||||
def _collect_unmanaged_saver(self, tool):
|
||||
# TODO: this should not be done this way - this should actually
|
||||
# get the data as stored on the tool explicitly (however)
|
||||
# that would disallow any 'regular saver' to be collected
|
||||
# unless the instance data is stored on it to begin with
|
||||
|
||||
print("Collecting unmanaged saver..")
|
||||
comp = tool.Comp()
|
||||
|
||||
# Allow regular non-managed savers to also be picked up
|
||||
project = legacy_io.Session["AVALON_PROJECT"]
|
||||
asset = legacy_io.Session["AVALON_ASSET"]
|
||||
task = legacy_io.Session["AVALON_TASK"]
|
||||
|
||||
asset_doc = get_asset_by_name(project_name=project, asset_name=asset)
|
||||
|
||||
path = tool["Clip"][comp.TIME_UNDEFINED]
|
||||
fname = os.path.basename(path)
|
||||
fname, _ext = os.path.splitext(fname)
|
||||
variant = fname.rstrip(".")
|
||||
subset = self.get_subset_name(
|
||||
variant=variant,
|
||||
task_name=task,
|
||||
asset_doc=asset_doc,
|
||||
project_name=project,
|
||||
# get frame padding from anatomy templates
|
||||
anatomy = Anatomy()
|
||||
frame_padding = int(
|
||||
anatomy.templates["render"].get("frame_padding", 4)
|
||||
)
|
||||
|
||||
attrs = tool.GetAttrs()
|
||||
passthrough = attrs["TOOLB_PassThrough"]
|
||||
return {
|
||||
# Required data
|
||||
"project": project,
|
||||
"asset": asset,
|
||||
"subset": subset,
|
||||
"task": task,
|
||||
"variant": variant,
|
||||
"active": not passthrough,
|
||||
"family": self.family,
|
||||
# Unique identifier for instance and this creator
|
||||
"id": "pyblish.avalon.instance",
|
||||
"creator_identifier": self.identifier,
|
||||
}
|
||||
# Subset change detected
|
||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||
formatting_data.update({
|
||||
"workdir": workdir,
|
||||
"frame": "0" * frame_padding,
|
||||
"ext": "exr"
|
||||
})
|
||||
|
||||
# build file path to render
|
||||
filepath = self.temp_rendering_path_template.format(
|
||||
**formatting_data)
|
||||
|
||||
tool["Clip"] = os.path.normpath(filepath)
|
||||
|
||||
# Rename tool
|
||||
if tool.Name != subset:
|
||||
print(f"Renaming {tool.Name} -> {subset}")
|
||||
tool.SetAttrs({"TOOLS_Name": subset})
|
||||
|
||||
def get_managed_tool_data(self, tool):
|
||||
"""Return data of the tool if it matches creator identifier"""
|
||||
|
|
@ -238,3 +226,25 @@ class CreateSaver(Creator):
|
|||
default=("reviewable" in self.instance_attributes),
|
||||
label="Review",
|
||||
)
|
||||
|
||||
def apply_settings(
|
||||
self,
|
||||
project_settings,
|
||||
system_settings
|
||||
):
|
||||
"""Method called on initialization of plugin to apply settings."""
|
||||
|
||||
# plugin settings
|
||||
plugin_settings = (
|
||||
project_settings["fusion"]["create"][self.__class__.__name__]
|
||||
)
|
||||
|
||||
# individual attributes
|
||||
self.instance_attributes = plugin_settings.get(
|
||||
"instance_attributes") or self.instance_attributes
|
||||
self.default_variants = plugin_settings.get(
|
||||
"default_variants") or self.default_variants
|
||||
self.temp_rendering_path_template = (
|
||||
plugin_settings.get("temp_rendering_path_template")
|
||||
or self.temp_rendering_path_template
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,29 +1,39 @@
|
|||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import OptionalPyblishPluginMixin
|
||||
from openpype.pipeline import KnownPublishError
|
||||
|
||||
class FusionIncrementCurrentFile(pyblish.api.ContextPlugin):
|
||||
|
||||
class FusionIncrementCurrentFile(
|
||||
pyblish.api.ContextPlugin, OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Increment the current file.
|
||||
|
||||
Saves the current file with an increased version number.
|
||||
|
||||
"""
|
||||
|
||||
label = "Increment current file"
|
||||
label = "Increment workfile version"
|
||||
order = pyblish.api.IntegratorOrder + 9.0
|
||||
hosts = ["fusion"]
|
||||
families = ["workfile"]
|
||||
optional = True
|
||||
|
||||
def process(self, context):
|
||||
if not self.is_active(context.data):
|
||||
return
|
||||
|
||||
from openpype.lib import version_up
|
||||
from openpype.pipeline.publish import get_errored_plugins_from_context
|
||||
|
||||
errored_plugins = get_errored_plugins_from_context(context)
|
||||
if any(plugin.__name__ == "FusionSubmitDeadline"
|
||||
for plugin in errored_plugins):
|
||||
raise RuntimeError("Skipping incrementing current file because "
|
||||
"submission to render farm failed.")
|
||||
if any(
|
||||
plugin.__name__ == "FusionSubmitDeadline"
|
||||
for plugin in errored_plugins
|
||||
):
|
||||
raise KnownPublishError(
|
||||
"Skipping incrementing current file because "
|
||||
"submission to render farm failed."
|
||||
)
|
||||
|
||||
comp = context.data.get("currentComp")
|
||||
assert comp, "Must have comp"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
from openpype.pipeline import PublishValidationError
|
||||
from openpype.pipeline import (
|
||||
publish,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError,
|
||||
)
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
|
||||
class ValidateBackgroundDepth(
|
||||
pyblish.api.InstancePlugin, OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Validate if all Background tool are set to float32 bit"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
|
|
@ -15,11 +20,10 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
|
|||
families = ["render"]
|
||||
optional = True
|
||||
|
||||
actions = [SelectInvalidAction, RepairAction]
|
||||
actions = [SelectInvalidAction, publish.RepairAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
context = instance.context
|
||||
comp = context.data.get("currentComp")
|
||||
assert comp, "Must have Comp object"
|
||||
|
|
@ -31,12 +35,16 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
|
|||
return [i for i in backgrounds if i.GetInput("Depth") != 4.0]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise PublishValidationError(
|
||||
"Found {} Backgrounds tools which"
|
||||
" are not set to float32".format(len(invalid)),
|
||||
title=self.label)
|
||||
title=self.label,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
|
|
|
|||
|
|
@ -81,7 +81,13 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
# TODO: make sure this doesn't trigger when
|
||||
# opening with last workfile.
|
||||
_set_context_settings()
|
||||
shelves.generate_shelves()
|
||||
|
||||
if not IS_HEADLESS:
|
||||
import hdefereval # noqa, hdefereval is only available in ui mode
|
||||
# Defer generation of shelves due to issue on Windows where shelf
|
||||
# initialization during start up delays Houdini UI by minutes
|
||||
# making it extremely slow to launch.
|
||||
hdefereval.executeDeferred(shelves.generate_shelves)
|
||||
|
||||
if not IS_HEADLESS:
|
||||
import hdefereval # noqa, hdefereval is only available in ui mode
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ def get_default_render_folder(project_setting=None):
|
|||
["default_render_image_folder"])
|
||||
|
||||
|
||||
def set_framerange(start_frame, end_frame):
|
||||
def set_render_frame_range(start_frame, end_frame):
|
||||
"""
|
||||
Note:
|
||||
Frame range can be specified in different types. Possible values are:
|
||||
|
|
@ -150,10 +150,10 @@ def set_framerange(start_frame, end_frame):
|
|||
Todo:
|
||||
Current type is hard-coded, there should be a custom setting for this.
|
||||
"""
|
||||
rt.rendTimeType = 4
|
||||
rt.rendTimeType = 3
|
||||
if start_frame is not None and end_frame is not None:
|
||||
frame_range = "{0}-{1}".format(start_frame, end_frame)
|
||||
rt.rendPickupFrames = frame_range
|
||||
rt.rendStart = int(start_frame)
|
||||
rt.rendEnd = int(end_frame)
|
||||
|
||||
|
||||
def get_multipass_setting(project_setting=None):
|
||||
|
|
@ -173,10 +173,16 @@ def set_scene_resolution(width: int, height: int):
|
|||
None
|
||||
|
||||
"""
|
||||
# make sure the render dialog is closed
|
||||
# for the update of resolution
|
||||
# Changing the Render Setup dialog settingsshould be done
|
||||
# with the actual Render Setup dialog in a closed state.
|
||||
if rt.renderSceneDialog.isOpen():
|
||||
rt.renderSceneDialog.close()
|
||||
|
||||
rt.renderWidth = width
|
||||
rt.renderHeight = height
|
||||
|
||||
|
||||
def reset_scene_resolution():
|
||||
"""Apply the scene resolution from the project definition
|
||||
|
||||
|
|
@ -243,6 +249,7 @@ def reset_frame_range(fps: bool = True):
|
|||
frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"])
|
||||
frange_cmd = f"animationRange = interval {frame_start} {frame_end}"
|
||||
rt.execute(frange_cmd)
|
||||
set_render_frame_range(frame_start, frame_end)
|
||||
|
||||
|
||||
def set_context_setting():
|
||||
|
|
@ -259,6 +266,7 @@ def set_context_setting():
|
|||
None
|
||||
"""
|
||||
reset_scene_resolution()
|
||||
reset_frame_range()
|
||||
|
||||
|
||||
def get_max_version():
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ class RenderProducts(object):
|
|||
container)
|
||||
|
||||
context = get_current_project_asset()
|
||||
startFrame = context["data"].get("frameStart")
|
||||
endFrame = context["data"].get("frameEnd") + 1
|
||||
# TODO: change the frame range follows the current render setting
|
||||
startFrame = int(rt.rendStart)
|
||||
endFrame = int(rt.rendEnd) + 1
|
||||
|
||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
||||
full_render_list = self.beauty_render_product(output_file,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from openpype.pipeline import legacy_io
|
|||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
|
||||
from openpype.hosts.max.api.lib import (
|
||||
set_framerange,
|
||||
set_render_frame_range,
|
||||
get_current_renderer,
|
||||
get_default_render_folder
|
||||
)
|
||||
|
|
@ -68,7 +68,7 @@ class RenderSettings(object):
|
|||
# Set Frame Range
|
||||
frame_start = context["data"].get("frame_start")
|
||||
frame_end = context["data"].get("frame_end")
|
||||
set_framerange(frame_start, frame_end)
|
||||
set_render_frame_range(frame_start, frame_end)
|
||||
# get the production render
|
||||
renderer_class = get_current_renderer()
|
||||
renderer = str(renderer_class).split(":")[0]
|
||||
|
|
@ -105,6 +105,9 @@ class RenderSettings(object):
|
|||
|
||||
rt.rendSaveFile = True
|
||||
|
||||
if rt.renderSceneDialog.isOpen():
|
||||
rt.renderSceneDialog.close()
|
||||
|
||||
def arnold_setup(self):
|
||||
# get Arnold RenderView run in the background
|
||||
# for setting up renderable camera
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher):
|
|||
|
||||
def context_setting():
|
||||
return lib.set_context_setting()
|
||||
|
||||
rt.callbacks.addScript(rt.Name('systemPostNew'),
|
||||
context_setting)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ class CreateRender(plugin.MaxCreator):
|
|||
# for additional work on the node:
|
||||
# instance_node = rt.getNodeByName(instance.get("instance_node"))
|
||||
|
||||
# make sure the render dialog is closed
|
||||
# for the update of resolution
|
||||
# Changing the Render Setup dialog settings should be done
|
||||
# with the actual Render Setup dialog in a closed state.
|
||||
|
||||
# set viewport camera for rendering(mandatory for deadline)
|
||||
RenderSettings().set_render_camera(sel_obj)
|
||||
# set output paths for rendering(mandatory for deadline)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ class CollectRender(pyblish.api.InstancePlugin):
|
|||
|
||||
self.log.debug(f"Setting {version_int} to context.")
|
||||
context.data["version"] = version_int
|
||||
|
||||
# setup the plugin as 3dsmax for the internal renderer
|
||||
data = {
|
||||
"subset": instance.name,
|
||||
|
|
@ -59,8 +58,8 @@ class CollectRender(pyblish.api.InstancePlugin):
|
|||
"source": filepath,
|
||||
"expectedFiles": render_layer_files,
|
||||
"plugin": "3dsmax",
|
||||
"frameStart": context.data['frameStart'],
|
||||
"frameEnd": context.data['frameEnd'],
|
||||
"frameStart": int(rt.rendStart),
|
||||
"frameEnd": int(rt.rendEnd),
|
||||
"version": version_int,
|
||||
"farm": True
|
||||
}
|
||||
|
|
|
|||
64
openpype/hosts/max/plugins/publish/validate_frame_range.py
Normal file
64
openpype/hosts/max/plugins/publish/validate_frame_range.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from openpype.pipeline import (
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError
|
||||
)
|
||||
|
||||
|
||||
class ValidateFrameRange(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validates the frame ranges.
|
||||
|
||||
This is an optional validator checking if the frame range on instance
|
||||
matches the frame range specified for the asset.
|
||||
|
||||
It also validates render frame ranges of render layers.
|
||||
|
||||
Repair action will change everything to match the asset frame range.
|
||||
|
||||
This can be turned off by the artist to allow custom ranges.
|
||||
"""
|
||||
|
||||
label = "Validate Frame Range"
|
||||
order = ValidateContentsOrder
|
||||
families = ["maxrender"]
|
||||
hosts = ["max"]
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
self.log.info("Skipping validation...")
|
||||
return
|
||||
context = instance.context
|
||||
|
||||
frame_start = int(context.data.get("frameStart"))
|
||||
frame_end = int(context.data.get("frameEnd"))
|
||||
|
||||
inst_frame_start = int(instance.data.get("frameStart"))
|
||||
inst_frame_end = int(instance.data.get("frameEnd"))
|
||||
|
||||
errors = []
|
||||
if frame_start != inst_frame_start:
|
||||
errors.append(
|
||||
f"Start frame ({inst_frame_start}) on instance does not match " # noqa
|
||||
f"with the start frame ({frame_start}) set on the asset data. ") # noqa
|
||||
if frame_end != inst_frame_end:
|
||||
errors.append(
|
||||
f"End frame ({inst_frame_end}) on instance does not match "
|
||||
f"with the end frame ({frame_start}) from the asset data. ")
|
||||
|
||||
if errors:
|
||||
errors.append("You can use repair action to fix it.")
|
||||
raise PublishValidationError("\n".join(errors))
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
rt.rendStart = instance.context.data.get("frameStart")
|
||||
rt.rendEnd = instance.context.data.get("frameEnd")
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import (
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from pymxs import runtime as rt
|
||||
from openpype.hosts.max.api.lib import reset_scene_resolution
|
||||
|
||||
from openpype.pipeline.context_tools import (
|
||||
get_current_project_asset,
|
||||
get_current_project
|
||||
)
|
||||
|
||||
|
||||
class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validate the resolution setting aligned with DB"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
families = ["maxrender"]
|
||||
hosts = ["max"]
|
||||
label = "Validate Resolution Setting"
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
width, height = self.get_db_resolution(instance)
|
||||
current_width = rt.renderwidth
|
||||
current_height = rt.renderHeight
|
||||
if current_width != width and current_height != height:
|
||||
raise PublishValidationError("Resolution Setting "
|
||||
"not matching resolution "
|
||||
"set on asset or shot.")
|
||||
if current_width != width:
|
||||
raise PublishValidationError("Width in Resolution Setting "
|
||||
"not matching resolution set "
|
||||
"on asset or shot.")
|
||||
|
||||
if current_height != height:
|
||||
raise PublishValidationError("Height in Resolution Setting "
|
||||
"not matching resolution set "
|
||||
"on asset or shot.")
|
||||
|
||||
def get_db_resolution(self, instance):
|
||||
data = ["data.resolutionWidth", "data.resolutionHeight"]
|
||||
project_resolution = get_current_project(fields=data)
|
||||
project_resolution_data = project_resolution["data"]
|
||||
asset_resolution = get_current_project_asset(fields=data)
|
||||
asset_resolution_data = asset_resolution["data"]
|
||||
# Set project resolution
|
||||
project_width = int(
|
||||
project_resolution_data.get("resolutionWidth", 1920))
|
||||
project_height = int(
|
||||
project_resolution_data.get("resolutionHeight", 1080))
|
||||
width = int(
|
||||
asset_resolution_data.get("resolutionWidth", project_width))
|
||||
height = int(
|
||||
asset_resolution_data.get("resolutionHeight", project_height))
|
||||
|
||||
return width, height
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
reset_scene_resolution()
|
||||
|
|
@ -50,7 +50,8 @@ class ValidateShaderName(pyblish.api.InstancePlugin):
|
|||
asset_name = instance.data.get("asset", None)
|
||||
|
||||
# Check the number of connected shadingEngines per shape
|
||||
r = re.compile(cls.regex)
|
||||
regex_compile = re.compile(cls.regex)
|
||||
error_message = "object {0} has invalid shader name {1}"
|
||||
for shape in shapes:
|
||||
shading_engines = cmds.listConnections(shape,
|
||||
destination=True,
|
||||
|
|
@ -60,19 +61,18 @@ class ValidateShaderName(pyblish.api.InstancePlugin):
|
|||
)
|
||||
|
||||
for shader in shaders:
|
||||
m = r.match(cls.regex, shader)
|
||||
m = regex_compile.match(shader)
|
||||
if m is None:
|
||||
invalid.append(shape)
|
||||
cls.log.error(
|
||||
"object {0} has invalid shader name {1}".format(shape,
|
||||
shader)
|
||||
)
|
||||
cls.log.error(error_message.format(shape, shader))
|
||||
else:
|
||||
if 'asset' in r.groupindex:
|
||||
if 'asset' in regex_compile.groupindex:
|
||||
if m.group('asset') != asset_name:
|
||||
invalid.append(shape)
|
||||
cls.log.error(("object {0} has invalid "
|
||||
"shader name {1}").format(shape,
|
||||
shader))
|
||||
message = error_message
|
||||
message += " with missing asset name \"{2}\""
|
||||
cls.log.error(
|
||||
message.format(shape, shader, asset_name)
|
||||
)
|
||||
|
||||
return invalid
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ class UnrealAddon(OpenPypeModule, IHostAddon):
|
|||
UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon"
|
||||
)
|
||||
if not Path(unreal_plugin_path).exists():
|
||||
if compatible_versions := get_compatible_integration(
|
||||
compatible_versions = get_compatible_integration(
|
||||
ue_version, Path(UNREAL_ROOT_DIR) / "integration"
|
||||
):
|
||||
)
|
||||
if compatible_versions:
|
||||
unreal_plugin_path = compatible_versions[-1] / "Ayon"
|
||||
unreal_plugin_path = unreal_plugin_path.as_posix()
|
||||
|
||||
|
|
|
|||
|
|
@ -89,50 +89,26 @@ class ExistingLayoutLoader(plugin.Loader):
|
|||
raise NotImplementedError(
|
||||
f"Unreal version {ue_major} not supported")
|
||||
|
||||
def _get_transform(self, ext, import_data, lasset):
|
||||
conversion = unreal.Matrix.IDENTITY.transform()
|
||||
fbx_tuning = unreal.Matrix.IDENTITY.transform()
|
||||
def _transform_from_basis(self, transform, basis):
|
||||
"""Transform a transform from a basis to a new basis."""
|
||||
# Get the basis matrix
|
||||
basis_matrix = unreal.Matrix(
|
||||
basis[0],
|
||||
basis[1],
|
||||
basis[2],
|
||||
basis[3]
|
||||
)
|
||||
transform_matrix = unreal.Matrix(
|
||||
transform[0],
|
||||
transform[1],
|
||||
transform[2],
|
||||
transform[3]
|
||||
)
|
||||
|
||||
basis = unreal.Matrix(
|
||||
lasset.get('basis')[0],
|
||||
lasset.get('basis')[1],
|
||||
lasset.get('basis')[2],
|
||||
lasset.get('basis')[3]
|
||||
).transform()
|
||||
transform = unreal.Matrix(
|
||||
lasset.get('transform_matrix')[0],
|
||||
lasset.get('transform_matrix')[1],
|
||||
lasset.get('transform_matrix')[2],
|
||||
lasset.get('transform_matrix')[3]
|
||||
).transform()
|
||||
new_transform = (
|
||||
basis_matrix.get_inverse() * transform_matrix * basis_matrix)
|
||||
|
||||
# Check for the conversion settings. We cannot access
|
||||
# the alembic conversion settings, so we assume that
|
||||
# the maya ones have been applied.
|
||||
if ext == '.fbx':
|
||||
loc = import_data.import_translation
|
||||
rot = import_data.import_rotation.to_vector()
|
||||
scale = import_data.import_uniform_scale
|
||||
conversion = unreal.Transform(
|
||||
location=[loc.x, loc.y, loc.z],
|
||||
rotation=[rot.x, rot.y, rot.z],
|
||||
scale=[-scale, scale, scale]
|
||||
)
|
||||
fbx_tuning = unreal.Transform(
|
||||
rotation=[180.0, 0.0, 90.0],
|
||||
scale=[1.0, 1.0, 1.0]
|
||||
)
|
||||
elif ext == '.abc':
|
||||
# This is the standard conversion settings for
|
||||
# alembic files from Maya.
|
||||
conversion = unreal.Transform(
|
||||
location=[0.0, 0.0, 0.0],
|
||||
rotation=[0.0, 0.0, 0.0],
|
||||
scale=[1.0, -1.0, 1.0]
|
||||
)
|
||||
|
||||
new_transform = (basis.inverse() * transform * basis)
|
||||
return fbx_tuning * conversion.inverse() * new_transform
|
||||
return new_transform.transform()
|
||||
|
||||
def _spawn_actor(self, obj, lasset):
|
||||
actor = EditorLevelLibrary.spawn_actor_from_object(
|
||||
|
|
@ -140,16 +116,13 @@ class ExistingLayoutLoader(plugin.Loader):
|
|||
)
|
||||
|
||||
actor.set_actor_label(lasset.get('instance_name'))
|
||||
smc = actor.get_editor_property('static_mesh_component')
|
||||
mesh = smc.get_editor_property('static_mesh')
|
||||
import_data = mesh.get_editor_property('asset_import_data')
|
||||
filename = import_data.get_first_filename()
|
||||
path = Path(filename)
|
||||
|
||||
transform = self._get_transform(
|
||||
path.suffix, import_data, lasset)
|
||||
transform = lasset.get('transform_matrix')
|
||||
basis = lasset.get('basis')
|
||||
|
||||
actor.set_actor_transform(transform, False, True)
|
||||
computed_transform = self._transform_from_basis(transform, basis)
|
||||
|
||||
actor.set_actor_transform(computed_transform, False, True)
|
||||
|
||||
@staticmethod
|
||||
def _get_fbx_loader(loaders, family):
|
||||
|
|
@ -320,9 +293,12 @@ class ExistingLayoutLoader(plugin.Loader):
|
|||
containers.append(container)
|
||||
|
||||
# Set the transform for the actor.
|
||||
transform = self._get_transform(
|
||||
path.suffix, import_data, lasset)
|
||||
actor.set_actor_transform(transform, False, True)
|
||||
transform = lasset.get('transform_matrix')
|
||||
basis = lasset.get('basis')
|
||||
|
||||
computed_transform = self._transform_from_basis(
|
||||
transform, basis)
|
||||
actor.set_actor_transform(computed_transform, False, True)
|
||||
|
||||
actors_matched.append(actor)
|
||||
found = True
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ class CollectRenderInstances(pyblish.api.InstancePlugin):
|
|||
new_data["level"] = data.get("level")
|
||||
new_data["output"] = s.get('output')
|
||||
new_data["fps"] = seq.get_display_rate().numerator
|
||||
new_data["frameStart"] = s.get('frame_range')[0]
|
||||
new_data["frameEnd"] = s.get('frame_range')[1]
|
||||
new_data["frameStart"] = int(s.get('frame_range')[0])
|
||||
new_data["frameEnd"] = int(s.get('frame_range')[1])
|
||||
new_data["sequence"] = seq.get_path_name()
|
||||
new_data["master_sequence"] = data["master_sequence"]
|
||||
new_data["master_level"] = data["master_level"]
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
|
||||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractRender(publish.Extractor):
|
||||
"""Extract render."""
|
||||
|
||||
label = "Extract Render"
|
||||
hosts = ["unreal"]
|
||||
families = ["render"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
# Get the render output directory
|
||||
project_dir = unreal.Paths.project_dir()
|
||||
render_dir = (f"{project_dir}/Saved/MovieRenders/"
|
||||
f"{instance.data['subset']}")
|
||||
|
||||
assert unreal.Paths.directory_exists(render_dir), \
|
||||
"Render directory does not exist"
|
||||
|
||||
render_path = Path(render_dir)
|
||||
|
||||
frames = []
|
||||
|
||||
for x in render_path.iterdir():
|
||||
if x.is_file() and x.suffix == '.png':
|
||||
frames.append(str(x))
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
render_representation = {
|
||||
'name': 'png',
|
||||
'ext': 'png',
|
||||
'files': frames,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(render_representation)
|
||||
|
|
@ -311,6 +311,7 @@ def _load_modules():
|
|||
# Look for OpenPype modules in paths defined with `get_module_dirs`
|
||||
# - dynamically imported OpenPype modules and addons
|
||||
module_dirs = get_module_dirs()
|
||||
|
||||
# Add current directory at first place
|
||||
# - has small differences in import logic
|
||||
current_dir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
|
@ -318,8 +319,11 @@ def _load_modules():
|
|||
module_dirs.insert(0, hosts_dir)
|
||||
module_dirs.insert(0, current_dir)
|
||||
|
||||
addons_dir = os.path.join(os.path.dirname(current_dir), "addons")
|
||||
module_dirs.append(addons_dir)
|
||||
|
||||
processed_paths = set()
|
||||
for dirpath in module_dirs:
|
||||
for dirpath in frozenset(module_dirs):
|
||||
# Skip already processed paths
|
||||
if dirpath in processed_paths:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -378,7 +378,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
|
|||
existing_tasks.append(task_name_low)
|
||||
|
||||
for instance in instances_by_task_name[task_name_low]:
|
||||
instance["ftrackTask"] = child
|
||||
instance.data["ftrackTask"] = child
|
||||
|
||||
for task_name in tasks:
|
||||
task_type = tasks[task_name]["type"]
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin):
|
|||
|
||||
order = pyblish.api.IntegratorOrder
|
||||
label = "Kitsu Note and Status"
|
||||
families = ["render", "kitsu"]
|
||||
families = ["render", "image", "online", "plate", "kitsu"]
|
||||
|
||||
# status settings
|
||||
set_status_note = False
|
||||
|
|
@ -52,8 +52,9 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin):
|
|||
for instance in context:
|
||||
# Check if instance is a review by checking its family
|
||||
# Allow a match to primary family or any of families
|
||||
families = set([instance.data["family"]] +
|
||||
instance.data.get("families", []))
|
||||
families = set(
|
||||
[instance.data["family"]] + instance.data.get("families", [])
|
||||
)
|
||||
if "review" not in families:
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,10 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin):
|
|||
|
||||
order = pyblish.api.IntegratorOrder + 0.01
|
||||
label = "Kitsu Review"
|
||||
families = ["render", "kitsu"]
|
||||
families = ["render", "image", "online", "plate", "kitsu"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# Check comment has been created
|
||||
comment_id = instance.data.get("kitsu_comment", {}).get("id")
|
||||
if not comment_id:
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from . import (
|
|||
register_inventory_action_path,
|
||||
register_creator_plugin_path,
|
||||
deregister_loader_plugin_path,
|
||||
deregister_inventory_action_path,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins")
|
|||
# Global plugin paths
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||
|
||||
|
||||
def _get_modules_manager():
|
||||
|
|
@ -158,6 +160,7 @@ def install_openpype_plugins(project_name=None, host_name=None):
|
|||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
pyblish.api.register_discovery_filter(filter_pyblish_plugins)
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_inventory_action_path(INVENTORY_PATH)
|
||||
|
||||
if host_name is None:
|
||||
host_name = os.environ.get("AVALON_APP")
|
||||
|
|
@ -223,6 +226,7 @@ def uninstall_host():
|
|||
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
|
||||
pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)
|
||||
deregister_loader_plugin_path(LOAD_PATH)
|
||||
deregister_inventory_action_path(INVENTORY_PATH)
|
||||
log.info("Global plug-ins unregistred")
|
||||
|
||||
deregister_host()
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from openpype.lib.attribute_definitions import (
|
|||
get_default_values,
|
||||
)
|
||||
from openpype.host import IPublishHost, IWorkfileHost
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline import legacy_io, Anatomy
|
||||
from openpype.pipeline.plugin_discover import DiscoverResult
|
||||
|
||||
from .creator_plugins import (
|
||||
|
|
@ -1383,6 +1383,8 @@ class CreateContext:
|
|||
self._current_task_name = None
|
||||
self._current_workfile_path = None
|
||||
|
||||
self._current_project_anatomy = None
|
||||
|
||||
self._host_is_valid = host_is_valid
|
||||
# Currently unused variable
|
||||
self.headless = headless
|
||||
|
|
@ -1546,6 +1548,18 @@ class CreateContext:
|
|||
|
||||
return self._current_workfile_path
|
||||
|
||||
def get_current_project_anatomy(self):
|
||||
"""Project anatomy for current project.
|
||||
|
||||
Returns:
|
||||
Anatomy: Anatomy object ready to be used.
|
||||
"""
|
||||
|
||||
if self._current_project_anatomy is None:
|
||||
self._current_project_anatomy = Anatomy(
|
||||
self._current_project_name)
|
||||
return self._current_project_anatomy
|
||||
|
||||
@property
|
||||
def context_has_changed(self):
|
||||
"""Host context has changed.
|
||||
|
|
@ -1568,6 +1582,7 @@ class CreateContext:
|
|||
)
|
||||
|
||||
project_name = property(get_current_project_name)
|
||||
project_anatomy = property(get_current_project_anatomy)
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
|
|
@ -1680,6 +1695,8 @@ class CreateContext:
|
|||
self._current_task_name = task_name
|
||||
self._current_workfile_path = workfile_path
|
||||
|
||||
self._current_project_anatomy = None
|
||||
|
||||
def reset_plugins(self, discover_publish_plugins=True):
|
||||
"""Reload plugins.
|
||||
|
||||
|
|
|
|||
|
|
@ -231,10 +231,24 @@ class BaseCreator:
|
|||
|
||||
@property
|
||||
def project_name(self):
|
||||
"""Family that plugin represents."""
|
||||
"""Current project name.
|
||||
|
||||
Returns:
|
||||
str: Name of a project.
|
||||
"""
|
||||
|
||||
return self.create_context.project_name
|
||||
|
||||
@property
|
||||
def project_anatomy(self):
|
||||
"""Current project anatomy.
|
||||
|
||||
Returns:
|
||||
Anatomy: Project anatomy object.
|
||||
"""
|
||||
|
||||
return self.create_context.project_anatomy
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self.create_context.host
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ class BuildWorkfile:
|
|||
|
||||
if link_context_profiles:
|
||||
# Find and append linked assets if preset has set linked mapping
|
||||
link_assets = get_linked_assets(current_asset_entity)
|
||||
link_assets = get_linked_assets(project_name, current_asset_entity)
|
||||
if link_assets:
|
||||
assets.extend(link_assets)
|
||||
|
||||
|
|
|
|||
51
openpype/plugins/inventory/remove_and_load.py
Normal file
51
openpype/plugins/inventory/remove_and_load.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
from openpype.pipeline import InventoryAction
|
||||
from openpype.pipeline import get_current_project_name
|
||||
from openpype.pipeline.load.plugins import discover_loader_plugins
|
||||
from openpype.pipeline.load.utils import (
|
||||
get_loader_identifier,
|
||||
remove_container,
|
||||
load_container,
|
||||
)
|
||||
from openpype.client import get_representation_by_id
|
||||
|
||||
|
||||
class RemoveAndLoad(InventoryAction):
|
||||
"""Delete inventory item and reload it."""
|
||||
|
||||
label = "Remove and load"
|
||||
icon = "refresh"
|
||||
|
||||
def process(self, containers):
|
||||
project_name = get_current_project_name()
|
||||
loaders_by_name = {
|
||||
get_loader_identifier(plugin): plugin
|
||||
for plugin in discover_loader_plugins(project_name=project_name)
|
||||
}
|
||||
for container in containers:
|
||||
# Get loader
|
||||
loader_name = container["loader"]
|
||||
loader = loaders_by_name.get(loader_name, None)
|
||||
if not loader:
|
||||
raise RuntimeError(
|
||||
"Failed to get loader '{}', can't remove "
|
||||
"and load container".format(loader_name)
|
||||
)
|
||||
|
||||
# Get representation
|
||||
representation = get_representation_by_id(
|
||||
project_name, container["representation"]
|
||||
)
|
||||
if not representation:
|
||||
self.log.warning(
|
||||
"Skipping remove and load because representation id is not"
|
||||
" found in database: '{}'".format(
|
||||
container["representation"]
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# Remove container
|
||||
remove_container(container)
|
||||
|
||||
# Load container
|
||||
load_container(loader, representation)
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
import clique
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class ValidateSequenceFrames(pyblish.api.InstancePlugin):
|
||||
"""Ensure the sequence of frames is complete
|
||||
|
||||
The files found in the folder are checked against the startFrame and
|
||||
endFrame of the instance. If the first or last file is not
|
||||
corresponding with the first or last frame it is flagged as invalid.
|
||||
|
||||
Used regular expression pattern handles numbers in the file names
|
||||
(eg "Main_beauty.v001.1001.exr", "Main_beauty_v001.1001.exr",
|
||||
"Main_beauty.1001.1001.exr") but not numbers behind frames (eg.
|
||||
"Main_beauty.1001.v001.exr")
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Sequence Frames"
|
||||
families = ["imagesequence", "render"]
|
||||
hosts = ["shell", "unreal"]
|
||||
|
||||
def process(self, instance):
|
||||
representations = instance.data.get("representations")
|
||||
if not representations:
|
||||
return
|
||||
for repr in representations:
|
||||
repr_files = repr["files"]
|
||||
if isinstance(repr_files, str):
|
||||
continue
|
||||
|
||||
ext = repr.get("ext")
|
||||
if not ext:
|
||||
_, ext = os.path.splitext(repr_files[0])
|
||||
elif not ext.startswith("."):
|
||||
ext = ".{}".format(ext)
|
||||
pattern = r"\D?(?P<index>(?P<padding>0*)\d+){}$".format(
|
||||
re.escape(ext))
|
||||
patterns = [pattern]
|
||||
|
||||
collections, remainder = clique.assemble(
|
||||
repr_files, minimum_items=1, patterns=patterns)
|
||||
|
||||
assert not remainder, "Must not have remainder"
|
||||
assert len(collections) == 1, "Must detect single collection"
|
||||
collection = collections[0]
|
||||
frames = list(collection.indexes)
|
||||
|
||||
if instance.data.get("slate"):
|
||||
# Slate is not part of the frame range
|
||||
frames = frames[1:]
|
||||
|
||||
current_range = (frames[0], frames[-1])
|
||||
|
||||
required_range = (instance.data["frameStart"],
|
||||
instance.data["frameEnd"])
|
||||
|
||||
if current_range != required_range:
|
||||
raise ValueError(f"Invalid frame range: {current_range} - "
|
||||
f"expected: {required_range}")
|
||||
|
||||
missing = collection.holes().indexes
|
||||
assert not missing, "Missing frames: %s" % (missing,)
|
||||
|
|
@ -21,5 +21,18 @@
|
|||
"copy_path": "~/.openpype/hosts/fusion/profiles",
|
||||
"copy_status": false,
|
||||
"force_sync": false
|
||||
},
|
||||
"create": {
|
||||
"CreateSaver": {
|
||||
"temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}",
|
||||
"default_variants": [
|
||||
"Main",
|
||||
"Mask"
|
||||
],
|
||||
"instance_attributes": [
|
||||
"reviewable",
|
||||
"farm_rendering"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@
|
|||
"png": {
|
||||
"ext": "png",
|
||||
"tags": [
|
||||
"ftrackreview"
|
||||
"ftrackreview",
|
||||
"kitsureview"
|
||||
],
|
||||
"burnins": [],
|
||||
"ffmpeg_args": {
|
||||
|
|
|
|||
|
|
@ -19,5 +19,12 @@
|
|||
"custFloats": "custFloats",
|
||||
"custVecs": "custVecs"
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
"ValidateFrameRange": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -734,6 +734,7 @@
|
|||
"ValidateShaderName": {
|
||||
"enabled": false,
|
||||
"optional": true,
|
||||
"active": true,
|
||||
"regex": "(?P<asset>.*)_(.*)_SHD"
|
||||
},
|
||||
"ValidateShadingEngine": {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,50 @@
|
|||
"label": "Resync profile on each launch"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "create",
|
||||
"label": "Creator plugins",
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "CreateSaver",
|
||||
"label": "Create Saver",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "temp_rendering_path_template",
|
||||
"label": "Temporary rendering path template"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default variants",
|
||||
"object_type": {
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "instance_attributes",
|
||||
"label": "Instance attributes",
|
||||
"type": "enum",
|
||||
"multiselection": true,
|
||||
"enum_items": [
|
||||
{
|
||||
"reviewable": "Reviewable"
|
||||
},
|
||||
{
|
||||
"farm_rendering": "Farm rendering"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_max_publish"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "publish",
|
||||
"label": "Publish plugins",
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "ValidateFrameRange",
|
||||
"label": "Validate Frame Range",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "active",
|
||||
"label": "Active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -126,6 +126,11 @@
|
|||
"key": "optional",
|
||||
"label": "Optional"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "active",
|
||||
"label": "Active"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Shader name regex can use named capture group <b>asset</b> to validate against current asset name.<p><b>Example:</b><br/><code>^.*(?P=<asset>.+)_SHD</code></p>"
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ class ScriptsMenu(QtWidgets.QMenu):
|
|||
|
||||
Args:
|
||||
title (str): the name of the root menu which will be created
|
||||
|
||||
|
||||
parent (QtWidgets.QObject) : the QObject to parent the menu to
|
||||
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ class ScriptsMenu(QtWidgets.QMenu):
|
|||
parent(QtWidgets.QWidget): the object to parent the menu to
|
||||
|
||||
title(str): the title of the menu
|
||||
|
||||
|
||||
Returns:
|
||||
QtWidget.QMenu instance
|
||||
"""
|
||||
|
|
@ -111,7 +111,7 @@ class ScriptsMenu(QtWidgets.QMenu):
|
|||
return menu
|
||||
|
||||
def add_script(self, parent, title, command, sourcetype, icon=None,
|
||||
tags=None, label=None, tooltip=None):
|
||||
tags=None, label=None, tooltip=None, shortcut=None):
|
||||
"""Create an action item which runs a script when clicked
|
||||
|
||||
Args:
|
||||
|
|
@ -134,6 +134,8 @@ class ScriptsMenu(QtWidgets.QMenu):
|
|||
|
||||
tooltip (str): A tip for the user about the usage fo the tool
|
||||
|
||||
shortcut (str): A shortcut to run the command
|
||||
|
||||
Returns:
|
||||
QtWidget.QAction instance
|
||||
|
||||
|
|
@ -166,6 +168,9 @@ class ScriptsMenu(QtWidgets.QMenu):
|
|||
raise RuntimeError("Script action can't be "
|
||||
"processed: {}".format(e))
|
||||
|
||||
if shortcut:
|
||||
script_action.setShortcut(shortcut)
|
||||
|
||||
if icon:
|
||||
iconfile = os.path.expandvars(icon)
|
||||
script_action.iconfile = iconfile
|
||||
|
|
@ -253,7 +258,7 @@ class ScriptsMenu(QtWidgets.QMenu):
|
|||
|
||||
def _update_search(self, search):
|
||||
"""Hide all the samples which do not match the user's import
|
||||
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.15.7-nightly.2"
|
||||
__version__ = "3.15.7"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.15.6" # OpenPype
|
||||
version = "3.15.7" # OpenPype
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue