mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
a403605c03
62 changed files with 1957 additions and 114 deletions
10
openpype/hosts/max/__init__.py
Normal file
10
openpype/hosts/max/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from .addon import (
|
||||
MaxAddon,
|
||||
MAX_HOST_DIR,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"MaxAddon",
|
||||
"MAX_HOST_DIR",
|
||||
)
|
||||
16
openpype/hosts/max/addon.py
Normal file
16
openpype/hosts/max/addon.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
MAX_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class MaxAddon(OpenPypeModule, IHostAddon):
|
||||
name = "max"
|
||||
host_name = "max"
|
||||
|
||||
def initialize(self, module_settings):
|
||||
self.enabled = True
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return [".max"]
|
||||
20
openpype/hosts/max/api/__init__.py
Normal file
20
openpype/hosts/max/api/__init__.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Public API for 3dsmax"""
|
||||
|
||||
from .pipeline import (
|
||||
MaxHost,
|
||||
)
|
||||
|
||||
|
||||
from .lib import (
|
||||
maintained_selection,
|
||||
lsattr,
|
||||
get_all_children
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"MaxHost",
|
||||
"maintained_selection",
|
||||
"lsattr",
|
||||
"get_all_children"
|
||||
]
|
||||
122
openpype/hosts/max/api/lib.py
Normal file
122
openpype/hosts/max/api/lib.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Library of functions useful for 3dsmax pipeline."""
|
||||
import json
|
||||
import six
|
||||
from pymxs import runtime as rt
|
||||
from typing import Union
|
||||
import contextlib
|
||||
|
||||
|
||||
JSON_PREFIX = "JSON::"
|
||||
|
||||
|
||||
def imprint(node_name: str, data: dict) -> bool:
|
||||
node = rt.getNodeByName(node_name)
|
||||
if not node:
|
||||
return False
|
||||
|
||||
for k, v in data.items():
|
||||
if isinstance(v, (dict, list)):
|
||||
rt.setUserProp(node, k, f'{JSON_PREFIX}{json.dumps(v)}')
|
||||
else:
|
||||
rt.setUserProp(node, k, v)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def lsattr(
|
||||
attr: str,
|
||||
value: Union[str, None] = None,
|
||||
root: Union[str, None] = None) -> list:
|
||||
"""List nodes having attribute with specified value.
|
||||
|
||||
Args:
|
||||
attr (str): Attribute name to match.
|
||||
value (str, Optional): Value to match, of omitted, all nodes
|
||||
with specified attribute are returned no matter of value.
|
||||
root (str, Optional): Root node name. If omitted, scene root is used.
|
||||
|
||||
Returns:
|
||||
list of nodes.
|
||||
"""
|
||||
root = rt.rootnode if root is None else rt.getNodeByName(root)
|
||||
|
||||
def output_node(node, nodes):
|
||||
nodes.append(node)
|
||||
for child in node.Children:
|
||||
output_node(child, nodes)
|
||||
|
||||
nodes = []
|
||||
output_node(root, nodes)
|
||||
return [
|
||||
n for n in nodes
|
||||
if rt.getUserProp(n, attr) == value
|
||||
] if value else [
|
||||
n for n in nodes
|
||||
if rt.getUserProp(n, attr)
|
||||
]
|
||||
|
||||
|
||||
def read(container) -> dict:
|
||||
data = {}
|
||||
props = rt.getUserPropBuffer(container)
|
||||
# this shouldn't happen but let's guard against it anyway
|
||||
if not props:
|
||||
return data
|
||||
|
||||
for line in props.split("\r\n"):
|
||||
try:
|
||||
key, value = line.split("=")
|
||||
except ValueError:
|
||||
# if the line cannot be split we can't really parse it
|
||||
continue
|
||||
|
||||
value = value.strip()
|
||||
if isinstance(value.strip(), six.string_types) and \
|
||||
value.startswith(JSON_PREFIX):
|
||||
try:
|
||||
value = json.loads(value[len(JSON_PREFIX):])
|
||||
except json.JSONDecodeError:
|
||||
# not a json
|
||||
pass
|
||||
|
||||
data[key.strip()] = value
|
||||
|
||||
data["instance_node"] = container.name
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_selection():
|
||||
previous_selection = rt.getCurrentSelection()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if previous_selection:
|
||||
rt.select(previous_selection)
|
||||
else:
|
||||
rt.select()
|
||||
|
||||
|
||||
def get_all_children(parent, node_type=None):
|
||||
"""Handy function to get all the children of a given node
|
||||
|
||||
Args:
|
||||
parent (3dsmax Node1): Node to get all children of.
|
||||
node_type (None, runtime.class): give class to check for
|
||||
e.g. rt.FFDBox/rt.GeometryClass etc.
|
||||
|
||||
Returns:
|
||||
list: list of all children of the parent node
|
||||
"""
|
||||
def list_children(node):
|
||||
children = []
|
||||
for c in node.Children:
|
||||
children.append(c)
|
||||
children = children + list_children(c)
|
||||
return children
|
||||
child_list = list_children(parent)
|
||||
|
||||
return ([x for x in child_list if rt.superClassOf(x) == node_type]
|
||||
if node_type else child_list)
|
||||
130
openpype/hosts/max/api/menu.py
Normal file
130
openpype/hosts/max/api/menu.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""3dsmax menu definition of OpenPype."""
|
||||
from Qt import QtWidgets, QtCore
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from openpype.tools.utils import host_tools
|
||||
|
||||
|
||||
class OpenPypeMenu(object):
|
||||
"""Object representing OpenPype menu.
|
||||
|
||||
This is using "hack" to inject itself before "Help" menu of 3dsmax.
|
||||
For some reason `postLoadingMenus` event doesn't fire, and main menu
|
||||
if probably re-initialized by menu templates, se we wait for at least
|
||||
1 event Qt event loop before trying to insert.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.main_widget = self.get_main_widget()
|
||||
self.menu = None
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
# set number of event loops to wait.
|
||||
timer.setInterval(1)
|
||||
timer.timeout.connect(self._on_timer)
|
||||
timer.start()
|
||||
|
||||
self._timer = timer
|
||||
self._counter = 0
|
||||
|
||||
def _on_timer(self):
|
||||
if self._counter < 1:
|
||||
self._counter += 1
|
||||
return
|
||||
|
||||
self._counter = 0
|
||||
self._timer.stop()
|
||||
self.build_openpype_menu()
|
||||
|
||||
@staticmethod
|
||||
def get_main_widget():
|
||||
"""Get 3dsmax main window."""
|
||||
return QtWidgets.QWidget.find(rt.windows.getMAXHWND())
|
||||
|
||||
def get_main_menubar(self) -> QtWidgets.QMenuBar:
|
||||
"""Get main Menubar by 3dsmax main window."""
|
||||
return list(self.main_widget.findChildren(QtWidgets.QMenuBar))[0]
|
||||
|
||||
def get_or_create_openpype_menu(
|
||||
self, name: str = "&OpenPype",
|
||||
before: str = "&Help") -> QtWidgets.QAction:
|
||||
"""Create OpenPype menu.
|
||||
|
||||
Args:
|
||||
name (str, Optional): OpenPypep menu name.
|
||||
before (str, Optional): Name of the 3dsmax main menu item to
|
||||
add OpenPype menu before.
|
||||
|
||||
Returns:
|
||||
QtWidgets.QAction: OpenPype menu action.
|
||||
|
||||
"""
|
||||
if self.menu is not None:
|
||||
return self.menu
|
||||
|
||||
menu_bar = self.get_main_menubar()
|
||||
menu_items = menu_bar.findChildren(
|
||||
QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly)
|
||||
help_action = None
|
||||
for item in menu_items:
|
||||
if name in item.title():
|
||||
# we already have OpenPype menu
|
||||
return item
|
||||
|
||||
if before in item.title():
|
||||
help_action = item.menuAction()
|
||||
|
||||
op_menu = QtWidgets.QMenu("&OpenPype")
|
||||
menu_bar.insertMenu(help_action, op_menu)
|
||||
|
||||
self.menu = op_menu
|
||||
return op_menu
|
||||
|
||||
def build_openpype_menu(self) -> QtWidgets.QAction:
|
||||
"""Build items in OpenPype menu."""
|
||||
openpype_menu = self.get_or_create_openpype_menu()
|
||||
load_action = QtWidgets.QAction("Load...", openpype_menu)
|
||||
load_action.triggered.connect(self.load_callback)
|
||||
openpype_menu.addAction(load_action)
|
||||
|
||||
publish_action = QtWidgets.QAction("Publish...", openpype_menu)
|
||||
publish_action.triggered.connect(self.publish_callback)
|
||||
openpype_menu.addAction(publish_action)
|
||||
|
||||
manage_action = QtWidgets.QAction("Manage...", openpype_menu)
|
||||
manage_action.triggered.connect(self.manage_callback)
|
||||
openpype_menu.addAction(manage_action)
|
||||
|
||||
library_action = QtWidgets.QAction("Library...", openpype_menu)
|
||||
library_action.triggered.connect(self.library_callback)
|
||||
openpype_menu.addAction(library_action)
|
||||
|
||||
openpype_menu.addSeparator()
|
||||
|
||||
workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu)
|
||||
workfiles_action.triggered.connect(self.workfiles_callback)
|
||||
openpype_menu.addAction(workfiles_action)
|
||||
return openpype_menu
|
||||
|
||||
def load_callback(self):
|
||||
"""Callback to show Loader tool."""
|
||||
host_tools.show_loader(parent=self.main_widget)
|
||||
|
||||
def publish_callback(self):
|
||||
"""Callback to show Publisher tool."""
|
||||
host_tools.show_publisher(parent=self.main_widget)
|
||||
|
||||
def manage_callback(self):
|
||||
"""Callback to show Scene Manager/Inventory tool."""
|
||||
host_tools.show_subset_manager(parent=self.main_widget)
|
||||
|
||||
def library_callback(self):
|
||||
"""Callback to show Library Loader tool."""
|
||||
host_tools.show_library_loader(parent=self.main_widget)
|
||||
|
||||
def workfiles_callback(self):
|
||||
"""Callback to show Workfiles tool."""
|
||||
host_tools.show_workfiles(parent=self.main_widget)
|
||||
145
openpype/hosts/max/api/pipeline.py
Normal file
145
openpype/hosts/max/api/pipeline.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pipeline tools for OpenPype Houdini integration."""
|
||||
import os
|
||||
import logging
|
||||
|
||||
import json
|
||||
|
||||
from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher
|
||||
import pyblish.api
|
||||
from openpype.pipeline import (
|
||||
register_creator_plugin_path,
|
||||
register_loader_plugin_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from openpype.hosts.max.api.menu import OpenPypeMenu
|
||||
from openpype.hosts.max.api import lib
|
||||
from openpype.hosts.max import MAX_HOST_DIR
|
||||
|
||||
from pymxs import runtime as rt # noqa
|
||||
|
||||
log = logging.getLogger("openpype.hosts.max")
|
||||
|
||||
PLUGINS_DIR = os.path.join(MAX_HOST_DIR, "plugins")
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
||||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||
|
||||
|
||||
class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher):
|
||||
|
||||
name = "max"
|
||||
menu = None
|
||||
|
||||
def __init__(self):
|
||||
super(MaxHost, self).__init__()
|
||||
self._op_events = {}
|
||||
self._has_been_setup = False
|
||||
|
||||
def install(self):
|
||||
pyblish.api.register_host("max")
|
||||
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
|
||||
# self._register_callbacks()
|
||||
self.menu = OpenPypeMenu()
|
||||
|
||||
self._has_been_setup = True
|
||||
|
||||
def has_unsaved_changes(self):
|
||||
# TODO: how to get it from 3dsmax?
|
||||
return True
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return [".max"]
|
||||
|
||||
def save_workfile(self, dst_path=None):
|
||||
rt.saveMaxFile(dst_path)
|
||||
return dst_path
|
||||
|
||||
def open_workfile(self, filepath):
|
||||
rt.checkForSave()
|
||||
rt.loadMaxFile(filepath)
|
||||
return filepath
|
||||
|
||||
def get_current_workfile(self):
|
||||
return os.path.join(rt.maxFilePath, rt.maxFileName)
|
||||
|
||||
def get_containers(self):
|
||||
return ls()
|
||||
|
||||
def _register_callbacks(self):
|
||||
rt.callbacks.removeScripts(id=rt.name("OpenPypeCallbacks"))
|
||||
|
||||
rt.callbacks.addScript(
|
||||
rt.Name("postLoadingMenus"),
|
||||
self._deferred_menu_creation, id=rt.Name('OpenPypeCallbacks'))
|
||||
|
||||
def _deferred_menu_creation(self):
|
||||
self.log.info("Building menu ...")
|
||||
self.menu = OpenPypeMenu()
|
||||
|
||||
@staticmethod
|
||||
def create_context_node():
|
||||
"""Helper for creating context holding node."""
|
||||
|
||||
root_scene = rt.rootScene
|
||||
|
||||
create_attr_script = ("""
|
||||
attributes "OpenPypeContext"
|
||||
(
|
||||
parameters main rollout:params
|
||||
(
|
||||
context type: #string
|
||||
)
|
||||
|
||||
rollout params "OpenPype Parameters"
|
||||
(
|
||||
editText editTextContext "Context" type: #string
|
||||
)
|
||||
)
|
||||
""")
|
||||
|
||||
attr = rt.execute(create_attr_script)
|
||||
rt.custAttributes.add(root_scene, attr)
|
||||
|
||||
return root_scene.OpenPypeContext.context
|
||||
|
||||
def update_context_data(self, data, changes):
|
||||
try:
|
||||
_ = rt.rootScene.OpenPypeContext.context
|
||||
except AttributeError:
|
||||
# context node doesn't exists
|
||||
self.create_context_node()
|
||||
|
||||
rt.rootScene.OpenPypeContext.context = json.dumps(data)
|
||||
|
||||
def get_context_data(self):
|
||||
try:
|
||||
context = rt.rootScene.OpenPypeContext.context
|
||||
except AttributeError:
|
||||
# context node doesn't exists
|
||||
context = self.create_context_node()
|
||||
if not context:
|
||||
context = "{}"
|
||||
return json.loads(context)
|
||||
|
||||
def save_file(self, dst_path=None):
|
||||
# Force forwards slashes to avoid segfault
|
||||
dst_path = dst_path.replace("\\", "/")
|
||||
rt.saveMaxFile(dst_path)
|
||||
|
||||
|
||||
def ls() -> list:
|
||||
"""Get all OpenPype instances."""
|
||||
objs = rt.objects
|
||||
containers = [
|
||||
obj for obj in objs
|
||||
if rt.getUserProp(obj, "id") == AVALON_CONTAINER_ID
|
||||
]
|
||||
|
||||
for container in sorted(containers, key=lambda name: container.name):
|
||||
yield lib.read(container)
|
||||
111
openpype/hosts/max/api/plugin.py
Normal file
111
openpype/hosts/max/api/plugin.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""3dsmax specific Avalon/Pyblish plugin definitions."""
|
||||
from pymxs import runtime as rt
|
||||
import six
|
||||
from abc import ABCMeta
|
||||
from openpype.pipeline import (
|
||||
CreatorError,
|
||||
Creator,
|
||||
CreatedInstance
|
||||
)
|
||||
from openpype.lib import BoolDef
|
||||
from .lib import imprint, read, lsattr
|
||||
|
||||
|
||||
class OpenPypeCreatorError(CreatorError):
|
||||
pass
|
||||
|
||||
|
||||
class MaxCreatorBase(object):
|
||||
|
||||
@staticmethod
|
||||
def cache_subsets(shared_data):
|
||||
if shared_data.get("max_cached_subsets") is None:
|
||||
shared_data["max_cached_subsets"] = {}
|
||||
cached_instances = lsattr("id", "pyblish.avalon.instance")
|
||||
for i in cached_instances:
|
||||
creator_id = rt.getUserProp(i, "creator_identifier")
|
||||
if creator_id not in shared_data["max_cached_subsets"]:
|
||||
shared_data["max_cached_subsets"][creator_id] = [i.name]
|
||||
else:
|
||||
shared_data[
|
||||
"max_cached_subsets"][creator_id].append(i.name) # noqa
|
||||
return shared_data
|
||||
|
||||
@staticmethod
|
||||
def create_instance_node(node_name: str, parent: str = ""):
|
||||
parent_node = rt.getNodeByName(parent) if parent else rt.rootScene
|
||||
if not parent_node:
|
||||
raise OpenPypeCreatorError(f"Specified parent {parent} not found")
|
||||
|
||||
container = rt.container(name=node_name)
|
||||
container.Parent = parent_node
|
||||
|
||||
return container
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class MaxCreator(Creator, MaxCreatorBase):
|
||||
selected_nodes = []
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
if pre_create_data.get("use_selection"):
|
||||
self.selected_nodes = rt.getCurrentSelection()
|
||||
|
||||
instance_node = self.create_instance_node(subset_name)
|
||||
instance_data["instance_node"] = instance_node.name
|
||||
instance = CreatedInstance(
|
||||
self.family,
|
||||
subset_name,
|
||||
instance_data,
|
||||
self
|
||||
)
|
||||
for node in self.selected_nodes:
|
||||
node.Parent = instance_node
|
||||
|
||||
self._add_instance_to_context(instance)
|
||||
imprint(instance_node.name, instance.data_to_store())
|
||||
|
||||
return instance
|
||||
|
||||
def collect_instances(self):
|
||||
self.cache_subsets(self.collection_shared_data)
|
||||
for instance in self.collection_shared_data[
|
||||
"max_cached_subsets"].get(self.identifier, []):
|
||||
created_instance = CreatedInstance.from_existing(
|
||||
read(rt.getNodeByName(instance)), self
|
||||
)
|
||||
self._add_instance_to_context(created_instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, _changes in update_list:
|
||||
instance_node = created_inst.get("instance_node")
|
||||
|
||||
new_values = {
|
||||
key: new_value
|
||||
for key, (_old_value, new_value) in _changes.items()
|
||||
}
|
||||
imprint(
|
||||
instance_node,
|
||||
new_values,
|
||||
)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
"""Remove specified instance from the scene.
|
||||
|
||||
This is only removing `id` parameter so instance is no longer
|
||||
instance, because it might contain valuable data for artist.
|
||||
|
||||
"""
|
||||
for instance in instances:
|
||||
instance_node = rt.getNodeByName(
|
||||
instance.data.get("instance_node"))
|
||||
if instance_node:
|
||||
rt.delete(rt.getNodeByName(instance_node))
|
||||
|
||||
self._remove_instance_from_context(instance)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
return [
|
||||
BoolDef("use_selection", label="Use selection")
|
||||
]
|
||||
17
openpype/hosts/max/hooks/set_paths.py
Normal file
17
openpype/hosts/max/hooks/set_paths.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from openpype.lib import PreLaunchHook
|
||||
|
||||
|
||||
class SetPath(PreLaunchHook):
|
||||
"""Set current dir to workdir.
|
||||
|
||||
Hook `GlobalHostDataHook` must be executed before this hook.
|
||||
"""
|
||||
app_groups = ["max"]
|
||||
|
||||
def execute(self):
|
||||
workdir = self.launch_context.env.get("AVALON_WORKDIR", "")
|
||||
if not workdir:
|
||||
self.log.warning("BUG: Workdir is not filled.")
|
||||
return
|
||||
|
||||
self.launch_context.kwargs["cwd"] = workdir
|
||||
0
openpype/hosts/max/plugins/__init__.py
Normal file
0
openpype/hosts/max/plugins/__init__.py
Normal file
22
openpype/hosts/max/plugins/create/create_pointcache.py
Normal file
22
openpype/hosts/max/plugins/create/create_pointcache.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating pointcache alembics."""
|
||||
from openpype.hosts.max.api import plugin
|
||||
from openpype.pipeline import CreatedInstance
|
||||
|
||||
|
||||
class CreatePointCache(plugin.MaxCreator):
|
||||
identifier = "io.openpype.creators.max.pointcache"
|
||||
label = "Point Cache"
|
||||
family = "pointcache"
|
||||
icon = "gear"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
# from pymxs import runtime as rt
|
||||
|
||||
_ = super(CreatePointCache, self).create(
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
|
||||
# for additional work on the node:
|
||||
# instance_node = rt.getNodeByName(instance.get("instance_node"))
|
||||
65
openpype/hosts/max/plugins/load/load_pointcache.py
Normal file
65
openpype/hosts/max/plugins/load/load_pointcache.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Simple alembic loader for 3dsmax.
|
||||
|
||||
Because of limited api, alembics can be only loaded, but not easily updated.
|
||||
|
||||
"""
|
||||
import os
|
||||
from openpype.pipeline import (
|
||||
load
|
||||
)
|
||||
|
||||
|
||||
class AbcLoader(load.LoaderPlugin):
|
||||
"""Alembic loader."""
|
||||
|
||||
families = ["model", "animation", "pointcache"]
|
||||
label = "Load 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_export_cmd = (f"""
|
||||
AlembicImport.ImportToRoot = false
|
||||
|
||||
importFile @"{file_path}" #noPrompt
|
||||
""")
|
||||
|
||||
self.log.debug(f"Executing command: {abc_export_cmd}")
|
||||
rt.execute(abc_export_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()
|
||||
|
||||
container_name = f"{name}_CON"
|
||||
container = rt.container(name=container_name)
|
||||
abc_container.Parent = container
|
||||
|
||||
return container
|
||||
|
||||
def remove(self, container):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
node = container["node"]
|
||||
rt.delete(node)
|
||||
63
openpype/hosts/max/plugins/publish/collect_workfile.py
Normal file
63
openpype/hosts/max/plugins/publish/collect_workfile.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect current work file."""
|
||||
import os
|
||||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from openpype.pipeline import legacy_io
|
||||
|
||||
|
||||
class CollectWorkfile(pyblish.api.ContextPlugin):
|
||||
"""Inject the current working file into context"""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.01
|
||||
label = "Collect 3dsmax Workfile"
|
||||
hosts = ['max']
|
||||
|
||||
def process(self, context):
|
||||
"""Inject the current working file."""
|
||||
folder = rt.maxFilePath
|
||||
file = rt.maxFileName
|
||||
if not folder or not file:
|
||||
self.log.error("Scene is not saved.")
|
||||
current_file = os.path.join(folder, file)
|
||||
|
||||
context.data['currentFile'] = current_file
|
||||
|
||||
filename, ext = os.path.splitext(file)
|
||||
|
||||
task = legacy_io.Session["AVALON_TASK"]
|
||||
|
||||
data = {}
|
||||
|
||||
# create instance
|
||||
instance = context.create_instance(name=filename)
|
||||
subset = 'workfile' + task.capitalize()
|
||||
|
||||
data.update({
|
||||
"subset": subset,
|
||||
"asset": os.getenv("AVALON_ASSET", None),
|
||||
"label": subset,
|
||||
"publish": True,
|
||||
"family": 'workfile',
|
||||
"families": ['workfile'],
|
||||
"setMembers": [current_file],
|
||||
"frameStart": context.data['frameStart'],
|
||||
"frameEnd": context.data['frameEnd'],
|
||||
"handleStart": context.data['handleStart'],
|
||||
"handleEnd": context.data['handleEnd']
|
||||
})
|
||||
|
||||
data['representations'] = [{
|
||||
'name': ext.lstrip("."),
|
||||
'ext': ext.lstrip("."),
|
||||
'files': file,
|
||||
"stagingDir": folder,
|
||||
}]
|
||||
|
||||
instance.data.update(data)
|
||||
|
||||
self.log.info('Collected instance: {}'.format(file))
|
||||
self.log.info('Scene path: {}'.format(current_file))
|
||||
self.log.info('staging Dir: {}'.format(folder))
|
||||
self.log.info('subset: {}'.format(subset))
|
||||
100
openpype/hosts/max/plugins/publish/extract_pointcache.py
Normal file
100
openpype/hosts/max/plugins/publish/extract_pointcache.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Export alembic file.
|
||||
|
||||
Note:
|
||||
Parameters on AlembicExport (AlembicExport.Parameter):
|
||||
|
||||
ParticleAsMesh (bool): Sets whether particle shapes are exported
|
||||
as meshes.
|
||||
AnimTimeRange (enum): How animation is saved:
|
||||
#CurrentFrame: saves current frame
|
||||
#TimeSlider: saves the active time segments on time slider (default)
|
||||
#StartEnd: saves a range specified by the Step
|
||||
StartFrame (int)
|
||||
EnFrame (int)
|
||||
ShapeSuffix (bool): When set to true, appends the string "Shape" to the
|
||||
name of each exported mesh. This property is set to false by default.
|
||||
SamplesPerFrame (int): Sets the number of animation samples per frame.
|
||||
Hidden (bool): When true, export hidden geometry.
|
||||
UVs (bool): When true, export the mesh UV map channel.
|
||||
Normals (bool): When true, export the mesh normals.
|
||||
VertexColors (bool): When true, export the mesh vertex color map 0 and the
|
||||
current vertex color display data when it differs
|
||||
ExtraChannels (bool): When true, export the mesh extra map channels
|
||||
(map channels greater than channel 1)
|
||||
Velocity (bool): When true, export the meh vertex and particle velocity
|
||||
data.
|
||||
MaterialIDs (bool): When true, export the mesh material ID as
|
||||
Alembic face sets.
|
||||
Visibility (bool): When true, export the node visibility data.
|
||||
LayerName (bool): When true, export the node layer name as an Alembic
|
||||
object property.
|
||||
MaterialName (bool): When true, export the geometry node material name as
|
||||
an Alembic object property
|
||||
ObjectID (bool): When true, export the geometry node g-buffer object ID as
|
||||
an Alembic object property.
|
||||
CustomAttributes (bool): When true, export the node and its modifiers
|
||||
custom attributes into an Alembic object compound property.
|
||||
"""
|
||||
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 ExtractAlembic(publish.Extractor):
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract Pointcache"
|
||||
hosts = ["max"]
|
||||
families = ["pointcache", "camera"]
|
||||
|
||||
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 pointcache ...")
|
||||
|
||||
parent_dir = self.staging_dir(instance)
|
||||
file_name = "{name}.abc".format(**instance.data)
|
||||
path = os.path.join(parent_dir, file_name)
|
||||
|
||||
# We run the render
|
||||
self.log.info("Writing alembic '%s' to '%s'" % (file_name,
|
||||
parent_dir))
|
||||
|
||||
abc_export_cmd = (
|
||||
f"""
|
||||
AlembicExport.ArchiveType = #ogawa
|
||||
AlembicExport.CoordinateSystem = #maya
|
||||
AlembicExport.StartFrame = {start}
|
||||
AlembicExport.EndFrame = {end}
|
||||
|
||||
exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport
|
||||
|
||||
""")
|
||||
|
||||
self.log.debug(f"Executing command: {abc_export_cmd}")
|
||||
|
||||
with maintained_selection():
|
||||
# select and export
|
||||
|
||||
rt.select(get_all_children(rt.getNodeByName(container)))
|
||||
rt.execute(abc_export_cmd)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'abc',
|
||||
'ext': 'abc',
|
||||
'files': file_name,
|
||||
"stagingDir": parent_dir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
18
openpype/hosts/max/plugins/publish/validate_scene_saved.py
Normal file
18
openpype/hosts/max/plugins/publish/validate_scene_saved.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class ValidateSceneSaved(pyblish.api.InstancePlugin):
|
||||
"""Validate that workfile was saved."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["workfile"]
|
||||
hosts = ["max"]
|
||||
label = "Validate Workfile is saved"
|
||||
|
||||
def process(self, instance):
|
||||
if not rt.maxFilePath or not rt.maxFileName:
|
||||
raise PublishValidationError(
|
||||
"Workfile is not saved", title=self.label)
|
||||
9
openpype/hosts/max/startup/startup.ms
Normal file
9
openpype/hosts/max/startup/startup.ms
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-- OpenPype Init Script
|
||||
(
|
||||
local sysPath = dotNetClass "System.IO.Path"
|
||||
local sysDir = dotNetClass "System.IO.Directory"
|
||||
local localScript = getThisScriptFilename()
|
||||
local startup = sysPath.Combine (sysPath.GetDirectoryName localScript) "startup.py"
|
||||
|
||||
python.ExecuteFile startup
|
||||
)
|
||||
6
openpype/hosts/max/startup/startup.py
Normal file
6
openpype/hosts/max/startup/startup.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from openpype.hosts.max.api import MaxHost
|
||||
from openpype.pipeline import install_host
|
||||
|
||||
host = MaxHost()
|
||||
install_host(host)
|
||||
88
openpype/hosts/maya/api/gltf.py
Normal file
88
openpype/hosts/maya/api/gltf.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Tools to work with GLTF."""
|
||||
import logging
|
||||
|
||||
from maya import cmds, mel # noqa
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_gltf_options = {
|
||||
"of": str, # outputFolder
|
||||
"cpr": str, # copyright
|
||||
"sno": bool, # selectedNodeOnly
|
||||
"sn": str, # sceneName
|
||||
"glb": bool, # binary
|
||||
"nbu": bool, # niceBufferURIs
|
||||
"hbu": bool, # hashBufferURI
|
||||
"ext": bool, # externalTextures
|
||||
"ivt": int, # initialValuesTime
|
||||
"acn": str, # animationClipName
|
||||
"ast": int, # animationClipStartTime
|
||||
"aet": int, # animationClipEndTime
|
||||
"afr": float, # animationClipFrameRate
|
||||
"dsa": int, # detectStepAnimations
|
||||
"mpa": str, # meshPrimitiveAttributes
|
||||
"bpa": str, # blendPrimitiveAttributes
|
||||
"i32": bool, # force32bitIndices
|
||||
"ssm": bool, # skipStandardMaterials
|
||||
"eut": bool, # excludeUnusedTexcoord
|
||||
"dm": bool, # defaultMaterial
|
||||
"cm": bool, # colorizeMaterials
|
||||
"dmy": str, # dumpMaya
|
||||
"dgl": str, # dumpGLTF
|
||||
"imd": str, # ignoreMeshDeformers
|
||||
"ssc": bool, # skipSkinClusters
|
||||
"sbs": bool, # skipBlendShapes
|
||||
"rvp": bool, # redrawViewport
|
||||
"vno": bool # visibleNodesOnly
|
||||
}
|
||||
|
||||
|
||||
def extract_gltf(parent_dir,
|
||||
filename,
|
||||
**kwargs):
|
||||
|
||||
"""Sets GLTF export options from data in the instance.
|
||||
|
||||
"""
|
||||
|
||||
cmds.loadPlugin('maya2glTF', quiet=True)
|
||||
# load the UI to run mel command
|
||||
mel.eval("maya2glTF_UI()")
|
||||
|
||||
parent_dir = parent_dir.replace('\\', '/')
|
||||
options = {
|
||||
"dsa": 1,
|
||||
"glb": True
|
||||
}
|
||||
options.update(kwargs)
|
||||
|
||||
for key, value in options.copy().items():
|
||||
if key not in _gltf_options:
|
||||
log.warning("extract_gltf() does not support option '%s'. "
|
||||
"Flag will be ignored..", key)
|
||||
options.pop(key)
|
||||
options.pop(value)
|
||||
continue
|
||||
|
||||
job_args = list()
|
||||
default_opt = "maya2glTF -of \"{0}\" -sn \"{1}\"".format(parent_dir, filename) # noqa
|
||||
job_args.append(default_opt)
|
||||
|
||||
for key, value in options.items():
|
||||
if isinstance(value, str):
|
||||
job_args.append("-{0} \"{1}\"".format(key, value))
|
||||
elif isinstance(value, bool):
|
||||
if value:
|
||||
job_args.append("-{0}".format(key))
|
||||
else:
|
||||
job_args.append("-{0} {1}".format(key, value))
|
||||
|
||||
job_str = " ".join(job_args)
|
||||
log.info("{}".format(job_str))
|
||||
mel.eval(job_str)
|
||||
|
||||
# close the gltf export after finish the export
|
||||
gltf_UI = "maya2glTF_exporter_window"
|
||||
if cmds.window(gltf_UI, q=True, exists=True):
|
||||
cmds.deleteUI(gltf_UI)
|
||||
|
|
@ -128,13 +128,18 @@ def get_main_window():
|
|||
|
||||
@contextlib.contextmanager
|
||||
def suspended_refresh(suspend=True):
|
||||
"""Suspend viewport refreshes"""
|
||||
original_state = cmds.refresh(query=True, suspend=True)
|
||||
"""Suspend viewport refreshes
|
||||
|
||||
cmds.ogs(pause=True) is a toggle so we cant pass False.
|
||||
"""
|
||||
original_state = cmds.ogs(query=True, pause=True)
|
||||
try:
|
||||
cmds.refresh(suspend=suspend)
|
||||
if suspend and not original_state:
|
||||
cmds.ogs(pause=True)
|
||||
yield
|
||||
finally:
|
||||
cmds.refresh(suspend=original_state)
|
||||
if suspend and not original_state:
|
||||
cmds.ogs(pause=True)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from collections import OrderedDict
|
||||
|
||||
from openpype.hosts.maya.api import (
|
||||
lib,
|
||||
plugin
|
||||
|
|
@ -9,12 +7,26 @@ from maya import cmds
|
|||
|
||||
|
||||
class CreateAss(plugin.Creator):
|
||||
"""Arnold Archive"""
|
||||
"""Arnold Scene Source"""
|
||||
|
||||
name = "ass"
|
||||
label = "Ass StandIn"
|
||||
label = "Arnold Scene Source"
|
||||
family = "ass"
|
||||
icon = "cube"
|
||||
expandProcedurals = False
|
||||
motionBlur = True
|
||||
motionBlurKeys = 2
|
||||
motionBlurLength = 0.5
|
||||
maskOptions = False
|
||||
maskCamera = False
|
||||
maskLight = False
|
||||
maskShape = False
|
||||
maskShader = False
|
||||
maskOverride = False
|
||||
maskDriver = False
|
||||
maskFilter = False
|
||||
maskColor_manager = False
|
||||
maskOperator = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateAss, self).__init__(*args, **kwargs)
|
||||
|
|
@ -22,17 +34,27 @@ class CreateAss(plugin.Creator):
|
|||
# Add animation data
|
||||
self.data.update(lib.collect_animation_data())
|
||||
|
||||
# Vertex colors with the geometry
|
||||
self.data["exportSequence"] = False
|
||||
self.data["expandProcedurals"] = self.expandProcedurals
|
||||
self.data["motionBlur"] = self.motionBlur
|
||||
self.data["motionBlurKeys"] = self.motionBlurKeys
|
||||
self.data["motionBlurLength"] = self.motionBlurLength
|
||||
|
||||
# Masks
|
||||
self.data["maskOptions"] = self.maskOptions
|
||||
self.data["maskCamera"] = self.maskCamera
|
||||
self.data["maskLight"] = self.maskLight
|
||||
self.data["maskShape"] = self.maskShape
|
||||
self.data["maskShader"] = self.maskShader
|
||||
self.data["maskOverride"] = self.maskOverride
|
||||
self.data["maskDriver"] = self.maskDriver
|
||||
self.data["maskFilter"] = self.maskFilter
|
||||
self.data["maskColor_manager"] = self.maskColor_manager
|
||||
self.data["maskOperator"] = self.maskOperator
|
||||
|
||||
def process(self):
|
||||
instance = super(CreateAss, self).process()
|
||||
|
||||
# data = OrderedDict(**self.data)
|
||||
|
||||
|
||||
|
||||
nodes = list()
|
||||
nodes = []
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
nodes = cmds.ls(selection=True)
|
||||
|
|
@ -42,7 +64,3 @@ class CreateAss(plugin.Creator):
|
|||
assContent = cmds.sets(name="content_SET")
|
||||
assProxy = cmds.sets(name="proxy_SET", empty=True)
|
||||
cmds.sets([assContent, assProxy], forceElement=instance)
|
||||
|
||||
# self.log.info(data)
|
||||
#
|
||||
# self.data = data
|
||||
|
|
|
|||
35
openpype/hosts/maya/plugins/create/create_proxy_abc.py
Normal file
35
openpype/hosts/maya/plugins/create/create_proxy_abc.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from openpype.hosts.maya.api import (
|
||||
lib,
|
||||
plugin
|
||||
)
|
||||
|
||||
|
||||
class CreateProxyAlembic(plugin.Creator):
|
||||
"""Proxy Alembic for animated data"""
|
||||
|
||||
name = "proxyAbcMain"
|
||||
label = "Proxy Alembic"
|
||||
family = "proxyAbc"
|
||||
icon = "gears"
|
||||
write_color_sets = False
|
||||
write_face_sets = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateProxyAlembic, self).__init__(*args, **kwargs)
|
||||
|
||||
# Add animation data
|
||||
self.data.update(lib.collect_animation_data())
|
||||
|
||||
# Vertex colors with the geometry.
|
||||
self.data["writeColorSets"] = self.write_color_sets
|
||||
# Vertex colors with the geometry.
|
||||
self.data["writeFaceSets"] = self.write_face_sets
|
||||
# Default to exporting world-space
|
||||
self.data["worldSpace"] = True
|
||||
|
||||
# name suffix for the bounding box
|
||||
self.data["nameSuffix"] = "_BBox"
|
||||
|
||||
# Add options for custom attributes
|
||||
self.data["attr"] = ""
|
||||
self.data["attrPrefix"] = ""
|
||||
|
|
@ -48,3 +48,21 @@ class CreateUnrealSkeletalMesh(plugin.Creator):
|
|||
cmds.sets(node, forceElement=joints_set)
|
||||
else:
|
||||
cmds.sets(node, forceElement=geometry_set)
|
||||
|
||||
# Add animation data
|
||||
self.data.update(lib.collect_animation_data())
|
||||
|
||||
# Only renderable visible shapes
|
||||
self.data["renderableOnly"] = False
|
||||
# only nodes that are visible
|
||||
self.data["visibleOnly"] = False
|
||||
# Include parent groups
|
||||
self.data["includeParentHierarchy"] = False
|
||||
# Default to exporting world-space
|
||||
self.data["worldSpace"] = True
|
||||
# Default to suspend refresh.
|
||||
self.data["refresh"] = False
|
||||
|
||||
# Add options for custom attributes
|
||||
self.data["attr"] = ""
|
||||
self.data["attrPrefix"] = ""
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class SetFrameRangeLoader(load.LoaderPlugin):
|
|||
|
||||
families = ["animation",
|
||||
"camera",
|
||||
"proxyAbc",
|
||||
"pointcache"]
|
||||
representations = ["abc"]
|
||||
|
||||
|
|
@ -48,6 +49,7 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
|
|||
|
||||
families = ["animation",
|
||||
"camera",
|
||||
"proxyAbc",
|
||||
"pointcache"]
|
||||
representations = ["abc"]
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from openpype.settings import get_project_settings
|
|||
class AlembicStandinLoader(load.LoaderPlugin):
|
||||
"""Load Alembic as Arnold Standin"""
|
||||
|
||||
families = ["animation", "model", "pointcache"]
|
||||
families = ["animation", "model", "proxyAbc", "pointcache"]
|
||||
representations = ["abc"]
|
||||
|
||||
label = "Import Alembic as Arnold Standin"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from openpype.settings import get_project_settings
|
|||
class GpuCacheLoader(load.LoaderPlugin):
|
||||
"""Load Alembic as gpuCache"""
|
||||
|
||||
families = ["model", "animation", "pointcache"]
|
||||
families = ["model", "animation", "proxyAbc", "pointcache"]
|
||||
representations = ["abc"]
|
||||
|
||||
label = "Import Gpu Cache"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
|
||||
families = ["model",
|
||||
"pointcache",
|
||||
"proxyAbc",
|
||||
"animation",
|
||||
"mayaAscii",
|
||||
"mayaScene",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from maya import cmds
|
||||
from openpype.pipeline.publish import KnownPublishError
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
|
@ -6,6 +7,7 @@ import pyblish.api
|
|||
class CollectAssData(pyblish.api.InstancePlugin):
|
||||
"""Collect Ass data."""
|
||||
|
||||
# Offset to be after renderable camera collection.
|
||||
order = pyblish.api.CollectorOrder + 0.2
|
||||
label = 'Collect Ass'
|
||||
families = ["ass"]
|
||||
|
|
@ -23,8 +25,23 @@ class CollectAssData(pyblish.api.InstancePlugin):
|
|||
instance.data['setMembers'] = members
|
||||
self.log.debug('content members: {}'.format(members))
|
||||
elif objset.startswith("proxy_SET"):
|
||||
assert len(members) == 1, "You have multiple proxy meshes, please only use one"
|
||||
if len(members) != 1:
|
||||
msg = "You have multiple proxy meshes, please only use one"
|
||||
raise KnownPublishError(msg)
|
||||
instance.data['proxy'] = members
|
||||
self.log.debug('proxy members: {}'.format(members))
|
||||
|
||||
# Use camera in object set if present else default to render globals
|
||||
# camera.
|
||||
cameras = cmds.ls(type="camera", long=True)
|
||||
renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)]
|
||||
camera = renderable[0]
|
||||
for node in instance.data["setMembers"]:
|
||||
camera_shapes = cmds.listRelatives(
|
||||
node, shapes=True, type="camera"
|
||||
)
|
||||
if camera_shapes:
|
||||
camera = node
|
||||
instance.data["camera"] = camera
|
||||
|
||||
self.log.debug("data: {}".format(instance.data))
|
||||
|
|
|
|||
17
openpype/hosts/maya/plugins/publish/collect_gltf.py
Normal file
17
openpype/hosts/maya/plugins/publish/collect_gltf.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectGLTF(pyblish.api.InstancePlugin):
|
||||
"""Collect Assets for GLTF/GLB export."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.2
|
||||
label = "Collect Asset for GLTF/GLB export"
|
||||
families = ["model", "animation", "pointcache"]
|
||||
|
||||
def process(self, instance):
|
||||
if not instance.data.get("families"):
|
||||
instance.data["families"] = []
|
||||
|
||||
if "gltf" not in instance.data["families"]:
|
||||
instance.data["families"].append("gltf")
|
||||
|
|
@ -1,77 +1,93 @@
|
|||
import os
|
||||
|
||||
from maya import cmds
|
||||
import arnold
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
from openpype.hosts.maya.api.lib import maintained_selection, attribute_values
|
||||
|
||||
|
||||
class ExtractAssStandin(publish.Extractor):
|
||||
"""Extract the content of the instance to a ass file
|
||||
"""Extract the content of the instance to a ass file"""
|
||||
|
||||
Things to pay attention to:
|
||||
- If animation is toggled, are the frames correct
|
||||
-
|
||||
"""
|
||||
|
||||
label = "Ass Standin (.ass)"
|
||||
label = "Arnold Scene Source (.ass)"
|
||||
hosts = ["maya"]
|
||||
families = ["ass"]
|
||||
asciiAss = False
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
sequence = instance.data.get("exportSequence", False)
|
||||
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filename = "{}.ass".format(instance.name)
|
||||
filenames = list()
|
||||
filenames = []
|
||||
file_path = os.path.join(staging_dir, filename)
|
||||
|
||||
# Mask
|
||||
mask = arnold.AI_NODE_ALL
|
||||
|
||||
node_types = {
|
||||
"options": arnold.AI_NODE_OPTIONS,
|
||||
"camera": arnold.AI_NODE_CAMERA,
|
||||
"light": arnold.AI_NODE_LIGHT,
|
||||
"shape": arnold.AI_NODE_SHAPE,
|
||||
"shader": arnold.AI_NODE_SHADER,
|
||||
"override": arnold.AI_NODE_OVERRIDE,
|
||||
"driver": arnold.AI_NODE_DRIVER,
|
||||
"filter": arnold.AI_NODE_FILTER,
|
||||
"color_manager": arnold.AI_NODE_COLOR_MANAGER,
|
||||
"operator": arnold.AI_NODE_OPERATOR
|
||||
}
|
||||
|
||||
for key in node_types.keys():
|
||||
if instance.data.get("mask" + key.title()):
|
||||
mask = mask ^ node_types[key]
|
||||
|
||||
# Motion blur
|
||||
values = {
|
||||
"defaultArnoldRenderOptions.motion_blur_enable": instance.data.get(
|
||||
"motionBlur", True
|
||||
),
|
||||
"defaultArnoldRenderOptions.motion_steps": instance.data.get(
|
||||
"motionBlurKeys", 2
|
||||
),
|
||||
"defaultArnoldRenderOptions.motion_frames": instance.data.get(
|
||||
"motionBlurLength", 0.5
|
||||
)
|
||||
}
|
||||
|
||||
# Write out .ass file
|
||||
kwargs = {
|
||||
"filename": file_path,
|
||||
"startFrame": instance.data.get("frameStartHandle", 1),
|
||||
"endFrame": instance.data.get("frameEndHandle", 1),
|
||||
"frameStep": instance.data.get("step", 1),
|
||||
"selected": True,
|
||||
"asciiAss": self.asciiAss,
|
||||
"shadowLinks": True,
|
||||
"lightLinks": True,
|
||||
"boundingBox": True,
|
||||
"expandProcedurals": instance.data.get("expandProcedurals", False),
|
||||
"camera": instance.data["camera"],
|
||||
"mask": mask
|
||||
}
|
||||
|
||||
self.log.info("Writing: '%s'" % file_path)
|
||||
with maintained_selection():
|
||||
self.log.info("Writing: {}".format(instance.data["setMembers"]))
|
||||
cmds.select(instance.data["setMembers"], noExpand=True)
|
||||
with attribute_values(values):
|
||||
with maintained_selection():
|
||||
self.log.info(
|
||||
"Writing: {}".format(instance.data["setMembers"])
|
||||
)
|
||||
cmds.select(instance.data["setMembers"], noExpand=True)
|
||||
|
||||
if sequence:
|
||||
self.log.info("Extracting ass sequence")
|
||||
self.log.info(
|
||||
"Extracting ass sequence with: {}".format(kwargs)
|
||||
)
|
||||
|
||||
# Collect the start and end including handles
|
||||
start = instance.data.get("frameStartHandle", 1)
|
||||
end = instance.data.get("frameEndHandle", 1)
|
||||
step = instance.data.get("step", 0)
|
||||
exported_files = cmds.arnoldExportAss(**kwargs)
|
||||
|
||||
exported_files = cmds.arnoldExportAss(filename=file_path,
|
||||
selected=True,
|
||||
asciiAss=self.asciiAss,
|
||||
shadowLinks=True,
|
||||
lightLinks=True,
|
||||
boundingBox=True,
|
||||
startFrame=start,
|
||||
endFrame=end,
|
||||
frameStep=step
|
||||
)
|
||||
for file in exported_files:
|
||||
filenames.append(os.path.split(file)[1])
|
||||
|
||||
self.log.info("Exported: {}".format(filenames))
|
||||
else:
|
||||
self.log.info("Extracting ass")
|
||||
cmds.arnoldExportAss(filename=file_path,
|
||||
selected=True,
|
||||
asciiAss=False,
|
||||
shadowLinks=True,
|
||||
lightLinks=True,
|
||||
boundingBox=True
|
||||
)
|
||||
self.log.info("Extracted {}".format(filename))
|
||||
filenames = filename
|
||||
optionals = [
|
||||
"frameStart", "frameEnd", "step", "handles",
|
||||
"handleEnd", "handleStart"
|
||||
]
|
||||
for key in optionals:
|
||||
instance.data.pop(key, None)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
@ -79,13 +95,11 @@ class ExtractAssStandin(publish.Extractor):
|
|||
representation = {
|
||||
'name': 'ass',
|
||||
'ext': 'ass',
|
||||
'files': filenames,
|
||||
"stagingDir": staging_dir
|
||||
'files': filenames if len(filenames) > 1 else filenames[0],
|
||||
"stagingDir": staging_dir,
|
||||
'frameStart': kwargs["startFrame"]
|
||||
}
|
||||
|
||||
if sequence:
|
||||
representation['frameStart'] = start
|
||||
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s"
|
||||
|
|
|
|||
65
openpype/hosts/maya/plugins/publish/extract_gltf.py
Normal file
65
openpype/hosts/maya/plugins/publish/extract_gltf.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import os
|
||||
|
||||
from maya import cmds, mel
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
from openpype.hosts.maya.api.gltf import extract_gltf
|
||||
|
||||
|
||||
class ExtractGLB(publish.Extractor):
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
hosts = ["maya"]
|
||||
label = "Extract GLB"
|
||||
families = ["gltf"]
|
||||
|
||||
def process(self, instance):
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filename = "{0}.glb".format(instance.name)
|
||||
path = os.path.join(staging_dir, filename)
|
||||
|
||||
self.log.info("Extracting GLB to: {}".format(path))
|
||||
|
||||
nodes = instance[:]
|
||||
|
||||
self.log.info("Instance: {0}".format(nodes))
|
||||
|
||||
start_frame = instance.data('frameStart') or \
|
||||
int(cmds.playbackOptions(query=True,
|
||||
animationStartTime=True))# noqa
|
||||
end_frame = instance.data('frameEnd') or \
|
||||
int(cmds.playbackOptions(query=True,
|
||||
animationEndTime=True)) # noqa
|
||||
fps = mel.eval('currentTimeUnitToFPS()')
|
||||
|
||||
options = {
|
||||
"sno": True, # selectedNodeOnly
|
||||
"nbu": True, # .bin instead of .bin0
|
||||
"ast": start_frame,
|
||||
"aet": end_frame,
|
||||
"afr": fps,
|
||||
"dsa": 1,
|
||||
"acn": instance.name,
|
||||
"glb": True,
|
||||
"vno": True # visibleNodeOnly
|
||||
}
|
||||
with lib.maintained_selection():
|
||||
cmds.select(nodes, hi=True, noExpand=True)
|
||||
extract_gltf(staging_dir,
|
||||
instance.name,
|
||||
**options)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'glb',
|
||||
'ext': 'glb',
|
||||
'files': filename,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extract GLB successful to: {0}".format(path))
|
||||
|
|
@ -86,7 +86,8 @@ class ExtractAlembic(publish.Extractor):
|
|||
start=start,
|
||||
end=end))
|
||||
|
||||
with suspended_refresh(suspend=instance.data.get("refresh", False)):
|
||||
suspend = not instance.data.get("refresh", False)
|
||||
with suspended_refresh(suspend=suspend):
|
||||
with maintained_selection():
|
||||
cmds.select(nodes, noExpand=True)
|
||||
extract_alembic(
|
||||
|
|
|
|||
109
openpype/hosts/maya/plugins/publish/extract_proxy_abc.py
Normal file
109
openpype/hosts/maya/plugins/publish/extract_proxy_abc.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import os
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
extract_alembic,
|
||||
suspended_refresh,
|
||||
maintained_selection,
|
||||
iter_visible_nodes_in_range
|
||||
)
|
||||
|
||||
|
||||
class ExtractProxyAlembic(publish.Extractor):
|
||||
"""Produce an alembic for bounding box geometry
|
||||
"""
|
||||
|
||||
label = "Extract Proxy (Alembic)"
|
||||
hosts = ["maya"]
|
||||
families = ["proxyAbc"]
|
||||
|
||||
def process(self, instance):
|
||||
name_suffix = instance.data.get("nameSuffix")
|
||||
# Collect the start and end including handles
|
||||
start = float(instance.data.get("frameStartHandle", 1))
|
||||
end = float(instance.data.get("frameEndHandle", 1))
|
||||
|
||||
attrs = instance.data.get("attr", "").split(";")
|
||||
attrs = [value for value in attrs if value.strip()]
|
||||
attrs += ["cbId"]
|
||||
|
||||
attr_prefixes = instance.data.get("attrPrefix", "").split(";")
|
||||
attr_prefixes = [value for value in attr_prefixes if value.strip()]
|
||||
|
||||
self.log.info("Extracting Proxy Alembic..")
|
||||
dirname = self.staging_dir(instance)
|
||||
|
||||
filename = "{name}.abc".format(**instance.data)
|
||||
path = os.path.join(dirname, filename)
|
||||
|
||||
proxy_root = self.create_proxy_geometry(instance,
|
||||
name_suffix,
|
||||
start,
|
||||
end)
|
||||
|
||||
options = {
|
||||
"step": instance.data.get("step", 1.0),
|
||||
"attr": attrs,
|
||||
"attrPrefix": attr_prefixes,
|
||||
"writeVisibility": True,
|
||||
"writeCreases": True,
|
||||
"writeColorSets": instance.data.get("writeColorSets", False),
|
||||
"writeFaceSets": instance.data.get("writeFaceSets", False),
|
||||
"uvWrite": True,
|
||||
"selection": True,
|
||||
"worldSpace": instance.data.get("worldSpace", True),
|
||||
"root": proxy_root
|
||||
}
|
||||
|
||||
if int(cmds.about(version=True)) >= 2017:
|
||||
# Since Maya 2017 alembic supports multiple uv sets - write them.
|
||||
options["writeUVSets"] = True
|
||||
|
||||
with suspended_refresh():
|
||||
with maintained_selection():
|
||||
cmds.select(proxy_root, hi=True, noExpand=True)
|
||||
extract_alembic(file=path,
|
||||
startFrame=start,
|
||||
endFrame=end,
|
||||
**options)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'abc',
|
||||
'ext': 'abc',
|
||||
'files': filename,
|
||||
"stagingDir": dirname
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
instance.context.data["cleanupFullPaths"].append(path)
|
||||
|
||||
self.log.info("Extracted {} to {}".format(instance, dirname))
|
||||
# remove the bounding box
|
||||
bbox_master = cmds.ls("bbox_grp")
|
||||
cmds.delete(bbox_master)
|
||||
|
||||
def create_proxy_geometry(self, instance, name_suffix, start, end):
|
||||
nodes = instance[:]
|
||||
nodes = list(iter_visible_nodes_in_range(nodes,
|
||||
start=start,
|
||||
end=end))
|
||||
|
||||
inst_selection = cmds.ls(nodes, long=True)
|
||||
cmds.geomToBBox(inst_selection,
|
||||
nameSuffix=name_suffix,
|
||||
keepOriginal=True,
|
||||
single=False,
|
||||
bakeAnimation=True,
|
||||
startTime=start,
|
||||
endTime=end)
|
||||
# create master group for bounding
|
||||
# boxes as the main root
|
||||
master_group = cmds.group(name="bbox_grp")
|
||||
bbox_sel = cmds.ls(master_group, long=True)
|
||||
self.log.debug("proxy_root: {}".format(bbox_sel))
|
||||
return bbox_sel
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Create Unreal Skeletal Mesh data to be extracted as FBX."""
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
|
||||
from maya import cmds # noqa
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
extract_alembic,
|
||||
suspended_refresh,
|
||||
maintained_selection
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def renamed(original_name, renamed_name):
|
||||
# type: (str, str) -> None
|
||||
try:
|
||||
cmds.rename(original_name, renamed_name)
|
||||
yield
|
||||
finally:
|
||||
cmds.rename(renamed_name, original_name)
|
||||
|
||||
|
||||
class ExtractUnrealSkeletalMeshAbc(publish.Extractor):
|
||||
"""Extract Unreal Skeletal Mesh as FBX from Maya. """
|
||||
|
||||
label = "Extract Unreal Skeletal Mesh - Alembic"
|
||||
hosts = ["maya"]
|
||||
families = ["skeletalMesh"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info("Extracting pointcache..")
|
||||
|
||||
geo = cmds.listRelatives(
|
||||
instance.data.get("geometry"), allDescendents=True, fullPath=True)
|
||||
joints = cmds.listRelatives(
|
||||
instance.data.get("joints"), allDescendents=True, fullPath=True)
|
||||
|
||||
nodes = geo + joints
|
||||
|
||||
attrs = instance.data.get("attr", "").split(";")
|
||||
attrs = [value for value in attrs if value.strip()]
|
||||
attrs += ["cbId"]
|
||||
|
||||
attr_prefixes = instance.data.get("attrPrefix", "").split(";")
|
||||
attr_prefixes = [value for value in attr_prefixes if value.strip()]
|
||||
|
||||
# Define output path
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filename = "{0}.abc".format(instance.name)
|
||||
path = os.path.join(staging_dir, filename)
|
||||
|
||||
# The export requires forward slashes because we need
|
||||
# to format it into a string in a mel expression
|
||||
path = path.replace('\\', '/')
|
||||
|
||||
self.log.info("Extracting ABC to: {0}".format(path))
|
||||
self.log.info("Members: {0}".format(nodes))
|
||||
self.log.info("Instance: {0}".format(instance[:]))
|
||||
|
||||
options = {
|
||||
"step": instance.data.get("step", 1.0),
|
||||
"attr": attrs,
|
||||
"attrPrefix": attr_prefixes,
|
||||
"writeVisibility": True,
|
||||
"writeCreases": True,
|
||||
"writeColorSets": instance.data.get("writeColorSets", False),
|
||||
"writeFaceSets": instance.data.get("writeFaceSets", False),
|
||||
"uvWrite": True,
|
||||
"selection": True,
|
||||
"worldSpace": instance.data.get("worldSpace", True)
|
||||
}
|
||||
|
||||
self.log.info("Options: {}".format(options))
|
||||
|
||||
if int(cmds.about(version=True)) >= 2017:
|
||||
# Since Maya 2017 alembic supports multiple uv sets - write them.
|
||||
options["writeUVSets"] = True
|
||||
|
||||
if not instance.data.get("includeParentHierarchy", True):
|
||||
# Set the root nodes if we don't want to include parents
|
||||
# The roots are to be considered the ones that are the actual
|
||||
# direct members of the set
|
||||
options["root"] = instance.data.get("setMembers")
|
||||
|
||||
with suspended_refresh(suspend=instance.data.get("refresh", False)):
|
||||
with maintained_selection():
|
||||
cmds.select(nodes, noExpand=True)
|
||||
extract_alembic(file=path,
|
||||
# startFrame=start,
|
||||
# endFrame=end,
|
||||
**options)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'abc',
|
||||
'ext': 'abc',
|
||||
'files': filename,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extract ABC successful to: {0}".format(path))
|
||||
|
|
@ -21,12 +21,13 @@ def renamed(original_name, renamed_name):
|
|||
cmds.rename(renamed_name, original_name)
|
||||
|
||||
|
||||
class ExtractUnrealSkeletalMesh(publish.Extractor):
|
||||
class ExtractUnrealSkeletalMeshFbx(publish.Extractor):
|
||||
"""Extract Unreal Skeletal Mesh as FBX from Maya. """
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.1
|
||||
label = "Extract Unreal Skeletal Mesh"
|
||||
label = "Extract Unreal Skeletal Mesh - FBX"
|
||||
families = ["skeletalMesh"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
fbx_exporter = fbx.FBXExtractor(log=self.log)
|
||||
|
|
@ -20,7 +20,7 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin):
|
|||
"""
|
||||
|
||||
order = ValidateContentsOrder
|
||||
families = ['animation', "pointcache"]
|
||||
families = ['animation', "pointcache", "proxyAbc"]
|
||||
hosts = ['maya']
|
||||
label = 'Animation Out Set Related Node Ids'
|
||||
actions = [
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin):
|
|||
families = ["animation",
|
||||
"pointcache",
|
||||
"camera",
|
||||
"proxyAbc",
|
||||
"renderlayer",
|
||||
"review",
|
||||
"yeticache"]
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin):
|
|||
parent.split("|")[1] for parent in (joints_parents + geo_parents)
|
||||
}
|
||||
|
||||
if len(set(parents_set)) != 1:
|
||||
self.log.info(parents_set)
|
||||
|
||||
if len(set(parents_set)) > 2:
|
||||
raise PublishXmlValidationError(
|
||||
self,
|
||||
"Multiple roots on geometry or joints."
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
|
||||
from openpype.hosts.maya.api.action import (
|
||||
SelectInvalidAction,
|
||||
)
|
||||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
)
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
||||
class ValidateSkeletalMeshTriangulated(pyblish.api.InstancePlugin):
|
||||
"""Validates that the geometry has been triangulated."""
|
||||
|
||||
order = ValidateContentsOrder
|
||||
hosts = ["maya"]
|
||||
families = ["skeletalMesh"]
|
||||
label = "Skeletal Mesh Triangulated"
|
||||
optional = True
|
||||
actions = [
|
||||
SelectInvalidAction,
|
||||
RepairAction
|
||||
]
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
"The following objects needs to be triangulated: "
|
||||
"{}".format(invalid))
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
geo = instance.data.get("geometry")
|
||||
|
||||
invalid = []
|
||||
|
||||
for obj in cmds.listRelatives(
|
||||
cmds.ls(geo), allDescendents=True, fullPath=True):
|
||||
n_triangles = cmds.polyEvaluate(obj, triangle=True)
|
||||
n_faces = cmds.polyEvaluate(obj, face=True)
|
||||
|
||||
if not (isinstance(n_triangles, int) and isinstance(n_faces, int)):
|
||||
continue
|
||||
|
||||
# We check if the number of triangles is equal to the number of
|
||||
# faces for each transform node.
|
||||
# If it is, the object is triangulated.
|
||||
if cmds.objectType(obj, i="transform") and n_triangles != n_faces:
|
||||
invalid.append(obj)
|
||||
|
||||
return invalid
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
for node in cls.get_invalid(instance):
|
||||
cmds.polyTriangulate(node)
|
||||
162
openpype/hosts/unreal/plugins/load/load_alembic_animation.py
Normal file
162
openpype/hosts/unreal/plugins/load/load_alembic_animation.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Load Alembic Animation."""
|
||||
import os
|
||||
|
||||
from openpype.pipeline import (
|
||||
get_representation_path,
|
||||
AVALON_CONTAINER_ID
|
||||
)
|
||||
from openpype.hosts.unreal.api import plugin
|
||||
from openpype.hosts.unreal.api import pipeline as unreal_pipeline
|
||||
import unreal # noqa
|
||||
|
||||
|
||||
class AnimationAlembicLoader(plugin.Loader):
|
||||
"""Load Unreal SkeletalMesh from Alembic"""
|
||||
|
||||
families = ["animation"]
|
||||
label = "Import Alembic Animation"
|
||||
representations = ["abc"]
|
||||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
def get_task(self, filename, asset_dir, asset_name, replace):
|
||||
task = unreal.AssetImportTask()
|
||||
options = unreal.AbcImportSettings()
|
||||
sm_settings = unreal.AbcStaticMeshSettings()
|
||||
conversion_settings = unreal.AbcConversionSettings(
|
||||
preset=unreal.AbcConversionPreset.CUSTOM,
|
||||
flip_u=False, flip_v=False,
|
||||
rotation=[0.0, 0.0, 0.0],
|
||||
scale=[1.0, 1.0, -1.0])
|
||||
|
||||
task.set_editor_property('filename', filename)
|
||||
task.set_editor_property('destination_path', asset_dir)
|
||||
task.set_editor_property('destination_name', asset_name)
|
||||
task.set_editor_property('replace_existing', replace)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', True)
|
||||
|
||||
options.set_editor_property(
|
||||
'import_type', unreal.AlembicImportType.SKELETAL)
|
||||
|
||||
options.static_mesh_settings = sm_settings
|
||||
options.conversion_settings = conversion_settings
|
||||
task.options = options
|
||||
|
||||
return task
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
"""Load and containerise representation into Content Browser.
|
||||
|
||||
This is two step process. First, import FBX to temporary path and
|
||||
then call `containerise()` on it - this moves all content to new
|
||||
directory and then it will create AssetContainer there and imprint it
|
||||
with metadata. This will mark this path as container.
|
||||
|
||||
Args:
|
||||
context (dict): application context
|
||||
name (str): subset name
|
||||
namespace (str): in Unreal this is basically path to container.
|
||||
This is not passed here, so namespace is set
|
||||
by `containerise()` because only then we know
|
||||
real path.
|
||||
data (dict): Those would be data to be imprinted. This is not used
|
||||
now, data are imprinted by `containerise()`.
|
||||
|
||||
Returns:
|
||||
list(str): list of container content
|
||||
"""
|
||||
|
||||
# Create directory for asset and openpype container
|
||||
root = "/Game/OpenPype/Assets"
|
||||
asset = context.get('asset').get('name')
|
||||
suffix = "_CON"
|
||||
if asset:
|
||||
asset_name = "{}_{}".format(asset, name)
|
||||
else:
|
||||
asset_name = "{}".format(name)
|
||||
version = context.get('version').get('name')
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
task = self.get_task(self.fname, asset_dir, asset_name, False)
|
||||
|
||||
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||
asset_tools.import_asset_tasks([task])
|
||||
|
||||
# Create Asset Container
|
||||
unreal_pipeline.create_container(
|
||||
container=container_name, path=asset_dir)
|
||||
|
||||
data = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"asset": asset,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": context["representation"]["_id"],
|
||||
"parent": context["representation"]["parent"],
|
||||
"family": context["representation"]["context"]["family"]
|
||||
}
|
||||
unreal_pipeline.imprint(
|
||||
"{}/{}".format(asset_dir, container_name), data)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, representation):
|
||||
name = container["asset_name"]
|
||||
source_path = get_representation_path(representation)
|
||||
destination_path = container["namespace"]
|
||||
|
||||
task = self.get_task(source_path, destination_path, name, True)
|
||||
|
||||
# do import fbx and replace existing data
|
||||
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||
asset_tools.import_asset_tasks([task])
|
||||
|
||||
container_path = f"{container['namespace']}/{container['objectName']}"
|
||||
|
||||
# update metadata
|
||||
unreal_pipeline.imprint(
|
||||
container_path,
|
||||
{
|
||||
"representation": str(representation["_id"]),
|
||||
"parent": str(representation["parent"])
|
||||
})
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
destination_path, recursive=True, include_folder=True
|
||||
)
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
path = container["namespace"]
|
||||
parent_path = os.path.dirname(path)
|
||||
|
||||
unreal.EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
unreal.EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
|
@ -14,7 +14,7 @@ import unreal # noqa
|
|||
class SkeletalMeshAlembicLoader(plugin.Loader):
|
||||
"""Load Unreal SkeletalMesh from Alembic"""
|
||||
|
||||
families = ["pointcache"]
|
||||
families = ["pointcache", "skeletalMesh"]
|
||||
label = "Import Alembic Skeletal Mesh"
|
||||
representations = ["abc"]
|
||||
icon = "cube"
|
||||
|
|
@ -14,7 +14,7 @@ import unreal # noqa
|
|||
class StaticMeshAlembicLoader(plugin.Loader):
|
||||
"""Load Unreal StaticMesh from Alembic"""
|
||||
|
||||
families = ["model"]
|
||||
families = ["model", "staticMesh"]
|
||||
label = "Import Alembic Static Mesh"
|
||||
representations = ["abc"]
|
||||
icon = "cube"
|
||||
|
|
@ -422,7 +422,7 @@ class TemplateResult(str):
|
|||
|
||||
cls = self.__class__
|
||||
return cls(
|
||||
os.path.normpath(self),
|
||||
os.path.normpath(self.replace("\\", "/")),
|
||||
self.template,
|
||||
self.solved,
|
||||
self.used_values,
|
||||
|
|
|
|||
|
|
@ -135,9 +135,9 @@ class FirstVersionStatus(BaseEvent):
|
|||
|
||||
new_status = asset_version_statuses.get(found_item["status"])
|
||||
if not new_status:
|
||||
self.log.warning(
|
||||
self.log.warning((
|
||||
"AssetVersion doesn't have status `{}`."
|
||||
).format(found_item["status"])
|
||||
).format(found_item["status"]))
|
||||
continue
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import os
|
||||
import threading
|
||||
|
||||
import gazu
|
||||
|
||||
from openpype.client import (
|
||||
get_project,
|
||||
get_assets,
|
||||
get_asset_by_name
|
||||
)
|
||||
from openpype.client import get_project, get_assets, get_asset_by_name
|
||||
from openpype.pipeline import AvalonMongoDB
|
||||
from .credentials import validate_credentials
|
||||
from .update_op_with_zou import (
|
||||
|
|
@ -397,6 +394,13 @@ def start_listeners(login: str, password: str):
|
|||
login (str): Kitsu user login
|
||||
password (str): Kitsu user password
|
||||
"""
|
||||
# Refresh token every week
|
||||
def refresh_token_every_week():
|
||||
print("Refreshing token...")
|
||||
gazu.refresh_token()
|
||||
threading.Timer(7 * 3600 * 24, refresh_token_every_week).start()
|
||||
|
||||
refresh_token_every_week()
|
||||
|
||||
# Connect to server
|
||||
listener = Listener(login, password)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.CollectorOrder + 0.495
|
||||
families = ["workfile",
|
||||
"pointcache",
|
||||
"proxyAbc",
|
||||
"camera",
|
||||
"animation",
|
||||
"model",
|
||||
|
|
@ -50,6 +51,7 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
|
|||
"source",
|
||||
"assembly",
|
||||
"fbx",
|
||||
"gltf",
|
||||
"textures",
|
||||
"action",
|
||||
"background",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.IntegratorOrder
|
||||
families = ["workfile",
|
||||
"pointcache",
|
||||
"proxyAbc",
|
||||
"camera",
|
||||
"animation",
|
||||
"model",
|
||||
|
|
@ -111,6 +112,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
"image",
|
||||
"assembly",
|
||||
"fbx",
|
||||
"gltf",
|
||||
"textures",
|
||||
"action",
|
||||
"harmony.template",
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.IntegratorOrder + 0.00001
|
||||
families = ["workfile",
|
||||
"pointcache",
|
||||
"proxyAbc",
|
||||
"camera",
|
||||
"animation",
|
||||
"model",
|
||||
|
|
@ -106,6 +107,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
"image",
|
||||
"assembly",
|
||||
"fbx",
|
||||
"gltf",
|
||||
"textures",
|
||||
"action",
|
||||
"harmony.template",
|
||||
|
|
|
|||
BIN
openpype/resources/app_icons/3dsmax.png
Normal file
BIN
openpype/resources/app_icons/3dsmax.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -149,6 +149,14 @@
|
|||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateProxyAlembic": {
|
||||
"enabled": true,
|
||||
"write_color_sets": false,
|
||||
"write_face_sets": false,
|
||||
"defaults": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateMultiverseUsd": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
|
|
@ -171,7 +179,21 @@
|
|||
"enabled": true,
|
||||
"defaults": [
|
||||
"Main"
|
||||
]
|
||||
],
|
||||
"expandProcedurals": false,
|
||||
"motionBlur": true,
|
||||
"motionBlurKeys": 2,
|
||||
"motionBlurLength": 0.5,
|
||||
"maskOptions": false,
|
||||
"maskCamera": false,
|
||||
"maskLight": false,
|
||||
"maskShape": false,
|
||||
"maskShader": false,
|
||||
"maskOverride": false,
|
||||
"maskDriver": false,
|
||||
"maskFilter": false,
|
||||
"maskColor_manager": false,
|
||||
"maskOperator": false
|
||||
},
|
||||
"CreateAssembly": {
|
||||
"enabled": true,
|
||||
|
|
@ -250,6 +272,9 @@
|
|||
"CollectFbxCamera": {
|
||||
"enabled": false
|
||||
},
|
||||
"CollectGLTF": {
|
||||
"enabled": false
|
||||
},
|
||||
"ValidateInstanceInContext": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
|
|
@ -569,6 +594,12 @@
|
|||
"optional": false,
|
||||
"active": true
|
||||
},
|
||||
"ExtractProxyAlembic": {
|
||||
"enabled": true,
|
||||
"families": [
|
||||
"proxyAbc"
|
||||
]
|
||||
},
|
||||
"ExtractAlembic": {
|
||||
"enabled": true,
|
||||
"families": [
|
||||
|
|
@ -915,7 +946,7 @@
|
|||
"current_context": [
|
||||
{
|
||||
"subset_name_filters": [
|
||||
"\".+[Mm]ain\""
|
||||
".+[Mm]ain"
|
||||
],
|
||||
"families": [
|
||||
"model"
|
||||
|
|
@ -932,7 +963,8 @@
|
|||
"subset_name_filters": [],
|
||||
"families": [
|
||||
"animation",
|
||||
"pointcache"
|
||||
"pointcache",
|
||||
"proxyAbc"
|
||||
],
|
||||
"repre_names": [
|
||||
"abc"
|
||||
|
|
@ -1007,4 +1039,4 @@
|
|||
"ValidateNoAnimation": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"3dsmax": {
|
||||
"enabled": true,
|
||||
"label": "3ds max",
|
||||
"icon": "{}/app_icons/3dsmax.png",
|
||||
"host_name": "max",
|
||||
"environment": {
|
||||
"ADSK_3DSMAX_STARTUPSCRIPTS_ADDON_DIR": "{OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup"
|
||||
},
|
||||
"variants": {
|
||||
"2023": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\3ds Max 2023\\3dsmax.exe"
|
||||
],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"environment": {
|
||||
"3DSMAX_VERSION": "2023"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"flame": {
|
||||
"enabled": true,
|
||||
"label": "Flame",
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ class HostsEnumEntity(BaseEnumEntity):
|
|||
|
||||
schema_types = ["hosts-enum"]
|
||||
all_host_names = [
|
||||
"max",
|
||||
"aftereffects",
|
||||
"blender",
|
||||
"celaction",
|
||||
|
|
|
|||
|
|
@ -200,7 +200,128 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "CreateProxyAlembic",
|
||||
"label": "Create Proxy Alembic",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "write_color_sets",
|
||||
"label": "Write Color Sets"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "write_face_sets",
|
||||
"label": "Write Face Sets"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "defaults",
|
||||
"label": "Default Subsets",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "CreateAss",
|
||||
"label": "Create Ass",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "defaults",
|
||||
"label": "Default Subsets",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "expandProcedurals",
|
||||
"label": "Expand Procedurals"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "motionBlur",
|
||||
"label": "Motion Blur"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "motionBlurKeys",
|
||||
"label": "Motion Blur Keys",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "motionBlurLength",
|
||||
"label": "Motion Blur Length",
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "maskOptions",
|
||||
"label": "Mask Options"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "maskCamera",
|
||||
"label": "Mask Camera"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "maskLight",
|
||||
"label": "Mask Light"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "maskShape",
|
||||
"label": "Mask Shape"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "maskShader",
|
||||
"label": "Mask Shader"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "maskOverride",
|
||||
"label": "Mask Override"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "maskDriver",
|
||||
"label": "Mask Driver"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "maskFilter",
|
||||
"label": "Mask Filter"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "maskColor_manager",
|
||||
"label": "Mask Color Manager"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "maskOperator",
|
||||
"label": "Mask Operator"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_create_plugin",
|
||||
|
|
@ -217,10 +338,6 @@
|
|||
"key": "CreateMultiverseUsdOver",
|
||||
"label": "Create Multiverse USD Override"
|
||||
},
|
||||
{
|
||||
"key": "CreateAss",
|
||||
"label": "Create Ass"
|
||||
},
|
||||
{
|
||||
"key": "CreateAssembly",
|
||||
"label": "Create Assembly"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,20 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "CollectGLTF",
|
||||
"label": "Collect Assets for GLTF/GLB export",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
|
|
@ -62,7 +76,7 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "ValidateFrameRange",
|
||||
|
|
@ -638,6 +652,26 @@
|
|||
"type": "label",
|
||||
"label": "Extractors"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "ExtractProxyAlembic",
|
||||
"label": "Extract Proxy Alembic",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"key": "families",
|
||||
"label": "Families",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
{"nukenodes": "nukenodes"},
|
||||
{"plate": "plate"},
|
||||
{"pointcache": "pointcache"},
|
||||
{"proxyAbc": "proxyAbc"},
|
||||
{"prerender": "prerender"},
|
||||
{"redshiftproxy": "redshiftproxy"},
|
||||
{"reference": "reference"},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"key": "3dsmax",
|
||||
"label": "Autodesk 3ds Max",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_host_unchangables"
|
||||
},
|
||||
{
|
||||
"key": "environment",
|
||||
"label": "Environment",
|
||||
"type": "raw-json"
|
||||
},
|
||||
{
|
||||
"type": "dict-modifiable",
|
||||
"key": "variants",
|
||||
"collapsible_key": true,
|
||||
"use_label_wrap": false,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_host_variant_items"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -9,6 +9,10 @@
|
|||
"type": "schema",
|
||||
"name": "schema_maya"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_3dsmax"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_flame"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ def main(user_role=None):
|
|||
user_role, ", ".join(allowed_roles)
|
||||
))
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
app = QtWidgets.QApplication.instance()
|
||||
if not app:
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
app.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
|
||||
|
||||
widget = MainWidget(user_role)
|
||||
|
|
|
|||
|
|
@ -186,19 +186,11 @@ class FamilyWidget(QtWidgets.QWidget):
|
|||
if item is None:
|
||||
return
|
||||
|
||||
asset_doc = None
|
||||
if asset_name != self.NOT_SELECTED:
|
||||
# Get the assets from the database which match with the name
|
||||
project_name = self.dbcon.active_project()
|
||||
asset_doc = get_asset_by_name(
|
||||
project_name, asset_name, fields=["_id"]
|
||||
)
|
||||
|
||||
# Get plugin and family
|
||||
plugin = item.data(PluginRole)
|
||||
|
||||
# Early exit if no asset name
|
||||
if not asset_name.strip():
|
||||
if (
|
||||
asset_name == self.NOT_SELECTED
|
||||
or not asset_name.strip()
|
||||
):
|
||||
self._build_menu([])
|
||||
item.setData(ExistsRole, False)
|
||||
print("Asset name is required ..")
|
||||
|
|
@ -210,8 +202,10 @@ class FamilyWidget(QtWidgets.QWidget):
|
|||
asset_doc = get_asset_by_name(
|
||||
project_name, asset_name, fields=["_id"]
|
||||
)
|
||||
|
||||
# Get plugin
|
||||
plugin = item.data(PluginRole)
|
||||
|
||||
if asset_doc and plugin:
|
||||
asset_id = asset_doc["_id"]
|
||||
task_name = self.dbcon.Session["AVALON_TASK"]
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ Click = "^7"
|
|||
dnspython = "^2.1.0"
|
||||
ftrack-python-api = "^2.3.3"
|
||||
shotgun_api3 = {git = "https://github.com/shotgunsoftware/python-api.git", rev = "v3.3.3"}
|
||||
gazu = "^0.8.28"
|
||||
gazu = "^0.8.32"
|
||||
google-api-python-client = "^1.12.8" # sync server google support (should be separate?)
|
||||
jsonschema = "^2.6.0"
|
||||
keyring = "^22.0.1"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ openpype_console module kitsu sync-service -l me@domain.ext -p my_password
|
|||
### Events listening
|
||||
Listening to Kitsu events is the key to automation of many tasks like _project/episode/sequence/shot/asset/task create/update/delete_ and some more. Events listening should run at all times to perform the required processing as it is not possible to catch some of them retrospectively with strong reliability. If such timeout has been encountered, you must relaunch the `sync-service` command to run the synchronization step again.
|
||||
|
||||
Connection token is refreshed every week.
|
||||
|
||||
### Push to Kitsu
|
||||
An utility function is provided to help update Kitsu data (a.k.a Zou database) with OpenPype data if the publishing to the production tracker hasn't been possible for some time. Running `push-to-zou` will create the data on behalf of the user.
|
||||
:::caution
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue