diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index bc0e00f740..c01ab5122c 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -35,6 +35,9 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
+ - 3.18.8-nightly.1
+ - 3.18.7
+ - 3.18.7-nightly.5
- 3.18.7-nightly.4
- 3.18.7-nightly.3
- 3.18.7-nightly.2
@@ -132,9 +135,6 @@ body:
- 3.15.11-nightly.3
- 3.15.11-nightly.2
- 3.15.11-nightly.1
- - 3.15.10
- - 3.15.10-nightly.2
- - 3.15.10-nightly.1
validations:
required: true
- type: dropdown
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 009150ae7d..4ec3448570 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,407 @@
# Changelog
+## [3.18.7](https://github.com/ynput/OpenPype/tree/3.18.7)
+
+
+[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.6...3.18.7)
+
+### **🆕 New features**
+
+
+
+Chore: Wrapper for click proposal #5928
+
+This is a proposal how to resolve issues with `click` python module. Issue https://github.com/ynput/OpenPype/issues/5921 reported that in Houdini 20+ is our click clashing with click in houdini, where is expected higher version. We can't update our version to support older pythons (NOTE older Python 3).
+
+
+___
+
+
+
+### **🚀 Enhancements**
+
+
+
+Maya: Add repair action to hidden joints validator #6214
+
+Joints Hidden is missing repair action, this adds it back
+
+
+___
+
+
+
+
+
+Blender: output node and EXR #6086
+
+Output node now works correctly for Multilayer EXR and keeps existing links. The output now is handled entirely by the compositor node tree.
+
+
+___
+
+
+
+
+
+AYON Switch tool: Keep version after switch #6104
+
+Keep version if only representation did change. The AYON variant of https://github.com/ynput/OpenPype/pull/4629
+
+
+___
+
+
+
+
+
+Loader AYON: Reset loader window on open #6170
+
+Make sure loader tool is reset on each show.
+
+
+___
+
+
+
+
+
+Publisher: Show message with error on action failure #6179
+
+This PR adds support for the publisher to show error message from running actions.Errors from actions will otherwise be hidden from user in various console outputs.Also include card for when action is finished.
+
+
+___
+
+
+
+
+
+AYON Applications: Remove djvview group from default applications #6188
+
+The djv does not have group defined in models so the values are not used anywhere.
+
+
+___
+
+
+
+
+
+General: added fallback for broken ffprobe return #6189
+
+Customer provided .exr returned width and height equal to 0 which caused error in `extract_thumbnail`. This tries to use oiiotool to get metadata about file, in our case it read it correctly.
+
+
+___
+
+
+
+
+
+Photoshop: High scaling in UIs #6190
+
+Use `get_openpype_qt_app` to create `QApplication` in Photoshop.
+
+
+___
+
+
+
+
+
+Ftrack: Status update settings are not case insensitive. #6195
+
+Make values for project_settings/ftrack/events/status_update case insensitive.
+
+
+___
+
+
+
+
+
+Thumbnail product filtering #6197
+
+This PR introduces subset filtering for thumbnail extraction. This is to skip passes like zdepth which is not needed and can cause issues with extraction. Also speeds up publishing.
+
+
+___
+
+
+
+
+
+TimersManager: Idle dialog always on top #6201
+
+Make stop timer dialog always on tophttps://app.clickup.com/t/6658547/OP-8033
+
+
+___
+
+
+
+
+
+AfterEffects: added toggle for applying values from DB during creation #6204
+
+Previously values (resolution, duration) from Asset (eg. DB) were applied explicitly when instance of `render` product type was created. This PR adds toggle to Settings to disable this. (This allows artist to publish non standard length of composition, disabling of `Validate Scene Settings` is still required.)
+
+
+___
+
+
+
+
+
+Unreal: Update plugin commit #6208
+
+Updated unreal plugin to latest main.
+
+
+___
+
+
+
+### **🐛 Bug fixes**
+
+
+
+Traypublisher: editorial avoid audio tracks processing #6038
+
+Avoiding audio tracks from EDL editorial publishing.
+
+
+___
+
+
+
+
+
+Resolve Inventory offsets clips when swapping versions #6128
+
+Swapped version retain the offset and IDT of the timelime clip.closes: https://github.com/ynput/OpenPype/issues/6125
+
+
+___
+
+
+
+
+
+Publisher window as dialog #6176
+
+Changing back Publisher window to QDialog.
+
+
+___
+
+
+
+
+
+Nuke: Validate write node fix error report - OP-8088 #6183
+
+Report error was not printing the expected values from settings, but instead the values on the write node, leading to confusing messages like:
+```
+Traceback (most recent call last):
+ File "C:\Users\tokejepsen\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process
+ runner(*args)
+ File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 135, in process
+ self._make_error(check)
+ File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 149, in _make_error
+ raise PublishXmlValidationError(
+openpype.pipeline.publish.publish_plugins.PublishXmlValidationError: Write node's knobs values are not correct!
+Knob 'channels' > Correct: `rgb` > Wrong: `rgb`
+```
+This PR changes the error report to:
+```
+Traceback (most recent call last):
+ File "C:\Users\tokejepsen\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process
+ runner(*args)
+ File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 135, in process
+ self._make_error(check)
+ File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 149, in _make_error
+ raise PublishXmlValidationError(
+openpype.pipeline.publish.publish_plugins.PublishXmlValidationError: Write node's knobs values are not correct!
+Knob 'channels' > Expected: `['rg']` > Current: `rgb`
+```
+
+
+
+___
+
+
+
+
+
+Nuke: Camera product type loaded is not updating - OP-7973 #6184
+
+When updating the camera this error would appear:
+```
+(...)openpype/hosts/nuke/plugins/load/load_camera_abc.py", line 142, in update
+ camera_node = nuke.toNode(object_name)
+TypeError: toNode() argument 1 must be str, not Node
+```
+
+
+
+___
+
+
+
+
+
+AYON settings: Use bundle name as variant in dev mode #6187
+
+Make sure the bundle name is used in dev mode for settings variant.
+
+
+___
+
+
+
+
+
+Fusion: fix unwanted change to field name in Settings #6193
+
+It should be `image_format` but in previous refactoring PR it fell back to original `output_formats` which caused enum not to show up and propagate into plugin.
+
+
+___
+
+
+
+
+
+Bugfix: AYON menu disappeared when the workspace has been changed in 3dsMax #6200
+
+AYON plugins are not correctly registered when switching to different workspaces.
+
+
+___
+
+
+
+
+
+TrayPublisher: adding settings category to base creator classes #6202
+
+Settings are resolving correctly as they suppose to.
+
+
+___
+
+
+
+
+
+Nuke: expose knobs backward compatibility fix - OP-8164 #6211
+
+Fix backwards compatibility for settings `project_settings/nuke/create/CreateWriteRender/exposed_knobs`.
+
+
+___
+
+
+
+
+
+AE: fix local render doesn't push thumbnail to Ftrack #6212
+
+Without thumbnail review is not clickable from main Versions list
+
+
+___
+
+
+
+
+
+Nuke: openpype expose knobs validator - OP-8166 #6213
+
+Fix exposed knobs validator for backwards compatibility with missing settings.
+
+
+___
+
+
+
+
+
+Ftrack: Post-launch hook fix value lowering #6221
+
+Fix lowerin of values in status mapping.
+
+
+___
+
+
+
+### **🔀 Refactored code**
+
+
+
+Maya: Remove `shelf` class and shelf build on maya `userSetup.py` #5837
+
+Remove shelf builder logic. It appeared to be unused and had bugs.
+
+
+___
+
+
+
+### **Merged pull requests**
+
+
+
+Max: updated implementation of save_scene + small QOL improvements to host #6186
+
+- Removed `has_unsaved_changes` from Max host as it looks to have been unused and unimplemented.
+- Added and implemented `workfile_has_unsaved_changes` to Max host.
+- Mirrored the Houdini host to implement the above into `save_scene` publish for Max.
+- Added a line to `startup.ms` which opens the usual 'default' menu inside of Max (see screenshots).Current (Likely opens this menu due to one or more of the startup scripts used to insert OP menu):New:
+
+
+___
+
+
+
+
+
+Fusion: Use better resolution of Ayon apps on 4k display #6199
+
+Changes size (makes it smaller) of Ayon apps (Workfiles, Loader) in Fusion on high definitions displays.
+
+
+___
+
+
+
+
+
+Update CONTRIBUTING.md #6210
+
+Updating contributing guidelines to reflect the EOL state of repository
+___
+
+
+
+
+
+Deadline: Remove redundant instance_skeleton_data code - OP-8269 #6219
+
+This PR https://github.com/ynput/OpenPype/pull/5186 re-introduced code about for the `instance_skeleton_data` but its actually not used since this variable gets overwritten later.
+
+
+___
+
+
+
+
+
+
## [3.18.6](https://github.com/ynput/OpenPype/tree/3.18.6)
diff --git a/openpype/hosts/blender/api/render_lib.py b/openpype/hosts/blender/api/render_lib.py
index b437078ad8..c09cfce502 100644
--- a/openpype/hosts/blender/api/render_lib.py
+++ b/openpype/hosts/blender/api/render_lib.py
@@ -2,6 +2,7 @@ from pathlib import Path
import bpy
+from openpype import AYON_SERVER_ENABLED
from openpype.settings import get_project_settings
from openpype.pipeline import get_current_project_name
@@ -47,6 +48,22 @@ def get_multilayer(settings):
["multilayer_exr"])
+def get_renderer(settings):
+ """Get renderer from blender settings."""
+
+ return (settings["blender"]
+ ["RenderSettings"]
+ ["renderer"])
+
+
+def get_compositing(settings):
+ """Get compositing from blender settings."""
+
+ return (settings["blender"]
+ ["RenderSettings"]
+ ["compositing"])
+
+
def get_render_product(output_path, name, aov_sep):
"""
Generate the path to the render product. Blender interprets the `#`
@@ -91,66 +108,121 @@ def set_render_format(ext, multilayer):
image_settings.file_format = "TIFF"
-def set_render_passes(settings):
- aov_list = (settings["blender"]
- ["RenderSettings"]
- ["aov_list"])
-
- custom_passes = (settings["blender"]
- ["RenderSettings"]
- ["custom_passes"])
+def set_render_passes(settings, renderer):
+ aov_list = set(settings["blender"]["RenderSettings"]["aov_list"])
+ custom_passes = settings["blender"]["RenderSettings"]["custom_passes"]
+ # Common passes for both renderers
vl = bpy.context.view_layer
+ # Data Passes
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
+
+ # Light Passes
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
+ # Cryptomatte Passes
+ vl.use_pass_cryptomatte_object = "cryptomatte_object" in aov_list
+ vl.use_pass_cryptomatte_material = "cryptomatte_material" in aov_list
+ vl.use_pass_cryptomatte_asset = "cryptomatte_asset" in aov_list
- 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
+ if renderer == "BLENDER_EEVEE":
+ # Eevee exclusive passes
+ eevee = vl.eevee
+
+ # Light Passes
+ vl.use_pass_shadow = "shadow" in aov_list
+ eevee.use_pass_volume_direct = "volume_light" in aov_list
+
+ # Effects Passes
+ eevee.use_pass_bloom = "bloom" in aov_list
+ eevee.use_pass_transparent = "transparent" in aov_list
+
+ # Cryptomatte Passes
+ vl.use_pass_cryptomatte_accurate = "cryptomatte_accurate" in aov_list
+ elif renderer == "CYCLES":
+ # Cycles exclusive passes
+ cycles = vl.cycles
+
+ # Data Passes
+ vl.use_pass_position = "position" in aov_list
+ vl.use_pass_vector = "vector" in aov_list
+ vl.use_pass_uv = "uv" in aov_list
+ cycles.denoising_store_passes = "denoising" in aov_list
+ vl.use_pass_object_index = "object_index" in aov_list
+ vl.use_pass_material_index = "material_index" in aov_list
+ cycles.pass_debug_sample_count = "sample_count" in aov_list
+
+ # Light Passes
+ vl.use_pass_diffuse_indirect = "diffuse_indirect" in aov_list
+ vl.use_pass_glossy_indirect = "specular_indirect" in aov_list
+ vl.use_pass_transmission_direct = "transmission_direct" in aov_list
+ vl.use_pass_transmission_indirect = "transmission_indirect" in aov_list
+ vl.use_pass_transmission_color = "transmission_color" in aov_list
+ cycles.use_pass_volume_direct = "volume_light" in aov_list
+ cycles.use_pass_volume_indirect = "volume_indirect" in aov_list
+ cycles.use_pass_shadow_catcher = "shadow" in aov_list
aovs_names = [aov.name for aov in vl.aovs]
for cp in custom_passes:
- cp_name = cp[0]
+ cp_name = cp["attribute"] if AYON_SERVER_ENABLED else 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")
+ aov.type = (cp["value"]
+ if AYON_SERVER_ENABLED else cp[1].get("type", "VALUE"))
- return aov_list, custom_passes
+ return list(aov_list), custom_passes
-def set_node_tree(output_path, name, aov_sep, ext, multilayer):
+def _create_aov_slot(name, aov_sep, slots, rpass_name, multi_exr, output_path):
+ filename = f"{name}{aov_sep}{rpass_name}.####"
+ slot = slots.new(rpass_name if multi_exr else filename)
+ filepath = str(output_path / filename.lstrip("/"))
+
+ return slot, filepath
+
+
+def set_node_tree(
+ output_path, render_product, name, aov_sep, ext, multilayer, compositing
+):
# 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
+ comp_layer_type = "CompositorNodeRLayers"
+ output_type = "CompositorNodeOutputFile"
+ compositor_type = "CompositorNodeComposite"
+
+ # Get the Render Layer, Composite and the previous output nodes
+ render_layer_node = None
+ composite_node = None
+ old_output_node = None
for node in tree.nodes:
- if node.bl_idname == "CompositorNodeRLayers":
- rl_node = node
+ if node.bl_idname == comp_layer_type:
+ render_layer_node = node
+ elif node.bl_idname == compositor_type:
+ composite_node = node
+ elif node.bl_idname == output_type and "AYON" in node.name:
+ old_output_node = node
+ if render_layer_node and composite_node and old_output_node:
break
# If there's not a Render Layers node, we create it
- if not rl_node:
- rl_node = tree.nodes.new("CompositorNodeRLayers")
+ if not render_layer_node:
+ render_layer_node = tree.nodes.new(comp_layer_type)
# Get the enabled output sockets, that are the active passes for the
# render.
@@ -158,48 +230,81 @@ def set_node_tree(output_path, name, aov_sep, ext, multilayer):
exclude_sockets = ["Image", "Alpha", "Noisy Image"]
passes = [
socket
- for socket in rl_node.outputs
+ for socket in render_layer_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")
+ output = tree.nodes.new(output_type)
image_settings = bpy.context.scene.render.image_settings
output.format.file_format = image_settings.file_format
+ slots = None
+
# 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 []
+ multi_exr = ext == "exr" and multilayer
+ slots = output.layer_slots if multi_exr else output.file_slots
+ output.base_path = render_product if multi_exr else str(output_path)
- output.file_slots.clear()
- output.base_path = str(output_path)
+ slots.clear()
aov_file_products = []
+ old_links = {
+ link.from_socket.name: link for link in tree.links
+ if link.to_node == old_output_node}
+
+ # Create a new socket for the beauty output
+ pass_name = "rgba" if multi_exr else "beauty"
+ slot, _ = _create_aov_slot(
+ name, aov_sep, slots, pass_name, multi_exr, output_path)
+ tree.links.new(render_layer_node.outputs["Image"], slot)
+
+ if compositing:
+ # Create a new socket for the composite output
+ pass_name = "composite"
+ comp_socket, filepath = _create_aov_slot(
+ name, aov_sep, slots, pass_name, multi_exr, output_path)
+ aov_file_products.append(("Composite", filepath))
+
# 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}.####"
+ for rpass in passes:
+ slot, filepath = _create_aov_slot(
+ name, aov_sep, slots, rpass.name, multi_exr, output_path)
+ aov_file_products.append((rpass.name, filepath))
- output.file_slots.new(filepath)
+ # If the rpass was not connected with the old output node, we connect
+ # it with the new one.
+ if not old_links.get(rpass.name):
+ tree.links.new(rpass, slot)
- filename = str(output_path / filepath.lstrip("/"))
+ for link in list(old_links.values()):
+ # Check if the socket is still available in the new output node.
+ socket = output.inputs.get(link.to_socket.name)
+ # If it is, we connect it with the new output node.
+ if socket:
+ tree.links.new(link.from_socket, socket)
+ # Then, we remove the old link.
+ tree.links.remove(link)
- aov_file_products.append((render_pass.name, filename))
+ # If there's a composite node, we connect its input with the new output
+ if compositing and composite_node:
+ for link in tree.links:
+ if link.to_node == composite_node:
+ tree.links.new(link.from_socket, comp_socket)
+ break
- node_input = output.inputs[-1]
+ if old_output_node:
+ output.location = old_output_node.location
+ tree.nodes.remove(old_output_node)
- tree.links.new(render_pass, node_input)
+ output.name = "AYON File Output"
+ output.label = "AYON File Output"
- return aov_file_products
+ return [] if multi_exr else aov_file_products
def imprint_render_settings(node, data):
@@ -228,17 +333,23 @@ def prepare_rendering(asset_group):
aov_sep = get_aov_separator(settings)
ext = get_image_format(settings)
multilayer = get_multilayer(settings)
+ renderer = get_renderer(settings)
+ compositing = get_compositing(settings)
set_render_format(ext, multilayer)
- aov_list, custom_passes = set_render_passes(settings)
+ bpy.context.scene.render.engine = renderer
+ aov_list, custom_passes = set_render_passes(settings, renderer)
output_path = Path.joinpath(dirpath, 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)
+ output_path, render_product, name, aov_sep,
+ ext, multilayer, compositing)
- bpy.context.scene.render.filepath = render_product
+ # Clear the render filepath, so that the output is handled only by the
+ # output node in the compositor.
+ bpy.context.scene.render.filepath = ""
render_settings = {
"render_folder": render_folder,
diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py
index 7fb3e5eb00..e728579286 100644
--- a/openpype/hosts/blender/plugins/create/create_render.py
+++ b/openpype/hosts/blender/plugins/create/create_render.py
@@ -1,8 +1,10 @@
"""Create render."""
import bpy
+from openpype.lib import version_up
from openpype.hosts.blender.api import plugin
from openpype.hosts.blender.api.render_lib import prepare_rendering
+from openpype.hosts.blender.api.workio import save_file
class CreateRenderlayer(plugin.BaseCreator):
@@ -37,6 +39,7 @@ class CreateRenderlayer(plugin.BaseCreator):
# 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)
+ filepath = version_up(bpy.data.filepath)
+ save_file(filepath, copy=False)
return collection
diff --git a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py
index bb243f08cc..f7860dd75d 100644
--- a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py
+++ b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py
@@ -28,15 +28,27 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
def process(self, instance):
if not self.is_active(instance.data):
return
+
+ tree = bpy.context.scene.node_tree
+ output_type = "CompositorNodeOutputFile"
+ output_node = None
+ # Remove all output nodes that inlcude "AYON" in the name.
+ # There should be only one.
+ for node in tree.nodes:
+ if node.bl_idname == output_type and "AYON" in node.name:
+ output_node = node
+ break
+ if not output_node:
+ raise PublishValidationError(
+ "No output node found in the compositor tree."
+ )
filepath = bpy.data.filepath
file = os.path.basename(filepath)
filename, ext = os.path.splitext(file)
- if filename not in bpy.context.scene.render.filepath:
+ if filename not in output_node.base_path:
raise PublishValidationError(
- "Render output folder "
- "doesn't match the blender scene name! "
- "Use Repair action to "
- "fix the folder file path."
+ "Render output folder doesn't match the blender scene name! "
+ "Use Repair action to fix the folder file path."
)
@classmethod
diff --git a/openpype/hosts/max/api/action.py b/openpype/hosts/max/api/action.py
new file mode 100644
index 0000000000..c3c1957af1
--- /dev/null
+++ b/openpype/hosts/max/api/action.py
@@ -0,0 +1,42 @@
+from pymxs import runtime as rt
+
+import pyblish.api
+
+from openpype.pipeline.publish import get_errored_instances_from_context
+
+
+class SelectInvalidAction(pyblish.api.Action):
+ """Select invalid objects in Blender when a publish plug-in failed."""
+ label = "Select Invalid"
+ on = "failed"
+ icon = "search"
+
+ def process(self, context, plugin):
+ errored_instances = get_errored_instances_from_context(context,
+ plugin=plugin)
+
+ # Get the invalid nodes for the plug-ins
+ self.log.info("Finding invalid nodes...")
+ invalid = list()
+ for instance in errored_instances:
+ invalid_nodes = plugin.get_invalid(instance)
+ if invalid_nodes:
+ if isinstance(invalid_nodes, (list, tuple)):
+ invalid.extend(invalid_nodes)
+ else:
+ self.log.warning(
+ "Failed plug-in doesn't have any selectable objects."
+ )
+
+ if not invalid:
+ self.log.info("No invalid nodes found.")
+ return
+ invalid_names = [obj.name for obj in invalid if isinstance(obj, str)]
+ if not invalid_names:
+ invalid_names = [obj.name for obj, _ in invalid]
+ invalid = [obj for obj, _ in invalid]
+ self.log.info(
+ "Selecting invalid objects: %s", ", ".join(invalid_names)
+ )
+
+ rt.Select(invalid)
diff --git a/openpype/hosts/max/plugins/publish/validate_camera_attributes.py b/openpype/hosts/max/plugins/publish/validate_camera_attributes.py
new file mode 100644
index 0000000000..4eec1951e5
--- /dev/null
+++ b/openpype/hosts/max/plugins/publish/validate_camera_attributes.py
@@ -0,0 +1,88 @@
+import pyblish.api
+from pymxs import runtime as rt
+
+from openpype.pipeline.publish import (
+ RepairAction,
+ OptionalPyblishPluginMixin,
+ PublishValidationError
+)
+from openpype.hosts.max.api.action import SelectInvalidAction
+
+
+class ValidateCameraAttributes(OptionalPyblishPluginMixin,
+ pyblish.api.InstancePlugin):
+ """Validates Camera has no invalid attribute properties
+ or values.(For 3dsMax Cameras only)
+
+ """
+
+ order = pyblish.api.ValidatorOrder
+ families = ['camera']
+ hosts = ['max']
+ label = 'Validate Camera Attributes'
+ actions = [SelectInvalidAction, RepairAction]
+ optional = True
+
+ DEFAULTS = ["fov", "nearrange", "farrange",
+ "nearclip", "farclip"]
+ CAM_TYPE = ["Freecamera", "Targetcamera",
+ "Physical"]
+
+ @classmethod
+ def get_invalid(cls, instance):
+ invalid = []
+ if rt.units.DisplayType != rt.Name("Generic"):
+ cls.log.warning(
+ "Generic Type is not used as a scene unit\n\n"
+ "sure you tweak the settings with your own values\n\n"
+ "before validation.")
+ cameras = instance.data["members"]
+ project_settings = instance.context.data["project_settings"].get("max")
+ cam_attr_settings = (
+ project_settings["publish"]["ValidateCameraAttributes"]
+ )
+ for camera in cameras:
+ if str(rt.ClassOf(camera)) not in cls.CAM_TYPE:
+ cls.log.debug(
+ "Skipping camera created from external plugin..")
+ continue
+ for attr in cls.DEFAULTS:
+ default_value = cam_attr_settings.get(attr)
+ if default_value == float(0):
+ cls.log.debug(
+ f"the value of {attr} in setting set to"
+ " zero. Skipping the check.")
+ continue
+ if round(rt.getProperty(camera, attr), 1) != default_value:
+ cls.log.error(
+ f"Invalid attribute value for {camera.name}:{attr} "
+ f"(should be: {default_value}))")
+ invalid.append(camera)
+
+ return invalid
+
+ def process(self, instance):
+ if not self.is_active(instance.data):
+ self.log.debug("Skipping Validate Camera Attributes.")
+ return
+ invalid = self.get_invalid(instance)
+
+ if invalid:
+ raise PublishValidationError(
+ "Invalid camera attributes found. See log.")
+
+ @classmethod
+ def repair(cls, instance):
+ invalid_cameras = cls.get_invalid(instance)
+ project_settings = instance.context.data["project_settings"].get("max")
+ cam_attr_settings = (
+ project_settings["publish"]["ValidateCameraAttributes"]
+ )
+ for camera in invalid_cameras:
+ for attr in cls.DEFAULTS:
+ expected_value = cam_attr_settings.get(attr)
+ if expected_value == float(0):
+ cls.log.debug(
+ f"the value of {attr} in setting set to zero.")
+ continue
+ rt.setProperty(camera, attr, expected_value)
diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
index f622ec9a00..ae951cacec 100644
--- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py
+++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
@@ -322,7 +322,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
return deadline_publish_job_id
-
def process(self, instance):
# type: (pyblish.api.Instance) -> None
"""Process plugin.
@@ -339,151 +338,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
self.log.debug("Skipping local instance.")
return
- data = instance.data.copy()
- context = instance.context
- self.context = context
- self.anatomy = instance.context.data["anatomy"]
-
- asset = data.get("asset") or context.data["asset"]
- subset = data.get("subset")
-
- start = instance.data.get("frameStart")
- if start is None:
- start = context.data["frameStart"]
-
- end = instance.data.get("frameEnd")
- if end is None:
- end = context.data["frameEnd"]
-
- handle_start = instance.data.get("handleStart")
- if handle_start is None:
- handle_start = context.data["handleStart"]
-
- handle_end = instance.data.get("handleEnd")
- if handle_end is None:
- handle_end = context.data["handleEnd"]
-
- fps = instance.data.get("fps")
- if fps is None:
- fps = context.data["fps"]
-
- if data.get("extendFrames", False):
- start, end = self._extend_frames(
- asset,
- subset,
- start,
- end,
- data["overrideExistingFrame"])
-
- try:
- source = data["source"]
- except KeyError:
- source = context.data["currentFile"]
-
- success, rootless_path = (
- self.anatomy.find_root_template_from_path(source)
- )
- if success:
- source = rootless_path
-
- else:
- # `rootless_path` is not set to `source` if none of roots match
- self.log.warning((
- "Could not find root path for remapping \"{}\"."
- " This may cause issues."
- ).format(source))
-
- family = "render"
- if ("prerender" in instance.data["families"] or
- "prerender.farm" in instance.data["families"]):
- family = "prerender"
- families = [family]
-
- # pass review to families if marked as review
- do_not_add_review = False
- if data.get("review"):
- families.append("review")
- elif data.get("review") is False:
- self.log.debug("Instance has review explicitly disabled.")
- do_not_add_review = True
-
- instance_skeleton_data = {
- "family": family,
- "subset": subset,
- "families": families,
- "asset": asset,
- "frameStart": start,
- "frameEnd": end,
- "handleStart": handle_start,
- "handleEnd": handle_end,
- "frameStartHandle": start - handle_start,
- "frameEndHandle": end + handle_end,
- "comment": instance.data["comment"],
- "fps": fps,
- "source": source,
- "extendFrames": data.get("extendFrames"),
- "overrideExistingFrame": data.get("overrideExistingFrame"),
- "pixelAspect": data.get("pixelAspect", 1),
- "resolutionWidth": data.get("resolutionWidth", 1920),
- "resolutionHeight": data.get("resolutionHeight", 1080),
- "multipartExr": data.get("multipartExr", False),
- "jobBatchName": data.get("jobBatchName", ""),
- "useSequenceForReview": data.get("useSequenceForReview", True),
- # map inputVersions `ObjectId` -> `str` so json supports it
- "inputVersions": list(map(str, data.get("inputVersions", []))),
- "colorspace": instance.data.get("colorspace"),
- "stagingDir_persistent": instance.data.get(
- "stagingDir_persistent", False
- )
- }
-
- # skip locking version if we are creating v01
- instance_version = instance.data.get("version") # take this if exists
- if instance_version != 1:
- instance_skeleton_data["version"] = instance_version
-
- # transfer specific families from original instance to new render
- for item in self.families_transfer:
- if item in instance.data.get("families", []):
- instance_skeleton_data["families"] += [item]
-
- # transfer specific properties from original instance based on
- # mapping dictionary `instance_transfer`
- for key, values in self.instance_transfer.items():
- if key in instance.data.get("families", []):
- for v in values:
- instance_skeleton_data[v] = instance.data.get(v)
-
- # look into instance data if representations are not having any
- # which are having tag `publish_on_farm` and include them
- for repre in instance.data.get("representations", []):
- staging_dir = repre.get("stagingDir")
- if staging_dir:
- success, rootless_staging_dir = (
- self.anatomy.find_root_template_from_path(
- staging_dir
- )
- )
- if success:
- repre["stagingDir"] = rootless_staging_dir
- else:
- self.log.warning((
- "Could not find root path for remapping \"{}\"."
- " This may cause issues on farm."
- ).format(staging_dir))
- repre["stagingDir"] = staging_dir
-
- if "publish_on_farm" in repre.get("tags"):
- # create representations attribute of not there
- if "representations" not in instance_skeleton_data.keys():
- instance_skeleton_data["representations"] = []
-
- instance_skeleton_data["representations"].append(repre)
-
- instances = None
- assert data.get("expectedFiles"), ("Submission from old Pype version"
- " - missing expectedFiles")
-
anatomy = instance.context.data["anatomy"]
instance_skeleton_data = create_skeleton_instance(
diff --git a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
index 5c780a51c4..1876ff20eb 100644
--- a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
+++ b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
@@ -132,7 +132,7 @@ class PostFtrackHook(PostLaunchHook):
if key in already_tested:
continue
- value = value.lower()
+ value = [i.lower() for i in value]
if actual_status in value or "__any__" in value:
if key != "__ignore__":
next_status_name = key
diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json
index 385e97ef91..03a5400ced 100644
--- a/openpype/settings/defaults/project_settings/blender.json
+++ b/openpype/settings/defaults/project_settings/blender.json
@@ -22,7 +22,9 @@
"aov_separator": "underscore",
"image_format": "exr",
"multilayer_exr": true,
- "aov_list": [],
+ "renderer": "CYCLES",
+ "compositing": true,
+ "aov_list": ["combined"],
"custom_passes": []
},
"workfile_builder": {
diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json
index b02cfa8207..0c4b282d10 100644
--- a/openpype/settings/defaults/project_settings/deadline.json
+++ b/openpype/settings/defaults/project_settings/deadline.json
@@ -129,6 +129,7 @@
"deadline_priority": 50,
"publishing_script": "",
"skip_integration_repre_list": [],
+ "families_transfer": ["render3d", "render2d", "ftrack", "slate"],
"aov_filter": {
"maya": [
".*([Bb]eauty).*"
diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json
index d1610610dc..a0a4fcf83d 100644
--- a/openpype/settings/defaults/project_settings/max.json
+++ b/openpype/settings/defaults/project_settings/max.json
@@ -56,6 +56,16 @@
"enabled": false,
"attributes": {}
},
+ "ValidateCameraAttributes": {
+ "enabled": true,
+ "optional": true,
+ "active": false,
+ "fov": 45.0,
+ "nearrange": 0.0,
+ "farrange": 1000.0,
+ "nearclip": 1.0,
+ "farclip": 1000.0
+ },
"ValidateLoadedPlugin": {
"enabled": false,
"optional": true,
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json
index 535d9434a3..2ffdc6070d 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json
@@ -103,6 +103,22 @@
"type": "label",
"label": "Note: Multilayer EXR is only used when output format type set to EXR."
},
+ {
+ "key": "renderer",
+ "label": "Renderer",
+ "type": "enum",
+ "multiselection": false,
+ "defaults": "CYCLES",
+ "enum_items": [
+ {"CYCLES": "Cycles"},
+ {"BLENDER_EEVEE": "Eevee"}
+ ]
+ },
+ {
+ "key": "compositing",
+ "type": "boolean",
+ "label": "Enable Compositing"
+ },
{
"key": "aov_list",
"label": "AOVs to create",
@@ -110,23 +126,38 @@
"multiselection": true,
"defaults": "empty",
"enum_items": [
- {"empty": "< empty >"},
{"combined": "Combined"},
{"z": "Z"},
{"mist": "Mist"},
{"normal": "Normal"},
- {"diffuse_light": "Diffuse Light"},
+ {"position": "Position (Cycles Only)"},
+ {"vector": "Vector (Cycles Only)"},
+ {"uv": "UV (Cycles Only)"},
+ {"denoising": "Denoising Data (Cycles Only)"},
+ {"object_index": "Object Index (Cycles Only)"},
+ {"material_index": "Material Index (Cycles Only)"},
+ {"sample_count": "Sample Count (Cycles Only)"},
+ {"diffuse_light": "Diffuse Light/Direct"},
+ {"diffuse_indirect": "Diffuse Indirect (Cycles Only)"},
{"diffuse_color": "Diffuse Color"},
- {"specular_light": "Specular Light"},
- {"specular_color": "Specular Color"},
- {"volume_light": "Volume Light"},
+ {"specular_light": "Specular (Glossy) Light/Direct"},
+ {"specular_indirect": "Specular (Glossy) Indirect (Cycles Only)"},
+ {"specular_color": "Specular (Glossy) Color"},
+ {"transmission_light": "Transmission Light/Direct (Cycles Only)"},
+ {"transmission_indirect": "Transmission Indirect (Cycles Only)"},
+ {"transmission_color": "Transmission Color (Cycles Only)"},
+ {"volume_light": "Volume Light/Direct"},
+ {"volume_indirect": "Volume Indirect (Cycles Only)"},
{"emission": "Emission"},
{"environment": "Environment"},
- {"shadow": "Shadow"},
+ {"shadow": "Shadow/Shadow Catcher"},
{"ao": "Ambient Occlusion"},
- {"denoising": "Denoising"},
- {"volume_direct": "Direct Volumetric Scattering"},
- {"volume_indirect": "Indirect Volumetric Scattering"}
+ {"bloom": "Bloom (Eevee Only)"},
+ {"transparent": "Transparent (Eevee Only)"},
+ {"cryptomatte_object": "Cryptomatte Object"},
+ {"cryptomatte_material": "Cryptomatte Material"},
+ {"cryptomatte_asset": "Cryptomatte Asset"},
+ {"cryptomatte_accurate": "Cryptomatte Accurate Mode (Eevee Only)"}
]
},
{
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
index 42dea33ef9..bb8e0b5cd4 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
@@ -693,6 +693,14 @@
"type": "text"
}
},
+ {
+ "type": "list",
+ "key": "families_transfer",
+ "label": "List of family names to transfer\nto generated instances (AOVs for example).",
+ "object_type": {
+ "type": "text"
+ }
+ },
{
"type": "dict-modifiable",
"docstring": "Regular expression to filter for which subset review should be created in publish job.",
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json
index b4d85bda98..1e7a7c0c73 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json
@@ -48,6 +48,76 @@
}
]
},
+ {
+ "type": "dict",
+ "collapsible": true,
+ "checkbox_key": "enabled",
+ "key": "ValidateCameraAttributes",
+ "label": "Validate Camera Attributes",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "type": "boolean",
+ "key": "optional",
+ "label": "Optional"
+ },
+ {
+ "type": "boolean",
+ "key": "active",
+ "label": "Active"
+ },
+ {
+ "type": "number",
+ "key": "fov",
+ "label": "Focal Length",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 100.0
+ },
+ {
+ "type": "label",
+ "label": "If the value of the camera attributes set to 0, the system automatically skips checking it"
+ },
+ {
+ "type": "number",
+ "key": "nearrange",
+ "label": "Near Range",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 100.0
+ },
+ {
+ "type": "number",
+ "key": "farrange",
+ "label": "Far Range",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 2000.0
+ },
+ {
+ "type": "number",
+ "key": "nearclip",
+ "label": "Near Clip",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 100.0
+ },
+ {
+ "type": "number",
+ "key": "farclip",
+ "label": "Far Clip",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 2000.0
+ }
+ ]
+ },
+
{
"type": "dict",
"collapsible": true,
diff --git a/openpype/version.py b/openpype/version.py
index 9e1bd39b3a..95203e17c9 100644
--- a/openpype/version.py
+++ b/openpype/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
-__version__ = "3.18.7-nightly.4"
+__version__ = "3.18.8-nightly.1"
diff --git a/pyproject.toml b/pyproject.toml
index 453833aae2..eef6a2e978 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
-version = "3.18.6" # OpenPype
+version = "3.18.7" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team "]
license = "MIT License"
diff --git a/server_addon/blender/server/settings/render_settings.py b/server_addon/blender/server/settings/render_settings.py
index f91ba1627a..f992ea6fcc 100644
--- a/server_addon/blender/server/settings/render_settings.py
+++ b/server_addon/blender/server/settings/render_settings.py
@@ -23,6 +23,13 @@ def image_format_enum():
]
+def renderers_enum():
+ return [
+ {"value": "CYCLES", "label": "Cycles"},
+ {"value": "BLENDER_EEVEE", "label": "Eevee"},
+ ]
+
+
def aov_list_enum():
return [
{"value": "empty", "label": "< none >"},
@@ -30,18 +37,52 @@ def aov_list_enum():
{"value": "z", "label": "Z"},
{"value": "mist", "label": "Mist"},
{"value": "normal", "label": "Normal"},
- {"value": "diffuse_light", "label": "Diffuse Light"},
+ {"value": "position", "label": "Position (Cycles Only)"},
+ {"value": "vector", "label": "Vector (Cycles Only)"},
+ {"value": "uv", "label": "UV (Cycles Only)"},
+ {"value": "denoising", "label": "Denoising Data (Cycles Only)"},
+ {"value": "object_index", "label": "Object Index (Cycles Only)"},
+ {"value": "material_index", "label": "Material Index (Cycles Only)"},
+ {"value": "sample_count", "label": "Sample Count (Cycles Only)"},
+ {"value": "diffuse_light", "label": "Diffuse Light/Direct"},
+ {
+ "value": "diffuse_indirect",
+ "label": "Diffuse Indirect (Cycles Only)"
+ },
{"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": "specular_light", "label": "Specular (Glossy) Light/Direct"},
+ {
+ "value": "specular_indirect",
+ "label": "Specular (Glossy) Indirect (Cycles Only)"
+ },
+ {"value": "specular_color", "label": "Specular (Glossy) Color"},
+ {
+ "value": "transmission_light",
+ "label": "Transmission Light/Direct (Cycles Only)"
+ },
+ {
+ "value": "transmission_indirect",
+ "label": "Transmission Indirect (Cycles Only)"
+ },
+ {
+ "value": "transmission_color",
+ "label": "Transmission Color (Cycles Only)"
+ },
+ {"value": "volume_light", "label": "Volume Light/Direct"},
+ {"value": "volume_indirect", "label": "Volume Indirect (Cycles Only)"},
{"value": "emission", "label": "Emission"},
{"value": "environment", "label": "Environment"},
- {"value": "shadow", "label": "Shadow"},
+ {"value": "shadow", "label": "Shadow/Shadow Catcher"},
{"value": "ao", "label": "Ambient Occlusion"},
- {"value": "denoising", "label": "Denoising"},
- {"value": "volume_direct", "label": "Direct Volumetric Scattering"},
- {"value": "volume_indirect", "label": "Indirect Volumetric Scattering"}
+ {"value": "bloom", "label": "Bloom (Eevee Only)"},
+ {"value": "transparent", "label": "Transparent (Eevee Only)"},
+ {"value": "cryptomatte_object", "label": "Cryptomatte Object"},
+ {"value": "cryptomatte_material", "label": "Cryptomatte Material"},
+ {"value": "cryptomatte_asset", "label": "Cryptomatte Asset"},
+ {
+ "value": "cryptomatte_accurate",
+ "label": "Cryptomatte Accurate Mode (Eevee Only)"
+ },
]
@@ -81,6 +122,14 @@ class RenderSettingsModel(BaseSettingsModel):
multilayer_exr: bool = SettingsField(
title="Multilayer (EXR)"
)
+ renderer: str = SettingsField(
+ "CYCLES",
+ title="Renderer",
+ enum_resolver=renderers_enum
+ )
+ compositing: bool = SettingsField(
+ title="Enable Compositing"
+ )
aov_list: list[str] = SettingsField(
default_factory=list,
enum_resolver=aov_list_enum,
@@ -102,6 +151,8 @@ DEFAULT_RENDER_SETTINGS = {
"aov_separator": "underscore",
"image_format": "exr",
"multilayer_exr": True,
- "aov_list": [],
+ "renderer": "CYCLES",
+ "compositing": True,
+ "aov_list": ["combined"],
"custom_passes": []
}
diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py
index 1276d0254f..0a8da88258 100644
--- a/server_addon/blender/server/version.py
+++ b/server_addon/blender/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.5"
+__version__ = "0.1.6"
diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py
index da782cb494..9c301d10b7 100644
--- a/server_addon/max/server/settings/publishers.py
+++ b/server_addon/max/server/settings/publishers.py
@@ -27,6 +27,17 @@ class ValidateAttributesModel(BaseSettingsModel):
return value
+class ValidateCameraAttributesModel(BaseSettingsModel):
+ enabled: bool = SettingsField(title="Enabled")
+ optional: bool = SettingsField(title="Optional")
+ active: bool = SettingsField(title="Active")
+ fov: float = SettingsField(0.0, title="Focal Length")
+ nearrange: float = SettingsField(0.0, title="Near Range")
+ farrange: float = SettingsField(0.0, title="Far Range")
+ nearclip: float = SettingsField(0.0, title="Near Clip")
+ farclip: float = SettingsField(0.0, title="Far Clip")
+
+
class FamilyMappingItemModel(BaseSettingsModel):
product_types: list[str] = SettingsField(
default_factory=list,
@@ -63,7 +74,14 @@ class PublishersModel(BaseSettingsModel):
default_factory=ValidateAttributesModel,
title="Validate Attributes"
)
-
+ ValidateCameraAttributes: ValidateCameraAttributesModel = SettingsField(
+ default_factory=ValidateCameraAttributesModel,
+ title="Validate Camera Attributes",
+ description=(
+ "If the value of the camera attributes set to 0, "
+ "the system automatically skips checking it"
+ )
+ )
ValidateLoadedPlugin: ValidateLoadedPluginModel = SettingsField(
default_factory=ValidateLoadedPluginModel,
title="Validate Loaded Plugin"
@@ -101,6 +119,16 @@ DEFAULT_PUBLISH_SETTINGS = {
"enabled": False,
"attributes": "{}"
},
+ "ValidateCameraAttributes": {
+ "enabled": True,
+ "optional": True,
+ "active": False,
+ "fov": 45.0,
+ "nearrange": 0.0,
+ "farrange": 1000.0,
+ "nearclip": 1.0,
+ "farclip": 1000.0
+ },
"ValidateLoadedPlugin": {
"enabled": False,
"optional": True,
diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py
index bbab0242f6..1276d0254f 100644
--- a/server_addon/max/server/version.py
+++ b/server_addon/max/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.4"
+__version__ = "0.1.5"