Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editorial_pkg: publishing sequence into OTIO timeline with rendered media #132

Open
wants to merge 38 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
72c2f1e
wip for creator, collector and extractor for editorial product type
moonyuet Oct 10, 2024
ef4e87d
remove collect editorial as it is not gonna use for this workflow
moonyuet Oct 10, 2024
9f24405
remove otio-related workflow as it is not related to this PR
moonyuet Oct 10, 2024
f96b691
wip the creator and render for clip function
moonyuet Oct 11, 2024
bbfafef
wip the creator
moonyuet Oct 11, 2024
58e46bf
wip on the creator and collector
moonyuet Oct 14, 2024
b6082c4
wip on the creator
moonyuet Oct 14, 2024
427828e
use editorial pkg product type
moonyuet Oct 15, 2024
f4a6956
remove to return unnecessary function
moonyuet Oct 15, 2024
4962722
wip on implementation
moonyuet Oct 15, 2024
ea40c2d
draft the otio_unreal_export script
moonyuet Oct 31, 2024
9377c8d
draft the otio_unreal_export script
moonyuet Oct 31, 2024
47dd36a
resolve conflict
moonyuet Nov 1, 2024
3001478
wip creator
moonyuet Nov 1, 2024
521d94b
wip creator
moonyuet Nov 1, 2024
1e60e01
add intermediate renders and supports rendering for editorial package
moonyuet Nov 4, 2024
4864345
add the check on the publish error in the collector if the users do n…
moonyuet Nov 4, 2024
306cd6e
make sure the frame range is correct
moonyuet Nov 4, 2024
6f93a9c
edit rendering py for rendering out the image in different directory
moonyuet Nov 4, 2024
c6afe1e
fix the enumerate value
moonyuet Nov 4, 2024
be237a5
update the intermediate render product type as editorial publish and …
moonyuet Nov 5, 2024
2cbab6e
edit the unreal_export.py
moonyuet Nov 5, 2024
b146626
coverting otio logic to accompany wioth unreal in unreal_export.py
moonyuet Nov 5, 2024
9291eaa
coverting otio logic to accompany wioth unreal in unreal_export.py
moonyuet Nov 5, 2024
78d794e
implement unreal logic into unreal_export.py
moonyuet Nov 6, 2024
beb46a3
impelement the extractor for intermediate render
moonyuet Nov 7, 2024
fd08023
implement pre-launch hook for otio installation
moonyuet Nov 8, 2024
adf1d84
implement extract edidtorial package.py
moonyuet Nov 8, 2024
454c941
make sure the opentimelineio is using 0.16.0
moonyuet Nov 8, 2024
aa662db
edit the unreal export.py for getting correct track name
moonyuet Nov 8, 2024
726e169
update the extract data for the representation
moonyuet Nov 8, 2024
8e5a726
add remove tags
moonyuet Nov 8, 2024
90a08c9
add remove tags
moonyuet Nov 8, 2024
b08b410
ruff cosmetic fix
moonyuet Nov 8, 2024
f5a6ce1
uses get_name instead of get_sequence().get_name()
moonyuet Nov 11, 2024
ac16030
uses get_name instead of get_sequence().get_name()
moonyuet Nov 11, 2024
403cf56
resolve conflict
moonyuet Nov 13, 2024
aa61db2
make sure the image sequences not being rendered per sub sequence dur…
moonyuet Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions client/ayon_unreal/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,34 @@ def import_animation(
sequence.get_playback_end())
sec_params = section.get_editor_property('params')
sec_params.set_editor_property('animation', animation)


def get_shot_track_names(sel_objects=None, get_name=True):
selection = [
a for a in sel_objects
if a.get_class().get_name() == "LevelSequence"
]

sub_sequence_tracks = [
track for sel in selection for track in
sel.find_master_tracks_by_type(unreal.MovieSceneSubTrack)
]

if get_name:
return [shot_tracks.get_display_name() for shot_tracks in
sub_sequence_tracks]
else:
return [shot_tracks for shot_tracks in sub_sequence_tracks]


def get_shot_tracks(members):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
selected_sequences = [
ar.get_asset_by_object_path(member).get_asset() for member in members
]
return get_shot_track_names(selected_sequences, get_name=False)


def get_screen_resolution():
game_user_settings = unreal.GameUserSettings.get_game_user_settings()
return game_user_settings.get_screen_resolution()
56 changes: 56 additions & 0 deletions client/ayon_unreal/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,30 @@ def get_subsequences(sequence: unreal.LevelSequence):
return []


def get_movie_shot_tracks(sequence: unreal.LevelSequence):
"""Get list of movie shot tracks from sequence.

Args:
sequence (unreal.LevelSequence): Sequence

Returns:
list(unreal.LevelSequence): List of movie shot tracks

"""
tracks = sequence.find_master_tracks_by_type(unreal.MovieSceneSubTrack)
subscene_track = next(
(
t
for t in tracks
if t.get_class() == unreal.MovieSceneCinematicShotTrack.static_class()
),
None,
)
if subscene_track is not None and subscene_track.get_sections():
return subscene_track.get_sections()
return []


def set_sequence_hierarchy(
seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths
):
Expand Down Expand Up @@ -928,6 +952,38 @@ def get_sequence(files):
return [os.path.basename(filename) for filename in collections[0]]


def get_sequence_for_otio(files):
"""Get sequence from filename.

This will only return files if they exist on disk as it tries
to collect the sequence using the filename pattern and searching
for them on disk.

Supports negative frame ranges like -001, 0000, 0001 and -0001,
0000, 0001.

Arguments:
files (str): List of files

Returns:
Optional[list[str]]: file sequence.
Optional[str]: file head.

"""
base_filenames = [os.path.basename(filename) for filename in files]
collections, _remainder = clique.assemble(
base_filenames,
patterns=[clique.PATTERNS["frames"]],
minimum_items=1)

if len(collections) > 1:
raise ValueError(
f"Multiple collections found for {collections}. "
"This is a bug.")
filename_padding = collections[0].padding
return filename_padding


def find_camera_actors_in_camera_tracks(sequence) -> list[Any]:
"""Find the camera actors in the tracks from the Level Sequence

Expand Down
15 changes: 9 additions & 6 deletions client/ayon_unreal/api/rendering.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os

import unreal

from ayon_core.settings import get_project_settings
Expand Down Expand Up @@ -130,7 +129,7 @@ def start_rendering():

for i in instances:
data = pipeline.parse_container(i.get_path_name())
if data["productType"] == "render":
if data["productType"] == "render" or "editorial_pkg":
inst_data.append(data)

try:
Expand Down Expand Up @@ -159,6 +158,8 @@ def start_rendering():
current_level_name = current_level.get_outer().get_path_name()

for i in inst_data:
if i["productType"] == "editorial_pkg":
render_dir = f"{root}/{project_name}/editorial_pkg"
sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset()

sequences = [{
Expand All @@ -176,12 +177,15 @@ def start_rendering():
for seq in sequences:
subscenes = pipeline.get_subsequences(seq.get('sequence'))

if subscenes:
if subscenes and i["productType"] != "editorial_pkg":
for sub_seq in subscenes:
sub_seq_obj = sub_seq.get_sequence()
if sub_seq_obj is None:
continue
sequences.append({
"sequence": sub_seq.get_sequence(),
"sequence": sub_seq_obj,
"output": (f"{seq.get('output')}/"
f"{sub_seq.get_sequence().get_name()}"),
f"{sub_seq_obj.get_name()}"),
"frame_range": (
sub_seq.get_start_frame(), sub_seq.get_end_frame())
})
Expand Down Expand Up @@ -213,7 +217,6 @@ def start_rendering():
# read in the job's OnJobFinished callback. We could,
# for instance, pass the AyonPublishInstance's path to the job.
# job.user_data = ""

output_dir = render_setting.get('output')
shot_name = render_setting.get('sequence').get_name()

Expand Down
237 changes: 237 additions & 0 deletions client/ayon_unreal/hooks/pre_otio_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import os
import subprocess
from platform import system
from ayon_applications import PreLaunchHook, LaunchTypes


class InstallOtioToBlender(PreLaunchHook):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class InstallOtioToBlender(PreLaunchHook):
class InstallOtioToUnreal(PreLaunchHook):

"""Install Qt binding to Unreal's python packages.

Prelaunch hook does 2 things:
1.) Unreal's python packages are pushed to the beginning of PYTHONPATH.
2.) Check if Unreal has installed otio and will try to install if not.

