diff --git a/openpype/hosts/blender/blender_addon/addons/RenderPlayblast.py b/openpype/hosts/blender/blender_addon/addons/RenderPlayblast.py index 81aa53e438a..51003589f40 100644 --- a/openpype/hosts/blender/blender_addon/addons/RenderPlayblast.py +++ b/openpype/hosts/blender/blender_addon/addons/RenderPlayblast.py @@ -49,11 +49,13 @@ class VIEW3D_PT_render_playblast(bpy.types.Panel): bl_category = "Quad" use_camera_view: bpy.props.BoolProperty(name="Use Camera View") + use_transparent_bg: bpy.props.BoolProperty(name="Use Transparent Background") def draw(self, context): layout = self.layout col = layout.column() col.prop(context.scene, 'use_camera_view') + col.prop(context.scene, 'use_transparent_bg') col.operator('playblast.render', text="Render Playblast") col.operator('playblast.open', text="Open Last Playblast Folder") @@ -62,12 +64,12 @@ class OBJECT_OT_render_playblast(bpy.types.Operator): bl_idname = "playblast.render" bl_label = "Render Playblast" - def execute(self, context): scene = bpy.context.scene region = get_view_3D_region() use_camera_view = context.scene.use_camera_view + use_transparent_bg = context.scene.use_transparent_bg memorized_render_filepath = scene.render.filepath memorized_file_format = scene.render.image_settings.file_format @@ -80,12 +82,24 @@ def execute(self, context): render_filepath = get_render_filepath() Path(render_filepath).resolve().parent.mkdir(parents=True, exist_ok=True) + if use_transparent_bg: + # save current render parameters + memorized_engine = bpy.context.scene.render.engine + memorized_film_transparency = bpy.context.scene.render.film_transparent + memorized_image_settings = bpy.context.scene.render.image_settings.color_mode + + # set scene transparency for alpha in png + bpy.context.scene.render.engine = 'CYCLES' + bpy.context.scene.render.film_transparent = True + bpy.context.scene.render.image_settings.color_mode = 'RGBA' + for file_format, options in get_renders_types_and_options(): scene.render.image_settings.file_format = file_format scene.render.filepath = render_filepath.format(ext=options['extension']) container = options.get('container') - if container : scene.render.ffmpeg.format = container + if container: + scene.render.ffmpeg.format = container logging.info(f"{'Camera view' if use_camera_view else 'Viewport'} will be rendered at following path : {scene.render.filepath}") @@ -98,6 +112,13 @@ def execute(self, context): scene.render.use_file_extension = memorized_file_extension_use if region and use_camera_view: region.view_perspective = memorized_region + + if use_transparent_bg: + # reset to memorized parameters for render + bpy.context.scene.render.engine = memorized_engine + bpy.context.scene.render.film_transparent = memorized_film_transparency + bpy.context.scene.render.image_settings.color_mode = memorized_image_settings + return {'FINISHED'} @@ -113,7 +134,7 @@ def execute(self, context): self.report({'ERROR'}, "File '{}' not found".format(latest_playblast_filepath)) return {'CANCELLED'} - subprocess.Popen(['start', str(latest_playblast_filepath.resolve())], shell=True) + subprocess.Popen('explorer "' + str(latest_playblast_filepath.resolve()) + '"', shell=True) return {'FINISHED'} @@ -124,6 +145,7 @@ def register(): bpy.utils.register_class(OBJECT_OT_open_playblast_folder) bpy.types.Scene.use_camera_view = bpy.props.BoolProperty(default=False) + bpy.types.Scene.use_transparent_bg = bpy.props.BoolProperty(default=True) def unregister(): @@ -132,3 +154,4 @@ def unregister(): bpy.utils.unregister_class(OBJECT_OT_open_playblast_folder) del bpy.types.Scene.use_camera_view + del bpy.types.Scene.use_transparent_bg diff --git a/openpype/hosts/blender/blender_addon/addons/SetRenderPaths.py b/openpype/hosts/blender/blender_addon/addons/SetRenderPaths.py new file mode 100644 index 00000000000..96de1115b45 --- /dev/null +++ b/openpype/hosts/blender/blender_addon/addons/SetRenderPaths.py @@ -0,0 +1,119 @@ +import bpy +import logging +import os +import subprocess +from enum import Enum +from pathlib import Path + +from libs import paths, templates + + +logging.basicConfig(level=logging.INFO) +log = logging.getLogger(__name__) + + +bl_info = { + "name": "Render Playblast", + "description": "Render sequences of images + video, with OpenGL, from viewport or camera view", + "author": "Quad", + "version": (1, 1), + "blender": (2, 80, 0), + "category": "Render", + "location": "View 3D > UI", +} + + +class NodesNames(Enum): + RENDER_LAYERS = 'R_LAYERS' + OUTPUT_FILE = 'OUTPUT_FILE' + + +def get_render_folderpath(): + return templates.get_render_node_output_path() + + +def set_global_output_path(create_directory=False): + bpy.context.scene.render.filepath = templates.get_render_global_output_path() + log.info(f"Global output path has been set to '{bpy.context.scene.render.filepath}'") + if create_directory: + Path(bpy.context.scene.render.filepath).mkdir(parents=True, exist_ok=True) + log.info(f"Folder at path '{bpy.context.scene.render.filepath}' has been created.") + + +def set_render_nodes_output_path(): + for output_node in [node for node in bpy.context.scene.node_tree.nodes if node.type == NodesNames.OUTPUT_FILE.value]: + render_node = _browse_render_nodes(output_node.inputs) + render_layer_name = render_node.layer + render_node_output_path = templates.get_render_node_output_path(render_layer_name=render_layer_name) + + output_node.base_path = render_node_output_path + log.info(f"File output path has been set to '{output_node.base_path}'.") + + +def _browse_render_nodes(nodes_inputs): + node_links = list() + for nodes_input in nodes_inputs: + node_links.extend(nodes_input.links) + + for node_link in node_links: + target_node = node_link.from_node + if target_node.type == NodesNames.RENDER_LAYERS.value: + return target_node + + target_node = _browse_render_nodes(target_node.inputs) + if target_node: + return target_node + + +class VIEW3D_PT_set_render_paths(bpy.types.Panel): + bl_label = "Set Render Paths" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = "Quad" + + def draw(self, context): + layout = self.layout + col = layout.column() + col.operator('setpaths.render', text="Set Render Paths") + col.operator('setpaths.open', text="Open Last Render Folder") + + +class OBJECT_OT_set_paths(bpy.types.Operator): + bl_idname = "setpaths.render" + bl_label = "Set Render Path" + + def execute(self, context): + set_global_output_path(create_directory=True) + set_render_nodes_output_path() + + return {'FINISHED'} + + +class OBJECT_OT_open_render_folder(bpy.types.Operator): + bl_idname = "setpaths.open" + bl_label = "Open Last Render Folder" + + def execute(self, context): + latest_render_folderpath = paths.get_version_folder_fullpath( + get_render_folderpath() + ) + + if not latest_render_folderpath or not latest_render_folderpath.exists(): + self.report({'ERROR'}, "File '{}' not found".format(latest_render_folderpath)) + return {'CANCELLED'} + + subprocess.Popen('explorer "' + str(latest_render_folderpath.resolve()) + '"', shell=True) + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(VIEW3D_PT_set_render_paths) + bpy.utils.register_class(OBJECT_OT_set_paths) + bpy.utils.register_class(OBJECT_OT_open_render_folder) + + + +def unregister(): + bpy.utils.unregister_class(VIEW3D_PT_set_render_paths) + bpy.utils.unregister_class(OBJECT_OT_set_paths) + bpy.utils.unregister_class(OBJECT_OT_open_render_folder) diff --git a/openpype/hosts/blender/blender_addon/startup/custom_scripts/install_custom_addons.py b/openpype/hosts/blender/blender_addon/startup/custom_scripts/install_custom_addons.py index 2fcc0673802..a8ca65a3bf2 100644 --- a/openpype/hosts/blender/blender_addon/startup/custom_scripts/install_custom_addons.py +++ b/openpype/hosts/blender/blender_addon/startup/custom_scripts/install_custom_addons.py @@ -5,11 +5,15 @@ import logging import subprocess from pathlib import Path +from openpype.settings import get_system_settings def execute(): blender_addons_folder_path = get_addons_folder_path() - install_deadline_addon(blender_addons_folder_path) + system_settings = get_system_settings() + modules_settings = system_settings["modules"] + if modules_settings["deadline"].get("enabled", False): + install_deadline_addon(blender_addons_folder_path) enable_user_addons(blender_addons_folder_path) bpy.ops.wm.save_userpref() diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index 7d00259e025..10bcbcaf817 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -31,7 +31,7 @@ def execute(self): def inner_execute(self): # Get blender's python directory - version_regex = re.compile(r"^[2-4]\.[0-9]+$") + version_regex = re.compile(r"^([2-4])\.[0-9]+$") platform = system().lower() executable = self.launch_context.executable.executable_path @@ -42,7 +42,8 @@ def inner_execute(self): if os.path.basename(executable).lower() != expected_executable: self.log.info(( f"Executable does not lead to {expected_executable} file." - "Can't determine blender's python to check/install PySide2." + "Can't determine blender's python to check/install" + " Qt binding." )) return @@ -73,6 +74,15 @@ def inner_execute(self): return version_subfolder = version_subfolders[0] + before_blender_4 = False + if int(version_regex.match(version_subfolder).group(1)) < 4: + before_blender_4 = True + # Blender 4 has Python 3.11 which does not support 'PySide2' + # QUESTION could we always install PySide6? + qt_binding = "PySide2" if before_blender_4 else "PySide6" + # Use PySide6 6.6.3 because 6.7.0 had a bug + # - 'QTextEdit' can't be added to 'QBoxLayout' + qt_binding_version = None if before_blender_4 else "6.6.3" python_dir = os.path.join(versions_dir, version_subfolder, "python") python_lib = os.path.join(python_dir, "lib") @@ -87,12 +97,7 @@ def inner_execute(self): # Change PYTHONPATH to contain blender's packages as first python_paths = [ - # Python lib has been removed from PYTHONPATH because it was - # causing Blender to use the wrong Python version in specific - # cases (e.g. with Deadline add-on) instead of the embedded one. - # See https://github.com/quadproduction/issues/issues/214#issuecomment-1833613833 - # for more informations. - #python_lib, + python_lib, os.path.join(python_lib, "site-packages"), ] python_path = self.launch_context.env.get("PYTHONPATH") or "" @@ -121,22 +126,41 @@ def inner_execute(self): return # Check if PySide2 is installed and skip if yes - if self.is_pyside_installed(python_executable): + if self.is_pyside_installed(python_executable, qt_binding): self.log.debug("Blender has already installed PySide2.") return # Install PySide2 in blender's python if platform == "windows": - result = self.install_pyside_windows(python_executable) + result = self.install_pyside_windows( + python_executable, + qt_binding, + qt_binding_version, + before_blender_4, + ) else: - result = self.install_pyside(python_executable) + result = self.install_pyside( + python_executable, + qt_binding, + qt_binding_version, + ) if result: - self.log.info("Successfully installed PySide2 module to blender.") + self.log.info( + f"Successfully installed {qt_binding} module to blender." + ) else: - self.log.warning("Failed to install PySide2 module to blender.") + self.log.warning( + f"Failed to install {qt_binding} module to blender." + ) - def install_pyside_windows(self, python_executable): + def install_pyside_windows( + self, + python_executable, + qt_binding, + qt_binding_version, + before_blender_4, + ): """Install PySide2 python module to blender's python. Installation requires administration rights that's why it is required @@ -144,7 +168,6 @@ def install_pyside_windows(self, python_executable): administration rights. """ try: - import win32api import win32con import win32process import win32event @@ -155,12 +178,37 @@ def install_pyside_windows(self, python_executable): self.log.warning("Couldn't import \"pywin32\" modules") return + if qt_binding_version: + qt_binding = f"{qt_binding}=={qt_binding_version}" + try: # Parameters # - use "-m pip" as module pip to install PySide2 and argument # "--ignore-installed" is to force install module to blender's # site-packages and make sure it is binary compatible - parameters = "-m pip install --ignore-installed PySide2" + fake_exe = "fake.exe" + site_packages_prefix = os.path.dirname( + os.path.dirname(python_executable) + ) + args = [ + fake_exe, + "-m", + "pip", + "install", + "--ignore-installed", + qt_binding, + ] + if not before_blender_4: + # Define prefix for site package + # Python in blender 4.x is installing packages in AppData and + # not in blender's directory. + args.extend(["--prefix", site_packages_prefix]) + + parameters = ( + subprocess.list2cmdline(args) + .lstrip(fake_exe) + .lstrip(" ") + ) # Execute command and ask for administrator's rights process_info = ShellExecuteEx( @@ -178,20 +226,29 @@ def install_pyside_windows(self, python_executable): except pywintypes.error: pass - def install_pyside(self, python_executable): - """Install PySide2 python module to blender's python.""" + def install_pyside( + self, + python_executable, + qt_binding, + qt_binding_version, + ): + """Install Qt binding python module to blender's python.""" + if qt_binding_version: + qt_binding = f"{qt_binding}=={qt_binding_version}" try: # Parameters - # - use "-m pip" as module pip to install PySide2 and argument + # - use "-m pip" as module pip to install qt binding and argument # "--ignore-installed" is to force install module to blender's # site-packages and make sure it is binary compatible + # TODO find out if blender 4.x on linux/darwin does install + # qt binding to correct place. args = [ python_executable, "-m", "pip", "install", "--ignore-installed", - "PySide2", + qt_binding, ] process = subprocess.Popen( args, stdout=subprocess.PIPE, universal_newlines=True @@ -208,13 +265,15 @@ def install_pyside(self, python_executable): except subprocess.SubprocessError: pass - def is_pyside_installed(self, python_executable): + def is_pyside_installed(self, python_executable, qt_binding): """Check if PySide2 module is in blender's pip list. Check that PySide2 is installed directly in blender's site-packages. It is possible that it is installed in user's site-packages but that may be incompatible with blender's python. """ + + qt_binding_low = qt_binding.lower() # Get pip list from blender's python executable args = [python_executable, "-m", "pip", "list"] process = subprocess.Popen(args, stdout=subprocess.PIPE) @@ -231,6 +290,6 @@ def is_pyside_installed(self, python_executable): if not line: continue package_name = line[0:package_len].strip() - if package_name.lower() == "pyside2": + if package_name.lower() == qt_binding_low: return True return False diff --git a/openpype/hosts/blender/plugins/load/load_camera_abc.py b/openpype/hosts/blender/plugins/load/load_camera_abc.py index 05d3fb764dd..60906841228 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_abc.py +++ b/openpype/hosts/blender/plugins/load/load_camera_abc.py @@ -3,7 +3,7 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional - +import json import bpy from openpype.pipeline import ( @@ -106,6 +106,19 @@ def process_asset( nodes = list(asset_group.children) for obj in nodes: + if obj.type == 'CAMERA': + camera = obj.data + jsonpath_camera_data = (Path(str(libpath)).with_suffix('.json')) + camera_data = {} + if Path(jsonpath_camera_data).exists(): + with open(jsonpath_camera_data) as my_file: + camera_data = json.loads(my_file.read()) + + if camera_data: + for frame in camera_data["focal_data"].keys(): + camera.lens = camera_data["focal_data"][frame] + camera.keyframe_insert(data_path="lens", frame=int(frame)) + objects.append(obj) nodes.extend(list(obj.children)) diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index 036be7bf3c4..f9811f5b540 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -1,5 +1,5 @@ import os - +import json import bpy from openpype.pipeline import publish @@ -20,6 +20,8 @@ def process(self, instance): stagingdir = self.staging_dir(instance) filename = f"{instance.name}.abc" filepath = os.path.join(stagingdir, filename) + jsonname = f"{instance.name}.json" + json_path = os.path.join(stagingdir, jsonname) # Perform extraction self.log.info("Performing extraction..") @@ -35,11 +37,40 @@ def process(self, instance): # Need to cast to list because children is a tuple selected = list(asset_group.children) + + if not selected: + self.log.error("Extraction failed: No child objects found in the asset group.") + return + active = selected[0] + camera = None for obj in selected: + if obj.type == "CAMERA": + camera = (obj.data) obj.select_set(True) + # Create focal value dict throught time for blender + if camera: + camera_data_dict = {"focal_data": {}} + # save current frame to reset it after the dict creation + currentframe = bpy.context.scene.frame_current + + for frame in range (bpy.context.scene.frame_start, (bpy.context.scene.frame_end+1)): + bpy.context.scene.frame_set(frame) + camera_data_dict["focal_data"][frame] = camera.lens + + # reset old current frame + bpy.context.scene.frame_set(currentframe) + + # Performe json extraction + # Serializing json + json_object = json.dumps(camera_data_dict, indent=4) + + # Writing to json + with open(json_path, "w") as outfile: + outfile.write(json_object) + context = plugin.create_blender_context( active=active, selected=selected) @@ -64,5 +95,13 @@ def process(self, instance): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + json_representation = { + 'name': 'jsonCam', + 'ext': 'json', + 'files': jsonname, + "stagingDir": stagingdir, + } + instance.data["representations"].append(json_representation) + + self.log.info("Extracted instance '%s' to: %s\nExtracted instance '%s' to: %s", + instance.name, representation, jsonname, json_representation) diff --git a/openpype/hosts/blender/plugins/publish/validate_camera_contents.py b/openpype/hosts/blender/plugins/publish/validate_camera_contents.py new file mode 100644 index 00000000000..0c849d07289 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/validate_camera_contents.py @@ -0,0 +1,40 @@ +import pyblish.api +import bpy + +import openpype.hosts.blender.api.action +from openpype.pipeline.publish import ( + PublishValidationError, ValidateContentsOrder) + + +class ValidateCameraContents(pyblish.api.InstancePlugin): + """Validates Camera instance contents. + + A Camera instance may only hold a SINGLE camera, nothing else. + """ + + order = ValidateContentsOrder + families = ['camera'] + hosts = ['blender'] + label = 'Validate Camera Contents' + actions = [openpype.hosts.blender.api.action.SelectInvalidAction] + + @staticmethod + def get_invalid(instance): + + # get cameras + cameras = [obj for obj in instance if obj.type == "CAMERA"] + + invalid = [] + if len(cameras) != 1: + invalid.extend(cameras) + + invalid = list(set(invalid)) + return invalid + + def process(self, instance): + """Process all the nodes in the instance""" + + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Invalid camera contents, Camera instance must have a single camera: " + "Found {0}: {1}".format(len(invalid), invalid)) diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py index 1ea4ce2d212..d6f4853db90 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py @@ -1,5 +1,5 @@ import os - +import json from maya import cmds from openpype.pipeline import publish @@ -35,12 +35,33 @@ def process(self, instance): # validate required settings assert isinstance(step, float), "Step must be a float value" + + if not cameras: + self.log.error("No camera found") + return + camera = cameras[0] + # create focal value dict throught time for blender + camera_data_dict = {"focal_data": {}} + + for frame in range (start, (end+1)): + camera_data_dict["focal_data"][frame] = cmds.getAttr('{0}.focalLength'.format(camera), time=frame) + # Define extract output file path dir_path = self.staging_dir(instance) filename = "{0}.abc".format(instance.name) + jsonname = "{0}.json".format(instance.name) path = os.path.join(dir_path, filename) + json_path = os.path.join(dir_path, jsonname) + + # Performe json extraction + # Serializing json + json_object = json.dumps(camera_data_dict, indent=4) + + # Writing to json + with open(json_path, "w") as outfile: + outfile.write(json_object) # Perform alembic extraction member_shapes = cmds.ls( @@ -113,5 +134,13 @@ def process(self, instance): } instance.data["representations"].append(representation) - self.log.debug("Extracted instance '{0}' to: {1}".format( - instance.name, path)) + json_representation = { + 'name': 'jsonCam', + 'ext': 'json', + 'files': jsonname, + "stagingDir": dir_path, + } + instance.data["representations"].append(json_representation) + + self.log.debug("Extracted instance '{0}' to: {1}\nExtracted instance '{2}' to: {3}".format( + instance.name, path, jsonname, json_path)) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 9b83619fee2..5a458869275 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2599,13 +2599,18 @@ def make_format_string(self, **kwargs): def set_context_settings(self): os.environ["OP_NUKE_SKIP_SAVE_EVENT"] = "True" # replace reset resolution from avalon core to pype's - self.reset_resolution() + if self._get_set_resolution_startup(): + self.reset_resolution() # replace reset resolution from avalon core to pype's self.reset_frame_range_handles() # add colorspace menu item self.set_colorspace() del os.environ["OP_NUKE_SKIP_SAVE_EVENT"] + def _get_set_resolution_startup(self): + custom_settings = self.get_custom_settings() + return custom_settings.get("hosts", {}).get("nuke", {}).get("set_resolution_startup", True) + def set_custom_resolution(self): custom_settings = self.get_custom_settings() if not custom_settings: diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 421fce3d97c..2dd6d501d24 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -180,7 +180,8 @@ def add_nuke_callbacks(): nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) # set apply all custom settings on script load and save - nuke.addOnScriptLoad(workfile_settings.set_custom_resolution) + if workfile_settings._get_set_resolution_startup(): + nuke.addOnScriptLoad(workfile_settings.set_custom_resolution) # Emit events nuke.addOnCreate(_on_scene_open, nodeClass="Root") diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 7c25c5e9200..3c5aa639700 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -106,7 +106,11 @@ def update_placeholder(self, placeholder_item, placeholder_data): def _parse_placeholder_node_data(self, node): placeholder_data = {} - for key in self.get_placeholder_keys(): + # Allow the transition between legacy Creator and New one + placeholder_keys = self.get_placeholder_keys() + # TODO: Delete this once all the templates will used 'Create' instead of 'Creator' plugins + placeholder_keys.add("creator") + for key in placeholder_keys: knob = node.knob(key) value = None if knob is not None: @@ -560,6 +564,11 @@ def _parse_placeholder_node_data(self, node): nb_children = int(node_knobs["nb_children"].getValue()) placeholder_data["nb_children"] = nb_children + # TODO: Delete this once all the templates will used 'Create' instead of 'Creator' plugins + if "creator" in placeholder_data: + placeholder_data["create"] = placeholder_data["creator"] + del placeholder_data['creator'] + siblings = [] if "siblings" in node_knobs: siblings = node_knobs["siblings"].values() diff --git a/openpype/hosts/nuke/plugins/publish/extract_camera.py b/openpype/hosts/nuke/plugins/publish/extract_camera.py index 33df6258aef..44e24f4e23f 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_camera.py +++ b/openpype/hosts/nuke/plugins/publish/extract_camera.py @@ -1,5 +1,6 @@ import os import math +import json from pprint import pformat import nuke @@ -54,7 +55,9 @@ def process(self, instance): # create file name and path filename = subset + ".{}".format(extension) + jsonname = "{0}.json".format(instance.name) file_path = os.path.join(staging_dir, filename).replace("\\", "/") + json_path = os.path.join(staging_dir, jsonname).replace("\\", "/") with maintained_selection(): # bake camera with axeses onto word coordinate XYZ @@ -62,6 +65,20 @@ def process(self, instance): camera_node, output_range) rm_nodes.append(rm_n) + # Create focal value dict throught time for blender + camera_data_dict = {"focal_data": {}} + + for frame in range (first_frame, (last_frame+1)): + camera_data_dict["focal_data"][frame] = camera_node.knob('focal').getValue(time=frame) + + # Performe json extraction + # Serializing json + json_object = json.dumps(camera_data_dict, indent=4) + + # Writing to json + with open(json_path, "w") as outfile: + outfile.write(json_object) + # create scene node rm_n = nuke.createNode("Scene") rm_nodes.append(rm_n) @@ -100,6 +117,15 @@ def process(self, instance): } instance.data["representations"].append(representation) + json_representation = { + 'name': 'jsonCam', + 'ext': 'json', + 'files': jsonname, + "stagingDir": staging_dir, + } + instance.data["representations"].append(json_representation) + + instance.data.update({ "path": file_path, "outputDir": staging_dir, @@ -112,8 +138,8 @@ def process(self, instance): "frameEndHandle": last_frame, }) - self.log.info("Extracted instance '{0}' to: {1}".format( - instance.name, file_path)) + self.log.info("Extracted instance '{0}' to: {1}\nExtracted instance '{2}' to: {3}".format( + instance.name, file_path, jsonname, json_path)) def bakeCameraWithAxeses(camera_node, output_range): diff --git a/openpype/hosts/photoshop/api/extension/client/client.js b/openpype/hosts/photoshop/api/extension/client/client.js index 80bfa44abf9..9ff58ea6ead 100644 --- a/openpype/hosts/photoshop/api/extension/client/client.js +++ b/openpype/hosts/photoshop/api/extension/client/client.js @@ -155,6 +155,28 @@ }); }); + RPC.addRoute('Photoshop.get_activeDocument_format_resolution', function (data) { + log.warn('Server called client route ' + + '"get_activeDocument_format_resolution":', data); + return runEvalScript("getActiveDocumentFormatResolution()") + .then(function(result){ + log.warn("Get document resolution "); + return result; + }); + }); + + RPC.addRoute('Photoshop.crop_document_to_coordinate', function (data) { + log.warn('Server called client route "crop_document_to_coordinate":', data); + return runEvalScript("cropDocumentToCoordinate('" + data.x1 +"', " + + "'"+ data.y1 +"',"+ + "'"+ data.x2 +"'," + + "'"+ data.y2 +"'," +")") + .then(function(result){ + log.warn("Crop to given coordinates"); + return result; + }); + }); + RPC.addRoute('Photoshop.save', function (data) { log.warn('Server called client route "save":', data); diff --git a/openpype/hosts/photoshop/api/extension/host/index.jsx b/openpype/hosts/photoshop/api/extension/host/index.jsx index 968f329e44a..4bfbab83120 100644 --- a/openpype/hosts/photoshop/api/extension/host/index.jsx +++ b/openpype/hosts/photoshop/api/extension/host/index.jsx @@ -263,6 +263,42 @@ function getActiveDocumentFullName(){ }; } +function getActiveDocumentFormatResolution(){ + /** + * Returns a dict with the resolution of the current doc + * */ + if (documents.length == 0){ + return {}; + } + + try { + savedUnit = app.preferences.rulerUnits; + app.preferences.rulerUnits = Units.PIXELS; + resDict = {"width" :app.activeDocument.width.value, + "height": app.activeDocument.height.value} + app.preferences.rulerUnits = savedUnit; + return(JSON.stringify(resDict)); + }catch(e){ + // No doc is active. + return {} + }; +} + +function cropDocumentToCoordinate(x1, y1, x2, y2) { + /* + * Crop all the document and the layers in to the define coordinate + * x1,y1---------------------- + * | | + * | | + * | | + * | | + * ----------------------x2,y2 + */ + bounds = [UnitValue(x1, "px"), UnitValue(y1, "px"), UnitValue(x2, "px"), UnitValue(y2, "px")]; + app.activeDocument.crop(bounds); +} + + function imprint(payload){ /** * Sets headline content of current document with metadata. Stores diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py index 8eca3910e7e..ff5cb745e24 100644 --- a/openpype/hosts/photoshop/api/ws_stub.py +++ b/openpype/hosts/photoshop/api/ws_stub.py @@ -360,6 +360,37 @@ def select_layers(self, layers): ) ) + def get_activeDocument_format_resolution(self): + """Return the width and height in pixel of the active doc + + Returns(dict): + {"width": int, "height": int} or empty dict {} if document size isn't valid + """ + res = self.websocketserver.call( + self.client.call('Photoshop.get_activeDocument_format_resolution') + ) + if res: + return json.loads(res) + return {} + + def crop_document_to_coordinate(self, x1, y1, x2, y2): + """Crop the active document to the given coordinates + x1,y1---------------------- + | | + | | + | | + | | + ----------------------x2,y2 + Returns: None + """ + self.websocketserver.call( + self.client.call('Photoshop.crop_document_to_coordinate', + x1=x1, + y1=y1, + x2=x2, + y2=y2) + ) + def get_active_document_full_name(self): """Returns full name with path of active document via ws call diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_layers_name_uniqueness.py b/openpype/hosts/tvpaint/plugins/publish/validate_layers_name_uniqueness.py new file mode 100644 index 00000000000..56119487fec --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/validate_layers_name_uniqueness.py @@ -0,0 +1,68 @@ +import pyblish.api +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin, +) +from openpype.hosts.tvpaint.api.pipeline import ( + list_instances, + write_instances, +) +from openpype.hosts.tvpaint.api.lib import execute_george + + +class ValidateLayersNameUniquenessTvPaintSelect(pyblish.api.Action): + """Select the layers in fault. + """ + + label = "Select Layers" + icon = "briefcase" + on = "failed" + + def process(self, context, plugin): + """Select the layers that haven't a unique name""" + + for layer in context.data['transientData'][ValidateLayersNameUniquenessTvPaint.__name__]: + self.log.debug(execute_george(f'tv_layerselection {layer["layer_id"]} "true"')) + return True + + +class ValidateLayersNameUniquenessTvPaint( + OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin +): + """Validate if all the layers have unique names""" + + label = "Validate Layers Name Uniqueness" + order = pyblish.api.ValidatorOrder + hosts = ["tvpaint"] + actions = [ValidateLayersNameUniquenessTvPaintSelect] + optional = True + active = True + + def process(self, context): + + return_list = list() + msg = "" + for instance in context: + layers = instance.data.get("layers") + + if not layers: + continue + + layer_list = [layer["name"] for layer in layers] + duplicates = set() + + for layer in layers: + if layer["name"] in duplicates or layer_list.count(layer["name"]) == 1: + continue + + return_list.append(layer) + duplicates.add(layer["name"]) + msg = "{}\nThe name {} is not unique.".format(msg, layer["name"]) + + if return_list: + if not context.data.get('transientData'): + context.data['transientData'] = dict() + + context.data['transientData'][self.__class__.__name__] = return_list + raise PublishXmlValidationError(self, msg) diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index bedf19562de..1b418c299b6 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -175,28 +175,26 @@ def _import_module_from_dirpath_py3(dirpath, module_name, dst_module_name): return sys.modules[full_module_name] import importlib.util - from importlib._bootstrap_external import PathFinder + from importlib.machinery import PathFinder - # Find loader for passed path and name - loader = PathFinder.find_module(full_module_name, [dirpath]) + # Find Spec for passed path and name + spec = PathFinder.find_spec(full_module_name, [dirpath]) - # Load specs of module - spec = importlib.util.spec_from_loader( - full_module_name, loader, origin=dirpath - ) + # Safety Check + if spec is None: + return - # Create module based on specs + # Create Module Based on spec module = importlib.util.module_from_spec(spec) - # Store module to destination module and `sys.modules` - # WARNING this mus be done before module execution if dst_module is not None: setattr(dst_module, module_name, module) + # Add our custom module to sys.modules sys.modules[full_module_name] = module - # Execute module import - loader.exec_module(module) + # Load the module + spec.loader.exec_module(module) return module diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 3e4247eccd0..8735d71f85d 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1754,9 +1754,9 @@ def get_create_plugin_options(self, options=None): attribute_definitions.UISeparatorDef(), attribute_definitions.EnumDef( - "creator", - label="Creator", - default=options.get("creator"), + "create", + label="Create", + default=options.get("create"), items=creator_items, tooltip=( "Creator" @@ -1804,9 +1804,8 @@ def populate_create_placeholder(self, placeholder, pre_create_data=None): """ legacy_create = self.builder.use_legacy_creators - creator_name = placeholder.data["creator"] + creator_name = placeholder.data["create"] create_variant = placeholder.data["create_variant"] - creator_plugin = self.builder.get_creators_by_name()[creator_name] # create subset name @@ -1956,7 +1955,7 @@ def get_errors(self): "Failed to create {} instance using Creator {}" ).format( len(self._failed_created_publish_instances), - self.data["creator"] + self.data["create"] ) return [message] diff --git a/openpype/version.py b/openpype/version.py index 8257e730578..0652d97ceb6 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.9-quad-1.13.1" +__version__ = "3.16.9-quad-1.14.0"