From 7d7442590793dddc22e30e9b8661f68cc09fb2a2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Jan 2023 14:48:06 +0800 Subject: [PATCH] add extractors and validators for cameras --- .../hosts/max/plugins/create/create_camera.py | 26 +++++++ .../max/plugins/publish/extract_camera_abc.py | 69 +++++++++++++++++++ .../max/plugins/publish/extract_camera_fbx.py | 68 ++++++++++++++++++ .../plugins/publish/extract_max_scene_raw.py | 68 ++++++++++++++++++ .../max/plugins/publish/extract_pointcache.py | 2 +- .../publish/validate_camera_contents.py | 57 +++++++++++++++ 6 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/max/plugins/create/create_camera.py create mode 100644 openpype/hosts/max/plugins/publish/extract_camera_abc.py create mode 100644 openpype/hosts/max/plugins/publish/extract_camera_fbx.py create mode 100644 openpype/hosts/max/plugins/publish/extract_max_scene_raw.py create mode 100644 openpype/hosts/max/plugins/publish/validate_camera_contents.py diff --git a/openpype/hosts/max/plugins/create/create_camera.py b/openpype/hosts/max/plugins/create/create_camera.py new file mode 100644 index 0000000000..45f437b7ee --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_camera.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating camera.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance + + +class CreateCamera(plugin.MaxCreator): + identifier = "io.openpype.creators.max.camera" + label = "Camera" + family = "camera" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + sel_obj = list(rt.selection) + _ = super(CreateCamera, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + container = rt.getNodeByName(subset_name) + # TODO: Disable "Add to Containers?" Panel + # parent the selected cameras into the container + for obj in sel_obj: + obj.parent = container + # for additional work on the node: + # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py new file mode 100644 index 0000000000..83cf2c3a6e --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -0,0 +1,69 @@ +import os +import pyblish.api +from openpype.pipeline import publish +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractAlembicCamera(publish.Extractor): + """ + Extract Camera with AlembicExport + """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract Almebic Camera" + hosts = ["max"] + families = ["camera"] + optional = True + + def process(self, instance): + start = float(instance.data.get("frameStartHandle", 1)) + end = float(instance.data.get("frameEndHandle", 1)) + + container = instance.data["instance_node"] + + self.log.info("Extracting Camera ...") + + stagingdir = self.staging_dir(instance) + filename = "{name}.abc".format(**instance.data) + path = os.path.join(stagingdir, filename) + + # We run the render + self.log.info("Writing alembic '%s' to '%s'" % (filename, + stagingdir)) + + export_cmd = ( + f""" +AlembicExport.ArchiveType = #ogawa +AlembicExport.CoordinateSystem = #maya +AlembicExport.StartFrame = {start} +AlembicExport.EndFrame = {end} +AlembicExport.CustomAttributes = true + +exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport + + """) + + self.log.debug(f"Executing command: {export_cmd}") + + with maintained_selection(): + # select and export + rt.select(get_all_children(rt.getNodeByName(container))) + rt.execute(export_cmd) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'abc', + 'ext': 'abc', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + path)) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py new file mode 100644 index 0000000000..5a68edfbe8 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -0,0 +1,68 @@ +import os +import pyblish.api +from openpype.pipeline import publish +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractCameraFbx(publish.Extractor): + """ + Extract Camera with FbxExporter + """ + + order = pyblish.api.ExtractorOrder - 0.2 + label = "Extract Fbx Camera" + hosts = ["max"] + families = ["camera"] + + def process(self, instance): + container = instance.data["instance_node"] + + self.log.info("Extracting Camera ...") + stagingdir = self.staging_dir(instance) + filename = "{name}.fbx".format(**instance.data) + + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing fbx file '%s' to '%s'" % (filename, + filepath)) + + # Need to export: + # Animation = True + # Cameras = True + # AxisConversionMethod + fbx_export_cmd = ( + f""" + +FBXExporterSetParam "Animation" true +FBXExporterSetParam "Cameras" true +FBXExporterSetParam "AxisConversionMethod" "Animation" +FbxExporterSetParam "UpAxis" "Y" +FbxExporterSetParam "Preserveinstances" true + +exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP + + """) + + self.log.debug(f"Executing command: {fbx_export_cmd}") + + with maintained_selection(): + # select and export + rt.select(get_all_children(rt.getNodeByName(container))) + rt.execute(fbx_export_cmd) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + filepath)) \ No newline at end of file diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py new file mode 100644 index 0000000000..4524609a02 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -0,0 +1,68 @@ +import os +import pyblish.api +from openpype.pipeline import publish +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractMaxSceneRaw(publish.Extractor): + """ + Extract Raw Max Scene with SaveSelected + """ + + order = pyblish.api.ExtractorOrder - 0.2 + label = "Max Scene(Raw)" + hosts = ["max"] + families = ["camera"] + + def process(self, instance): + container = instance.data["instance_node"] + + # publish the raw scene for camera + self.log.info("Extracting Camera ...") + + stagingdir = self.staging_dir(instance) + filename = "{name}.max".format(**instance.data) + + max_path = os.path.join(stagingdir, filename) + self.log.info("Writing max file '%s' to '%s'" % (filename, + max_path)) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + #add extra blacklash for saveNodes in MaxScript + re_max_path = stagingdir + "\\\\" + filename + # saving max scene + raw_export_cmd = ( + f""" +sel = getCurrentSelection() +for s in sel do +( + select s + f="{re_max_path}" + print f + saveNodes selection f quiet:true +) + """) + + self.log.debug(f"Executing Maxscript command: {raw_export_cmd}") + + with maintained_selection(): + # need to figure out how to select the camera + rt.select(get_all_children(rt.getNodeByName(container))) + rt.execute(raw_export_cmd) + + self.log.info("Performing Extraction ...") + representation = { + 'name': 'max', + 'ext': 'max', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + max_path)) \ No newline at end of file diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 904c1656da..75d8a7972c 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -51,7 +51,7 @@ class ExtractAlembic(publish.Extractor): order = pyblish.api.ExtractorOrder label = "Extract Pointcache" hosts = ["max"] - families = ["pointcache", "camera"] + families = ["pointcache"] def process(self, instance): start = float(instance.data.get("frameStartHandle", 1)) diff --git a/openpype/hosts/max/plugins/publish/validate_camera_contents.py b/openpype/hosts/max/plugins/publish/validate_camera_contents.py new file mode 100644 index 0000000000..3990103e61 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_camera_contents.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt +from openpype.hosts.max.api import get_all_children + + +class ValidateCameraContent(pyblish.api.InstancePlugin): + """Validates Camera instance contents. + + A Camera instance may only hold a SINGLE camera's transform + """ + + order = pyblish.api.ValidatorOrder + families = ["camera"] + hosts = ["max"] + label = "Camera Contents" + camera_type = ["$Free_Camera", "$Target_Camera", + "$Physical_Camera", "$Target"] + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError("Camera instance must only include" + "camera (and camera target)") + + + def get_invalid(self, instance): + """ + Get invalid nodes if the instance is not camera + """ + invalid = list() + container = instance.data["instance_node"] + self.log.info("Validating look content for " + "'{}'".format(container)) + + con = rt.getNodeByName(container) + selection_list = self.list_children(con) + validation_msg = list() + for sel in selection_list: + # to avoid Attribute Error from pymxs wrapper + sel_tmp = str(sel) + for cam in self.camera_type: + if sel_tmp.startswith(cam): + validation_msg.append("Camera Found") + else: + validation_msg.append("Camera Not Found") + if "Camera Found" not in validation_msg: + invalid.append(sel) + # go through the camera type to see if there are same name + return invalid + + def list_children(self, node): + children = [] + for c in node.Children: + children.append(c) + return children