For pipeline implementation is required to have Qt binding installed in
Unreal's python packages.
"""

app_groups = {"unreal"}
launch_types = {LaunchTypes.local}

def execute(self):
# Prelaunch hook is not crucial
try:
self.inner_execute()
except Exception:
self.log.warning(
"Processing of {} crashed.".format(self.__class__.__name__),
exc_info=True
)

def inner_execute(self):
platform = system().lower()
executable = self.launch_context.executable.executable_path
expected_executable = "UnrealEditor"
if platform == "windows":
expected_executable += ".exe"

if os.path.basename(executable) != expected_executable:
self.log.info((
f"Executable does not lead to {expected_executable} file."
"Can't determine Unreal's python to check/install"
" otio binding."
))
return

versions_dir = self.find_parent_directory(executable)
otio_binding = "opentimelineio"
otio_binding_version = None

python_dir = os.path.join(versions_dir, "ThirdParty", "Python3", "Win64")
python_version = "python"

if platform == "windows":
python_executable = os.path.join(python_dir, "python.exe")
else:
python_executable = os.path.join(python_dir, python_version)
# Check for python with enabled 'pymalloc'
if not os.path.exists(python_executable):
python_executable += "m"

if not os.path.exists(python_executable):
self.log.warning(
"Couldn't find python executable for Unreal. {}".format(
executable
)
)
return

# Check if otio is installed and skip if yes
if self.is_otio_installed(python_executable, otio_binding):
self.log.debug("Unreal has already installed otio.")
return

# Install otio in Unreal's python
if platform == "windows":
result = self.install_otio_windows(
python_executable,
otio_binding,
otio_binding_version
)
else:
result = self.install_otio(
python_executable,
otio_binding,
otio_binding_version
)

if result:
self.log.info(
f"Successfully installed {otio_binding} module to Unreal."
)
else:
self.log.warning(
f"Failed to install {otio_binding} module to Unreal."
)

def install_otio_windows(
self,
python_executable,
otio_binding,
otio_binding_version
):
"""Install otio python module to Unreal's python.

Installation requires administration rights that's why it is required
to use "pywin32" module which can execute command's and ask for
administration rights.
"""
try:
import win32con
import win32process
import win32event
import pywintypes
from win32comext.shell.shell import ShellExecuteEx
from win32comext.shell import shellcon
except Exception:
self.log.warning("Couldn't import \"pywin32\" modules")
return


otio_binding = f"{otio_binding}==0.16.0"

try:
# Parameters
# - use "-m pip" as module pip to install otio and argument
# "--ignore-installed" is to force install module to Unreal's
# site-packages and make sure it is binary compatible
fake_exe = "fake.exe"
args = [
fake_exe,
"-m",
"pip",
"install",
"--ignore-installed",
otio_binding,
]

parameters = (
subprocess.list2cmdline(args)
.lstrip(fake_exe)
.lstrip(" ")
)

# Execute command and ask for administrator's rights
process_info = ShellExecuteEx(
nShow=win32con.SW_SHOWNORMAL,
fMask=shellcon.SEE_MASK_NOCLOSEPROCESS,
lpVerb="runas",
lpFile=python_executable,
lpParameters=parameters,
lpDirectory=os.path.dirname(python_executable)
)
process_handle = process_info["hProcess"]
win32event.WaitForSingleObject(process_handle, win32event.INFINITE)
returncode = win32process.GetExitCodeProcess(process_handle)
return returncode == 0
except pywintypes.error:
pass

def install_otio(
self,
python_executable,
otio_binding,
otio_binding_version,
):
"""Install Qt binding python module to Unreal's python."""
if otio_binding_version:
otio_binding = f"{otio_binding}=={otio_binding_version}"
try:
# Parameters
# - use "-m pip" as module pip to install qt binding and argument
# "--ignore-installed" is to force install module to Unreal's
# site-packages and make sure it is binary compatible
# TODO find out if Unreal 4.x on linux/darwin does install
# qt binding to correct place.
args = [
python_executable,
"-m",
"pip",
"install",
"--ignore-installed",
otio_binding,
]
process = subprocess.Popen(
args, stdout=subprocess.PIPE, universal_newlines=True
)
process.communicate()
return process.returncode == 0
except PermissionError:
self.log.warning(
"Permission denied with command:"
"\"{}\".".format(" ".join(args))
)
except OSError as error:
self.log.warning(f"OS error has occurred: \"{error}\".")
except subprocess.SubprocessError:
pass

def is_otio_installed(self, python_executable, otio_binding):
"""Check if OTIO module is in Unreal's pip list.

Check that otio is installed directly in Unreal's site-packages.
It is possible that it is installed in user's site-packages but that
may be incompatible with Unreal's python.
"""

otio_binding_low = otio_binding.lower()
# Get pip list from Unreal's python executable
args = [python_executable, "-m", "pip", "list"]
process = subprocess.Popen(args, stdout=subprocess.PIPE)
stdout, _ = process.communicate()
lines = stdout.decode().split(os.linesep)
# Second line contain dashes that define maximum length of module name.
# Second column of dashes define maximum length of module version.
package_dashes, *_ = lines[1].split(" ")
package_len = len(package_dashes)

# Got through printed lines starting at line 3
for idx in range(2, len(lines)):
line = lines[idx]
if not line:
continue
package_name = line[0:package_len].strip()
if package_name.lower() == otio_binding_low:
return True
return False

def find_parent_directory(self, file_path, target_dir="Binaries"):
# Split the path into components
path_components = file_path.split(os.sep)

# Traverse the path components to find the target directory
for i in range(len(path_components) - 1, -1, -1):
if path_components[i] == target_dir:
# Join the components to form the target directory path
return os.sep.join(path_components[:i + 1])
return None
Loading