From c2bb20f9053342e4c1ad4032f478becbdb6bf05c Mon Sep 17 00:00:00 2001 From: Onek8 Date: Thu, 23 Jul 2026 23:41:13 -0700 Subject: [PATCH 1/2] Moises Jpelaez: 5.2 Updates --- leenkx.py | 2 +- leenkx/blender/lnx/logicnode/__init__.py | 1 + leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py | 8 ++------ leenkx/blender/lnx/utils.py | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/leenkx.py b/leenkx.py index 801f0bb4..d0960cf0 100644 --- a/leenkx.py +++ b/leenkx.py @@ -7,7 +7,7 @@ bl_info = { "description": "Full Stack SDK", "author": "Leenkx.com", "version": (2026, 5, 0), - "blender": (4, 5, 0), + "blender": (5, 2, 0), "doc_url": "https://leenkx.com/", "tracker_url": "https://leenkx.com/support" } diff --git a/leenkx/blender/lnx/logicnode/__init__.py b/leenkx/blender/lnx/logicnode/__init__.py index 2c21aa46..84dfab6b 100644 --- a/leenkx/blender/lnx/logicnode/__init__.py +++ b/leenkx/blender/lnx/logicnode/__init__.py @@ -1,3 +1,4 @@ +import bpy import importlib import inspect import pkgutil diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py index 6f8cea4c..a8d9d797 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py @@ -182,12 +182,8 @@ if bpy.app.version > (4, 1, 0): pass if (node.inputs['Emission Strength'].is_linked or node.inputs['Emission Strength'].default_value != 0.0)\ and (node.inputs['Emission Color'].is_linked or not mat_utils.equals_color_socket(node.inputs['Emission Color'], (0.0, 0.0, 0.0), comp_alpha=False)): - if bpy.app.version >= (4, 4, 0): - emission_col = c.parse_vector_input(node.inputs[27]) - emission_strength = c.parse_value_input(node.inputs[28]) - else: - emission_col = c.parse_vector_input(node.inputs[26]) - emission_strength = c.parse_value_input(node.inputs[27]) + emission_col = c.parse_vector_input(node.inputs['Emission Color']) + emission_strength = c.parse_value_input(node.inputs['Emission Strength']) state.out_emission_col = '({0} * {1})'.format(emission_col, emission_strength) mat_state.emission_type = mat_state.EmissionType.SHADED else: diff --git a/leenkx/blender/lnx/utils.py b/leenkx/blender/lnx/utils.py index 65eb7fb5..4498812d 100644 --- a/leenkx/blender/lnx/utils.py +++ b/leenkx/blender/lnx/utils.py @@ -875,7 +875,7 @@ def check_blender_version(op: bpy.types.Operator): """Check whether the Blender version is supported by Leenkx, if not, report in UI. """ - if bpy.app.version[:2] not in [(4, 5), (4, 4), (4, 2), (3, 6), (3, 3)]: + if bpy.app.version[:2] not in [(5, 2), (4, 5), (4, 4), (4, 2), (3, 6), (3, 3)]: op.report({'INFO'}, 'INFO: For Leenkx to work correctly, use a Blender LTS version') From b77aca926a4083f8d6593d0a898425a0203d4a87 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 24 Jul 2026 01:59:51 -0700 Subject: [PATCH 2/2] Refactor shader sockets --- .../lnx/material/cycles_nodes/nodes_shader.py | 173 ++++++++++-------- leenkx/blender/lnx/material/node_meta.py | 3 +- 2 files changed, 95 insertions(+), 81 deletions(-) diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py index a8d9d797..cdd7793a 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py @@ -16,19 +16,29 @@ if lnx.is_reload(__name__): else: lnx.enable_reload(__name__) +# Principled BSDF socket names that differ between Blender versions +if bpy.app.version < (4, 0, 0): + EMISSION_COLOR = 'Emission' + SUBSURFACE = 'Subsurface' + SPECULAR = 'Specular' +else: + EMISSION_COLOR = 'Emission Color' + SUBSURFACE = 'Subsurface Weight' + SPECULAR = 'Specular IOR Level' + def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket, state: ParserState) -> None: # Skip mixing if only one input is effectively used - if not node.inputs[0].is_linked: - if node.inputs[0].default_value <= 0.0: + if not node.inputs['Fac'].is_linked: + if node.inputs['Fac'].default_value <= 0.0: c.parse_shader_input(node.inputs[1]) return - elif node.inputs[0].default_value >= 1.0: + elif node.inputs['Fac'].default_value >= 1.0: c.parse_shader_input(node.inputs[2]) return - prefix = '' if node.inputs[0].is_linked else 'const ' - fac = c.parse_value_input(node.inputs[0]) + prefix = '' if node.inputs['Fac'].is_linked else 'const ' + fac = c.parse_value_input(node.inputs['Fac']) fac_var = c.node_name(node.name) + '_fac' + state.get_parser_pass_suffix() fac_inv_var = c.node_name(node.name) + '_fac_inv' state.curshader.write('{0}float {1} = clamp({2}, 0.0, 1.0);'.format(prefix, fac_var, fac)) @@ -88,42 +98,42 @@ def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, # TODO: Refactor using c.get_*_input() -if bpy.app.version < (2, 92, 0): +if bpy.app.version < (2, 91, 0): def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[20]) - state.out_basecol = c.parse_vector_input(node.inputs[0]) - if node.inputs[1].is_linked or node.inputs[1].default_value > 0.0: + c.write_normal(node.inputs['Normal']) + state.out_basecol = c.parse_vector_input(node.inputs['Base Color']) + if node.inputs[SUBSURFACE].is_linked or node.inputs[SUBSURFACE].default_value > 0.0: mat_state.needs_sss = True - state.out_metallic = c.parse_value_input(node.inputs[4]) - state.out_specular = c.parse_value_input(node.inputs[5]) - state.out_roughness = c.parse_value_input(node.inputs[7]) - if node.inputs['Emission'].is_linked or not mat_utils.equals_color_socket(node.inputs['Emission'], (0.0, 0.0, 0.0), comp_alpha=False): - emission_col = c.parse_vector_input(node.inputs[17]) + state.out_metallic = c.parse_value_input(node.inputs['Metallic']) + state.out_specular = c.parse_value_input(node.inputs[SPECULAR]) + state.out_roughness = c.parse_value_input(node.inputs['Roughness']) + if node.inputs[EMISSION_COLOR].is_linked or not mat_utils.equals_color_socket(node.inputs[EMISSION_COLOR], (0.0, 0.0, 0.0), comp_alpha=False): + emission_col = c.parse_vector_input(node.inputs[EMISSION_COLOR]) state.out_emission_col = emission_col mat_state.emission_type = mat_state.EmissionType.SHADED else: mat_state.emission_type = mat_state.EmissionType.NO_EMISSION if state.parse_opacity: - state.out_ior = c.parse_value_input(node.inputs[14]) + state.out_ior = c.parse_value_input(node.inputs['IOR']) # In Blender 2.83, Alpha socket is at index 18, not 19 if 'Alpha' in node.inputs: state.out_opacity = c.parse_value_input(node.inputs['Alpha']) else: state.out_opacity = '1.0' -if bpy.app.version >= (2, 92, 0) and bpy.app.version <= (4, 1, 0): +if bpy.app.version >= (2, 91, 0) and bpy.app.version < (4, 0, 0): def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[22]) - state.out_basecol = c.parse_vector_input(node.inputs[0]) - if node.inputs[1].is_linked or node.inputs[1].default_value > 0.0: + c.write_normal(node.inputs['Normal']) + state.out_basecol = c.parse_vector_input(node.inputs['Base Color']) + if node.inputs[SUBSURFACE].is_linked or node.inputs[SUBSURFACE].default_value > 0.0: mat_state.needs_sss = True # subsurface_radius = c.parse_vector_input(node.inputs[2]) # subsurface_color = c.parse_vector_input(node.inputs[3]) - state.out_metallic = c.parse_value_input(node.inputs[6]) - state.out_specular = c.parse_value_input(node.inputs[7]) + state.out_metallic = c.parse_value_input(node.inputs['Metallic']) + state.out_specular = c.parse_value_input(node.inputs[SPECULAR]) # specular_tint = c.parse_vector_input(node.inputs[6]) - state.out_roughness = c.parse_value_input(node.inputs[9]) + state.out_roughness = c.parse_value_input(node.inputs['Roughness']) # aniso = c.parse_vector_input(node.inputs[8]) # aniso_rot = c.parse_vector_input(node.inputs[9]) # sheen = c.parse_vector_input(node.inputs[10]) @@ -134,9 +144,9 @@ if bpy.app.version >= (2, 92, 0) and bpy.app.version <= (4, 1, 0): # transmission = c.parse_vector_input(node.inputs[15]) # transmission_roughness = c.parse_vector_input(node.inputs[16]) if (node.inputs['Emission Strength'].is_linked or node.inputs['Emission Strength'].default_value != 0.0)\ - and (node.inputs['Emission'].is_linked or not mat_utils.equals_color_socket(node.inputs['Emission'], (0.0, 0.0, 0.0), comp_alpha=False)): - emission_col = c.parse_vector_input(node.inputs[19]) - emission_strength = c.parse_value_input(node.inputs[20]) + and (node.inputs[EMISSION_COLOR].is_linked or not mat_utils.equals_color_socket(node.inputs[EMISSION_COLOR], (0.0, 0.0, 0.0), comp_alpha=False)): + emission_col = c.parse_vector_input(node.inputs[EMISSION_COLOR]) + emission_strength = c.parse_value_input(node.inputs['Emission Strength']) state.out_emission_col = '({0} * {1})'.format(emission_col, emission_strength) mat_state.emission_type = mat_state.EmissionType.SHADED else: @@ -144,26 +154,32 @@ if bpy.app.version >= (2, 92, 0) and bpy.app.version <= (4, 1, 0): # clearcoar_normal = c.parse_vector_input(node.inputs[21]) # tangent = c.parse_vector_input(node.inputs[22]) if state.parse_opacity: - state.out_ior = c.parse_value_input(node.inputs[16]) - if len(node.inputs) >= 21: - state.out_opacity = c.parse_value_input(node.inputs[21]) -if bpy.app.version > (4, 1, 0): + state.out_ior = c.parse_value_input(node.inputs['IOR']) + if 'Alpha' in node.inputs: + state.out_opacity = c.parse_value_input(node.inputs['Alpha']) + else: + state.out_opacity = '1.0' +if bpy.app.version >= (4, 0, 0): def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[5]) - state.out_basecol = c.parse_vector_input(node.inputs[0]) + c.write_normal(node.inputs['Normal']) + state.out_basecol = c.parse_vector_input(node.inputs['Base Color']) - sss_input = node.inputs.get('Subsurface Weight') or node.inputs.get('Subsurface') + sss_input = node.inputs.get(SUBSURFACE) if sss_input is not None: if sss_input.is_linked or sss_input.default_value > 0.0: mat_state.needs_sss = True - subsurface = c.parse_value_input(node.inputs[7]) - subsurface_radius = c.parse_vector_input(node.inputs[9]) - subsurface_color = c.parse_vector_input(node.inputs[8]) - state.out_metallic = c.parse_value_input(node.inputs[1]) + subsurface = c.parse_value_input(node.inputs[SUBSURFACE]) + subsurface_radius = c.parse_vector_input(node.inputs['Subsurface Radius']) + subsurface_color_input = node.inputs.get('Subsurface Color') + if subsurface_color_input is not None: + subsurface_color = c.parse_vector_input(subsurface_color_input) + else: + subsurface_color = c.parse_vector_input(node.inputs['Base Color']) + state.out_metallic = c.parse_value_input(node.inputs['Metallic']) - specular_socket = node.inputs.get('Specular IOR Level') + specular_socket = node.inputs.get(SPECULAR) if specular_socket is not None: state.out_specular = f'({c.parse_value_input(specular_socket)} * 2.0)' else: @@ -173,7 +189,7 @@ if bpy.app.version > (4, 1, 0): else: state.out_specular = '1.0' - state.out_roughness = c.parse_value_input(node.inputs[2]) + state.out_roughness = c.parse_value_input(node.inputs['Roughness']) # Prevent black material when metal = 1.0 and roughness = 0.0 try: if float(state.out_roughness) < 0.00101: @@ -181,8 +197,8 @@ if bpy.app.version > (4, 1, 0): except ValueError: pass if (node.inputs['Emission Strength'].is_linked or node.inputs['Emission Strength'].default_value != 0.0)\ - and (node.inputs['Emission Color'].is_linked or not mat_utils.equals_color_socket(node.inputs['Emission Color'], (0.0, 0.0, 0.0), comp_alpha=False)): - emission_col = c.parse_vector_input(node.inputs['Emission Color']) + and (node.inputs[EMISSION_COLOR].is_linked or not mat_utils.equals_color_socket(node.inputs[EMISSION_COLOR], (0.0, 0.0, 0.0), comp_alpha=False)): + emission_col = c.parse_vector_input(node.inputs[EMISSION_COLOR]) emission_strength = c.parse_value_input(node.inputs['Emission Strength']) state.out_emission_col = '({0} * {1})'.format(emission_col, emission_strength) mat_state.emission_type = mat_state.EmissionType.SHADED @@ -199,58 +215,58 @@ if bpy.app.version > (4, 1, 0): #state.out_transmission = c.parse_vector_input(node.inputs[17]) #state.out_transmission_roughness = state.out_roughness if state.parse_opacity: - state.out_ior = c.parse_value_input(node.inputs[3]) - state.out_opacity = c.parse_value_input(node.inputs[4]) + state.out_ior = c.parse_value_input(node.inputs['IOR']) + state.out_opacity = c.parse_value_input(node.inputs['Alpha']) def parse_bsdfdiffuse(node: bpy.types.ShaderNodeBsdfDiffuse, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[2]) - state.out_basecol = c.parse_vector_input(node.inputs[0]) - state.out_roughness = c.parse_value_input(node.inputs[1]) + c.write_normal(node.inputs['Normal']) + state.out_basecol = c.parse_vector_input(node.inputs['Color']) + state.out_roughness = c.parse_value_input(node.inputs['Roughness']) state.out_specular = '0.0' if bpy.app.version >= (4, 0, 0): def parse_bsdfsheen(node: bpy.types.ShaderNodeBsdfSheen, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[2]) - state.out_basecol = c.parse_vector_input(node.inputs[0]) - state.out_roughness = c.parse_value_input(node.inputs[1]) + c.write_normal(node.inputs['Normal']) + state.out_basecol = c.parse_vector_input(node.inputs['Color']) + state.out_roughness = c.parse_value_input(node.inputs['Roughness']) -if bpy.app.version < (4, 1, 0): +if bpy.app.version < (4, 0, 0): def parse_bsdfglossy(node: bpy.types.ShaderNodeBsdfGlossy, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[2]) - state.out_basecol = c.parse_vector_input(node.inputs[0]) - state.out_roughness = c.parse_value_input(node.inputs[1]) + c.write_normal(node.inputs['Normal']) + state.out_basecol = c.parse_vector_input(node.inputs['Color']) + state.out_roughness = c.parse_value_input(node.inputs['Roughness']) state.out_metallic = '1.0' else: def parse_bsdfglossy(node: bpy.types.ShaderNodeBsdfAnisotropic, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[4]) - state.out_basecol = c.parse_vector_input(node.inputs[0]) - state.out_roughness = c.parse_value_input(node.inputs[1]) + c.write_normal(node.inputs['Normal']) + state.out_basecol = c.parse_vector_input(node.inputs['Color']) + state.out_roughness = c.parse_value_input(node.inputs['Roughness']) state.out_metallic = '1.0' def parse_ambientocclusion(node: bpy.types.ShaderNodeAmbientOcclusion, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: # Single channel - state.out_occlusion = c.parse_vector_input(node.inputs[0]) + '.r' + state.out_occlusion = c.parse_vector_input(node.inputs['Distance']) + '.r' def parse_bsdfanisotropic(node: bpy.types.ShaderNodeBsdfAnisotropic, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[4]) + c.write_normal(node.inputs['Normal']) # Revert to glossy - state.out_basecol = c.parse_vector_input(node.inputs[0]) - state.out_roughness = c.parse_value_input(node.inputs[1]) + state.out_basecol = c.parse_vector_input(node.inputs['Color']) + state.out_roughness = c.parse_value_input(node.inputs['Roughness']) state.out_metallic = '1.0' def parse_emission(node: bpy.types.ShaderNodeEmission, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - emission_col = c.parse_vector_input(node.inputs[0]) - emission_strength = c.parse_value_input(node.inputs[1]) + emission_col = c.parse_vector_input(node.inputs['Color']) + emission_strength = c.parse_value_input(node.inputs['Strength']) state.out_emission_col = '({0} * {1})'.format(emission_col, emission_strength) state.out_basecol = 'vec3(0.0)' state.out_specular = '0.0' @@ -260,12 +276,12 @@ def parse_emission(node: bpy.types.ShaderNodeEmission, out_socket: NodeSocket, s def parse_bsdfglass(node: bpy.types.ShaderNodeBsdfGlass, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - state.out_basecol = c.parse_vector_input(node.inputs[0]) - c.write_normal(node.inputs[3]) - state.out_roughness = c.parse_value_input(node.inputs[1]) + state.out_basecol = c.parse_vector_input(node.inputs['Color']) + c.write_normal(node.inputs['Normal']) + state.out_roughness = c.parse_value_input(node.inputs['Roughness']) if state.parse_opacity: state.out_opacity = '1.0' - state.out_ior = c.parse_value_input(node.inputs[2]) + state.out_ior = c.parse_value_input(node.inputs['IOR']) def parse_bsdfhair(node: bpy.types.ShaderNodeBsdfHair, out_socket: NodeSocket, state: ParserState) -> None: @@ -280,22 +296,19 @@ def parse_holdout(node: bpy.types.ShaderNodeHoldout, out_socket: NodeSocket, sta def parse_bsdfrefraction(node: bpy.types.ShaderNodeBsdfRefraction, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - state.out_basecol = c.parse_vector_input(node.inputs[0]) - c.write_normal(node.inputs[3]) - state.out_roughness = c.parse_value_input(node.inputs[1]) + state.out_basecol = c.parse_vector_input(node.inputs['Color']) + c.write_normal(node.inputs['Normal']) + state.out_roughness = c.parse_value_input(node.inputs['Roughness']) if state.parse_opacity: state.out_opacity = '1.0' - state.out_ior = c.parse_value_input(node.inputs[2]) + state.out_ior = c.parse_value_input(node.inputs['IOR']) def parse_subsurfacescattering(node: bpy.types.ShaderNodeSubsurfaceScattering, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: # Mark that this material needs SSS mat_state.needs_sss = True - if bpy.app.version < (4, 1, 0): - c.write_normal(node.inputs[4]) - else: - c.write_normal(node.inputs[6]) - state.out_basecol = c.parse_vector_input(node.inputs[0]) + c.write_normal(node.inputs['Normal']) + state.out_basecol = c.parse_vector_input(node.inputs['Color']) def parse_bsdftoon(node: bpy.types.ShaderNodeBsdfToon, out_socket: NodeSocket, state: ParserState) -> None: @@ -305,21 +318,21 @@ def parse_bsdftoon(node: bpy.types.ShaderNodeBsdfToon, out_socket: NodeSocket, s def parse_bsdftranslucent(node: bpy.types.ShaderNodeBsdfTranslucent, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[1]) + c.write_normal(node.inputs['Normal']) if state.parse_opacity: - state.out_opacity = '(1.0 - {0}.r)'.format(c.parse_vector_input(node.inputs[0])) + state.out_opacity = '(1.0 - {0}.r)'.format(c.parse_vector_input(node.inputs['Color'])) state.out_ior = '1.0' def parse_bsdftransparent(node: bpy.types.ShaderNodeBsdfTransparent, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_opacity: - state.out_opacity = '(1.0 - {0}.r)'.format(c.parse_vector_input(node.inputs[0])) + state.out_opacity = '(1.0 - {0}.r)'.format(c.parse_vector_input(node.inputs['Color'])) state.out_ior = '1.0' -if bpy.app.version < (4, 1, 0): +if bpy.app.version < (4, 0, 0): def parse_bsdfvelvet(node: bpy.types.ShaderNodeBsdfVelvet, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[2]) - state.out_basecol = c.parse_vector_input(node.inputs[0]) + c.write_normal(node.inputs['Normal']) + state.out_basecol = c.parse_vector_input(node.inputs['Color']) state.out_roughness = '1.0' state.out_metallic = '1.0' diff --git a/leenkx/blender/lnx/material/node_meta.py b/leenkx/blender/lnx/material/node_meta.py index b559a569..1052ffff 100644 --- a/leenkx/blender/lnx/material/node_meta.py +++ b/leenkx/blender/lnx/material/node_meta.py @@ -205,8 +205,9 @@ ALL_NODES: Dict[str, MaterialNodeMeta] = { if bpy.app.version > (3, 2, 0): ALL_NODES['SEPARATE_COLOR'] = MaterialNodeMeta(parse_func=nodes_converter.parse_separate_color) ALL_NODES['COMBINE_COLOR'] = MaterialNodeMeta(parse_func=nodes_converter.parse_combine_color) -if bpy.app.version < (4, 1, 0): +if bpy.app.version < (4, 0, 0): ALL_NODES['BSDF_VELVET'] = MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfvelvet) +if bpy.app.version < (4, 1, 0): ALL_NODES['TEX_MUSGRAVE'] = MaterialNodeMeta(parse_func=nodes_texture.parse_tex_musgrave) if bpy.app.version >= (4, 0, 0): ALL_NODES['BSDF_SHEEN'] = MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfsheen)