From d45c632dcd31634c81f83d29a3572ed3d66a27d0 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Tue, 24 Feb 2026 17:35:26 -0800 Subject: [PATCH] Patch_2 --- leenkx/blender/lnx/exporter.py | 24 +- leenkx/blender/lnx/handlers.py | 40 + leenkx/blender/lnx/keymap.py | 3 + .../lnx/lightmapper/utility/encoding.py | 5 +- .../lnx/lightmapper/utility/gui/Viewport.py | 5 +- .../lnx/logicnode/custom/LN_create_style.py | 5 +- .../blender/lnx/logicnode/lnx_node_group.py | 35 + leenkx/blender/lnx/make.py | 303 ++++ leenkx/blender/lnx/make_renderpath.py | 41 +- leenkx/blender/lnx/make_state.py | 4 + leenkx/blender/lnx/material/cycles.py | 9 +- .../lnx/material/cycles_nodes/nodes_shader.py | 37 +- leenkx/blender/lnx/material/make.py | 19 +- leenkx/blender/lnx/material/make_cluster.py | 2 +- leenkx/blender/lnx/material/make_mesh.py | 16 +- .../blender/lnx/material/make_morph_target.py | 4 + leenkx/blender/lnx/material/make_refract.py | 24 +- leenkx/blender/lnx/material/make_shader.py | 11 +- leenkx/blender/lnx/material/make_skin.py | 8 + leenkx/blender/lnx/material/make_transluc.py | 2 +- leenkx/blender/lnx/material/mat_state.py | 1 + leenkx/blender/lnx/material/shader.py | 26 +- leenkx/blender/lnx/props.py | 3 +- leenkx/blender/lnx/props_renderpath.py | 50 +- leenkx/blender/lnx/props_ui.py | 25 +- leenkx/blender/lnx/render_engine.py | 1341 +++++++++++++++++ leenkx/blender/lnx/write_data.py | 32 +- leenkx/blender/start.py | 4 + 28 files changed, 1982 insertions(+), 97 deletions(-) create mode 100644 leenkx/blender/lnx/render_engine.py diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index fdc8abc..bd7c0b6 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -595,6 +595,20 @@ class LeenkxExporter: return space.region_3d.window_matrix, space.region_3d.is_perspective return None, False + @staticmethod + def get_viewport_lens_fov() -> Optional[float]: + """Get FOV from viewport lens setting.""" + play_area = LeenkxExporter.get_view3d_area() + if play_area is None: + return None + for space in play_area.spaces: + if space.type == 'VIEW_3D': + lens = space.lens + sensor = 32.0 + fov = 2.0 * math.atan(sensor / (2.0 * lens)) + return fov + return None + def write_bone_matrices(self, scene, action): # profile_time = time.time() begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) @@ -2785,7 +2799,7 @@ class LeenkxExporter: wrd = bpy.data.worlds['Lnx'] phys_enabled = wrd.lnx_physics != 'Disabled' - phys_pkg = 'bullet' if wrd.lnx_physics_engine == 'Bullet' else 'oimo' + phys_pkg = 'bullet' if wrd.lnx_physics_engine == 'Bullet' else ('jolt' if wrd.lnx_physics_engine == 'Jolt' else 'oimo') # Rigid body trait if bobject.rigid_body is not None and phys_enabled: @@ -3099,7 +3113,7 @@ class LeenkxExporter: if wrd.lnx_physics != 'Disabled' and LeenkxExporter.export_physics: if 'traits' not in self.output: self.output['traits'] = [] - phys_pkg = 'bullet' if wrd.lnx_physics_engine == 'Bullet' else 'oimo' + phys_pkg = 'bullet' if wrd.lnx_physics_engine == 'Bullet' else ('jolt' if wrd.lnx_physics_engine == 'Jolt' else 'oimo') out_trait = { 'type': 'Script', @@ -3204,7 +3218,7 @@ class LeenkxExporter: LeenkxExporter.export_physics = True assets.add_khafile_def('lnx_physics_soft') - phys_pkg = 'bullet' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Bullet' else 'oimo' + phys_pkg = 'bullet' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Bullet' else ('jolt' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Jolt' else 'oimo') out_trait = {'type': 'Script', 'class_name': 'leenkx.trait.physics.' + phys_pkg + '.SoftBody'} # ClothModifier if modifier.type == 'CLOTH': @@ -3228,7 +3242,7 @@ class LeenkxExporter: def add_hook_mod(o, bobject: bpy.types.Object, target_name, group_name): LeenkxExporter.export_physics = True - phys_pkg = 'bullet' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Bullet' else 'oimo' + phys_pkg = 'bullet' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Bullet' else ('jolt' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Jolt' else 'oimo') out_trait = {'type': 'Script', 'class_name': 'leenkx.trait.physics.' + phys_pkg + '.PhysicsHook'} verts = [] @@ -3254,7 +3268,7 @@ class LeenkxExporter: return LeenkxExporter.export_physics = True - phys_pkg = 'bullet' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Bullet' else 'oimo' + phys_pkg = 'bullet' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Bullet' else ('jolt' if bpy.data.worlds['Lnx'].lnx_physics_engine == 'Jolt' else 'oimo') breaking_threshold = rbc.breaking_threshold if rbc.use_breaking else 0 trait = { diff --git a/leenkx/blender/lnx/handlers.py b/leenkx/blender/lnx/handlers.py index b29ecd1..04af3e5 100644 --- a/leenkx/blender/lnx/handlers.py +++ b/leenkx/blender/lnx/handlers.py @@ -37,6 +37,7 @@ else: _active_threads: Dict[threading.Thread, Callable] = {} _last_poll_time = 0.0 _consecutive_empty_polls = 0 +_last_render_engine = None @persistent def on_depsgraph_update_post(self): @@ -141,6 +142,43 @@ def always() -> float: return 0.5 +def check_render_engine() -> float: + global _last_render_engine + + try: + scene = None + if hasattr(bpy.context, 'scene') and bpy.context.scene is not None: + scene = bpy.context.scene + elif len(bpy.data.scenes) > 0: + scene = bpy.data.scenes[0] + + if scene is None: + return 1.0 + + current_engine = scene.render.engine + + if _last_render_engine != current_engine: + if current_engine == 'KROM_VIEWPORT': + try: + import lnx.make_world as make_world + make_world.build() + except Exception as e: + log.warn(f'World shader build failed: {e}') + + elif _last_render_engine == 'KROM_VIEWPORT': + try: + make.stop_viewport() + except Exception as e: + log.warn(f'Failed to stop viewport: {e}') + + _last_render_engine = current_engine + except Exception as e: + print(f"Engine Error: {e}") + import traceback + traceback.print_exc() + + return 0.5 + def poll_threads() -> float: """ @@ -389,6 +427,7 @@ def register(): bpy.app.timers.register(always, persistent=True) bpy.app.timers.register(poll_threads, persistent=True) + bpy.app.timers.register(check_render_engine, persistent=True) if lnx.utils.get_fp() != '': # TODO: On windows, on_load_post is not called when opening .blend file from explorer @@ -405,6 +444,7 @@ def register(): def unregister(): unload_py_libraries() + bpy.app.timers.unregister(check_render_engine) bpy.app.timers.unregister(poll_threads) bpy.app.timers.unregister(always) diff --git a/leenkx/blender/lnx/keymap.py b/leenkx/blender/lnx/keymap.py index 0643a4c..38c8b19 100644 --- a/leenkx/blender/lnx/keymap.py +++ b/leenkx/blender/lnx/keymap.py @@ -37,6 +37,8 @@ def register(): kmn.keymap_items.new('lnx.edit_group_tree', 'TAB', 'PRESS') kmn.keymap_items.new('node.tree_path_parent', 'TAB', 'PRESS', ctrl=True) kmn.keymap_items.new('lnx.ungroup_group_tree', 'G', 'PRESS', alt=True) + # Use custom frame operator that works with all node trees including custom ones + kmn.keymap_items.new('lnx.frame_selected_nodes', 'J', 'PRESS', ctrl=True) def unregister(): @@ -50,3 +52,4 @@ def unregister(): kmn.keymap_items.remove(kmn.keymap_items['lnx.edit_group_tree']) kmn.keymap_items.remove(kmn.keymap_items['node.tree_path_parent']) kmn.keymap_items.remove(kmn.keymap_items['lnx.ungroup_group_tree']) + kmn.keymap_items.remove(kmn.keymap_items['lnx.frame_selected_nodes']) diff --git a/leenkx/blender/lnx/lightmapper/utility/encoding.py b/leenkx/blender/lnx/lightmapper/utility/encoding.py index 1fa6de3..96e442b 100644 --- a/leenkx/blender/lnx/lightmapper/utility/encoding.py +++ b/leenkx/blender/lnx/lightmapper/utility/encoding.py @@ -1,12 +1,9 @@ -import bpy, math, os, gpu, importlib +import bpy, math, os, gpu, bgl, importlib import numpy as np from . import utility from fractions import Fraction from gpu_extras.batch import batch_for_shader -if bpy.app.version < (4, 0, 0): - import bgl - def splitLogLuvAlphaAtlas(imageIn, outDir, quality): pass diff --git a/leenkx/blender/lnx/lightmapper/utility/gui/Viewport.py b/leenkx/blender/lnx/lightmapper/utility/gui/Viewport.py index f338f82..4e1484a 100644 --- a/leenkx/blender/lnx/lightmapper/utility/gui/Viewport.py +++ b/leenkx/blender/lnx/lightmapper/utility/gui/Viewport.py @@ -1,9 +1,6 @@ -import bpy, blf, os, gpu +import bpy, blf, bgl, os, gpu from gpu_extras.batch import batch_for_shader -if bpy.app.version < (4, 0, 0): - import bgl - class ViewportDraw: def __init__(self, context, text): diff --git a/leenkx/blender/lnx/logicnode/custom/LN_create_style.py b/leenkx/blender/lnx/logicnode/custom/LN_create_style.py index c2d3eb5..6a320f1 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_create_style.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_create_style.py @@ -197,7 +197,10 @@ class CreateStyleNode(LnxLogicTreeNode): properties += self.inputs[ind].name + ':' ind += 1 - self['property1'] = properties + try: + self['property1'] = properties + except AttributeError: + pass # Skip write if context doesn't allow it return self.get('property0', 60) diff --git a/leenkx/blender/lnx/logicnode/lnx_node_group.py b/leenkx/blender/lnx/logicnode/lnx_node_group.py index 388e50c..67cc40a 100644 --- a/leenkx/blender/lnx/logicnode/lnx_node_group.py +++ b/leenkx/blender/lnx/logicnode/lnx_node_group.py @@ -537,6 +537,40 @@ class LnxAddCallGroupNode(bpy.types.Operator): return {'FINISHED'} +class LnxFrameSelectedNodes(bpy.types.Operator): + """Frame selected nodes - works with custom node trees""" + bl_idname = 'lnx.frame_selected_nodes' + bl_label = 'Frame Selected Nodes' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if context.space_data is None or context.space_data.type != 'NODE_EDITOR': + return False + return context.space_data.edit_tree is not None + + def execute(self, context): + tree = context.space_data.edit_tree + selected_nodes = [n for n in tree.nodes if n.select] + + if not selected_nodes: + self.report({'WARNING'}, "No nodes selected") + return {'CANCELLED'} + + frame = tree.nodes.new('NodeFrame') + frame.label = "Frame" + + for node in selected_nodes: + node.parent = frame + + for node in tree.nodes: + node.select = False + frame.select = True + tree.nodes.active = frame + + return {'FINISHED'} + + class LNX_PT_LogicGroupPanel(bpy.types.Panel): bl_label = 'Leenkx Logic Group' bl_idname = 'LNX_PT_LogicGroupPanel' @@ -575,6 +609,7 @@ __REG_CLASSES = ( TreeVarNameConflictItem, LnxUngroupGroupTree, LnxAddCallGroupNode, + LnxFrameSelectedNodes, LNX_PT_LogicGroupPanel ) register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) diff --git a/leenkx/blender/lnx/make.py b/leenkx/blender/lnx/make.py index b938eaa..13bb8c3 100644 --- a/leenkx/blender/lnx/make.py +++ b/leenkx/blender/lnx/make.py @@ -586,6 +586,298 @@ def play_done(): log.clear() live_patch.stop() + +_viewport_processes = {} + +def _generate_viewport_id(space_data=None): + """Generate a unique viewport ID from space_data pointer or random.""" + if space_data is not None: + try: + return hex(space_data.as_pointer())[-6:] + except: + pass + import random + return hex(random.randint(0, 0xFFFFFF))[2:].zfill(6) + +def _get_viewport_shmem_name(viewport_id): + """Get shared memory name for a specific viewport.""" + return f"KROM_VIEWPORT_FB_{viewport_id}" + +def _kill_viewport_process(viewport_id): + """Kill a specific viewport's Krom process.""" + global _viewport_processes + + if viewport_id in _viewport_processes: + proc = _viewport_processes[viewport_id] + try: + proc.terminate() + proc.wait(timeout=2) + except: + try: + proc.kill() + except: + pass + del _viewport_processes[viewport_id] + +def _kill_all_viewport_processes(): + """Kill all viewport Krom processes.""" + global _viewport_processes + + for viewport_id in list(_viewport_processes.keys()): + _kill_viewport_process(viewport_id) + + if lnx.utils.get_os() == 'win': + try: + result = subprocess.run( + ['tasklist', '/FI', 'IMAGENAME eq Krom.exe', '/FO', 'CSV', '/NH'], + capture_output=True, text=True, timeout=5 + ) + if 'Krom.exe' in result.stdout: + subprocess.run(['taskkill', '/F', '/IM', 'Krom.exe'], + capture_output=True, timeout=5) + import time + time.sleep(0.3) + except: + pass + +def run_viewport_runtime(viewport_id, width=1920, height=1080): + """Launch a viewport which gets its own Krom process with unique shared memory.""" + global _viewport_processes + + if 'Lnx' not in bpy.data.worlds: + log.warn('No Lnx world found - cannot start viewport server') + return None, None + + _kill_viewport_process(viewport_id) + + shmem_name = _get_viewport_shmem_name(viewport_id) + + wrd = bpy.data.worlds['Lnx'] + krom_location, krom_path = lnx.utils.krom_paths() + path = lnx.utils.get_fp_build() + '/debug/krom' + path_resources = path + '-resources' + + if not os.path.exists(path + '/krom.js'): + log.warn(f'Krom build not found at {path}/krom.js - build project first') + return None, None + + os.chdir(krom_location) + + cmd = [krom_path, path, path_resources] + if lnx.utils.get_os() == 'win': + cmd.append('--consolepid') + cmd.append(str(os.getpid())) + if wrd.lnx_audio == 'Disabled': + cmd.append('--nosound') + + cmd.append('--viewport-server') + cmd.append('--shmem') + cmd.append(shmem_name) + cmd.append('--viewport-width') + cmd.append(str(width)) + cmd.append('--viewport-height') + cmd.append(str(height)) + + try: + proc = subprocess.Popen(cmd) + _viewport_processes[viewport_id] = proc + log.info(f'Started: {viewport_id} (shmem={shmem_name})') + return proc, shmem_name + except Exception as e: + log.error(f'Failed to start viewport runtime: {e}') + return None, None + +def stop_viewport_runtime(viewport_id): + """Stop a specific viewport's Krom process.""" + _kill_viewport_process(viewport_id) + +_viewport_build_in_progress = False +_viewport_pending_launches = [] # List of (viewport_id, width, height) tuples +_viewport_proc_build = None # Separate process tracker for viewport builds + +def build_viewport(viewport_id, width=1920, height=1080): + """Build project for viewport""" + global _viewport_build_in_progress, _viewport_pending_launches, profile_time + + wrd = bpy.data.worlds.get('Lnx') + if not wrd: + log.warn('No Lnx world found - cannot build for viewport') + return + + krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js' + if os.path.exists(krom_js_path) and not wrd.lnx_recompile: + log.info(f'Using cached viewport build for {viewport_id}') + run_viewport_runtime(viewport_id, width, height) + return + + pending_entry = (viewport_id, width, height) + if pending_entry not in _viewport_pending_launches: + _viewport_pending_launches.append(pending_entry) + log.info(f'Queued viewport {viewport_id} for launch after build') + + # If a build is already in progress and there is an actual build process, wait + # _viewport_proc_build checks viewport ,not state.proc_build (which is for play button) + 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_id} - launching when ready') + return + else: + log.info(f'Resetting stale viewport build state') + _viewport_build_in_progress = False + + _viewport_build_in_progress = True + profile_time = time.time() + + log.info(f'Starting viewport build for {len(_viewport_pending_launches)} viewport(s)') + + # Set viewport mode flags but not the is_play flag meant for external launcher + state.is_viewport = True + state.viewport_width = width + state.viewport_height = height + state.target = 'krom' + state.is_play = False # NOT play mode + state.is_publish = False + state.is_export = False + + log.clear(clear_warnings=True, clear_errors=True) + + sdk_path = lnx.utils.get_sdk_path() + fp = lnx.utils.get_fp() + os.chdir(fp) + + sources_path = 'Sources/' + lnx.utils.safestr(wrd.lnx_project_package) + if not os.path.exists(sources_path): + os.makedirs(sources_path) + + log.info('Exporting scene data...') + export_data(fp, sdk_path) + + log.info('Starting Krom compilation for viewport...') + compile_viewport(assets_only=(not wrd.lnx_recompile)) + +def compile_viewport(assets_only=False): + """Compile for viewport mode using separate process tracking.""" + global _viewport_proc_build + + wrd = bpy.data.worlds['Lnx'] + fp = lnx.utils.get_fp() + os.chdir(fp) + + node_path = lnx.utils.get_node_path() + khamake_path = lnx.utils.get_khamake_path() + cmd = [node_path, khamake_path, 'krom'] + + ffmpeg_path = lnx.utils.get_ffmpeg_path() + if ffmpeg_path not in (None, ''): + cmd.append('--ffmpeg') + cmd.append(ffmpeg_path) + + cmd.append('-g') + cmd.append(lnx.utils.get_gapi()) + cmd.append('--shaderversion') + cmd.append('330') + + if lnx.utils.get_khamake_threads() != 1: + cmd.append('--parallelAssetConversion') + cmd.append(str(lnx.utils.get_khamake_threads())) + + cmd.append('--to') + cmd.append(lnx.utils.build_dir() + '/debug') + + if not wrd.lnx_verbose_output: + cmd.append("--quiet") + + if assets_only: + cmd.append('--nohaxe') + cmd.append('--noproject') + + log.info(f'Running: {" ".join(cmd)}') + _viewport_proc_build = run_proc(cmd, viewport_build_done) + +def viewport_build_done(): + """Called when viewport build completes - launches all pending Krom processes.""" + global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build + + log.info('Viewport compilation finished') + + if _viewport_proc_build is None: + _viewport_build_in_progress = False + return + + result = _viewport_proc_build.poll() + _viewport_proc_build = None + _viewport_build_in_progress = False + state.redraw_ui = True + + if result == 0: + bpy.data.worlds['Lnx'].lnx_recompile = False + + if _viewport_pending_launches: + pending = _viewport_pending_launches.copy() + _viewport_pending_launches.clear() + log.info(f'Launching {len(pending)} Krom instance(s)') + for viewport_id, width, height in pending: + run_viewport_runtime(viewport_id, width, height) + else: + log.info('No pending viewports to launch') + else: + _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 + + if not viewport_id: + return + + if 'Lnx' not in bpy.data.worlds: + return + + wrd = bpy.data.worlds['Lnx'] + + 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 + + pending_entry = (viewport_id, width, height) + if pending_entry not in _viewport_pending_launches: + _viewport_pending_launches.append(pending_entry) + + 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 # Reset stale state + + _viewport_build_in_progress = True + + sdk_path = lnx.utils.get_sdk_path() + fp = lnx.utils.get_fp() + os.chdir(fp) + + sources_path = 'Sources/' + lnx.utils.safestr(wrd.lnx_project_package) + if not os.path.exists(sources_path): + os.makedirs(sources_path) + + # export data same as play() but without setting state.is_play + state.target = 'krom' + state.is_publish = False + state.is_export = False + export_data(fp, sdk_path) + + compile_viewport(assets_only=(not wrd.lnx_recompile)) + + +def stop_viewport(viewport_id=None): + """Stop a specific viewport or all viewports.""" + if viewport_id: + _kill_viewport_process(viewport_id) + else: + _kill_all_viewport_processes() + def assets_done(): if state.proc_build == None: return @@ -761,6 +1053,17 @@ def build_success(): cmd.append(str(pid)) if wrd.lnx_audio == 'Disabled': cmd.append('--nosound') + if state.is_viewport: + cmd.append('--viewport-server') + cmd.append('--shmem') + if state.viewport_id: + cmd.append(f'KROM_VIEWPORT_FB_{state.viewport_id}') + else: + cmd.append('KROM_VIEWPORT_FB') + cmd.append('--viewport-width') + cmd.append(str(state.viewport_width)) + cmd.append('--viewport-height') + cmd.append(str(state.viewport_height)) elif state.target.startswith(('windows-hl', 'linux-hl', 'macos-hl')): log.info(f"Runtime Hashlink/C target: {state.target}") diff --git a/leenkx/blender/lnx/make_renderpath.py b/leenkx/blender/lnx/make_renderpath.py index bef12e2..0e09c2f 100644 --- a/leenkx/blender/lnx/make_renderpath.py +++ b/leenkx/blender/lnx/make_renderpath.py @@ -79,12 +79,6 @@ def add_world_defs(): assets.add_khafile_def('lnx_shadowmap_atlas_lod') assets.add_khafile_def('rp_shadowmap_atlas_lod_subdivisions={0}'.format(int(rpdat.rp_shadowmap_atlas_lod_subdivisions))) - # SS - if rpdat.rp_ssgi == 'RTGI' or rpdat.rp_ssgi == 'RTAO': - if rpdat.rp_ssgi == 'RTGI': - wrd.world_defs += '_RTGI' - if rpdat.lnx_ssgi_rays == '9': - wrd.world_defs += '_SSGICone9' if rpdat.rp_autoexposure: wrd.world_defs += '_AutoExposure' @@ -306,19 +300,25 @@ def build(): if rpdat.rp_supersampling == '4': assets.add_shader_pass('supersample_resolve') - assets.add_khafile_def('rp_ssgi={0}'.format(rpdat.rp_ssgi)) - if rpdat.rp_ssgi != 'Off': - if rpdat.rp_ssgi == 'SSAO': - wrd.world_defs += '_SSAO' - assets.add_shader_pass('ssao_pass') - assets.add_shader_pass('blur_edge_pass') - elif rpdat.rp_ssgi == 'SSGI': - wrd.world_defs += '_SSGI' - assets.add_shader_pass('ssgi_pass') - assets.add_shader_pass('blur_edge_pass') - else: - assets.add_shader_pass('ssgi_pass') - assets.add_shader_pass('blur_edge_pass') + if rpdat.rp_fsr1 != 'Off': + assets.add_khafile_def('rp_fsr1') + wrd.world_defs += '_FSR1_{0}'.format(rpdat.rp_fsr1) + assets.add_shader_pass('fsr1_easu_pass') + assets.add_shader_pass('fsr1_rcas_pass') + + if rpdat.rp_ssao: + assets.add_khafile_def('rp_ssao') + wrd.world_defs += '_SSAO' + assets.add_shader_pass('ssao_pass') + assets.add_shader_pass('blur_edge_pass') + if rpdat.lnx_ssao_half_res: + assets.add_khafile_def('rp_ssao_half') + + if rpdat.rp_ssgi: + assets.add_khafile_def('rp_ssgi') + wrd.world_defs += '_SSGI' + assets.add_shader_pass('ssgi_pass') + assets.add_shader_pass('ssgi_blur_pass') if rpdat.lnx_ssgi_half_res: assets.add_khafile_def('rp_ssgi_half') @@ -362,6 +362,7 @@ def build(): assets.add_khafile_def('rp_stereo') assets.add_khafile_def('lnx_vr') wrd.world_defs += '_VR' + wrd.world_defs += '_VRStereo' has_voxels = lnx.utils.voxel_support() if rpdat.rp_voxels != "Off" and has_voxels and rpdat.lnx_material_model == 'Full': @@ -421,7 +422,7 @@ def build(): wrd.world_defs += '_SSS' assets.add_shader_pass('sss_pass') - if (rpdat.rp_ssr and rpdat.lnx_ssr_half_res) or (rpdat.rp_ssgi != 'Off' and rpdat.lnx_ssgi_half_res) or rpdat.rp_voxels != "Off": + if (rpdat.rp_ssr and rpdat.lnx_ssr_half_res) or (rpdat.rp_ssao and rpdat.lnx_ssao_half_res) or (rpdat.rp_ssgi and rpdat.lnx_ssgi_half_res) or rpdat.rp_voxels != "Off": assets.add_shader_pass('downsample_depth') if rpdat.rp_motionblur != 'Off': diff --git a/leenkx/blender/lnx/make_state.py b/leenkx/blender/lnx/make_state.py index 2daf175..19afcda 100644 --- a/leenkx/blender/lnx/make_state.py +++ b/leenkx/blender/lnx/make_state.py @@ -18,3 +18,7 @@ if not lnx.is_reload(__name__): is_export = False is_play = False is_publish = False + is_viewport = False + viewport_width = 1920 + viewport_height = 1080 + viewport_id = None diff --git a/leenkx/blender/lnx/material/cycles.py b/leenkx/blender/lnx/material/cycles.py index f74fcf8..25ea4ac 100644 --- a/leenkx/blender/lnx/material/cycles.py +++ b/leenkx/blender/lnx/material/cycles.py @@ -16,11 +16,13 @@ # import os import shutil +import subprocess from typing import Any, Dict, Optional, Tuple import bpy +import os -import lnx.assets +import lnx.assets as assets import lnx.log as log import lnx.make_state import lnx.material.cycles_functions as c_functions @@ -1003,7 +1005,12 @@ def make_texture( wrd = bpy.data.worlds['Lnx'] max_size = int(wrd.lnx_max_texture_size) if max_size > 0 and image is not None: + original_filepath = filepath filepath = resize_texture_if_needed(image, filepath, max_size) + + if filepath != original_filepath: + resized_filename = lnx.utils.extract_filename(filepath) + tex['file'] = lnx.utils.safestr(resized_filename) # Link image path to assets # TODO: Khamake converts .PNG to .jpg? Convert ext to lowercase on windows diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py index c8f35bc..9658566 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py @@ -1,4 +1,3 @@ -from __future__ import annotations import bpy from bpy.types import NodeSocket @@ -36,12 +35,17 @@ def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket, state.curshader.write('{0}float {1} = 1.0 - {2};'.format(prefix, fac_inv_var, fac_var)) mat_state.emission_type = mat_state.EmissionType.NO_EMISSION + sss_before_1 = mat_state.needs_sss bc1, rough1, met1, occ1, spec1, opac1, ior1, emi1 = c.parse_shader_input(node.inputs[1]) + sss_1 = mat_state.needs_sss ek1 = mat_state.emission_type mat_state.emission_type = mat_state.EmissionType.NO_EMISSION + mat_state.needs_sss = sss_before_1 # Reset to state before parsing input 1 bc2, rough2, met2, occ2, spec2, opac2, ior2, emi2 = c.parse_shader_input(node.inputs[2]) + sss_2 = mat_state.needs_sss ek2 = mat_state.emission_type + mat_state.needs_sss = sss_1 or sss_2 if state.parse_surface: state.out_basecol = '({0} * {3} + {1} * {2})'.format(bc1, bc2, fac_var, fac_inv_var) @@ -57,12 +61,17 @@ def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket, def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, state: ParserState) -> None: mat_state.emission_type = mat_state.EmissionType.NO_EMISSION + sss_before_1 = mat_state.needs_sss bc1, rough1, met1, occ1, spec1, opac1, ior1, emi1 = c.parse_shader_input(node.inputs[0]) + sss_1 = mat_state.needs_sss ek1 = mat_state.emission_type mat_state.emission_type = mat_state.EmissionType.NO_EMISSION + mat_state.needs_sss = sss_before_1 # Reset to state before parsing input 0 bc2, rough2, met2, occ2, spec2, opac2, ior2, emi2 = c.parse_shader_input(node.inputs[1]) + sss_2 = mat_state.needs_sss ek2 = mat_state.emission_type + mat_state.needs_sss = sss_1 or sss_2 if state.parse_surface: state.out_basecol = '({0} + {1})'.format(bc1, bc2) @@ -82,6 +91,8 @@ if bpy.app.version < (2, 92, 0): 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: + 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]) @@ -103,7 +114,8 @@ if bpy.app.version >= (2, 92, 0) and bpy.app.version <= (4, 1, 0): if state.parse_surface: c.write_normal(node.inputs[22]) state.out_basecol = c.parse_vector_input(node.inputs[0]) - # subsurface = c.parse_vector_input(node.inputs[1]) + if node.inputs[1].is_linked or node.inputs[1].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]) @@ -138,14 +150,27 @@ if bpy.app.version > (4, 1, 0): if state.parse_surface: c.write_normal(node.inputs[5]) state.out_basecol = c.parse_vector_input(node.inputs[0]) + + sss_input = node.inputs.get('Subsurface Weight') or 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]) - if bpy.app.version > (4, 2, 4): - state.out_specular = c.parse_value_input(node.inputs[13]) + + specular_socket = node.inputs.get('Specular IOR Level') + if specular_socket is not None: + state.out_specular = f'({c.parse_value_input(specular_socket)} * 2.0)' else: - state.out_specular = c.parse_value_input(node.inputs[12]) + specular_socket = node.inputs.get('Specular') + if specular_socket is not None: + state.out_specular = c.parse_value_input(specular_socket) + else: + state.out_specular = '1.0' + state.out_roughness = c.parse_value_input(node.inputs[2]) # Prevent black material when metal = 1.0 and roughness = 0.0 try: @@ -278,6 +303,8 @@ def parse_bsdfrefraction(node: bpy.types.ShaderNodeBsdfRefraction, out_socket: N 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: diff --git a/leenkx/blender/lnx/material/make.py b/leenkx/blender/lnx/material/make.py index 7fb727f..757e86c 100644 --- a/leenkx/blender/lnx/material/make.py +++ b/leenkx/blender/lnx/material/make.py @@ -8,6 +8,7 @@ import lnx.log as log import lnx.material.cycles as cycles import lnx.material.make_shader as make_shader import lnx.material.mat_batch as mat_batch +import lnx.material.mat_state as mat_state import lnx.material.mat_utils as mat_utils import lnx.node_utils import lnx.utils @@ -17,6 +18,7 @@ if lnx.is_reload(__name__): cycles = lnx.reload_module(cycles) make_shader = lnx.reload_module(make_shader) mat_batch = lnx.reload_module(mat_batch) + mat_state = lnx.reload_module(mat_state) mat_utils = lnx.reload_module(mat_utils) lnx.node_utils = lnx.reload_module(lnx.node_utils) lnx.utils = lnx.reload_module(lnx.utils) @@ -60,6 +62,7 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]], shader_data_name = material.lnx_custom_material bind_constants = {'mesh': []} bind_textures = {'mesh': []} + mat_uses_sss = False make_shader.make_instancing_and_skinning(material, mat_users) @@ -77,10 +80,11 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]], log.warn(f'Material "{material.name}": skipping export of bind texture at slot {idx + 1} ("{item.uniform_name}") with no image selected') elif not wrd.lnx_batch_materials or material.name.startswith('lnxdefault'): - rpasses, shader_data, shader_data_name, bind_constants, bind_textures = make_shader.build(material, mat_users, mat_lnxusers) + rpasses, shader_data, shader_data_name, bind_constants, bind_textures, mat_uses_sss = make_shader.build(material, mat_users, mat_lnxusers) sd = shader_data.sd else: - rpasses, shader_data, shader_data_name, bind_constants, bind_textures = mat_batch.get(material) + result = mat_batch.get(material) + rpasses, shader_data, shader_data_name, bind_constants, bind_textures, mat_uses_sss = result sd = shader_data.sd sss_used = False @@ -106,9 +110,12 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]], elif rpdat.rp_sss_state != 'Off': const = {'name': 'materialID'} - if needs_sss: + # Use per-material SSS flag from shader build + if mat_uses_sss: const['intValue'] = 2 sss_used = True + if '_SSS' not in wrd.world_defs: + wrd.world_defs += '_SSS' else: const['intValue'] = 0 c['bind_constants'].append(const) @@ -167,4 +174,10 @@ def material_needs_sss(material: Material) -> bool: if sss_node is not None and sss_node.outputs[0].is_linked and (sss_node.inputs[8].is_linked or sss_node.inputs[8].default_value != 0.0): return True + for principled_node in lnx.node_utils.iter_nodes_by_type(material.node_tree, 'BSDF_PRINCIPLED'): + if principled_node is not None and principled_node.outputs[0].is_linked: + sss_input = principled_node.inputs.get('Subsurface Weight') or principled_node.inputs.get('Subsurface') + if sss_input is not None and (sss_input.is_linked or sss_input.default_value > 0.0): + return True + return False diff --git a/leenkx/blender/lnx/material/make_cluster.py b/leenkx/blender/lnx/material/make_cluster.py index abe68f1..1ee44e7 100644 --- a/leenkx/blender/lnx/material/make_cluster.py +++ b/leenkx/blender/lnx/material/make_cluster.py @@ -107,7 +107,7 @@ def write(vert: shader.Shader, frag: shader.Shader): if '_MicroShadowing' in wrd.world_defs and not is_mobile: frag.write('\t, occlusion') if '_SSRS' in wrd.world_defs: - frag.add_uniform('sampler2D gbufferD') + frag.add_uniform('sampler2D gbufferD', top=True) frag.add_uniform('mat4 invVP', '_inverseViewProjectionMatrix') frag.add_uniform('vec3 eye', '_cameraPosition') frag.write(', gbufferD, invVP, eye') diff --git a/leenkx/blender/lnx/material/make_mesh.py b/leenkx/blender/lnx/material/make_mesh.py index 6713f6b..3d62747 100644 --- a/leenkx/blender/lnx/material/make_mesh.py +++ b/leenkx/blender/lnx/material/make_mesh.py @@ -198,7 +198,7 @@ def make_deferred(con_mesh, rpasses): rpdat = lnx.utils.get_rp() lnx_discard = mat_state.material.lnx_discard - parse_opacity = lnx_discard or 'translucent' or 'refraction' in rpasses + parse_opacity = lnx_discard or 'translucent' in rpasses or 'refraction' in rpasses make_base(con_mesh, parse_opacity=parse_opacity) @@ -213,6 +213,7 @@ def make_deferred(con_mesh, rpasses): opac = '0.9999' # 1.0 - eps frag.write('if (opacity < {0}) discard;'.format(opac)) + frag.add_out(f'vec4 fragColor[GBUF_SIZE]') if '_gbuffer2' in wrd.world_defs: @@ -281,7 +282,7 @@ def make_deferred(con_mesh, rpasses): frag.write('#endif') if '_SSRefraction' in wrd.world_defs or '_VoxelRefract' in wrd.world_defs: - frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(1.0, 1.0, 0.0, 1.0);') + frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(1.0, 0.0, 0.0, 1.0);') return con_mesh @@ -559,7 +560,7 @@ def make_forward(con_mesh): frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') if rpdat.rp_ss_refraction or rpdat.lnx_voxelgi_refract: - frag.write(f'fragColor[2] = vec4(1.0, 1.0, 0.0, 0.0);') + frag.write(f'fragColor[2] = vec4(1.0, 0.0, 0.0, 1.0);') else: frag.add_out('vec4 fragColor[1]') @@ -716,8 +717,12 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): else: frag.write('vec3 indirect = envl;') + if '_VoxelShadow' in wrd.world_defs or '_VoxelGI' in wrd.world_defs: + velocity_already_defined = '_gbuffer2' in wrd.world_defs and '_Veloc' in wrd.world_defs + if not velocity_already_defined: + frag.write('vec2 velocity = gl_FragCoord.xy;') + if '_VoxelGI' in wrd.world_defs: - frag.write('vec2 velocity = gl_FragCoord.xy;') frag.write('vec4 diffuse_indirect = traceDiffuse(wposition, n, voxels, clipmaps);') frag.write('indirect = (diffuse_indirect.rgb * albedo * (1.0 - F) + envl * (1.0 - diffuse_indirect.a)) * voxelgiDiff;') frag.write('if (roughness < 1.0 && specular > 0.0) {') @@ -810,12 +815,11 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): frag.write(', true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight') if '_VoxelShadow' in wrd.world_defs: frag.write(', voxels, voxelsSDF, clipmaps') - if '_Veloc' in wrd.world_defs or '_VoxelShadow' in wrd.world_defs: frag.write(', velocity') if '_MicroShadowing' in wrd.world_defs: frag.write(', occlusion') if '_SSRS' in wrd.world_defs: - frag.add_uniform('sampler2D gbufferD') + frag.add_uniform('sampler2D gbufferD', top=True) frag.add_uniform('mat4 invVP', '_inverseViewProjectionMatrix') frag.add_uniform('vec3 eye', '_cameraPosition') frag.write(', gbufferD, invVP, eye') diff --git a/leenkx/blender/lnx/material/make_morph_target.py b/leenkx/blender/lnx/material/make_morph_target.py index 29d7394..16c233a 100644 --- a/leenkx/blender/lnx/material/make_morph_target.py +++ b/leenkx/blender/lnx/material/make_morph_target.py @@ -6,6 +6,8 @@ else: lnx.enable_reload(__name__) def morph_pos(vert): + if vert.has_attrib('vec2 texCoordMorph = morph * texUnpack;'): + return rpdat = lnx.utils.get_rp() vert.add_include('compiled.inc') vert.add_include('std/morph_target.glsl') @@ -22,6 +24,8 @@ def morph_pos(vert): vert.write_attrib('spos.xyz /= posUnpack;') def morph_nor(vert, is_bone, prep): + if vert.has_attrib('vec3 morphNor = vec3(0, 0, 0);'): + return vert.write_attrib('vec3 morphNor = vec3(0, 0, 0);') vert.write_attrib('getMorphedNormal(texCoordMorph, vec3(nor.xy, pos.w), morphNor);') if not is_bone: diff --git a/leenkx/blender/lnx/material/make_refract.py b/leenkx/blender/lnx/material/make_refract.py index b30f6b2..6b00856 100644 --- a/leenkx/blender/lnx/material/make_refract.py +++ b/leenkx/blender/lnx/material/make_refract.py @@ -18,7 +18,15 @@ else: def make(context_id): - con_refract = mat_state.data.add_context({ 'name': context_id, 'depth_write': True, 'compare_mode': 'less', 'cull_mode': 'clockwise' }) + con_refract = mat_state.data.add_context({ + 'name': context_id, + 'depth_write': False, + 'compare_mode': 'less', + 'cull_mode': 'clockwise', + 'blend_source': 'blend_one', + 'blend_destination': 'inverse_source_alpha', + 'blend_operation': 'add' + }) make_mesh.make_forward_base(con_refract, parse_opacity=True, transluc_pass=True) vert = con_refract.vert @@ -51,14 +59,16 @@ def make(context_id): frag.write('const uint matid = 0;') if rpdat.rp_renderer == 'Deferred': - frag.write('fragColor[0] = vec4(n.xy, roughness, packFloatInt16(metallic, matid));') - frag.write('fragColor[1] = vec4(direct + indirect, packFloat2(occlusion, specular));') + frag.write('fragColor[0] = vec4(n.xy, roughness, 1.0);') + frag.write('vec3 finalColor = direct + indirect;') + frag.write('fragColor[1] = vec4(finalColor * opacity, opacity);') else: - frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') - frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') + frag.write('vec3 finalColor = direct + indirect;') + frag.write('fragColor[0] = vec4(finalColor * opacity, opacity);') + frag.write('fragColor[1] = vec4(n.xy, roughness, 1.0);') - frag.write('fragColor[2] = vec4(ior, opacity, 0.0, 1.0);') - # frag.write('fragColor[2] = vec4(ior, opacity, packFloat2(basecol.r, basecol.g), basecol.b);') + frag.write('fragColor[2] = vec4(ior, 1.0 - opacity, gl_FragCoord.z, 1.0);') + # frag.write('fragColor[2] = vec4(ior, 1.0 - opacity, packFloat2(basecol.r, basecol.g), basecol.b);') make_finalize.make(con_refract) diff --git a/leenkx/blender/lnx/material/make_shader.py b/leenkx/blender/lnx/material/make_shader.py index 8fd4dda..983ae54 100644 --- a/leenkx/blender/lnx/material/make_shader.py +++ b/leenkx/blender/lnx/material/make_shader.py @@ -56,6 +56,9 @@ def build(material: Material, mat_users: Dict[Material, List[Object]], mat_lnxus if mat_state.output_node is None: # Place empty material output to keep compiler happy.. mat_state.output_node = mat_state.nodes.new('ShaderNodeOutputMaterial') + + # reset for each material + mat_state.needs_sss = False wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() @@ -130,7 +133,8 @@ def build(material: Material, mat_users: Dict[Material, List[Object]], mat_lnxus shader_data_path = lnx.utils.get_fp_build() + '/compiled/Shaders/' + shader_data_name + '.lnx' assets.add_shader_data(shader_data_path) - return rpasses, mat_state.data, shader_data_name, bind_constants, bind_textures + needs_sss_result = mat_state.needs_sss + return rpasses, mat_state.data, shader_data_name, bind_constants, bind_textures, needs_sss_result def write_shaders(rel_path: str, con: ShaderContext, rpass: str, matname: str) -> None: @@ -146,6 +150,11 @@ def write_shader(rel_path: str, shader: Shader, ext: str, rpass: str, matname: s if shader is None or shader.is_linked: return + validation_issues = shader.validate() + if validation_issues: + for issue in validation_issues: + log.warn(f"Shader validation issue in {matname}_{rpass}.{ext}: {issue}. ") + # TODO: blend context if rpass == 'mesh' and mat_state.material.lnx_blending: rpass = 'blend' diff --git a/leenkx/blender/lnx/material/make_skin.py b/leenkx/blender/lnx/material/make_skin.py index da49690..bbbcfee 100644 --- a/leenkx/blender/lnx/material/make_skin.py +++ b/leenkx/blender/lnx/material/make_skin.py @@ -7,6 +7,9 @@ else: def skin_pos(vert): + if vert.has_attrib('vec4 skinA;'): + return + vert.add_include('compiled.inc') rpdat = lnx.utils.get_rp() @@ -25,6 +28,11 @@ def skin_pos(vert): def skin_nor(vert, is_morph, prep): + morph_normal_code = 'wnormal = normalize(N * (morphNor + 2.0 * cross(skinA.xyz' + static_normal_code = 'wnormal = normalize(N * (vec3(nor.xy, pos.w) + 2.0 * cross(skinA.xyz' + if vert.has_attrib(morph_normal_code) or vert.has_attrib(static_normal_code): + return + rpdat = lnx.utils.get_rp() if(is_morph): vert.write_attrib(prep + 'wnormal = normalize(N * (morphNor + 2.0 * cross(skinA.xyz, cross(skinA.xyz, morphNor) + skinA.w * morphNor)));') diff --git a/leenkx/blender/lnx/material/make_transluc.py b/leenkx/blender/lnx/material/make_transluc.py index e3f52b4..dc39a3f 100644 --- a/leenkx/blender/lnx/material/make_transluc.py +++ b/leenkx/blender/lnx/material/make_transluc.py @@ -41,7 +41,7 @@ def make(context_id): frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') - frag.write('vec4 premultipliedReflect = vec4(vec3(direct + indirect * 0.5) * opacity, opacity);'); + frag.write('vec4 premultipliedReflect = vec4(vec3(direct + indirect) * opacity, opacity);'); frag.write('float w = clamp(pow(min(1.0, premultipliedReflect.a * 10.0) + 0.01, 3.0) * 1e8 * pow(1.0 - (gl_FragCoord.z) * 0.9, 3.0), 1e-2, 3e3);') frag.write('fragColor[0] = vec4(premultipliedReflect.rgb * w, premultipliedReflect.a);') frag.write('fragColor[1] = vec4(premultipliedReflect.a * w, 0.0, 0.0, 1.0);') diff --git a/leenkx/blender/lnx/material/mat_state.py b/leenkx/blender/lnx/material/mat_state.py index c78d367..be44b69 100644 --- a/leenkx/blender/lnx/material/mat_state.py +++ b/leenkx/blender/lnx/material/mat_state.py @@ -38,3 +38,4 @@ texture_grad = False # Sample textures using textureGrad() con_mesh = None # Mesh context uses_instancing = False # Whether the current material has at least one user with instancing enabled emission_type = EmissionType.NO_EMISSION +needs_sss = False diff --git a/leenkx/blender/lnx/material/shader.py b/leenkx/blender/lnx/material/shader.py index fa8be4f..9507358 100644 --- a/leenkx/blender/lnx/material/shader.py +++ b/leenkx/blender/lnx/material/shader.py @@ -361,9 +361,14 @@ class Shader: def write_header(self, s): self.header += s + '\n' - def write_attrib(self, s): + def write_attrib(self, s, unique=False): + if unique and s in self.main_attribs: + return self.main_attribs += '\t' + s + '\n' + def has_attrib(self, s): + return s in self.main_attribs + def is_equal(self, sh): self.vstruct_to_vsin() return self.ins == sh.ins and \ @@ -394,6 +399,25 @@ class Shader: for e in vs: self.add_in('vec' + self.data_size(e['data']) + ' ' + e['name']) + def validate(self): + import re + issues = [] + + # Check for duplicate variable declarations in main_attribs + var_pattern = re.compile(r'\b(vec[234]|float|int|mat[234])\s+(\w+)\s*[;=]') + declared_vars = {} + + for line in self.main_attribs.split('\n'): + match = var_pattern.search(line) + if match: + var_type, var_name = match.groups() + if var_name in declared_vars: + issues.append(f"Duplicate variable declaration: '{var_name}' (type: {var_type})") + else: + declared_vars[var_name] = var_type + + return issues + def get(self): if self.noprocessing: return self.main diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index 87afd2b..c2d746d 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -205,7 +205,8 @@ def init_properties(): name="Physics", default='Auto', update=assets.invalidate_compiler_cache) bpy.types.World.lnx_physics_engine = EnumProperty( items=[('Bullet', 'Bullet', 'Bullet'), - ('Oimo', 'Oimo', 'Oimo')], + ('Oimo', 'Oimo', 'Oimo'), + ('Jolt', 'Jolt', 'Jolt')], name="Physics Engine", default='Bullet', update=assets.invalidate_compiler_cache) bpy.types.World.lnx_physics_fixed_step = FloatProperty( name="Fixed Step", default=1/60, min=0, max=1, diff --git a/leenkx/blender/lnx/props_renderpath.py b/leenkx/blender/lnx/props_renderpath.py index 1e19a5d..9fd0542 100644 --- a/leenkx/blender/lnx/props_renderpath.py +++ b/leenkx/blender/lnx/props_renderpath.py @@ -72,7 +72,8 @@ def update_preset(self, context): rpdat.rp_antialiasing = 'SMAA' rpdat.rp_compositornodes = True rpdat.rp_volumetriclight = False - rpdat.rp_ssgi = 'SSAO' + rpdat.rp_ssao = True + rpdat.rp_ssgi = False rpdat.lnx_ssrs = False rpdat.lnx_micro_shadowing = False rpdat.rp_ssr = False @@ -110,7 +111,8 @@ def update_preset(self, context): rpdat.rp_antialiasing = 'Off' rpdat.rp_compositornodes = False rpdat.rp_volumetriclight = False - rpdat.rp_ssgi = 'Off' + rpdat.rp_ssao = False + rpdat.rp_ssgi = False rpdat.lnx_ssrs = False rpdat.lnx_micro_shadowing = False rpdat.rp_ssr = False @@ -152,8 +154,9 @@ def update_preset(self, context): rpdat.rp_antialiasing = 'TAA' rpdat.rp_compositornodes = True rpdat.rp_volumetriclight = False - rpdat.rp_ssgi = 'SSGI' - rpdat.lnx_ssrs = False + rpdat.rp_ssao = True + rpdat.rp_ssgi = True + rpdat.lnx_ssrs = True rpdat.lnx_micro_shadowing = True rpdat.rp_ssr = True rpdat.rp_ss_refraction = True @@ -193,7 +196,8 @@ def update_preset(self, context): rpdat.rp_antialiasing = 'Off' rpdat.rp_compositornodes = False rpdat.rp_volumetriclight = False - rpdat.rp_ssgi = 'Off' + rpdat.rp_ssao = False + rpdat.rp_ssgi = False rpdat.lnx_ssrs = False rpdat.lnx_micro_shadowing = False rpdat.rp_ssr = False @@ -380,6 +384,15 @@ class LnxRPListItem(bpy.types.PropertyGroup): ('2', '2', '2'), ('4', '4', '4')], name="Super Sampling", description="Screen resolution multiplier", default='1', update=update_renderpath) + rp_fsr1: EnumProperty( + items=[('Off', 'Off', 'Disable FSR'), + ('Ultra_Quality', 'Ultra Quality', 'FSR Ultra Quality - Maximum sharpening, best quality'), + ('Quality', 'Quality', 'FSR Quality - High quality sharpening'), + ('Balanced', 'Balanced', 'FSR Balanced - Balance between quality and performance'), + ('Performance', 'Performance', 'FSR Performance - Minimal sharpening, best performance'), + ('Custom', 'Custom', 'FSR Custom - Set your own sharpness value')], + name="FSR", description="AMD FidelityFX Super Resolution 1 quality preset", default='Quality', update=update_renderpath) + rp_fsr1_sharpness: FloatProperty(name="FSR Sharpness", description="Custom sharpness (0 = max sharp, 1 = no sharpening)", default=0.25, min=0.0, max=1.0, update=update_renderpath) rp_antialiasing: EnumProperty( items=[('Off', 'No AA', 'Off'), ('FXAA', 'FXAA', 'FXAA'), @@ -389,14 +402,8 @@ class LnxRPListItem(bpy.types.PropertyGroup): rp_volumetriclight: BoolProperty(name="Volumetric Light", description="Use volumetric lighting", default=False, update=update_renderpath) rp_ssr: BoolProperty(name="SSR", description="Screen space reflections", default=False, update=update_renderpath) rp_ss_refraction: BoolProperty(name="SSRefraction", description="Screen space refractions", default=False, update=update_renderpath) - rp_ssgi: EnumProperty( - items=[('Off', 'No AO', 'Off'), - ('SSAO', 'SSAO', 'Screen space ambient occlusion'), - ('SSGI', 'SSGI', 'Screen space global illumination') - #('RTAO', 'RTAO', 'Ray-traced ambient occlusion') - # ('RTGI', 'RTGI', 'Ray-traced global illumination') - ], - name="SSGI", description="Screen space global illumination", default='SSAO', update=update_renderpath) + rp_ssao: BoolProperty(name="SSAO", description="Screen space ambient occlusion", default=False, update=update_renderpath) + rp_ssgi: BoolProperty(name="SSGI", description="Screen space global illumination", default=False, update=update_renderpath) rp_bloom: BoolProperty(name="Bloom", description="Bloom processing", default=False, update=update_renderpath) lnx_bloom_follow_blender: BoolProperty(name="Use Blender Settings", description="Use Blender settings instead of Leenkx settings", default=True) rp_motionblur: EnumProperty( @@ -548,17 +555,14 @@ class LnxRPListItem(bpy.types.PropertyGroup): lnx_water_density: FloatProperty(name="Density", default=1.0, update=assets.invalidate_shader_cache) lnx_water_refract: FloatProperty(name="Refract", default=1.0, update=assets.invalidate_shader_cache) lnx_water_reflect: FloatProperty(name="Reflect", default=1.0, update=assets.invalidate_shader_cache) - lnx_ssgi_strength: FloatProperty(name="Strength", default=1.250, update=assets.invalidate_shader_cache) - lnx_ssgi_radius: FloatProperty(name="Radius", default=0.750, update=assets.invalidate_shader_cache) + lnx_ssao_strength: FloatProperty(name="Strength", default=1.0, update=assets.invalidate_shader_cache) + lnx_ssao_radius: FloatProperty(name="Radius", default=1.0, update=assets.invalidate_shader_cache) + lnx_ssao_samples: IntProperty(name="Samples", default=8, update=assets.invalidate_shader_cache) + lnx_ssao_half_res: BoolProperty(name="Half Res", description="Trace in half resolution", default=False, update=assets.invalidate_shader_cache) + lnx_ssgi_strength: FloatProperty(name="Strength", default=1.0, update=assets.invalidate_shader_cache) + lnx_ssgi_radius: FloatProperty(name="Radius", default=1.0, update=assets.invalidate_shader_cache) lnx_ssgi_step: FloatProperty(name="Step", default=2.0, update=assets.invalidate_shader_cache) - lnx_ssgi_samples: IntProperty(name="Samples", default=32, update=assets.invalidate_shader_cache) - """ - lnx_ssgi_rays: EnumProperty( - items=[('9', '9', '9'), - ('5', '5', '5'), - ], - name="Rays", description="Number of rays to trace for RTAO", default='5', update=assets.invalidate_shader_cache) - """ + lnx_ssgi_samples: IntProperty(name="Samples", default=16, update=assets.invalidate_shader_cache) lnx_ssgi_half_res: BoolProperty(name="Half Res", description="Trace in half resolution", default=False, update=assets.invalidate_shader_cache) lnx_bloom_threshold: FloatProperty(name="Threshold", description="Brightness above which a pixel is contributing to the bloom effect", min=0, default=0.8, update=assets.invalidate_shader_cache) lnx_bloom_knee: FloatProperty(name="Knee", description="Smoothen transition around the threshold (higher values = smoother transition)", min=0, max=1, default=0.5, update=assets.invalidate_shader_cache) diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index 1c0c555..cb0ebaa 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -385,6 +385,7 @@ class LNX_PT_WorldPropsPanel(bpy.types.Panel): layout.prop(world, 'lnx_light_ies_texture') layout.prop(world, 'lnx_light_clouds_texture') + layout.separator() layout.prop(world, 'lnx_use_clouds') col = layout.column(align=True) col.enabled = world.lnx_use_clouds @@ -1962,6 +1963,9 @@ class LNX_PT_RenderPathPostProcessPanel(bpy.types.Panel): col = layout.column() col.prop(rpdat, "rp_antialiasing") col.prop(rpdat, "rp_supersampling") + col.prop(rpdat, "rp_fsr1") + if rpdat.rp_fsr1 == 'Custom': + col.prop(rpdat, "rp_fsr1_sharpness") col = layout.column() col.prop(rpdat, 'lnx_rp_resolution') @@ -1971,12 +1975,21 @@ class LNX_PT_RenderPathPostProcessPanel(bpy.types.Panel): col.prop(rpdat, 'rp_dynres') layout.separator() + col = layout.column() + col.prop(rpdat, "rp_ssao") + sub = col.column() + sub.enabled = rpdat.rp_ssao + sub.prop(rpdat, 'lnx_ssao_half_res') + sub.prop(rpdat, 'lnx_ssao_radius') + sub.prop(rpdat, 'lnx_ssao_strength') + sub.prop(rpdat, 'lnx_ssao_samples') + layout.separator() + col = layout.column() col.prop(rpdat, "rp_ssgi") sub = col.column() - sub.enabled = rpdat.rp_ssgi != 'Off' + sub.enabled = rpdat.rp_ssgi sub.prop(rpdat, 'lnx_ssgi_half_res') - #sub.prop(rpdat, 'lnx_ssgi_rays') sub.prop(rpdat, 'lnx_ssgi_radius') sub.prop(rpdat, 'lnx_ssgi_strength') sub.prop(rpdat, 'lnx_ssgi_samples') @@ -2880,10 +2893,10 @@ class LNX_PT_PhysicsProps(bpy.types.Panel): layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] - if wrd.lnx_physics_engine != 'Bullet' and wrd.lnx_physics_engine != 'Oimo': + if wrd.lnx_physics_engine != 'Bullet' and wrd.lnx_physics_engine != 'Oimo' and wrd.lnx_physics_engine != 'Jolt': row = layout.row() row.alert = True - row.label(text="Physics debug drawing is only supported for the Bullet and Oimo physics engines") + row.label(text="Physics debug drawing is only supported for the Bullet, Oimo, and Jolt physics engines") col = layout.column(align=False) col.prop(wrd, "lnx_physics_fixed_step") @@ -2906,10 +2919,10 @@ class LNX_PT_PhysicsDebugDrawingPanel(bpy.types.Panel): layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] - if wrd.lnx_physics_engine != 'Bullet' and wrd.lnx_physics_engine != 'Oimo': + if wrd.lnx_physics_engine != 'Bullet' and wrd.lnx_physics_engine != 'Oimo' and wrd.lnx_physics_engine != 'Jolt': row = layout.row() row.alert = True - row.label(text="Physics debug drawing is only supported for the Bullet and Oimo physics engines") + row.label(text="Physics debug drawing is only supported for the Bullet, Oimo, and Jolt physics engines") col = layout.column(align=False) col.prop(wrd, "lnx_physics_dbg_draw_wireframe") diff --git a/leenkx/blender/lnx/render_engine.py b/leenkx/blender/lnx/render_engine.py new file mode 100644 index 0000000..22e5efd --- /dev/null +++ b/leenkx/blender/lnx/render_engine.py @@ -0,0 +1,1341 @@ +""" +Leenkx viewport framebuffer via shared memory. +""" + +import bpy +import gpu +from gpu_extras.batch import batch_for_shader +import struct +import ctypes +import sys +import os +import array +import atexit + +HAS_GPU_STATE = bpy.app.version >= (3, 0, 0) +if not HAS_GPU_STATE: + import bgl +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) +try: + import numpy as np + HAS_NUMPY = True +except ImportError: + HAS_NUMPY = False + +# TODO: Fix correctly +def _build_srgb_to_linear_lut(): + lut = [] + for i in range(256): + srgb = i / 255.0 + if srgb <= 0.04045: + linear = srgb / 12.92 + else: + linear = ((srgb + 0.055) / 1.055) ** 2.4 + lut.append(linear) + return lut + +_srgb_to_linear_lut_list = _build_srgb_to_linear_lut() +if HAS_NUMPY: + _srgb_to_linear_lut = np.array(_srgb_to_linear_lut_list, dtype=np.float32) + +if sys.platform == 'win32': + import mmap +else: + import mmap + try: + import posix_ipc + HAS_POSIX_IPC = True + except ImportError: + HAS_POSIX_IPC = False + +VIEWPORT_BUILD = 0x4B524F4D +VIEWPORT_VERSION = 1 +VIEWPORT_SHMEM_NAME_BASE = "KROM_VIEWPORT_FB" + +def _get_viewport_shmem_name(viewport_id=None): + """Get shared memory name for a specific viewport.""" + if viewport_id: + return f"{VIEWPORT_SHMEM_NAME_BASE}_{viewport_id}" + return VIEWPORT_SHMEM_NAME_BASE + +# Header structure offsets (must match viewport_server.h SharedFramebufferHeader) +# build: 0-3 (4 bytes, uint32) +# version: 4-7 (4 bytes, uint32) +# width: 8-11 (4 bytes, uint32) +# height: 12-15 (4 bytes, uint32) +# frame_id: 16-23 (8 bytes, uint64) +# ready_flag: 24-27 (4 bytes, uint32) +# format: 28-31 (4 bytes, uint32) +# viewport_width: 32-35 (4 bytes, uint32) +# viewport_height: 36-39 (4 bytes, uint32) +# resize_request: 40-43 (4 bytes, uint32) +# shutdown_flag: 44-47 (4 bytes, uint32) +# input_enabled: 48-51 (4 bytes, uint32) +# view_matrix: 52-115 (64 bytes, 16 floats) +# proj_matrix: 116-179 (64 bytes, 16 floats) +# krom_camera_pos: 180-191 (12 bytes, 3 floats) +# krom_camera_rot: 192-207 (16 bytes, 4 floats) +# krom_camera_dirty: 208-211 (4 bytes, uint32) +# input_write_idx: 212-215 (4 bytes, uint32) +# input_read_idx: 216-219 (4 bytes, uint32) +# input_events: 220-603 (384 bytes, 32 events * 12 bytes each) +# Total: 604 bytes + +OFFSET_BUILD = 0 +OFFSET_WIDTH = 8 +OFFSET_FRAME_ID = 16 +OFFSET_READY_FLAG = 24 +OFFSET_VIEWPORT_WIDTH = 32 +OFFSET_RESIZE_REQUEST = 40 +OFFSET_SHUTDOWN_FLAG = 44 +OFFSET_INPUT_ENABLED = 48 +OFFSET_VIEW_MATRIX = 52 +OFFSET_PROJ_MATRIX = 116 +OFFSET_KROM_CAMERA_POS = 180 # 12 bytes vec +OFFSET_KROM_CAMERA_ROT = 192 # 16 bytes quat +OFFSET_KROM_CAMERA_DIRTY = 208 # 1 uint32 +OFFSET_INPUT_WRITE_IDX = 212 +OFFSET_INPUT_READ_IDX = 216 +OFFSET_INPUT_EVENTS = 220 +MAX_INPUT_EVENTS = 32 +INPUT_EVENT_SIZE = 12 +VIEWPORT_HEADER_SIZE = 604 + +INPUT_EVENT_NONE = 0 +INPUT_EVENT_MOUSE_MOVE = 1 +INPUT_EVENT_MOUSE_DOWN = 2 +INPUT_EVENT_MOUSE_UP = 3 +INPUT_EVENT_KEY_DOWN = 4 +INPUT_EVENT_KEY_UP = 5 +INPUT_EVENT_MOUSE_WHEEL = 6 + +MOUSE_BUTTON_LEFT = 0 +MOUSE_BUTTON_RIGHT = 1 +MOUSE_BUTTON_MIDDLE = 2 + +_rendered_mode_pending = False +_rendered_mode_retries = 0 +_msgbus_owner = object() + +@bpy.app.handlers.persistent +def _on_depsgraph_update(scene, depsgraph): + """Called on depsgraph update - check if render engine changed to Krom.""" + try: + if scene.render.engine == 'KROM_VIEWPORT': + for window in bpy.context.window_manager.windows: + for area in window.screen.areas: + if area.type == 'VIEW_3D': + for space in area.spaces: + if space.type == 'VIEW_3D': + if space.shading.type != 'RENDERED': + space.shading.type = 'RENDERED' + area.tag_redraw() + return + except: + pass + +def _deferred_set_rendered_mode(): + """Module-level timer callback to set rendered mode.""" + global _rendered_mode_pending, _rendered_mode_retries + try: + current_engine = bpy.context.scene.render.engine + + for window in bpy.context.window_manager.windows: + screen = window.screen + for area in screen.areas: + if area.type == 'VIEW_3D': + for space in area.spaces: + if space.type == 'VIEW_3D': + current_shading = space.shading.type + + if current_shading == 'RENDERED': + _rendered_mode_pending = False + _rendered_mode_retries = 0 + return None + + space.shading.type = 'RENDERED' + area.tag_redraw() + + _rendered_mode_pending = False + _rendered_mode_retries = 0 + return None + + _rendered_mode_retries += 1 + if _rendered_mode_retries < 10: + print(f"Viewport VIEW_3D not found, retrying... ({_rendered_mode_retries})") + return 0.2 + except Exception as e: + print(f"Could not set rendered mode: {e}") + import traceback + traceback.print_exc() + _rendered_mode_retries += 1 + if _rendered_mode_retries < 10: + return 0.2 + _rendered_mode_pending = False + _rendered_mode_retries = 0 + return None + + +class KromViewportEngine(bpy.types.RenderEngine): + """Leenkx render engine that displays Krom runtime output in viewport.""" + + bl_idname = 'KROM_VIEWPORT' + bl_label = 'Krom Viewport' + bl_use_preview = False + bl_use_gpu_context = True + bl_use_shading_nodes_custom = False + bl_use_eevee_viewport = True + + + def _ensure_initialized(self): + if not hasattr(self, '_viewport_init_done'): + self._shm = None + self._shm_file = None + self._texture = None + self._last_frame_id = 0 + self._width = 0 + self._height = 0 + self._initialized = False + self._shader = None + self._batch = None + self._viewport_init_done = True + self._rendered_mode_set = False + self._last_shading_mode = None + self._viewport_id = None + self._shmem_name = None + self._krom_proc = None + self._krom_launch_attempted = False + self._pending_camera_sync = False + self._engine_id = id(self) + # print(f"New engine instance created: {self._engine_id}") + + def _set_rendered_mode(self, context): + if self._rendered_mode_set: + return + try: + space = context.space_data + if space and hasattr(space, 'shading'): + current = space.shading.type + if current != 'RENDERED': + space.shading.type = 'RENDERED' + self._rendered_mode_set = True + except Exception as e: + print(f"Failed to set rendered mode: {e}") + + def _handle_mode_switch(self, context): + """Detect shading mode changes and restart Krom on Solid->Rendered switch if needed.""" + try: + space = context.space_data + if not space or not hasattr(space, 'shading'): + return + + current_mode = space.shading.type + + if self._last_shading_mode == 'SOLID' and current_mode == 'RENDERED': + self._initialized = False + self._rendered_mode_set = False + self._krom_launch_attempted = False + self._cleanup_shared_memory() + self._pending_camera_sync = True + + if self._last_shading_mode == 'RENDERED' and current_mode != 'RENDERED': + self._rendered_mode_set = False + # keep krom running in solid mode for switching back ti rendered + + self._last_shading_mode = current_mode + except Exception as e: + pass + + def _sync_camera_once(self, context): + """Sync the camera once for Solid->Rendered mode""" + if not self._initialized: + return + + try: + rv3d = context.region_data + if not rv3d: + return + + view_matrix = rv3d.view_matrix + proj_matrix = rv3d.window_matrix + + view_data = struct.pack('<16f', *[view_matrix[i][j] for j in range(4) for i in range(4)]) + + proj_data = struct.pack('<16f', *[proj_matrix[i][j] for j in range(4) for i in range(4)]) + + if sys.platform == 'win32': + if not self._shm_ptr: + return + ctypes.memmove(self._shm_ptr + OFFSET_VIEW_MATRIX, view_data, len(view_data)) + ctypes.memmove(self._shm_ptr + OFFSET_PROJ_MATRIX, proj_data, len(proj_data)) + else: + self._shm.seek(OFFSET_VIEW_MATRIX) + self._shm.write(view_data) + self._shm.seek(OFFSET_PROJ_MATRIX) + self._shm.write(proj_data) + + except Exception as e: + print(f"Failed camera sync: {e}") + + def _launch_krom_process(self, context): + """Launch Krom process for this viewport instance.""" + if self._krom_launch_attempted: + return self._krom_proc is not None + self._krom_launch_attempted = True + if self._viewport_id is None and context is not None: + try: + space = context.space_data + if space: + self._viewport_id = hex(space.as_pointer())[-6:] + except: + pass + + if not self._viewport_id: + import time + self._viewport_id = hex(int(time.time() * 1000) % 0xFFFFFF)[-6:] + + region = context.region + width = region.width // 2 if region else 960 + height = region.height if region else 540 + + viewport_id = self._viewport_id + engine_id = self._engine_id + shmem_name = _get_viewport_shmem_name(viewport_id) + # print(f"ID: {engine_id} launching: {viewport_id} with shared memory: {shmem_name}") + + def deferred_launch(): + try: + import lnx.make as make + make.play_viewport(viewport_id, width, height) + except Exception as e: + print(f"Failed to launch viewport: {e}") + import traceback + traceback.print_exc() + return None + + bpy.app.timers.register(deferred_launch, first_interval=0.1) + return True + + def _stop_krom_process(self): + if self._krom_proc: + try: + import lnx.make as make + make.stop_viewport_runtime(self._viewport_id) + except: + pass + self._krom_proc = None + + def _init_shared_memory(self, context=None): + self._ensure_initialized() + if self._initialized: + return True + + if self._viewport_id is None and context is not None: + try: + space = context.space_data + if space: + self._viewport_id = hex(space.as_pointer())[-6:] + except: + pass + + if self._viewport_id is None: + import time + self._viewport_id = hex(int(time.time() * 1000) % 0xFFFFFF)[-6:] + + shmem_name = _get_viewport_shmem_name(self._viewport_id) + self._shmem_name = shmem_name + + try: + if sys.platform == 'win32': + max_size = VIEWPORT_HEADER_SIZE + (4096 * 4096 * 4) + + try: + import ctypes.wintypes + kernel32 = ctypes.windll.kernel32 + + FILE_MAP_ALL_ACCESS = 0x000F001F + + kernel32.OpenFileMappingW.argtypes = [ctypes.wintypes.DWORD, ctypes.wintypes.BOOL, ctypes.wintypes.LPCWSTR] + kernel32.OpenFileMappingW.restype = ctypes.wintypes.HANDLE + + kernel32.MapViewOfFile.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] + kernel32.MapViewOfFile.restype = ctypes.c_void_p + + kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p] + kernel32.UnmapViewOfFile.restype = ctypes.wintypes.BOOL + + kernel32.CloseHandle.argtypes = [ctypes.wintypes.HANDLE] + kernel32.CloseHandle.restype = ctypes.wintypes.BOOL + + kernel32.VirtualQuery.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t] + kernel32.VirtualQuery.restype = ctypes.c_size_t + + handle = kernel32.OpenFileMappingW( + FILE_MAP_ALL_ACCESS, + False, + shmem_name + ) + + if not handle: + return False + + # print(f"ID: {self._engine_id} CONNECTED: {shmem_name}") + + # use 0 to map entire region + ptr = kernel32.MapViewOfFile( + handle, + FILE_MAP_ALL_ACCESS, + 0, 0, 0 + ) + + if not ptr: + error = kernel32.GetLastError() + kernel32.CloseHandle(handle) + print(f"Failed to map shared memory: {error}") + return False + + self._shm_handle = handle + self._shm_ptr = ptr + + # VirtualQuery to get actual size of mapped region + class MEMORY_BASIC_INFORMATION(ctypes.Structure): + _fields_ = [ + ("BaseAddress", ctypes.c_void_p), + ("AllocationBase", ctypes.c_void_p), + ("AllocationProtect", ctypes.c_ulong), + ("RegionSize", ctypes.c_size_t), + ("State", ctypes.c_ulong), + ("Protect", ctypes.c_ulong), + ("Type", ctypes.c_ulong), + ] + + mbi = MEMORY_BASIC_INFORMATION() + result = kernel32.VirtualQuery(ptr, ctypes.byref(mbi), ctypes.sizeof(mbi)) + + if result == 0 or mbi.RegionSize == 0: + actual_size = VIEWPORT_HEADER_SIZE + (1920 * 1080 * 4) + else: + actual_size = mbi.RegionSize + + self._shm_size = actual_size + + self._shm_buffer = (ctypes.c_ubyte * actual_size).from_address(ptr) + + self._initialized = True + # print(f"Connected: {shmem_name}") + return True + except Exception as e: + print(f"Failed to open shared memory: {e}") + return False + else: + posix_shmem_name = f"/{shmem_name}" + max_size = VIEWPORT_HEADER_SIZE + (4096 * 4096 * 4) + + if HAS_POSIX_IPC: + try: + shm = posix_ipc.SharedMemory(posix_shmem_name) + self._shm = mmap.mmap(shm.fd, max_size, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE) + shm.close_fd() + self._initialized = True + # print(f"Connected: {posix_shmem_name}") + return True + except Exception as e: + print(f"Failed to open shared memory: {e}") + return False + else: + # fallback: try /dev/shm directly + shm_path = f"/dev/shm{posix_shmem_name}" + try: + self._shm_file = open(shm_path, 'r+b') + self._shm = mmap.mmap(self._shm_file.fileno(), max_size, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE) + self._initialized = True + # print(f"Connected: {shm_path}") + return True + except Exception as e: + print(f"Failed to open /dev/shm: {e}") + return False + + except Exception as e: + print(f"Error creating shared memory: {e}") + return False + + def _cleanup_shared_memory(self): + """Close shared memory handles.""" + if sys.platform == 'win32': + if hasattr(self, '_shm_ptr') and self._shm_ptr: + try: + kernel32 = ctypes.windll.kernel32 + kernel32.UnmapViewOfFile(self._shm_ptr) + except: + pass + self._shm_ptr = None + if hasattr(self, '_shm_handle') and self._shm_handle: + try: + kernel32 = ctypes.windll.kernel32 + kernel32.CloseHandle(self._shm_handle) + except: + pass + self._shm_handle = None + else: + # POSIX cleanup + if hasattr(self, '_shm') and self._shm: + try: + self._shm.close() + except: + pass + self._shm = None + if hasattr(self, '_shm_file') and self._shm_file: + try: + self._shm_file.close() + except: + pass + self._shm_file = None + + self._initialized = False + + def _read_header(self): + """Read and parse the shared memory header.""" + self._ensure_initialized() + if not self._initialized: + return None + + try: + if sys.platform == 'win32': + if not self._shm_ptr: + return None + # use memmove for safety + header_buffer = (ctypes.c_ubyte * VIEWPORT_HEADER_SIZE)() + ctypes.memmove(header_buffer, self._shm_ptr, VIEWPORT_HEADER_SIZE) + header_data = bytes(header_buffer) + else: + self._shm.seek(0) + header_data = self._shm.read(VIEWPORT_HEADER_SIZE) + + build, version, width, height = struct.unpack(' 4096 or height > 4096: + return None + + try: + pixel_size = width * height * 4 + + if sys.platform == 'win32': + if not self._shm_ptr: + return None + pixel_buffer = (ctypes.c_ubyte * pixel_size)() + ctypes.memmove(pixel_buffer, self._shm_ptr + VIEWPORT_HEADER_SIZE, pixel_size) + pixels = bytes(pixel_buffer) + + ready_bytes = struct.pack(' 0 else 10.0 + + target = camera_pos + forward * view_dist + + rv3d.view_location = target + rv3d.view_rotation = quat + + except Exception as e: + pass + + def _request_resize(self, width, height): + """Request Krom to resize the viewport.""" + self._ensure_initialized() + if not self._initialized: + return + + if hasattr(self, '_last_requested_width') and hasattr(self, '_last_requested_height'): + if self._last_requested_width == width and self._last_requested_height == height: + return + + self._last_requested_width = width + self._last_requested_height = height + + try: + data = struct.pack(' 0 and region.height > 0: + self._request_resize(region.width, region.height) + + def view_draw(self, context, depsgraph): + """Called to draw the viewport.""" + self._ensure_initialized() + self._handle_mode_switch(context) + + was_initialized = self._initialized + if not self._init_shared_memory(context): + if not self._krom_launch_attempted: + # print(f"Shared memory not found, launching Krom for viewport {self._viewport_id}") + self._launch_krom_process(context) + self._draw_placeholder(context) + return + + if not was_initialized and self._initialized: + region = context.region + if region and region.width > 0 and region.height > 0: + self._last_requested_width = 0 + self._last_requested_height = 0 + # width from region is 2x the actual viewport width.. TODO: re-investigate + initial_width = region.width // 2 + self._request_resize(initial_width, region.height) + + if self._pending_camera_sync: + self._pending_camera_sync = False + self._sync_camera_once(context) + + self._set_rendered_mode(context) + + # TODO: input forwarding when viewport is active + global _active_krom_engine, _active_krom_engines, _active_viewport_id, _input_operator_running + _active_krom_engine = self # legacy single-engine reference + + if self._viewport_id: + _active_krom_engines[self._viewport_id] = self + _active_viewport_id = self._viewport_id + + self._set_input_enabled(True) + + if not _input_operator_running: + bpy.app.timers.register(_start_input_capture, first_interval=0.1) + else: + if not hasattr(self, '_input_check_counter'): + self._input_check_counter = 0 + self._input_check_counter += 1 + if self._input_check_counter > 60: + self._input_check_counter = 0 + bpy.app.timers.register(_start_input_capture, first_interval=0.5) + + region = context.region + if region and region.width > 0 and region.height > 0: + self._request_resize(region.width, region.height) + + self._read_krom_camera(context) + + pixels = self._read_framebuffer() + + if pixels: + self._update_texture(pixels, self._width, self._height) + + if not self._texture: + self._draw_placeholder(context) + return + + self._create_shader() + + if HAS_GPU_STATE: + gpu.state.blend_set('NONE') + else: + bgl.glDisable(bgl.GL_BLEND) + + region = context.region + + vertices = ( + (0, 0), + (region.width, 0), + (region.width, region.height), + (0, region.height), + ) + tex_coords = ( + (0, 1), # Flip Y + (1, 1), + (1, 0), + (0, 0), + ) + indices = ((0, 1, 2), (0, 2, 3)) + + self._shader.bind() + self._shader.uniform_sampler("image", self._texture) + + batch = batch_for_shader(self._shader, 'TRIS', {"pos": vertices, "texCoord": tex_coords}, indices=indices) + batch.draw(self._shader) + + if HAS_GPU_STATE: + gpu.state.blend_set('NONE') + else: + bgl.glDisable(bgl.GL_BLEND) + + self.tag_redraw() + + def _draw_placeholder(self, context): + """Draw a placeholder when Krom is not connected.""" + # Simple colored background + if HAS_GPU_STATE: + gpu.state.blend_set('ALPHA') + else: + bgl.glEnable(bgl.GL_BLEND) + bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA) + + shader = gpu.shader.from_builtin(SHADER_UNIFORM_COLOR) + shader.bind() + shader.uniform_float("color", (0.1, 0.1, 0.15, 1.0)) + + region = context.region + vertices = ( + (0, 0), + (region.width, 0), + (region.width, region.height), + (0, region.height), + ) + indices = ((0, 1, 2), (0, 2, 3)) + + batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) + batch.draw(shader) + + if HAS_GPU_STATE: + gpu.state.blend_set('NONE') + else: + bgl.glDisable(bgl.GL_BLEND) + + +class KROM_OT_viewport_input(bpy.types.Operator): + """Modal operator to capture and forward input to Krom viewport""" + bl_idname = "krom.viewport_input" + bl_label = "Krom Viewport Input" + bl_options = {'INTERNAL'} + + _engine = None + _last_mouse_x = 0 + _last_mouse_y = 0 + + def modal(self, context, event): + global _input_operator_running + + # check if we should stop + if event.type == 'ESC': + try: + if self._engine: + self._engine._set_input_enabled(False) + except ReferenceError: + pass + _input_operator_running = False + return {'CANCELLED'} + + if event.type in {'SCREEN_SET', 'SCREEN_CHANGED'}: + return {'PASS_THROUGH'} + + if not context.space_data or not context.area: + return {'PASS_THROUGH'} + + if context.space_data.type == 'VIEW_3D': + if context.engine != 'KROM_VIEWPORT': + try: + if self._engine: + self._engine._set_input_enabled(False) + except ReferenceError: + pass + _input_operator_running = False + return {'CANCELLED'} + + current_engine = self._get_engine_for_area(context, event) + if current_engine: + self._engine = current_engine + + try: + if not self._engine or not self._engine._initialized: + return {'PASS_THROUGH'} + except ReferenceError: + _input_operator_running = False + return {'CANCELLED'} + + region = context.region + if not region: + return {'PASS_THROUGH'} + + + if region.type != 'WINDOW': + return {'PASS_THROUGH'} + + mouse_in_region = (0 <= event.mouse_region_x <= region.width and + 0 <= event.mouse_region_y <= region.height) + + if not mouse_in_region: + return {'PASS_THROUGH'} + + area = context.area + if area: + mouse_x_abs = event.mouse_x + mouse_y_abs = event.mouse_y + for rgn in area.regions: + # skip the main window region where we actually want input + if rgn.type == 'WINDOW': + continue + # check if mouse is over toolbar, header, UI panel, etc. + if (rgn.x <= mouse_x_abs <= rgn.x + rgn.width and + rgn.y <= mouse_y_abs <= rgn.y + rgn.height): + # mouse is over a UI region - pass through + return {'PASS_THROUGH'} + + mouse_x = event.mouse_region_x + # flip Y for Krom + mouse_y = region.height - event.mouse_region_y + + try: + if event.type == 'MOUSEMOVE': + self._engine._send_input_event(INPUT_EVENT_MOUSE_MOVE, x=mouse_x, y=mouse_y) + self._last_mouse_x = mouse_x + self._last_mouse_y = mouse_y + return {'RUNNING_MODAL'} + + if event.type == 'LEFTMOUSE': + if event.value == 'PRESS': + self._engine._send_input_event(INPUT_EVENT_MOUSE_DOWN, button=MOUSE_BUTTON_LEFT, x=mouse_x, y=mouse_y) + elif event.value == 'RELEASE': + self._engine._send_input_event(INPUT_EVENT_MOUSE_UP, button=MOUSE_BUTTON_LEFT, x=mouse_x, y=mouse_y) + return {'RUNNING_MODAL'} + + if event.type == 'RIGHTMOUSE': + if event.value == 'PRESS': + self._engine._send_input_event(INPUT_EVENT_MOUSE_DOWN, button=MOUSE_BUTTON_RIGHT, x=mouse_x, y=mouse_y) + elif event.value == 'RELEASE': + self._engine._send_input_event(INPUT_EVENT_MOUSE_UP, button=MOUSE_BUTTON_RIGHT, x=mouse_x, y=mouse_y) + return {'RUNNING_MODAL'} + + if event.type == 'MIDDLEMOUSE': + if event.value == 'PRESS': + self._engine._send_input_event(INPUT_EVENT_MOUSE_DOWN, button=MOUSE_BUTTON_MIDDLE, x=mouse_x, y=mouse_y) + elif event.value == 'RELEASE': + self._engine._send_input_event(INPUT_EVENT_MOUSE_UP, button=MOUSE_BUTTON_MIDDLE, x=mouse_x, y=mouse_y) + return {'RUNNING_MODAL'} + + if event.type == 'WHEELUPMOUSE': + self._engine._send_input_event(INPUT_EVENT_MOUSE_WHEEL, delta=1) + return {'RUNNING_MODAL'} + if event.type == 'WHEELDOWNMOUSE': + self._engine._send_input_event(INPUT_EVENT_MOUSE_WHEEL, delta=-1) + return {'RUNNING_MODAL'} + + if event.value in {'PRESS', 'RELEASE'}: + key_code = self._blender_to_krom_key(event.type) + if key_code is not None: + if event.value == 'PRESS': + self._engine._send_input_event(INPUT_EVENT_KEY_DOWN, button=key_code) + else: + self._engine._send_input_event(INPUT_EVENT_KEY_UP, button=key_code) + return {'RUNNING_MODAL'} + except ReferenceError: + _input_operator_running = False + return {'CANCELLED'} + + return {'PASS_THROUGH'} + + def _blender_to_krom_key(self, blender_key): + """Convert Blender key type to Kinc key code.""" + # Kinc key codes from keyboard.h + key_map = { + 'A': 65, 'B': 66, 'C': 67, 'D': 68, 'E': 69, 'F': 70, 'G': 71, 'H': 72, + 'I': 73, 'J': 74, 'K': 75, 'L': 76, 'M': 77, 'N': 78, 'O': 79, 'P': 80, + 'Q': 81, 'R': 82, 'S': 83, 'T': 84, 'U': 85, 'V': 86, 'W': 87, 'X': 88, + 'Y': 89, 'Z': 90, + 'ZERO': 48, 'ONE': 49, 'TWO': 50, 'THREE': 51, 'FOUR': 52, + 'FIVE': 53, 'SIX': 54, 'SEVEN': 55, 'EIGHT': 56, 'NINE': 57, + 'SPACE': 32, 'BACK_SPACE': 8, 'TAB': 9, 'RET': 13, + 'LEFT_SHIFT': 16, 'RIGHT_SHIFT': 16, 'LEFT_CTRL': 17, 'RIGHT_CTRL': 17, + 'LEFT_ALT': 18, 'RIGHT_ALT': 18, + 'UP_ARROW': 38, 'DOWN_ARROW': 40, 'LEFT_ARROW': 37, 'RIGHT_ARROW': 39, + 'ESC': 27, 'DEL': 46, 'HOME': 36, 'END': 35, + 'PAGE_UP': 33, 'PAGE_DOWN': 34, 'INSERT': 45, + 'F1': 112, 'F2': 113, 'F3': 114, 'F4': 115, 'F5': 116, 'F6': 117, + 'F7': 118, 'F8': 119, 'F9': 120, 'F10': 121, 'F11': 122, 'F12': 123, + } + return key_map.get(blender_key) + + def _get_engine_for_area(self, context, event=None): + """Get the correct engine for the viewport under the mouse cursor.""" + # Use mouse coordinates to find which viewport the mouse is over + if event: + mouse_x = event.mouse_x + mouse_y = event.mouse_y + + for window in bpy.context.window_manager.windows: + for area in window.screen.areas: + if area.type != 'VIEW_3D': + continue + + if (area.x <= mouse_x < area.x + area.width and + area.y <= mouse_y < area.y + area.height): + + for space in area.spaces: + if space.type == 'VIEW_3D': + try: + viewport_id = hex(space.as_pointer())[-6:] + if viewport_id in _active_krom_engines: + return _active_krom_engines[viewport_id] + except: + pass + break + + if context.space_data: + try: + viewport_id = hex(context.space_data.as_pointer())[-6:] + if viewport_id in _active_krom_engines: + return _active_krom_engines[viewport_id] + except: + pass + + if _active_viewport_id and _active_viewport_id in _active_krom_engines: + return _active_krom_engines[_active_viewport_id] + + return _active_krom_engine + + def invoke(self, context, event): + self._engine = self._get_engine_for_area(context) + if not self._engine: + return {'CANCELLED'} + + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + +_active_krom_engines = {} +_active_viewport_id = None +_input_operator_running = False + +_active_krom_engine = None + + +def _start_input_capture(): + """Timer callback to start input capture operator.""" + global _input_operator_running + if _active_krom_engine and not _input_operator_running: + try: + for window in bpy.context.window_manager.windows: + for area in window.screen.areas: + if area.type == 'VIEW_3D': + for region in area.regions: + if region.type == 'WINDOW': + override = {'window': window, 'area': area, 'region': region} + with bpy.context.temp_override(**override): + bpy.ops.krom.viewport_input('INVOKE_DEFAULT') + _input_operator_running = True + return None + except Exception as e: + pass + return None + + +def get_panels(): + """Get panels to ensure world settings and other properties are available.""" + exclude_panels = { + 'VIEWLAYER_PT_filter', + 'VIEWLAYER_PT_layer_passes', + } + + compatible_engines = {'BLENDER_RENDER', 'BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'} + + panels = [] + for panel in bpy.types.Panel.__subclasses__(): + if hasattr(panel, 'COMPAT_ENGINES'): + if panel.COMPAT_ENGINES & compatible_engines: + if panel.__name__ not in exclude_panels: + panels.append(panel) + + return panels + + +def _cleanup_krom_on_exit(): + """Kill any running Krom viewport processes when Blender exits.""" + try: + import lnx.make as make + if hasattr(make, '_viewport_processes'): + for viewport_id, proc in list(make._viewport_processes.items()): + if proc and proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=2) + except: + proc.kill() + make._viewport_processes.clear() + if hasattr(make, '_viewport_proc') and make._viewport_proc is not None: + if make._viewport_proc.poll() is None: + make._viewport_proc.terminate() + try: + make._viewport_proc.wait(timeout=2) + except: + make._viewport_proc.kill() + except: + pass + +class KROM_OT_stop_viewport(bpy.types.Operator): + """Stop the Krom viewport process and switch to solid shading""" + bl_idname = "krom.stop_viewport" + bl_label = "Stop Viewport" + bl_description = "Stop Krom viewport rendering and switch to solid mode" + bl_options = {'REGISTER'} + + viewport_id: bpy.props.StringProperty(default="") + + def execute(self, context): + import lnx.make as make + + viewport_id = self.viewport_id + if not viewport_id and context.space_data: + viewport_id = hex(context.space_data.as_pointer())[-6:] + if viewport_id: + make.stop_viewport(viewport_id) + global _active_krom_engines + if viewport_id in _active_krom_engines: + del _active_krom_engines[viewport_id] + if context.space_data and hasattr(context.space_data, 'shading'): + context.space_data.shading.type = 'SOLID' + + return {'FINISHED'} + + +def draw_viewport_stop_button(self, context): + """Draw X button in viewport header when Krom is running.""" + if context.engine != 'KROM_VIEWPORT': + return + + space = context.space_data + if not space or not hasattr(space, 'shading'): + return + + if space.shading.type != 'RENDERED': + return + + viewport_id = hex(space.as_pointer())[-6:] + + global _active_krom_engines + if viewport_id not in _active_krom_engines: + layout = self.layout + layout.label(text="Building...", icon='TIME') + return + + layout = self.layout + op = layout.operator("krom.stop_viewport", text="", icon='X', emboss=False) + op.viewport_id = viewport_id + + +def register(): + """Register the render engine.""" + bpy.utils.register_class(KromViewportEngine) + bpy.utils.register_class(KROM_OT_viewport_input) + bpy.utils.register_class(KROM_OT_stop_viewport) + + bpy.types.VIEW3D_HT_header.append(draw_viewport_stop_button) + + for panel in get_panels(): + panel.COMPAT_ENGINES.add('KROM_VIEWPORT') + + if _on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update) + atexit.register(_cleanup_krom_on_exit) + + +def unregister(): + """Unregister the render engine.""" + global _active_krom_engine, _active_krom_engines, _active_viewport_id, _input_operator_running, _msgbus_owner + _active_krom_engine = None + _active_krom_engines.clear() + _active_viewport_id = None + _input_operator_running = False + + if _on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update) + + _cleanup_krom_on_exit() + + try: + atexit.unregister(_cleanup_krom_on_exit) + except: + pass + + for panel in get_panels(): + if 'KROM_VIEWPORT' in panel.COMPAT_ENGINES: + panel.COMPAT_ENGINES.remove('KROM_VIEWPORT') + + bpy.types.VIEW3D_HT_header.remove(draw_viewport_stop_button) + + bpy.utils.unregister_class(KROM_OT_stop_viewport) + bpy.utils.unregister_class(KROM_OT_viewport_input) + bpy.utils.unregister_class(KromViewportEngine) diff --git a/leenkx/blender/lnx/write_data.py b/leenkx/blender/lnx/write_data.py index 438fd22..a152b35 100644 --- a/leenkx/blender/lnx/write_data.py +++ b/leenkx/blender/lnx/write_data.py @@ -154,6 +154,17 @@ project.addSources('Sources'); assets.add_khafile_def('lnx_oimo') if not os.path.exists('Libraries/oimo'): khafile.write(add_leenkx_library(sdk_path + '/lib/', 'oimo', rel_path=do_relpath_sdk)) + elif wrd.lnx_physics_engine == 'Jolt': + assets.add_khafile_def('lnx_jolt') + if not os.path.exists('Libraries/haxejolt'): + khafile.write(add_leenkx_library(sdk_path + '/lib/', 'haxejolt', rel_path=do_relpath_sdk)) + if state.target.startswith('krom') or state.target == 'html5' or state.target == 'node': + joltjs_path = sdk_path + '/lib/haxejolt/jolt/jolt.wasm.js' + joltjs_path = joltjs_path.replace('\\', '/').replace('//', '/') + khafile.write(add_assets(joltjs_path, rel_path=do_relpath_sdk)) + joltjs_wasm_path = sdk_path + '/lib/haxejolt/jolt/jolt.wasm.wasm' + joltjs_wasm_path = joltjs_wasm_path.replace('\\', '/').replace('//', '/') + khafile.write(add_assets(joltjs_wasm_path, rel_path=do_relpath_sdk)) if export_navigation: assets.add_khafile_def('lnx_navigation') @@ -209,6 +220,8 @@ project.addSources('Sources'); if wrd.lnx_render_viewport: assets.add_khafile_def('lnx_render_viewport') + if state.is_viewport: + assets.add_khafile_def('lnx_viewport') import_traits = list(set(import_traits)) for i in range(0, len(import_traits)): khafile.write("project.addParameter('" + import_traits[i] + "');\n") @@ -447,9 +460,12 @@ def write_config(resx, resy): 'window_msaa': int(rpdat.lnx_samples_per_pixel), 'window_scale': 1.0, 'rp_supersample': float(rpdat.rp_supersampling), + 'rp_fsr1': rpdat.rp_fsr1 if rpdat.rp_fsr1 != 'Off' else False, + 'rp_fsr1_sharpness': rpdat.rp_fsr1_sharpness if rpdat.rp_fsr1 == 'Custom' else 0.25, 'rp_shadowmap_cube': rp_shadowmap_cube, 'rp_shadowmap_cascade': rp_shadowmap_cascade, - 'rp_ssgi': rpdat.rp_ssgi != 'Off', + 'rp_ssao': rpdat.rp_ssao, + 'rp_ssgi': rpdat.rp_ssgi, 'rp_ssr': rpdat.rp_ssr != 'Off', 'rp_ss_refraction': rpdat.rp_ss_refraction != 'Off', 'rp_bloom': rpdat.rp_bloom != 'Off', @@ -672,18 +688,20 @@ const float waterReflect = """ + str(round(rpdat.lnx_water_reflect * 100) / 100) f'const float ditherStrengthValue = {rpdat.lnx_dithering_strength};\n' ) - if rpdat.rp_ssgi == 'SSAO' or rpdat.rp_ssgi == 'SSGI' or rpdat.rp_ssgi == 'RTAO' or rpdat.rp_volumetriclight: + if rpdat.rp_ssao or rpdat.rp_volumetriclight: f.write( -"""const float ssaoRadius = """ + str(round(rpdat.lnx_ssgi_radius * 100) / 100) + """; -const float ssaoStrength = """ + str(round(rpdat.lnx_ssgi_strength * 100) / 100) + """; -const float ssaoScale = """ + ("2.0" if rpdat.lnx_ssgi_half_res else "20.0") + """; +"""const float ssaoRadius = """ + str(round(rpdat.lnx_ssao_radius * 100) / 100) + """; +const float ssaoStrength = """ + str(round(rpdat.lnx_ssao_strength * 100) / 100) + """; +const float ssaoScale = """ + ("2.0" if rpdat.lnx_ssao_half_res else "20.0") + """; +const int ssaoSamples = """ + str(rpdat.lnx_ssao_samples) + """; """) - if rpdat.rp_ssgi == 'RTGI' or rpdat.rp_ssgi == 'RTAO' or rpdat.rp_ssgi == 'SSGI' : + if rpdat.rp_ssgi: f.write( """const int ssgiSamples = """ + str(rpdat.lnx_ssgi_samples) + """; const float ssgiRayStep = 0.005 * """ + str(round(rpdat.lnx_ssgi_step * 100) / 100) + """; const float ssgiStrength = """ + str(round(rpdat.lnx_ssgi_strength * 100) / 100) + """; +const float ssgiRadius = """ + str(round(rpdat.lnx_ssgi_radius * 100) / 100) + """; """) if rpdat.rp_bloom: @@ -836,7 +854,7 @@ const float voxelgiDiff = """ + str(round(rpdat.lnx_voxelgi_diff * 100) / 100) + const float voxelgiRefl = """ + str(round(rpdat.lnx_voxelgi_spec * 100) / 100) + """; const float voxelgiRefr = """ + str(round(rpdat.lnx_voxelgi_refr * 100) / 100) + """; """) - if rpdat.rp_sss: + if rpdat.rp_sss or '_SSS' in wrd.world_defs: f.write(f"const float sssWidth = {rpdat.lnx_sss_width / 10.0};\n") # Skinning diff --git a/leenkx/blender/start.py b/leenkx/blender/start.py index 5f45bcb..ac3a937 100644 --- a/leenkx/blender/start.py +++ b/leenkx/blender/start.py @@ -20,6 +20,7 @@ import lnx.props_camera_render_filter import lnx.handlers import lnx.utils import lnx.keymap +import lnx.render_engine reload_started = 0 @@ -49,6 +50,7 @@ if lnx.is_reload(__name__): lnx.handlers = lnx.reload_module(lnx.handlers) lnx.utils = lnx.reload_module(lnx.utils) lnx.keymap = lnx.reload_module(lnx.keymap) + lnx.render_engine = lnx.reload_module(lnx.render_engine) else: lnx.enable_reload(__name__) @@ -76,6 +78,7 @@ def register(local_sdk=False): lnx.keymap.register() lnx.handlers.register() lnx.props_collision_filter_mask.register() + lnx.render_engine.register() lnx.handlers.post_register() @@ -104,3 +107,4 @@ def unregister(): lnx.props_properties.unregister() lnx.props_collision_filter_mask.unregister() lnx.props_camera_render_filter.unregister() + lnx.render_engine.unregister()