From ebc4f1467d5c4cef02031ceda4243a683c4c22ed Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Jan 2024 16:20:58 +0800 Subject: [PATCH] allows users to set up the scene unit scale in Max with OP/AYON settings /refactor fbx extractors --- openpype/hosts/max/api/lib.py | 30 ++++++++++ openpype/hosts/max/api/menu.py | 8 +++ openpype/hosts/max/api/pipeline.py | 58 ++++++++++++++----- .../max/plugins/publish/extract_camera_fbx.py | 55 ------------------ .../{extract_model_fbx.py => extract_fbx.py} | 40 ++++++++++--- .../defaults/project_settings/max.json | 4 ++ .../projects_schema/schema_project_max.json | 31 ++++++++++ server_addon/max/server/settings/main.py | 30 ++++++++++ server_addon/max/server/version.py | 2 +- 9 files changed, 180 insertions(+), 78 deletions(-) delete mode 100644 openpype/hosts/max/plugins/publish/extract_camera_fbx.py rename openpype/hosts/max/plugins/publish/{extract_model_fbx.py => extract_fbx.py} (67%) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8531233bb2..e98d4632ba 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -294,6 +294,35 @@ def reset_frame_range(fps: bool = True): frame_range["frameStartHandle"], frame_range["frameEndHandle"]) +def reset_unit_scale(): + """Apply the unit scale setting to 3dsMax + """ + project_name = get_current_project_name() + settings = get_project_settings(project_name).get("max") + unit_scale_setting = settings.get("unit_scale_settings") + if unit_scale_setting: + scene_scale = unit_scale_setting["scene_unit_scale"] + rt.units.SystemType = rt.Name(scene_scale) + +def convert_unit_scale(): + """Convert system unit scale in 3dsMax + for fbx export + + Returns: + str: unit scale + """ + unit_scale_dict = { + "inches": "in", + "feet": "ft", + "miles": "mi", + "millimeters": "mm", + "centimeters": "cm", + "meters": "m", + "kilometers": "km" + } + current_unit_scale = rt.Execute("units.SystemType as string") + return unit_scale_dict[current_unit_scale] + def set_context_setting(): """Apply the project settings from the project definition @@ -310,6 +339,7 @@ def set_context_setting(): reset_scene_resolution() reset_frame_range() reset_colorspace() + reset_unit_scale() def get_max_version(): diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index caaa3e3730..9bdb6bd7ce 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -124,6 +124,10 @@ class OpenPypeMenu(object): colorspace_action.triggered.connect(self.colorspace_callback) openpype_menu.addAction(colorspace_action) + unit_scale_action = QtWidgets.QAction("Set Unit Scale", openpype_menu) + unit_scale_action.triggered.connect(self.unit_scale_callback) + openpype_menu.addAction(unit_scale_action) + return openpype_menu def load_callback(self): @@ -157,3 +161,7 @@ class OpenPypeMenu(object): def colorspace_callback(self): """Callback to reset colorspace""" return lib.reset_colorspace() + + def unit_scale_callback(self): + """Callback to reset unit scale""" + return lib.reset_unit_scale() diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index d0ae854dc8..ea4ff35557 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -3,6 +3,7 @@ import os import logging from operator import attrgetter +from functools import partial import json @@ -13,6 +14,10 @@ from openpype.pipeline import ( register_loader_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.lib import ( + register_event_callback, + emit_event +) from openpype.hosts.max.api.menu import OpenPypeMenu from openpype.hosts.max.api import lib from openpype.hosts.max.api.plugin import MS_CUSTOM_ATTRIB @@ -46,19 +51,14 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - # self._register_callbacks() + self._register_callbacks() self.menu = OpenPypeMenu() + register_event_callback( + "init", self._deferred_menu_creation) self._has_been_setup = True - - def context_setting(): - return lib.set_context_setting() - - rt.callbacks.addScript(rt.Name('systemPostNew'), - context_setting) - - rt.callbacks.addScript(rt.Name('filePostOpen'), - lib.check_colorspace) + register_event_callback("open", on_open) + register_event_callback("new", on_new) def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? @@ -83,11 +83,28 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return ls() def _register_callbacks(self): - rt.callbacks.removeScripts(id=rt.name("OpenPypeCallbacks")) - - rt.callbacks.addScript( + unique_id = rt.Name("openpype_callbacks") + for handler, event in self._op_events.copy().items(): + if event is None: + continue + try: + rt.callbacks.removeScripts(id=unique_id) + self._op_events[handler] = None + except RuntimeError as exc: + self.log.info(exc) + #self._deferred_menu_creation + self._op_events["init"] = rt.callbacks.addScript( rt.Name("postLoadingMenus"), - self._deferred_menu_creation, id=rt.Name('OpenPypeCallbacks')) + partial(_emit_event_notification_param, "init"), + id=unique_id) + self._op_events["new"] = rt.callbacks.addScript( + rt.Name('systemPostNew'), + partial(_emit_event_notification_param, "new"), + id=unique_id) + self._op_events["open"] = rt.callbacks.addScript( + rt.Name('filePostOpen'), + partial(_emit_event_notification_param, "open"), + id=unique_id) def _deferred_menu_creation(self): self.log.info("Building menu ...") @@ -144,6 +161,19 @@ attributes "OpenPypeContext" rt.saveMaxFile(dst_path) +def _emit_event_notification_param(event): + notification = rt.callbacks.notificationParam() + emit_event(event, {"notificationParam": notification}) + + +def on_open(): + return lib.check_colorspace() + + +def on_new(): + return lib.set_context_setting() + + def ls() -> list: """Get all OpenPype instances.""" objs = rt.objects diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py deleted file mode 100644 index 4b5631b05f..0000000000 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ /dev/null @@ -1,55 +0,0 @@ -import os - -import pyblish.api -from pymxs import runtime as rt - -from openpype.hosts.max.api import maintained_selection -from openpype.pipeline import OptionalPyblishPluginMixin, publish - - -class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): - """Extract Camera with FbxExporter.""" - - order = pyblish.api.ExtractorOrder - 0.2 - label = "Extract Fbx Camera" - hosts = ["max"] - families = ["camera"] - optional = True - - def process(self, instance): - if not self.is_active(instance.data): - return - - stagingdir = self.staging_dir(instance) - filename = "{name}.fbx".format(**instance.data) - - filepath = os.path.join(stagingdir, filename) - rt.FBXExporterSetParam("Animation", True) - rt.FBXExporterSetParam("Cameras", True) - rt.FBXExporterSetParam("AxisConversionMethod", "Animation") - rt.FBXExporterSetParam("UpAxis", "Y") - rt.FBXExporterSetParam("Preserveinstances", True) - - with maintained_selection(): - # select and export - node_list = instance.data["members"] - rt.Select(node_list) - rt.ExportFile( - filepath, - rt.Name("noPrompt"), - selectedOnly=True, - using=rt.FBXEXP, - ) - - 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(f"Extracted instance '{instance.name}' to: {filepath}") diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_fbx.py similarity index 67% rename from openpype/hosts/max/plugins/publish/extract_model_fbx.py rename to openpype/hosts/max/plugins/publish/extract_fbx.py index 6c42fd5364..d41f3e40fc 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_fbx.py @@ -3,6 +3,7 @@ import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection +from openpype.hosts.max.api.lib import convert_unit_scale class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): @@ -23,14 +24,7 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - - rt.FBXExporterSetParam("Animation", False) - rt.FBXExporterSetParam("Cameras", False) - rt.FBXExporterSetParam("Lights", False) - rt.FBXExporterSetParam("PointCache", False) - rt.FBXExporterSetParam("AxisConversionMethod", "Animation") - rt.FBXExporterSetParam("UpAxis", "Y") - rt.FBXExporterSetParam("Preserveinstances", True) + self._set_fbx_attributes() with maintained_selection(): # select and export @@ -56,3 +50,33 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): self.log.info( "Extracted instance '%s' to: %s" % (instance.name, filepath) ) + + def _set_fbx_attributes(self): + unit_scale = convert_unit_scale() + rt.FBXExporterSetParam("Animation", False) + rt.FBXExporterSetParam("Cameras", False) + rt.FBXExporterSetParam("Lights", False) + rt.FBXExporterSetParam("PointCache", False) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) + if unit_scale: + rt.FBXExporterSetParam("ConvertUnit", unit_scale) + +class ExtractCameraFbx(ExtractModelFbx): + """Extract Camera with FbxExporter.""" + + order = pyblish.api.ExtractorOrder - 0.2 + label = "Extract Fbx Camera" + families = ["camera"] + optional = True + + def _set_fbx_attributes(self): + unit_scale = convert_unit_scale() + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) + if unit_scale: + rt.FBXExporterSetParam("ConvertUnit", unit_scale) diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 19c9d10496..1b574dc4d3 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -1,4 +1,8 @@ { + "unit_scale_settings": { + "enabled": true, + "scene_unit_scale": "Inches" + }, "imageio": { "activate_host_color_management": true, "ocio_config": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 78cca357a3..1df14c04e1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -5,6 +5,37 @@ "label": "Max", "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": "scene_unit_scale", + "label": "Scene Unit Scale", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"Inches": "in"}, + {"Feet": "ft"}, + {"Miles": "mi"}, + {"Millimeters": "mm"}, + {"Centimeters": "cm"}, + {"Meters": "m"}, + {"Kilometers": "km"} + ] + } + ] + }, { "key": "imageio", "type": "dict", diff --git a/server_addon/max/server/settings/main.py b/server_addon/max/server/settings/main.py index ea6a11915a..94dee3e55c 100644 --- a/server_addon/max/server/settings/main.py +++ b/server_addon/max/server/settings/main.py @@ -12,6 +12,28 @@ from .publishers import ( ) +def unit_scale_enum(): + """Return enumerator for scene unit scale.""" + return [ + {"label": "in", "value": "Inches"}, + {"label": "ft", "value": "Feet"}, + {"label": "mi", "value": "Miles"}, + {"label": "mm", "value": "Millimeters"}, + {"label": "cm", "value": "Centimeters"}, + {"label": "m", "value": "Meters"}, + {"label": "km", "value": "Kilometers"} + ] + + +class UnitScaleSettings(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + scene_unit_scale: str = Field( + "Centimeters", + title="Scene Unit Scale", + enum_resolver=unit_scale_enum + ) + + class PRTAttributesModel(BaseSettingsModel): _layout = "compact" name: str = Field(title="Name") @@ -24,6 +46,10 @@ class PointCloudSettings(BaseSettingsModel): class MaxSettings(BaseSettingsModel): + unit_scale_settings: UnitScaleSettings = Field( + default_factory=UnitScaleSettings, + title="Set Unit Scale" + ) imageio: ImageIOSettings = Field( default_factory=ImageIOSettings, title="Color Management (ImageIO)" @@ -46,6 +72,10 @@ class MaxSettings(BaseSettingsModel): DEFAULT_VALUES = { + "unit_scale_settings": { + "enabled": True, + "scene_unit_scale": "cm" + }, "RenderSettings": DEFAULT_RENDER_SETTINGS, "CreateReview": DEFAULT_CREATE_REVIEW_SETTINGS, "PointCloud": { diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py index ae7362549b..bbab0242f6 100644 --- a/server_addon/max/server/version.py +++ b/server_addon/max/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4"