Merge pull request #4555 from ynput/feature/OP-4245Data_Exchange_Geometry

This commit is contained in:
Ondřej Samohel 2023-05-03 14:53:52 +02:00 committed by GitHub
commit 9f027def95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 767 additions and 4 deletions

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
"""Creator plugin for model."""
from openpype.hosts.max.api import plugin
from openpype.pipeline import CreatedInstance
class CreateModel(plugin.MaxCreator):
identifier = "io.openpype.creators.max.model"
label = "Model"
family = "model"
icon = "gear"
def create(self, subset_name, instance_data, pre_create_data):
from pymxs import runtime as rt
instance = super(CreateModel, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
container = rt.getNodeByName(instance.data.get("instance_node"))
# TODO: Disable "Add to Containers?" Panel
# parent the selected cameras into the container
sel_obj = None
if self.selected_nodes:
sel_obj = list(self.selected_nodes)
for obj in sel_obj:
obj.parent = container
# for additional work on the node:
# instance_node = rt.getNodeByName(instance.get("instance_node"))

View file

@ -10,7 +10,9 @@ class MaxSceneLoader(load.LoaderPlugin):
"""Max Scene Loader"""
families = ["camera",
"maxScene"]
"maxScene",
"model"]
representations = ["max"]
order = -8
icon = "code-fork"

View file

