mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/OP-6629_Maya-Export-Rig-Animation-as-FBX
This commit is contained in:
commit
004a032ed2
26 changed files with 1329 additions and 64 deletions
|
|
@ -38,6 +38,8 @@ from .lib import (
|
|||
|
||||
from .capture import capture
|
||||
|
||||
from .render_lib import prepare_rendering
|
||||
|
||||
|
||||
__all__ = [
|
||||
"install",
|
||||
|
|
@ -66,4 +68,5 @@ __all__ = [
|
|||
"get_selection",
|
||||
"capture",
|
||||
# "unique_name",
|
||||
"prepare_rendering",
|
||||
]
|
||||
|
|
|
|||
51
openpype/hosts/blender/api/colorspace.py
Normal file
51
openpype/hosts/blender/api/colorspace.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import attr
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
@attr.s
|
||||
class LayerMetadata(object):
|
||||
"""Data class for Render Layer metadata."""
|
||||
frameStart = attr.ib()
|
||||
frameEnd = attr.ib()
|
||||
|
||||
|
||||
@attr.s
|
||||
class RenderProduct(object):
|
||||
"""
|
||||
Getting Colorspace as Specific Render Product Parameter for submitting
|
||||
publish job.
|
||||
"""
|
||||
colorspace = attr.ib() # colorspace
|
||||
view = attr.ib() # OCIO view transform
|
||||
productName = attr.ib(default=None)
|
||||
|
||||
|
||||
class ARenderProduct(object):
|
||||
def __init__(self):
|
||||
"""Constructor."""
|
||||
# Initialize
|
||||
self.layer_data = self._get_layer_data()
|
||||
self.layer_data.products = self.get_render_products()
|
||||
|
||||
def _get_layer_data(self):
|
||||
scene = bpy.context.scene
|
||||
|
||||
return LayerMetadata(
|
||||
frameStart=int(scene.frame_start),
|
||||
frameEnd=int(scene.frame_end),
|
||||
)
|
||||
|
||||
def get_render_products(self):
|
||||
"""To be implemented by renderer class.
|
||||
This should return a list of RenderProducts.
|
||||
Returns:
|
||||
list: List of RenderProduct
|
||||
"""
|
||||
return [
|
||||
RenderProduct(
|
||||
colorspace="sRGB",
|
||||
view="ACES 1.0",
|
||||
productName=""
|
||||
)
|
||||
]
|
||||
|
|
@ -16,6 +16,7 @@ import bpy
|
|||
import bpy.utils.previews
|
||||
|
||||
from openpype import style
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.pipeline import get_current_asset_name, get_current_task_name
|
||||
from openpype.tools.utils import host_tools
|
||||
|
||||
|
|
@ -331,10 +332,11 @@ class LaunchWorkFiles(LaunchQtApp):
|
|||
|
||||
def execute(self, context):
|
||||
result = super().execute(context)
|
||||
self._window.set_context({
|
||||
"asset": get_current_asset_name(),
|
||||
"task": get_current_task_name()
|
||||
})
|
||||
if not AYON_SERVER_ENABLED:
|
||||
self._window.set_context({
|
||||
"asset": get_current_asset_name(),
|
||||
"task": get_current_task_name()
|
||||
})
|
||||
return result
|
||||
|
||||
def before_window_show(self):
|
||||
|
|
|
|||
255
openpype/hosts/blender/api/render_lib.py
Normal file
255
openpype/hosts/blender/api/render_lib.py
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
import os
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import get_current_project_name
|
||||
|
||||
|
||||
def get_default_render_folder(settings):
|
||||
"""Get default render folder from blender settings."""
|
||||
|
||||
return (settings["blender"]
|
||||
["RenderSettings"]
|
||||
["default_render_image_folder"])
|
||||
|
||||
|
||||
def get_aov_separator(settings):
|
||||
"""Get aov separator from blender settings."""
|
||||
|
||||
aov_sep = (settings["blender"]
|
||||
["RenderSettings"]
|
||||
["aov_separator"])
|
||||
|
||||
if aov_sep == "dash":
|
||||
return "-"
|
||||
elif aov_sep == "underscore":
|
||||
return "_"
|
||||
elif aov_sep == "dot":
|
||||
return "."
|
||||
else:
|
||||
raise ValueError(f"Invalid aov separator: {aov_sep}")
|
||||
|
||||
|
||||
def get_image_format(settings):
|
||||
"""Get image format from blender settings."""
|
||||
|
||||
return (settings["blender"]
|
||||
["RenderSettings"]
|
||||
["image_format"])
|
||||
|
||||
|
||||
def get_multilayer(settings):
|
||||
"""Get multilayer from blender settings."""
|
||||
|
||||
return (settings["blender"]
|
||||
["RenderSettings"]
|
||||
["multilayer_exr"])
|
||||
|
||||
|
||||
def get_render_product(output_path, name, aov_sep):
|
||||
"""
|
||||
Generate the path to the render product. Blender interprets the `#`
|
||||
as the frame number, when it renders.
|
||||
|
||||
Args:
|
||||
file_path (str): The path to the blender scene.
|
||||
render_folder (str): The render folder set in settings.
|
||||
file_name (str): The name of the blender scene.
|
||||
instance (pyblish.api.Instance): The instance to publish.
|
||||
ext (str): The image format to render.
|
||||
"""
|
||||
filepath = os.path.join(output_path, name)
|
||||
render_product = f"{filepath}{aov_sep}beauty.####"
|
||||
render_product = render_product.replace("\\", "/")
|
||||
|
||||
return render_product
|
||||
|
||||
|
||||
def set_render_format(ext, multilayer):
|
||||
# Set Blender to save the file with the right extension
|
||||
bpy.context.scene.render.use_file_extension = True
|
||||
|
||||
image_settings = bpy.context.scene.render.image_settings
|
||||
|
||||
if ext == "exr":
|
||||
image_settings.file_format = (
|
||||
"OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR")
|
||||
elif ext == "bmp":
|
||||
image_settings.file_format = "BMP"
|
||||
elif ext == "rgb":
|
||||
image_settings.file_format = "IRIS"
|
||||
elif ext == "png":
|
||||
image_settings.file_format = "PNG"
|
||||
elif ext == "jpeg":
|
||||
image_settings.file_format = "JPEG"
|
||||
elif ext == "jp2":
|
||||
image_settings.file_format = "JPEG2000"
|
||||
elif ext == "tga":
|
||||
image_settings.file_format = "TARGA"
|
||||
elif ext == "tif":
|
||||
image_settings.file_format = "TIFF"
|
||||
|
||||
|
||||
def set_render_passes(settings):
|
||||
aov_list = (settings["blender"]
|
||||
["RenderSettings"]
|
||||
["aov_list"])
|
||||
|
||||
custom_passes = (settings["blender"]
|
||||
["RenderSettings"]
|
||||
["custom_passes"])
|
||||
|
||||
vl = bpy.context.view_layer
|
||||
|
||||
vl.use_pass_combined = "combined" in aov_list
|
||||
vl.use_pass_z = "z" in aov_list
|
||||
vl.use_pass_mist = "mist" in aov_list
|
||||
vl.use_pass_normal = "normal" in aov_list
|
||||
vl.use_pass_diffuse_direct = "diffuse_light" in aov_list
|
||||
vl.use_pass_diffuse_color = "diffuse_color" in aov_list
|
||||
vl.use_pass_glossy_direct = "specular_light" in aov_list
|
||||
vl.use_pass_glossy_color = "specular_color" in aov_list
|
||||
vl.eevee.use_pass_volume_direct = "volume_light" in aov_list
|
||||
vl.use_pass_emit = "emission" in aov_list
|
||||
vl.use_pass_environment = "environment" in aov_list
|
||||
vl.use_pass_shadow = "shadow" in aov_list
|
||||
vl.use_pass_ambient_occlusion = "ao" in aov_list
|
||||
|
||||
cycles = vl.cycles
|
||||
|
||||
cycles.denoising_store_passes = "denoising" in aov_list
|
||||
cycles.use_pass_volume_direct = "volume_direct" in aov_list
|
||||
cycles.use_pass_volume_indirect = "volume_indirect" in aov_list
|
||||
|
||||
aovs_names = [aov.name for aov in vl.aovs]
|
||||
for cp in custom_passes:
|
||||
cp_name = cp[0]
|
||||
if cp_name not in aovs_names:
|
||||
aov = vl.aovs.add()
|
||||
aov.name = cp_name
|
||||
else:
|
||||
aov = vl.aovs[cp_name]
|
||||
aov.type = cp[1].get("type", "VALUE")
|
||||
|
||||
return aov_list, custom_passes
|
||||
|
||||
|
||||
def set_node_tree(output_path, name, aov_sep, ext, multilayer):
|
||||
# Set the scene to use the compositor node tree to render
|
||||
bpy.context.scene.use_nodes = True
|
||||
|
||||
tree = bpy.context.scene.node_tree
|
||||
|
||||
# Get the Render Layers node
|
||||
rl_node = None
|
||||
for node in tree.nodes:
|
||||
if node.bl_idname == "CompositorNodeRLayers":
|
||||
rl_node = node
|
||||
break
|
||||
|
||||
# If there's not a Render Layers node, we create it
|
||||
if not rl_node:
|
||||
rl_node = tree.nodes.new("CompositorNodeRLayers")
|
||||
|
||||
# Get the enabled output sockets, that are the active passes for the
|
||||
# render.
|
||||
# We also exclude some layers.
|
||||
exclude_sockets = ["Image", "Alpha", "Noisy Image"]
|
||||
passes = [
|
||||
socket
|
||||
for socket in rl_node.outputs
|
||||
if socket.enabled and socket.name not in exclude_sockets
|
||||
]
|
||||
|
||||
# Remove all output nodes
|
||||
for node in tree.nodes:
|
||||
if node.bl_idname == "CompositorNodeOutputFile":
|
||||
tree.nodes.remove(node)
|
||||
|
||||
# Create a new output node
|
||||
output = tree.nodes.new("CompositorNodeOutputFile")
|
||||
|
||||
image_settings = bpy.context.scene.render.image_settings
|
||||
output.format.file_format = image_settings.file_format
|
||||
|
||||
# In case of a multilayer exr, we don't need to use the output node,
|
||||
# because the blender render already outputs a multilayer exr.
|
||||
if ext == "exr" and multilayer:
|
||||
output.layer_slots.clear()
|
||||
return []
|
||||
|
||||
output.file_slots.clear()
|
||||
output.base_path = output_path
|
||||
|
||||
aov_file_products = []
|
||||
|
||||
# For each active render pass, we add a new socket to the output node
|
||||
# and link it
|
||||
for render_pass in passes:
|
||||
filepath = f"{name}{aov_sep}{render_pass.name}.####"
|
||||
|
||||
output.file_slots.new(filepath)
|
||||
|
||||
aov_file_products.append(
|
||||
(render_pass.name, os.path.join(output_path, filepath)))
|
||||
|
||||
node_input = output.inputs[-1]
|
||||
|
||||
tree.links.new(render_pass, node_input)
|
||||
|
||||
return aov_file_products
|
||||
|
||||
|
||||
def imprint_render_settings(node, data):
|
||||
RENDER_DATA = "render_data"
|
||||
if not node.get(RENDER_DATA):
|
||||
node[RENDER_DATA] = {}
|
||||
for key, value in data.items():
|
||||
if value is None:
|
||||
continue
|
||||
node[RENDER_DATA][key] = value
|
||||
|
||||
|
||||
def prepare_rendering(asset_group):
|
||||
name = asset_group.name
|
||||
|
||||
filepath = bpy.data.filepath
|
||||
assert filepath, "Workfile not saved. Please save the file first."
|
||||
|
||||
file_path = os.path.dirname(filepath)
|
||||
file_name = os.path.basename(filepath)
|
||||
file_name, _ = os.path.splitext(file_name)
|
||||
|
||||
project = get_current_project_name()
|
||||
settings = get_project_settings(project)
|
||||
|
||||
render_folder = get_default_render_folder(settings)
|
||||
aov_sep = get_aov_separator(settings)
|
||||
ext = get_image_format(settings)
|
||||
multilayer = get_multilayer(settings)
|
||||
|
||||
set_render_format(ext, multilayer)
|
||||
aov_list, custom_passes = set_render_passes(settings)
|
||||
|
||||
output_path = os.path.join(file_path, render_folder, file_name)
|
||||
|
||||
render_product = get_render_product(output_path, name, aov_sep)
|
||||
aov_file_product = set_node_tree(
|
||||
output_path, name, aov_sep, ext, multilayer)
|
||||
|
||||
bpy.context.scene.render.filepath = render_product
|
||||
|
||||
render_settings = {
|
||||
"render_folder": render_folder,
|
||||
"aov_separator": aov_sep,
|
||||
"image_format": ext,
|
||||
"multilayer_exr": multilayer,
|
||||
"aov_list": aov_list,
|
||||
"custom_passes": custom_passes,
|
||||
"render_product": render_product,
|
||||
"aov_file_product": aov_file_product,
|
||||
"review": True,
|
||||
}
|
||||
|
||||
imprint_render_settings(asset_group, render_settings)
|
||||
53
openpype/hosts/blender/plugins/create/create_render.py
Normal file
53
openpype/hosts/blender/plugins/create/create_render.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""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.render_lib import prepare_rendering
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
|
||||
|
||||
class CreateRenderlayer(plugin.Creator):
|
||||
"""Single baked camera"""
|
||||
|
||||
name = "renderingMain"
|
||||
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)
|
||||
|
||||
try:
|
||||
instances.children.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
prepare_rendering(asset_group)
|
||||
except Exception:
|
||||
# Remove the instance if there was an error
|
||||
bpy.data.collections.remove(asset_group)
|
||||
raise
|
||||
|
||||
# TODO: this is undesiderable, but it's the only way to be sure that
|
||||
# the file is saved before the render starts.
|
||||
# Blender, by design, doesn't set the file as dirty if modifications
|
||||
# happen by script. So, when creating the instance and setting the
|
||||
# render settings, the file is not marked as dirty. This means that
|
||||
# there is the risk of sending to deadline a file without the right
|
||||
# settings. Even the validator to check that the file is saved will
|
||||
# detect the file as saved, even if it isn't. The only solution for
|
||||
# now it is to force the file to be saved.
|
||||
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
|
||||
|
||||
return asset_group
|
||||
123
openpype/hosts/blender/plugins/publish/collect_render.py
Normal file
123
openpype/hosts/blender/plugins/publish/collect_render.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect render data."""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.hosts.blender.api import colorspace
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectBlenderRender(pyblish.api.InstancePlugin):
|
||||
"""Gather all publishable render layers from renderSetup."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.01
|
||||
hosts = ["blender"]
|
||||
families = ["render"]
|
||||
label = "Collect Render Layers"
|
||||
sync_workfile_version = False
|
||||
|
||||
@staticmethod
|
||||
def generate_expected_beauty(
|
||||
render_product, frame_start, frame_end, frame_step, ext
|
||||
):
|
||||
"""
|
||||
Generate the expected files for the render product for the beauty
|
||||
render. This returns a list of files that should be rendered. It
|
||||
replaces the sequence of `#` with the frame number.
|
||||
"""
|
||||
path = os.path.dirname(render_product)
|
||||
file = os.path.basename(render_product)
|
||||
|
||||
expected_files = []
|
||||
|
||||
for frame in range(frame_start, frame_end + 1, frame_step):
|
||||
frame_str = str(frame).rjust(4, "0")
|
||||
filename = re.sub("#+", frame_str, file)
|
||||
expected_file = f"{os.path.join(path, filename)}.{ext}"
|
||||
expected_files.append(expected_file.replace("\\", "/"))
|
||||
|
||||
return {
|
||||
"beauty": expected_files
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def generate_expected_aovs(
|
||||
aov_file_product, frame_start, frame_end, frame_step, ext
|
||||
):
|
||||
"""
|
||||
Generate the expected files for the render product for the beauty
|
||||
render. This returns a list of files that should be rendered. It
|
||||
replaces the sequence of `#` with the frame number.
|
||||
"""
|
||||
expected_files = {}
|
||||
|
||||
for aov_name, aov_file in aov_file_product:
|
||||
path = os.path.dirname(aov_file)
|
||||
file = os.path.basename(aov_file)
|
||||
|
||||
aov_files = []
|
||||
|
||||
for frame in range(frame_start, frame_end + 1, frame_step):
|
||||
frame_str = str(frame).rjust(4, "0")
|
||||
filename = re.sub("#+", frame_str, file)
|
||||
expected_file = f"{os.path.join(path, filename)}.{ext}"
|
||||
aov_files.append(expected_file.replace("\\", "/"))
|
||||
|
||||
expected_files[aov_name] = aov_files
|
||||
|
||||
return expected_files
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
|
||||
render_data = bpy.data.collections[str(instance)].get("render_data")
|
||||
|
||||
assert render_data, "No render data found."
|
||||
|
||||
self.log.info(f"render_data: {dict(render_data)}")
|
||||
|
||||
render_product = render_data.get("render_product")
|
||||
aov_file_product = render_data.get("aov_file_product")
|
||||
ext = render_data.get("image_format")
|
||||
multilayer = render_data.get("multilayer_exr")
|
||||
|
||||
frame_start = context.data["frameStart"]
|
||||
frame_end = context.data["frameEnd"]
|
||||
frame_handle_start = context.data["frameStartHandle"]
|
||||
frame_handle_end = context.data["frameEndHandle"]
|
||||
|
||||
expected_beauty = self.generate_expected_beauty(
|
||||
render_product, int(frame_start), int(frame_end),
|
||||
int(bpy.context.scene.frame_step), ext)
|
||||
|
||||
expected_aovs = self.generate_expected_aovs(
|
||||
aov_file_product, int(frame_start), int(frame_end),
|
||||
int(bpy.context.scene.frame_step), ext)
|
||||
|
||||
expected_files = expected_beauty | expected_aovs
|
||||
|
||||
instance.data.update({
|
||||
"family": "render.farm",
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"frameStartHandle": frame_handle_start,
|
||||
"frameEndHandle": frame_handle_end,
|
||||
"fps": context.data["fps"],
|
||||
"byFrameStep": bpy.context.scene.frame_step,
|
||||
"review": render_data.get("review", False),
|
||||
"multipartExr": ext == "exr" and multilayer,
|
||||
"farm": True,
|
||||
"expectedFiles": [expected_files],
|
||||
# OCIO not currently implemented in Blender, but the following
|
||||
# settings are required by the schema, so it is hardcoded.
|
||||
# TODO: Implement OCIO in Blender
|
||||
"colorspaceConfig": "",
|
||||
"colorspaceDisplay": "sRGB",
|
||||
"colorspaceView": "ACES 1.0 SDR-video",
|
||||
"renderProducts": colorspace.ARenderProduct(),
|
||||
})
|
||||
|
||||
self.log.info(f"data: {instance.data}")
|
||||
|
|
@ -9,7 +9,8 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
|||
label = "Increment Workfile Version"
|
||||
optional = True
|
||||
hosts = ["blender"]
|
||||
families = ["animation", "model", "rig", "action", "layout", "blendScene"]
|
||||
families = ["animation", "model", "rig", "action", "layout", "blendScene",
|
||||
"render"]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import os
|
||||
|
||||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from openpype.hosts.blender.api.render_lib import prepare_rendering
|
||||
|
||||
|
||||
class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validates Render File Directory is
|
||||
not the same in every submission
|
||||
"""
|
||||
|
||||
order = ValidateContentsOrder
|
||||
families = ["render.farm"]
|
||||
hosts = ["blender"]
|
||||
label = "Validate Render Output for Deadline"
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
filepath = bpy.data.filepath
|
||||
file = os.path.basename(filepath)
|
||||
filename, ext = os.path.splitext(file)
|
||||
if filename not in bpy.context.scene.render.filepath:
|
||||
raise PublishValidationError(
|
||||
"Render output folder "
|
||||
"doesn't match the blender scene name! "
|
||||
"Use Repair action to "
|
||||
"fix the folder file path.."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
container = bpy.data.collections[str(instance)]
|
||||
prepare_rendering(container)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
|
||||
cls.log.debug("Reset the render output folder...")
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class ValidateFileSaved(pyblish.api.InstancePlugin):
|
||||
"""Validate that the workfile has been saved."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
hosts = ["blender"]
|
||||
label = "Validate File Saved"
|
||||
optional = False
|
||||
exclude_families = []
|
||||
|
||||
def process(self, instance):
|
||||
if [ef for ef in self.exclude_families
|
||||
if instance.data["family"] in ef]:
|
||||
return
|
||||
if bpy.data.is_dirty:
|
||||
raise RuntimeError("Workfile is not saved.")
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin):
|
||||
"""Validate that there is a camera set as active for rendering."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
hosts = ["blender"]
|
||||
families = ["render"]
|
||||
label = "Validate Render Camera Is Set"
|
||||
optional = False
|
||||
|
||||
def process(self, instance):
|
||||
if not bpy.context.scene.camera:
|
||||
raise RuntimeError("No camera is active for rendering.")
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Submitting render job to Deadline."""
|
||||
|
||||
import os
|
||||
import getpass
|
||||
import attr
|
||||
from datetime import datetime
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.lib import is_running_from_build
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline.farm.tools import iter_expected_files
|
||||
from openpype.tests.lib import is_in_tests
|
||||
|
||||
from openpype_modules.deadline import abstract_submit_deadline
|
||||
from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
|
||||
|
||||
|
||||
@attr.s
|
||||
class BlenderPluginInfo():
|
||||
SceneFile = attr.ib(default=None) # Input
|
||||
Version = attr.ib(default=None) # Mandatory for Deadline
|
||||
SaveFile = attr.ib(default=True)
|
||||
|
||||
|
||||
class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline):
|
||||
label = "Submit Render to Deadline"
|
||||
hosts = ["blender"]
|
||||
families = ["render.farm"]
|
||||
|
||||
use_published = True
|
||||
priority = 50
|
||||
chunk_size = 1
|
||||
jobInfo = {}
|
||||
pluginInfo = {}
|
||||
group = None
|
||||
|
||||
def get_job_info(self):
|
||||
job_info = DeadlineJobInfo(Plugin="Blender")
|
||||
|
||||
job_info.update(self.jobInfo)
|
||||
|
||||
instance = self._instance
|
||||
context = instance.context
|
||||
|
||||
# Always use the original work file name for the Job name even when
|
||||
# rendering is done from the published Work File. The original work
|
||||
# file name is clearer because it can also have subversion strings,
|
||||
# etc. which are stripped for the published file.
|
||||
src_filepath = context.data["currentFile"]
|
||||
src_filename = os.path.basename(src_filepath)
|
||||
|
||||
if is_in_tests():
|
||||
src_filename += datetime.now().strftime("%d%m%Y%H%M%S")
|
||||
|
||||
job_info.Name = f"{src_filename} - {instance.name}"
|
||||
job_info.BatchName = src_filename
|
||||
instance.data.get("blenderRenderPlugin", "Blender")
|
||||
job_info.UserName = context.data.get("deadlineUser", getpass.getuser())
|
||||
|
||||
# Deadline requires integers in frame range
|
||||
frames = "{start}-{end}x{step}".format(
|
||||
start=int(instance.data["frameStartHandle"]),
|
||||
end=int(instance.data["frameEndHandle"]),
|
||||
step=int(instance.data["byFrameStep"]),
|
||||
)
|
||||
job_info.Frames = frames
|
||||
|
||||
job_info.Pool = instance.data.get("primaryPool")
|
||||
job_info.SecondaryPool = instance.data.get("secondaryPool")
|
||||
job_info.Comment = context.data.get("comment")
|
||||
job_info.Priority = instance.data.get("priority", self.priority)
|
||||
|
||||
if self.group != "none" and self.group:
|
||||
job_info.Group = self.group
|
||||
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
render_globals = instance.data.setdefault("renderGlobals", {})
|
||||
machine_list = attr_values.get("machineList", "")
|
||||
if machine_list:
|
||||
if attr_values.get("whitelist", True):
|
||||
machine_list_key = "Whitelist"
|
||||
else:
|
||||
machine_list_key = "Blacklist"
|
||||
render_globals[machine_list_key] = machine_list
|
||||
|
||||
job_info.Priority = attr_values.get("priority")
|
||||
job_info.ChunkSize = attr_values.get("chunkSize")
|
||||
|
||||
# Add options from RenderGlobals
|
||||
render_globals = instance.data.get("renderGlobals", {})
|
||||
job_info.update(render_globals)
|
||||
|
||||
keys = [
|
||||
"FTRACK_API_KEY",
|
||||
"FTRACK_API_USER",
|
||||
"FTRACK_SERVER",
|
||||
"OPENPYPE_SG_USER",
|
||||
"AVALON_PROJECT",
|
||||
"AVALON_ASSET",
|
||||
"AVALON_TASK",
|
||||
"AVALON_APP_NAME",
|
||||
"OPENPYPE_DEV"
|
||||
"IS_TEST"
|
||||
]
|
||||
|
||||
# Add OpenPype version if we are running from build.
|
||||
if is_running_from_build():
|
||||
keys.append("OPENPYPE_VERSION")
|
||||
|
||||
# Add mongo url if it's enabled
|
||||
if self._instance.context.data.get("deadlinePassMongoUrl"):
|
||||
keys.append("OPENPYPE_MONGO")
|
||||
|
||||
environment = dict({key: os.environ[key] for key in keys
|
||||
if key in os.environ}, **legacy_io.Session)
|
||||
|
||||
for key in keys:
|
||||
value = environment.get(key)
|
||||
if not value:
|
||||
continue
|
||||
job_info.EnvironmentKeyValue[key] = value
|
||||
|
||||
# to recognize job from PYPE for turning Event On/Off
|
||||
job_info.add_render_job_env_var()
|
||||
job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1"
|
||||
|
||||
# Adding file dependencies.
|
||||
if self.asset_dependencies:
|
||||
dependencies = instance.context.data["fileDependencies"]
|
||||
for dependency in dependencies:
|
||||
job_info.AssetDependency += dependency
|
||||
|
||||
# Add list of expected files to job
|
||||
# ---------------------------------
|
||||
exp = instance.data.get("expectedFiles")
|
||||
for filepath in iter_expected_files(exp):
|
||||
job_info.OutputDirectory += os.path.dirname(filepath)
|
||||
job_info.OutputFilename += os.path.basename(filepath)
|
||||
|
||||
return job_info
|
||||
|
||||
def get_plugin_info(self):
|
||||
plugin_info = BlenderPluginInfo(
|
||||
SceneFile=self.scene_path,
|
||||
Version=bpy.app.version_string,
|
||||
SaveFile=True,
|
||||
)
|
||||
|
||||
plugin_payload = attr.asdict(plugin_info)
|
||||
|
||||
# Patching with pluginInfo from settings
|
||||
for key, value in self.pluginInfo.items():
|
||||
plugin_payload[key] = value
|
||||
|
||||
return plugin_payload
|
||||
|
||||
def process_submission(self):
|
||||
instance = self._instance
|
||||
|
||||
expected_files = instance.data["expectedFiles"]
|
||||
if not expected_files:
|
||||
raise RuntimeError("No Render Elements found!")
|
||||
|
||||
first_file = next(iter_expected_files(expected_files))
|
||||
output_dir = os.path.dirname(first_file)
|
||||
instance.data["outputDir"] = output_dir
|
||||
instance.data["toBeRenderedOn"] = "deadline"
|
||||
|
||||
payload = self.assemble_payload()
|
||||
return self.submit(payload)
|
||||
|
||||
def from_published_scene(self):
|
||||
"""
|
||||
This is needed to set the correct path for the json metadata. Because
|
||||
the rendering path is set in the blend file during the collection,
|
||||
and the path is adjusted to use the published scene, this ensures that
|
||||
the metadata and the rendered files are in the same location.
|
||||
"""
|
||||
return super().from_published_scene(False)
|
||||
|
|
@ -96,7 +96,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
targets = ["local"]
|
||||
|
||||
hosts = ["fusion", "max", "maya", "nuke", "houdini",
|
||||
"celaction", "aftereffects", "harmony"]
|
||||
"celaction", "aftereffects", "harmony", "blender"]
|
||||
|
||||
families = ["render.farm", "render.frames_farm",
|
||||
"prerender.farm", "prerender.frames_farm",
|
||||
|
|
@ -107,6 +107,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
"redshift_rop"]
|
||||
|
||||
aov_filter = {"maya": [r".*([Bb]eauty).*"],
|
||||
"blender": [r".*([Bb]eauty).*"],
|
||||
"aftereffects": [r".*"], # for everything from AE
|
||||
"harmony": [r".*"], # for everything from AE
|
||||
"celaction": [r".*"],
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@
|
|||
"rules": {}
|
||||
}
|
||||
},
|
||||
"RenderSettings": {
|
||||
"default_render_image_folder": "renders/blender",
|
||||
"aov_separator": "underscore",
|
||||
"image_format": "exr",
|
||||
"multilayer_exr": true,
|
||||
"aov_list": [],
|
||||
"custom_passes": []
|
||||
},
|
||||
"workfile_builder": {
|
||||
"create_first_version": false,
|
||||
"custom_templates": []
|
||||
|
|
@ -27,6 +35,22 @@
|
|||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ValidateFileSaved": {
|
||||
"enabled": true,
|
||||
"optional": false,
|
||||
"active": true,
|
||||
"exclude_families": []
|
||||
},
|
||||
"ValidateRenderCameraIsSet": {
|
||||
"enabled": true,
|
||||
"optional": false,
|
||||
"active": true
|
||||
},
|
||||
"ValidateDeadlinePublish": {
|
||||
"enabled": true,
|
||||
"optional": false,
|
||||
"active": true
|
||||
},
|
||||
"ValidateMeshHasUvs": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
|
|
|
|||
|
|
@ -99,6 +99,15 @@
|
|||
"deadline_chunk_size": 10,
|
||||
"deadline_job_delay": "00:00:00:00"
|
||||
},
|
||||
"BlenderSubmitDeadline": {
|
||||
"enabled": true,
|
||||
"optional": false,
|
||||
"active": true,
|
||||
"use_published": true,
|
||||
"priority": 50,
|
||||
"chunk_size": 10,
|
||||
"group": "none"
|
||||
},
|
||||
"ProcessSubmittedJobOnFarm": {
|
||||
"enabled": true,
|
||||
"deadline_department": "",
|
||||
|
|
@ -112,6 +121,9 @@
|
|||
"maya": [
|
||||
".*([Bb]eauty).*"
|
||||
],
|
||||
"blender": [
|
||||
".*([Bb]eauty).*"
|
||||
],
|
||||
"aftereffects": [
|
||||
".*"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -54,6 +54,110 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "RenderSettings",
|
||||
"label": "Render Settings",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "default_render_image_folder",
|
||||
"label": "Default render image folder"
|
||||
},
|
||||
{
|
||||
"key": "aov_separator",
|
||||
"label": "AOV Separator Character",
|
||||
"type": "enum",
|
||||
"multiselection": false,
|
||||
"defaults": "underscore",
|
||||
"enum_items": [
|
||||
{"dash": "- (dash)"},
|
||||
{"underscore": "_ (underscore)"},
|
||||
{"dot": ". (dot)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "image_format",
|
||||
"label": "Output Image Format",
|
||||
"type": "enum",
|
||||
"multiselection": false,
|
||||
"defaults": "exr",
|
||||
"enum_items": [
|
||||
{"exr": "OpenEXR"},
|
||||
{"bmp": "BMP"},
|
||||
{"rgb": "Iris"},
|
||||
{"png": "PNG"},
|
||||
{"jpg": "JPEG"},
|
||||
{"jp2": "JPEG 2000"},
|
||||
{"tga": "Targa"},
|
||||
{"tif": "TIFF"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "multilayer_exr",
|
||||
"type": "boolean",
|
||||
"label": "Multilayer (EXR)"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Note: Multilayer EXR is only used when output format type set to EXR."
|
||||
},
|
||||
{
|
||||
"key": "aov_list",
|
||||
"label": "AOVs to create",
|
||||
"type": "enum",
|
||||
"multiselection": true,
|
||||
"defaults": "empty",
|
||||
"enum_items": [
|
||||
{"empty": "< empty >"},
|
||||
{"combined": "Combined"},
|
||||
{"z": "Z"},
|
||||
{"mist": "Mist"},
|
||||
{"normal": "Normal"},
|
||||
{"diffuse_light": "Diffuse Light"},
|
||||
{"diffuse_color": "Diffuse Color"},
|
||||
{"specular_light": "Specular Light"},
|
||||
{"specular_color": "Specular Color"},
|
||||
{"volume_light": "Volume Light"},
|
||||
{"emission": "Emission"},
|
||||
{"environment": "Environment"},
|
||||
{"shadow": "Shadow"},
|
||||
{"ao": "Ambient Occlusion"},
|
||||
{"denoising": "Denoising"},
|
||||
{"volume_direct": "Direct Volumetric Scattering"},
|
||||
{"volume_indirect": "Indirect Volumetric Scattering"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Add custom AOVs. They are added to the view layer and in the Compositing Nodetree,\nbut they need to be added manually to the Shader Nodetree."
|
||||
},
|
||||
{
|
||||
"type": "dict-modifiable",
|
||||
"store_as_list": true,
|
||||
"key": "custom_passes",
|
||||
"label": "Custom Passes",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "type",
|
||||
"label": "Type",
|
||||
"type": "enum",
|
||||
"multiselection": false,
|
||||
"default": "COLOR",
|
||||
"enum_items": [
|
||||
{"COLOR": "Color"},
|
||||
{"VALUE": "Value"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_workfile_options",
|
||||
|
|
|
|||
|
|
@ -531,6 +531,50 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "BlenderSubmitDeadline",
|
||||
"label": "Blender Submit to Deadline",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "active",
|
||||
"label": "Active"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "use_published",
|
||||
"label": "Use Published scene"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "priority",
|
||||
"label": "Priority"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "chunk_size",
|
||||
"label": "Frame per Task"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "group",
|
||||
"label": "Group Name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,39 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "ValidateFileSaved",
|
||||
"label": "Validate File Saved",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "active",
|
||||
"label": "Active"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"key": "exclude_families",
|
||||
"label": "Exclude Families",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "collapsible-wrap",
|
||||
"label": "Model",
|
||||
|
|
@ -46,6 +79,66 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "collapsible-wrap",
|
||||
"label": "Render",
|
||||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "ValidateRenderCameraIsSet",
|
||||
"label": "Validate Render Camera Is Set",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "active",
|
||||
"label": "Active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "ValidateDeadlinePublish",
|
||||
"label": "Validate Render Output for Deadline",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "active",
|
||||
"label": "Active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -914,10 +914,12 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
|
||||
# Controller actions
|
||||
@abstractmethod
|
||||
def open_workfile(self, filepath):
|
||||
"""Open a workfile.
|
||||
def open_workfile(self, folder_id, task_id, filepath):
|
||||
"""Open a workfile for context.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
task_id (str): Task id.
|
||||
filepath (str): Workfile path.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -452,12 +452,12 @@ class BaseWorkfileController(
|
|||
self._emit_event("controller.refresh.finished")
|
||||
|
||||
# Controller actions
|
||||
def open_workfile(self, filepath):
|
||||
def open_workfile(self, folder_id, task_id, filepath):
|
||||
self._emit_event("open_workfile.started")
|
||||
|
||||
failed = False
|
||||
try:
|
||||
self._host_open_workfile(filepath)
|
||||
self._open_workfile(folder_id, task_id, filepath)
|
||||
|
||||
except Exception:
|
||||
failed = True
|
||||
|
|
@ -575,6 +575,53 @@ class BaseWorkfileController(
|
|||
self._expected_selection.get_expected_selection_data(),
|
||||
)
|
||||
|
||||
def _get_event_context_data(
|
||||
self, project_name, folder_id, task_id, folder=None, task=None
|
||||
):
|
||||
if folder is None:
|
||||
folder = self.get_folder_entity(folder_id)
|
||||
if task is None:
|
||||
task = self.get_task_entity(task_id)
|
||||
# NOTE keys should be OpenPype compatible
|
||||
return {
|
||||
"project_name": project_name,
|
||||
"folder_id": folder_id,
|
||||
"asset_id": folder_id,
|
||||
"asset_name": folder["name"],
|
||||
"task_id": task_id,
|
||||
"task_name": task["name"],
|
||||
"host_name": self.get_host_name(),
|
||||
}
|
||||
|
||||
def _open_workfile(self, folder_id, task_id, filepath):
|
||||
project_name = self.get_current_project_name()
|
||||
event_data = self._get_event_context_data(
|
||||
project_name, folder_id, task_id
|
||||
)
|
||||
event_data["filepath"] = filepath
|
||||
|
||||
emit_event("workfile.open.before", event_data, source="workfiles.tool")
|
||||
|
||||
# Change context
|
||||
task_name = event_data["task_name"]
|
||||
if (
|
||||
folder_id != self.get_current_folder_id()
|
||||
or task_name != self.get_current_task_name()
|
||||
):
|
||||
# Use OpenPype asset-like object
|
||||
asset_doc = get_asset_by_id(
|
||||
event_data["project_name"],
|
||||
event_data["folder_id"],
|
||||
)
|
||||
change_current_context(
|
||||
asset_doc,
|
||||
event_data["task_name"]
|
||||
)
|
||||
|
||||
self._host_open_workfile(filepath)
|
||||
|
||||
emit_event("workfile.open.after", event_data, source="workfiles.tool")
|
||||
|
||||
def _save_as_workfile(
|
||||
self,
|
||||
folder_id,
|
||||
|
|
@ -591,18 +638,14 @@ class BaseWorkfileController(
|
|||
task_name = task["name"]
|
||||
|
||||
# QUESTION should the data be different for 'before' and 'after'?
|
||||
# NOTE keys should be OpenPype compatible
|
||||
event_data = {
|
||||
"project_name": project_name,
|
||||
"folder_id": folder_id,
|
||||
"asset_id": folder_id,
|
||||
"asset_name": folder["name"],
|
||||
"task_id": task_id,
|
||||
"task_name": task_name,
|
||||
"host_name": self.get_host_name(),
|
||||
event_data = self._get_event_context_data(
|
||||
project_name, folder_id, task_id, folder, task
|
||||
)
|
||||
event_data.update({
|
||||
"filename": filename,
|
||||
"workdir_path": workdir,
|
||||
}
|
||||
})
|
||||
|
||||
emit_event("workfile.save.before", event_data, source="workfiles.tool")
|
||||
|
||||
# Create workfiles root folder
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
self._on_published_cancel_clicked)
|
||||
|
||||
self._selected_folder_id = None
|
||||
self._selected_tak_name = None
|
||||
self._selected_task_id = None
|
||||
self._selected_task_name = None
|
||||
|
||||
self._pre_select_folder_id = None
|
||||
self._pre_select_task_name = None
|
||||
|
|
@ -178,7 +179,7 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
# -------------------------------------------------------------
|
||||
# Workarea workfiles
|
||||
# -------------------------------------------------------------
|
||||
def _open_workfile(self, filepath):
|
||||
def _open_workfile(self, folder_id, task_name, filepath):
|
||||
if self._controller.has_unsaved_changes():
|
||||
result = self._save_changes_prompt()
|
||||
if result is None:
|
||||
|
|
@ -186,12 +187,15 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
if result:
|
||||
self._controller.save_current_workfile()
|
||||
self._controller.open_workfile(filepath)
|
||||
self._controller.open_workfile(folder_id, task_name, filepath)
|
||||
|
||||
def _on_workarea_open_clicked(self):
|
||||
path = self._workarea_widget.get_selected_path()
|
||||
if path:
|
||||
self._open_workfile(path)
|
||||
if not path:
|
||||
return
|
||||
folder_id = self._selected_folder_id
|
||||
task_id = self._selected_task_id
|
||||
self._open_workfile(folder_id, task_id, path)
|
||||
|
||||
def _on_current_open_requests(self):
|
||||
self._on_workarea_open_clicked()
|
||||
|
|
@ -238,8 +242,12 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
}
|
||||
|
||||
filepath = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
|
||||
if filepath:
|
||||
self._open_workfile(filepath)
|
||||
if not filepath:
|
||||
return
|
||||
|
||||
folder_id = self._selected_folder_id
|
||||
task_id = self._selected_task_id
|
||||
self._open_workfile(folder_id, task_id, filepath)
|
||||
|
||||
def _on_workarea_save_clicked(self):
|
||||
result = self._exec_save_as_dialog()
|
||||
|
|
@ -279,10 +287,11 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
def _on_task_changed(self, event):
|
||||
self._selected_folder_id = event["folder_id"]
|
||||
self._selected_tak_name = event["task_name"]
|
||||
self._selected_task_id = event["task_id"]
|
||||
self._selected_task_name = event["task_name"]
|
||||
self._valid_selected_context = (
|
||||
self._selected_folder_id is not None
|
||||
and self._selected_tak_name is not None
|
||||
and self._selected_task_id is not None
|
||||
)
|
||||
self._update_published_btns_state()
|
||||
|
||||
|
|
@ -311,7 +320,7 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
if enabled:
|
||||
self._pre_select_folder_id = self._selected_folder_id
|
||||
self._pre_select_task_name = self._selected_tak_name
|
||||
self._pre_select_task_name = self._selected_task_name
|
||||
else:
|
||||
self._pre_select_folder_id = None
|
||||
self._pre_select_task_name = None
|
||||
|
|
@ -334,7 +343,7 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
return True
|
||||
if self._pre_select_task_name is None:
|
||||
return False
|
||||
return self._pre_select_task_name != self._selected_tak_name
|
||||
return self._pre_select_task_name != self._selected_task_name
|
||||
|
||||
def _on_published_cancel_clicked(self):
|
||||
folder_id = self._pre_select_folder_id
|
||||
|
|
|
|||
|
|
@ -176,11 +176,10 @@ class PublishReportMaker:
|
|||
self._create_discover_result = None
|
||||
self._convert_discover_result = None
|
||||
self._publish_discover_result = None
|
||||
self._plugin_data = []
|
||||
self._plugin_data_with_plugin = []
|
||||
|
||||
self._stored_plugins = set()
|
||||
self._current_plugin_data = []
|
||||
self._plugin_data_by_id = {}
|
||||
self._current_plugin = None
|
||||
self._current_plugin_data = {}
|
||||
self._all_instances_by_id = {}
|
||||
self._current_context = None
|
||||
|
||||
|
|
@ -192,9 +191,9 @@ class PublishReportMaker:
|
|||
create_context.convertor_discover_result
|
||||
)
|
||||
self._publish_discover_result = create_context.publish_discover_result
|
||||
self._plugin_data = []
|
||||
self._plugin_data_with_plugin = []
|
||||
self._stored_plugins = set()
|
||||
|
||||
self._plugin_data_by_id = {}
|
||||
self._current_plugin = None
|
||||
self._current_plugin_data = {}
|
||||
self._all_instances_by_id = {}
|
||||
self._current_context = context
|
||||
|
|
@ -211,18 +210,11 @@ class PublishReportMaker:
|
|||
if self._current_plugin_data:
|
||||
self._current_plugin_data["passed"] = True
|
||||
|
||||
self._current_plugin = plugin
|
||||
self._current_plugin_data = self._add_plugin_data_item(plugin)
|
||||
|
||||
def _get_plugin_data_item(self, plugin):
|
||||
store_item = None
|
||||
for item in self._plugin_data_with_plugin:
|
||||
if item["plugin"] is plugin:
|
||||
store_item = item["data"]
|
||||
break
|
||||
return store_item
|
||||
|
||||
def _add_plugin_data_item(self, plugin):
|
||||
if plugin in self._stored_plugins:
|
||||
if plugin.id in self._plugin_data_by_id:
|
||||
# A plugin would be processed more than once. What can cause it:
|
||||
# - there is a bug in controller
|
||||
# - plugin class is imported into multiple files
|
||||
|
|
@ -230,15 +222,9 @@ class PublishReportMaker:
|
|||
raise ValueError(
|
||||
"Plugin '{}' is already stored".format(str(plugin)))
|
||||
|
||||
self._stored_plugins.add(plugin)
|
||||
|
||||
plugin_data_item = self._create_plugin_data_item(plugin)
|
||||
self._plugin_data_by_id[plugin.id] = plugin_data_item
|
||||
|
||||
self._plugin_data_with_plugin.append({
|
||||
"plugin": plugin,
|
||||
"data": plugin_data_item
|
||||
})
|
||||
self._plugin_data.append(plugin_data_item)
|
||||
return plugin_data_item
|
||||
|
||||
def _create_plugin_data_item(self, plugin):
|
||||
|
|
@ -279,7 +265,7 @@ class PublishReportMaker:
|
|||
"""Add result of single action."""
|
||||
plugin = result["plugin"]
|
||||
|
||||
store_item = self._get_plugin_data_item(plugin)
|
||||
store_item = self._plugin_data_by_id.get(plugin.id)
|
||||
if store_item is None:
|
||||
store_item = self._add_plugin_data_item(plugin)
|
||||
|
||||
|
|
@ -301,14 +287,24 @@ class PublishReportMaker:
|
|||
instance, instance in self._current_context
|
||||
)
|
||||
|
||||
plugins_data = copy.deepcopy(self._plugin_data)
|
||||
if plugins_data and not plugins_data[-1]["passed"]:
|
||||
plugins_data[-1]["passed"] = True
|
||||
plugins_data_by_id = copy.deepcopy(
|
||||
self._plugin_data_by_id
|
||||
)
|
||||
|
||||
# Ensure the current plug-in is marked as `passed` in the result
|
||||
# so that it shows on reports for paused publishes
|
||||
if self._current_plugin is not None:
|
||||
current_plugin_data = plugins_data_by_id.get(
|
||||
self._current_plugin.id
|
||||
)
|
||||
if current_plugin_data and not current_plugin_data["passed"]:
|
||||
current_plugin_data["passed"] = True
|
||||
|
||||
if publish_plugins:
|
||||
for plugin in publish_plugins:
|
||||
if plugin not in self._stored_plugins:
|
||||
plugins_data.append(self._create_plugin_data_item(plugin))
|
||||
if plugin.id not in plugins_data_by_id:
|
||||
plugins_data_by_id[plugin.id] = \
|
||||
self._create_plugin_data_item(plugin)
|
||||
|
||||
reports = []
|
||||
if self._create_discover_result is not None:
|
||||
|
|
@ -329,7 +325,7 @@ class PublishReportMaker:
|
|||
)
|
||||
|
||||
return {
|
||||
"plugins_data": plugins_data,
|
||||
"plugins_data": list(plugins_data_by_id.values()),
|
||||
"instances": instances_details,
|
||||
"context": self._extract_context_data(self._current_context),
|
||||
"crashed_file_paths": crashed_file_paths,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ from .publish_plugins import (
|
|||
PublishPuginsModel,
|
||||
DEFAULT_BLENDER_PUBLISH_SETTINGS
|
||||
)
|
||||
from .render_settings import (
|
||||
RenderSettingsModel,
|
||||
DEFAULT_RENDER_SETTINGS
|
||||
)
|
||||
|
||||
|
||||
class UnitScaleSettingsModel(BaseSettingsModel):
|
||||
|
|
@ -37,6 +41,8 @@ class BlenderSettings(BaseSettingsModel):
|
|||
default_factory=BlenderImageIOModel,
|
||||
title="Color Management (ImageIO)"
|
||||
)
|
||||
render_settings: RenderSettingsModel = Field(
|
||||
default_factory=RenderSettingsModel, title="Render Settings")
|
||||
workfile_builder: TemplateWorkfileBaseOptions = Field(
|
||||
default_factory=TemplateWorkfileBaseOptions,
|
||||
title="Workfile Builder"
|
||||
|
|
@ -55,6 +61,7 @@ DEFAULT_VALUES = {
|
|||
},
|
||||
"set_frames_startup": True,
|
||||
"set_resolution_startup": True,
|
||||
"render_settings": DEFAULT_RENDER_SETTINGS,
|
||||
"publish": DEFAULT_BLENDER_PUBLISH_SETTINGS,
|
||||
"workfile_builder": {
|
||||
"create_first_version": False,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,16 @@ class ValidatePluginModel(BaseSettingsModel):
|
|||
active: bool = Field(title="Active")
|
||||
|
||||
|
||||
class ValidateFileSavedModel(BaseSettingsModel):
|
||||
enabled: bool = Field(title="ValidateFileSaved")
|
||||
optional: bool = Field(title="Optional")
|
||||
active: bool = Field(title="Active")
|
||||
exclude_families: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="Exclude product types"
|
||||
)
|
||||
|
||||
|
||||
class ExtractBlendModel(BaseSettingsModel):
|
||||
enabled: bool = Field(True)
|
||||
optional: bool = Field(title="Optional")
|
||||
|
|
@ -53,6 +63,21 @@ class PublishPuginsModel(BaseSettingsModel):
|
|||
title="Validate Camera Zero Keyframe",
|
||||
section="Validators"
|
||||
)
|
||||
ValidateFileSaved: ValidateFileSavedModel = Field(
|
||||
default_factory=ValidateFileSavedModel,
|
||||
title="Validate File Saved",
|
||||
section="Validators"
|
||||
)
|
||||
ValidateRenderCameraIsSet: ValidatePluginModel = Field(
|
||||
default_factory=ValidatePluginModel,
|
||||
title="Validate Render Camera Is Set",
|
||||
section="Validators"
|
||||
)
|
||||
ValidateDeadlinePublish: ValidatePluginModel = Field(
|
||||
default_factory=ValidatePluginModel,
|
||||
title="Validate Render Output for Deadline",
|
||||
section="Validators"
|
||||
)
|
||||
ValidateMeshHasUvs: ValidatePluginModel = Field(
|
||||
default_factory=ValidatePluginModel,
|
||||
title="Validate Mesh Has Uvs"
|
||||
|
|
@ -118,6 +143,22 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = {
|
|||
"optional": True,
|
||||
"active": True
|
||||
},
|
||||
"ValidateFileSaved": {
|
||||
"enabled": True,
|
||||
"optional": False,
|
||||
"active": True,
|
||||
"exclude_families": []
|
||||
},
|
||||
"ValidateRenderCameraIsSet": {
|
||||
"enabled": True,
|
||||
"optional": False,
|
||||
"active": True
|
||||
},
|
||||
"ValidateDeadlinePublish": {
|
||||
"enabled": True,
|
||||
"optional": False,
|
||||
"active": True
|
||||
},
|
||||
"ValidateMeshHasUvs": {
|
||||
"enabled": True,
|
||||
"optional": True,
|
||||
|
|
|
|||
109
server_addon/blender/server/settings/render_settings.py
Normal file
109
server_addon/blender/server/settings/render_settings.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""Providing models and values for Blender Render Settings."""
|
||||
from pydantic import Field
|
||||
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
|
||||
|
||||
def aov_separators_enum():
|
||||
return [
|
||||
{"value": "dash", "label": "- (dash)"},
|
||||
{"value": "underscore", "label": "_ (underscore)"},
|
||||
{"value": "dot", "label": ". (dot)"}
|
||||
]
|
||||
|
||||
|
||||
def image_format_enum():
|
||||
return [
|
||||
{"value": "exr", "label": "OpenEXR"},
|
||||
{"value": "bmp", "label": "BMP"},
|
||||
{"value": "rgb", "label": "Iris"},
|
||||
{"value": "png", "label": "PNG"},
|
||||
{"value": "jpg", "label": "JPEG"},
|
||||
{"value": "jp2", "label": "JPEG 2000"},
|
||||
{"value": "tga", "label": "Targa"},
|
||||
{"value": "tif", "label": "TIFF"},
|
||||
]
|
||||
|
||||
|
||||
def aov_list_enum():
|
||||
return [
|
||||
{"value": "empty", "label": "< none >"},
|
||||
{"value": "combined", "label": "Combined"},
|
||||
{"value": "z", "label": "Z"},
|
||||
{"value": "mist", "label": "Mist"},
|
||||
{"value": "normal", "label": "Normal"},
|
||||
{"value": "diffuse_light", "label": "Diffuse Light"},
|
||||
{"value": "diffuse_color", "label": "Diffuse Color"},
|
||||
{"value": "specular_light", "label": "Specular Light"},
|
||||
{"value": "specular_color", "label": "Specular Color"},
|
||||
{"value": "volume_light", "label": "Volume Light"},
|
||||
{"value": "emission", "label": "Emission"},
|
||||
{"value": "environment", "label": "Environment"},
|
||||
{"value": "shadow", "label": "Shadow"},
|
||||
{"value": "ao", "label": "Ambient Occlusion"},
|
||||
{"value": "denoising", "label": "Denoising"},
|
||||
{"value": "volume_direct", "label": "Direct Volumetric Scattering"},
|
||||
{"value": "volume_indirect", "label": "Indirect Volumetric Scattering"}
|
||||
]
|
||||
|
||||
|
||||
def custom_passes_types_enum():
|
||||
return [
|
||||
{"value": "COLOR", "label": "Color"},
|
||||
{"value": "VALUE", "label": "Value"},
|
||||
]
|
||||
|
||||
|
||||
class CustomPassesModel(BaseSettingsModel):
|
||||
"""Custom Passes"""
|
||||
_layout = "compact"
|
||||
|
||||
attribute: str = Field("", title="Attribute name")
|
||||
value: str = Field(
|
||||
"COLOR",
|
||||
title="Type",
|
||||
enum_resolver=custom_passes_types_enum
|
||||
)
|
||||
|
||||
|
||||
class RenderSettingsModel(BaseSettingsModel):
|
||||
default_render_image_folder: str = Field(
|
||||
title="Default Render Image Folder"
|
||||
)
|
||||
aov_separator: str = Field(
|
||||
"underscore",
|
||||
title="AOV Separator Character",
|
||||
enum_resolver=aov_separators_enum
|
||||
)
|
||||
image_format: str = Field(
|
||||
"exr",
|
||||
title="Image Format",
|
||||
enum_resolver=image_format_enum
|
||||
)
|
||||
multilayer_exr: bool = Field(
|
||||
title="Multilayer (EXR)"
|
||||
)
|
||||
aov_list: list[str] = Field(
|
||||
default_factory=list,
|
||||
enum_resolver=aov_list_enum,
|
||||
title="AOVs to create"
|
||||
)
|
||||
custom_passes: list[CustomPassesModel] = Field(
|
||||
default_factory=list,
|
||||
title="Custom Passes",
|
||||
description=(
|
||||
"Add custom AOVs. They are added to the view layer and in the "
|
||||
"Compositing Nodetree,\nbut they need to be added manually to "
|
||||
"the Shader Nodetree."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_RENDER_SETTINGS = {
|
||||
"default_render_image_folder": "renders/blender",
|
||||
"aov_separator": "underscore",
|
||||
"image_format": "exr",
|
||||
"multilayer_exr": True,
|
||||
"aov_list": [],
|
||||
"custom_passes": []
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.1"
|
||||
__version__ = "0.1.3"
|
||||
|
|
|
|||
|
|
@ -208,6 +208,16 @@ class CelactionSubmitDeadlineModel(BaseSettingsModel):
|
|||
)
|
||||
|
||||
|
||||
class BlenderSubmitDeadlineModel(BaseSettingsModel):
|
||||
enabled: bool = Field(True)
|
||||
optional: bool = Field(title="Optional")
|
||||
active: bool = Field(title="Active")
|
||||
use_published: bool = Field(title="Use Published scene")
|
||||
priority: int = Field(title="Priority")
|
||||
chunk_size: int = Field(title="Frame per Task")
|
||||
group: str = Field("", title="Group Name")
|
||||
|
||||
|
||||
class AOVFilterSubmodel(BaseSettingsModel):
|
||||
_layout = "expanded"
|
||||
name: str = Field(title="Host")
|
||||
|
|
@ -276,8 +286,10 @@ class PublishPluginsModel(BaseSettingsModel):
|
|||
title="After Effects to deadline")
|
||||
CelactionSubmitDeadline: CelactionSubmitDeadlineModel = Field(
|
||||
default_factory=CelactionSubmitDeadlineModel,
|
||||
title="Celaction Submit Deadline"
|
||||
)
|
||||
title="Celaction Submit Deadline")
|
||||
BlenderSubmitDeadline: BlenderSubmitDeadlineModel = Field(
|
||||
default_factory=BlenderSubmitDeadlineModel,
|
||||
title="Blender Submit Deadline")
|
||||
ProcessSubmittedJobOnFarm: ProcessSubmittedJobOnFarmModel = Field(
|
||||
default_factory=ProcessSubmittedJobOnFarmModel,
|
||||
title="Process submitted job on farm.")
|
||||
|
|
@ -384,6 +396,15 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = {
|
|||
"deadline_chunk_size": 10,
|
||||
"deadline_job_delay": "00:00:00:00"
|
||||
},
|
||||
"BlenderSubmitDeadline": {
|
||||
"enabled": True,
|
||||
"optional": False,
|
||||
"active": True,
|
||||
"use_published": True,
|
||||
"priority": 50,
|
||||
"chunk_size": 10,
|
||||
"group": "none"
|
||||
},
|
||||
"ProcessSubmittedJobOnFarm": {
|
||||
"enabled": True,
|
||||
"deadline_department": "",
|
||||
|
|
@ -400,6 +421,12 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = {
|
|||
".*([Bb]eauty).*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "blender",
|
||||
"value": [
|
||||
".*([Bb]eauty).*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "aftereffects",
|
||||
"value": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue