allows users to set up the scene unit scale in Max with OP/AYON settings /refactor fbx extractors

This commit is contained in:
Kayla Man 2024-01-02 16:20:58 +08:00
parent 8ea81950e2
commit ebc4f1467d
9 changed files with 180 additions and 78 deletions

View file

@ -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():

View file

@ -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()

View file

@ -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

View file

@ -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}")

View file

@ -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)

View file

@ -1,4 +1,8 @@
{
"unit_scale_settings": {
"enabled": true,
"scene_unit_scale": "Inches"
},
"imageio": {
"activate_host_color_management": true,
"ocio_config": {

View file

@ -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",

View file

@ -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": {

View file

@ -1 +1 @@
__version__ = "0.1.3"
__version__ = "0.1.4"