@ -0,0 +1,109 @@
import os
from openpype.pipeline import (
load, get_representation_path
)
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import maintained_selection
class ModelAbcLoader(load.LoaderPlugin):
"""Loading model with the Alembic loader."""
families = ["model"]
label = "Load Model(Alembic)"
representations = ["abc"]
order = -10
icon = "code-fork"
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
file_path = os.path.normpath(self.fname)
abc_before = {
c for c in rt.rootNode.Children
if rt.classOf(c) == rt.AlembicContainer
}
abc_import_cmd = (f"""
AlembicImport.ImportToRoot = false
AlembicImport.CustomAttributes = true
AlembicImport.UVs = true
AlembicImport.VertexColors = true
importFile @"{file_path}" #noPrompt
""")
self.log.debug(f"Executing command: {abc_import_cmd}")
rt.execute(abc_import_cmd)
abc_after = {
c for c in rt.rootNode.Children
if rt.classOf(c) == rt.AlembicContainer
}
# This should yield new AlembicContainer node
abc_containers = abc_after.difference(abc_before)
if len(abc_containers) != 1:
self.log.error("Something failed when loading.")
abc_container = abc_containers.pop()
return containerise(
name, [abc_container], context, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
rt.select(node.Children)
for alembic in rt.selection:
abc = rt.getNodeByName(alembic.name)
rt.select(abc.Children)
for abc_con in rt.selection:
container = rt.getNodeByName(abc_con.name)
container.source = path
rt.select(container.Children)
for abc_obj in rt.selection:
alembic_obj = rt.getNodeByName(abc_obj.name)
alembic_obj.source = path
with maintained_selection():
rt.select(node)
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
def switch(self, container, representation):
self.update(container, representation)
def remove(self, container):
from pymxs import runtime as rt
node = rt.getNodeByName(container["instance_node"])
rt.delete(node)
@staticmethod
def get_container_children(parent, type_name):
from pymxs import runtime as rt
def list_children(node):
children = []
for c in node.Children:
children.append(c)
children += list_children(c)
return children
filtered = []
for child in list_children(parent):
class_type = str(rt.classOf(child.baseObject))
if class_type == type_name:
filtered.append(child)
return filtered

View file

@ -0,0 +1,77 @@
import os
from openpype.pipeline import (
load,
get_representation_path
)
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import maintained_selection
class FbxModelLoader(load.LoaderPlugin):
"""Fbx Model Loader"""
families = ["model"]
representations = ["fbx"]
order = -9
icon = "code-fork"
color = "white"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
filepath = os.path.normpath(self.fname)
fbx_import_cmd = (
f"""
FBXImporterSetParam "Animation" false
FBXImporterSetParam "Cameras" false
FBXImporterSetParam "AxisConversionMethod" true
FbxExporterSetParam "UpAxis" "Y"
FbxExporterSetParam "Preserveinstances" true
importFile @"{filepath}" #noPrompt using:FBXIMP
""")
self.log.debug(f"Executing command: {fbx_import_cmd}")
rt.execute(fbx_import_cmd)
asset = rt.getNodeByName(f"{name}")
return containerise(
name, [asset], context, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
rt.select(node.Children)
fbx_reimport_cmd = (
f"""
FBXImporterSetParam "Animation" false
FBXImporterSetParam "Cameras" false
FBXImporterSetParam "AxisConversionMethod" true
FbxExporterSetParam "UpAxis" "Y"
FbxExporterSetParam "Preserveinstances" true
importFile @"{path}" #noPrompt using:FBXIMP
""")
rt.execute(fbx_reimport_cmd)
with maintained_selection():
rt.select(node)
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
def switch(self, container, representation):
self.update(container, representation)
def remove(self, container):
from pymxs import runtime as rt
node = rt.getNodeByName(container["instance_node"])
rt.delete(node)

View file

@ -0,0 +1,68 @@
import os
from openpype.pipeline import (
load,
get_representation_path
)
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import maintained_selection
class ObjLoader(load.LoaderPlugin):
"""Obj Loader"""
families = ["model"]
representations = ["obj"]
order = -9
icon = "code-fork"
color = "white"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
filepath = os.path.normpath(self.fname)
self.log.debug(f"Executing command to import..")
rt.execute(f'importFile @"{filepath}" #noPrompt using:ObjImp')
# create "missing" container for obj import
container = rt.container()
container.name = f"{name}"
# get current selection
for selection in rt.getCurrentSelection():
selection.Parent = container
asset = rt.getNodeByName(f"{name}")
return containerise(
name, [asset], context, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node_name = container["instance_node"]
node = rt.getNodeByName(node_name)
instance_name, _ = node_name.split("_")
container = rt.getNodeByName(instance_name)
for n in container.Children:
rt.delete(n)
rt.execute(f'importFile @"{path}" #noPrompt using:ObjImp')
# get current selection
for selection in rt.getCurrentSelection():
selection.Parent = container
with maintained_selection():
rt.select(node)
lib.imprint(node_name, {
"representation": str(representation["_id"])
})
def remove(self, container):
from pymxs import runtime as rt
node = rt.getNodeByName(container["instance_node"])
rt.delete(node)

View file

@ -0,0 +1,78 @@
import os
from openpype.pipeline import (
load, get_representation_path
)
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import maintained_selection
class ModelUSDLoader(load.LoaderPlugin):
"""Loading model with the USD loader."""
families = ["model"]
label = "Load Model(USD)"
representations = ["usda"]
order = -10
icon = "code-fork"
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
# asset_filepath
filepath = os.path.normpath(self.fname)
import_options = rt.USDImporter.CreateOptions()
base_filename = os.path.basename(filepath)
filename, ext = os.path.splitext(base_filename)
log_filepath = filepath.replace(ext, "txt")
rt.LogPath = log_filepath
rt.LogLevel = rt.name('info')
rt.USDImporter.importFile(filepath,
importOptions=import_options)
asset = rt.getNodeByName(f"{name}")
return containerise(
name, [asset], context, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node_name = container["instance_node"]
node = rt.getNodeByName(node_name)
for n in node.Children:
for r in n.Children:
rt.delete(r)
rt.delete(n)
instance_name, _ = node_name.split("_")
import_options = rt.USDImporter.CreateOptions()
base_filename = os.path.basename(path)
_, ext = os.path.splitext(base_filename)
log_filepath = path.replace(ext, "txt")
rt.LogPath = log_filepath
rt.LogLevel = rt.name('info')
rt.USDImporter.importFile(path,
importOptions=import_options)
asset = rt.getNodeByName(f"{instance_name}")
asset.Parent = node
with maintained_selection():
rt.select(node)
lib.imprint(node_name, {
"representation": str(representation["_id"])
})
def switch(self, container, representation):
self.update(container, representation)
def remove(self, container):
from pymxs import runtime as rt
node = rt.getNodeByName(container["instance_node"])
rt.delete(node)

View file

@ -15,8 +15,7 @@ from openpype.hosts.max.api import lib
class AbcLoader(load.LoaderPlugin):
"""Alembic loader."""
families = ["model",
"camera",
families = ["camera",
"animation",
"pointcache"]
label = "Load Alembic"

View file

@ -21,7 +21,8 @@ class ExtractMaxSceneRaw(publish.Extractor,
label = "Extract Max Scene (Raw)"
hosts = ["max"]
families = ["camera",
"maxScene"]
"maxScene",
"model"]
optional = True
def process(self, instance):

View file

@ -0,0 +1,74 @@
import os
import pyblish.api
from openpype.pipeline import (
publish,
OptionalPyblishPluginMixin
)
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection,
get_all_children
)
class ExtractModel(publish.Extractor,
OptionalPyblishPluginMixin):
"""
Extract Geometry in Alembic Format
"""
order = pyblish.api.ExtractorOrder - 0.1
label = "Extract Geometry (Alembic)"
hosts = ["max"]
families = ["model"]
optional = True
def process(self, instance):
if not self.is_active(instance.data):
return
container = instance.data["instance_node"]
self.log.info("Extracting Geometry ...")
stagingdir = self.staging_dir(instance)
filename = "{name}.abc".format(**instance.data)
filepath = 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.CustomAttributes = true
AlembicExport.UVs = true
AlembicExport.VertexColors = true
AlembicExport.PreserveInstances = true
exportFile @"{filepath}" #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,
filepath))

View file

@ -0,0 +1,74 @@
import os
import pyblish.api
from openpype.pipeline import (
publish,
OptionalPyblishPluginMixin
)
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection,
get_all_children
)
class ExtractModelFbx(publish.Extractor,
OptionalPyblishPluginMixin):
"""
Extract Geometry in FBX Format
"""
order = pyblish.api.ExtractorOrder - 0.05
label = "Extract FBX"
hosts = ["max"]
families = ["model"]
optional = True
def process(self, instance):
if not self.is_active(instance.data):
return
container = instance.data["instance_node"]
self.log.info("Extracting Geometry ...")
stagingdir = self.staging_dir(instance)
filename = "{name}.fbx".format(**instance.data)
filepath = os.path.join(stagingdir,
filename)
self.log.info("Writing FBX '%s' to '%s'" % (filepath,
stagingdir))
export_fbx_cmd = (
f"""
FBXExporterSetParam "Animation" false
FBXExporterSetParam "Cameras" false
FBXExporterSetParam "Lights" false
FBXExporterSetParam "PointCache" false
FBXExporterSetParam "AxisConversionMethod" "Animation"
FbxExporterSetParam "UpAxis" "Y"
FbxExporterSetParam "Preserveinstances" true
exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP
""")
self.log.debug(f"Executing command: {export_fbx_cmd}")
with maintained_selection():
# select and export
rt.select(get_all_children(rt.getNodeByName(container)))
rt.execute(export_fbx_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))

View file

@ -0,0 +1,59 @@
import os
import pyblish.api
from openpype.pipeline import (
publish,
OptionalPyblishPluginMixin
)
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection,
get_all_children
)
class ExtractModelObj(publish.Extractor,
OptionalPyblishPluginMixin):
"""
Extract Geometry in OBJ Format
"""
order = pyblish.api.ExtractorOrder - 0.05
label = "Extract OBJ"
hosts = ["max"]
families = ["model"]
optional = True
def process(self, instance):
if not self.is_active(instance.data):
return
container = instance.data["instance_node"]
self.log.info("Extracting Geometry ...")
stagingdir = self.staging_dir(instance)
filename = "{name}.obj".format(**instance.data)
filepath = os.path.join(stagingdir,
filename)
self.log.info("Writing OBJ '%s' to '%s'" % (filepath,
stagingdir))
with maintained_selection():
# select and export
rt.select(get_all_children(rt.getNodeByName(container)))
rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') # noqa
self.log.info("Performing Extraction ...")
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'obj',
'ext': 'obj',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
filepath))

View file

@ -0,0 +1,114 @@
import os
import pyblish.api
from openpype.pipeline import (
publish,
OptionalPyblishPluginMixin
)
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection
)
class ExtractModelUSD(publish.Extractor,
OptionalPyblishPluginMixin):
"""
Extract Geometry in USDA Format
"""
order = pyblish.api.ExtractorOrder - 0.05
label = "Extract Geometry (USD)"
hosts = ["max"]
families = ["model"]
optional = True
def process(self, instance):
if not self.is_active(instance.data):
return
container = instance.data["instance_node"]
self.log.info("Extracting Geometry ...")
stagingdir = self.staging_dir(instance)
asset_filename = "{name}.usda".format(**instance.data)
asset_filepath = os.path.join(stagingdir,
asset_filename)
self.log.info("Writing USD '%s' to '%s'" % (asset_filepath,
stagingdir))
log_filename = "{name}.txt".format(**instance.data)
log_filepath = os.path.join(stagingdir,
log_filename)
self.log.info("Writing log '%s' to '%s'" % (log_filepath,
stagingdir))
# get the nodes which need to be exported
export_options = self.get_export_options(log_filepath)
with maintained_selection():
# select and export
node_list = self.get_node_list(container)
rt.USDExporter.ExportFile(asset_filepath,
exportOptions=export_options,
contentSource=rt.name("selected"),
nodeList=node_list)
self.log.info("Performing Extraction ...")
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'usda',
'ext': 'usda',
'files': asset_filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
log_representation = {
'name': 'txt',
'ext': 'txt',
'files': log_filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(log_representation)
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
asset_filepath))
def get_node_list(self, container):
"""
Get the target nodes which are
the children of the container
"""
node_list = []
container_node = rt.getNodeByName(container)
target_node = container_node.Children
rt.select(target_node)
for sel in rt.selection:
node_list.append(sel)
return node_list
def get_export_options(self, log_path):
"""Set Export Options for USD Exporter"""
export_options = rt.USDExporter.createOptions()
export_options.Meshes = True
export_options.Shapes = False
export_options.Lights = False
export_options.Cameras = False
export_options.Materials = False
export_options.MeshFormat = rt.name('fromScene')
export_options.FileFormat = rt.name('ascii')
export_options.UpAxis = rt.name('y')
export_options.LogLevel = rt.name('info')
export_options.LogPath = log_path
export_options.PreserveEdgeOrientation = True
export_options.TimeMode = rt.name('current')
rt.USDexporter.UIOptions = export_options
return export_options

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
from pymxs import runtime as rt
class ValidateModelContent(pyblish.api.InstancePlugin):
"""Validates Model instance contents.
A model instance may only hold either geometry-related
object(excluding Shapes) or editable meshes.
"""
order = pyblish.api.ValidatorOrder
families = ["model"]
hosts = ["max"]
label = "Model Contents"
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError("Model instance must only include"
"Geometry and Editable Mesh")
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 = list(con.Children) or rt.getCurrentSelection()
for sel in selection_list:
if rt.classOf(sel) in rt.Camera.classes:
invalid.append(sel)
if rt.classOf(sel) in rt.Light.classes:
invalid.append(sel)
if rt.classOf(sel) in rt.Shape.classes:
invalid.append(sel)
return invalid

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
from pymxs import runtime as rt
class ValidateUSDPlugin(pyblish.api.InstancePlugin):
"""Validates if USD plugin is installed or loaded in Max
"""
order = pyblish.api.ValidatorOrder - 0.01
families = ["model"]
hosts = ["max"]
label = "USD Plugin"
def process(self, instance):
plugin_mgr = rt.pluginManager
plugin_count = plugin_mgr.pluginDllCount
plugin_info = self.get_plugins(plugin_mgr,
plugin_count)
usd_import = "usdimport.dli"
if usd_import not in plugin_info:
raise PublishValidationError("USD Plugin {}"
" not found".format(usd_import))
usd_export = "usdexport.dle"
if usd_export not in plugin_info:
raise PublishValidationError("USD Plugin {}"
" not found".format(usd_export))
def get_plugins(self, manager, count):
plugin_info_list = list()
for p in range(1, count + 1):
plugin_info = manager.pluginDllName(p)
plugin_info_list.append(plugin_info)
return plugin_info_list