Merge branch 'develop' into enhancement/OP-7072_Validate-Loaded-Plugins

This commit is contained in:
Kayla Man 2023-11-15 21:05:25 +08:00
commit 45f0e8de59
118 changed files with 10392 additions and 358 deletions

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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