Merge pull request 'main' (#132) from Onek8/LNXSDK:main into main

Reviewed-on: #132
This commit is contained in:
2026-07-24 09:22:31 +00:00
5 changed files with 99 additions and 88 deletions

View File

@ -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"
}

View File

@ -1,3 +1,4 @@
import bpy
import importlib
import inspect
import pkgutil

View File

@ -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,13 +197,9 @@ 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)):
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])
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:
@ -203,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'
@ -264,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:
@ -284,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:
@ -309,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'

View File

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

View File

@ -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')