From 5f0ce4f88dd09caee68a04e94db672620c6d0416 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Sep 2023 18:38:40 +0800 Subject: [PATCH] add tycache family --- .../max/plugins/create/create_tycache.py | 34 ++++ .../hosts/max/plugins/load/load_tycache.py | 67 +++++++ .../max/plugins/publish/extract_tycache.py | 183 ++++++++++++++++++ .../plugins/publish/validate_pointcloud.py | 59 +----- .../plugins/publish/validate_tyflow_data.py | 74 +++++++ 5 files changed, 361 insertions(+), 56 deletions(-) create mode 100644 openpype/hosts/max/plugins/create/create_tycache.py create mode 100644 openpype/hosts/max/plugins/load/load_tycache.py create mode 100644 openpype/hosts/max/plugins/publish/extract_tycache.py create mode 100644 openpype/hosts/max/plugins/publish/validate_tyflow_data.py diff --git a/openpype/hosts/max/plugins/create/create_tycache.py b/openpype/hosts/max/plugins/create/create_tycache.py new file mode 100644 index 0000000000..0fe0f32eed --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_tycache.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating TyCache.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import EnumDef + + +class CreateTyCache(plugin.MaxCreator): + """Creator plugin for TyCache.""" + identifier = "io.openpype.creators.max.tycache" + label = "TyCache" + family = "tycache" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + instance_data = pre_create_data.get("tycache_type") + super(CreateTyCache, self).create( + subset_name, + instance_data, + pre_create_data) + + def get_pre_create_attr_defs(self): + attrs = super(CreateTyCache, self).get_pre_create_attr_defs() + + tycache_format_enum = ["tycache", "tycachespline"] + + + return attrs + [ + + EnumDef("tycache_type", + tycache_format_enum, + default="tycache", + label="TyCache Type") + ] diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py new file mode 100644 index 0000000000..657e743087 --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -0,0 +1,67 @@ +import os + +from openpype.hosts.max.api import lib, maintained_selection +from openpype.hosts.max.api.lib import ( + unique_namespace, + +) +from openpype.hosts.max.api.pipeline import ( + containerise, + get_previous_loaded_object, + update_custom_attribute_data +) +from openpype.pipeline import get_representation_path, load + + +class PointCloudLoader(load.LoaderPlugin): + """Point Cloud Loader.""" + + families = ["tycache"] + representations = ["tyc"] + order = -8 + icon = "code-fork" + color = "green" + + def load(self, context, name=None, namespace=None, data=None): + """Load tyCache""" + from pymxs import runtime as rt + filepath = os.path.normpath(self.filepath_from_context(context)) + obj = rt.tyCache() + obj.filename = filepath + + namespace = unique_namespace( + name + "_", + suffix="_", + ) + obj.name = f"{namespace}:{obj.name}" + + return containerise( + name, [obj], context, + namespace, loader=self.__class__.__name__) + + def update(self, container, representation): + """update the container""" + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.GetNodeByName(container["instance_node"]) + node_list = get_previous_loaded_object(node) + update_custom_attribute_data( + node, node_list) + with maintained_selection(): + rt.Select(node_list) + for prt in rt.Selection: + prt.filename = path + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + """remove the container""" + from pymxs import runtime as rt + + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py new file mode 100644 index 0000000000..8fcdd6d65c --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -0,0 +1,183 @@ +import os + +import pyblish.api +from pymxs import runtime as rt + +from openpype.hosts.max.api import maintained_selection +from openpype.pipeline import publish + + +class ExtractTyCache(publish.Extractor): + """ + Extract tycache format with tyFlow operators. + Notes: + - TyCache only works for TyFlow Pro Plugin. + + Args: + self.export_particle(): sets up all job arguments for attributes + to be exported in MAXscript + + self.get_operators(): get the export_particle operator + + self.get_files(): get the files with tyFlow naming convention + before publishing + """ + + order = pyblish.api.ExtractorOrder - 0.2 + label = "Extract TyCache" + hosts = ["max"] + families = ["tycache"] + + def process(self, instance): + # TODO: let user decide the param + start = int(instance.context.data.get("frameStart")) + end = int(instance.context.data.get("frameEnd")) + self.log.info("Extracting Tycache...") + + stagingdir = self.staging_dir(instance) + filename = "{name}.tyc".format(**instance.data) + path = os.path.join(stagingdir, filename) + filenames = self.get_file(path, start, end) + with maintained_selection(): + job_args = None + if instance.data["tycache_type"] == "tycache": + job_args = self.export_particle( + instance.data["members"], + start, end, path) + elif instance.data["tycache_type"] == "tycachespline": + job_args = self.export_particle( + instance.data["members"], + start, end, path, + tycache_spline_enabled=True) + + for job in job_args: + rt.Execute(job) + + representation = { + 'name': 'tyc', + 'ext': 'tyc', + 'files': filenames if len(filenames) > 1 else filenames[0], + "stagingDir": stagingdir + } + instance.data["representations"].append(representation) + self.log.info(f"Extracted instance '{instance.name}' to: {path}") + + def get_file(self, filepath, start_frame, end_frame): + filenames = [] + filename = os.path.basename(filepath) + orig_name, _ = os.path.splitext(filename) + for frame in range(int(start_frame), int(end_frame) + 1): + actual_name = "{}_{:05}".format(orig_name, frame) + actual_filename = filepath.replace(orig_name, actual_name) + filenames.append(os.path.basename(actual_filename)) + + return filenames + + def export_particle(self, members, start, end, + filepath, tycache_spline_enabled=False): + """Sets up all job arguments for attributes. + + Those attributes are to be exported in MAX Script. + + Args: + members (list): Member nodes of the instance. + start (int): Start frame. + end (int): End frame. + filepath (str): Output path of the TyCache file. + + Returns: + list of arguments for MAX Script. + + """ + job_args = [] + opt_list = self.get_operators(members) + for operator in opt_list: + if tycache_spline_enabled: + export_mode = f'{operator}.exportMode=3' + has_tyc_spline = f'{operator}.tycacheSplines=true' + job_args.extend([export_mode, has_tyc_spline]) + else: + export_mode = f'{operator}.exportMode=2' + job_args.append(export_mode) + start_frame = f"{operator}.frameStart={start}" + job_args.append(start_frame) + end_frame = f"{operator}.frameEnd={end}" + job_args.append(end_frame) + filepath = filepath.replace("\\", "/") + tycache_filename = f'{operator}.tyCacheFilename="{filepath}"' + job_args.append(tycache_filename) + additional_args = self.get_custom_attr(operator) + job_args.extend(iter(additional_args)) + tycache_export = f"{operator}.exportTyCache()" + job_args.append(tycache_export) + + return job_args + + @staticmethod + def get_operators(members): + """Get Export Particles Operator. + + Args: + members (list): Instance members. + + Returns: + list of particle operators + + """ + opt_list = [] + for member in members: + obj = member.baseobject + # TODO: to see if it can be used maxscript instead + anim_names = rt.GetSubAnimNames(obj) + for anim_name in anim_names: + sub_anim = rt.GetSubAnim(obj, anim_name) + boolean = rt.IsProperty(sub_anim, "Export_Particles") + if boolean: + event_name = sub_anim.Name + opt = f"${member.Name}.{event_name}.export_particles" + opt_list.append(opt) + + return opt_list + +""" +.exportMode : integer +.frameStart : integer +.frameEnd : integer + + .tycacheChanAge : boolean + .tycacheChanGroups : boolean + .tycacheChanPos : boolean + .tycacheChanRot : boolean + .tycacheChanScale : boolean + .tycacheChanVel : boolean + .tycacheChanSpin : boolean + .tycacheChanShape : boolean + .tycacheChanMatID : boolean + .tycacheChanMapping : boolean + .tycacheChanMaterials : boolean + .tycacheChanCustomFloat : boolean + .tycacheChanCustomVector : boolean + .tycacheChanCustomTM : boolean + .tycacheChanPhysX : boolean + .tycacheMeshBackup : boolean + .tycacheCreateObject : boolean + .tycacheCreateObjectIfNotCreated : boolean + .tycacheLayer : string + .tycacheObjectName : string + .tycacheAdditionalCloth : boolean + .tycacheAdditionalSkin : boolean + .tycacheAdditionalSkinID : boolean + .tycacheAdditionalSkinIDValue : integer + .tycacheAdditionalTerrain : boolean + .tycacheAdditionalVDB : boolean + .tycacheAdditionalSplinePaths : boolean + .tycacheAdditionalTyMesher : boolean + .tycacheAdditionalGeo : boolean + .tycacheAdditionalObjectList_deprecated : node array + .tycacheAdditionalObjectList : maxObject array + .tycacheAdditionalGeoActivateModifiers : boolean + .tycacheSplinesAdditionalSplines : boolean + .tycacheSplinesAdditionalSplinesObjectList_deprecated : node array + .tycacheSplinesAdditionalObjectList : maxObject array + +""" diff --git a/openpype/hosts/max/plugins/publish/validate_pointcloud.py b/openpype/hosts/max/plugins/publish/validate_pointcloud.py index 295a23f1f6..3ccc9dfda8 100644 --- a/openpype/hosts/max/plugins/publish/validate_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/validate_pointcloud.py @@ -14,29 +14,16 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): def process(self, instance): """ Notes: - - 1. Validate the container only include tyFlow objects - 2. Validate if tyFlow operator Export Particle exists - 3. Validate if the export mode of Export Particle is at PRT format - 4. Validate the partition count and range set as default value + 1. Validate if the export mode of Export Particle is at PRT format + 2. Validate the partition count and range set as default value Partition Count : 100 Partition Range : 1 to 1 - 5. Validate if the custom attribute(s) exist as parameter(s) + 3. Validate if the custom attribute(s) exist as parameter(s) of export_particle operator """ report = [] - invalid_object = self.get_tyflow_object(instance) - if invalid_object: - report.append(f"Non tyFlow object found: {invalid_object}") - - invalid_operator = self.get_tyflow_operator(instance) - if invalid_operator: - report.append(( - "tyFlow ExportParticle operator not " - f"found: {invalid_operator}")) - if self.validate_export_mode(instance): report.append("The export mode is not at PRT") @@ -52,46 +39,6 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): if report: raise PublishValidationError(f"{report}") - def get_tyflow_object(self, instance): - invalid = [] - container = instance.data["instance_node"] - self.log.info(f"Validating tyFlow container for {container}") - - selection_list = instance.data["members"] - for sel in selection_list: - sel_tmp = str(sel) - if rt.ClassOf(sel) in [rt.tyFlow, - rt.Editable_Mesh]: - if "tyFlow" not in sel_tmp: - invalid.append(sel) - else: - invalid.append(sel) - - return invalid - - def get_tyflow_operator(self, instance): - invalid = [] - container = instance.data["instance_node"] - self.log.info(f"Validating tyFlow object for {container}") - selection_list = instance.data["members"] - bool_list = [] - for sel in selection_list: - obj = sel.baseobject - anim_names = rt.GetSubAnimNames(obj) - for anim_name in anim_names: - # get all the names of the related tyFlow nodes - sub_anim = rt.GetSubAnim(obj, anim_name) - # check if there is export particle operator - boolean = rt.IsProperty(sub_anim, "Export_Particles") - bool_list.append(str(boolean)) - # if the export_particles property is not there - # it means there is not a "Export Particle" operator - if "True" not in bool_list: - self.log.error("Operator 'Export Particles' not found!") - invalid.append(sel) - - return invalid - def validate_custom_attribute(self, instance): invalid = [] container = instance.data["instance_node"] diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py new file mode 100644 index 0000000000..de8d161b9d --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -0,0 +1,74 @@ +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt + + +class ValidatePointCloud(pyblish.api.InstancePlugin): + """Validate that TyFlow plugins or + relevant operators being set correctly.""" + + order = pyblish.api.ValidatorOrder + families = ["pointcloud", "tycache"] + hosts = ["max"] + label = "TyFlow Data" + + def process(self, instance): + """ + Notes: + 1. Validate the container only include tyFlow objects + 2. Validate if tyFlow operator Export Particle exists + + """ + report = [] + + invalid_object = self.get_tyflow_object(instance) + if invalid_object: + report.append(f"Non tyFlow object found: {invalid_object}") + + invalid_operator = self.get_tyflow_operator(instance) + if invalid_operator: + report.append(( + "tyFlow ExportParticle operator not " + f"found: {invalid_operator}")) + if report: + raise PublishValidationError(f"{report}") + + def get_tyflow_object(self, instance): + invalid = [] + container = instance.data["instance_node"] + self.log.info(f"Validating tyFlow container for {container}") + + selection_list = instance.data["members"] + for sel in selection_list: + sel_tmp = str(sel) + if rt.ClassOf(sel) in [rt.tyFlow, + rt.Editable_Mesh]: + if "tyFlow" not in sel_tmp: + invalid.append(sel) + else: + invalid.append(sel) + + return invalid + + def get_tyflow_operator(self, instance): + invalid = [] + container = instance.data["instance_node"] + self.log.info(f"Validating tyFlow object for {container}") + selection_list = instance.data["members"] + bool_list = [] + for sel in selection_list: + obj = sel.baseobject + anim_names = rt.GetSubAnimNames(obj) + for anim_name in anim_names: + # get all the names of the related tyFlow nodes + sub_anim = rt.GetSubAnim(obj, anim_name) + # check if there is export particle operator + boolean = rt.IsProperty(sub_anim, "Export_Particles") + bool_list.append(str(boolean)) + # if the export_particles property is not there + # it means there is not a "Export Particle" operator + if "True" not in bool_list: + self.log.error("Operator 'Export Particles' not found!") + invalid.append(sel) + + return invalid