Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
18d3022
Fix PySide install for Blender 5.0+
BigRoy Dec 1, 2025
ccf1fa5
Fix scene node tree compatibility access for Blender 5.0 (and backwar…
BigRoy Dec 1, 2025
d5c3b19
Do not hard fail on loading to compositor due to colorspace not setta…
BigRoy Dec 1, 2025
df0eb25
Only apply the image shader colorspace mapping in Blender 5.0 and bel…
BigRoy Dec 1, 2025
6132659
export_textures was removed in Blender 5 and replaced with `export_te…
BigRoy Dec 1, 2025
9ffa075
Fix Load blend in Blender 5.0
BigRoy Dec 1, 2025
c251526
Fix node tree usage
BigRoy Dec 1, 2025
a9739cc
Mark repair in Blender 5.0 as not implemented
BigRoy Dec 1, 2025
7d84a9c
Fix collect render for Blender 5.0
BigRoy Dec 1, 2025
b69a57e
Remove debug reloads
BigRoy Dec 1, 2025
7053272
Support creating render instances and render setup in Blender 5+
BigRoy Dec 1, 2025
26f2214
Cosmetics
BigRoy Dec 1, 2025
d5265ff
Remove debug reloads
BigRoy Dec 1, 2025
14089e0
Detect AOVs in a better way on Blender 5
BigRoy Dec 2, 2025
afe0102
Fix validation and repair for Blender 5 behavior
BigRoy Dec 2, 2025
0a6cacc
Merge branch 'develop' into 203-yn-0246-blender-50-compatibility
BigRoy Dec 6, 2025
8745e94
Disable appending user scripts in Blender 5+ releases, add explanator…
BigRoy Dec 6, 2025
2798de2
Update client/ayon_blender/api/lib.py
BigRoy Dec 7, 2025
9d8b141
Merge branch 'develop' into 203-yn-0246-blender-50-compatibility
BigRoy Dec 11, 2025
d9e886f
force disable "Composite" connection in Blender 5+
BigRoy Dec 11, 2025
c4b2227
Merge branch '203-yn-0246-blender-50-compatibility' of https://github…
BigRoy Dec 11, 2025
b8a0546
Merge branch 'develop' into 203-yn-0246-blender-50-compatibility
BigRoy Dec 15, 2025
6d91d1f
Merge branch 'develop' into 203-yn-0246-blender-50-compatibility
BigRoy Dec 17, 2025
539897f
Merge branch 'develop' into 203-yn-0246-blender-50-compatibility
BigRoy Dec 18, 2025
04d3554
Merge branch 'develop' into 203-yn-0246-blender-50-compatibility
BigRoy Dec 19, 2025
7ebd1d3
Ensure node tree existence
BigRoy Dec 19, 2025
2caf1d6
Fix setting scene multilayer EXR in Blender 5+
BigRoy Dec 19, 2025
52cc74f
Fix setting `OPEN_EXR_MULTILAYER` in blender 5
BigRoy Dec 19, 2025
1836df1
Match socket type of the source slots on creation
BigRoy Jan 6, 2026
b3d1194
Fix Beauty slot to be created as `FLOAT` instead of `RGBA` (Color)
BigRoy Jan 8, 2026
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
43 changes: 42 additions & 1 deletion client/ayon_blender/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ def load_scripts(paths):

It is possible that this function will be changed in future and usage will
be based on Blender version.

This does not work in Blender 5+ due to `bpy_types` being unavailable. But
usually this is not needed for Blender 5+ anyway, because it does allow
better user scripts management through environment variables than older
releases of Blender.
"""
import bpy_types

Expand Down Expand Up @@ -131,6 +136,16 @@ def new_paths():


def append_user_scripts():
"""Apply user scripts to Blender.

This was originally used for early Blender 4 versions due to requiring
AYON to be sources from `BLENDER_USER_SCRIPTS` paths which unfortunately
allowed only a single path, *and* it had the side effect of not loading the
default user scripts anymore.

In Blender 5+ this is irrelevant and instead additional Script Directories
can be configured and used instead.
"""
default_user_prefs = os.path.join(
bpy.utils.resource_path('USER'),
"scripts",
Expand Down Expand Up @@ -743,7 +758,7 @@ def search_replace_render_paths(src: str, dest: str) -> bool:
changes = True

# Base paths for Compositor File Output Nodes
node_tree = bpy.context.scene.node_tree
node_tree = get_scene_node_tree()
if node_tree:
for node in node_tree.nodes:
if node.bl_idname != "CompositorNodeOutputFile":
Expand Down Expand Up @@ -787,3 +802,29 @@ def map_colorspace_name(colorspace: str) -> str:
}

return colorspace_mapping.get(colorspace, colorspace)


def get_scene_node_tree(ensure_exists=False):
"""Return the node tree

Arguments:
ensure_exists (bool): When enabled, make sure a compositor node tree is
enabled and set.
"""
if get_blender_version() >= (5, 0, 0):
# Blender 5.0+
if not bpy.context.scene.compositing_node_group and ensure_exists:
# In Blender 5 if no comp node tree is set, create one
tree = bpy.data.node_groups.new("Compositor Nodes",
"CompositorNodeTree")
bpy.context.scene.compositing_node_group = tree
return tree

return bpy.context.scene.compositing_node_group
else:
# Blender 4.0 and below
if not bpy.context.scene.node_tree and ensure_exists:
# Force enable compositor in Blender 4
bpy.context.scene.use_nodes = True

return bpy.context.scene.node_tree
8 changes: 6 additions & 2 deletions client/ayon_blender/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,11 @@ def install():
register_loader_plugin_path(str(LOAD_PATH))
register_creator_plugin_path(str(CREATE_PATH))

lib.append_user_scripts()
if lib.get_blender_version() < (5, 0, 0):
# User script directories had issues in custom management in older
# Blender releases - appending user scripts within AYON was a
# workaround only in-place to solve that issue.
lib.append_user_scripts()
lib.set_app_templates_path()

register_event_callback("new", on_new)
Expand Down Expand Up @@ -751,7 +755,7 @@ def ls() -> Iterator:
yield parse_container(container)

# Compositor nodes are not in `bpy.data` that `lib.lsattr` looks in.
node_tree = bpy.context.scene.node_tree
node_tree = lib.get_scene_node_tree()
if node_tree:
for node in node_tree.nodes:
ayon_prop = node.get(AYON_PROPERTY)
Expand Down
11 changes: 7 additions & 4 deletions client/ayon_blender/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
)
from .lib import (
imprint,
get_blender_version
get_blender_version,
get_scene_node_tree
)


Expand Down Expand Up @@ -212,8 +213,9 @@ def cache_instance_data(shared_data):

# Consider any node tree objects as well
node_tree_objects = []
if bpy.context.scene.node_tree:
node_tree_objects = bpy.context.scene.node_tree.nodes
node_tree = get_scene_node_tree()
if node_tree:
node_tree_objects = node_tree.nodes

for obj_or_col in itertools.chain(
ayon_instance_objs,
Expand Down Expand Up @@ -377,7 +379,8 @@ def remove_instances(self, instances: List[CreatedInstance]):

# Remove compositor node
elif isinstance(node, bpy.types.CompositorNode):
bpy.context.scene.node_tree.nodes.remove(node)
node_tree = get_scene_node_tree()
node_tree.nodes.remove(node)

self._remove_instance_from_context(instance)

Expand Down
133 changes: 79 additions & 54 deletions client/ayon_blender/api/render_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def get_renderer(project_settings) -> str:

def get_compositing(project_settings) -> bool:
"""Get whether 'Composite' render is enabled from blender settings."""
# Blender 5+ does not have the "Composite" node, so it's always False
if lib.get_blender_version() >= (5, 0, 0):
return False

return project_settings["blender"]["RenderSettings"]["compositing"]


Expand All @@ -54,9 +58,12 @@ def set_render_format(ext: str, multilayer: bool):
bpy.context.scene.render.use_file_extension = True
image_settings = bpy.context.scene.render.image_settings

if multilayer and lib.get_blender_version() >= (5, 0, 0):
image_settings.media_type = "MULTI_LAYER_IMAGE"

if ext == "exr":
image_settings.file_format = (
"OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR")
file_format = "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR"
image_settings.file_format = file_format
elif ext == "bmp":
image_settings.file_format = "BMP"
elif ext == "rgb":
Expand Down Expand Up @@ -90,10 +97,19 @@ def get_file_format_extension(file_format: str) -> str:
return "jpeg"
elif file_format == "JPEG2000":
return "jp2"
elif file_format == "TARGA":
elif file_format == "TARGA" or file_format == "TARGA_RAW":
return "tga"
elif file_format == "TIFF":
return "tif"
# Blender 5+
elif file_format == "CINEON":
return "cin"
elif file_format == "DPX":
return "dpx"
elif file_format == "WEBP":
return "webp"
elif file_format == "HDR":
return "hdr"
else:
raise ValueError(f"Unsupported file format: {file_format}")

Expand Down Expand Up @@ -230,32 +246,6 @@ def existing_aov_options(
return aov_list


def _create_aov_slot(
slots: "bpy.types.RenderSlots",
variant_name: str,
aov_sep: str,
renderpass_name: str,
is_multi_exr: bool,
render_layer: str,
) -> "bpy.types.RenderSlot":
"""Add a new render output slot to the slots.

The slots usually are the file slots of the compositor output node.
The filepath is based on the render layer, variant name and render pass.

If it's multi-exr, the slot will be named after the render pass only.

Returns:
The created slot

"""
filename = (
f"{render_layer}/"
f"{variant_name}_{render_layer}{aov_sep}{renderpass_name}.####"
)
return slots.new(renderpass_name if is_multi_exr else filename)


def get_base_render_output_path(
variant_name: str,
multi_exr: Optional[bool] = None,
Expand Down Expand Up @@ -303,16 +293,12 @@ def create_render_node_tree(
create render layer nodes for.
project_settings (dict): The project settings dictionary.
"""
# Set the scene to use the compositor node tree to render
if not bpy.context.scene.use_nodes:
bpy.context.scene.use_nodes = True

aov_sep = get_aov_separator(project_settings)
ext = get_image_format(project_settings)
multilayer = get_multilayer(project_settings)
compositing = get_compositing(project_settings)

tree = bpy.context.scene.node_tree
tree = lib.get_scene_node_tree(ensure_exists=True)

comp_composite_type = "CompositorNodeComposite"

Expand All @@ -330,26 +316,68 @@ def create_render_node_tree(
output.name = variant_name
output.label = variant_name

# Multi-exr
multi_exr: bool = ext == "exr" and multilayer
blender_version = lib.get_blender_version()
if blender_version >= (5, 0, 0):
output.format.media_type = (
"MULTI_LAYER_IMAGE" if multi_exr else "IMAGE"
)
# By default, match output format from scene file format
image_settings = bpy.context.scene.render.image_settings
output.format.file_format = image_settings.file_format
multi_exr: bool = ext == "exr" and multilayer

# Define the base path for the File Output node.
output.base_path = get_base_render_output_path(
base_path = get_base_render_output_path(
variant_name, project_settings=project_settings
)
if blender_version >= (5, 0, 0):
base_path_dir, base_path_filename = os.path.split(base_path)
if not multi_exr:
base_path_filename += aov_sep

output.directory = base_path_dir
output.file_name = base_path_filename
slots = output.file_output_items
else:
output.base_path = base_path
slots = output.layer_slots if multi_exr else output.file_slots

def _create_aov_slot(
renderpass_name: str,
render_layer: str,
socket_type: str = "FLOAT",
) -> "bpy.types.RenderSlot":
"""Add a new render output slot to the slots.

The slots usually are the file slots of the compositor output node.
The filepath is based on the render layer, variant name and render pass.

If it's multi-exr, the slot will be named after the render pass only.

Returns:
The created slot

"""
if lib.get_blender_version() >= (5, 0, 0):
new_output_item = output.file_output_items.new(
socket_type, renderpass_name
)
return output.inputs[new_output_item.name]

filename: str = (
f"{render_layer}/"
f"{variant_name}_{render_layer}{aov_sep}{renderpass_name}.####"
)
return slots.new(renderpass_name if multi_exr else filename)

slots = output.layer_slots if multi_exr else output.file_slots
slots.clear()

# Create a new socket for the Beauty output
pass_name = "Beauty"
for render_layer_node in render_layer_nodes:
render_layer = render_layer_node.layer
slot = _create_aov_slot(
slots, variant_name, aov_sep, pass_name, multi_exr, render_layer
)
slot = _create_aov_slot(pass_name, render_layer, socket_type="RGBA")
tree.links.new(render_layer_node.outputs["Image"], slot)

last_found_renderlayer_node = next(
Expand All @@ -360,9 +388,7 @@ def create_render_node_tree(
# with only the one view layer
pass_name = "Composite"
render_layer = last_found_renderlayer_node.layer
slot = _create_aov_slot(
slots, variant_name, aov_sep, pass_name, multi_exr, render_layer
)
slot = _create_aov_slot(pass_name, render_layer)
# If there's a composite node, we connect its 'Image' input with the
# new slot on the output
if composite_node:
Expand All @@ -384,15 +410,17 @@ def create_render_node_tree(
if not output_socket.enabled:
continue

socket_type: str = "FLOAT" # Only relevant for Blender 5+
if lib.get_blender_version() >= (5, 0, 0):
socket_type = output_socket.type
if socket_type == "VALUE":
socket_type = "FLOAT"

slot = _create_aov_slot(
slots,
variant_name,
aov_sep,
output_socket.name,
multi_exr,
render_layer,
socket_type=socket_type
)

tree.links.new(output_socket, slot)

return output
Expand Down Expand Up @@ -423,13 +451,9 @@ def prepare_rendering(
view_layers = bpy.context.scene.view_layers
set_render_passes(project_settings, renderer, view_layers)

# Ensure compositor nodes are enabled before accessing node tree
if not bpy.context.scene.use_nodes:
bpy.context.scene.use_nodes = True

# Use selected renderlayer nodes, or assume we want a renderlayer node for
# each view layer so we retrieve all of them.
node_tree = bpy.context.scene.node_tree
node_tree = lib.get_scene_node_tree(ensure_exists=True)
selected_renderlayer_nodes = []

# Check if node_tree is available before accessing nodes
Expand Down Expand Up @@ -501,7 +525,8 @@ def get_or_create_render_layer_nodes(
view_layers: list["bpy.types.ViewLayer"],
) -> set[bpy.types.CompositorNodeRLayers]:
"""Get existing render layer nodes or create new ones."""
tree = bpy.context.scene.node_tree
tree = lib.get_scene_node_tree(ensure_exists=True)

view_layer_names: set[str] = {
view_layer.name for view_layer in view_layers
}
Expand Down
2 changes: 1 addition & 1 deletion client/ayon_blender/hooks/pre_pyside_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def execute(self):

def inner_execute(self):
# Get blender's python directory
version_regex = re.compile(r"^([2-4])\.[0-9]+$")
version_regex = re.compile(r"^([2-5])\.[0-9]+$")

platform = system().lower()
executable = self.launch_context.executable.executable_path
Expand Down
Loading