Merge branch 'develop' into bugfix/OP-5883_3dsmax-Publishing-Deadline-jobs-from-RedShift

This commit is contained in:
Ondřej Samohel 2023-05-31 18:13:14 +02:00 committed by GitHub
commit 2e40a26beb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1248 additions and 402 deletions

View file

@ -35,6 +35,8 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
- 3.15.9
- 3.15.9-nightly.2
- 3.15.9-nightly.1
- 3.15.8
- 3.15.8-nightly.3
@ -133,8 +135,6 @@ body:
- 3.14.3-nightly.1
- 3.14.2
- 3.14.2-nightly.5
- 3.14.2-nightly.4
- 3.14.2-nightly.3
validations:
required: true
- type: dropdown

View file

@ -1,6 +1,341 @@
# Changelog
## [3.15.9](https://github.com/ynput/OpenPype/tree/3.15.9)
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.8...3.15.9)
### **🆕 New features**
<details>
<summary>Blender: Implemented Loading of Alembic Camera <a href="https://github.com/ynput/OpenPype/pull/4990">#4990</a></summary>
Implemented loading of Alembic cameras in Blender.
___
</details>
<details>
<summary>Unreal: Implemented Creator, Loader and Extractor for Levels <a href="https://github.com/ynput/OpenPype/pull/5008">#5008</a></summary>
Creator, Loader and Extractor for Unreal Levels have been implemented.
___
</details>
### **🚀 Enhancements**
<details>
<summary>Blender: Added setting for base unit scale <a href="https://github.com/ynput/OpenPype/pull/4987">#4987</a></summary>
A setting for the base unit scale has been added for Blender.The unit scale is automatically applied when opening a file or creating a new one.
___
</details>
<details>
<summary>Unreal: Changed naming and path of Camera Levels <a href="https://github.com/ynput/OpenPype/pull/5010">#5010</a></summary>
The levels created for the camera in Unreal now include `_camera` in the name, to be better identifiable, and are placed in the camera folder.
___
</details>
<details>
<summary>Settings: Added option to nest settings templates <a href="https://github.com/ynput/OpenPype/pull/5022">#5022</a></summary>
It is possible to nest settings templates in another templates.
___
</details>
<details>
<summary>Enhancement/publisher: Remove "hit play to continue" label on continue <a href="https://github.com/ynput/OpenPype/pull/5029">#5029</a></summary>
Remove "hit play to continue" message on continue so that it doesn't show anymore when play was clicked.
___
</details>
<details>
<summary>Ftrack: Limit number of ftrack events to query at once <a href="https://github.com/ynput/OpenPype/pull/5033">#5033</a></summary>
Limit the amount of ftrack events received from mongo at once to 100.
___
</details>
<details>
<summary>General: Small code cleanups <a href="https://github.com/ynput/OpenPype/pull/5034">#5034</a></summary>
Small code cleanup and updates.
___
</details>
<details>
<summary>Global: collect frames to fix with settings <a href="https://github.com/ynput/OpenPype/pull/5036">#5036</a></summary>
Settings for `Collect Frames to Fix` will allow disable per project the plugin. Also `Rewriting latest version` attribute is hiddable from settings.
___
</details>
<details>
<summary>General: Publish plugin apply settings can expect only project settings <a href="https://github.com/ynput/OpenPype/pull/5037">#5037</a></summary>
Only project settings are passed to optional `apply_settings` method, if the method expects only one argument.
___
</details>
### **🐛 Bug fixes**
<details>
<summary>Maya: Load Assembly fix invalid imports <a href="https://github.com/ynput/OpenPype/pull/4859">#4859</a></summary>
Refactors imports so they are now correct.
___
</details>
<details>
<summary>Maya: Skipping rendersetup for members. <a href="https://github.com/ynput/OpenPype/pull/4973">#4973</a></summary>
When publishing a `rendersetup`, the objectset is and should be empty.
___
</details>
<details>
<summary>Maya: Validate Rig Output IDs <a href="https://github.com/ynput/OpenPype/pull/5016">#5016</a></summary>
Absolute names of node were not used, so plugin did not fetch the nodes properly.Also missed pymel command.
___
</details>
<details>
<summary>Deadline: escape rootless path in publish job <a href="https://github.com/ynput/OpenPype/pull/4910">#4910</a></summary>
If the publish path on Deadline job contains spaces or other characters, command was failing because the path wasn't properly escaped. This is fixing it.
___
</details>
<details>
<summary>General: Company name and URL changed <a href="https://github.com/ynput/OpenPype/pull/4974">#4974</a></summary>
The current records were obsolete in inno_setup, changed to the up-to-date.
___
</details>
<details>
<summary>Unreal: Fix usage of 'get_full_path' function <a href="https://github.com/ynput/OpenPype/pull/5014">#5014</a></summary>
This PR changes all the occurrences of `get_full_path` functions to alternatives to get the path of the objects.
___
</details>
<details>
<summary>Unreal: Fix sequence frames validator to use correct data <a href="https://github.com/ynput/OpenPype/pull/5021">#5021</a></summary>
Fix sequence frames validator to use clipIn and clipOut data instead of frameStart and frameEnd.
___
</details>
<details>
<summary>Unreal: Fix render instances collection to use correct data <a href="https://github.com/ynput/OpenPype/pull/5023">#5023</a></summary>
Fix render instances collection to use `frameStart` and `frameEnd` from the Project Manager, instead of the sequence's ones.
___
</details>
<details>
<summary>Resolve: loader is opening even if no timeline in project <a href="https://github.com/ynput/OpenPype/pull/5025">#5025</a></summary>
Loader is opening now even no timeline is available in a project.
___
</details>
<details>
<summary>nuke: callback for dirmapping is on demand <a href="https://github.com/ynput/OpenPype/pull/5030">#5030</a></summary>
Nuke was slowed down on processing due this callback. Since it is disabled by default it made sense to add it only on demand.
___
</details>
<details>
<summary>Publisher: UI works with instances without label <a href="https://github.com/ynput/OpenPype/pull/5032">#5032</a></summary>
Publisher UI does not crash if instance don't have filled 'label' key in instance data.
___
</details>
<details>
<summary>Publisher: Call explicitly prepared tab methods <a href="https://github.com/ynput/OpenPype/pull/5044">#5044</a></summary>
It is not possible to go to Create tab during publishing from OpenPype menu.
___
</details>
<details>
<summary>Ftrack: Role names are not case sensitive in ftrack event server status action <a href="https://github.com/ynput/OpenPype/pull/5058">#5058</a></summary>
Event server status action is not case sensitive for role names of user.
___
</details>
<details>
<summary>Publisher: Fix border widget <a href="https://github.com/ynput/OpenPype/pull/5063">#5063</a></summary>
Fixed border lines in Publisher UI to be painted correctly with correct indentation and size.
___
</details>
<details>
<summary>Unreal: Fix Commandlet Project and Permissions <a href="https://github.com/ynput/OpenPype/pull/5066">#5066</a></summary>
Fix problem when creating an Unreal Project when Commandlet Project is in a protected location.
___
</details>
<details>
<summary>Unreal: Added verification for Unreal app name format <a href="https://github.com/ynput/OpenPype/pull/5070">#5070</a></summary>
The Unreal app name is used to determine the Unreal version folder, so it is necessary that if follows the format `x-x`, where `x` is any integer. This PR adds a verification that the app name follows that format.
___
</details>
### **📃 Documentation**
<details>
<summary>Docs: Display wrong image in ExtractOIIOTranscode <a href="https://github.com/ynput/OpenPype/pull/5045">#5045</a></summary>
Wrong image display in `https://openpype.io/docs/project_settings/settings_project_global#extract-oiio-transcode`.
___
</details>
### **Merged pull requests**
<details>
<summary>Drop-down menu to list all families in create placeholder <a href="https://github.com/ynput/OpenPype/pull/4928">#4928</a></summary>
Currently in the create placeholder window, we need to write the family manually. This replace the text field by an enum field with all families for the current software.
___
</details>
<details>
<summary>add sync to specific projects or listen only <a href="https://github.com/ynput/OpenPype/pull/4919">#4919</a></summary>
Extend kitsu sync service with additional arguments to sync specific projects.
___
</details>
## [3.15.8](https://github.com/ynput/OpenPype/tree/3.15.8)

View file

@ -14,10 +14,10 @@ AppId={{B9E9DF6A-5BDA-42DD-9F35-C09D564C4D93}
AppName={#MyAppName}
AppVersion={#AppVer}
AppVerName={#MyAppName} version {#AppVer}
AppPublisher=Orbi Tools s.r.o
AppPublisherURL=http://pype.club
AppSupportURL=http://pype.club
AppUpdatesURL=http://pype.club
AppPublisher=Ynput s.r.o
AppPublisherURL=https://ynput.io
AppSupportURL=https://ynput.io
AppUpdatesURL=https://ynput.io
DefaultDirName={autopf}\{#MyAppName}\{#AppVer}
UsePreviousAppDir=no
DisableProgramGroupPage=yes

View file

@ -26,6 +26,8 @@ from openpype.lib import (
emit_event
)
import openpype.hosts.blender
from openpype.settings import get_project_settings
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
@ -83,6 +85,31 @@ def uninstall():
ops.unregister()
def show_message(title, message):
from openpype.widgets.message_window import Window
from .ops import BlenderApplication
BlenderApplication.get_app()
Window(
parent=None,
title=title,
message=message,
level="warning")
def message_window(title, message):
from .ops import (
MainThreadItem,
execute_in_main_thread,
_process_app_events
)
mti = MainThreadItem(show_message, title, message)
execute_in_main_thread(mti)
_process_app_events()
def set_start_end_frames():
project_name = legacy_io.active_project()
asset_name = legacy_io.Session["AVALON_ASSET"]
@ -125,10 +152,36 @@ def set_start_end_frames():
def on_new():
set_start_end_frames()
project = os.environ.get("AVALON_PROJECT")
settings = get_project_settings(project)
unit_scale_settings = settings.get("blender").get("unit_scale_settings")
unit_scale_enabled = unit_scale_settings.get("enabled")
if unit_scale_enabled:
unit_scale = unit_scale_settings.get("base_file_unit_scale")
bpy.context.scene.unit_settings.scale_length = unit_scale
def on_open():
set_start_end_frames()
project = os.environ.get("AVALON_PROJECT")
settings = get_project_settings(project)
unit_scale_settings = settings.get("blender").get("unit_scale_settings")
unit_scale_enabled = unit_scale_settings.get("enabled")
apply_on_opening = unit_scale_settings.get("apply_on_opening")
if unit_scale_enabled and apply_on_opening:
unit_scale = unit_scale_settings.get("base_file_unit_scale")
prev_unit_scale = bpy.context.scene.unit_settings.scale_length
if unit_scale != prev_unit_scale:
bpy.context.scene.unit_settings.scale_length = unit_scale
message_window(
"Base file unit scale changed",
"Base file unit scale changed to match the project settings.")
@bpy.app.handlers.persistent
def _on_save_pre(*args):

View file

@ -0,0 +1,209 @@
"""Load an asset in Blender from an Alembic file."""
from pathlib import Path
from pprint import pformat
from typing import Dict, List, Optional
import bpy
from openpype.pipeline import (
get_representation_path,
AVALON_CONTAINER_ID,
)
from openpype.hosts.blender.api import plugin, lib
from openpype.hosts.blender.api.pipeline import (
AVALON_CONTAINERS,
AVALON_PROPERTY,
)
class AbcCameraLoader(plugin.AssetLoader):
"""Load a camera from Alembic file.
Stores the imported asset in an empty named after the asset.
"""
families = ["camera"]
representations = ["abc"]
label = "Load Camera (ABC)"
icon = "code-fork"
color = "orange"
def _remove(self, asset_group):
objects = list(asset_group.children)
for obj in objects:
if obj.type == "CAMERA":
bpy.data.cameras.remove(obj.data)
elif obj.type == "EMPTY":
objects.extend(obj.children)
bpy.data.objects.remove(obj)
def _process(self, libpath, asset_group, group_name):
plugin.deselect_all()
bpy.ops.wm.alembic_import(filepath=libpath)
objects = lib.get_selection()
for obj in objects:
obj.parent = asset_group
for obj in objects:
name = obj.name
obj.name = f"{group_name}:{name}"
if obj.type != "EMPTY":
name_data = obj.data.name
obj.data.name = f"{group_name}:{name_data}"
if not obj.get(AVALON_PROPERTY):
obj[AVALON_PROPERTY] = dict()
avalon_info = obj[AVALON_PROPERTY]
avalon_info.update({"container_name": group_name})
plugin.deselect_all()
return objects
def process_asset(
self,
context: dict,
name: str,
namespace: Optional[str] = None,
options: Optional[Dict] = None,
) -> Optional[List]:
"""
Arguments:
name: Use pre-defined name
namespace: Use pre-defined namespace
context: Full parenthood of representation to load
options: Additional settings dictionary
"""
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
asset_name = plugin.asset_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
bpy.context.scene.collection.children.link(avalon_container)
asset_group = bpy.data.objects.new(group_name, object_data=None)
avalon_container.objects.link(asset_group)
objects = self._process(libpath, asset_group, group_name)
objects = []
nodes = list(asset_group.children)
for obj in nodes:
objects.append(obj)
nodes.extend(list(obj.children))
bpy.context.scene.collection.objects.link(asset_group)
asset_group[AVALON_PROPERTY] = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": name,
"namespace": namespace or "",
"loader": str(self.__class__.__name__),
"representation": str(context["representation"]["_id"]),
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"objectName": group_name,
}
self[:] = objects
return objects
def exec_update(self, container: Dict, representation: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
ones and add them to the collection.
If the objects of the collection are used in another collection they
will not be removed, only unlinked. Normally this should not be the
case though.
Warning:
No nested collections are supported at the moment!
"""
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(get_representation_path(representation))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
)
assert asset_group, (
f"The asset is not loaded: {container['objectName']}")
assert libpath, (
f"No existing library file found for {container['objectName']}")
assert libpath.is_file(), f"The file doesn't exist: {libpath}"
assert extension in plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}")
metadata = asset_group.get(AVALON_PROPERTY)
group_libpath = metadata["libpath"]
normalized_group_libpath = str(
Path(bpy.path.abspath(group_libpath)).resolve())
normalized_libpath = str(
Path(bpy.path.abspath(str(libpath))).resolve())
self.log.debug(
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_group_libpath,
normalized_libpath,
)
if normalized_group_libpath == normalized_libpath:
self.log.info("Library already loaded, not updating...")
return
mat = asset_group.matrix_basis.copy()
self._remove(asset_group)
self._process(str(libpath), asset_group, object_name)
asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.
Arguments:
container (openpype:container-1.0): Container to remove,
from `host.ls()`.
Returns:
bool: Whether the container was deleted.
Warning:
No nested collections are supported at the moment!
"""
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
if not asset_group:
return False
self._remove(asset_group)
bpy.data.objects.remove(asset_group)
return True

View file

@ -41,8 +41,8 @@ class LoadClip(phiero.SequenceLoader):
clip_name_template = "{asset}_{subset}_{representation}"
@classmethod
def apply_settings(cls, project_settings, system_settings):
plugin_type_settings = (
project_settings
.get("hiero", {})

View file

@ -28,7 +28,9 @@ from openpype.pipeline import (
)
from openpype.hosts.maya.api.lib import (
matrix_equals,
unique_namespace
unique_namespace,
get_container_transforms,
DEFAULT_MATRIX
)
log = logging.getLogger("PackageLoader")
@ -183,8 +185,6 @@ def _add(instance, representation_id, loaders, namespace, root="|"):
"""
from openpype.hosts.maya.lib import get_container_transforms
# Process within the namespace
with namespaced(namespace, new=False) as namespace:
@ -379,8 +379,6 @@ def update_scene(set_container, containers, current_data, new_data, new_file):
"""
from openpype.hosts.maya.lib import DEFAULT_MATRIX, get_container_transforms
set_namespace = set_container['namespace']
project_name = legacy_io.active_project()

View file

@ -1,8 +1,14 @@
import maya.cmds as cmds
from openpype.pipeline import (
load,
remove_container
)
from openpype.hosts.maya.api.pipeline import containerise
from openpype.hosts.maya.api.lib import unique_namespace
from openpype.hosts.maya.api import setdress
class AssemblyLoader(load.LoaderPlugin):
@ -16,9 +22,6 @@ class AssemblyLoader(load.LoaderPlugin):
def load(self, context, name, namespace, data):
from openpype.hosts.maya.api.pipeline import containerise
from openpype.hosts.maya.api.lib import unique_namespace
asset = context['asset']['name']
namespace = namespace or unique_namespace(
asset + "_",
@ -26,8 +29,6 @@ class AssemblyLoader(load.LoaderPlugin):
suffix="_",
)
from openpype.hosts.maya.api import setdress
containers = setdress.load_package(
filepath=self.fname,
name=name,
@ -50,15 +51,11 @@ class AssemblyLoader(load.LoaderPlugin):
def update(self, container, representation):
from openpype import setdress
return setdress.update_package(container, representation)
def remove(self, container):
"""Remove all sub containers"""
from openpype import setdress
import maya.cmds as cmds
# Remove all members
member_containers = setdress.get_contained_containers(container)
for member_container in member_containers:

View file

@ -13,7 +13,6 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin):
@classmethod
def get_invalid(cls, instance):
invalid = list()
if not instance.data["setMembers"]:
objectset_name = instance.data['name']
@ -22,6 +21,10 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin):
return invalid
def process(self, instance):
# Allow renderlayer and workfile to be empty
skip_families = ["workfile", "renderlayer", "rendersetup"]
if instance.data.get("family") in skip_families:
return
invalid = self.get_invalid(instance)
if invalid:

View file

@ -55,7 +55,8 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin):
if shapes:
instance_nodes.extend(shapes)
scene_nodes = cmds.ls(type="transform") + cmds.ls(type="mesh")
scene_nodes = cmds.ls(type="transform", long=True)
scene_nodes += cmds.ls(type="mesh", long=True)
scene_nodes = set(scene_nodes) - set(instance_nodes)
scene_nodes_by_basename = defaultdict(list)
@ -76,7 +77,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin):
if len(ids) > 1:
cls.log.error(
"\"{}\" id mismatch to: {}".format(
instance_node.longName(), matches
instance_node, matches
)
)
invalid[instance_node] = matches

View file

@ -1,5 +1,7 @@
import os
import re
from openpype.modules import IHostAddon, OpenPypeModule
from openpype.widgets.message_window import Window
UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
@ -19,6 +21,20 @@ class UnrealAddon(OpenPypeModule, IHostAddon):
from .lib import get_compatible_integration
pattern = re.compile(r'^\d+-\d+$')
if not pattern.match(app.name):
msg = (
"Unreal application key in the settings must be in format"
"'5-0' or '5-1'"
)
Window(
parent=None,
title="Unreal application name format",
message=msg,
level="critical")
raise ValueError(msg)
ue_version = app.name.replace("-", ".")
unreal_plugin_path = os.path.join(
UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon"

View file

@ -22,6 +22,8 @@ from .pipeline import (
show_tools_popup,
instantiate,
UnrealHost,
set_sequence_hierarchy,
generate_sequence,
maintained_selection
)
@ -41,5 +43,7 @@ __all__ = [
"show_tools_popup",
"instantiate",
"UnrealHost",
"set_sequence_hierarchy",
"generate_sequence",
"maintained_selection"
]

View file

@ -9,12 +9,14 @@ import time
import pyblish.api
from openpype.client import get_asset_by_name, get_assets
from openpype.pipeline import (
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AYON_CONTAINER_ID,
legacy_io,
)
from openpype.tools.utils import host_tools
import openpype.hosts.unreal
@ -512,6 +514,141 @@ def get_subsequences(sequence: unreal.LevelSequence):
return []
def set_sequence_hierarchy(
seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths
):
# Get existing sequencer tracks or create them if they don't exist
tracks = seq_i.get_master_tracks()
subscene_track = None
visibility_track = None
for t in tracks:
if t.get_class() == unreal.MovieSceneSubTrack.static_class():
subscene_track = t
if (t.get_class() ==
unreal.MovieSceneLevelVisibilityTrack.static_class()):
visibility_track = t
if not subscene_track:
subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack)
if not visibility_track:
visibility_track = seq_i.add_master_track(
unreal.MovieSceneLevelVisibilityTrack)
# Create the sub-scene section
subscenes = subscene_track.get_sections()
subscene = None
for s in subscenes:
if s.get_editor_property('sub_sequence') == seq_j:
subscene = s
break
if not subscene:
subscene = subscene_track.add_section()
subscene.set_row_index(len(subscene_track.get_sections()))
subscene.set_editor_property('sub_sequence', seq_j)
subscene.set_range(
min_frame_j,
max_frame_j + 1)
# Create the visibility section
ar = unreal.AssetRegistryHelpers.get_asset_registry()
maps = []
for m in map_paths:
# Unreal requires to load the level to get the map name
unreal.EditorLevelLibrary.save_all_dirty_levels()
unreal.EditorLevelLibrary.load_level(m)
maps.append(str(ar.get_asset_by_object_path(m).asset_name))
vis_section = visibility_track.add_section()
index = len(visibility_track.get_sections())
vis_section.set_range(
min_frame_j,
max_frame_j + 1)
vis_section.set_visibility(unreal.LevelVisibility.VISIBLE)
vis_section.set_row_index(index)
vis_section.set_level_names(maps)
if min_frame_j > 1:
hid_section = visibility_track.add_section()
hid_section.set_range(
1,
min_frame_j)
hid_section.set_visibility(unreal.LevelVisibility.HIDDEN)
hid_section.set_row_index(index)
hid_section.set_level_names(maps)
if max_frame_j < max_frame_i:
hid_section = visibility_track.add_section()
hid_section.set_range(
max_frame_j + 1,
max_frame_i + 1)
hid_section.set_visibility(unreal.LevelVisibility.HIDDEN)
hid_section.set_row_index(index)
hid_section.set_level_names(maps)
def generate_sequence(h, h_dir):
tools = unreal.AssetToolsHelpers().get_asset_tools()
sequence = tools.create_asset(
asset_name=h,
package_path=h_dir,
asset_class=unreal.LevelSequence,
factory=unreal.LevelSequenceFactoryNew()
)
project_name = legacy_io.active_project()
asset_data = get_asset_by_name(
project_name,
h_dir.split('/')[-1],
fields=["_id", "data.fps"]
)
start_frames = []
end_frames = []
elements = list(get_assets(
project_name,
parent_ids=[asset_data["_id"]],
fields=["_id", "data.clipIn", "data.clipOut"]
))
for e in elements:
start_frames.append(e.get('data').get('clipIn'))
end_frames.append(e.get('data').get('clipOut'))
elements.extend(get_assets(
project_name,
parent_ids=[e["_id"]],
fields=["_id", "data.clipIn", "data.clipOut"]
))
min_frame = min(start_frames)
max_frame = max(end_frames)
fps = asset_data.get('data').get("fps")
sequence.set_display_rate(
unreal.FrameRate(fps, 1.0))
sequence.set_playback_start(min_frame)
sequence.set_playback_end(max_frame)
sequence.set_work_range_start(min_frame / fps)
sequence.set_work_range_end(max_frame / fps)
sequence.set_view_range_start(min_frame / fps)
sequence.set_view_range_end(max_frame / fps)
tracks = sequence.get_master_tracks()
track = None
for t in tracks:
if (t.get_class() ==
unreal.MovieSceneCameraCutTrack.static_class()):
track = t
break
if not track:
track = sequence.add_master_track(
unreal.MovieSceneCameraCutTrack)
return sequence, (min_frame, max_frame)
@contextmanager
def maintained_selection():
"""Stub to be either implemented or replaced.

View file

@ -3,16 +3,24 @@
from pathlib import Path
import unreal
from unreal import EditorAssetLibrary
from unreal import EditorLevelLibrary
from unreal import EditorLevelUtils
from openpype.client import get_assets, get_asset_by_name
from unreal import (
EditorAssetLibrary,
EditorLevelLibrary,
EditorLevelUtils,
LevelSequenceEditorBlueprintLibrary as LevelSequenceLib,
)
from openpype.client import get_asset_by_name
from openpype.pipeline import (
AYON_CONTAINER_ID,
legacy_io,
)
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api import pipeline as unreal_pipeline
from openpype.hosts.unreal.api.pipeline import (
generate_sequence,
set_sequence_hierarchy,
create_container,
imprint,
)
class CameraLoader(plugin.Loader):
@ -24,32 +32,6 @@ class CameraLoader(plugin.Loader):
icon = "cube"
color = "orange"
def _set_sequence_hierarchy(
self, seq_i, seq_j, min_frame_j, max_frame_j
):
tracks = seq_i.get_master_tracks()
track = None
for t in tracks:
if t.get_class() == unreal.MovieSceneSubTrack.static_class():
track = t
break
if not track:
track = seq_i.add_master_track(unreal.MovieSceneSubTrack)
subscenes = track.get_sections()
subscene = None
for s in subscenes:
if s.get_editor_property('sub_sequence') == seq_j:
subscene = s
break
if not subscene:
subscene = track.add_section()
subscene.set_row_index(len(track.get_sections()))
subscene.set_editor_property('sub_sequence', seq_j)
subscene.set_range(
min_frame_j,
max_frame_j + 1)
def _import_camera(
self, world, sequence, bindings, import_fbx_settings, import_filename
):
@ -110,10 +92,7 @@ class CameraLoader(plugin.Loader):
hierarchy_dir_list.append(hierarchy_dir)
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
asset_name = f"{asset}_{name}" if asset else f"{name}"
tools = unreal.AssetToolsHelpers().get_asset_tools()
@ -127,23 +106,15 @@ class CameraLoader(plugin.Loader):
# Get highest number to make a unique name
folders = [a for a in asset_content
if a[-1] == "/" and f"{name}_" in a]
f_numbers = []
for f in folders:
# Get number from folder name. Splits the string by "_" and
# removes the last element (which is a "/").
f_numbers.append(int(f.split("_")[-1][:-1]))
# Get number from folder name. Splits the string by "_" and
# removes the last element (which is a "/").
f_numbers = [int(f.split("_")[-1][:-1]) for f in folders]
f_numbers.sort()
if not f_numbers:
unique_number = 1
else:
unique_number = f_numbers[-1] + 1
unique_number = f_numbers[-1] + 1 if f_numbers else 1
asset_dir, container_name = tools.create_unique_asset_name(
f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="")
asset_path = Path(asset_dir)
asset_path_parent = str(asset_path.parent.as_posix())
container_name += suffix
EditorAssetLibrary.make_directory(asset_dir)
@ -156,9 +127,9 @@ class CameraLoader(plugin.Loader):
if not EditorAssetLibrary.does_asset_exist(master_level):
EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map")
level = f"{asset_path_parent}/{asset}_map.{asset}_map"
level = f"{asset_dir}/{asset}_map_camera.{asset}_map_camera"
if not EditorAssetLibrary.does_asset_exist(level):
EditorLevelLibrary.new_level(f"{asset_path_parent}/{asset}_map")
EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map_camera")
EditorLevelLibrary.load_level(master_level)
EditorLevelUtils.add_level_to_world(
@ -169,27 +140,13 @@ class CameraLoader(plugin.Loader):
EditorLevelLibrary.save_all_dirty_levels()
EditorLevelLibrary.load_level(level)
project_name = legacy_io.active_project()
# TODO refactor
# - Creating of hierarchy should be a function in unreal integration
# - it's used in multiple loaders but must not be loader's logic
# - hard to say what is purpose of the loop
# - variables does not match their meaning
# - why scene is stored to sequences?
# - asset documents vs. elements
# - cleanup variable names in whole function
# - e.g. 'asset', 'asset_name', 'asset_data', 'asset_doc'
# - really inefficient queries of asset documents
# - existing asset in scene is considered as "with correct values"
# - variable 'elements' is modified during it's loop
# Get all the sequences in the hierarchy. It will create them, if
# they don't exist.
sequences = []
frame_ranges = []
i = 0
for h in hierarchy_dir_list:
sequences = []
for (h_dir, h) in zip(hierarchy_dir_list, hierarchy):
root_content = EditorAssetLibrary.list_assets(
h, recursive=False, include_folder=False)
h_dir, recursive=False, include_folder=False)
existing_sequences = [
EditorAssetLibrary.find_asset_data(asset)
@ -198,57 +155,17 @@ class CameraLoader(plugin.Loader):
asset).get_class().get_name() == 'LevelSequence'
]
if not existing_sequences:
scene = tools.create_asset(
asset_name=hierarchy[i],
package_path=h,
asset_class=unreal.LevelSequence,
factory=unreal.LevelSequenceFactoryNew()
)
asset_data = get_asset_by_name(
project_name,
h.split('/')[-1],
fields=["_id", "data.fps"]
)
start_frames = []
end_frames = []
elements = list(get_assets(
project_name,
parent_ids=[asset_data["_id"]],
fields=["_id", "data.clipIn", "data.clipOut"]
))
for e in elements:
start_frames.append(e.get('data').get('clipIn'))
end_frames.append(e.get('data').get('clipOut'))
elements.extend(get_assets(
project_name,
parent_ids=[e["_id"]],
fields=["_id", "data.clipIn", "data.clipOut"]
))
min_frame = min(start_frames)
max_frame = max(end_frames)
scene.set_display_rate(
unreal.FrameRate(asset_data.get('data').get("fps"), 1.0))
scene.set_playback_start(min_frame)
scene.set_playback_end(max_frame)
sequences.append(scene)
frame_ranges.append((min_frame, max_frame))
else:
for e in existing_sequences:
sequences.append(e.get_asset())
if existing_sequences:
for seq in existing_sequences:
sequences.append(seq.get_asset())
frame_ranges.append((
e.get_asset().get_playback_start(),
e.get_asset().get_playback_end()))
seq.get_asset().get_playback_start(),
seq.get_asset().get_playback_end()))
else:
sequence, frame_range = generate_sequence(h, h_dir)
i += 1
sequences.append(sequence)
frame_ranges.append(frame_range)
EditorAssetLibrary.make_directory(asset_dir)
@ -260,19 +177,24 @@ class CameraLoader(plugin.Loader):
)
# Add sequences data to hierarchy
for i in range(0, len(sequences) - 1):
self._set_sequence_hierarchy(
for i in range(len(sequences) - 1):
set_sequence_hierarchy(
sequences[i], sequences[i + 1],
frame_ranges[i + 1][0], frame_ranges[i + 1][1])
frame_ranges[i][1],
frame_ranges[i + 1][0], frame_ranges[i + 1][1],
[level])
project_name = legacy_io.active_project()
data = get_asset_by_name(project_name, asset)["data"]
cam_seq.set_display_rate(
unreal.FrameRate(data.get("fps"), 1.0))
cam_seq.set_playback_start(data.get('clipIn'))
cam_seq.set_playback_end(data.get('clipOut') + 1)
self._set_sequence_hierarchy(
set_sequence_hierarchy(
sequences[-1], cam_seq,
data.get('clipIn'), data.get('clipOut'))
frame_ranges[-1][1],
data.get('clipIn'), data.get('clipOut'),
[level])
settings = unreal.MovieSceneUserImportFBXSettings()
settings.set_editor_property('reduce_keys', False)
@ -307,7 +229,7 @@ class CameraLoader(plugin.Loader):
key.set_time(unreal.FrameNumber(value=new_time))
# Create Asset Container
unreal_pipeline.create_container(
create_container(
container=container_name, path=asset_dir)
data = {
@ -322,14 +244,14 @@ class CameraLoader(plugin.Loader):
"parent": context["representation"]["parent"],
"family": context["representation"]["context"]["family"]
}
unreal_pipeline.imprint(
"{}/{}".format(asset_dir, container_name), data)
imprint(f"{asset_dir}/{container_name}", data)
EditorLevelLibrary.save_all_dirty_levels()
EditorLevelLibrary.load_level(master_level)
# Save all assets in the hierarchy
asset_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=True
hierarchy_dir_list[0], recursive=True, include_folder=False
)
for a in asset_content:
@ -340,29 +262,27 @@ class CameraLoader(plugin.Loader):
def update(self, container, representation):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
root = "/Game/ayon"
curr_level_sequence = LevelSequenceLib.get_current_level_sequence()
curr_time = LevelSequenceLib.get_current_time()
is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport()
editor_subsystem = unreal.UnrealEditorSubsystem()
vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info()
asset_dir = container.get('namespace')
context = representation.get("context")
hierarchy = context.get('hierarchy').split("/")
h_dir = f"{root}/{hierarchy[0]}"
h_asset = hierarchy[0]
master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map"
EditorLevelLibrary.save_current_level()
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[asset_dir],
recursive_paths=False)
sequences = ar.get_assets(filter)
filter = unreal.ARFilter(
sequences = ar.get_assets(_filter)
_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[str(Path(asset_dir).parent.as_posix())],
package_paths=[asset_dir],
recursive_paths=True)
maps = ar.get_assets(filter)
maps = ar.get_assets(_filter)
# There should be only one map in the list
EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name())
@ -401,12 +321,18 @@ class CameraLoader(plugin.Loader):
root = "/Game/Ayon"
namespace = container.get('namespace').replace(f"{root}/", "")
ms_asset = namespace.split('/')[0]
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[f"{root}/{ms_asset}"],
recursive_paths=False)
sequences = ar.get_assets(filter)
sequences = ar.get_assets(_filter)
master_sequence = sequences[0].get_asset()
_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{root}/{ms_asset}"],
recursive_paths=False)
levels = ar.get_assets(_filter)
master_level = levels[0].get_asset().get_path_name()
sequences = [master_sequence]
@ -418,26 +344,20 @@ class CameraLoader(plugin.Loader):
for t in tracks:
if t.get_class() == unreal.MovieSceneSubTrack.static_class():
subscene_track = t
break
if subscene_track:
sections = subscene_track.get_sections()
for ss in sections:
if ss.get_sequence().get_name() == sequence_name:
parent = s
sub_scene = ss
# subscene_track.remove_section(ss)
break
sequences.append(ss.get_sequence())
# Update subscenes indexes.
i = 0
for ss in sections:
for i, ss in enumerate(sections):
ss.set_row_index(i)
i += 1
if parent:
break
assert parent, "Could not find the parent sequence"
assert parent, "Could not find the parent sequence"
EditorAssetLibrary.delete_asset(level_sequence.get_path_name())
@ -466,33 +386,63 @@ class CameraLoader(plugin.Loader):
str(representation["data"]["path"])
)
# Set range of all sections
# Changing the range of the section is not enough. We need to change
# the frame of all the keys in the section.
project_name = legacy_io.active_project()
asset = container.get('asset')
data = get_asset_by_name(project_name, asset)["data"]
for possessable in new_sequence.get_possessables():
for tracks in possessable.get_tracks():
for section in tracks.get_sections():
section.set_range(
data.get('clipIn'),
data.get('clipOut') + 1)
for channel in section.get_all_channels():
for key in channel.get_keys():
old_time = key.get_time().get_editor_property(
'frame_number')
old_time_value = old_time.get_editor_property(
'value')
new_time = old_time_value + (
data.get('clipIn') - data.get('frameStart')
)
key.set_time(unreal.FrameNumber(value=new_time))
data = {
"representation": str(representation["_id"]),
"parent": str(representation["parent"])
}
unreal_pipeline.imprint(
"{}/{}".format(asset_dir, container.get('container_name')), data)
imprint(f"{asset_dir}/{container.get('container_name')}", data)
EditorLevelLibrary.save_current_level()
asset_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=False)
f"{root}/{ms_asset}", recursive=True, include_folder=False)
for a in asset_content:
EditorAssetLibrary.save_asset(a)
EditorLevelLibrary.load_level(master_level)
if curr_level_sequence:
LevelSequenceLib.open_level_sequence(curr_level_sequence)
LevelSequenceLib.set_current_time(curr_time)
LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock)
editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot)
def remove(self, container):
path = Path(container.get("namespace"))
parent_path = str(path.parent.as_posix())
asset_dir = container.get('namespace')
path = Path(asset_dir)
ar = unreal.AssetRegistryHelpers.get_asset_registry()
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[f"{str(path.as_posix())}"],
package_paths=[asset_dir],
recursive_paths=False)
sequences = ar.get_assets(filter)
sequences = ar.get_assets(_filter)
if not sequences:
raise Exception("Could not find sequence.")
@ -500,11 +450,11 @@ class CameraLoader(plugin.Loader):
world = ar.get_asset_by_object_path(
EditorLevelLibrary.get_editor_world().get_path_name())
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{parent_path}"],
package_paths=[asset_dir],
recursive_paths=True)
maps = ar.get_assets(filter)
maps = ar.get_assets(_filter)
# There should be only one map in the list
if not maps:
@ -534,12 +484,18 @@ class CameraLoader(plugin.Loader):
root = "/Game/Ayon"
namespace = container.get('namespace').replace(f"{root}/", "")
ms_asset = namespace.split('/')[0]
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[f"{root}/{ms_asset}"],
recursive_paths=False)
sequences = ar.get_assets(filter)
sequences = ar.get_assets(_filter)
master_sequence = sequences[0].get_asset()
_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{root}/{ms_asset}"],
recursive_paths=False)
levels = ar.get_assets(_filter)
master_level = levels[0].get_full_name()
sequences = [master_sequence]
@ -547,10 +503,13 @@ class CameraLoader(plugin.Loader):
for s in sequences:
tracks = s.get_master_tracks()
subscene_track = None
visibility_track = None
for t in tracks:
if t.get_class() == unreal.MovieSceneSubTrack.static_class():
subscene_track = t
break
if (t.get_class() ==
unreal.MovieSceneLevelVisibilityTrack.static_class()):
visibility_track = t
if subscene_track:
sections = subscene_track.get_sections()
for ss in sections:
@ -560,23 +519,48 @@ class CameraLoader(plugin.Loader):
break
sequences.append(ss.get_sequence())
# Update subscenes indexes.
i = 0
for ss in sections:
for i, ss in enumerate(sections):
ss.set_row_index(i)
i += 1
if visibility_track:
sections = visibility_track.get_sections()
for ss in sections:
if (unreal.Name(f"{container.get('asset')}_map_camera")
in ss.get_level_names()):
visibility_track.remove_section(ss)
# Update visibility sections indexes.
i = -1
prev_name = []
for ss in sections:
if prev_name != ss.get_level_names():
i += 1
ss.set_row_index(i)
prev_name = ss.get_level_names()
if parent:
break
assert parent, "Could not find the parent sequence"
EditorAssetLibrary.delete_directory(str(path.as_posix()))
# Create a temporary level to delete the layout level.
EditorLevelLibrary.save_all_dirty_levels()
EditorAssetLibrary.make_directory(f"{root}/tmp")
tmp_level = f"{root}/tmp/temp_map"
if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"):
EditorLevelLibrary.new_level(tmp_level)
else:
EditorLevelLibrary.load_level(tmp_level)
# Delete the layout directory.
EditorAssetLibrary.delete_directory(asset_dir)
EditorLevelLibrary.load_level(master_level)
EditorAssetLibrary.delete_directory(f"{root}/tmp")
# Check if there isn't any more assets in the parent folder, and
# delete it if not.
asset_content = EditorAssetLibrary.list_assets(
parent_path, recursive=False, include_folder=True
path.parent.as_posix(), recursive=False, include_folder=True
)
if len(asset_content) == 0:
EditorAssetLibrary.delete_directory(parent_path)
EditorAssetLibrary.delete_directory(path.parent.as_posix())

View file

@ -5,15 +5,18 @@ import collections
from pathlib import Path
import unreal
from unreal import EditorAssetLibrary
from unreal import EditorLevelLibrary
from unreal import EditorLevelUtils
from unreal import AssetToolsHelpers
from unreal import FBXImportType
from unreal import MovieSceneLevelVisibilityTrack
from unreal import MovieSceneSubTrack
from unreal import (
EditorAssetLibrary,
EditorLevelLibrary,
EditorLevelUtils,
AssetToolsHelpers,
FBXImportType,
MovieSceneLevelVisibilityTrack,
MovieSceneSubTrack,
LevelSequenceEditorBlueprintLibrary as LevelSequenceLib,
)
from openpype.client import get_asset_by_name, get_assets, get_representations
from openpype.client import get_asset_by_name, get_representations
from openpype.pipeline import (
discover_loader_plugins,
loaders_from_representation,
@ -25,7 +28,13 @@ from openpype.pipeline import (
from openpype.pipeline.context_tools import get_current_project_asset
from openpype.settings import get_current_project_settings
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api import pipeline as unreal_pipeline
from openpype.hosts.unreal.api.pipeline import (
generate_sequence,
set_sequence_hierarchy,
create_container,
imprint,
ls,
)
class LayoutLoader(plugin.Loader):
@ -91,77 +100,6 @@ class LayoutLoader(plugin.Loader):
return None
@staticmethod
def _set_sequence_hierarchy(
seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths
):
# Get existing sequencer tracks or create them if they don't exist
tracks = seq_i.get_master_tracks()
subscene_track = None
visibility_track = None
for t in tracks:
if t.get_class() == unreal.MovieSceneSubTrack.static_class():
subscene_track = t
if (t.get_class() ==
unreal.MovieSceneLevelVisibilityTrack.static_class()):
visibility_track = t
if not subscene_track:
subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack)
if not visibility_track:
visibility_track = seq_i.add_master_track(
unreal.MovieSceneLevelVisibilityTrack)
# Create the sub-scene section
subscenes = subscene_track.get_sections()
subscene = None
for s in subscenes:
if s.get_editor_property('sub_sequence') == seq_j:
subscene = s
break
if not subscene:
subscene = subscene_track.add_section()
subscene.set_row_index(len(subscene_track.get_sections()))
subscene.set_editor_property('sub_sequence', seq_j)
subscene.set_range(
min_frame_j,
max_frame_j + 1)
# Create the visibility section
ar = unreal.AssetRegistryHelpers.get_asset_registry()
maps = []
for m in map_paths:
# Unreal requires to load the level to get the map name
EditorLevelLibrary.save_all_dirty_levels()
EditorLevelLibrary.load_level(m)
maps.append(str(ar.get_asset_by_object_path(m).asset_name))
vis_section = visibility_track.add_section()
index = len(visibility_track.get_sections())
vis_section.set_range(
min_frame_j,
max_frame_j + 1)
vis_section.set_visibility(unreal.LevelVisibility.VISIBLE)
vis_section.set_row_index(index)
vis_section.set_level_names(maps)
if min_frame_j > 1:
hid_section = visibility_track.add_section()
hid_section.set_range(
1,
min_frame_j)
hid_section.set_visibility(unreal.LevelVisibility.HIDDEN)
hid_section.set_row_index(index)
hid_section.set_level_names(maps)
if max_frame_j < max_frame_i:
hid_section = visibility_track.add_section()
hid_section.set_range(
max_frame_j + 1,
max_frame_i + 1)
hid_section.set_visibility(unreal.LevelVisibility.HIDDEN)
hid_section.set_row_index(index)
hid_section.set_level_names(maps)
def _transform_from_basis(self, transform, basis):
"""Transform a transform from a basis to a new basis."""
# Get the basis matrix
@ -352,63 +290,6 @@ class LayoutLoader(plugin.Loader):
sec_params = section.get_editor_property('params')
sec_params.set_editor_property('animation', animation)
@staticmethod
def _generate_sequence(h, h_dir):
tools = unreal.AssetToolsHelpers().get_asset_tools()
sequence = tools.create_asset(
asset_name=h,
package_path=h_dir,
asset_class=unreal.LevelSequence,
factory=unreal.LevelSequenceFactoryNew()
)
project_name = legacy_io.active_project()
asset_data = get_asset_by_name(
project_name,
h_dir.split('/')[-1],
fields=["_id", "data.fps"]
)
start_frames = []
end_frames = []
elements = list(get_assets(
project_name,
parent_ids=[asset_data["_id"]],
fields=["_id", "data.clipIn", "data.clipOut"]
))
for e in elements:
start_frames.append(e.get('data').get('clipIn'))
end_frames.append(e.get('data').get('clipOut'))
elements.extend(get_assets(
project_name,
parent_ids=[e["_id"]],
fields=["_id", "data.clipIn", "data.clipOut"]
))
min_frame = min(start_frames)
max_frame = max(end_frames)
sequence.set_display_rate(
unreal.FrameRate(asset_data.get('data').get("fps"), 1.0))
sequence.set_playback_start(min_frame)
sequence.set_playback_end(max_frame)
tracks = sequence.get_master_tracks()
track = None
for t in tracks:
if (t.get_class() ==
unreal.MovieSceneCameraCutTrack.static_class()):
track = t
break
if not track:
track = sequence.add_master_track(
unreal.MovieSceneCameraCutTrack)
return sequence, (min_frame, max_frame)
def _get_repre_docs_by_version_id(self, data):
version_ids = {
element.get("version")
@ -696,7 +577,7 @@ class LayoutLoader(plugin.Loader):
]
if not existing_sequences:
sequence, frame_range = self._generate_sequence(h, h_dir)
sequence, frame_range = generate_sequence(h, h_dir)
sequences.append(sequence)
frame_ranges.append(frame_range)
@ -716,7 +597,7 @@ class LayoutLoader(plugin.Loader):
# sequences and frame_ranges have the same length
for i in range(0, len(sequences) - 1):
self._set_sequence_hierarchy(
set_sequence_hierarchy(
sequences[i], sequences[i + 1],
frame_ranges[i][1],
frame_ranges[i + 1][0], frame_ranges[i + 1][1],
@ -729,7 +610,7 @@ class LayoutLoader(plugin.Loader):
shot.set_playback_start(0)
shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1)
if sequences:
self._set_sequence_hierarchy(
set_sequence_hierarchy(
sequences[-1], shot,
frame_ranges[-1][1],
data.get('clipIn'), data.get('clipOut'),
@ -745,7 +626,7 @@ class LayoutLoader(plugin.Loader):
EditorLevelLibrary.save_current_level()
# Create Asset Container
unreal_pipeline.create_container(
create_container(
container=container_name, path=asset_dir)
data = {
@ -761,11 +642,13 @@ class LayoutLoader(plugin.Loader):
"family": context["representation"]["context"]["family"],
"loaded_assets": loaded_assets
}
unreal_pipeline.imprint(
imprint(
"{}/{}".format(asset_dir, container_name), data)
save_dir = hierarchy_dir_list[0] if create_sequences else asset_dir
asset_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=False)
save_dir, recursive=True, include_folder=False)
for a in asset_content:
EditorAssetLibrary.save_asset(a)
@ -781,16 +664,24 @@ class LayoutLoader(plugin.Loader):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
curr_level_sequence = LevelSequenceLib.get_current_level_sequence()
curr_time = LevelSequenceLib.get_current_time()
is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport()
editor_subsystem = unreal.UnrealEditorSubsystem()
vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info()
root = "/Game/Ayon"
asset_dir = container.get('namespace')
context = representation.get("context")
hierarchy = context.get('hierarchy').split("/")
sequence = None
master_level = None
if create_sequences:
hierarchy = context.get('hierarchy').split("/")
h_dir = f"{root}/{hierarchy[0]}"
h_asset = hierarchy[0]
master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map"
@ -843,13 +734,15 @@ class LayoutLoader(plugin.Loader):
"parent": str(representation["parent"]),
"loaded_assets": loaded_assets
}
unreal_pipeline.imprint(
imprint(
"{}/{}".format(asset_dir, container.get('container_name')), data)
EditorLevelLibrary.save_current_level()
save_dir = f"{root}/{hierarchy[0]}" if create_sequences else asset_dir
asset_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=False)
save_dir, recursive=True, include_folder=False)
for a in asset_content:
EditorAssetLibrary.save_asset(a)
@ -859,6 +752,13 @@ class LayoutLoader(plugin.Loader):
elif prev_level:
EditorLevelLibrary.load_level(prev_level)
if curr_level_sequence:
LevelSequenceLib.open_level_sequence(curr_level_sequence)
LevelSequenceLib.set_current_time(curr_time)
LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock)
editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot)
def remove(self, container):
"""
Delete the layout. First, check if the assets loaded with the layout
@ -870,7 +770,7 @@ class LayoutLoader(plugin.Loader):
root = "/Game/Ayon"
path = Path(container.get("namespace"))
containers = unreal_pipeline.ls()
containers = ls()
layout_containers = [
c for c in containers
if (c.get('asset_name') != container.get('asset_name') and

View file

@ -6,6 +6,8 @@ import subprocess
from distutils import dir_util
from pathlib import Path
from typing import List, Union
import tempfile
from distutils.dir_util import copy_tree
import openpype.hosts.unreal.lib as ue_lib
@ -90,9 +92,20 @@ class UEProjectGenerationWorker(QtCore.QObject):
("Generating a new UE project ... 1 out of "
f"{stage_count}"))
# Need to copy the commandlet project to a temporary folder where
# users don't need admin rights to write to.
cmdlet_tmp = tempfile.TemporaryDirectory()
cmdlet_filename = cmdlet_project.name
cmdlet_dir = cmdlet_project.parent.as_posix()
cmdlet_tmp_name = Path(cmdlet_tmp.name)
cmdlet_tmp_file = cmdlet_tmp_name.joinpath(cmdlet_filename)
copy_tree(
cmdlet_dir,
cmdlet_tmp_name.as_posix())
commandlet_cmd = [
f"{ue_editor_exe.as_posix()}",
f"{cmdlet_project.as_posix()}",
f"{cmdlet_tmp_file.as_posix()}",
"-run=AyonGenerateProject",
f"{project_file.resolve().as_posix()}",
]
@ -111,6 +124,8 @@ class UEProjectGenerationWorker(QtCore.QObject):
gen_process.stdout.close()
return_code = gen_process.wait()
cmdlet_tmp.cleanup()
if return_code and return_code != 0:
msg = (
f"Failed to generate {self.project_name} "

View file

@ -275,7 +275,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
args = [
"--headless",
'publish',
rootless_metadata_path,
'"{}"'.format(rootless_metadata_path),
"--targets", "deadline",
"--targets", "farm"
]

View file

@ -234,6 +234,10 @@ class BaseAction(BaseHandler):
if not settings_roles:
return default
user_roles = {
role_name.lower()
for role_name in user_roles
}
for role_name in settings_roles:
if role_name.lower() in user_roles:
return True
@ -264,8 +268,15 @@ class BaseAction(BaseHandler):
return user_entity
@classmethod
def get_user_roles_from_event(cls, session, event):
"""Query user entity from event."""
def get_user_roles_from_event(cls, session, event, lower=True):
"""Get user roles based on data in event.
Args:
session (ftrack_api.Session): Prepared ftrack session.
event (ftrack_api.event.Event): Event which is processed.
lower (Optional[bool]): Lower the role names. Default 'True'.
"""
not_set = object()
user_roles = event["data"].get("user_roles", not_set)
@ -273,7 +284,10 @@ class BaseAction(BaseHandler):
user_roles = []
user_entity = cls.get_user_entity_from_event(session, event)
for role in user_entity["user_security_roles"]:
user_roles.append(role["security_role"]["name"].lower())
role_name = role["security_role"]["name"]
if lower:
role_name = role_name.lower()
user_roles.append(role_name)
event["data"]["user_roles"] = user_roles
return user_roles
@ -322,7 +336,8 @@ class BaseAction(BaseHandler):
if not settings.get(self.settings_enabled_key, True):
return False
user_role_list = self.get_user_roles_from_event(session, event)
user_role_list = self.get_user_roles_from_event(
session, event, lower=False)
if not self.roles_check(settings.get("role_list"), user_role_list):
return False
return True

View file

@ -296,9 +296,9 @@ def server_activity_validate_user(event):
if not user_ent:
return False
role_list = ["Pypeclub", "Administrator"]
role_list = {"pypeclub", "administrator"}
for role in user_ent["user_security_roles"]:
if role["security_role"]["name"] in role_list:
if role["security_role"]["name"].lower() in role_list:
return True
return False

View file

@ -94,7 +94,7 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction):
return {
"publish": [os.path.join(current_dir, "plugins", "publish")],
"actions": [os.path.join(current_dir, "actions")]
"actions": [os.path.join(current_dir, "actions")],
}
def cli(self, click_group):
@ -128,15 +128,35 @@ def push_to_zou(login, password):
@click.option(
"-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username"
)
def sync_service(login, password):
@click.option(
"-prj",
"--project",
"projects",
multiple=True,
default=[],
help="Sync specific kitsu projects",
)
@click.option(
"-lo",
"--listen-only",
"listen_only",
is_flag=True,
default=False,
help="Listen to events only without any syncing",
)
def sync_service(login, password, projects, listen_only):
"""Synchronize openpype database from Zou sever database.
Args:
login (str): Kitsu user login
password (str): Kitsu user password
projects (tuple): specific kitsu projects
listen_only (bool): run listen only without any syncing
"""
from .utils.update_op_with_zou import sync_all_projects
from .utils.sync_service import start_listeners
sync_all_projects(login, password)
if not listen_only:
sync_all_projects(login, password, filter_projects=projects)
start_listeners(login, password)

View file

@ -94,9 +94,7 @@ def update_op_assets(
if not item_doc: # Create asset
op_asset = create_op_asset(item)
insert_result = dbcon.insert_one(op_asset)
item_doc = get_asset_by_id(
project_name, insert_result.inserted_id
)
item_doc = get_asset_by_id(project_name, insert_result.inserted_id)
# Update asset
item_data = deepcopy(item_doc["data"])
@ -329,7 +327,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne:
"code": project_code,
"fps": float(project["fps"]),
"zou_id": project["id"],
"active": project['project_status_name'] != "Closed",
"active": project["project_status_name"] != "Closed",
}
)
@ -359,7 +357,10 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne:
def sync_all_projects(
login: str, password: str, ignore_projects: list = None
login: str,
password: str,
ignore_projects: list = None,
filter_projects: tuple = None,
):
"""Update all OP projects in DB with Zou data.
@ -367,6 +368,7 @@ def sync_all_projects(
login (str): Kitsu user login
password (str): Kitsu user password
ignore_projects (list): List of unsynced project names
filter_projects (tuple): Tuple of filter project names to sync with
Raises:
gazu.exception.AuthFailedException: Wrong user login and/or password
"""
@ -381,7 +383,24 @@ def sync_all_projects(
dbcon = AvalonMongoDB()
dbcon.install()
all_projects = gazu.project.all_projects()
for project in all_projects:
project_to_sync = []
if filter_projects:
all_kitsu_projects = {p["name"]: p for p in all_projects}
for proj_name in filter_projects:
if proj_name in all_kitsu_projects:
project_to_sync.append(all_kitsu_projects[proj_name])
else:
log.info(
f"`{proj_name}` project does not exist in Kitsu."
f" Please make sure the project is spelled correctly."
)
else:
# all project
project_to_sync = all_projects
for project in project_to_sync:
if ignore_projects and project["name"] in ignore_projects:
continue
sync_project_from_kitsu(dbcon, project)
@ -408,14 +427,13 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict):
# Get all statuses for projects from Kitsu
all_status = gazu.project.all_project_status()
for status in all_status:
if project['project_status_id'] == status['id']:
project['project_status_name'] = status['name']
if project["project_status_id"] == status["id"]:
project["project_status_name"] = status["name"]
break
# Do not sync closed kitsu project that is not found in openpype
if (
project['project_status_name'] == "Closed"
and not get_project(project['name'])
if project["project_status_name"] == "Closed" and not get_project(
project["name"]
):
return
@ -444,7 +462,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict):
log.info("Project created: {}".format(project_name))
bulk_writes.append(write_project_to_op(project, dbcon))
if project['project_status_name'] == "Closed":
if project["project_status_name"] == "Closed":
return
# Try to find project document

View file

@ -26,11 +26,13 @@ class CollectFramesFixDef(
targets = ["local"]
hosts = ["nuke"]
families = ["render", "prerender"]
enabled = True
rewrite_version_enable = False
def process(self, instance):
attribute_values = self.get_attr_values_from_data(instance.data)
frames_to_fix = attribute_values.get("frames_to_fix")
rewrite_version = attribute_values.get("rewrite_version")
if frames_to_fix:
@ -71,10 +73,19 @@ class CollectFramesFixDef(
@classmethod
def get_attribute_defs(cls):
return [
attributes = [
TextDef("frames_to_fix", label="Frames to fix",
placeholder="5,10-15",
regex="[0-9,-]+"),
BoolDef("rewrite_version", label="Rewrite latest version",
default=False),
regex="[0-9,-]+")
]
if cls.rewrite_version_enable:
attributes.append(
BoolDef(
"rewrite_version",
label="Rewrite latest version",
default=False
)
)
return attributes

View file

@ -1,4 +1,9 @@
{
"unit_scale_settings": {
"enabled": true,
"apply_on_opening": false,
"base_file_unit_scale": 0.01
},
"imageio": {
"ocio_config": {
"enabled": false,

View file

@ -46,6 +46,10 @@
"enabled": false,
"families": []
},
"CollectFramesFixDef": {
"enabled": true,
"rewrite_version_enable": true
},
"ValidateEditorialAssetName": {
"enabled": true,
"optional": false
@ -252,7 +256,9 @@
}
},
{
"families": ["review"],
"families": [
"review"
],
"hosts": [
"maya",
"houdini"

View file

@ -15,6 +15,6 @@
"preroll_frames": 0,
"render_format": "png",
"project_setup": {
"dev_mode": true
"dev_mode": false
}
}

View file

@ -5,6 +5,32 @@
"label": "Blender",
"is_file": true,
"children": [
{
"key": "unit_scale_settings",
"type": "dict",
"label": "Set Unit Scale",
"collapsible": true,
"is_group": true,
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"key": "apply_on_opening",
"type": "boolean",
"label": "Apply on Opening Existing Files"
},
{
"key": "base_file_unit_scale",
"type": "number",
"label": "Base File Unit Scale",
"decimal": 10
}
]
},
{
"key": "imageio",
"type": "dict",

View file

@ -81,6 +81,26 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "CollectFramesFixDef",
"label": "Collect Frames to Fix",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "rewrite_version_enable",
"label": "Show 'Rewrite latest version' toggle"
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -14,32 +14,44 @@ class _VLineWidget(QtWidgets.QWidget):
It is expected that parent widget will set width.
"""
def __init__(self, color, left, parent):
def __init__(self, color, line_size, left, parent):
super(_VLineWidget, self).__init__(parent)
self._color = color
self._left = left
self._line_size = line_size
def set_line_size(self, line_size):
self._line_size = line_size
def paintEvent(self, event):
if not self.isVisible():
return
if self._left:
pos_x = 0
else:
pos_x = self.width()
pos_x = self._line_size * 0.5
if not self._left:
pos_x = self.width() - pos_x
painter = QtGui.QPainter(self)
painter.setRenderHints(
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if self._color:
pen = QtGui.QPen(self._color)
else:
pen = painter.pen()
pen.setWidth(1)
pen.setWidth(self._line_size)
painter.setPen(pen)
painter.setBrush(QtCore.Qt.transparent)
painter.drawLine(pos_x, 0, pos_x, self.height())
painter.drawRect(
QtCore.QRectF(
pos_x,
-self._line_size,
pos_x + (self.width() * 2),
self.height() + (self._line_size * 2)
)
)
painter.end()
@ -56,34 +68,46 @@ class _HBottomLineWidget(QtWidgets.QWidget):
It is expected that parent widget will set height and radius.
"""
def __init__(self, color, parent):
def __init__(self, color, line_size, parent):
super(_HBottomLineWidget, self).__init__(parent)
self._color = color
self._radius = 0
self._line_size = line_size
def set_radius(self, radius):
self._radius = radius
def set_line_size(self, line_size):
self._line_size = line_size
def paintEvent(self, event):
if not self.isVisible():
return
rect = QtCore.QRect(
0, -self._radius, self.width(), self.height() + self._radius
x_offset = self._line_size * 0.5
rect = QtCore.QRectF(
x_offset,
-self._radius,
self.width() - (2 * x_offset),
(self.height() + self._radius) - x_offset
)
painter = QtGui.QPainter(self)
painter.setRenderHints(
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if self._color:
pen = QtGui.QPen(self._color)
else:
pen = painter.pen()
pen.setWidth(1)
pen.setWidth(self._line_size)
painter.setPen(pen)
painter.setBrush(QtCore.Qt.transparent)
painter.drawRoundedRect(rect, self._radius, self._radius)
if self._radius:
painter.drawRoundedRect(rect, self._radius, self._radius)
else:
painter.drawRect(rect)
painter.end()
@ -102,30 +126,38 @@ class _HTopCornerLineWidget(QtWidgets.QWidget):
It is expected that parent widget will set height and radius.
"""
def __init__(self, color, left_side, parent):
def __init__(self, color, line_size, left_side, parent):
super(_HTopCornerLineWidget, self).__init__(parent)
self._left_side = left_side
self._line_size = line_size
self._color = color
self._radius = 0
def set_radius(self, radius):
self._radius = radius
def set_line_size(self, line_size):
self._line_size = line_size
def paintEvent(self, event):
if not self.isVisible():
return
pos_y = self.height() / 2
pos_y = self.height() * 0.5
x_offset = self._line_size * 0.5
if self._left_side:
rect = QtCore.QRect(
0, pos_y, self.width() + self._radius, self.height()
rect = QtCore.QRectF(
x_offset,
pos_y,
self.width() + self._radius + x_offset,
self.height()
)
else:
rect = QtCore.QRect(
-self._radius,
rect = QtCore.QRectF(
(-self._radius),
pos_y,
self.width() + self._radius,
(self.width() + self._radius) - x_offset,
self.height()
)
@ -138,10 +170,13 @@ class _HTopCornerLineWidget(QtWidgets.QWidget):
pen = QtGui.QPen(self._color)
else:
pen = painter.pen()
pen.setWidth(1)
pen.setWidth(self._line_size)
painter.setPen(pen)
painter.setBrush(QtCore.Qt.transparent)
painter.drawRoundedRect(rect, self._radius, self._radius)
if self._radius:
painter.drawRoundedRect(rect, self._radius, self._radius)
else:
painter.drawRect(rect)
painter.end()
@ -163,8 +198,10 @@ class BorderedLabelWidget(QtWidgets.QFrame):
if color_value:
color = color_value.get_qcolor()
top_left_w = _HTopCornerLineWidget(color, True, self)
top_right_w = _HTopCornerLineWidget(color, False, self)
line_size = 1
top_left_w = _HTopCornerLineWidget(color, line_size, True, self)
top_right_w = _HTopCornerLineWidget(color, line_size, False, self)
label_widget = QtWidgets.QLabel(label, self)
@ -175,10 +212,10 @@ class BorderedLabelWidget(QtWidgets.QFrame):
top_layout.addWidget(label_widget, 0)
top_layout.addWidget(top_right_w, 1)
left_w = _VLineWidget(color, True, self)
right_w = _VLineWidget(color, False, self)
left_w = _VLineWidget(color, line_size, True, self)
right_w = _VLineWidget(color, line_size, False, self)
bottom_w = _HBottomLineWidget(color, self)
bottom_w = _HBottomLineWidget(color, line_size, self)
center_layout = QtWidgets.QHBoxLayout()
center_layout.setContentsMargins(5, 5, 5, 5)
@ -201,6 +238,7 @@ class BorderedLabelWidget(QtWidgets.QFrame):
self._widget = None
self._radius = 0
self._line_size = line_size
self._top_left_w = top_left_w
self._top_right_w = top_right_w
@ -216,14 +254,38 @@ class BorderedLabelWidget(QtWidgets.QFrame):
value, value, value, value
)
def set_line_size(self, line_size):
if self._line_size == line_size:
return
self._line_size = line_size
for widget in (
self._top_left_w,
self._top_right_w,
self._left_w,
self._right_w,
self._bottom_w
):
widget.set_line_size(line_size)
self._recalculate_sizes()
def showEvent(self, event):
super(BorderedLabelWidget, self).showEvent(event)
self._recalculate_sizes()
def _recalculate_sizes(self):
height = self._label_widget.height()
radius = (height + (height % 2)) / 2
radius = int((height + (height % 2)) / 2)
self._radius = radius
side_width = 1 + radius
radius_size = self._line_size + 1
if radius_size < radius:
radius_size = radius
if radius:
side_width = self._line_size + radius
else:
side_width = self._line_size + 1
# Don't use fixed width/height as that would set also set
# the other size (When fixed width is set then is also set
# fixed height).
@ -231,8 +293,8 @@ class BorderedLabelWidget(QtWidgets.QFrame):
self._left_w.setMaximumWidth(side_width)
self._right_w.setMinimumWidth(side_width)
self._right_w.setMaximumWidth(side_width)
self._bottom_w.setMinimumHeight(radius)
self._bottom_w.setMaximumHeight(radius)
self._bottom_w.setMinimumHeight(radius_size)
self._bottom_w.setMaximumHeight(radius_size)
self._bottom_w.set_radius(radius)
self._top_right_w.set_radius(radius)
self._top_left_w.set_radius(radius)

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.15.9-nightly.1"
__version__ = "3.15.9"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.15.8" # OpenPype
version = "3.15.9" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"

View file

@ -18,9 +18,20 @@ This setting is available for all the users of the OpenPype instance.
## Synchronize
Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets.
Once this sync is done, the thread will automatically start a loop to listen to Kitsu events.
- `-prj, --project` This flag accepts multiple project name to sync specific projects, and the default to sync all projects.
- `-lo, --listen-only` This flag to run listen to Kitsu events only without any sync.
Note: You must use one argument of `-pro` or `-lo`, because the listen only flag override syncing flag.
```bash
// sync all projects then run listen
openpype_console module kitsu sync-service -l me@domain.ext -p my_password
// sync specific projects then run listen
openpype_console module kitsu sync-service -l me@domain.ext -p my_password -prj project_name01 -prj project_name02
// start listen only for all projects
openpype_console module kitsu sync-service -l me@domain.ext -p my_password -lo
```
### Events listening

View file

@ -63,7 +63,7 @@ Example here describes use case for creation of new color coded review of png im
![global_oiio_transcode](assets/global_oiio_transcode.png)
Another use case is to transcode in Maya only `beauty` render layers and use collected `Display` and `View` colorspaces from DCC.
![global_oiio_transcode_in_Maya](assets/global_oiio_transcode.png)n
![global_oiio_transcode_in_Maya](assets/global_oiio_transcode2.png)
## Profile filters