mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
Merge branch 'develop' into enhancement/OP-7072_Validate-Loaded-Plugins
This commit is contained in:
commit
45f0e8de59
118 changed files with 10392 additions and 358 deletions
|
|
@ -266,9 +266,57 @@ 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.
|
||||
"""
|
||||
try:
|
||||
area = next(
|
||||
area for area in bpy.context.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=bpy.context.window,
|
||||
area=area,
|
||||
region=region,
|
||||
screen=bpy.context.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
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ from openpype.pipeline import (
|
|||
LegacyCreator,
|
||||
LoaderPlugin,
|
||||
)
|
||||
from .pipeline import AVALON_CONTAINERS
|
||||
from .pipeline import (
|
||||
AVALON_CONTAINERS,
|
||||
AVALON_PROPERTY,
|
||||
)
|
||||
from .ops import (
|
||||
MainThreadItem,
|
||||
execute_in_main_thread
|
||||
|
|
@ -40,9 +43,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:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ class CreateBlendScene(plugin.Creator):
|
|||
family = "blendScene"
|
||||
icon = "cubes"
|
||||
|
||||
maintain_selection = False
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
|
|
@ -31,21 +33,20 @@ class CreateBlendScene(plugin.Creator):
|
|||
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)
|
||||
|
||||
# Create the new asset group as collection
|
||||
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)
|
||||
|
||||
# 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)
|
||||
selection = lib.get_selection(include_collections=True)
|
||||
|
||||
for data in selection:
|
||||
if isinstance(data, bpy.types.Collection):
|
||||
asset_group.children.link(data)
|
||||
elif isinstance(data, bpy.types.Object):
|
||||
asset_group.objects.link(data)
|
||||
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
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.asset_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_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)
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
from typing import Generator
|
||||
|
||||
import bpy
|
||||
|
|
@ -50,6 +49,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
for group in asset_groups:
|
||||
instance = self.create_instance(context, group)
|
||||
instance.data["instance_group"] = group
|
||||
members = []
|
||||
if isinstance(group, bpy.types.Collection):
|
||||
members = list(group.objects)
|
||||
|
|
@ -65,6 +65,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
members.append(group)
|
||||
instance[:] = members
|
||||
self.log.debug(json.dumps(instance.data, indent=4))
|
||||
self.log.debug(instance.data)
|
||||
for obj in instance:
|
||||
self.log.debug(obj)
|
||||
|
|
|
|||
|
|
@ -25,14 +25,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':
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
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):
|
||||
asset_group = instance.data["instance_group"]
|
||||
|
||||
if isinstance(asset_group, bpy.types.Collection):
|
||||
if not (asset_group.objects or asset_group.children):
|
||||
raise RuntimeError(f"Instance {instance.name} is empty.")
|
||||
elif isinstance(asset_group, bpy.types.Object):
|
||||
if not asset_group.children:
|
||||
raise RuntimeError(f"Instance {instance.name} is empty.")
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -271,7 +271,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 +317,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 +325,8 @@ class MayaAutoCreator(AutoCreator, MayaCreatorBase):
|
|||
any arguments.
|
||||
"""
|
||||
|
||||
settings_category = "maya"
|
||||
|
||||
def collect_instances(self):
|
||||
return self._default_collect_instances()
|
||||
|
||||
|
|
@ -360,6 +344,8 @@ class MayaHiddenCreator(HiddenCreator, MayaCreatorBase):
|
|||
arguments for 'create' method.
|
||||
"""
|
||||
|
||||
settings_category = "maya"
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
return MayaCreator.create(self, *args, **kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,16 @@ class ExtractFBXAnimation(publish.Extractor):
|
|||
# Export from the rig's namespace so that the exported
|
||||
# FBX does not include the namespace but preserves the node
|
||||
# names as existing in the rig workfile
|
||||
if not out_members:
|
||||
skeleton_set = [
|
||||
i for i in instance
|
||||
if i.endswith("skeletonAnim_SET")
|
||||
]
|
||||
self.log.debug(
|
||||
"Top group of animated skeleton not found in "
|
||||
"{}.\nSkipping fbx animation extraction.".format(skeleton_set))
|
||||
return
|
||||
|
||||
namespace = get_namespace(out_members[0])
|
||||
relative_out_members = [
|
||||
strip_namespace(node, namespace) for node in out_members
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from openpype.pipeline.publish import (
|
|||
PublishValidationError,
|
||||
)
|
||||
from openpype.hosts.maya.api import lib
|
||||
from openpype.hosts.maya.api.lib_rendersettings import RenderSettings
|
||||
|
||||
|
||||
def convert_to_int_or_float(string_value):
|
||||
|
|
@ -129,13 +130,13 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
layer = instance.data['renderlayer']
|
||||
cameras = instance.data.get("cameras", [])
|
||||
|
||||
# Get the node attributes for current renderer
|
||||
attrs = lib.RENDER_ATTRS.get(renderer, lib.RENDER_ATTRS['default'])
|
||||
# Prefix attribute can return None when a value was never set
|
||||
prefix = lib.get_attr_in_layer(cls.ImagePrefixes[renderer],
|
||||
layer=layer) or ""
|
||||
padding = lib.get_attr_in_layer("{node}.{padding}".format(**attrs),
|
||||
layer=layer)
|
||||
padding = lib.get_attr_in_layer(
|
||||
attr=RenderSettings.get_padding_attr(renderer),
|
||||
layer=layer
|
||||
)
|
||||
|
||||
anim_override = lib.get_attr_in_layer("defaultRenderGlobals.animation",
|
||||
layer=layer)
|
||||
|
|
@ -372,8 +373,6 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
lib.set_attribute(data["attribute"], data["values"][0], node)
|
||||
|
||||
with lib.renderlayer(layer_node):
|
||||
default = lib.RENDER_ATTRS['default']
|
||||
render_attrs = lib.RENDER_ATTRS.get(renderer, default)
|
||||
|
||||
# Repair animation must be enabled
|
||||
cmds.setAttr("defaultRenderGlobals.animation", True)
|
||||
|
|
@ -391,15 +390,13 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
default_prefix = default_prefix.replace(variant, "")
|
||||
|
||||
if renderer != "renderman":
|
||||
node = render_attrs["node"]
|
||||
prefix_attr = render_attrs["prefix"]
|
||||
|
||||
prefix_attr = RenderSettings.get_image_prefix_attr(renderer)
|
||||
fname_prefix = default_prefix
|
||||
cmds.setAttr("{}.{}".format(node, prefix_attr),
|
||||
fname_prefix, type="string")
|
||||
|
||||
# Repair padding
|
||||
padding_attr = render_attrs["padding"]
|
||||
padding_attr = RenderSettings.get_padding_attr(renderer)
|
||||
cmds.setAttr("{}.{}".format(node, padding_attr),
|
||||
cls.DEFAULT_PADDING)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.pipeline import registered_host
|
||||
|
||||
|
||||
def load_stylesheet():
|
||||
|
|
@ -49,6 +50,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
)
|
||||
|
||||
self.setWindowTitle("OpenPype")
|
||||
save_current_btn = QtWidgets.QPushButton("Save current file", self)
|
||||
workfiles_btn = QtWidgets.QPushButton("Workfiles ...", self)
|
||||
create_btn = QtWidgets.QPushButton("Create ...", self)
|
||||
publish_btn = QtWidgets.QPushButton("Publish ...", self)
|
||||
|
|
@ -70,6 +72,10 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(10, 20, 10, 20)
|
||||
|
||||
layout.addWidget(save_current_btn)
|
||||
|
||||
layout.addWidget(Spacer(15, self))
|
||||
|
||||
layout.addWidget(workfiles_btn)
|
||||
layout.addWidget(create_btn)
|
||||
layout.addWidget(publish_btn)
|
||||
|
|
@ -94,6 +100,8 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
|
||||
self.setLayout(layout)
|
||||
|
||||
save_current_btn.clicked.connect(self.on_save_current_clicked)
|
||||
save_current_btn.setShortcut(QtGui.QKeySequence.Save)
|
||||
workfiles_btn.clicked.connect(self.on_workfile_clicked)
|
||||
create_btn.clicked.connect(self.on_create_clicked)
|
||||
publish_btn.clicked.connect(self.on_publish_clicked)
|
||||
|
|
@ -106,6 +114,18 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
# reset_resolution_btn.clicked.connect(self.on_set_resolution_clicked)
|
||||
experimental_btn.clicked.connect(self.on_experimental_clicked)
|
||||
|
||||
def on_save_current_clicked(self):
|
||||
host = registered_host()
|
||||
current_file = host.get_current_workfile()
|
||||
if not current_file:
|
||||
print("Current project is not saved. "
|
||||
"Please save once first via workfiles tool.")
|
||||
host_tools.show_workfiles()
|
||||
return
|
||||
|
||||
print(f"Saving current file to: {current_file}")
|
||||
host.save_workfile(current_file)
|
||||
|
||||
def on_workfile_clicked(self):
|
||||
print("Clicked Workfile")
|
||||
host_tools.show_workfiles()
|
||||
|
|
|
|||
|
|
@ -701,6 +701,8 @@ or updating already created. Publishing will create OTIO file.
|
|||
# parent time properties
|
||||
"trackStartFrame": track_start_frame,
|
||||
"timelineOffset": timeline_offset,
|
||||
"isEditorial": True,
|
||||
|
||||
# creator_attributes
|
||||
"creator_attributes": creator_attributes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@ class CollectSequenceFrameData(
|
|||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# editorial would fail since they might not be in database yet
|
||||
is_editorial = instance.data.get("isEditorial")
|
||||
if is_editorial:
|
||||
self.log.debug("Instance is Editorial. Skipping.")
|
||||
return
|
||||
|
||||
frame_data = self.get_frame_data_from_repre_sequence(instance)
|
||||
|
||||
if not frame_data:
|
||||
|
|
|
|||
|
|
@ -30,12 +30,17 @@ class ValidateFrameRange(OptionalPyblishPluginMixin,
|
|||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# editorial would fail since they might not be in database yet
|
||||
is_editorial = instance.data.get("isEditorial")
|
||||
if is_editorial:
|
||||
self.log.debug("Instance is Editorial. Skipping.")
|
||||
return
|
||||
|
||||
if (self.skip_timelines_check and
|
||||
any(re.search(pattern, instance.data["task"])
|
||||
for pattern in self.skip_timelines_check)):
|
||||
self.log.info("Skipping for {} task".format(instance.data["task"]))
|
||||
|
||||
asset_doc = instance.data["assetEntity"]
|
||||
asset_data = asset_doc["data"]
|
||||
frame_start = asset_data["frameStart"]
|
||||
frame_end = asset_data["frameEnd"]
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ class LoadImage(plugin.Loader):
|
|||
|
||||
if pop_idx is None:
|
||||
self.log.warning(
|
||||
"Didn't found container in workfile containers. {}".format(
|
||||
"Didn't find container in workfile containers. {}".format(
|
||||
container
|
||||
)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue