Compare commits

..

13 Commits

10 changed files with 212 additions and 171 deletions

Binary file not shown.

Binary file not shown.

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

@ -136,7 +136,10 @@ def always() -> float:
# Force ui redraw
if state.redraw_ui:
if bpy.context.screen is not None:
krom_active = bool(lnx.render_engine._active_krom_engines)
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D' and krom_active:
continue
if area.type in ('NODE_EDITOR', 'PROPERTIES', 'VIEW_3D'):
area.tag_redraw()
state.redraw_ui = False

View File

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

View File

@ -804,41 +804,71 @@ def viewport_build_done():
_viewport_pending_launches.clear()
log.error('Viewport build failed, check console')
def play_viewport(viewport_id, width=1920, height=1080):
"""Launch Krom in a viewport"""
global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build
def play(viewport_id=None, width=1920, height=1080):
"""Launch player external or embedded when viewport_id is passed"""
global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build, scripts_mtime
if not viewport_id:
log.error('No viewport_id: play_viewport requires an id')
return
if 'Lnx' not in bpy.data.worlds:
if viewport_id and 'Lnx' not in bpy.data.worlds:
log.error('No Lnx world found - cannot start viewport server')
return
wrd = bpy.data.worlds['Lnx']
target = 'krom' if viewport_id else runtime_to_target()
krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js'
if os.path.exists(krom_js_path) and not wrd.lnx_recompile:
run_viewport_runtime(viewport_id, width, height)
return
khajs_path = get_khajs_path(target)
if not wrd.lnx_cache_build or \
not os.path.isfile(khajs_path) or \
assets.khafile_defs_last != assets.khafile_defs or \
state.last_target != state.target:
wrd.lnx_recompile = True
pending_entry = (viewport_id, width, height)
if pending_entry not in _viewport_pending_launches:
_viewport_pending_launches.append(pending_entry)
state.last_target = state.target
if _viewport_build_in_progress:
if _viewport_proc_build is not None and _viewport_proc_build.poll() is None:
log.info(f'Build in progress, viewport {viewport_id} will launch when ready')
state.mod_scripts = []
script_path = lnx.utils.get_fp() + '/Sources/' + lnx.utils.safestr(wrd.lnx_project_package)
if os.path.isdir(script_path):
new_mtime = scripts_mtime
for fn in glob.iglob(os.path.join(script_path, '**', '*.hx'), recursive=True):
mtime = os.path.getmtime(fn)
if scripts_mtime < mtime:
lnx.utils.fetch_script_props(fn)
fn = fn.split('Sources/')[1]
fn = fn[:-3]
fn = fn.replace('/', '.')
state.mod_scripts.append(fn)
wrd.lnx_recompile = True
if new_mtime < mtime:
new_mtime = mtime
scripts_mtime = new_mtime
if len(state.mod_scripts) > 0:
lnx.utils.fetch_trait_props()
if viewport_id:
krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js'
if os.path.exists(krom_js_path) and not wrd.lnx_recompile:
run_viewport_runtime(viewport_id, width, height)
return
else:
_viewport_build_in_progress = False
_viewport_build_in_progress = True
pending_entry = (viewport_id, width, height)
if pending_entry not in _viewport_pending_launches:
_viewport_pending_launches.append(pending_entry)
build(target='krom', is_viewport=True, viewport_width=width, viewport_height=height)
if _viewport_build_in_progress:
if _viewport_proc_build is not None and _viewport_proc_build.poll() is None:
log.info(f'Build in progress, viewport {viewport_id} will launch when ready')
return
else:
_viewport_build_in_progress = False
compile_viewport(assets_only=(not wrd.lnx_recompile))
_viewport_build_in_progress = True
assets.invalidate_enabled = False
build(target='krom', is_viewport=True, viewport_width=width, viewport_height=height)
compile_viewport(assets_only=(not wrd.lnx_recompile))
assets.invalidate_enabled = True
else:
build(target=target, is_play=True)
compile(assets_only=(not wrd.lnx_recompile))
def stop_viewport(viewport_id=None):
@ -905,43 +935,6 @@ def get_khajs_path(target):
return lnx.utils.build_dir() + '/debug/krom/krom.js'
return lnx.utils.build_dir() + '/debug/html5/kha.js'
def play():
global scripts_mtime
wrd = bpy.data.worlds['Lnx']
build(target=runtime_to_target(), is_play=True)
khajs_path = get_khajs_path(state.target)
if not wrd.lnx_cache_build or \
not os.path.isfile(khajs_path) or \
assets.khafile_defs_last != assets.khafile_defs or \
state.last_target != state.target:
wrd.lnx_recompile = True
state.last_target = state.target
# Trait sources modified
state.mod_scripts = []
script_path = lnx.utils.get_fp() + '/Sources/' + lnx.utils.safestr(wrd.lnx_project_package)
if os.path.isdir(script_path):
new_mtime = scripts_mtime
for fn in glob.iglob(os.path.join(script_path, '**', '*.hx'), recursive=True):
mtime = os.path.getmtime(fn)
if scripts_mtime < mtime:
lnx.utils.fetch_script_props(fn) # Trait props
fn = fn.split('Sources/')[1]
fn = fn[:-3] #.hx
fn = fn.replace('/', '.')
state.mod_scripts.append(fn)
wrd.lnx_recompile = True
if new_mtime < mtime:
new_mtime = mtime
scripts_mtime = new_mtime
if len(state.mod_scripts) > 0: # Trait props
lnx.utils.fetch_trait_props()
compile(assets_only=(not wrd.lnx_recompile))
def build_success():
log.clear()
wrd = bpy.data.worlds['Lnx']

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

@ -11,12 +11,16 @@ import sys
import os
import array
import atexit
import time
from mathutils import Quaternion, Matrix, Vector
import lnx.make_state as state
HAS_GPU_STATE = bpy.app.version >= (3, 0, 0)
if not HAS_GPU_STATE:
try:
import bgl
HAS_BGL = callable(getattr(bgl, 'glGenTextures', None))
except ImportError:
HAS_BGL = False
SHADER_UNIFORM_COLOR = 'UNIFORM_COLOR' if bpy.app.version >= (3, 4, 0) else '2D_UNIFORM_COLOR'
SHADER_IMAGE = 'IMAGE' if bpy.app.version >= (3, 4, 0) else '2D_IMAGE'
HAS_GPU_TEXTURE = bpy.app.version >= (3, 0, 0)
@ -124,6 +128,8 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._last_frame_id = 0
self._width = 0
self._height = 0
self._tex_width = 0
self._tex_height = 0
self._initialized = False
self._shader = None
self._batch = None
@ -140,6 +146,8 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._last_region_dims = None
self._header_buffer = (ctypes.c_ubyte * VIEWPORT_HEADER_SIZE)()
self._engine_id = id(self)
self._viewport_image = None
self._float_buffer = None
# print(f"New engine instance created: {self._engine_id}")
def _restore_overlay(self, context=None):
@ -247,7 +255,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
def deferred_launch():
try:
import lnx.make as make
make.play_viewport(viewport_id, width, height)
make.play(viewport_id, width, height)
except Exception as e:
print(f"Failed to launch viewport: {e}")
import traceback
@ -498,7 +506,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
"""Handle invalid/stale shared memory by cleaning up and allowing re-init."""
self._cleanup_shared_memory()
self._initialized = False
if not HAS_GPU_TEXTURE and hasattr(self, '_gl_texture_buf') and self._gl_texture_buf[0] != 0:
if HAS_BGL and hasattr(self, '_gl_texture_buf') and self._gl_texture_buf[0] != 0:
try:
bgl.glDeleteTextures(1, self._gl_texture_buf)
except:
@ -783,21 +791,8 @@ class KromViewportEngine(bpy.types.RenderEngine):
if len(pixels) < num_pixels:
return
if HAS_GPU_TEXTURE:
if HAS_NUMPY:
pixels[3::4] = 255
float_array = pixels.astype(np.float32) / 255.0
buffer = gpu.types.Buffer('FLOAT', num_pixels, float_array)
else:
pixel_array = array.array('B', pixels[:num_pixels])
for i in range(3, len(pixel_array), 4):
pixel_array[i] = 255
float_pixels = [p / 255.0 for p in pixel_array]
buffer = gpu.types.Buffer('FLOAT', num_pixels, float_pixels)
self._texture = gpu.types.GPUTexture((width, height), format='SRGB8_A8', data=buffer)
else:
# bgl path for Blender < 3.0
if HAS_BGL:
# Fast path: bgl with GL_UNSIGNED_BYTE, no float conversion, in-place update
if not hasattr(self, '_gl_texture_buf') or self._gl_texture_buf[0] == 0:
self._gl_texture_buf = bgl.Buffer(bgl.GL_INT, 1)
bgl.glGenTextures(1, self._gl_texture_buf)
@ -817,12 +812,43 @@ class KromViewportEngine(bpy.types.RenderEngine):
pixel_buffer = bgl.Buffer(bgl.GL_UNSIGNED_BYTE, num_pixels, bytes(pixel_array))
internal_format = getattr(bgl, 'GL_SRGB8_ALPHA8', bgl.GL_RGBA8)
bgl.glTexImage2D(bgl.GL_TEXTURE_2D, 0, internal_format, width, height, 0,
bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, pixel_buffer)
if self._tex_width == width and self._tex_height == height:
bgl.glTexSubImage2D(bgl.GL_TEXTURE_2D, 0, 0, 0, width, height,
bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, pixel_buffer)
else:
bgl.glTexImage2D(bgl.GL_TEXTURE_2D, 0, internal_format, width, height, 0,
bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, pixel_buffer)
bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)
self._texture = tex_id
elif HAS_GPU_TEXTURE:
# Blender 4.x+ path: use bpy.Image + gpu.texture.from_image
# from_image returns a cached GPUTexture (no 20ms constructor)
# pixels updated via foreach_set (fast C-level copy) + update()
if self._viewport_image is None or self._viewport_image.size[0] != width or self._viewport_image.size[1] != height:
if self._viewport_image is not None:
bpy.data.images.remove(self._viewport_image)
self._viewport_image = bpy.data.images.new(
"_lnx_viewport", width, height, float_buffer=False)
self._float_buffer = None
if HAS_NUMPY:
pixels[3::4] = 255
if self._float_buffer is None or len(self._float_buffer) != num_pixels:
self._float_buffer = np.empty(num_pixels, dtype=np.float32)
self._float_buffer[:num_pixels] = pixels[:num_pixels]
self._float_buffer *= (1.0 / 255.0)
self._viewport_image.pixels.foreach_set(self._float_buffer)
else:
pixel_array = array.array('B', pixels[:num_pixels])
for i in range(3, len(pixel_array), 4):
pixel_array[i] = 255
float_pixels = [p / 255.0 for p in pixel_array]
self._viewport_image.pixels.foreach_set(float_pixels)
self._viewport_image.update()
self._texture = gpu.texture.from_image(self._viewport_image)
self._tex_width = width
self._tex_height = height
@ -838,11 +864,17 @@ class KromViewportEngine(bpy.types.RenderEngine):
"""Called by Blender when the engine instance is destroyed."""
self._stop_krom_process()
self._restore_overlay()
if not HAS_GPU_TEXTURE and hasattr(self, '_gl_texture_buf') and self._gl_texture_buf[0] != 0:
if HAS_BGL and hasattr(self, '_gl_texture_buf') and self._gl_texture_buf[0] != 0:
try:
bgl.glDeleteTextures(1, self._gl_texture_buf)
except:
pass
if self._viewport_image is not None:
try:
bpy.data.images.remove(self._viewport_image)
except:
pass
self._viewport_image = None
if self._viewport_id and self._viewport_id in _active_krom_engines:
del _active_krom_engines[self._viewport_id]
@ -928,6 +960,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._last_region_dims = dims
self._request_resize(region.width, region.height)
self._write_camera_matrices(context)
self._read_krom_camera(context)
pixels = self._read_framebuffer()
@ -966,7 +999,8 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._batch_dims = dims
self._shader.bind()
if HAS_GPU_TEXTURE:
_is_gl_texture = isinstance(self._texture, int)
if not _is_gl_texture:
self._shader.uniform_sampler("image", self._texture)
else:
bgl.glActiveTexture(bgl.GL_TEXTURE0)
@ -975,7 +1009,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._batch.draw(self._shader)
if not HAS_GPU_TEXTURE:
if _is_gl_texture:
bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)
if HAS_GPU_STATE:

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