Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Blender: output node and EXR #6086

Merged
merged 20 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0fc23ce
Changed logic for creation of output node
simonebarbieri Dec 21, 2023
fa5b977
Create a new version of the workfile instead of overwriting current one
simonebarbieri Dec 21, 2023
9f5ea0f
Merge branch 'develop' into enhancement/OP-7120-blender_output-node-exr
simonebarbieri Jan 12, 2024
0534c9c
Fix error when getting setting value in Ayon
simonebarbieri Jan 18, 2024
1eb07bc
Added setting to choose render engine
simonebarbieri Jan 18, 2024
2f58708
Updated aov list
simonebarbieri Jan 18, 2024
91f67ca
Updated default settings for render passes
simonebarbieri Jan 19, 2024
dd0533e
Added composite output
simonebarbieri Jan 19, 2024
c911c67
Updated OpenPype Settings
simonebarbieri Jan 19, 2024
5858b26
Merge branch 'develop' into enhancement/OP-7120-blender_output-node-exr
LiborBatek Jan 22, 2024
44282f8
Fixed deadline validator to check compositor tree output
simonebarbieri Feb 1, 2024
518b9c2
Merge branch 'develop' into enhancement/OP-7120-blender_output-node-exr
simonebarbieri Feb 1, 2024
78a3ec2
Fixed issue with double entries in the json manifest
simonebarbieri Feb 5, 2024
7bcd7ba
Merge branch 'develop' into enhancement/OP-7120-blender_output-node-exr
LiborBatek Feb 5, 2024
f242968
Fix old links and duplicated entries in the manifest
simonebarbieri Feb 6, 2024
bcd2169
Cast aov_list to set to improve performance
simonebarbieri Feb 14, 2024
6002da8
Added an option to disable composite output
simonebarbieri Feb 14, 2024
1b5d52b
Merge branch 'develop' into enhancement/OP-7120-blender_output-node-exr
LiborBatek Feb 20, 2024
e1f5bdb
Removed redundant option in aov list
simonebarbieri Feb 20, 2024
a52a58d
Merge branch 'develop' into enhancement/OP-7120-blender_output-node-exr
LiborBatek Feb 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 164 additions & 53 deletions openpype/hosts/blender/api/render_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import bpy

from openpype import AYON_SERVER_ENABLED
from openpype.settings import get_project_settings
from openpype.pipeline import get_current_project_name

Expand Down Expand Up @@ -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 `#`
Expand Down Expand Up @@ -91,115 +108,203 @@ 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

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

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 list(aov_list), custom_passes

return aov_list, custom_passes

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("/"))

def set_node_tree(output_path, name, aov_sep, ext, multilayer):
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.
# We also exclude some layers.
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 = []

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

filename = str(output_path / filepath.lstrip("/"))
old_links = {
link.from_socket.name: link for link in tree.links
if link.to_node == old_output_node}

aov_file_products.append((render_pass.name, filename))
# 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)

node_input = output.inputs[-1]
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))

tree.links.new(render_pass, node_input)

return aov_file_products
# For each active render pass, we add a new socket to the output node
# and link it
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))

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

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)

# 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

if old_output_node:
output.location = old_output_node.location
tree.nodes.remove(old_output_node)

output.name = "AYON File Output"
output.label = "AYON File Output"

return [] if multi_exr else aov_file_products


def imprint_render_settings(node, data):
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion openpype/hosts/blender/plugins/create/create_render.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -37,6 +39,7 @@ def create(
# 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion openpype/settings/defaults/project_settings/blender.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading
Loading