mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge branch 'develop' into bugfix/maya-cmds-mel-imports
This commit is contained in:
commit
c9a494876f
248 changed files with 4769 additions and 2620 deletions
|
|
@ -1,6 +1,7 @@
|
|||
from .mongo import (
|
||||
OpenPypeMongoConnection,
|
||||
)
|
||||
from .server.utils import get_ayon_server_api_connection
|
||||
|
||||
from .entities import (
|
||||
get_projects,
|
||||
|
|
@ -43,6 +44,8 @@ from .entities import (
|
|||
get_thumbnail_id_from_source,
|
||||
|
||||
get_workfile_info,
|
||||
|
||||
get_asset_name_identifier,
|
||||
)
|
||||
|
||||
from .entity_links import (
|
||||
|
|
@ -59,6 +62,8 @@ from .operations import (
|
|||
__all__ = (
|
||||
"OpenPypeMongoConnection",
|
||||
|
||||
"get_ayon_server_api_connection",
|
||||
|
||||
"get_projects",
|
||||
"get_project",
|
||||
"get_whole_project",
|
||||
|
|
@ -105,4 +110,6 @@ __all__ = (
|
|||
"get_linked_representation_id",
|
||||
|
||||
"create_project",
|
||||
|
||||
"get_asset_name_identifier",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,3 +4,22 @@ if not AYON_SERVER_ENABLED:
|
|||
from .mongo.entities import *
|
||||
else:
|
||||
from .server.entities import *
|
||||
|
||||
|
||||
def get_asset_name_identifier(asset_doc):
|
||||
"""Get asset name identifier by asset document.
|
||||
|
||||
This function is added because of AYON implementation where name
|
||||
identifier is not just a name but full path.
|
||||
|
||||
Asset document must have "name" key, and "data.parents" when in AYON mode.
|
||||
|
||||
Args:
|
||||
asset_doc (dict[str, Any]): Asset document.
|
||||
"""
|
||||
|
||||
if not AYON_SERVER_ENABLED:
|
||||
return asset_doc["name"]
|
||||
parents = list(asset_doc["data"]["parents"])
|
||||
parents.append(asset_doc["name"])
|
||||
return "/" + "/".join(parents)
|
||||
|
|
|
|||
|
|
@ -138,16 +138,22 @@ def _template_replacements_to_v3(template):
|
|||
)
|
||||
|
||||
|
||||
def _convert_template_item(template):
|
||||
# Others won't have 'directory'
|
||||
if "directory" not in template:
|
||||
return
|
||||
folder = _template_replacements_to_v3(template.pop("directory"))
|
||||
template["folder"] = folder
|
||||
template["file"] = _template_replacements_to_v3(template["file"])
|
||||
template["path"] = "/".join(
|
||||
(folder, template["file"])
|
||||
)
|
||||
def _convert_template_item(template_item):
|
||||
for key, value in tuple(template_item.items()):
|
||||
template_item[key] = _template_replacements_to_v3(value)
|
||||
|
||||
# Change 'directory' to 'folder'
|
||||
if "directory" in template_item:
|
||||
template_item["folder"] = template_item.pop("directory")
|
||||
|
||||
if (
|
||||
"path" not in template_item
|
||||
and "file" in template_item
|
||||
and "folder" in template_item
|
||||
):
|
||||
template_item["path"] = "/".join(
|
||||
(template_item["folder"], template_item["file"])
|
||||
)
|
||||
|
||||
|
||||
def _fill_template_category(templates, cat_templates, cat_key):
|
||||
|
|
@ -212,10 +218,27 @@ def convert_v4_project_to_v3(project):
|
|||
_convert_template_item(template)
|
||||
new_others_templates[name] = template
|
||||
|
||||
staging_templates = templates.pop("staging", None)
|
||||
# Key 'staging_directories' is legacy key that changed
|
||||
# to 'staging_dir'
|
||||
_legacy_staging_templates = templates.pop("staging_directories", None)
|
||||
if staging_templates is None:
|
||||
staging_templates = _legacy_staging_templates
|
||||
|
||||
if staging_templates is None:
|
||||
staging_templates = {}
|
||||
|
||||
# Prefix all staging template names with 'staging_' prefix
|
||||
# and add them to 'others'
|
||||
for name, template in staging_templates.items():
|
||||
_convert_template_item(template)
|
||||
new_name = "staging_{}".format(name)
|
||||
new_others_templates[new_name] = template
|
||||
|
||||
for key in (
|
||||
"work",
|
||||
"publish",
|
||||
"hero"
|
||||
"hero",
|
||||
):
|
||||
cat_templates = templates.pop(key)
|
||||
_fill_template_category(templates, cat_templates, key)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import collections
|
||||
|
||||
from ayon_api import get_server_api_connection
|
||||
|
||||
from openpype.client.mongo.operations import CURRENT_THUMBNAIL_SCHEMA
|
||||
|
||||
from .utils import get_ayon_server_api_connection
|
||||
from .openpype_comp import get_folders_with_tasks
|
||||
from .conversion_utils import (
|
||||
project_fields_v3_to_v4,
|
||||
|
|
@ -37,7 +36,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None):
|
|||
elif inactive:
|
||||
active = False
|
||||
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
fields = project_fields_v3_to_v4(fields, con)
|
||||
for project in con.get_projects(active, library, fields=fields):
|
||||
yield convert_v4_project_to_v3(project)
|
||||
|
|
@ -45,7 +44,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None):
|
|||
|
||||
def get_project(project_name, active=True, inactive=False, fields=None):
|
||||
# Skip if both are disabled
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
fields = project_fields_v3_to_v4(fields, con)
|
||||
return convert_v4_project_to_v3(
|
||||
con.get_project(project_name, fields=fields)
|
||||
|
|
@ -66,7 +65,7 @@ def _get_subsets(
|
|||
fields=None
|
||||
):
|
||||
# Convert fields and add minimum required fields
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
fields = subset_fields_v3_to_v4(fields, con)
|
||||
if fields is not None:
|
||||
for key in (
|
||||
|
|
@ -102,7 +101,7 @@ def _get_versions(
|
|||
active=None,
|
||||
fields=None
|
||||
):
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
|
||||
fields = version_fields_v3_to_v4(fields, con)
|
||||
|
||||
|
|
@ -183,6 +182,19 @@ def get_asset_by_name(project_name, asset_name, fields=None):
|
|||
return None
|
||||
|
||||
|
||||
def _folders_query(project_name, con, fields, **kwargs):
|
||||
if fields is None or "tasks" in fields:
|
||||
folders = get_folders_with_tasks(
|
||||
con, project_name, fields=fields, **kwargs
|
||||
)
|
||||
|
||||
else:
|
||||
folders = con.get_folders(project_name, fields=fields, **kwargs)
|
||||
|
||||
for folder in folders:
|
||||
yield folder
|
||||
|
||||
|
||||
def get_assets(
|
||||
project_name,
|
||||
asset_ids=None,
|
||||
|
|
@ -198,24 +210,43 @@ def get_assets(
|
|||
if archived:
|
||||
active = None
|
||||
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
fields = folder_fields_v3_to_v4(fields, con)
|
||||
kwargs = dict(
|
||||
folder_ids=asset_ids,
|
||||
folder_names=asset_names,
|
||||
parent_ids=parent_ids,
|
||||
active=active,
|
||||
fields=fields
|
||||
)
|
||||
if not asset_names:
|
||||
for folder in _folders_query(project_name, con, fields, **kwargs):
|
||||
yield convert_v4_folder_to_v3(folder, project_name)
|
||||
return
|
||||
|
||||
if fields is None or "tasks" in fields:
|
||||
folders = get_folders_with_tasks(con, project_name, **kwargs)
|
||||
new_asset_names = set()
|
||||
folder_paths = set()
|
||||
for name in asset_names:
|
||||
if "/" in name:
|
||||
folder_paths.add(name)
|
||||
else:
|
||||
new_asset_names.add(name)
|
||||
|
||||
else:
|
||||
folders = con.get_folders(project_name, **kwargs)
|
||||
yielded_ids = set()
|
||||
if folder_paths:
|
||||
for folder in _folders_query(
|
||||
project_name, con, fields, folder_paths=folder_paths, **kwargs
|
||||
):
|
||||
yielded_ids.add(folder["id"])
|
||||
yield convert_v4_folder_to_v3(folder, project_name)
|
||||
|
||||
for folder in folders:
|
||||
yield convert_v4_folder_to_v3(folder, project_name)
|
||||
if not new_asset_names:
|
||||
return
|
||||
|
||||
for folder in _folders_query(
|
||||
project_name, con, fields, folder_names=new_asset_names, **kwargs
|
||||
):
|
||||
if folder["id"] not in yielded_ids:
|
||||
yielded_ids.add(folder["id"])
|
||||
yield convert_v4_folder_to_v3(folder, project_name)
|
||||
|
||||
|
||||
def get_archived_assets(
|
||||
|
|
@ -236,7 +267,7 @@ def get_archived_assets(
|
|||
|
||||
|
||||
def get_asset_ids_with_subsets(project_name, asset_ids=None):
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
return con.get_folder_ids_with_products(project_name, asset_ids)
|
||||
|
||||
|
||||
|
|
@ -282,7 +313,7 @@ def get_subsets(
|
|||
|
||||
|
||||
def get_subset_families(project_name, subset_ids=None):
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
return con.get_product_type_names(project_name, subset_ids)
|
||||
|
||||
|
||||
|
|
@ -430,7 +461,7 @@ def get_output_link_versions(project_name, version_id, fields=None):
|
|||
if not version_id:
|
||||
return []
|
||||
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
version_links = con.get_version_links(
|
||||
project_name, version_id, link_direction="out")
|
||||
|
||||
|
|
@ -446,7 +477,7 @@ def get_output_link_versions(project_name, version_id, fields=None):
|
|||
|
||||
|
||||
def version_is_latest(project_name, version_id):
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
return con.version_is_latest(project_name, version_id)
|
||||
|
||||
|
||||
|
|
@ -501,7 +532,7 @@ def get_representations(
|
|||
else:
|
||||
active = None
|
||||
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
fields = representation_fields_v3_to_v4(fields, con)
|
||||
if fields and active is not None:
|
||||
fields.add("active")
|
||||
|
|
@ -535,7 +566,7 @@ def get_representations_parents(project_name, representations):
|
|||
repre["_id"]
|
||||
for repre in representations
|
||||
}
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
parents_by_repre_id = con.get_representations_parents(project_name,
|
||||
repre_ids)
|
||||
folder_ids = set()
|
||||
|
|
@ -677,7 +708,7 @@ def get_workfile_info(
|
|||
if not asset_id or not task_name or not filename:
|
||||
return None
|
||||
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
task = con.get_task_by_name(
|
||||
project_name, asset_id, task_name, fields=["id", "name", "folderId"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import ayon_api
|
||||
from ayon_api import get_folder_links, get_versions_links
|
||||
|
||||
from .utils import get_ayon_server_api_connection
|
||||
from .entities import get_assets, get_representation_by_id
|
||||
|
||||
|
||||
|
|
@ -28,7 +26,8 @@ def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None):
|
|||
if not asset_id:
|
||||
asset_id = asset_doc["_id"]
|
||||
|
||||
links = get_folder_links(project_name, asset_id, link_direction="in")
|
||||
con = get_ayon_server_api_connection()
|
||||
links = con.get_folder_links(project_name, asset_id, link_direction="in")
|
||||
return [
|
||||
link["entityId"]
|
||||
for link in links
|
||||
|
|
@ -115,6 +114,7 @@ def get_linked_representation_id(
|
|||
if link_type:
|
||||
link_types = [link_type]
|
||||
|
||||
con = get_ayon_server_api_connection()
|
||||
# Store already found version ids to avoid recursion, and also to store
|
||||
# output -> Don't forget to remove 'version_id' at the end!!!
|
||||
linked_version_ids = {version_id}
|
||||
|
|
@ -124,7 +124,7 @@ def get_linked_representation_id(
|
|||
if not versions_to_check:
|
||||
break
|
||||
|
||||
links = get_versions_links(
|
||||
links = con.get_versions_links(
|
||||
project_name,
|
||||
versions_to_check,
|
||||
link_types=link_types,
|
||||
|
|
@ -145,8 +145,8 @@ def get_linked_representation_id(
|
|||
linked_version_ids.remove(version_id)
|
||||
if not linked_version_ids:
|
||||
return []
|
||||
|
||||
representations = ayon_api.get_representations(
|
||||
con = get_ayon_server_api_connection()
|
||||
representations = con.get_representations(
|
||||
project_name,
|
||||
version_ids=linked_version_ids,
|
||||
fields=["id"])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import collections
|
||||
import json
|
||||
|
||||
import six
|
||||
from ayon_api.graphql import GraphQlQuery, FIELD_VALUE, fields_to_dict
|
||||
|
||||
from .constants import DEFAULT_FOLDER_FIELDS
|
||||
|
|
@ -84,12 +87,12 @@ def get_folders_with_tasks(
|
|||
for folder. All possible folder fields are returned if 'None'
|
||||
is passed.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Queried folder entities.
|
||||
Yields:
|
||||
Dict[str, Any]: Queried folder entities.
|
||||
"""
|
||||
|
||||
if not project_name:
|
||||
return []
|
||||
return
|
||||
|
||||
filters = {
|
||||
"projectName": project_name
|
||||
|
|
@ -97,25 +100,25 @@ def get_folders_with_tasks(
|
|||
if folder_ids is not None:
|
||||
folder_ids = set(folder_ids)
|
||||
if not folder_ids:
|
||||
return []
|
||||
return
|
||||
filters["folderIds"] = list(folder_ids)
|
||||
|
||||
if folder_paths is not None:
|
||||
folder_paths = set(folder_paths)
|
||||
if not folder_paths:
|
||||
return []
|
||||
return
|
||||
filters["folderPaths"] = list(folder_paths)
|
||||
|
||||
if folder_names is not None:
|
||||
folder_names = set(folder_names)
|
||||
if not folder_names:
|
||||
return []
|
||||
return
|
||||
filters["folderNames"] = list(folder_names)
|
||||
|
||||
if parent_ids is not None:
|
||||
parent_ids = set(parent_ids)
|
||||
if not parent_ids:
|
||||
return []
|
||||
return
|
||||
if None in parent_ids:
|
||||
# Replace 'None' with '"root"' which is used during GraphQl
|
||||
# query for parent ids filter for folders without folder
|
||||
|
|
@ -147,10 +150,10 @@ def get_folders_with_tasks(
|
|||
|
||||
parsed_data = query.query(con)
|
||||
folders = parsed_data["project"]["folders"]
|
||||
if active is None:
|
||||
return folders
|
||||
return [
|
||||
folder
|
||||
for folder in folders
|
||||
if folder["active"] is active
|
||||
]
|
||||
for folder in folders:
|
||||
if active is not None and folder["active"] is not active:
|
||||
continue
|
||||
folder_data = folder.get("data")
|
||||
if isinstance(folder_data, six.string_types):
|
||||
folder["data"] = json.loads(folder_data)
|
||||
yield folder
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import uuid
|
|||
import datetime
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
from ayon_api import get_server_api_connection
|
||||
|
||||
from openpype.client.operations_base import (
|
||||
REMOVED_VALUE,
|
||||
|
|
@ -41,7 +40,7 @@ from .conversion_utils import (
|
|||
convert_update_representation_to_v4,
|
||||
convert_update_workfile_info_to_v4,
|
||||
)
|
||||
from .utils import create_entity_id
|
||||
from .utils import create_entity_id, get_ayon_server_api_connection
|
||||
|
||||
|
||||
def _create_or_convert_to_id(entity_id=None):
|
||||
|
|
@ -680,7 +679,7 @@ class OperationsSession(BaseOperationsSession):
|
|||
def __init__(self, con=None, *args, **kwargs):
|
||||
super(OperationsSession, self).__init__(*args, **kwargs)
|
||||
if con is None:
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
self._con = con
|
||||
self._project_cache = {}
|
||||
self._nested_operations = collections.defaultdict(list)
|
||||
|
|
@ -858,7 +857,7 @@ def create_project(
|
|||
"""
|
||||
|
||||
if con is None:
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
|
||||
return con.create_project(
|
||||
project_name,
|
||||
|
|
@ -870,12 +869,12 @@ def create_project(
|
|||
|
||||
def delete_project(project_name, con=None):
|
||||
if con is None:
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
|
||||
return con.delete_project(project_name)
|
||||
|
||||
|
||||
def create_thumbnail(project_name, src_filepath, thumbnail_id=None, con=None):
|
||||
if con is None:
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
return con.create_thumbnail(project_name, src_filepath, thumbnail_id)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,33 @@
|
|||
import os
|
||||
import uuid
|
||||
|
||||
import ayon_api
|
||||
|
||||
from openpype.client.operations_base import REMOVED_VALUE
|
||||
|
||||
|
||||
class _GlobalCache:
|
||||
initialized = False
|
||||
|
||||
|
||||
def get_ayon_server_api_connection():
|
||||
if _GlobalCache.initialized:
|
||||
con = ayon_api.get_server_api_connection()
|
||||
else:
|
||||
from openpype.lib.local_settings import get_local_site_id
|
||||
|
||||
_GlobalCache.initialized = True
|
||||
site_id = get_local_site_id()
|
||||
version = os.getenv("AYON_VERSION")
|
||||
if ayon_api.is_connection_created():
|
||||
con = ayon_api.get_server_api_connection()
|
||||
con.set_site_id(site_id)
|
||||
con.set_client_version(version)
|
||||
else:
|
||||
con = ayon_api.create_connection(site_id, version)
|
||||
return con
|
||||
|
||||
|
||||
def create_entity_id():
|
||||
return uuid.uuid1().hex
|
||||
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ class HostBase(object):
|
|||
if project_name:
|
||||
items.append(project_name)
|
||||
if asset_name:
|
||||
items.append(asset_name)
|
||||
items.append(asset_name.lstrip("/"))
|
||||
if task_name:
|
||||
items.append(task_name)
|
||||
if items:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from openpype import AYON_SERVER_ENABLED
|
||||
import openpype.hosts.aftereffects.api as api
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline import (
|
||||
|
|
@ -43,6 +44,14 @@ class AEWorkfileCreator(AutoCreator):
|
|||
task_name = context.get_current_task_name()
|
||||
host_name = context.host_name
|
||||
|
||||
existing_asset_name = None
|
||||
if existing_instance is not None:
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_asset_name = existing_instance.get("folderPath")
|
||||
|
||||
if existing_asset_name is None:
|
||||
existing_asset_name = existing_instance["asset"]
|
||||
|
||||
if existing_instance is None:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
|
|
@ -50,10 +59,13 @@ class AEWorkfileCreator(AutoCreator):
|
|||
project_name, host_name
|
||||
)
|
||||
data = {
|
||||
"asset": asset_name,
|
||||
"task": task_name,
|
||||
"variant": self.default_variant
|
||||
}
|
||||
if AYON_SERVER_ENABLED:
|
||||
data["folderPath"] = asset_name
|
||||
else:
|
||||
data["asset"] = asset_name
|
||||
data.update(self.get_dynamic_data(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name, None
|
||||
|
|
@ -68,7 +80,7 @@ class AEWorkfileCreator(AutoCreator):
|
|||
new_instance.data_to_store())
|
||||
|
||||
elif (
|
||||
existing_instance["asset"] != asset_name
|
||||
existing_asset_name != asset_name
|
||||
or existing_instance["task"] != task_name
|
||||
):
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
|
|
@ -76,6 +88,10 @@ class AEWorkfileCreator(AutoCreator):
|
|||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name
|
||||
)
|
||||
existing_instance["asset"] = asset_name
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_instance["folderPath"] = asset_name
|
||||
else:
|
||||
existing_instance["asset"] = asset_name
|
||||
|
||||
existing_instance["task"] = task_name
|
||||
existing_instance["subset"] = subset_name
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.client import get_asset_name_identifier
|
||||
from openpype.pipeline.create import get_subset_name
|
||||
|
||||
|
||||
|
|
@ -48,9 +50,11 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
|
|||
asset_entity = context.data["assetEntity"]
|
||||
project_entity = context.data["projectEntity"]
|
||||
|
||||
asset_name = get_asset_name_identifier(asset_entity)
|
||||
|
||||
instance_data = {
|
||||
"active": True,
|
||||
"asset": asset_entity["name"],
|
||||
"asset": asset_name,
|
||||
"task": task,
|
||||
"frameStart": context.data['frameStart'],
|
||||
"frameEnd": context.data['frameEnd'],
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from .pipeline import (
|
|||
ls,
|
||||
publish,
|
||||
containerise,
|
||||
BlenderHost,
|
||||
)
|
||||
|
||||
from .plugin import (
|
||||
|
|
@ -47,6 +48,7 @@ __all__ = [
|
|||
"ls",
|
||||
"publish",
|
||||
"containerise",
|
||||
"BlenderHost",
|
||||
|
||||
"Creator",
|
||||
"Loader",
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ def imprint(node: bpy.types.bpy_struct_meta_idprop, data: Dict):
|
|||
# Support values evaluated at imprint
|
||||
value = value()
|
||||
|
||||
if not isinstance(value, (int, float, bool, str, list)):
|
||||
if not isinstance(value, (int, float, bool, str, list, dict)):
|
||||
raise TypeError(f"Unsupported type: {type(value)}")
|
||||
|
||||
imprint_data[key] = value
|
||||
|
|
@ -266,9 +266,59 @@ def read(node: bpy.types.bpy_struct_meta_idprop):
|
|||
return data
|
||||
|
||||
|
||||
def get_selection() -> List[bpy.types.Object]:
|
||||
"""Return the selected objects from the current scene."""
|
||||
return [obj for obj in bpy.context.scene.objects if obj.select_get()]
|
||||
def get_selected_collections():
|
||||
"""
|
||||
Returns a list of the currently selected collections in the outliner.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the outliner cannot be found in the main Blender
|
||||
window.
|
||||
|
||||
Returns:
|
||||
list: A list of `bpy.types.Collection` objects that are currently
|
||||
selected in the outliner.
|
||||
"""
|
||||
window = bpy.context.window or bpy.context.window_manager.windows[0]
|
||||
|
||||
try:
|
||||
area = next(
|
||||
area for area in window.screen.areas
|
||||
if area.type == 'OUTLINER')
|
||||
region = next(
|
||||
region for region in area.regions
|
||||
if region.type == 'WINDOW')
|
||||
except StopIteration as e:
|
||||
raise RuntimeError("Could not find outliner. An outliner space "
|
||||
"must be in the main Blender window.") from e
|
||||
|
||||
with bpy.context.temp_override(
|
||||
window=window,
|
||||
area=area,
|
||||
region=region,
|
||||
screen=window.screen
|
||||
):
|
||||
ids = bpy.context.selected_ids
|
||||
|
||||
return [id for id in ids if isinstance(id, bpy.types.Collection)]
|
||||
|
||||
|
||||
def get_selection(include_collections: bool = False) -> List[bpy.types.Object]:
|
||||
"""
|
||||
Returns a list of selected objects in the current Blender scene.
|
||||
|
||||
Args:
|
||||
include_collections (bool, optional): Whether to include selected
|
||||
collections in the result. Defaults to False.
|
||||
|
||||
Returns:
|
||||
List[bpy.types.Object]: A list of selected objects.
|
||||
"""
|
||||
selection = [obj for obj in bpy.context.scene.objects if obj.select_get()]
|
||||
|
||||
if include_collections:
|
||||
selection.extend(get_selected_collections())
|
||||
|
||||
return selection
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
|
|||
|
|
@ -31,6 +31,14 @@ PREVIEW_COLLECTIONS: Dict = dict()
|
|||
TIMER_INTERVAL: float = 0.01 if platform.system() == "Windows" else 0.1
|
||||
|
||||
|
||||
def execute_function_in_main_thread(f):
|
||||
"""Decorator to move a function call into main thread items"""
|
||||
def wrapper(*args, **kwargs):
|
||||
mti = MainThreadItem(f, *args, **kwargs)
|
||||
execute_in_main_thread(mti)
|
||||
return wrapper
|
||||
|
||||
|
||||
class BlenderApplication(QtWidgets.QApplication):
|
||||
_instance = None
|
||||
blender_windows = {}
|
||||
|
|
@ -238,8 +246,24 @@ class LaunchQtApp(bpy.types.Operator):
|
|||
|
||||
self.before_window_show()
|
||||
|
||||
def pull_to_front(window):
|
||||
"""Pull window forward to screen.
|
||||
|
||||
If Window is minimized this will un-minimize, then it can be raised
|
||||
and activated to the front.
|
||||
"""
|
||||
window.setWindowState(
|
||||
(window.windowState() & ~QtCore.Qt.WindowMinimized) |
|
||||
QtCore.Qt.WindowActive
|
||||
)
|
||||
window.raise_()
|
||||
window.activateWindow()
|
||||
|
||||
if isinstance(self._window, ModuleType):
|
||||
self._window.show()
|
||||
pull_to_front(self._window)
|
||||
|
||||
# Pull window to the front
|
||||
window = None
|
||||
if hasattr(self._window, "window"):
|
||||
window = self._window.window
|
||||
|
|
@ -254,6 +278,7 @@ class LaunchQtApp(bpy.types.Operator):
|
|||
on_top_flags = origin_flags | QtCore.Qt.WindowStaysOnTopHint
|
||||
self._window.setWindowFlags(on_top_flags)
|
||||
self._window.show()
|
||||
pull_to_front(self._window)
|
||||
|
||||
# if on_top_flags != origin_flags:
|
||||
# self._window.setWindowFlags(origin_flags)
|
||||
|
|
@ -275,6 +300,10 @@ class LaunchCreator(LaunchQtApp):
|
|||
def before_window_show(self):
|
||||
self._window.refresh()
|
||||
|
||||
def execute(self, context):
|
||||
host_tools.show_publisher(tab="create")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LaunchLoader(LaunchQtApp):
|
||||
"""Launch Avalon Loader."""
|
||||
|
|
@ -299,7 +328,7 @@ class LaunchPublisher(LaunchQtApp):
|
|||
bl_label = "Publish..."
|
||||
|
||||
def execute(self, context):
|
||||
host_tools.show_publish()
|
||||
host_tools.show_publisher(tab="publish")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
|
@ -416,7 +445,6 @@ class TOPBAR_MT_avalon(bpy.types.Menu):
|
|||
layout.operator(SetResolution.bl_idname, text="Set Resolution")
|
||||
layout.separator()
|
||||
layout.operator(LaunchWorkFiles.bl_idname, text="Work Files...")
|
||||
# TODO (jasper): maybe add 'Reload Pipeline'
|
||||
|
||||
|
||||
def draw_avalon_menu(self, context):
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ from . import ops
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.host import (
|
||||
HostBase,
|
||||
IWorkfileHost,
|
||||
IPublishHost,
|
||||
ILoadHost
|
||||
)
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline import (
|
||||
schema,
|
||||
|
|
@ -29,6 +35,14 @@ from openpype.lib import (
|
|||
)
|
||||
import openpype.hosts.blender
|
||||
from openpype.settings import get_project_settings
|
||||
from .workio import (
|
||||
open_file,
|
||||
save_file,
|
||||
current_file,
|
||||
has_unsaved_changes,
|
||||
file_extensions,
|
||||
work_root,
|
||||
)
|
||||
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__))
|
||||
|
|
@ -47,6 +61,101 @@ IS_HEADLESS = bpy.app.background
|
|||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
class BlenderHost(HostBase, IWorkfileHost, IPublishHost, ILoadHost):
|
||||
name = "blender"
|
||||
|
||||
def install(self):
|
||||
"""Override install method from HostBase.
|
||||
Install Blender host functionality."""
|
||||
install()
|
||||
|
||||
def get_containers(self) -> Iterator:
|
||||
"""List containers from active Blender scene."""
|
||||
return ls()
|
||||
|
||||
def get_workfile_extensions(self) -> List[str]:
|
||||
"""Override get_workfile_extensions method from IWorkfileHost.
|
||||
Get workfile possible extensions.
|
||||
|
||||
Returns:
|
||||
List[str]: Workfile extensions.
|
||||
"""
|
||||
return file_extensions()
|
||||
|
||||
def save_workfile(self, dst_path: str = None):
|
||||
"""Override save_workfile method from IWorkfileHost.
|
||||
Save currently opened workfile.
|
||||
|
||||
Args:
|
||||
dst_path (str): Where the current scene should be saved. Or use
|
||||
current path if `None` is passed.
|
||||
"""
|
||||
save_file(dst_path if dst_path else bpy.data.filepath)
|
||||
|
||||
def open_workfile(self, filepath: str):
|
||||
"""Override open_workfile method from IWorkfileHost.
|
||||
Open workfile at specified filepath in the host.
|
||||
|
||||
Args:
|
||||
filepath (str): Path to workfile.
|
||||
"""
|
||||
open_file(filepath)
|
||||
|
||||
def get_current_workfile(self) -> str:
|
||||
"""Override get_current_workfile method from IWorkfileHost.
|
||||
Retrieve currently opened workfile path.
|
||||
|
||||
Returns:
|
||||
str: Path to currently opened workfile.
|
||||
"""
|
||||
return current_file()
|
||||
|
||||
def workfile_has_unsaved_changes(self) -> bool:
|
||||
"""Override wokfile_has_unsaved_changes method from IWorkfileHost.
|
||||
Returns True if opened workfile has no unsaved changes.
|
||||
|
||||
Returns:
|
||||
bool: True if scene is saved and False if it has unsaved
|
||||
modifications.
|
||||
"""
|
||||
return has_unsaved_changes()
|
||||
|
||||
def work_root(self, session) -> str:
|
||||
"""Override work_root method from IWorkfileHost.
|
||||
Modify workdir per host.
|
||||
|
||||
Args:
|
||||
session (dict): Session context data.
|
||||
|
||||
Returns:
|
||||
str: Path to new workdir.
|
||||
"""
|
||||
return work_root(session)
|
||||
|
||||
def get_context_data(self) -> dict:
|
||||
"""Override abstract method from IPublishHost.
|
||||
Get global data related to creation-publishing from workfile.
|
||||
|
||||
Returns:
|
||||
dict: Context data stored using 'update_context_data'.
|
||||
"""
|
||||
property = bpy.context.scene.get(AVALON_PROPERTY)
|
||||
if property:
|
||||
return property.to_dict()
|
||||
return {}
|
||||
|
||||
def update_context_data(self, data: dict, changes: dict):
|
||||
"""Override abstract method from IPublishHost.
|
||||
Store global context data to workfile.
|
||||
|
||||
Args:
|
||||
data (dict): New data as are.
|
||||
changes (dict): Only data that has been changed. Each value has
|
||||
tuple with '(<old>, <new>)' value.
|
||||
"""
|
||||
bpy.context.scene[AVALON_PROPERTY] = data
|
||||
|
||||
|
||||
def pype_excepthook_handler(*args):
|
||||
traceback.print_exception(*args)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,34 @@
|
|||
"""Shared functionality for pipeline plugins for Blender."""
|
||||
|
||||
import itertools
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
Creator,
|
||||
CreatedInstance,
|
||||
LoaderPlugin,
|
||||
)
|
||||
from .pipeline import AVALON_CONTAINERS
|
||||
from openpype.lib import BoolDef
|
||||
|
||||
from .pipeline import (
|
||||
AVALON_CONTAINERS,
|
||||
AVALON_INSTANCES,
|
||||
AVALON_PROPERTY,
|
||||
)
|
||||
from .ops import (
|
||||
MainThreadItem,
|
||||
execute_in_main_thread
|
||||
)
|
||||
from .lib import (
|
||||
imprint,
|
||||
get_selection
|
||||
)
|
||||
from .lib import imprint
|
||||
|
||||
VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"]
|
||||
|
||||
|
||||
def asset_name(
|
||||
def prepare_scene_name(
|
||||
asset: str, subset: str, namespace: Optional[str] = None
|
||||
) -> str:
|
||||
"""Return a consistent name for an asset."""
|
||||
|
|
@ -40,9 +46,16 @@ def get_unique_number(
|
|||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
return "01"
|
||||
asset_groups = avalon_container.all_objects
|
||||
|
||||
container_names = [c.name for c in asset_groups if c.type == 'EMPTY']
|
||||
# Check the names of both object and collection containers
|
||||
obj_asset_groups = avalon_container.objects
|
||||
obj_group_names = {
|
||||
c.name for c in obj_asset_groups
|
||||
if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)}
|
||||
coll_asset_groups = avalon_container.children
|
||||
coll_group_names = {
|
||||
c.name for c in coll_asset_groups
|
||||
if c.get(AVALON_PROPERTY)}
|
||||
container_names = obj_group_names.union(coll_group_names)
|
||||
count = 1
|
||||
name = f"{asset}_{count:0>2}_{subset}"
|
||||
while name in container_names:
|
||||
|
|
@ -134,20 +147,224 @@ def deselect_all():
|
|||
bpy.context.view_layer.objects.active = active
|
||||
|
||||
|
||||
class Creator(LegacyCreator):
|
||||
"""Base class for Creator plug-ins."""
|
||||
class BaseCreator(Creator):
|
||||
"""Base class for Blender Creator plug-ins."""
|
||||
defaults = ['Main']
|
||||
|
||||
def process(self):
|
||||
collection = bpy.data.collections.new(name=self.data["subset"])
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
imprint(collection, self.data)
|
||||
create_as_asset_group = False
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
for obj in get_selection():
|
||||
collection.objects.link(obj)
|
||||
@staticmethod
|
||||
def cache_subsets(shared_data):
|
||||
"""Cache instances for Creators shared data.
|
||||
|
||||
return collection
|
||||
Create `blender_cached_subsets` key when needed in shared data and
|
||||
fill it with all collected instances from the scene under its
|
||||
respective creator identifiers.
|
||||
|
||||
If legacy instances are detected in the scene, create
|
||||
`blender_cached_legacy_subsets` key and fill it with
|
||||
all legacy subsets from this family as a value. # key or value?
|
||||
|
||||
Args:
|
||||
shared_data(Dict[str, Any]): Shared data.
|
||||
|
||||
Return:
|
||||
Dict[str, Any]: Shared data with cached subsets.
|
||||
"""
|
||||
if not shared_data.get('blender_cached_subsets'):
|
||||
cache = {}
|
||||
cache_legacy = {}
|
||||
|
||||
avalon_instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
avalon_instance_objs = (
|
||||
avalon_instances.objects if avalon_instances else []
|
||||
)
|
||||
|
||||
for obj_or_col in itertools.chain(
|
||||
avalon_instance_objs,
|
||||
bpy.data.collections
|
||||
):
|
||||
avalon_prop = obj_or_col.get(AVALON_PROPERTY, {})
|
||||
if not avalon_prop:
|
||||
continue
|
||||
|
||||
if avalon_prop.get('id') != 'pyblish.avalon.instance':
|
||||
continue
|
||||
|
||||
creator_id = avalon_prop.get('creator_identifier')
|
||||
if creator_id:
|
||||
# Creator instance
|
||||
cache.setdefault(creator_id, []).append(obj_or_col)
|
||||
else:
|
||||
family = avalon_prop.get('family')
|
||||
if family:
|
||||
# Legacy creator instance
|
||||
cache_legacy.setdefault(family, []).append(obj_or_col)
|
||||
|
||||
shared_data["blender_cached_subsets"] = cache
|
||||
shared_data["blender_cached_legacy_subsets"] = cache_legacy
|
||||
|
||||
return shared_data
|
||||
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
"""Override abstract method from Creator.
|
||||
Create new instance and store it.
|
||||
|
||||
Args:
|
||||
subset_name(str): Subset name of created instance.
|
||||
instance_data(dict): Instance base data.
|
||||
pre_create_data(dict): Data based on pre creation attributes.
|
||||
Those may affect how creator works.
|
||||
"""
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create asset group
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset_name = instance_data["folderPath"]
|
||||
else:
|
||||
asset_name = instance_data["asset"]
|
||||
|
||||
name = prepare_scene_name(asset_name, subset_name)
|
||||
if self.create_as_asset_group:
|
||||
# Create instance as empty
|
||||
instance_node = bpy.data.objects.new(name=name, object_data=None)
|
||||
instance_node.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(instance_node)
|
||||
else:
|
||||
# Create instance collection
|
||||
instance_node = bpy.data.collections.new(name=name)
|
||||
instances.children.link(instance_node)
|
||||
|
||||
self.set_instance_data(subset_name, instance_data)
|
||||
|
||||
instance = CreatedInstance(
|
||||
self.family, subset_name, instance_data, self
|
||||
)
|
||||
instance.transient_data["instance_node"] = instance_node
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
imprint(instance_node, instance_data)
|
||||
|
||||
return instance_node
|
||||
|
||||
def collect_instances(self):
|
||||
"""Override abstract method from BaseCreator.
|
||||
Collect existing instances related to this creator plugin."""
|
||||
|
||||
# Cache subsets in shared data
|
||||
self.cache_subsets(self.collection_shared_data)
|
||||
|
||||
# Get cached subsets
|
||||
cached_subsets = self.collection_shared_data.get(
|
||||
"blender_cached_subsets"
|
||||
)
|
||||
if not cached_subsets:
|
||||
return
|
||||
|
||||
# Process only instances that were created by this creator
|
||||
for instance_node in cached_subsets.get(self.identifier, []):
|
||||
property = instance_node.get(AVALON_PROPERTY)
|
||||
# Create instance object from existing data
|
||||
instance = CreatedInstance.from_existing(
|
||||
instance_data=property.to_dict(),
|
||||
creator=self
|
||||
)
|
||||
instance.transient_data["instance_node"] = instance_node
|
||||
|
||||
# Add instance to create context
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
"""Override abstract method from BaseCreator.
|
||||
Store changes of existing instances so they can be recollected.
|
||||
|
||||
Args:
|
||||
update_list(List[UpdateData]): Changed instances
|
||||
and their changes, as a list of tuples.
|
||||
"""
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset_name_key = "folderPath"
|
||||
else:
|
||||
asset_name_key = "asset"
|
||||
|
||||
for created_instance, changes in update_list:
|
||||
data = created_instance.data_to_store()
|
||||
node = created_instance.transient_data["instance_node"]
|
||||
if not node:
|
||||
# We can't update if we don't know the node
|
||||
self.log.error(
|
||||
f"Unable to update instance {created_instance} "
|
||||
f"without instance node."
|
||||
)
|
||||
return
|
||||
|
||||
# Rename the instance node in the scene if subset or asset changed
|
||||
if (
|
||||
"subset" in changes.changed_keys
|
||||
or asset_name_key in changes.changed_keys
|
||||
):
|
||||
asset_name = data[asset_name_key]
|
||||
name = prepare_scene_name(
|
||||
asset=asset_name, subset=data["subset"]
|
||||
)
|
||||
node.name = name
|
||||
|
||||
imprint(node, data)
|
||||
|
||||
def remove_instances(self, instances: List[CreatedInstance]):
|
||||
|
||||
for instance in instances:
|
||||
node = instance.transient_data["instance_node"]
|
||||
|
||||
if isinstance(node, bpy.types.Collection):
|
||||
for children in node.children_recursive:
|
||||
if isinstance(children, bpy.types.Collection):
|
||||
bpy.data.collections.remove(children)
|
||||
else:
|
||||
bpy.data.objects.remove(children)
|
||||
|
||||
bpy.data.collections.remove(node)
|
||||
elif isinstance(node, bpy.types.Object):
|
||||
bpy.data.objects.remove(node)
|
||||
|
||||
self._remove_instance_from_context(instance)
|
||||
|
||||
def set_instance_data(
|
||||
self,
|
||||
subset_name: str,
|
||||
instance_data: dict
|
||||
):
|
||||
"""Fill instance data with required items.
|
||||
|
||||
Args:
|
||||
subset_name(str): Subset name of created instance.
|
||||
instance_data(dict): Instance base data.
|
||||
instance_node(bpy.types.ID): Instance node in blender scene.
|
||||
"""
|
||||
if not instance_data:
|
||||
instance_data = {}
|
||||
|
||||
instance_data.update(
|
||||
{
|
||||
"id": "pyblish.avalon.instance",
|
||||
"creator_identifier": self.identifier,
|
||||
"subset": subset_name,
|
||||
}
|
||||
)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
return [
|
||||
BoolDef("use_selection",
|
||||
label="Use selection",
|
||||
default=True)
|
||||
]
|
||||
|
||||
|
||||
class Loader(LoaderPlugin):
|
||||
|
|
@ -241,7 +458,7 @@ class AssetLoader(LoaderPlugin):
|
|||
namespace: Use pre-defined namespace
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
# TODO (jasper): make it possible to add the asset several times by
|
||||
# TODO: make it possible to add the asset several times by
|
||||
# just re-using the collection
|
||||
filepath = self.filepath_from_context(context)
|
||||
assert Path(filepath).exists(), f"{filepath} doesn't exist."
|
||||
|
|
@ -252,7 +469,7 @@ class AssetLoader(LoaderPlugin):
|
|||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
name = name or asset_name(
|
||||
name = name or prepare_scene_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
|
|
@ -281,7 +498,9 @@ class AssetLoader(LoaderPlugin):
|
|||
|
||||
# asset = context["asset"]["name"]
|
||||
# subset = context["subset"]["name"]
|
||||
# instance_name = asset_name(asset, subset, unique_number) + '_CON'
|
||||
# instance_name = prepare_scene_name(
|
||||
# asset, subset, unique_number
|
||||
# ) + '_CON'
|
||||
|
||||
# return self._get_instance_collection(instance_name, nodes)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from openpype.pipeline import install_host
|
||||
from openpype.hosts.blender import api
|
||||
from openpype.hosts.blender.api import BlenderHost
|
||||
|
||||
|
||||
def register():
|
||||
install_host(api)
|
||||
install_host(BlenderHost())
|
||||
|
||||
|
||||
def unregister():
|
||||
|
|
|
|||
78
openpype/hosts/blender/plugins/create/convert_legacy.py
Normal file
78
openpype/hosts/blender/plugins/create/convert_legacy.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Converter for legacy Houdini subsets."""
|
||||
from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin
|
||||
from openpype.hosts.blender.api.lib import imprint
|
||||
|
||||
|
||||
class BlenderLegacyConvertor(SubsetConvertorPlugin):
|
||||
"""Find and convert any legacy subsets in the scene.
|
||||
|
||||
This Converter will find all legacy subsets in the scene and will
|
||||
transform them to the current system. Since the old subsets doesn't
|
||||
retain any information about their original creators, the only mapping
|
||||
we can do is based on their families.
|
||||
|
||||
Its limitation is that you can have multiple creators creating subset
|
||||
of the same family and there is no way to handle it. This code should
|
||||
nevertheless cover all creators that came with OpenPype.
|
||||
|
||||
"""
|
||||
identifier = "io.openpype.creators.blender.legacy"
|
||||
family_to_id = {
|
||||
"action": "io.openpype.creators.blender.action",
|
||||
"camera": "io.openpype.creators.blender.camera",
|
||||
"animation": "io.openpype.creators.blender.animation",
|
||||
"blendScene": "io.openpype.creators.blender.blendscene",
|
||||
"layout": "io.openpype.creators.blender.layout",
|
||||
"model": "io.openpype.creators.blender.model",
|
||||
"pointcache": "io.openpype.creators.blender.pointcache",
|
||||
"render": "io.openpype.creators.blender.render",
|
||||
"review": "io.openpype.creators.blender.review",
|
||||
"rig": "io.openpype.creators.blender.rig",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BlenderLegacyConvertor, self).__init__(*args, **kwargs)
|
||||
self.legacy_subsets = {}
|
||||
|
||||
def find_instances(self):
|
||||
"""Find legacy subsets in the scene.
|
||||
|
||||
Legacy subsets are the ones that doesn't have `creator_identifier`
|
||||
parameter on them.
|
||||
|
||||
This is using cached entries done in
|
||||
:py:meth:`~BaseCreator.cache_subsets()`
|
||||
|
||||
"""
|
||||
self.legacy_subsets = self.collection_shared_data.get(
|
||||
"blender_cached_legacy_subsets")
|
||||
if not self.legacy_subsets:
|
||||
return
|
||||
self.add_convertor_item(
|
||||
"Found {} incompatible subset{}".format(
|
||||
len(self.legacy_subsets),
|
||||
"s" if len(self.legacy_subsets) > 1 else ""
|
||||
)
|
||||
)
|
||||
|
||||
def convert(self):
|
||||
"""Convert all legacy subsets to current.
|
||||
|
||||
It is enough to add `creator_identifier` and `instance_node`.
|
||||
|
||||
"""
|
||||
if not self.legacy_subsets:
|
||||
return
|
||||
|
||||
for family, instance_nodes in self.legacy_subsets.items():
|
||||
if family in self.family_to_id:
|
||||
for instance_node in instance_nodes:
|
||||
creator_identifier = self.family_to_id[family]
|
||||
self.log.info(
|
||||
"Converting {} to {}".format(instance_node.name,
|
||||
creator_identifier)
|
||||
)
|
||||
imprint(instance_node, data={
|
||||
"creator_identifier": creator_identifier
|
||||
})
|
||||
|
|
@ -2,30 +2,29 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from openpype.hosts.blender.api import lib
|
||||
from openpype.hosts.blender.api import lib, plugin
|
||||
|
||||
|
||||
class CreateAction(openpype.hosts.blender.api.plugin.Creator):
|
||||
"""Action output for character rigs"""
|
||||
class CreateAction(plugin.BaseCreator):
|
||||
"""Action output for character rigs."""
|
||||
|
||||
name = "actionMain"
|
||||
identifier = "io.openpype.creators.blender.action"
|
||||
label = "Action"
|
||||
family = "action"
|
||||
icon = "male"
|
||||
|
||||
def process(self):
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(collection, self.data)
|
||||
# Get instance name
|
||||
name = plugin.prepare_scene_name(instance_data["asset"], subset_name)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
for obj in lib.get_selection():
|
||||
if (obj.animation_data is not None
|
||||
and obj.animation_data.action is not None):
|
||||
|
|
|
|||
|
|
@ -1,51 +1,32 @@
|
|||
"""Create an animation asset."""
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateAnimation(plugin.Creator):
|
||||
"""Animation output for character rigs"""
|
||||
class CreateAnimation(plugin.BaseCreator):
|
||||
"""Animation output for character rigs."""
|
||||
|
||||
name = "animationMain"
|
||||
identifier = "io.openpype.creators.blender.animation"
|
||||
label = "Animation"
|
||||
family = "animation"
|
||||
icon = "male"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
# name = self.name
|
||||
# if not name:
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
# asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
# asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
asset_group = bpy.data.collections.new(name=name)
|
||||
instances.children.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
asset_group.objects.link(obj)
|
||||
elif (self.options or {}).get("asset_group"):
|
||||
obj = (self.options or {}).get("asset_group")
|
||||
asset_group.objects.link(obj)
|
||||
collection.objects.link(obj)
|
||||
elif pre_create_data.get("asset_group"):
|
||||
# Use for Load Blend automated creation of animation instances
|
||||
# upon loading rig files
|
||||
obj = pre_create_data.get("asset_group")
|
||||
collection.objects.link(obj)
|
||||
|
||||
return asset_group
|
||||
return collection
|
||||
|
|
|
|||
|
|
@ -2,50 +2,33 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateBlendScene(plugin.Creator):
|
||||
"""Generic group of assets"""
|
||||
class CreateBlendScene(plugin.BaseCreator):
|
||||
"""Generic group of assets."""
|
||||
|
||||
name = "blendScene"
|
||||
identifier = "io.openpype.creators.blender.blendscene"
|
||||
label = "Blender Scene"
|
||||
family = "blendScene"
|
||||
icon = "cubes"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
maintain_selection = False
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
instance_node = super().create(subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
if pre_create_data.get("use_selection"):
|
||||
selection = lib.get_selection(include_collections=True)
|
||||
for data in selection:
|
||||
if isinstance(data, bpy.types.Collection):
|
||||
instance_node.children.link(data)
|
||||
elif isinstance(data, bpy.types.Object):
|
||||
instance_node.objects.link(data)
|
||||
|
||||
return asset_group
|
||||
return instance_node
|
||||
|
|
|
|||
|
|
@ -2,62 +2,41 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
|
||||
|
||||
class CreateCamera(plugin.Creator):
|
||||
"""Polygonal static geometry"""
|
||||
class CreateCamera(plugin.BaseCreator):
|
||||
"""Polygonal static geometry."""
|
||||
|
||||
name = "cameraMain"
|
||||
identifier = "io.openpype.creators.blender.camera"
|
||||
label = "Camera"
|
||||
family = "camera"
|
||||
icon = "video-camera"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
create_as_asset_group = True
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = super().create(subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
print(f"self.data: {self.data}")
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
if pre_create_data.get("use_selection"):
|
||||
for obj in lib.get_selection():
|
||||
obj.parent = asset_group
|
||||
else:
|
||||
plugin.deselect_all()
|
||||
camera = bpy.data.cameras.new(subset)
|
||||
camera_obj = bpy.data.objects.new(subset, camera)
|
||||
camera = bpy.data.cameras.new(subset_name)
|
||||
camera_obj = bpy.data.objects.new(subset_name, camera)
|
||||
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
instances.objects.link(camera_obj)
|
||||
|
||||
camera_obj.select_set(True)
|
||||
asset_group.select_set(True)
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
camera_obj.parent = asset_group
|
||||
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -2,50 +2,31 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateLayout(plugin.Creator):
|
||||
"""Layout output for character rigs"""
|
||||
class CreateLayout(plugin.BaseCreator):
|
||||
"""Layout output for character rigs."""
|
||||
|
||||
name = "layoutMain"
|
||||
identifier = "io.openpype.creators.blender.layout"
|
||||
label = "Layout"
|
||||
family = "layout"
|
||||
icon = "cubes"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
create_as_asset_group = True
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
asset_group = super().create(subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
for obj in lib.get_selection():
|
||||
obj.parent = asset_group
|
||||
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -2,50 +2,30 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateModel(plugin.Creator):
|
||||
"""Polygonal static geometry"""
|
||||
class CreateModel(plugin.BaseCreator):
|
||||
"""Polygonal static geometry."""
|
||||
|
||||
name = "modelMain"
|
||||
identifier = "io.openpype.creators.blender.model"
|
||||
label = "Model"
|
||||
family = "model"
|
||||
icon = "cube"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
create_as_asset_group = True
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
asset_group = super().create(subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
for obj in lib.get_selection():
|
||||
obj.parent = asset_group
|
||||
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -1,51 +1,29 @@
|
|||
"""Create a pointcache asset."""
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreatePointcache(plugin.Creator):
|
||||
"""Polygonal static geometry"""
|
||||
class CreatePointcache(plugin.BaseCreator):
|
||||
"""Polygonal static geometry."""
|
||||
|
||||
name = "pointcacheMain"
|
||||
identifier = "io.openpype.creators.blender.pointcache"
|
||||
label = "Point Cache"
|
||||
family = "pointcache"
|
||||
icon = "gears"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
if pre_create_data.get("use_selection"):
|
||||
objects = lib.get_selection()
|
||||
for obj in objects:
|
||||
collection.objects.link(obj)
|
||||
if obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
|
||||
return asset_group
|
||||
return collection
|
||||
|
|
|
|||
|
|
@ -1,42 +1,31 @@
|
|||
"""Create render."""
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.render_lib import prepare_rendering
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
|
||||
|
||||
class CreateRenderlayer(plugin.Creator):
|
||||
"""Single baked camera"""
|
||||
class CreateRenderlayer(plugin.BaseCreator):
|
||||
"""Single baked camera."""
|
||||
|
||||
name = "renderingMain"
|
||||
identifier = "io.openpype.creators.blender.render"
|
||||
label = "Render"
|
||||
family = "render"
|
||||
icon = "eye"
|
||||
|
||||
def process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.collections.new(name=name)
|
||||
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
try:
|
||||
instances.children.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
prepare_rendering(asset_group)
|
||||
prepare_rendering(collection)
|
||||
except Exception:
|
||||
# Remove the instance if there was an error
|
||||
bpy.data.collections.remove(asset_group)
|
||||
bpy.data.collections.remove(collection)
|
||||
raise
|
||||
|
||||
# TODO: this is undesiderable, but it's the only way to be sure that
|
||||
|
|
@ -50,4 +39,4 @@ class CreateRenderlayer(plugin.Creator):
|
|||
# now it is to force the file to be saved.
|
||||
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
|
||||
|
||||
return asset_group
|
||||
return collection
|
||||
|
|
|
|||
|
|
@ -1,47 +1,27 @@
|
|||
"""Create review."""
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateReview(plugin.Creator):
|
||||
"""Single baked camera"""
|
||||
class CreateReview(plugin.BaseCreator):
|
||||
"""Single baked camera."""
|
||||
|
||||
name = "reviewDefault"
|
||||
identifier = "io.openpype.creators.blender.review"
|
||||
label = "Review"
|
||||
family = "review"
|
||||
icon = "video-camera"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.collections.new(name=name)
|
||||
instances.children.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
asset_group.objects.link(obj)
|
||||
elif (self.options or {}).get("asset_group"):
|
||||
obj = (self.options or {}).get("asset_group")
|
||||
asset_group.objects.link(obj)
|
||||
collection.objects.link(obj)
|
||||
|
||||
return asset_group
|
||||
return collection
|
||||
|
|
|
|||
|
|
@ -2,50 +2,30 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateRig(plugin.Creator):
|
||||
"""Artist-friendly rig with controls to direct motion"""
|
||||
class CreateRig(plugin.BaseCreator):
|
||||
"""Artist-friendly rig with controls to direct motion."""
|
||||
|
||||
name = "rigMain"
|
||||
identifier = "io.openpype.creators.blender.rig"
|
||||
label = "Rig"
|
||||
family = "rig"
|
||||
icon = "wheelchair"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
create_as_asset_group = True
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
asset_group = super().create(subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
for obj in lib.get_selection():
|
||||
obj.parent = asset_group
|
||||
|
||||
return asset_group
|
||||
|
|
|
|||
121
openpype/hosts/blender/plugins/create/create_workfile.py
Normal file
121
openpype/hosts/blender/plugins/create/create_workfile.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import bpy
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.pipeline import CreatedInstance, AutoCreator
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.hosts.blender.api.plugin import BaseCreator
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
AVALON_PROPERTY,
|
||||
AVALON_CONTAINERS
|
||||
)
|
||||
|
||||
|
||||
class CreateWorkfile(BaseCreator, AutoCreator):
|
||||
"""Workfile auto-creator.
|
||||
|
||||
The workfile instance stores its data on the `AVALON_CONTAINERS` collection
|
||||
as custom attributes, because unlike other instances it doesn't have an
|
||||
instance node of its own.
|
||||
|
||||
"""
|
||||
identifier = "io.openpype.creators.blender.workfile"
|
||||
label = "Workfile"
|
||||
family = "workfile"
|
||||
icon = "fa5.file"
|
||||
|
||||
def create(self):
|
||||
"""Create workfile instances."""
|
||||
existing_instance = next(
|
||||
(
|
||||
instance for instance in self.create_context.instances
|
||||
if instance.creator_identifier == self.identifier
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
project_name = self.project_name
|
||||
asset_name = self.create_context.get_current_asset_name()
|
||||
task_name = self.create_context.get_current_task_name()
|
||||
host_name = self.create_context.host_name
|
||||
|
||||
existing_asset_name = None
|
||||
if existing_instance is not None:
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_asset_name = existing_instance.get("folderPath")
|
||||
|
||||
if existing_asset_name is None:
|
||||
existing_asset_name = existing_instance["asset"]
|
||||
|
||||
if not existing_instance:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
task_name, task_name, asset_doc, project_name, host_name
|
||||
)
|
||||
data = {
|
||||
"task": task_name,
|
||||
"variant": task_name,
|
||||
}
|
||||
if AYON_SERVER_ENABLED:
|
||||
data["folderPath"] = asset_name
|
||||
else:
|
||||
data["asset"] = asset_name
|
||||
data.update(
|
||||
self.get_dynamic_data(
|
||||
task_name,
|
||||
task_name,
|
||||
asset_doc,
|
||||
project_name,
|
||||
host_name,
|
||||
existing_instance,
|
||||
)
|
||||
)
|
||||
self.log.info("Auto-creating workfile instance...")
|
||||
current_instance = CreatedInstance(
|
||||
self.family, subset_name, data, self
|
||||
)
|
||||
instance_node = bpy.data.collections.get(AVALON_CONTAINERS, {})
|
||||
current_instance.transient_data["instance_node"] = instance_node
|
||||
self._add_instance_to_context(current_instance)
|
||||
elif (
|
||||
existing_asset_name != asset_name
|
||||
or existing_instance["task"] != task_name
|
||||
):
|
||||
# Update instance context if it's different
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
task_name, task_name, asset_doc, project_name, host_name
|
||||
)
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_instance["folderPath"] = asset_name
|
||||
else:
|
||||
existing_instance["asset"] = asset_name
|
||||
|
||||
existing_instance["task"] = task_name
|
||||
existing_instance["subset"] = subset_name
|
||||
|
||||
def collect_instances(self):
|
||||
|
||||
instance_node = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not instance_node:
|
||||
return
|
||||
|
||||
property = instance_node.get(AVALON_PROPERTY)
|
||||
if not property:
|
||||
return
|
||||
|
||||
# Create instance object from existing data
|
||||
instance = CreatedInstance.from_existing(
|
||||
instance_data=property.to_dict(),
|
||||
creator=self
|
||||
)
|
||||
instance.transient_data["instance_node"] = instance_node
|
||||
|
||||
# Add instance to create context
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
for instance in instances:
|
||||
node = instance.transient_data["instance_node"]
|
||||
del node[AVALON_PROPERTY]
|
||||
|
||||
self._remove_instance_from_context(instance)
|
||||
|
|
@ -7,7 +7,7 @@ def append_workfile(context, fname, do_import):
|
|||
asset = context['asset']['name']
|
||||
subset = context['subset']['name']
|
||||
|
||||
group_name = plugin.asset_name(asset, subset)
|
||||
group_name = plugin.prepare_scene_name(asset, subset)
|
||||
|
||||
# We need to preserve the original names of the scenes, otherwise,
|
||||
# if there are duplicate names in the current workfile, the imported
|
||||
|
|
|
|||
|
|
@ -137,9 +137,9 @@ class CacheModelLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
containers = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from typing import Dict, List, Optional
|
|||
|
||||
import bpy
|
||||
from openpype.pipeline import get_representation_path
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
containerise_existing,
|
||||
AVALON_PROPERTY,
|
||||
|
|
@ -16,7 +16,7 @@ from openpype.hosts.blender.api.pipeline import (
|
|||
logger = logging.getLogger("openpype").getChild("blender").getChild("load_action")
|
||||
|
||||
|
||||
class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
||||
class BlendActionLoader(plugin.AssetLoader):
|
||||
"""Load action from a .blend file.
|
||||
|
||||
Warning:
|
||||
|
|
@ -46,8 +46,8 @@ class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
|||
libpath = self.filepath_from_context(context)
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
container_name = openpype.hosts.blender.api.plugin.asset_name(
|
||||
lib_container = plugin.prepare_scene_name(asset, subset)
|
||||
container_name = plugin.prepare_scene_name(
|
||||
asset, subset, namespace
|
||||
)
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
|||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in openpype.hosts.blender.api.plugin.VALID_EXTENSIONS, (
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ class AudioLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ from pathlib import Path
|
|||
import bpy
|
||||
|
||||
from openpype.pipeline import (
|
||||
legacy_create,
|
||||
get_representation_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
registered_host
|
||||
)
|
||||
from openpype.pipeline.create import get_legacy_creator_by_name
|
||||
from openpype.pipeline.create import CreateContext
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.lib import imprint
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
|
|
@ -20,7 +20,7 @@ from openpype.hosts.blender.api.pipeline import (
|
|||
class BlendLoader(plugin.AssetLoader):
|
||||
"""Load assets from a .blend file."""
|
||||
|
||||
families = ["model", "rig", "layout", "camera", "blendScene"]
|
||||
families = ["model", "rig", "layout", "camera"]
|
||||
representations = ["blend"]
|
||||
|
||||
label = "Append Blend"
|
||||
|
|
@ -57,19 +57,21 @@ class BlendLoader(plugin.AssetLoader):
|
|||
obj.get(AVALON_PROPERTY).get('family') == 'rig'
|
||||
)
|
||||
]
|
||||
if not rigs:
|
||||
return
|
||||
|
||||
# Create animation instances for each rig
|
||||
creator_identifier = "io.openpype.creators.blender.animation"
|
||||
host = registered_host()
|
||||
create_context = CreateContext(host)
|
||||
|
||||
for rig in rigs:
|
||||
creator_plugin = get_legacy_creator_by_name("CreateAnimation")
|
||||
legacy_create(
|
||||
creator_plugin,
|
||||
name=rig.name.split(':')[-1] + "_animation",
|
||||
asset=asset,
|
||||
options={
|
||||
"useSelection": False,
|
||||
create_context.create(
|
||||
creator_identifier=creator_identifier,
|
||||
variant=rig.name.split(':')[-1],
|
||||
pre_create_data={
|
||||
"use_selection": False,
|
||||
"asset_group": rig
|
||||
},
|
||||
data={
|
||||
"dependencies": representation
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -90,7 +92,6 @@ class BlendLoader(plugin.AssetLoader):
|
|||
members.append(data)
|
||||
|
||||
container = self._get_asset_container(data_to.objects)
|
||||
print(container)
|
||||
assert container, "No asset group found"
|
||||
|
||||
container.name = group_name
|
||||
|
|
@ -104,8 +105,6 @@ class BlendLoader(plugin.AssetLoader):
|
|||
print(obj)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
|
||||
print("")
|
||||
|
||||
# Remove the library from the blend file
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
|
@ -134,9 +133,9 @@ class BlendLoader(plugin.AssetLoader):
|
|||
|
||||
representation = str(context["representation"]["_id"])
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
221
openpype/hosts/blender/plugins/load/load_blendscene.py
Normal file
221
openpype/hosts/blender/plugins/load/load_blendscene.py
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
from typing import Dict, List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import (
|
||||
get_representation_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.lib import imprint
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
AVALON_CONTAINERS,
|
||||
AVALON_PROPERTY,
|
||||
)
|
||||
|
||||
|
||||
class BlendSceneLoader(plugin.AssetLoader):
|
||||
"""Load assets from a .blend file."""
|
||||
|
||||
families = ["blendScene"]
|
||||
representations = ["blend"]
|
||||
|
||||
label = "Append Blend"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
@staticmethod
|
||||
def _get_asset_container(collections):
|
||||
for coll in collections:
|
||||
parents = [c for c in collections if c.user_of_id(coll)]
|
||||
if coll.get(AVALON_PROPERTY) and not parents:
|
||||
return coll
|
||||
|
||||
return None
|
||||
|
||||
def _process_data(self, libpath, group_name, family):
|
||||
# Append all the data from the .blend file
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=False, relative=False
|
||||
) as (data_from, data_to):
|
||||
for attr in dir(data_to):
|
||||
setattr(data_to, attr, getattr(data_from, attr))
|
||||
|
||||
members = []
|
||||
|
||||
# Rename the object to add the asset name
|
||||
for attr in dir(data_to):
|
||||
for data in getattr(data_to, attr):
|
||||
data.name = f"{group_name}:{data.name}"
|
||||
members.append(data)
|
||||
|
||||
container = self._get_asset_container(
|
||||
data_to.collections)
|
||||
assert container, "No asset group found"
|
||||
|
||||
container.name = group_name
|
||||
|
||||
# Link the group to the scene
|
||||
bpy.context.scene.collection.children.link(container)
|
||||
|
||||
# Remove the library from the blend file
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
return container, members
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
libpath = self.filepath_from_context(context)
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
try:
|
||||
family = context["representation"]["context"]["family"]
|
||||
except ValueError:
|
||||
family = "model"
|
||||
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
|
||||
bpy.context.scene.collection.children.link(avalon_container)
|
||||
|
||||
container, members = self._process_data(libpath, group_name, family)
|
||||
|
||||
avalon_container.children.link(container)
|
||||
|
||||
data = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace or '',
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": str(context["representation"]["_id"]),
|
||||
"libpath": libpath,
|
||||
"asset_name": asset_name,
|
||||
"parent": str(context["representation"]["parent"]),
|
||||
"family": context["representation"]["context"]["family"],
|
||||
"objectName": group_name,
|
||||
"members": members,
|
||||
}
|
||||
|
||||
container[AVALON_PROPERTY] = data
|
||||
|
||||
objects = [
|
||||
obj for obj in bpy.data.objects
|
||||
if obj.name.startswith(f"{group_name}:")
|
||||
]
|
||||
|
||||
self[:] = objects
|
||||
return objects
|
||||
|
||||
def exec_update(self, container: Dict, representation: Dict):
|
||||
"""
|
||||
Update the loaded asset.
|
||||
"""
|
||||
group_name = container["objectName"]
|
||||
asset_group = bpy.data.collections.get(group_name)
|
||||
libpath = Path(get_representation_path(representation)).as_posix()
|
||||
|
||||
assert asset_group, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
|
||||
# Get the parents of the members of the asset group, so we can
|
||||
# re-link them after the update.
|
||||
# Also gets the transform for each object to reapply after the update.
|
||||
collection_parents = {}
|
||||
member_transforms = {}
|
||||
members = asset_group.get(AVALON_PROPERTY).get("members", [])
|
||||
loaded_collections = {c for c in bpy.data.collections if c in members}
|
||||
loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS))
|
||||
for member in members:
|
||||
if isinstance(member, bpy.types.Object):
|
||||
member_parents = set(member.users_collection)
|
||||
member_transforms[member.name] = member.matrix_basis.copy()
|
||||
elif isinstance(member, bpy.types.Collection):
|
||||
member_parents = {
|
||||
c for c in bpy.data.collections if c.user_of_id(member)}
|
||||
else:
|
||||
continue
|
||||
|
||||
member_parents = member_parents.difference(loaded_collections)
|
||||
if member_parents:
|
||||
collection_parents[member.name] = list(member_parents)
|
||||
|
||||
old_data = dict(asset_group.get(AVALON_PROPERTY))
|
||||
|
||||
self.exec_remove(container)
|
||||
|
||||
family = container["family"]
|
||||
asset_group, members = self._process_data(libpath, group_name, family)
|
||||
|
||||
for member in members:
|
||||
if member.name in collection_parents:
|
||||
for parent in collection_parents[member.name]:
|
||||
if isinstance(member, bpy.types.Object):
|
||||
parent.objects.link(member)
|
||||
elif isinstance(member, bpy.types.Collection):
|
||||
parent.children.link(member)
|
||||
if member.name in member_transforms and isinstance(
|
||||
member, bpy.types.Object
|
||||
):
|
||||
member.matrix_basis = member_transforms[member.name]
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
avalon_container.children.link(asset_group)
|
||||
|
||||
# Restore the old data, but reset members, as they don't exist anymore
|
||||
# This avoids a crash, because the memory addresses of those members
|
||||
# are not valid anymore
|
||||
old_data["members"] = []
|
||||
asset_group[AVALON_PROPERTY] = old_data
|
||||
|
||||
new_data = {
|
||||
"libpath": libpath,
|
||||
"representation": str(representation["_id"]),
|
||||
"parent": str(representation["parent"]),
|
||||
"members": members,
|
||||
}
|
||||
|
||||
imprint(asset_group, new_data)
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""
|
||||
Remove an existing container from a Blender scene.
|
||||
"""
|
||||
group_name = container["objectName"]
|
||||
asset_group = bpy.data.collections.get(group_name)
|
||||
|
||||
members = set(asset_group.get(AVALON_PROPERTY).get("members", []))
|
||||
|
||||
if members:
|
||||
for attr_name in dir(bpy.data):
|
||||
attr = getattr(bpy.data, attr_name)
|
||||
if not isinstance(attr, bpy.types.bpy_prop_collection):
|
||||
continue
|
||||
|
||||
# ensure to make a list copy because we
|
||||
# we remove members as we iterate
|
||||
for data in list(attr):
|
||||
if data not in members or data == asset_group:
|
||||
continue
|
||||
|
||||
attr.remove(data)
|
||||
|
||||
bpy.data.collections.remove(asset_group)
|
||||
|
|
@ -87,9 +87,9 @@ class AbcCameraLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@ class FbxCameraLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -134,9 +134,9 @@ class FbxModelLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
# raise ValueError("Creator plugin \"CreateCamera\" was "
|
||||
# "not found.")
|
||||
|
||||
# TODO: Refactor legacy create usage to new style creators
|
||||
# legacy_create(
|
||||
# creator_plugin,
|
||||
# name="camera",
|
||||
|
|
@ -148,9 +149,9 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -96,14 +96,14 @@ class BlendLookLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
lib_container = plugin.asset_name(
|
||||
lib_container = plugin.prepare_scene_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
container_name = plugin.asset_name(
|
||||
container_name = plugin.prepare_scene_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,72 +1,15 @@
|
|||
import os
|
||||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import get_current_task_name, get_current_asset_name
|
||||
from openpype.hosts.blender.api import workio
|
||||
|
||||
|
||||
class SaveWorkfiledAction(pyblish.api.Action):
|
||||
"""Save Workfile."""
|
||||
label = "Save Workfile"
|
||||
on = "failed"
|
||||
icon = "save"
|
||||
|
||||
def process(self, context, plugin):
|
||||
bpy.ops.wm.avalon_workfiles()
|
||||
|
||||
|
||||
class CollectBlenderCurrentFile(pyblish.api.ContextPlugin):
|
||||
"""Inject the current working file into context"""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.5
|
||||
label = "Blender Current File"
|
||||
hosts = ["blender"]
|
||||
actions = [SaveWorkfiledAction]
|
||||
|
||||
def process(self, context):
|
||||
"""Inject the current working file"""
|
||||
current_file = workio.current_file()
|
||||
|
||||
context.data["currentFile"] = current_file
|
||||
|
||||
assert current_file, (
|
||||
"Current file is empty. Save the file before continuing."
|
||||
)
|
||||
|
||||
folder, file = os.path.split(current_file)
|
||||
filename, ext = os.path.splitext(file)
|
||||
|
||||
task = get_current_task_name()
|
||||
|
||||
data = {}
|
||||
|
||||
# create instance
|
||||
instance = context.create_instance(name=filename)
|
||||
subset = "workfile" + task.capitalize()
|
||||
|
||||
data.update({
|
||||
"subset": subset,
|
||||
"asset": get_current_asset_name(),
|
||||
"label": subset,
|
||||
"publish": True,
|
||||
"family": "workfile",
|
||||
"families": ["workfile"],
|
||||
"setMembers": [current_file],
|
||||
"frameStart": bpy.context.scene.frame_start,
|
||||
"frameEnd": bpy.context.scene.frame_end,
|
||||
})
|
||||
|
||||
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))
|
||||
|
|
|
|||
43
openpype/hosts/blender/plugins/publish/collect_instance.py
Normal file
43
openpype/hosts/blender/plugins/publish/collect_instance.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import KnownPublishError
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class CollectBlenderInstanceData(pyblish.api.InstancePlugin):
|
||||
"""Validator to verify that the instance is not empty"""
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model", "pointcache", "animation", "rig", "camera", "layout",
|
||||
"blendScene"]
|
||||
label = "Collect Instance"
|
||||
|
||||
def process(self, instance):
|
||||
instance_node = instance.data["transientData"]["instance_node"]
|
||||
|
||||
# Collect members of the instance
|
||||
members = [instance_node]
|
||||
if isinstance(instance_node, bpy.types.Collection):
|
||||
members.extend(instance_node.objects)
|
||||
members.extend(instance_node.children)
|
||||
|
||||
# Special case for animation instances, include armatures
|
||||
if instance.data["family"] == "animation":
|
||||
for obj in instance_node.objects:
|
||||
if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY):
|
||||
members.extend(
|
||||
child for child in obj.children
|
||||
if child.type == 'ARMATURE'
|
||||
)
|
||||
elif isinstance(instance_node, bpy.types.Object):
|
||||
members.extend(instance_node.children_recursive)
|
||||
else:
|
||||
raise KnownPublishError(
|
||||
f"Unsupported instance node type '{type(instance_node)}' "
|
||||
f"for instance '{instance}'"
|
||||
)
|
||||
|
||||
instance[:] = members
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import json
|
||||
from typing import Generator
|
||||
|
||||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
AVALON_INSTANCES,
|
||||
AVALON_PROPERTY,
|
||||
)
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
"""Collect the data of a model."""
|
||||
|
||||
hosts = ["blender"]
|
||||
label = "Collect Instances"
|
||||
order = pyblish.api.CollectorOrder
|
||||
|
||||
@staticmethod
|
||||
def get_asset_groups() -> Generator:
|
||||
"""Return all instances that are empty objects asset groups.
|
||||
"""
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
for obj in list(instances.objects) + list(instances.children):
|
||||
avalon_prop = obj.get(AVALON_PROPERTY) or {}
|
||||
if avalon_prop.get('id') == 'pyblish.avalon.instance':
|
||||
yield obj
|
||||
|
||||
@staticmethod
|
||||
def create_instance(context, group):
|
||||
avalon_prop = group[AVALON_PROPERTY]
|
||||
asset = avalon_prop['asset']
|
||||
family = avalon_prop['family']
|
||||
subset = avalon_prop['subset']
|
||||
task = avalon_prop['task']
|
||||
name = f"{asset}_{subset}"
|
||||
return context.create_instance(
|
||||
name=name,
|
||||
family=family,
|
||||
families=[family],
|
||||
subset=subset,
|
||||
asset=asset,
|
||||
task=task,
|
||||
)
|
||||
|
||||
def process(self, context):
|
||||
"""Collect the models from the current Blender scene."""
|
||||
asset_groups = self.get_asset_groups()
|
||||
|
||||
for group in asset_groups:
|
||||
instance = self.create_instance(context, group)
|
||||
members = []
|
||||
if isinstance(group, bpy.types.Collection):
|
||||
members = list(group.objects)
|
||||
family = instance.data["family"]
|
||||
if family == "animation":
|
||||
for obj in group.objects:
|
||||
if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY):
|
||||
members.extend(
|
||||
child for child in obj.children
|
||||
if child.type == 'ARMATURE')
|
||||
else:
|
||||
members = group.children_recursive
|
||||
|
||||
members.append(group)
|
||||
instance[:] = members
|
||||
self.log.debug(json.dumps(instance.data, indent=4))
|
||||
for obj in instance:
|
||||
self.log.debug(obj)
|
||||
|
|
@ -73,11 +73,12 @@ class CollectBlenderRender(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
context = instance.context
|
||||
|
||||
render_data = bpy.data.collections[str(instance)].get("render_data")
|
||||
instance_node = instance.data["transientData"]["instance_node"]
|
||||
render_data = instance_node.get("render_data")
|
||||
|
||||
assert render_data, "No render data found."
|
||||
|
||||
self.log.info(f"render_data: {dict(render_data)}")
|
||||
self.log.debug(f"render_data: {dict(render_data)}")
|
||||
|
||||
render_product = render_data.get("render_product")
|
||||
aov_file_product = render_data.get("aov_file_product")
|
||||
|
|
@ -120,4 +121,4 @@ class CollectBlenderRender(pyblish.api.InstancePlugin):
|
|||
"renderProducts": colorspace.ARenderProduct(),
|
||||
})
|
||||
|
||||
self.log.info(f"data: {instance.data}")
|
||||
self.log.debug(f"data: {instance.data}")
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
|
||||
self.log.debug(f"instance: {instance}")
|
||||
|
||||
datablock = instance.data["transientData"]["instance_node"]
|
||||
|
||||
# get cameras
|
||||
cameras = [
|
||||
obj
|
||||
for obj in instance
|
||||
for obj in datablock.all_objects
|
||||
if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA"
|
||||
]
|
||||
|
||||
|
|
|
|||
37
openpype/hosts/blender/plugins/publish/collect_workfile.py
Normal file
37
openpype/hosts/blender/plugins/publish/collect_workfile.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from pathlib import Path
|
||||
|
||||
from pyblish.api import InstancePlugin, CollectorOrder
|
||||
|
||||
|
||||
class CollectWorkfile(InstancePlugin):
|
||||
"""Inject workfile data into its instance."""
|
||||
|
||||
order = CollectorOrder
|
||||
label = "Collect Workfile"
|
||||
hosts = ["blender"]
|
||||
families = ["workfile"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Process collector."""
|
||||
|
||||
context = instance.context
|
||||
filepath = Path(context.data["currentFile"])
|
||||
ext = filepath.suffix
|
||||
|
||||
instance.data.update(
|
||||
{
|
||||
"setMembers": [filepath.as_posix()],
|
||||
"frameStart": context.data.get("frameStart", 1),
|
||||
"frameEnd": context.data.get("frameEnd", 1),
|
||||
"handleStart": context.data.get("handleStart", 1),
|
||||
"handledEnd": context.data.get("handleEnd", 1),
|
||||
"representations": [
|
||||
{
|
||||
"name": ext.lstrip("."),
|
||||
"ext": ext.lstrip("."),
|
||||
"files": filepath.name,
|
||||
"stagingDir": filepath.parent,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
|
@ -4,10 +4,9 @@ import bpy
|
|||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractABC(publish.Extractor):
|
||||
class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract as ABC."""
|
||||
|
||||
label = "Extract ABC"
|
||||
|
|
@ -15,9 +14,15 @@ class ExtractABC(publish.Extractor):
|
|||
families = ["pointcache"]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.abc"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.abc"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
|
|
@ -25,18 +30,16 @@ class ExtractABC(publish.Extractor):
|
|||
|
||||
plugin.deselect_all()
|
||||
|
||||
selected = []
|
||||
active = None
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
selected = []
|
||||
for obj in instance:
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
# Set as active the asset group
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
active = obj
|
||||
if isinstance(obj, bpy.types.Object):
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
|
||||
context = plugin.create_blender_context(
|
||||
active=active, selected=selected)
|
||||
active=asset_group, selected=selected)
|
||||
|
||||
with bpy.context.temp_override(**context):
|
||||
# We export the abc
|
||||
|
|
@ -59,8 +62,8 @@ class ExtractABC(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
||||
|
||||
class ExtractModelABC(ExtractABC):
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ from openpype.pipeline import publish
|
|||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class ExtractAnimationABC(publish.Extractor):
|
||||
class ExtractAnimationABC(
|
||||
publish.Extractor,
|
||||
publish.OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Extract as ABC."""
|
||||
|
||||
label = "Extract Animation ABC"
|
||||
|
|
@ -15,9 +18,16 @@ class ExtractAnimationABC(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.abc"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.abc"
|
||||
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
|
|
@ -26,7 +36,7 @@ class ExtractAnimationABC(publish.Extractor):
|
|||
plugin.deselect_all()
|
||||
|
||||
selected = []
|
||||
asset_group = None
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
objects = []
|
||||
for obj in instance:
|
||||
|
|
@ -66,5 +76,5 @@ class ExtractAnimationABC(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import bpy
|
|||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractBlend(publish.Extractor):
|
||||
class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract a blend file."""
|
||||
|
||||
label = "Extract Blend"
|
||||
|
|
@ -14,10 +14,16 @@ class ExtractBlend(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.blend"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.blend"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
|
|
@ -25,14 +31,16 @@ class ExtractBlend(publish.Extractor):
|
|||
|
||||
data_blocks = set()
|
||||
|
||||
for obj in instance:
|
||||
data_blocks.add(obj)
|
||||
for data in instance:
|
||||
data_blocks.add(data)
|
||||
# Pack used images in the blend files.
|
||||
if obj.type != 'MESH':
|
||||
if not (
|
||||
isinstance(data, bpy.types.Object) and data.type == 'MESH'
|
||||
):
|
||||
continue
|
||||
for material_slot in obj.material_slots:
|
||||
for material_slot in data.material_slots:
|
||||
mat = material_slot.material
|
||||
if not(mat and mat.use_nodes):
|
||||
if not (mat and mat.use_nodes):
|
||||
continue
|
||||
tree = mat.node_tree
|
||||
if tree.type != 'SHADER':
|
||||
|
|
@ -58,5 +66,5 @@ class ExtractBlend(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import bpy
|
|||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractBlendAnimation(publish.Extractor):
|
||||
class ExtractBlendAnimation(
|
||||
publish.Extractor,
|
||||
publish.OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Extract a blend file."""
|
||||
|
||||
label = "Extract Blend"
|
||||
|
|
@ -14,10 +17,16 @@ class ExtractBlendAnimation(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.blend"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.blend"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
|
|
@ -50,5 +59,5 @@ class ExtractBlendAnimation(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin
|
|||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractCameraABC(publish.Extractor):
|
||||
class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract camera as ABC."""
|
||||
|
||||
label = "Extract Camera (ABC)"
|
||||
|
|
@ -16,9 +16,15 @@ class ExtractCameraABC(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.abc"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.abc"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
|
|
@ -26,12 +32,7 @@ class ExtractCameraABC(publish.Extractor):
|
|||
|
||||
plugin.deselect_all()
|
||||
|
||||
asset_group = None
|
||||
for obj in instance:
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
asset_group = obj
|
||||
break
|
||||
assert asset_group, "No asset group found"
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
# Need to cast to list because children is a tuple
|
||||
selected = list(asset_group.children)
|
||||
|
|
@ -64,5 +65,5 @@ class ExtractCameraABC(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from openpype.pipeline import publish
|
|||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class ExtractCamera(publish.Extractor):
|
||||
class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract as the camera as FBX."""
|
||||
|
||||
label = "Extract Camera (FBX)"
|
||||
|
|
@ -15,9 +15,15 @@ class ExtractCamera(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.fbx"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.fbx"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
|
|
@ -73,5 +79,5 @@ class ExtractCamera(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin
|
|||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractFBX(publish.Extractor):
|
||||
class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract as FBX."""
|
||||
|
||||
label = "Extract FBX"
|
||||
|
|
@ -16,9 +16,15 @@ class ExtractFBX(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.fbx"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.fbx"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
|
|
@ -26,14 +32,12 @@ class ExtractFBX(publish.Extractor):
|
|||
|
||||
plugin.deselect_all()
|
||||
|
||||
selected = []
|
||||
asset_group = None
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
selected = []
|
||||
for obj in instance:
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
asset_group = obj
|
||||
|
||||
context = plugin.create_blender_context(
|
||||
active=asset_group, selected=selected)
|
||||
|
|
@ -84,5 +88,5 @@ class ExtractFBX(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,41 @@ from openpype.hosts.blender.api import plugin
|
|||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractAnimationFBX(publish.Extractor):
|
||||
def get_all_parents(obj):
|
||||
"""Get all recursive parents of object"""
|
||||
result = []
|
||||
while True:
|
||||
obj = obj.parent
|
||||
if not obj:
|
||||
break
|
||||
result.append(obj)
|
||||
return result
|
||||
|
||||
|
||||
def get_highest_root(objects):
|
||||
# Get the highest object that is also in the collection
|
||||
included_objects = {obj.name_full for obj in objects}
|
||||
num_parents_to_obj = {}
|
||||
for obj in objects:
|
||||
if isinstance(obj, bpy.types.Object):
|
||||
parents = get_all_parents(obj)
|
||||
# included parents
|
||||
parents = [parent for parent in parents if
|
||||
parent.name_full in included_objects]
|
||||
if not parents:
|
||||
# A node without parents must be a highest root
|
||||
return obj
|
||||
|
||||
num_parents_to_obj.setdefault(len(parents), obj)
|
||||
|
||||
minimum_parent = min(num_parents_to_obj)
|
||||
return num_parents_to_obj[minimum_parent]
|
||||
|
||||
|
||||
class ExtractAnimationFBX(
|
||||
publish.Extractor,
|
||||
publish.OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Extract as animation."""
|
||||
|
||||
label = "Extract FBX"
|
||||
|
|
@ -19,23 +53,43 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
||||
# Perform extraction
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
# The first collection object in the instance is taken, as there
|
||||
# should be only one that contains the asset group.
|
||||
collection = [
|
||||
obj for obj in instance if type(obj) is bpy.types.Collection][0]
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
# Again, the first object in the collection is taken , as there
|
||||
# should be only the asset group in the collection.
|
||||
asset_group = collection.objects[0]
|
||||
# Get objects in this collection (but not in children collections)
|
||||
# and for those objects include the children hierarchy
|
||||
# TODO: Would it make more sense for the Collect Instance collector
|
||||
# to also always retrieve all the children?
|
||||
objects = set(asset_group.objects)
|
||||
|
||||
armature = [
|
||||
obj for obj in asset_group.children if obj.type == 'ARMATURE'][0]
|
||||
# From the direct children of the collection find the 'root' node
|
||||
# that we want to export - it is the 'highest' node in a hierarchy
|
||||
root = get_highest_root(objects)
|
||||
|
||||
for obj in list(objects):
|
||||
objects.update(obj.children_recursive)
|
||||
|
||||
# Find all armatures among the objects, assume to find only one
|
||||
armatures = [obj for obj in objects if obj.type == "ARMATURE"]
|
||||
if not armatures:
|
||||
raise RuntimeError(
|
||||
f"Unable to find ARMATURE in collection: "
|
||||
f"{asset_group.name}"
|
||||
)
|
||||
elif len(armatures) > 1:
|
||||
self.log.warning(
|
||||
"Found more than one ARMATURE, using "
|
||||
f"only first of: {armatures}"
|
||||
)
|
||||
armature = armatures[0]
|
||||
|
||||
object_action_pairs = []
|
||||
original_actions = []
|
||||
|
|
@ -44,9 +98,6 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
ending_frames = []
|
||||
|
||||
# For each armature, we make a copy of the current action
|
||||
curr_action = None
|
||||
copy_action = None
|
||||
|
||||
if armature.animation_data and armature.animation_data.action:
|
||||
curr_action = armature.animation_data.action
|
||||
copy_action = curr_action.copy()
|
||||
|
|
@ -56,12 +107,20 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
starting_frames.append(curr_frame_range[0])
|
||||
ending_frames.append(curr_frame_range[1])
|
||||
else:
|
||||
self.log.info("Object have no animation.")
|
||||
self.log.info(
|
||||
f"Armature '{armature.name}' has no animation, "
|
||||
f"skipping FBX animation extraction for {instance}."
|
||||
)
|
||||
return
|
||||
|
||||
asset_group_name = asset_group.name
|
||||
asset_group.name = asset_group.get(AVALON_PROPERTY).get("asset_name")
|
||||
asset_name = asset_group.get(AVALON_PROPERTY).get("asset_name")
|
||||
if asset_name:
|
||||
# Rename for the export; this data is only present when loaded
|
||||
# from a JSON Layout (layout family)
|
||||
asset_group.name = asset_name
|
||||
|
||||
# Remove : from the armature name for the export
|
||||
armature_name = armature.name
|
||||
original_name = armature_name.split(':')[1]
|
||||
armature.name = original_name
|
||||
|
|
@ -84,13 +143,16 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
for obj in bpy.data.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
asset_group.select_set(True)
|
||||
root.select_set(True)
|
||||
armature.select_set(True)
|
||||
fbx_filename = f"{instance.name}_{armature.name}.fbx"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
fbx_filename = f"{instance_name}_{armature.name}.fbx"
|
||||
filepath = os.path.join(stagingdir, fbx_filename)
|
||||
|
||||
override = plugin.create_blender_context(
|
||||
active=asset_group, selected=[asset_group, armature])
|
||||
active=root, selected=[root, armature])
|
||||
bpy.ops.export_scene.fbx(
|
||||
override,
|
||||
filepath=filepath,
|
||||
|
|
@ -104,7 +166,7 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
)
|
||||
armature.name = armature_name
|
||||
asset_group.name = asset_group_name
|
||||
asset_group.select_set(False)
|
||||
root.select_set(True)
|
||||
armature.select_set(False)
|
||||
|
||||
# We delete the baked action and set the original one back
|
||||
|
|
@ -119,7 +181,7 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
pair[1].user_clear()
|
||||
bpy.data.actions.remove(pair[1])
|
||||
|
||||
json_filename = f"{instance.name}.json"
|
||||
json_filename = f"{instance_name}.json"
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
json_dict = {
|
||||
|
|
@ -158,5 +220,5 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
instance.data["representations"].append(fbx_representation)
|
||||
instance.data["representations"].append(json_representation)
|
||||
|
||||
self.log.info("Extracted instance '{}' to: {}".format(
|
||||
instance.name, fbx_representation))
|
||||
self.log.debug("Extracted instance '{}' to: {}".format(
|
||||
instance.name, fbx_representation))
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from openpype.hosts.blender.api import plugin
|
|||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractLayout(publish.Extractor):
|
||||
class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract a layout."""
|
||||
|
||||
label = "Extract Layout"
|
||||
|
|
@ -45,7 +45,7 @@ class ExtractLayout(publish.Extractor):
|
|||
starting_frames.append(curr_frame_range[0])
|
||||
ending_frames.append(curr_frame_range[1])
|
||||
else:
|
||||
self.log.info("Object have no animation.")
|
||||
self.log.info("Object has no animation.")
|
||||
continue
|
||||
|
||||
asset_group_name = asset.name
|
||||
|
|
@ -113,6 +113,9 @@ class ExtractLayout(publish.Extractor):
|
|||
return None, n
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
||||
|
|
@ -125,13 +128,22 @@ class ExtractLayout(publish.Extractor):
|
|||
json_data = []
|
||||
fbx_files = []
|
||||
|
||||
asset_group = bpy.data.objects[str(instance)]
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
fbx_count = 0
|
||||
|
||||
project_name = instance.context.data["projectEntity"]["name"]
|
||||
for asset in asset_group.children:
|
||||
metadata = asset.get(AVALON_PROPERTY)
|
||||
if not metadata:
|
||||
# Avoid raising error directly if there's just invalid data
|
||||
# inside the instance; better to log it to the artist
|
||||
# TODO: This should actually be validated in a validator
|
||||
self.log.warning(
|
||||
f"Found content in layout that is not a loaded "
|
||||
f"asset, skipping: {asset.name_full}"
|
||||
)
|
||||
continue
|
||||
|
||||
version_id = metadata["parent"]
|
||||
family = metadata["family"]
|
||||
|
|
@ -212,7 +224,11 @@ class ExtractLayout(publish.Extractor):
|
|||
|
||||
json_data.append(json_element)
|
||||
|
||||
json_filename = "{}.json".format(instance.name)
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
json_filename = f"{instance_name}.json"
|
||||
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
|
|
@ -245,5 +261,5 @@ class ExtractLayout(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(fbx_representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, json_representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, json_representation)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from openpype.hosts.blender.api import capture
|
|||
from openpype.hosts.blender.api.lib import maintained_time
|
||||
|
||||
|
||||
class ExtractPlayblast(publish.Extractor):
|
||||
class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""
|
||||
Extract viewport playblast.
|
||||
|
||||
|
|
@ -24,7 +24,8 @@ class ExtractPlayblast(publish.Extractor):
|
|||
order = pyblish.api.ExtractorOrder + 0.01
|
||||
|
||||
def process(self, instance):
|
||||
self.log.debug("Extracting capture..")
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# get scene fps
|
||||
fps = instance.data.get("fps")
|
||||
|
|
@ -50,7 +51,10 @@ class ExtractPlayblast(publish.Extractor):
|
|||
|
||||
# get output path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = instance.name
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
filename = f"{asset_name}_{subset}"
|
||||
|
||||
path = os.path.join(stagingdir, filename)
|
||||
|
||||
self.log.debug(f"Outputting images to {path}")
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ class ExtractThumbnail(publish.Extractor):
|
|||
self.log.debug("Extracting capture..")
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = instance.name
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
filename = f"{asset_name}_{subset}"
|
||||
|
||||
path = os.path.join(stagingdir, filename)
|
||||
|
||||
self.log.debug(f"Outputting images to {path}")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline.publish import OptionalPyblishPluginMixin
|
||||
from openpype.hosts.blender.api.workio import save_file
|
||||
|
||||
|
||||
class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
||||
class IncrementWorkfileVersion(
|
||||
pyblish.api.ContextPlugin,
|
||||
OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Increment current workfile version."""
|
||||
|
||||
order = pyblish.api.IntegratorOrder + 0.9
|
||||
|
|
@ -13,6 +17,8 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
|||
"pointcache", "render"]
|
||||
|
||||
def process(self, context):
|
||||
if not self.is_active(context.data):
|
||||
return
|
||||
|
||||
assert all(result["success"] for result in context.data["results"]), (
|
||||
"Publishing not successful so version is not increased.")
|
||||
|
|
@ -23,4 +29,4 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
|||
|
||||
save_file(filepath, copy=False)
|
||||
|
||||
self.log.info('Incrementing script version')
|
||||
self.log.debug('Incrementing blender workfile version')
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import json
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline.publish import OptionalPyblishPluginMixin
|
||||
|
||||
|
||||
class IntegrateAnimation(pyblish.api.InstancePlugin):
|
||||
class IntegrateAnimation(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Generate a JSON file for animation."""
|
||||
|
||||
label = "Integrate Animation"
|
||||
|
|
@ -13,7 +17,7 @@ class IntegrateAnimation(pyblish.api.InstancePlugin):
|
|||
families = ["setdress"]
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info("Integrate Animation")
|
||||
self.log.debug("Integrate Animation")
|
||||
|
||||
representation = instance.data.get('representations')[0]
|
||||
json_path = representation.get('publishedFiles')[0]
|
||||
|
|
|
|||
|
|
@ -5,10 +5,15 @@ import bpy
|
|||
import pyblish.api
|
||||
|
||||
import openpype.hosts.blender.api.action
|
||||
from openpype.pipeline.publish import ValidateContentsOrder
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
|
||||
|
||||
class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin):
|
||||
class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Camera must have a keyframe at frame 0.
|
||||
|
||||
Unreal shifts the first keyframe to frame 0. Forcing the camera to have
|
||||
|
|
@ -40,8 +45,12 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Camera must have a keyframe at frame 0: {invalid}"
|
||||
names = ", ".join(obj.name for obj in invalid)
|
||||
raise PublishValidationError(
|
||||
f"Camera must have a keyframe at frame 0: {names}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,12 +36,12 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
|
|||
"Render output folder "
|
||||
"doesn't match the blender scene name! "
|
||||
"Use Repair action to "
|
||||
"fix the folder file path.."
|
||||
"fix the folder file path."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
container = bpy.data.collections[str(instance)]
|
||||
container = instance.data["transientData"]["instance_node"]
|
||||
prepare_rendering(container)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
|
||||
cls.log.debug("Reset the render output folder...")
|
||||
|
|
|
|||
|
|
@ -2,8 +2,24 @@ import bpy
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import (
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
|
||||
class ValidateFileSaved(pyblish.api.InstancePlugin):
|
||||
|
||||
class SaveWorkfileAction(pyblish.api.Action):
|
||||
"""Save Workfile."""
|
||||
label = "Save Workfile"
|
||||
on = "failed"
|
||||
icon = "save"
|
||||
|
||||
def process(self, context, plugin):
|
||||
bpy.ops.wm.avalon_workfiles()
|
||||
|
||||
|
||||
class ValidateFileSaved(pyblish.api.ContextPlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validate that the workfile has been saved."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
|
|
@ -11,10 +27,35 @@ class ValidateFileSaved(pyblish.api.InstancePlugin):
|
|||
label = "Validate File Saved"
|
||||
optional = False
|
||||
exclude_families = []
|
||||
actions = [SaveWorkfileAction]
|
||||
|
||||
def process(self, instance):
|
||||
if [ef for ef in self.exclude_families
|
||||
if instance.data["family"] in ef]:
|
||||
def process(self, context):
|
||||
if not self.is_active(context.data):
|
||||
return
|
||||
|
||||
if not context.data["currentFile"]:
|
||||
# File has not been saved at all and has no filename
|
||||
raise PublishValidationError(
|
||||
"Current file is empty. Save the file before continuing."
|
||||
)
|
||||
|
||||
# Do not validate workfile has unsaved changes if only instances
|
||||
# present of families that should be excluded
|
||||
families = {
|
||||
instance.data["family"] for instance in context
|
||||
# Consider only enabled instances
|
||||
if instance.data.get("publish", True)
|
||||
and instance.data.get("active", True)
|
||||
}
|
||||
|
||||
def is_excluded(family):
|
||||
return any(family in exclude_family
|
||||
for exclude_family in self.exclude_families)
|
||||
|
||||
if all(is_excluded(family) for family in families):
|
||||
self.log.debug("Only excluded families found, skipping workfile "
|
||||
"unsaved changes validation..")
|
||||
return
|
||||
|
||||
if bpy.data.is_dirty:
|
||||
raise RuntimeError("Workfile is not saved.")
|
||||
raise PublishValidationError("Workfile has unsaved changes.")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline.publish import PublishValidationError
|
||||
|
||||
|
||||
class ValidateInstanceEmpty(pyblish.api.InstancePlugin):
|
||||
"""Validator to verify that the instance is not empty"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
hosts = ["blender"]
|
||||
families = ["model", "pointcache", "rig", "camera" "layout", "blendScene"]
|
||||
label = "Validate Instance is not Empty"
|
||||
optional = False
|
||||
|
||||
def process(self, instance):
|
||||
# Members are collected by `collect_instance` so we only need to check
|
||||
# whether any member is included. The instance node will be included
|
||||
# as a member as well, hence we will check for at least 2 members
|
||||
if len(instance) < 2:
|
||||
raise PublishValidationError(f"Instance {instance.name} is empty.")
|
||||
|
|
@ -4,17 +4,24 @@ import bpy
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import ValidateContentsOrder
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
import openpype.hosts.blender.api.action
|
||||
|
||||
|
||||
class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
|
||||
class ValidateMeshHasUvs(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Validate that the current mesh has UV's."""
|
||||
|
||||
order = ValidateContentsOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model"]
|
||||
label = "Mesh Has UV's"
|
||||
label = "Mesh Has UVs"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
optional = True
|
||||
|
||||
|
|
@ -49,8 +56,11 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
raise PublishValidationError(
|
||||
f"Meshes found in instance without valid UV's: {invalid}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,16 @@ import bpy
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import ValidateContentsOrder
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
import openpype.hosts.blender.api.action
|
||||
|
||||
|
||||
class ValidateMeshNoNegativeScale(pyblish.api.Validator):
|
||||
class ValidateMeshNoNegativeScale(pyblish.api.Validator,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Ensure that meshes don't have a negative scale."""
|
||||
|
||||
order = ValidateContentsOrder
|
||||
|
|
@ -27,8 +32,12 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Meshes found in instance with negative scale: {invalid}"
|
||||
names = ", ".join(obj.name for obj in invalid)
|
||||
raise PublishValidationError(
|
||||
f"Meshes found in instance with negative scale: {names}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,15 @@ import bpy
|
|||
import pyblish.api
|
||||
|
||||
import openpype.hosts.blender.api.action
|
||||
from openpype.pipeline.publish import ValidateContentsOrder
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
|
||||
|
||||
class ValidateNoColonsInName(pyblish.api.InstancePlugin):
|
||||
class ValidateNoColonsInName(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""There cannot be colons in names
|
||||
|
||||
Object or bone names cannot include colons. Other software do not
|
||||
|
|
@ -36,8 +41,12 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Objects found with colon in name: {invalid}"
|
||||
names = ", ".join(obj.name for obj in invalid)
|
||||
raise PublishValidationError(
|
||||
f"Objects found with colon in name: {names}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,17 @@ from typing import List
|
|||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline.publish import (
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
import openpype.hosts.blender.api.action
|
||||
|
||||
|
||||
class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin):
|
||||
class ValidateObjectIsInObjectMode(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Validate that the objects in the instance are in Object Mode."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
|
|
@ -25,8 +32,12 @@ class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Object found in instance is not in Object Mode: {invalid}"
|
||||
names = ", ".join(obj.name for obj in invalid)
|
||||
raise PublishValidationError(
|
||||
f"Object found in instance is not in Object Mode: {names}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,14 @@ import bpy
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import (
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
|
||||
class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin):
|
||||
|
||||
class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validate that there is a camera set as active for rendering."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
|
|
@ -13,5 +19,8 @@ class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin):
|
|||
optional = False
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
if not bpy.context.scene.camera:
|
||||
raise RuntimeError("No camera is active for rendering.")
|
||||
raise PublishValidationError("No camera is active for rendering.")
|
||||
|
|
|
|||
|
|
@ -6,10 +6,15 @@ import bpy
|
|||
import pyblish.api
|
||||
|
||||
import openpype.hosts.blender.api.action
|
||||
from openpype.pipeline.publish import ValidateContentsOrder
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
|
||||
|
||||
class ValidateTransformZero(pyblish.api.InstancePlugin):
|
||||
class ValidateTransformZero(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Transforms can't have any values
|
||||
|
||||
To solve this issue, try freezing the transforms. So long
|
||||
|
|
@ -38,9 +43,13 @@ class ValidateTransformZero(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
"Object found in instance has not"
|
||||
f" transform to zero: {invalid}"
|
||||
names = ", ".join(obj.name for obj in invalid)
|
||||
raise PublishValidationError(
|
||||
"Objects found in instance which do not"
|
||||
f" have transform set to zero: {names}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
|
||||
from openpype.client import get_asset_name_identifier
|
||||
|
||||
|
||||
class CollectCelactionInstances(pyblish.api.ContextPlugin):
|
||||
""" Adds the celaction render instances """
|
||||
|
|
@ -17,8 +19,10 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
|
|||
asset_entity = context.data["assetEntity"]
|
||||
project_entity = context.data["projectEntity"]
|
||||
|
||||
asset_name = get_asset_name_identifier(asset_entity)
|
||||
|
||||
shared_instance_data = {
|
||||
"asset": asset_entity["name"],
|
||||
"asset": asset_name,
|
||||
"frameStart": asset_entity["data"]["frameStart"],
|
||||
"frameEnd": asset_entity["data"]["frameEnd"],
|
||||
"handleStart": asset_entity["data"]["handleStart"],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import pyblish.api
|
||||
|
||||
from openpype.client import get_asset_name_identifier
|
||||
import openpype.hosts.flame.api as opfapi
|
||||
from openpype.hosts.flame.otio import flame_export
|
||||
from openpype.pipeline.create import get_subset_name
|
||||
|
|
@ -33,13 +34,15 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin):
|
|||
project_settings=context.data["project_settings"]
|
||||
)
|
||||
|
||||
asset_name = get_asset_name_identifier(asset_doc)
|
||||
|
||||
# adding otio timeline to context
|
||||
with opfapi.maintained_segment_selection(sequence) as selected_seg:
|
||||
otio_timeline = flame_export.create_otio_timeline(sequence)
|
||||
|
||||
instance_data = {
|
||||
"name": subset_name,
|
||||
"asset": asset_doc["name"],
|
||||
"asset": asset_name,
|
||||
"subset": subset_name,
|
||||
"family": "workfile",
|
||||
"families": []
|
||||
|
|
|
|||
|
|
@ -149,9 +149,7 @@ class CreateSaver(NewCreator):
|
|||
|
||||
# get frame padding from anatomy templates
|
||||
anatomy = Anatomy()
|
||||
frame_padding = int(
|
||||
anatomy.templates["render"].get("frame_padding", 4)
|
||||
)
|
||||
frame_padding = anatomy.templates["frame_padding"]
|
||||
|
||||
# Subset change detected
|
||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp
|
||||
)
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline import (
|
||||
AutoCreator,
|
||||
|
|
@ -68,6 +69,13 @@ class FusionWorkfileCreator(AutoCreator):
|
|||
task_name = self.create_context.get_current_task_name()
|
||||
host_name = self.create_context.host_name
|
||||
|
||||
if existing_instance is None:
|
||||
existing_instance_asset = None
|
||||
elif AYON_SERVER_ENABLED:
|
||||
existing_instance_asset = existing_instance["folderPath"]
|
||||
else:
|
||||
existing_instance_asset = existing_instance["asset"]
|
||||
|
||||
if existing_instance is None:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
|
|
@ -75,10 +83,13 @@ class FusionWorkfileCreator(AutoCreator):
|
|||
project_name, host_name
|
||||
)
|
||||
data = {
|
||||
"asset": asset_name,
|
||||
"task": task_name,
|
||||
"variant": self.default_variant
|
||||
}
|
||||
if AYON_SERVER_ENABLED:
|
||||
data["folderPath"] = asset_name
|
||||
else:
|
||||
data["asset"] = asset_name
|
||||
data.update(self.get_dynamic_data(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name, None
|
||||
|
|
@ -91,7 +102,7 @@ class FusionWorkfileCreator(AutoCreator):
|
|||
self._add_instance_to_context(new_instance)
|
||||
|
||||
elif (
|
||||
existing_instance["asset"] != asset_name
|
||||
existing_instance_asset != asset_name
|
||||
or existing_instance["task"] != task_name
|
||||
):
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
|
|
@ -99,6 +110,9 @@ class FusionWorkfileCreator(AutoCreator):
|
|||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name
|
||||
)
|
||||
existing_instance["asset"] = asset_name
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_instance["folderPath"] = asset_name
|
||||
else:
|
||||
existing_instance["asset"] = asset_name
|
||||
existing_instance["task"] = task_name
|
||||
existing_instance["subset"] = subset_name
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import qargparse
|
|||
from openpype.settings import get_current_project_settings
|
||||
from openpype.lib import Logger
|
||||
from openpype.pipeline import LoaderPlugin, LegacyCreator
|
||||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
from openpype.pipeline.load import get_representation_path_from_context
|
||||
from . import lib
|
||||
|
||||
|
|
@ -32,7 +31,7 @@ def load_stylesheet():
|
|||
class CreatorWidget(QtWidgets.QDialog):
|
||||
|
||||
# output items
|
||||
items = dict()
|
||||
items = {}
|
||||
|
||||
def __init__(self, name, info, ui_inputs, parent=None):
|
||||
super(CreatorWidget, self).__init__(parent)
|
||||
|
|
@ -494,9 +493,8 @@ class ClipLoader:
|
|||
joint `data` key with asset.data dict into the representation
|
||||
|
||||
"""
|
||||
asset_name = self.context["representation"]["context"]["asset"]
|
||||
asset_doc = get_current_project_asset(asset_name)
|
||||
log.debug("__ asset_doc: {}".format(pformat(asset_doc)))
|
||||
|
||||
asset_doc = self.context["asset"]
|
||||
self.data["assetData"] = asset_doc["data"]
|
||||
|
||||
def _make_track_item(self, source_bin_item, audio=False):
|
||||
|
|
@ -644,8 +642,8 @@ class PublishClip:
|
|||
Returns:
|
||||
hiero.core.TrackItem: hiero track item object with pype tag
|
||||
"""
|
||||
vertical_clip_match = dict()
|
||||
tag_data = dict()
|
||||
vertical_clip_match = {}
|
||||
tag_data = {}
|
||||
types = {
|
||||
"shot": "shot",
|
||||
"folder": "folder",
|
||||
|
|
@ -707,9 +705,10 @@ class PublishClip:
|
|||
self._create_parents()
|
||||
|
||||
def convert(self):
|
||||
|
||||
# solve track item data and add them to tag data
|
||||
self._convert_to_tag_data()
|
||||
tag_hierarchy_data = self._convert_to_tag_data()
|
||||
|
||||
self.tag_data.update(tag_hierarchy_data)
|
||||
|
||||
# if track name is in review track name and also if driving track name
|
||||
# is not in review track name: skip tag creation
|
||||
|
|
@ -723,16 +722,23 @@ class PublishClip:
|
|||
if self.rename:
|
||||
# rename track item
|
||||
self.track_item.setName(new_name)
|
||||
self.tag_data["asset"] = new_name
|
||||
self.tag_data["asset_name"] = new_name
|
||||
else:
|
||||
self.tag_data["asset"] = self.ti_name
|
||||
self.tag_data["asset_name"] = self.ti_name
|
||||
self.tag_data["hierarchyData"]["shot"] = self.ti_name
|
||||
|
||||
# AYON unique identifier
|
||||
folder_path = "/{}/{}".format(
|
||||
tag_hierarchy_data["hierarchy"],
|
||||
self.tag_data["asset_name"]
|
||||
)
|
||||
self.tag_data["folderPath"] = folder_path
|
||||
if self.tag_data["heroTrack"] and self.review_layer:
|
||||
self.tag_data.update({"reviewTrack": self.review_layer})
|
||||
else:
|
||||
self.tag_data.update({"reviewTrack": None})
|
||||
|
||||
# TODO: remove debug print
|
||||
log.debug("___ self.tag_data: {}".format(
|
||||
pformat(self.tag_data)
|
||||
))
|
||||
|
|
@ -891,7 +897,7 @@ class PublishClip:
|
|||
tag_hierarchy_data = hero_data
|
||||
|
||||
# add data to return data dict
|
||||
self.tag_data.update(tag_hierarchy_data)
|
||||
return tag_hierarchy_data
|
||||
|
||||
def _solve_tag_hierarchy_data(self, hierarchy_formatting_data):
|
||||
""" Solve tag data from hierarchy data and templates. """
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import json
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.client import get_asset_name_identifier
|
||||
|
||||
|
||||
class CollectFrameTagInstances(pyblish.api.ContextPlugin):
|
||||
"""Collect frames from tags.
|
||||
|
|
@ -99,6 +101,9 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
# first collect all available subset tag frames
|
||||
subset_data = {}
|
||||
context_asset_doc = context.data["assetEntity"]
|
||||
context_asset_name = get_asset_name_identifier(context_asset_doc)
|
||||
|
||||
for tag_data in sequence_tags:
|
||||
frame = int(tag_data["start"])
|
||||
|
||||
|
|
@ -115,7 +120,7 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin):
|
|||
subset_data[subset] = {
|
||||
"frames": [frame],
|
||||
"format": tag_data["format"],
|
||||
"asset": context.data["assetEntity"]["name"]
|
||||
"asset": context_asset_name
|
||||
}
|
||||
return subset_data
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import pyblish
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.pipeline.editorial import is_overlapping_otio_ranges
|
||||
|
||||
from openpype.hosts.hiero import api as phiero
|
||||
from openpype.hosts.hiero.api.otio import hiero_export
|
||||
import hiero
|
||||
|
||||
import hiero
|
||||
# # developer reload modules
|
||||
from pprint import pformat
|
||||
|
||||
|
|
@ -80,25 +83,24 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
if k not in ("id", "applieswhole", "label")
|
||||
})
|
||||
|
||||
asset = tag_data["asset"]
|
||||
asset, asset_name = self._get_asset_data(tag_data)
|
||||
|
||||
subset = tag_data["subset"]
|
||||
|
||||
# insert family into families
|
||||
family = tag_data["family"]
|
||||
families = [str(f) for f in tag_data["families"]]
|
||||
families.insert(0, str(family))
|
||||
|
||||
# form label
|
||||
label = asset
|
||||
if asset != clip_name:
|
||||
label = "{} -".format(asset)
|
||||
if asset_name != clip_name:
|
||||
label += " ({})".format(clip_name)
|
||||
label += " {}".format(subset)
|
||||
label += " {}".format("[" + ", ".join(families) + "]")
|
||||
|
||||
data.update({
|
||||
"name": "{}_{}".format(asset, subset),
|
||||
"label": label,
|
||||
"asset": asset,
|
||||
"asset_name": asset_name,
|
||||
"item": track_item,
|
||||
"families": families,
|
||||
"publish": tag_data["publish"],
|
||||
|
|
@ -176,9 +178,9 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
})
|
||||
|
||||
def create_shot_instance(self, context, **data):
|
||||
subset = "shotMain"
|
||||
master_layer = data.get("heroTrack")
|
||||
hierarchy_data = data.get("hierarchyData")
|
||||
asset = data.get("asset")
|
||||
item = data.get("item")
|
||||
clip_name = item.name()
|
||||
|
||||
|
|
@ -189,23 +191,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
return
|
||||
|
||||
asset = data["asset"]
|
||||
subset = "shotMain"
|
||||
asset_name = data["asset_name"]
|
||||
|
||||
# insert family into families
|
||||
family = "shot"
|
||||
|
||||
# form label
|
||||
label = asset
|
||||
if asset != clip_name:
|
||||
label = "{} -".format(asset)
|
||||
if asset_name != clip_name:
|
||||
label += " ({}) ".format(clip_name)
|
||||
label += " {}".format(subset)
|
||||
label += " [{}]".format(family)
|
||||
|
||||
data.update({
|
||||
"name": "{}_{}".format(asset, subset),
|
||||
"label": label,
|
||||
"subset": subset,
|
||||
"asset": asset,
|
||||
"family": family,
|
||||
"families": []
|
||||
})
|
||||
|
|
@ -215,7 +215,33 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
self.log.debug(
|
||||
"_ instance.data: {}".format(pformat(instance.data)))
|
||||
|
||||
def _get_asset_data(self, data):
|
||||
folder_path = data.pop("folderPath", None)
|
||||
|
||||
if data.get("asset_name"):
|
||||
asset_name = data["asset_name"]
|
||||
else:
|
||||
asset_name = data["asset"]
|
||||
|
||||
# backward compatibility for clip tags
|
||||
# which are missing folderPath key
|
||||
# TODO remove this in future versions
|
||||
if not folder_path:
|
||||
hierarchy_path = data["hierarchy"]
|
||||
folder_path = "/{}/{}".format(
|
||||
hierarchy_path,
|
||||
asset_name
|
||||
)
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset = folder_path
|
||||
else:
|
||||
asset = asset_name
|
||||
|
||||
return asset, asset_name
|
||||
|
||||
def create_audio_instance(self, context, **data):
|
||||
subset = "audioMain"
|
||||
master_layer = data.get("heroTrack")
|
||||
|
||||
if not master_layer:
|
||||
|
|
@ -230,23 +256,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
return
|
||||
|
||||
asset = data["asset"]
|
||||
subset = "audioMain"
|
||||
asset_name = data["asset_name"]
|
||||
|
||||
# insert family into families
|
||||
family = "audio"
|
||||
|
||||
# form label
|
||||
label = asset
|
||||
if asset != clip_name:
|
||||
label = "{} -".format(asset)
|
||||
if asset_name != clip_name:
|
||||
label += " ({}) ".format(clip_name)
|
||||
label += " {}".format(subset)
|
||||
label += " [{}]".format(family)
|
||||
|
||||
data.update({
|
||||
"name": "{}_{}".format(asset, subset),
|
||||
"label": label,
|
||||
"subset": subset,
|
||||
"asset": asset,
|
||||
"family": family,
|
||||
"families": ["clip"]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from qtpy.QtGui import QPixmap
|
|||
|
||||
import hiero.ui
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.hosts.hiero.api.otio import hiero_export
|
||||
|
||||
|
||||
|
|
@ -17,9 +18,11 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
order = pyblish.api.CollectorOrder - 0.491
|
||||
|
||||
def process(self, context):
|
||||
|
||||
asset = context.data["asset"]
|
||||
subset = "workfile"
|
||||
asset_name = asset
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset_name = asset_name.split("/")[-1]
|
||||
|
||||
active_timeline = hiero.ui.activeSequence()
|
||||
project = active_timeline.project()
|
||||
fps = active_timeline.framerate().toFloat()
|
||||
|
|
@ -27,7 +30,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
# adding otio timeline to context
|
||||
otio_timeline = hiero_export.create_otio_timeline()
|
||||
|
||||
# get workfile thumnail paths
|
||||
# get workfile thumbnail paths
|
||||
tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_")
|
||||
thumbnail_name = "workfile_thumbnail.png"
|
||||
thumbnail_path = os.path.join(tmp_staging, thumbnail_name)
|
||||
|
|
@ -49,8 +52,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
}
|
||||
|
||||
# get workfile paths
|
||||
curent_file = project.path()
|
||||
staging_dir, base_name = os.path.split(curent_file)
|
||||
current_file = project.path()
|
||||
staging_dir, base_name = os.path.split(current_file)
|
||||
|
||||
# creating workfile representation
|
||||
workfile_representation = {
|
||||
|
|
@ -59,13 +62,16 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
'files': base_name,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
|
||||
family = "workfile"
|
||||
instance_data = {
|
||||
"name": "{}_{}".format(asset, subset),
|
||||
"asset": asset,
|
||||
"subset": "{}{}".format(asset, subset.capitalize()),
|
||||
"label": "{} - {}Main".format(
|
||||
asset, family),
|
||||
"name": "{}_{}".format(asset_name, family),
|
||||
"asset": context.data["asset"],
|
||||
# TODO use 'get_subset_name'
|
||||
"subset": "{}{}Main".format(asset_name, family.capitalize()),
|
||||
"item": project,
|
||||
"family": "workfile",
|
||||
"family": family,
|
||||
"families": [],
|
||||
"representations": [workfile_representation, thumb_representation]
|
||||
}
|
||||
|
|
@ -78,7 +84,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
"activeProject": project,
|
||||
"activeTimeline": active_timeline,
|
||||
"otioTimeline": otio_timeline,
|
||||
"currentFile": curent_file,
|
||||
"currentFile": current_file,
|
||||
"colorspace": self.get_colorspace(project),
|
||||
"fps": fps
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from pyblish import api
|
||||
from openpype.client import get_assets
|
||||
|
||||
from openpype.client import get_assets, get_asset_name_identifier
|
||||
|
||||
|
||||
class CollectAssetBuilds(api.ContextPlugin):
|
||||
|
|
@ -19,10 +20,13 @@ class CollectAssetBuilds(api.ContextPlugin):
|
|||
def process(self, context):
|
||||
project_name = context.data["projectName"]
|
||||
asset_builds = {}
|
||||
for asset in get_assets(project_name):
|
||||
if asset["data"]["entityType"] == "AssetBuild":
|
||||
self.log.debug("Found \"{}\" in database.".format(asset))
|
||||
asset_builds[asset["name"]] = asset
|
||||
for asset_doc in get_assets(project_name):
|
||||
if asset_doc["data"].get("entityType") != "AssetBuild":
|
||||
continue
|
||||
|
||||
asset_name = get_asset_name_identifier(asset_doc)
|
||||
self.log.debug("Found \"{}\" in database.".format(asset_doc))
|
||||
asset_builds[asset_name] = asset_doc
|
||||
|
||||
for instance in context:
|
||||
if instance.data["family"] != "clip":
|
||||
|
|
@ -50,9 +54,7 @@ class CollectAssetBuilds(api.ContextPlugin):
|
|||
# Collect asset builds.
|
||||
data = {"assetbuilds": []}
|
||||
for name in asset_names:
|
||||
data["assetbuilds"].append(
|
||||
asset_builds[name]
|
||||
)
|
||||
data["assetbuilds"].append(asset_builds[name])
|
||||
self.log.debug(
|
||||
"Found asset builds: {}".format(data["assetbuilds"])
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ from abc import (
|
|||
)
|
||||
import six
|
||||
import hou
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.pipeline import (
|
||||
CreatorError,
|
||||
LegacyCreator,
|
||||
|
|
@ -142,12 +144,13 @@ class HoudiniCreatorBase(object):
|
|||
|
||||
@staticmethod
|
||||
def create_instance_node(
|
||||
node_name, parent,
|
||||
node_type="geometry"):
|
||||
asset_name, node_name, parent, node_type="geometry"
|
||||
):
|
||||
# type: (str, str, str) -> hou.Node
|
||||
"""Create node representing instance.
|
||||
|
||||
Arguments:
|
||||
asset_name (str): Asset name.
|
||||
node_name (str): Name of the new node.
|
||||
parent (str): Name of the parent node.
|
||||
node_type (str, optional): Type of the node.
|
||||
|
|
@ -182,8 +185,13 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase):
|
|||
if node_type is None:
|
||||
node_type = "geometry"
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset_name = instance_data["folderPath"]
|
||||
else:
|
||||
asset_name = instance_data["asset"]
|
||||
|
||||
instance_node = self.create_instance_node(
|
||||
subset_name, "/out", node_type)
|
||||
asset_name, subset_name, "/out", node_type)
|
||||
|
||||
self.customize_node_look(instance_node)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ class CreateHDA(plugin.HoudiniCreator):
|
|||
icon = "gears"
|
||||
maintain_selection = False
|
||||
|
||||
def _check_existing(self, subset_name):
|
||||
def _check_existing(self, asset_name, subset_name):
|
||||
# type: (str) -> bool
|
||||
"""Check if existing subset name versions already exists."""
|
||||
# Get all subsets of the current asset
|
||||
project_name = self.project_name
|
||||
asset_doc = get_asset_by_name(
|
||||
project_name, self.data["asset"], fields=["_id"]
|
||||
project_name, asset_name, fields=["_id"]
|
||||
)
|
||||
subset_docs = get_subsets(
|
||||
project_name, asset_ids=[asset_doc["_id"]], fields=["name"]
|
||||
|
|
@ -35,7 +35,8 @@ class CreateHDA(plugin.HoudiniCreator):
|
|||
return subset_name.lower() in existing_subset_names_low
|
||||
|
||||
def create_instance_node(
|
||||
self, node_name, parent, node_type="geometry"):
|
||||
self, asset_name, node_name, parent, node_type="geometry"
|
||||
):
|
||||
|
||||
parent_node = hou.node("/obj")
|
||||
if self.selected_nodes:
|
||||
|
|
@ -61,7 +62,7 @@ class CreateHDA(plugin.HoudiniCreator):
|
|||
hda_file_name="$HIP/{}.hda".format(node_name)
|
||||
)
|
||||
hda_node.layoutChildren()
|
||||
elif self._check_existing(node_name):
|
||||
elif self._check_existing(asset_name, node_name):
|
||||
raise plugin.OpenPypeCreatorError(
|
||||
("subset {} is already published with different HDA"
|
||||
"definition.").format(node_name))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating workfiles."""
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.hosts.houdini.api import plugin
|
||||
from openpype.hosts.houdini.api.lib import read, imprint
|
||||
from openpype.hosts.houdini.api.pipeline import CONTEXT_CONTAINER
|
||||
|
|
@ -30,16 +31,27 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator):
|
|||
task_name = self.create_context.get_current_task_name()
|
||||
host_name = self.host_name
|
||||
|
||||
if current_instance is None:
|
||||
current_instance_asset = None
|
||||
elif AYON_SERVER_ENABLED:
|
||||
current_instance_asset = current_instance["folderPath"]
|
||||
else:
|
||||
current_instance_asset = current_instance["asset"]
|
||||
|
||||
if current_instance is None:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
variant, task_name, asset_doc, project_name, host_name
|
||||
)
|
||||
data = {
|
||||
"asset": asset_name,
|
||||
"task": task_name,
|
||||
"variant": variant
|
||||
}
|
||||
if AYON_SERVER_ENABLED:
|
||||
data["folderPath"] = asset_name
|
||||
else:
|
||||
data["asset"] = asset_name
|
||||
|
||||
data.update(
|
||||
self.get_dynamic_data(
|
||||
variant, task_name, asset_doc,
|
||||
|
|
@ -51,15 +63,18 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator):
|
|||
)
|
||||
self._add_instance_to_context(current_instance)
|
||||
elif (
|
||||
current_instance["asset"] != asset_name
|
||||
or current_instance["task"] != task_name
|
||||
current_instance_asset != asset_name
|
||||
or current_instance["task"] != task_name
|
||||
):
|
||||
# Update instance context if is not the same
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
variant, task_name, asset_doc, project_name, host_name
|
||||
)
|
||||
current_instance["asset"] = asset_name
|
||||
if AYON_SERVER_ENABLED:
|
||||
current_instance["folderPath"] = asset_name
|
||||
else:
|
||||
current_instance["asset"] = asset_name
|
||||
current_instance["task"] = task_name
|
||||
current_instance["subset"] = subset_name
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import pyblish.api
|
||||
|
||||
from openpype.client import get_subset_by_name, get_asset_by_name
|
||||
from openpype.client import (
|
||||
get_subset_by_name,
|
||||
get_asset_by_name,
|
||||
get_asset_name_identifier,
|
||||
)
|
||||
import openpype.lib.usdlib as usdlib
|
||||
|
||||
|
||||
|
|
@ -51,8 +55,9 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin):
|
|||
self.log.debug("Add bootstrap for: %s" % bootstrap)
|
||||
|
||||
project_name = instance.context.data["projectName"]
|
||||
asset = get_asset_by_name(project_name, instance.data["asset"])
|
||||
assert asset, "Asset must exist: %s" % asset
|
||||
asset_name = instance.data["asset"]
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
assert asset_doc, "Asset must exist: %s" % asset_name
|
||||
|
||||
# Check which are not about to be created and don't exist yet
|
||||
required = {"shot": ["usdShot"], "asset": ["usdAsset"]}.get(bootstrap)
|
||||
|
|
@ -67,19 +72,21 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin):
|
|||
required += list(layers)
|
||||
|
||||
self.log.debug("Checking required bootstrap: %s" % required)
|
||||
for subset in required:
|
||||
if self._subset_exists(project_name, instance, subset, asset):
|
||||
for subset_name in required:
|
||||
if self._subset_exists(
|
||||
project_name, instance, subset_name, asset_doc
|
||||
):
|
||||
continue
|
||||
|
||||
self.log.debug(
|
||||
"Creating {0} USD bootstrap: {1} {2}".format(
|
||||
bootstrap, asset["name"], subset
|
||||
bootstrap, asset_name, subset_name
|
||||
)
|
||||
)
|
||||
|
||||
new = instance.context.create_instance(subset)
|
||||
new.data["subset"] = subset
|
||||
new.data["label"] = "{0} ({1})".format(subset, asset["name"])
|
||||
new = instance.context.create_instance(subset_name)
|
||||
new.data["subset"] = subset_name
|
||||
new.data["label"] = "{0} ({1})".format(subset_name, asset_name)
|
||||
new.data["family"] = "usd.bootstrap"
|
||||
new.data["comment"] = "Automated bootstrap USD file."
|
||||
new.data["publishFamilies"] = ["usd"]
|
||||
|
|
@ -91,21 +98,23 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin):
|
|||
for key in ["asset"]:
|
||||
new.data[key] = instance.data[key]
|
||||
|
||||
def _subset_exists(self, project_name, instance, subset, asset):
|
||||
def _subset_exists(self, project_name, instance, subset_name, asset_doc):
|
||||
"""Return whether subset exists in current context or in database."""
|
||||
# Allow it to be created during this publish session
|
||||
context = instance.context
|
||||
|
||||
asset_doc_name = get_asset_name_identifier(asset_doc)
|
||||
for inst in context:
|
||||
if (
|
||||
inst.data["subset"] == subset
|
||||
and inst.data["asset"] == asset["name"]
|
||||
inst.data["subset"] == subset_name
|
||||
and inst.data["asset"] == asset_doc_name
|
||||
):
|
||||
return True
|
||||
|
||||
# Or, if they already exist in the database we can
|
||||
# skip them too.
|
||||
if get_subset_by_name(
|
||||
project_name, subset, asset["_id"], fields=["_id"]
|
||||
project_name, subset_name, asset_doc["_id"], fields=["_id"]
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -1,32 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
import hou
|
||||
|
||||
|
||||
class ValidateHoudiniCommercialLicense(pyblish.api.InstancePlugin):
|
||||
"""Validate the Houdini instance runs a Commercial license.
|
||||
class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin):
|
||||
"""Validate the Houdini instance runs a non Apprentice license.
|
||||
|
||||
When extracting USD files from a non-commercial Houdini license, even with
|
||||
Houdini Indie license, the resulting files will get "scrambled" with
|
||||
a license protection and get a special .usdnc or .usdlc suffix.
|
||||
USD ROPs:
|
||||
When extracting USD files from an apprentice Houdini license,
|
||||
the resulting files will get "scrambled" with a license protection
|
||||
and get a special .usdnc suffix.
|
||||
|
||||
This currently breaks the Subset/representation pipeline so we disallow
|
||||
any publish with those licenses. Only the commercial license is valid.
|
||||
This currently breaks the Subset/representation pipeline so we disallow
|
||||
any publish with apprentice license.
|
||||
|
||||
Alembic ROPs:
|
||||
Houdini Apprentice does not export Alembic.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["usd"]
|
||||
families = ["usd", "abc"]
|
||||
hosts = ["houdini"]
|
||||
label = "Houdini Commercial License"
|
||||
label = "Houdini Apprentice License"
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
import hou
|
||||
if hou.isApprentice():
|
||||
# Find which family was matched with the plug-in
|
||||
families = {instance.data["family"]}
|
||||
families.update(instance.data.get("families", []))
|
||||
disallowed_families = families.intersection(self.families)
|
||||
families = " ".join(sorted(disallowed_families)).title()
|
||||
|
||||
license = hou.licenseCategory()
|
||||
if license != hou.licenseCategoryType.Commercial:
|
||||
raise PublishValidationError(
|
||||
("USD Publishing requires a full Commercial "
|
||||
"license. You are on: {}").format(license),
|
||||
"{} publishing requires a non apprentice license."
|
||||
.format(families),
|
||||
title=self.label)
|
||||
|
|
|
|||
|
|
@ -54,12 +54,13 @@ class ValidateSubsetName(pyblish.api.InstancePlugin,
|
|||
rop_node = hou.node(instance.data["instance_node"])
|
||||
|
||||
# Check subset name
|
||||
asset_doc = instance.data["assetEntity"]
|
||||
subset_name = get_subset_name(
|
||||
family=instance.data["family"],
|
||||
variant=instance.data["variant"],
|
||||
task_name=instance.data["task"],
|
||||
asset_doc=instance.data["assetEntity"],
|
||||
dynamic_data={"asset": instance.data["asset"]}
|
||||
asset_doc=asset_doc,
|
||||
dynamic_data={"asset": asset_doc["name"]}
|
||||
)
|
||||
|
||||
if instance.data.get("subset") != subset_name:
|
||||
|
|
@ -76,12 +77,13 @@ class ValidateSubsetName(pyblish.api.InstancePlugin,
|
|||
rop_node = hou.node(instance.data["instance_node"])
|
||||
|
||||
# Check subset name
|
||||
asset_doc = instance.data["assetEntity"]
|
||||
subset_name = get_subset_name(
|
||||
family=instance.data["family"],
|
||||
variant=instance.data["variant"],
|
||||
task_name=instance.data["task"],
|
||||
asset_doc=instance.data["assetEntity"],
|
||||
dynamic_data={"asset": instance.data["asset"]}
|
||||
asset_doc=asset_doc,
|
||||
dynamic_data={"asset": asset_doc["name"]}
|
||||
)
|
||||
|
||||
instance.data["subset"] = subset_name
|
||||
|
|
|
|||
|
|
@ -359,8 +359,6 @@ def reset_colorspace():
|
|||
colorspace_mgr.Mode = rt.Name("OCIO_Custom")
|
||||
colorspace_mgr.OCIOConfigPath = ocio_config_path
|
||||
|
||||
colorspace_mgr.OCIOConfigPath = ocio_config_path
|
||||
|
||||
|
||||
def check_colorspace():
|
||||
parent = get_main_window()
|
||||
|
|
|
|||
|
|
@ -204,6 +204,8 @@ class MaxCreator(Creator, MaxCreatorBase):
|
|||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
if pre_create_data.get("use_selection"):
|
||||
self.selected_nodes = rt.GetCurrentSelection()
|
||||
if rt.getNodeByName(subset_name):
|
||||
raise CreatorError(f"'{subset_name}' is already created..")
|
||||
|
||||
instance_node = self.create_instance_node(subset_name)
|
||||
instance_data["instance_node"] = instance_node.name
|
||||
|
|
@ -246,14 +248,25 @@ class MaxCreator(Creator, MaxCreatorBase):
|
|||
def update_instances(self, update_list):
|
||||
for created_inst, changes in update_list:
|
||||
instance_node = created_inst.get("instance_node")
|
||||
|
||||
new_values = {
|
||||
key: changes[key].new_value
|
||||
for key in changes.changed_keys
|
||||
}
|
||||
subset = new_values.get("subset", "")
|
||||
if subset and instance_node != subset:
|
||||
node = rt.getNodeByName(instance_node)
|
||||
new_subset_name = new_values["subset"]
|
||||
if rt.getNodeByName(new_subset_name):
|
||||
raise CreatorError(
|
||||
"The subset '{}' already exists.".format(
|
||||
new_subset_name))
|
||||
instance_node = new_subset_name
|
||||
created_inst["instance_node"] = instance_node
|
||||
node.name = instance_node
|
||||
|
||||
imprint(
|
||||
instance_node,
|
||||
new_values,
|
||||
created_inst.data_to_store(),
|
||||
)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
|
|
|
|||
|
|
@ -23,27 +23,36 @@ def play_preview_when_done(has_autoplay):
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def viewport_camera(camera):
|
||||
"""Set viewport camera during context
|
||||
def viewport_layout_and_camera(camera, layout="layout_1"):
|
||||
"""Set viewport layout and camera during context
|
||||
***For 3dsMax 2024+
|
||||
Args:
|
||||
camera (str): viewport camera
|
||||
layout (str): layout to use in viewport, defaults to `layout_1`
|
||||
Use None to not change viewport layout during context.
|
||||
"""
|
||||
original = rt.viewport.getCamera()
|
||||
if not original:
|
||||
original_camera = rt.viewport.getCamera()
|
||||
original_layout = rt.viewport.getLayout()
|
||||
if not original_camera:
|
||||
# if there is no original camera
|
||||
# use the current camera as original
|
||||
original = rt.getNodeByName(camera)
|
||||
original_camera = rt.getNodeByName(camera)
|
||||
review_camera = rt.getNodeByName(camera)
|
||||
try:
|
||||
if layout is not None:
|
||||
layout = rt.Name(layout)
|
||||
if rt.viewport.getLayout() != layout:
|
||||
rt.viewport.setLayout(layout)
|
||||
rt.viewport.setCamera(review_camera)
|
||||
yield
|
||||
finally:
|
||||
rt.viewport.setCamera(original)
|
||||
rt.viewport.setLayout(original_layout)
|
||||
rt.viewport.setCamera(original_camera)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def viewport_preference_setting(general_viewport,
|
||||
nitrous_manager,
|
||||
nitrous_viewport,
|
||||
vp_button_mgr):
|
||||
"""Function to set viewport setting during context
|
||||
|
|
@ -51,6 +60,7 @@ def viewport_preference_setting(general_viewport,
|
|||
Args:
|
||||
camera (str): Viewport camera for review render
|
||||
general_viewport (dict): General viewport setting
|
||||
nitrous_manager (dict): Nitrous graphic manager
|
||||
nitrous_viewport (dict): Nitrous setting for
|
||||
preview animation
|
||||
vp_button_mgr (dict): Viewport button manager Setting
|
||||
|
|
@ -64,6 +74,9 @@ def viewport_preference_setting(general_viewport,
|
|||
vp_button_mgr_original = {
|
||||
key: getattr(rt.ViewportButtonMgr, key) for key in vp_button_mgr
|
||||
}
|
||||
nitrous_manager_original = {
|
||||
key: getattr(nitrousGraphicMgr, key) for key in nitrous_manager
|
||||
}
|
||||
nitrous_viewport_original = {
|
||||
key: getattr(viewport_setting, key) for key in nitrous_viewport
|
||||
}
|
||||
|
|
@ -73,6 +86,8 @@ def viewport_preference_setting(general_viewport,
|
|||
rt.viewport.EnableSolidBackgroundColorMode(general_viewport["dspBkg"])
|
||||
for key, value in vp_button_mgr.items():
|
||||
setattr(rt.ViewportButtonMgr, key, value)
|
||||
for key, value in nitrous_manager.items():
|
||||
setattr(nitrousGraphicMgr, key, value)
|
||||
for key, value in nitrous_viewport.items():
|
||||
if nitrous_viewport[key] != nitrous_viewport_original[key]:
|
||||
setattr(viewport_setting, key, value)
|
||||
|
|
@ -83,6 +98,8 @@ def viewport_preference_setting(general_viewport,
|
|||
rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg)
|
||||
for key, value in vp_button_mgr_original.items():
|
||||
setattr(rt.ViewportButtonMgr, key, value)
|
||||
for key, value in nitrous_manager_original.items():
|
||||
setattr(nitrousGraphicMgr, key, value)
|
||||
for key, value in nitrous_viewport_original.items():
|
||||
setattr(viewport_setting, key, value)
|
||||
|
||||
|
|
@ -149,24 +166,27 @@ def _render_preview_animation_max_2024(
|
|||
|
||||
|
||||
def _render_preview_animation_max_pre_2024(
|
||||
filepath, startFrame, endFrame, percentSize, ext):
|
||||
filepath, startFrame, endFrame,
|
||||
width, height, percentSize, ext):
|
||||
"""Render viewport animation by creating bitmaps
|
||||
***For 3dsMax Version <2024
|
||||
Args:
|
||||
filepath (str): filepath without frame numbers and extension
|
||||
startFrame (int): start frame
|
||||
endFrame (int): end frame
|
||||
width (int): render resolution width
|
||||
height (int): render resolution height
|
||||
percentSize (float): render resolution multiplier by 100
|
||||
e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x
|
||||
ext (str): image extension
|
||||
Returns:
|
||||
list: Created filepaths
|
||||
"""
|
||||
|
||||
# get the screenshot
|
||||
percent = percentSize / 100.0
|
||||
res_width = int(round(rt.renderWidth * percent))
|
||||
res_height = int(round(rt.renderHeight * percent))
|
||||
viewportRatio = float(res_width / res_height)
|
||||
res_width = width * percent
|
||||
res_height = height * percent
|
||||
frame_template = "{}.{{:04}}.{}".format(filepath, ext)
|
||||
frame_template.replace("\\", "/")
|
||||
files = []
|
||||
|
|
@ -178,23 +198,29 @@ def _render_preview_animation_max_pre_2024(
|
|||
res_width, res_height, filename=filepath
|
||||
)
|
||||
dib = rt.gw.getViewportDib()
|
||||
dib_width = float(dib.width)
|
||||
dib_height = float(dib.height)
|
||||
renderRatio = float(dib_width / dib_height)
|
||||
if viewportRatio <= renderRatio:
|
||||
dib_width = rt.renderWidth
|
||||
dib_height = rt.renderHeight
|
||||
# aspect ratio
|
||||
viewportRatio = dib_width / dib_height
|
||||
renderRatio = float(res_width / res_height)
|
||||
if viewportRatio < renderRatio:
|
||||
heightCrop = (dib_width / renderRatio)
|
||||
topEdge = int((dib_height - heightCrop) / 2.0)
|
||||
tempImage_bmp = rt.bitmap(dib_width, heightCrop)
|
||||
src_box_value = rt.Box2(0, topEdge, dib_width, heightCrop)
|
||||
else:
|
||||
rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0))
|
||||
rt.copy(tempImage_bmp, preview_res)
|
||||
rt.close(tempImage_bmp)
|
||||
elif viewportRatio > renderRatio:
|
||||
widthCrop = dib_height * renderRatio
|
||||
leftEdge = int((dib_width - widthCrop) / 2.0)
|
||||
tempImage_bmp = rt.bitmap(widthCrop, dib_height)
|
||||
src_box_value = rt.Box2(0, leftEdge, dib_width, dib_height)
|
||||
rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0))
|
||||
# copy the bitmap and close it
|
||||
rt.copy(tempImage_bmp, preview_res)
|
||||
rt.close(tempImage_bmp)
|
||||
src_box_value = rt.Box2(leftEdge, 0, widthCrop, dib_height)
|
||||
rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0))
|
||||
rt.copy(tempImage_bmp, preview_res)
|
||||
rt.close(tempImage_bmp)
|
||||
else:
|
||||
rt.copy(dib, preview_res)
|
||||
rt.save(preview_res)
|
||||
rt.close(preview_res)
|
||||
rt.close(dib)
|
||||
|
|
@ -243,22 +269,25 @@ def render_preview_animation(
|
|||
if viewport_options is None:
|
||||
viewport_options = viewport_options_for_preview_animation()
|
||||
with play_preview_when_done(False):
|
||||
with viewport_camera(camera):
|
||||
with render_resolution(width, height):
|
||||
if int(get_max_version()) < 2024:
|
||||
with viewport_preference_setting(
|
||||
viewport_options["general_viewport"],
|
||||
viewport_options["nitrous_viewport"],
|
||||
viewport_options["vp_btn_mgr"]
|
||||
):
|
||||
return _render_preview_animation_max_pre_2024(
|
||||
filepath,
|
||||
start_frame,
|
||||
end_frame,
|
||||
percentSize,
|
||||
ext
|
||||
)
|
||||
else:
|
||||
with viewport_layout_and_camera(camera):
|
||||
if int(get_max_version()) < 2024:
|
||||
with viewport_preference_setting(
|
||||
viewport_options["general_viewport"],
|
||||
viewport_options["nitrous_manager"],
|
||||
viewport_options["nitrous_viewport"],
|
||||
viewport_options["vp_btn_mgr"]
|
||||
):
|
||||
return _render_preview_animation_max_pre_2024(
|
||||
filepath,
|
||||
start_frame,
|
||||
end_frame,
|
||||
width,
|
||||
height,
|
||||
percentSize,
|
||||
ext
|
||||
)
|
||||
else:
|
||||
with render_resolution(width, height):
|
||||
return _render_preview_animation_max_2024(
|
||||
filepath,
|
||||
start_frame,
|
||||
|
|
@ -299,6 +328,9 @@ def viewport_options_for_preview_animation():
|
|||
"dspBkg": True,
|
||||
"dspGrid": False
|
||||
}
|
||||
viewport_options["nitrous_manager"] = {
|
||||
"AntialiasingQuality": "None"
|
||||
}
|
||||
viewport_options["nitrous_viewport"] = {
|
||||
"VisualStyleMode": "defaultshading",
|
||||
"ViewportPreset": "highquality",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,32 @@ class CreateReview(plugin.MaxCreator):
|
|||
family = "review"
|
||||
icon = "video-camera"
|
||||
|
||||
review_width = 1920
|
||||
review_height = 1080
|
||||
percentSize = 100
|
||||
keep_images = False
|
||||
image_format = "png"
|
||||
visual_style = "Realistic"
|
||||
viewport_preset = "Quality"
|
||||
vp_texture = True
|
||||
anti_aliasing = "None"
|
||||
|
||||
def apply_settings(self, project_settings):
|
||||
settings = project_settings["max"]["CreateReview"] # noqa
|
||||
|
||||
# Take some defaults from settings
|
||||
self.review_width = settings.get("review_width", self.review_width)
|
||||
self.review_height = settings.get("review_height", self.review_height)
|
||||
self.percentSize = settings.get("percentSize", self.percentSize)
|
||||
self.keep_images = settings.get("keep_images", self.keep_images)
|
||||
self.image_format = settings.get("image_format", self.image_format)
|
||||
self.visual_style = settings.get("visual_style", self.visual_style)
|
||||
self.viewport_preset = settings.get(
|
||||
"viewport_preset", self.viewport_preset)
|
||||
self.anti_aliasing = settings.get(
|
||||
"anti_aliasing", self.anti_aliasing)
|
||||
self.vp_texture = settings.get("vp_texture", self.vp_texture)
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
# Transfer settings from pre create to instance
|
||||
creator_attributes = instance_data.setdefault(
|
||||
|
|
@ -23,6 +49,7 @@ class CreateReview(plugin.MaxCreator):
|
|||
"percentSize",
|
||||
"visualStyleMode",
|
||||
"viewportPreset",
|
||||
"antialiasingQuality",
|
||||
"vpTexture"]:
|
||||
if key in pre_create_data:
|
||||
creator_attributes[key] = pre_create_data[key]
|
||||
|
|
@ -33,7 +60,7 @@ class CreateReview(plugin.MaxCreator):
|
|||
pre_create_data)
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
image_format_enum = ["exr", "jpg", "png"]
|
||||
image_format_enum = ["exr", "jpg", "png", "tga"]
|
||||
|
||||
visual_style_preset_enum = [
|
||||
"Realistic", "Shaded", "Facets",
|
||||
|
|
@ -45,41 +72,46 @@ class CreateReview(plugin.MaxCreator):
|
|||
preview_preset_enum = [
|
||||
"Quality", "Standard", "Performance",
|
||||
"DXMode", "Customize"]
|
||||
anti_aliasing_enum = ["None", "2X", "4X", "8X"]
|
||||
|
||||
return [
|
||||
NumberDef("review_width",
|
||||
label="Review width",
|
||||
decimals=0,
|
||||
minimum=0,
|
||||
default=1920),
|
||||
default=self.review_width),
|
||||
NumberDef("review_height",
|
||||
label="Review height",
|
||||
decimals=0,
|
||||
minimum=0,
|
||||
default=1080),
|
||||
BoolDef("keepImages",
|
||||
label="Keep Image Sequences",
|
||||
default=False),
|
||||
EnumDef("imageFormat",
|
||||
image_format_enum,
|
||||
default="png",
|
||||
label="Image Format Options"),
|
||||
default=self.review_height),
|
||||
NumberDef("percentSize",
|
||||
label="Percent of Output",
|
||||
default=100,
|
||||
default=self.percentSize,
|
||||
minimum=1,
|
||||
decimals=0),
|
||||
BoolDef("keepImages",
|
||||
label="Keep Image Sequences",
|
||||
default=self.keep_images),
|
||||
EnumDef("imageFormat",
|
||||
image_format_enum,
|
||||
default=self.image_format,
|
||||
label="Image Format Options"),
|
||||
EnumDef("visualStyleMode",
|
||||
visual_style_preset_enum,
|
||||
default="Realistic",
|
||||
default=self.visual_style,
|
||||
label="Preference"),
|
||||
EnumDef("viewportPreset",
|
||||
preview_preset_enum,
|
||||
default="Quality",
|
||||
label="Pre-View Preset"),
|
||||
default=self.viewport_preset,
|
||||
label="Preview Preset"),
|
||||
EnumDef("antialiasingQuality",
|
||||
anti_aliasing_enum,
|
||||
default=self.anti_aliasing,
|
||||
label="Anti-aliasing Quality"),
|
||||
BoolDef("vpTexture",
|
||||
label="Viewport Texture",
|
||||
default=False)
|
||||
default=self.vp_texture)
|
||||
]
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ class CollectReview(pyblish.api.InstancePlugin,
|
|||
"dspBkg": attr_values.get("dspBkg"),
|
||||
"dspGrid": attr_values.get("dspGrid")
|
||||
}
|
||||
nitrous_manager = {
|
||||
"AntialiasingQuality": creator_attrs["antialiasingQuality"],
|
||||
}
|
||||
nitrous_viewport = {
|
||||
"VisualStyleMode": creator_attrs["visualStyleMode"],
|
||||
"ViewportPreset": creator_attrs["viewportPreset"],
|
||||
|
|
@ -97,6 +100,7 @@ class CollectReview(pyblish.api.InstancePlugin,
|
|||
}
|
||||
preview_data = {
|
||||
"general_viewport": general_viewport,
|
||||
"nitrous_manager": nitrous_manager,
|
||||
"nitrous_viewport": nitrous_viewport,
|
||||
"vp_btn_mgr": {"EnableButtons": False}
|
||||
}
|
||||
|
|
|
|||
133
openpype/hosts/max/plugins/publish/validate_loaded_plugin.py
Normal file
133
openpype/hosts/max/plugins/publish/validate_loaded_plugin.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validator for Loaded Plugin."""
|
||||
import os
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
from openpype.hosts.max.api.lib import get_plugins
|
||||
|
||||
|
||||
class ValidateLoadedPlugin(OptionalPyblishPluginMixin,
|
||||
pyblish.api.InstancePlugin):
|
||||
"""Validates if the specific plugin is loaded in 3ds max.
|
||||
Studio Admin(s) can add the plugins they want to check in validation
|
||||
via studio defined project settings
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
hosts = ["max"]
|
||||
label = "Validate Loaded Plugins"
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
||||
family_plugins_mapping = {}
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
"""Plugin entry point."""
|
||||
family_plugins_mapping = cls.family_plugins_mapping
|
||||
if not family_plugins_mapping:
|
||||
return
|
||||
|
||||
invalid = []
|
||||
# Find all plug-in requirements for current instance
|
||||
instance_families = {instance.data["family"]}
|
||||
instance_families.update(instance.data.get("families", []))
|
||||
cls.log.debug("Checking plug-in validation "
|
||||
f"for instance families: {instance_families}")
|
||||
all_required_plugins = set()
|
||||
|
||||
for mapping in family_plugins_mapping:
|
||||
# Check for matching families
|
||||
if not mapping:
|
||||
return
|
||||
|
||||
match_families = {fam.strip() for fam in mapping["families"]}
|
||||
has_match = "*" in match_families or match_families.intersection(
|
||||
instance_families)
|
||||
|
||||
if not has_match:
|
||||
continue
|
||||
|
||||
cls.log.debug(
|
||||
f"Found plug-in family requirements: {match_families}")
|
||||
required_plugins = [
|
||||
# match lowercase and format with os.environ to allow
|
||||
# plugin names defined by max version, e.g. {3DSMAX_VERSION}
|
||||
plugin.format(**os.environ).lower()
|
||||
for plugin in mapping["plugins"]
|
||||
# ignore empty fields in settings
|
||||
if plugin.strip()
|
||||
]
|
||||
|
||||
all_required_plugins.update(required_plugins)
|
||||
|
||||
if not all_required_plugins:
|
||||
# Instance has no plug-in requirements
|
||||
return
|
||||
|
||||
# get all DLL loaded plugins in Max and their plugin index
|
||||
available_plugins = {
|
||||
plugin_name.lower(): index for index, plugin_name in enumerate(
|
||||
get_plugins())
|
||||
}
|
||||
# validate the required plug-ins
|
||||
for plugin in sorted(all_required_plugins):
|
||||
plugin_index = available_plugins.get(plugin)
|
||||
if plugin_index is None:
|
||||
debug_msg = (
|
||||
f"Plugin {plugin} does not exist"
|
||||
" in 3dsMax Plugin List."
|
||||
)
|
||||
invalid.append((plugin, debug_msg))
|
||||
continue
|
||||
if not rt.pluginManager.isPluginDllLoaded(plugin_index):
|
||||
debug_msg = f"Plugin {plugin} not loaded."
|
||||
invalid.append((plugin, debug_msg))
|
||||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
self.log.debug("Skipping Validate Loaded Plugin...")
|
||||
return
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
bullet_point_invalid_statement = "\n".join(
|
||||
"- {}".format(message) for _, message in invalid
|
||||
)
|
||||
report = (
|
||||
"Required plugins are not loaded.\n\n"
|
||||
f"{bullet_point_invalid_statement}\n\n"
|
||||
"You can use repair action to load the plugin."
|
||||
)
|
||||
raise PublishValidationError(
|
||||
report, title="Missing Required Plugins")
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
# get all DLL loaded plugins in Max and their plugin index
|
||||
invalid = cls.get_invalid(instance)
|
||||
if not invalid:
|
||||
return
|
||||
|
||||
# get all DLL loaded plugins in Max and their plugin index
|
||||
available_plugins = {
|
||||
plugin_name.lower(): index for index, plugin_name in enumerate(
|
||||
get_plugins())
|
||||
}
|
||||
|
||||
for invalid_plugin, _ in invalid:
|
||||
plugin_index = available_plugins.get(invalid_plugin)
|
||||
|
||||
if plugin_index is None:
|
||||
cls.log.warning(
|
||||
f"Can't enable missing plugin: {invalid_plugin}")
|
||||
continue
|
||||
|
||||
if not rt.pluginManager.isPluginDllLoaded(plugin_index):
|
||||
rt.pluginManager.loadPluginDll(plugin_index)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validator for USD plugin."""
|
||||
from pyblish.api import InstancePlugin, ValidatorOrder
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from openpype.pipeline import (
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
|
||||
|
||||
def get_plugins() -> list:
|
||||
"""Get plugin list from 3ds max."""
|
||||
manager = rt.PluginManager
|
||||
count = manager.pluginDllCount
|
||||
plugin_info_list = []
|
||||
for p in range(1, count + 1):
|
||||
plugin_info = manager.pluginDllName(p)
|
||||
plugin_info_list.append(plugin_info)
|
||||
|
||||
return plugin_info_list
|
||||
|
||||
|
||||
class ValidateUSDPlugin(OptionalPyblishPluginMixin,
|
||||
InstancePlugin):
|
||||
"""Validates if USD plugin is installed or loaded in 3ds max."""
|
||||
|
||||
order = ValidatorOrder - 0.01
|
||||
families = ["model"]
|
||||
hosts = ["max"]
|
||||
label = "Validate USD Plugin loaded"
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
"""Plugin entry point."""
|
||||
|
||||
for sc in ValidateUSDPlugin.__subclasses__():
|
||||
self.log.info(sc)
|
||||
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
plugin_info = get_plugins()
|
||||
usd_import = "usdimport.dli"
|
||||
if usd_import not in plugin_info:
|
||||
raise PublishValidationError(f"USD Plugin {usd_import} not found")
|
||||
usd_export = "usdexport.dle"
|
||||
if usd_export not in plugin_info:
|
||||
raise PublishValidationError(f"USD Plugin {usd_export} not found")
|
||||
|
|
@ -156,7 +156,7 @@ class FBXExtractor:
|
|||
# Parse export options
|
||||
options = self.default_options
|
||||
options = self.parse_overrides(instance, options)
|
||||
self.log.info("Export options: {0}".format(options))
|
||||
self.log.debug("Export options: {0}".format(options))
|
||||
|
||||
# Collect the start and end including handles
|
||||
start = instance.data.get("frameStartHandle") or \
|
||||
|
|
@ -186,7 +186,7 @@ class FBXExtractor:
|
|||
template = "FBXExport{0} {1}" if key == "UpAxis" else \
|
||||
"FBXExport{0} -v {1}" # noqa
|
||||
cmd = template.format(key, value)
|
||||
self.log.info(cmd)
|
||||
self.log.debug(cmd)
|
||||
mel.eval(cmd)
|
||||
|
||||
# Never show the UI or generate a log
|
||||
|
|
|
|||
|
|
@ -62,19 +62,6 @@ SHAPE_ATTRS = {"castsShadows",
|
|||
"doubleSided",
|
||||
"opposite"}
|
||||
|
||||
RENDER_ATTRS = {"vray": {
|
||||
"node": "vraySettings",
|
||||
"prefix": "fileNamePrefix",
|
||||
"padding": "fileNamePadding",
|
||||
"ext": "imageFormatStr"
|
||||
},
|
||||
"default": {
|
||||
"node": "defaultRenderGlobals",
|
||||
"prefix": "imageFilePrefix",
|
||||
"padding": "extensionPadding"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_MATRIX = [1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ class RenderSettings(object):
|
|||
def get_image_prefix_attr(cls, renderer):
|
||||
return cls._image_prefix_nodes[renderer]
|
||||
|
||||
@staticmethod
|
||||
def get_padding_attr(renderer):
|
||||
"""Return attribute for renderer that defines frame padding amount"""
|
||||
if renderer == "vray":
|
||||
return "vraySettings.fileNamePadding"
|
||||
else:
|
||||
return "defaultRenderGlobals.extensionPadding"
|
||||
|
||||
def __init__(self, project_settings=None):
|
||||
if not project_settings:
|
||||
project_settings = get_project_settings(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import six
|
|||
from maya import cmds
|
||||
from maya.app.renderSetup.model import renderSetup
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.lib import BoolDef, Logger
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import (
|
||||
|
|
@ -271,7 +272,7 @@ class MayaCreatorBase(object):
|
|||
@six.add_metaclass(ABCMeta)
|
||||
class MayaCreator(NewCreator, MayaCreatorBase):
|
||||
|
||||
settings_name = None
|
||||
settings_category = "maya"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
|
||||
|
|
@ -317,24 +318,6 @@ class MayaCreator(NewCreator, MayaCreatorBase):
|
|||
default=True)
|
||||
]
|
||||
|
||||
def apply_settings(self, project_settings):
|
||||
"""Method called on initialization of plugin to apply settings."""
|
||||
|
||||
settings_name = self.settings_name
|
||||
if settings_name is None:
|
||||
settings_name = self.__class__.__name__
|
||||
|
||||
settings = project_settings["maya"]["create"]
|
||||
settings = settings.get(settings_name)
|
||||
if settings is None:
|
||||
self.log.debug(
|
||||
"No settings found for {}".format(self.__class__.__name__)
|
||||
)
|
||||
return
|
||||
|
||||
for key, value in settings.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
class MayaAutoCreator(AutoCreator, MayaCreatorBase):
|
||||
"""Automatically triggered creator for Maya.
|
||||
|
|
@ -343,6 +326,8 @@ class MayaAutoCreator(AutoCreator, MayaCreatorBase):
|
|||
any arguments.
|
||||
"""
|
||||
|
||||
settings_category = "maya"
|
||||
|
||||
def collect_instances(self):
|
||||
return self._default_collect_instances()
|
||||
|
||||
|
|
@ -360,6 +345,8 @@ class MayaHiddenCreator(HiddenCreator, MayaCreatorBase):
|
|||
arguments for 'create' method.
|
||||
"""
|
||||
|
||||
settings_category = "maya"
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
return MayaCreator.create(self, *args, **kwargs)
|
||||
|
||||
|
|
@ -463,14 +450,16 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase):
|
|||
# this instance will not have the `instance_node` data yet
|
||||
# until it's been saved/persisted at least once.
|
||||
project_name = self.create_context.get_current_project_name()
|
||||
|
||||
asset_name = self.create_context.get_current_asset_name()
|
||||
instance_data = {
|
||||
"asset": self.create_context.get_current_asset_name(),
|
||||
"task": self.create_context.get_current_task_name(),
|
||||
"variant": layer.name(),
|
||||
}
|
||||
asset_doc = get_asset_by_name(project_name,
|
||||
instance_data["asset"])
|
||||
if AYON_SERVER_ENABLED:
|
||||
instance_data["folderPath"] = asset_name
|
||||
else:
|
||||
instance_data["asset"] = asset_name
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
layer.name(),
|
||||
instance_data["task"],
|
||||
|
|
|
|||
|
|
@ -45,10 +45,14 @@ class CreateMultishotLayout(plugin.MayaCreator):
|
|||
above is done.
|
||||
"""
|
||||
|
||||
current_folder = get_folder_by_name(
|
||||
project_name=get_current_project_name(),
|
||||
folder_name=get_current_asset_name(),
|
||||
)
|
||||
project_name = get_current_project_name()
|
||||
folder_path = get_current_asset_name()
|
||||
if "/" in folder_path:
|
||||
current_folder = get_folder_by_path(project_name, folder_path)
|
||||
else:
|
||||
current_folder = get_folder_by_name(
|
||||
project_name, folder_name=folder_path
|
||||
)
|
||||
|
||||
current_path_parts = current_folder["path"].split("/")
|
||||
|
||||
|
|
@ -154,7 +158,7 @@ class CreateMultishotLayout(plugin.MayaCreator):
|
|||
# Create layout instance by the layout creator
|
||||
|
||||
instance_data = {
|
||||
"asset": shot["name"],
|
||||
"folderPath": shot["path"],
|
||||
"variant": layout_creator.get_default_variant()
|
||||
}
|
||||
if layout_task:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import json
|
|||
|
||||
from maya import cmds
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.hosts.maya.api import (
|
||||
lib,
|
||||
plugin
|
||||
|
|
@ -43,7 +44,11 @@ class CreateReview(plugin.MayaCreator):
|
|||
members = cmds.ls(selection=True)
|
||||
|
||||
project_name = self.project_name
|
||||
asset_doc = get_asset_by_name(project_name, instance_data["asset"])
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset_name = instance_data["folderPath"]
|
||||
else:
|
||||
asset_name = instance_data["asset"]
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
task_name = instance_data["task"]
|
||||
preset = lib.get_capture_preset(
|
||||
task_name,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class CreateUnrealSkeletalMesh(plugin.MayaCreator):
|
|||
# We reorganize the geometry that was originally added into the
|
||||
# set into either 'joints_SET' or 'geometry_SET' based on the
|
||||
# joint_hints from project settings
|
||||
members = cmds.sets(instance_node, query=True)
|
||||
members = cmds.sets(instance_node, query=True) or []
|
||||
cmds.sets(clear=instance_node)
|
||||
|
||||
geometry_set = cmds.sets(name="geometry_SET", empty=True)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating workfiles."""
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.pipeline import CreatedInstance, AutoCreator
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.client import get_asset_by_name, get_asset_name_identifier
|
||||
from openpype.hosts.maya.api import plugin
|
||||
from maya import cmds
|
||||
|
||||
|
|
@ -29,16 +30,27 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator):
|
|||
task_name = self.create_context.get_current_task_name()
|
||||
host_name = self.create_context.host_name
|
||||
|
||||
if current_instance is None:
|
||||
current_instance_asset = None
|
||||
elif AYON_SERVER_ENABLED:
|
||||
current_instance_asset = current_instance["folderPath"]
|
||||
else:
|
||||
current_instance_asset = current_instance["asset"]
|
||||
|
||||
if current_instance is None:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
variant, task_name, asset_doc, project_name, host_name
|
||||
)
|
||||
data = {
|
||||
"asset": asset_name,
|
||||
"task": task_name,
|
||||
"variant": variant
|
||||
}
|
||||
if AYON_SERVER_ENABLED:
|
||||
data["folderPath"] = asset_name
|
||||
else:
|
||||
data["asset"] = asset_name
|
||||
|
||||
data.update(
|
||||
self.get_dynamic_data(
|
||||
variant, task_name, asset_doc,
|
||||
|
|
@ -50,15 +62,20 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator):
|
|||
)
|
||||
self._add_instance_to_context(current_instance)
|
||||
elif (
|
||||
current_instance["asset"] != asset_name
|
||||
or current_instance["task"] != task_name
|
||||
current_instance_asset != asset_name
|
||||
or current_instance["task"] != task_name
|
||||
):
|
||||
# Update instance context if is not the same
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
variant, task_name, asset_doc, project_name, host_name
|
||||
)
|
||||
current_instance["asset"] = asset_name
|
||||
asset_name = get_asset_name_identifier(asset_doc)
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
current_instance["folderPath"] = asset_name
|
||||
else:
|
||||
current_instance["asset"] = asset_name
|
||||
current_instance["task"] = task_name
|
||||
current_instance["subset"] = subset_name
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from maya import cmds, mel
|
|||
import pyblish.api
|
||||
|
||||
from openpype.client import get_subset_by_name
|
||||
from openpype.pipeline import legacy_io, KnownPublishError
|
||||
from openpype.pipeline import KnownPublishError
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
|
|
@ -116,10 +116,10 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
instance.data['remove'] = True
|
||||
|
||||
else:
|
||||
task = legacy_io.Session["AVALON_TASK"]
|
||||
legacy_subset_name = task + 'Review'
|
||||
project_name = instance.context.data["projectName"]
|
||||
asset_doc = instance.context.data['assetEntity']
|
||||
project_name = legacy_io.active_project()
|
||||
task = instance.context.data["task"]
|
||||
legacy_subset_name = task + 'Review'
|
||||
subset_doc = get_subset_by_name(
|
||||
project_name,
|
||||
legacy_subset_name,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue