import glob import importlib.util import io import json import os import shutil import stat import html from typing import List import bpy import lnx.assets as assets import lnx.log as log import lnx.make_renderpath as make_renderpath import lnx.make_state as state import lnx.utils if lnx.is_reload(__name__): import lnx assets = lnx.reload_module(assets) log = lnx.reload_module(log) make_renderpath = lnx.reload_module(make_renderpath) state = lnx.reload_module(state) lnx.utils = lnx.reload_module(lnx.utils) else: lnx.enable_reload(__name__) def get_library_hooks(): """Discover and load `lnx_hooks.py` from Libraries and Subprojects. Each hook module can define a write_main() function that returns a dict with: - 'imports': Haxe import statements to add at the top - 'main_pre': Code to add at the start of main() before Starter.main() - 'main_post': Code to add at the end of main() after Starter.main() - 'wrap_init': Tuple of (before, after) strings to wrap around the main() body - 'priority': Integer for wrap_init nesting order (lower = outer, default = 0) - 'defines': List of Haxe defines to add to khafile.js - 'parameters': List of Haxe parameters to add to khafile.js - 'assets': List of asset entries for khafile.js. Each entry can be: - A string path: "Assets/icons.png" - A tuple (path, options_dict): ("Assets/icons.png", {"noCompress": True}) Multiple wrap_init hooks are nested based on priority: - Priority 0 (outer): LibA.init(function() { - Priority 10 (inner): LibB.init(function() { - Starter.main(); - Priority 10 (inner): }); - Priority 0 (outer): }); Returns a dict with combined strings for each injection point. """ hooks = { 'imports': '', # Haxe import statements 'main_pre': '', # Code to add at the start of main() before Starter.main() 'main_post': '', # Code to add at the end of main() after Starter.main() 'wrap_inits': [], # List of (priority, before, after) tuples 'defines': [], # List of Haxe defines for khafile.js 'parameters': [], # List of Haxe parameters for khafile.js 'assets': [] # List of assets for khafile.js } project_path = lnx.utils.get_fp() hook_dirs = [] # Check Libraries folder libraries_path = os.path.join(project_path, 'Libraries') if os.path.exists(libraries_path): for lib in os.listdir(libraries_path): lib_path = os.path.join(libraries_path, lib) if os.path.isdir(lib_path): hook_dirs.append((lib, lib_path)) # Check Subprojects folder subprojects_path = os.path.join(project_path, 'Subprojects') if os.path.exists(subprojects_path): for lib in os.listdir(subprojects_path): lib_path = os.path.join(subprojects_path, lib) if os.path.isdir(lib_path): hook_dirs.append((lib, lib_path)) # Load each hook module for lib_name, lib_path in hook_dirs: hook_file = os.path.join(lib_path, 'lnx_hooks.py') if os.path.isfile(hook_file): try: spec = importlib.util.spec_from_file_location(f"lnx_hooks_{lib_name}", hook_file) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) if hasattr(module, 'write_main'): result = module.write_main() if result: if 'imports' in result and result['imports']: hooks['imports'] += result['imports'] + '\n' if 'main_pre' in result and result['main_pre']: hooks['main_pre'] += result['main_pre'] + '\n' if 'main_post' in result and result['main_post']: hooks['main_post'] += result['main_post'] + '\n' if 'wrap_init' in result and result['wrap_init']: priority = result.get('priority', 0) hooks['wrap_inits'].append((priority, result['wrap_init'][0], result['wrap_init'][1])) if 'defines' in result and result['defines']: hooks['defines'].extend(result['defines']) if 'parameters' in result and result['parameters']: hooks['parameters'].extend(result['parameters']) if 'assets' in result and result['assets']: hooks['assets'].extend(result['assets']) log.info(f"Loaded library hook: {lib_name}") except Exception as e: log.warn(f"Failed to load hook from {lib_name}: {e}") # Sort wrap_inits by priority (lower = outer wrapper) hooks['wrap_inits'].sort(key=lambda x: x[0]) return hooks def on_same_drive(path1: str, path2: str) -> bool: drive_path1, _ = os.path.splitdrive(path1) drive_path2, _ = os.path.splitdrive(path2) return drive_path1 == drive_path2 def add_leenkx_library(sdk_path: str, name: str, rel_path=False) -> str: if rel_path: sdk_path = '../' + os.path.relpath(sdk_path, lnx.utils.get_fp()).replace('\\', '/') return ('project.addLibrary("' + sdk_path + '/' + name + '");\n').replace('\\', '/').replace('//', '/') def add_assets(path: str, quality=1.0, use_data_dir=False, rel_path=False) -> str: if not bpy.data.worlds['Lnx'].lnx_minimize and path.endswith('.lnx'): path = path[:-4] + '.json' if rel_path: path = os.path.relpath(path, lnx.utils.get_fp()).replace('\\', '/') notinlist = not path.endswith('.ttf') # TODO s = 'project.addAssets("' + path + '", { notinlist: ' + str(notinlist).lower() + ' ' if quality < 1.0: s += ', quality: ' + str(quality) if use_data_dir: s += ', destination: "data/{name}"' s += '});\n' return s def add_shaders(path: str, rel_path=False) -> str: if rel_path: path = os.path.relpath(path, lnx.utils.get_fp()) return 'project.addShaders("' + path.replace('\\', '/').replace('//', '/') + '");\n' def remove_readonly(func, path, excinfo): os.chmod(path, stat.S_IWRITE) func(path) def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, export_ui: bool, export_network: bool, is_publish: bool, import_traits: List[str]) -> None: wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() # Get library hooks for defines and parameters hooks = get_library_hooks() sdk_path = lnx.utils.get_sdk_path() rel_path = lnx.utils.get_relative_paths() # Convert absolute paths to relative project_path = lnx.utils.get_fp() build_dir = lnx.utils.build_dir() # Whether to use relative paths for paths inside the SDK do_relpath_sdk = rel_path and on_same_drive(sdk_path, project_path) # Determine if assets should go to data folder (used for hook assets and later) use_data_dir = is_publish and (state.target == 'krom-windows' or state.target == 'krom-linux' or state.target == 'windows-hl' or state.target == 'linux-hl' or state.target == 'html5') with open('khafile.js', 'w', encoding="utf-8") as khafile: khafile.write( """// Auto-generated let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' + wrd.lnx_project_version) + """'); """) # Add library hook assets for asset in hooks['assets']: dest_opt = ', destination: "data/{name}"' if use_data_dir else '' if isinstance(asset, tuple): path, options = asset opts = [] for key, value in options.items(): if isinstance(value, bool): opts.append(f"{key}: {'true' if value else 'false'}") elif isinstance(value, str): opts.append(f"{key}: '{value}'") else: opts.append(f"{key}: {value}") if use_data_dir and 'destination' not in options: opts.append('destination: "data/{name}"') opts_str = ', '.join(opts) khafile.write(f'project.addAssets("{path}", {{ {opts_str} }});\n') else: khafile.write(f'project.addAssets("{asset}", {{ notinlist: true{dest_opt} }});\n') # Add library hook defines for d in hooks['defines']: khafile.write("project.addDefine('" + d + "');\n") for p in assets.khafile_params: khafile.write("project.addParameter('" + p + "');\n") # Add library hook parameters for p in hooks['parameters']: khafile.write("project.addParameter('" + p + "');\n") khafile.write("project.addSources('Sources');\n") # Auto-add assets located in Bundled directory if os.path.exists('Bundled'): for file in glob.glob("Bundled/**", recursive=True): if os.path.isfile(file): assets.add(file) # Auto-add shape key textures if exists if os.path.exists('MorphTargets'): for file in glob.glob("MorphTargets/**", recursive=True): if os.path.isfile(file): assets.add(file) # Add project shaders if os.path.exists('Shaders'): # Copy to enable includes shader_path = os.path.join(build_dir, 'compiled', 'Shaders', 'Project') if os.path.exists(shader_path): shutil.rmtree(shader_path, onerror=remove_readonly) shutil.copytree('Shaders', shader_path) khafile.write("project.addShaders('" + build_dir + "/compiled/Shaders/Project/**');\n") # for file in glob.glob("Shaders/**", recursive=True): # if os.path.isfile(file): # assets.add_shader(file) # Add engine sources if the project does not use its own leenkx/iron versions if not os.path.exists(os.path.join('Libraries', 'leenkx')): khafile.write(add_leenkx_library(sdk_path, 'leenkx', rel_path=do_relpath_sdk)) if not os.path.exists(os.path.join('Libraries', 'iron')): khafile.write(add_leenkx_library(sdk_path, 'iron', rel_path=do_relpath_sdk)) # Project libraries if os.path.exists('Libraries'): libs = os.listdir('Libraries') for lib in libs: if os.path.isdir('Libraries/' + lib): khafile.write('project.addLibrary("{0}");\n'.format(lib.replace('//', '/'))) # Subprojects, merge this with libraries if os.path.exists('Subprojects'): libs = os.listdir('Subprojects') for lib in libs: if os.path.isdir('Subprojects/' + lib): khafile.write('await project.addProject("Subprojects/{0}");\n'.format(lib)) if state.target.startswith('krom'): assets.add_khafile_def('js-es=6') if export_physics: assets.add_khafile_def('lnx_physics') if wrd.lnx_physics_engine == 'Bullet': assets.add_khafile_def('lnx_bullet') if not os.path.exists('Libraries/haxebullet'): khafile.write(add_leenkx_library(sdk_path + '/lib/', 'haxebullet', rel_path=do_relpath_sdk)) if state.target.startswith('krom'): ammojs_path = sdk_path + '/lib/haxebullet/ammo/ammo.wasm.js' ammojs_path = ammojs_path.replace('\\', '/').replace('//', '/') khafile.write(add_assets(ammojs_path, rel_path=do_relpath_sdk)) ammojs_wasm_path = sdk_path + '/lib/haxebullet/ammo/ammo.wasm.wasm' ammojs_wasm_path = ammojs_wasm_path.replace('\\', '/').replace('//', '/') khafile.write(add_assets(ammojs_wasm_path, rel_path=do_relpath_sdk)) elif state.target == 'html5' or state.target == 'node': ammojs_path = sdk_path + '/lib/haxebullet/ammo/ammo.js' ammojs_path = ammojs_path.replace('\\', '/').replace('//', '/') khafile.write(add_assets(ammojs_path, rel_path=do_relpath_sdk)) elif wrd.lnx_physics_engine == 'Oimo': 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') if not os.path.exists('Libraries/haxerecast'): khafile.write(add_leenkx_library(sdk_path + '/lib/', 'haxerecast', rel_path=do_relpath_sdk)) if state.target.startswith('krom'): recastjs_path = sdk_path + '/lib/haxerecast/recastjs/recast.wasm.js' recastjs_path = recastjs_path.replace('\\', '/').replace('//', '/') khafile.write(add_assets(recastjs_path, rel_path=do_relpath_sdk)) recastjs_wasm_path = sdk_path + '/lib/haxerecast/recastjs/recast.wasm.wasm' recastjs_wasm_path = recastjs_wasm_path.replace('\\', '/').replace('//', '/') khafile.write(add_assets(recastjs_wasm_path, rel_path=do_relpath_sdk)) elif state.target == 'html5' or state.target == 'node': recastjs_path = sdk_path + '/lib/haxerecast/recastjs/recast.js' recastjs_path = recastjs_path.replace('\\', '/').replace('//', '/') khafile.write(add_assets(recastjs_path, rel_path=do_relpath_sdk)) if is_publish: assets.add_khafile_def('lnx_published') if wrd.lnx_dce: khafile.write("project.addParameter('-dce full');\n") if wrd.lnx_no_traces: khafile.write("project.addParameter('--no-traces');\n") if wrd.lnx_asset_compression: assets.add_khafile_def('lnx_compress') else: assets.add_khafile_def(f'lnx_assert_level={wrd.lnx_assert_level}') if wrd.lnx_assert_quit: assets.add_khafile_def('lnx_assert_quit') # khafile.write("""project.addParameter("--macro include('leenkx.trait')");\n""") # khafile.write("""project.addParameter("--macro include('leenkx.trait.internal')");\n""") # if export_physics: # khafile.write("""project.addParameter("--macro include('leenkx.trait.physics')");\n""") # if wrd.lnx_physics_engine == 'Bullet': # khafile.write("""project.addParameter("--macro include('leenkx.trait.physics.bullet')");\n""") # else: # khafile.write("""project.addParameter("--macro include('leenkx.trait.physics.oimo')");\n""") # if export_navigation: # khafile.write("""project.addParameter("--macro include('leenkx.trait.navigation')");\n""") if not wrd.lnx_compiler_inline: khafile.write("project.addParameter('--no-inline');\n") use_live_patch = lnx.utils.is_livepatch_enabled() if wrd.lnx_debug_console or use_live_patch: import_traits.append('leenkx.trait.internal.Bridge') if use_live_patch: assets.add_khafile_def('lnx_patch') # Include all logic node classes so that they can later # get instantiated khafile.write("""project.addParameter("--macro include('leenkx.logicnode')");\n""") 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") khafile.write("""project.addParameter("--macro keep('""" + import_traits[i] + """')");\n""") noembed = wrd.lnx_cache_build and not is_publish and state.target == 'krom' if noembed: # Load shaders manually assets.add_khafile_def('lnx_noembed') noembed = False # TODO: always embed shaders for now, check compatibility with haxe compile server shaders_path = build_dir + '/compiled/Shaders/*.glsl' if rel_path: shaders_path = os.path.relpath(shaders_path, project_path).replace('\\', '/') khafile.write('project.addShaders("' + shaders_path + '", { noembed: ' + str(noembed).lower() + '});\n') if lnx.utils.get_gapi() == 'direct3d11': # noprocessing flag - gets renamed to .d3d11 shaders_path = build_dir + '/compiled/Hlsl/*.glsl' if rel_path: shaders_path = os.path.relpath(shaders_path, project_path).replace('\\', '/') khafile.write('project.addShaders("' + shaders_path + '", { noprocessing: true, noembed: ' + str(noembed).lower() + ' });\n') # Add define for data directory usage if use_data_dir: assets.add_khafile_def('lnx_data_dir') ext = 'lnx' if wrd.lnx_minimize else 'json' assets_path = build_dir + '/compiled/Assets/**' assets_path_sh = build_dir + '/compiled/Shaders/*.' + ext if rel_path: assets_path = os.path.relpath(assets_path, project_path).replace('\\', '/') assets_path_sh = os.path.relpath(assets_path_sh, project_path).replace('\\', '/') dest = '' if use_data_dir: dest += ', destination: "data/{name}"' khafile.write('project.addAssets("' + assets_path + '", { notinlist: true ' + dest + '});\n') khafile.write('project.addAssets("' + assets_path_sh + '", { notinlist: true ' + dest + '});\n') shader_data_references = sorted(list(set(assets.shader_datas))) for ref in shader_data_references: ref = ref.replace('\\', '/').replace('//', '/') if '/compiled/' in ref: # Asset already included continue do_relpath_shaders = rel_path and on_same_drive(ref, project_path) khafile.write(add_assets(ref, use_data_dir=use_data_dir, rel_path=do_relpath_shaders)) asset_references = sorted(list(set(assets.assets))) for ref in asset_references: ref = ref.replace('\\', '/').replace('//', '/') if '/compiled/' in ref: # Asset already included continue quality = 1.0 s = ref.lower() if s.endswith('.wav'): quality = wrd.lnx_sound_quality elif s.endswith('.png') or s.endswith('.jpg'): quality = wrd.lnx_texture_quality do_relpath_assets = rel_path and on_same_drive(ref, project_path) khafile.write(add_assets(ref, quality=quality, use_data_dir=use_data_dir, rel_path=do_relpath_assets)) if wrd.lnx_sound_quality < 1.0 or state.target == 'html5': assets.add_khafile_def('lnx_soundcompress') if wrd.lnx_audio == 'Disabled': assets.add_khafile_def('kha_no_ogg') else: assets.add_khafile_def('lnx_audio') lnxAudio_path = sdk_path + '/lib/aura' lnxAudio_path = lnxAudio_path.replace('\\', '/').replace('//', '/') khafile.write("await project.addProject('" + lnxAudio_path + "');\n".format("aura")) if wrd.lnx_texture_quality < 1.0: assets.add_khafile_def('lnx_texcompress') if wrd.lnx_debug_console: assets.add_khafile_def('lnx_debug') if rpdat.rp_renderer == 'Forward': # deferred line frag shader is currently handled in make.py, # only add forward shader here khafile.write(add_shaders(sdk_path + "/leenkx/Shaders/debug_draw/line.frag.glsl", rel_path=do_relpath_sdk)) khafile.write(add_shaders(sdk_path + "/leenkx/Shaders/debug_draw/line.vert.glsl", rel_path=do_relpath_sdk)) if not is_publish and state.target == 'html5': khafile.write("project.addParameter('--debug');\n") if lnx.utils.get_pref_or_default('haxe_times', False): khafile.write("project.addParameter('--times');\n") if export_ui or wrd.lnx_debug_console: if not os.path.exists('Libraries/zui'): khafile.write(add_leenkx_library(sdk_path, 'lib/zui', rel_path=do_relpath_sdk)) p = sdk_path + '/leenkx/Assets/font_default.ttf' p = p.replace('//', '/') khafile.write(add_assets(p.replace('\\', '/'), use_data_dir=use_data_dir, rel_path=do_relpath_sdk)) assets.add_khafile_def('lnx_ui') if export_network: if not os.path.exists('Libraries/network'): khafile.write(add_leenkx_library(sdk_path, 'lib/network', rel_path=do_relpath_sdk)) assets.add_khafile_def('lnx_network') lnxjs_path = sdk_path + '/lib/leenkx_tools/lnxjs/Leenkx.js' lnxjs_path = lnxjs_path.replace('\\', '/').replace('//', '/') khafile.write(add_assets(lnxjs_path, rel_path=do_relpath_sdk)) if not wrd.lnx_minimize: assets.add_khafile_def('lnx_json') if wrd.lnx_deinterleaved_buffers: assets.add_khafile_def('lnx_deinterleaved') if wrd.lnx_batch_meshes: assets.add_khafile_def('lnx_batch') if wrd.lnx_stream_scene: assets.add_khafile_def('lnx_stream') rpdat = lnx.utils.get_rp() if rpdat.lnx_skin != 'Off': assets.add_khafile_def('lnx_skin') if rpdat.lnx_morph_target != 'Off': assets.add_khafile_def('lnx_morph_target') if rpdat.lnx_particles != 'Off': assets.add_khafile_def('lnx_particles') if rpdat.lnx_particles == 'GPU': assets.add_khafile_def('lnx_gpu_particles') elif rpdat.lnx_particles == 'CPU': assets.add_khafile_def('lnx_cpu_particles') if rpdat.rp_draw_order == 'Index': assets.add_khafile_def('lnx_draworder_index') if lnx.utils.get_viewport_controls() == 'azerty': assets.add_khafile_def('lnx_azerty') if os.path.exists(project_path + '/Bundled/config.lnx'): assets.add_khafile_def('lnx_config') if is_publish and wrd.lnx_loadscreen: assets.add_khafile_def('lnx_loadscreen') if wrd.lnx_winresize or state.target == 'html5': assets.add_khafile_def('lnx_resizable') # if bpy.data.scenes[0].unit_settings.system_rotation == 'DEGREES': # assets.add_khafile_def('lnx_degrees') # Allow libraries to recognize Leenkx assets.add_khafile_def('leenkx') for d in assets.khafile_defs: khafile.write("project.addDefine('" + d + "');\n") if state.target.startswith('android'): bundle = 'org.leenkx3d.' + wrd.lnx_project_package if wrd.lnx_project_bundle == '' else wrd.lnx_project_bundle khafile.write("project.targetOptions.android_native.package = '{0}';\n".format(lnx.utils.safestr(bundle))) if wrd.lnx_winorient != 'Multi': khafile.write("project.targetOptions.android_native.screenOrientation = '{0}';\n".format(wrd.lnx_winorient.lower())) # Android SDK Versions khafile.write("project.targetOptions.android_native.compileSdkVersion = '{0}';\n".format(wrd.lnx_project_android_sdk_compile)) khafile.write("project.targetOptions.android_native.minSdkVersion = '{0}';\n".format(wrd.lnx_project_android_sdk_min)) khafile.write("project.targetOptions.android_native.targetSdkVersion = '{0}';\n".format(wrd.lnx_project_android_sdk_target)) # Permissions if len(wrd.lnx_exporter_android_permission_list) > 0: perms = '' for item in wrd.lnx_exporter_android_permission_list: perm = "'android.permission."+ item.lnx_android_permissions + "'" # Checking In if perms.find(perm) == -1: if len(perms) > 0: perms = perms + ', ' + perm else: perms = perm if len(perms) > 0: khafile.write("project.targetOptions.android_native.permissions = [{0}];\n".format(perms)) # Android ABI Filters if len(wrd.lnx_exporter_android_abi_list) > 0: abis = '' for item in wrd.lnx_exporter_android_abi_list: abi = "'"+ item.lnx_android_abi + "'" # Checking In if abis.find(abi) == -1: if len(abis) > 0: abis = abis + ', ' + abi else: abis = abi if len(abis) > 0: khafile.write("project.targetOptions.android_native.abiFilters = [{0}];\n".format(abis)) elif state.target.startswith('ios'): bundle = 'org.leenkx3d.' + wrd.lnx_project_package if wrd.lnx_project_bundle == '' else wrd.lnx_project_bundle khafile.write("project.targetOptions.ios.bundle = '{0}';\n".format(lnx.utils.safestr(bundle))) if wrd.lnx_project_icon != '': icon_src = os.path.normpath(bpy.path.abspath(wrd.lnx_project_icon)) icon_dst = os.path.normpath(os.path.join(project_path, 'icon.png')) if not os.path.isfile(icon_dst) or not os.path.samefile(icon_src, icon_dst): shutil.copy2(icon_src, icon_dst) if wrd.lnx_khafile is not None: khafile.write(wrd.lnx_khafile.as_string()) khafile.write("\n\nresolve(project);\n") def get_winmode(lnx_winmode): if lnx_winmode == 'Window': return 0 elif lnx_winmode == 'Fullscreen': return 1 else: # Headless return 2 def write_config(resx, resy): wrd = bpy.data.worlds['Lnx'] p = os.path.join(lnx.utils.get_fp(), 'Bundled') if not os.path.exists(p): os.makedirs(p) rpdat = lnx.utils.get_rp() rp_shadowmap_cube = int(rpdat.rp_shadowmap_cube) if rpdat.rp_shadows else 0 rp_shadowmap_cascade = lnx.utils.get_cascade_size(rpdat) if rpdat.rp_shadows else 0 output = { 'window_mode': get_winmode(wrd.lnx_winmode), 'window_w': int(resx), 'window_h': int(resy), 'window_resizable': wrd.lnx_winresize, 'window_maximizable': wrd.lnx_winresize and wrd.lnx_winmaximize, 'window_minimizable': wrd.lnx_winminimize, 'window_vsync': wrd.lnx_vsync, '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_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', 'rp_chromatic_aberration': rpdat.rp_chromatic_aberration != 'Off', 'rp_motionblur': rpdat.rp_motionblur != 'Off', 'rp_gi': rpdat.rp_voxels != "Off", 'rp_dynres': rpdat.rp_dynres } with open(os.path.join(p, 'config.lnx'), 'w') as configfile: configfile.write(json.dumps(output, sort_keys=True, indent=4)) def write_mainhx(scene_name, resx, resy, is_play, is_publish): wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() scene_ext = '.lz4' if (wrd.lnx_asset_compression and is_publish) else '' if scene_ext == '' and not wrd.lnx_minimize: scene_ext = '.json' winmode = get_winmode(wrd.lnx_winmode) # Detect custom render path pathpack = 'leenkx' if os.path.isfile(lnx.utils.get_fp() + '/Sources/' + wrd.lnx_project_package + '/renderpath/RenderPathCreator.hx'): pathpack = wrd.lnx_project_package elif rpdat.rp_driver != 'Leenkx': pathpack = rpdat.rp_driver.lower() # Get library hooks hooks = get_library_hooks() with open('Sources/Main.hx', 'w', encoding="utf-8") as f: f.write( """// Auto-generated package; """) # Write hook imports if hooks['imports']: f.write(hooks['imports']) f.write(""" class Main { public static inline var projectName = '""" + lnx.utils.safestr(wrd.lnx_project_name) + """'; public static inline var projectVersion = '""" + lnx.utils.safestr(wrd.lnx_project_version) + """'; public static inline var projectPackage = '""" + lnx.utils.safestr(wrd.lnx_project_package) + """';""") if rpdat.rp_voxels == 'Voxel GI' or rpdat.rp_voxels == 'Voxel AO': f.write(""" public static inline var voxelgiClipmapCount = """ + str(rpdat.lnx_voxelgi_clipmap_count) + """; public static inline var voxelgiVoxelSize = """ + str(round(rpdat.lnx_voxelgi_size * 100) / 100) + """;""") if rpdat.rp_bloom: follow_blender = rpdat.lnx_bloom_follow_blender if bpy.app.version < (4, 3, 0) else False f.write(f"public static var bloomRadius = {bpy.context.scene.eevee.bloom_radius if follow_blender else rpdat.lnx_bloom_radius};") if rpdat.lnx_rp_resolution == 'Custom': f.write(""" public static inline var resolutionSize = """ + str(rpdat.lnx_rp_resolution_size) + """;""") f.write("""\n public static function main() {""") # Calculate base indentation (2 tabs = 8 spaces) base_indent = " " wrap_count = len(hooks['wrap_inits']) # Write wrap_init opening statements (outer to inner, sorted by priority) for i, (priority, before, after) in enumerate(hooks['wrap_inits']): indent = base_indent + (" " * i) f.write("\n" + indent + before) # Calculate content indentation based on wrap depth content_indent = base_indent + (" " * wrap_count) # Write main_pre hooks if hooks['main_pre']: f.write("\n" + content_indent + hooks['main_pre'].replace('\n', '\n' + content_indent)) if rpdat.lnx_skin != 'Off': f.write("\n" + content_indent + "iron.object.BoneAnimation.skinMaxBones = " + str(rpdat.lnx_skin_max_bones) + ";") if rpdat.rp_shadows: if rpdat.rp_shadowmap_cascades != '1': f.write("\n" + content_indent + "iron.object.LightObject.cascadeCount = " + str(rpdat.rp_shadowmap_cascades) + ";") f.write("\n" + content_indent + "iron.object.LightObject.cascadeSplitFactor = " + str(rpdat.lnx_shadowmap_split) + ";") if rpdat.lnx_shadowmap_bounds != 1.0: f.write("\n" + content_indent + "iron.object.LightObject.cascadeBounds = " + str(rpdat.lnx_shadowmap_bounds) + ";") if is_publish and wrd.lnx_loadscreen: asset_references = list(set(assets.assets)) loadscreen_class = 'leenkx.trait.internal.LoadingScreen' if os.path.isfile(lnx.utils.get_fp() + '/Sources/' + wrd.lnx_project_package + '/LoadingScreen.hx'): loadscreen_class = wrd.lnx_project_package + '.LoadingScreen' f.write("\n" + content_indent + "leenkx.system.Starter.numAssets = " + str(len(asset_references)) + ";") f.write("\n" + content_indent + "leenkx.system.Starter.drawLoading = " + loadscreen_class + ".render;") if wrd.lnx_ui == 'Enabled': if wrd.lnx_canvas_img_scaling_quality == 'low': f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;") elif wrd.lnx_canvas_img_scaling_quality == 'high': f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;") # Write Starter.main call starter_indent = content_indent + " " f.write("\n" + content_indent + "leenkx.system.Starter.main(") f.write("\n" + starter_indent + "'" + lnx.utils.safestr(scene_name) + scene_ext + "',") f.write("\n" + starter_indent + str(winmode) + ",") f.write("\n" + starter_indent + ('true' if wrd.lnx_winresize else 'false') + ",") f.write("\n" + starter_indent + ('true' if wrd.lnx_winminimize else 'false') + ",") f.write("\n" + starter_indent + ('true' if (wrd.lnx_winresize and wrd.lnx_winmaximize) else 'false') + ",") f.write("\n" + starter_indent + str(resx) + ",") f.write("\n" + starter_indent + str(resy) + ",") f.write("\n" + starter_indent + str(int(rpdat.lnx_samples_per_pixel)) + ",") f.write("\n" + starter_indent + ('true' if wrd.lnx_vsync else 'false') + ",") f.write("\n" + starter_indent + pathpack + ".renderpath.RenderPathCreator.get") f.write("\n" + content_indent + ");") # Write main_post hooks if hooks['main_post']: f.write("\n" + content_indent + hooks['main_post'].replace('\n', '\n' + content_indent)) # Write wrap_init closing statements (inner to outer, reverse order) for i, (priority, before, after) in enumerate(reversed(hooks['wrap_inits'])): indent = base_indent + (" " * (wrap_count - 1 - i)) f.write("\n" + indent + after) f.write(""" } }""") def write_indexhtml(w, h, is_publish): wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() dest = '/html5' if is_publish else '/debug/html5' if not os.path.exists(lnx.utils.build_dir() + dest): os.makedirs(lnx.utils.build_dir() + dest) popupmenu_in_browser = '' if wrd.lnx_project_html5_popupmenu_in_browser: popupmenu_in_browser = ' oncontextmenu="return false"' with open(lnx.utils.build_dir() + dest + '/index.html', 'w') as f: f.write( """ """) if rpdat.rp_stereo or wrd.lnx_winmode == 'Fullscreen': f.write(""" """) f.write(""" """+html.escape( wrd.lnx_project_name)+""" """) if rpdat.rp_stereo or wrd.lnx_winmode == 'Fullscreen': f.write(""" """) else: if wrd.lnx_winmode != 'Headless': f.write("""

""") else: f.write(""" """) f.write(""" """) add_compiledglsl = '' def write_compiledglsl(defs, make_variants): rpdat = lnx.utils.get_rp() wrd = bpy.data.worlds['Lnx'] shadowmap_size = lnx.utils.get_cascade_size(rpdat) if rpdat.rp_shadows else 0 inc_path = lnx.utils.build_dir() + '/compiled/Shaders/compiled.inc' with io.StringIO() as f: f.write( """#ifndef _COMPILED_GLSL_ #define _COMPILED_GLSL_ """) for d in defs: if make_variants and d.endswith('var'): continue # Write a shader variant instead f.write("#define " + d + "\n") if rpdat.rp_renderer == "Deferred": gbuffer_size = make_renderpath.get_num_gbuffer_rts_deferred() f.write(f'#define GBUF_SIZE {gbuffer_size}\n') # Write indices of G-Buffer render targets f.write('#define GBUF_IDX_0 0\n') f.write('#define GBUF_IDX_1 1\n') idx_emission = 2 idx_refraction = 2 if '_gbuffer2' in wrd.world_defs: f.write('#define GBUF_IDX_2 2\n') idx_emission += 1 idx_refraction += 1 if '_EmissionShaded' in wrd.world_defs: f.write(f'#define GBUF_IDX_EMISSION {idx_emission}\n') idx_refraction += 1 if '_SSRefraction' in wrd.world_defs or '_VoxelRefract' in wrd.world_defs: f.write(f'#define GBUF_IDX_REFRACTION {idx_refraction}\n') f.write("""#if defined(HLSL) || defined(METAL) #define _InvY #endif """) if state.target == 'html5' or lnx.utils.get_gapi() == 'direct3d11': f.write("#define _FlipY\n") f.write("""const float PI = 3.1415926535; const float PI2 = PI * 2.0; const vec2 shadowmapSize = vec2(""" + str(shadowmap_size) + """, """ + str(shadowmap_size) + """); const float shadowmapCubePcfSize = """ + str((round(rpdat.lnx_pcfsize * 100) / 100) / 1000) + """; const int shadowmapCascades = """ + str(rpdat.rp_shadowmap_cascades) + """; """) if rpdat.rp_water: f.write( """const float waterLevel = """ + str(round(rpdat.lnx_water_level * 100) / 100) + """; const float waterDisplace = """ + str(round(rpdat.lnx_water_displace * 100) / 100) + """; const float waterSpeed = """ + str(round(rpdat.lnx_water_speed * 100) / 100) + """; const float waterFreq = """ + str(round(rpdat.lnx_water_freq * 100) / 100) + """; const vec3 waterColor = vec3(""" + str(round(rpdat.lnx_water_color[0] * 100) / 100) + """, """ + str(round(rpdat.lnx_water_color[1] * 100) / 100) + """, """ + str(round(rpdat.lnx_water_color[2] * 100) / 100) + """); const float waterDensity = """ + str(round(rpdat.lnx_water_density * 100) / 100) + """; const float waterRefract = """ + str(round(rpdat.lnx_water_refract * 100) / 100) + """; const float waterReflect = """ + str(round(rpdat.lnx_water_reflect * 100) / 100) + """; """) if '_CDitheringStrength' in defs: f.write( f'const float ditherStrengthValue = {rpdat.lnx_dithering_strength};\n' ) if rpdat.rp_ssao or rpdat.rp_volumetriclight: f.write( """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: 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: follow_blender = rpdat.lnx_bloom_follow_blender if bpy.app.version < (4, 3, 0) else False eevee_settings = bpy.context.scene.eevee threshold = eevee_settings.bloom_threshold if follow_blender else rpdat.lnx_bloom_threshold strength = eevee_settings.bloom_intensity if follow_blender else rpdat.lnx_bloom_strength knee = eevee_settings.bloom_knee if follow_blender else rpdat.lnx_bloom_knee f.write( """const float bloomThreshold = """ + str(round(threshold * 100) / 100) + """; const float bloomStrength = """ + str(round(strength * 100) / 100) + """; const float bloomKnee = """ + str(round(knee * 100) / 100) + """; const float bloomRadius = """ + str(round(rpdat.lnx_bloom_radius * 100) / 100) + """; """) # TODO remove radius if old bloom pass is removed if rpdat.rp_motionblur != 'Off': f.write( """const float motionBlurIntensity = """ + str(round(rpdat.lnx_motion_blur_intensity * 100) / 100) + """; """) if rpdat.rp_ssr: f.write( """const float ssrRayStep = """ + str(round(rpdat.lnx_ssr_ray_step * 100) / 100) + """; const float ssrSearchDist = """ + str(round(rpdat.lnx_ssr_search_dist * 100) / 100) + """; const float ssrFalloffExp = """ + str(round(rpdat.lnx_ssr_falloff_exp * 100) / 100) + """; const float ssrJitter = """ + str(round(rpdat.lnx_ssr_jitter * 100) / 100) + """; """) if rpdat.rp_ss_refraction: f.write( """const float ss_refractionRayStep = """ + str(round(rpdat.lnx_ss_refraction_ray_step * 100) / 100) + """; const float ss_refractionSearchDist = """ + str(round(rpdat.lnx_ss_refraction_search_dist * 100) / 100) + """; const float ss_refractionFalloffExp = """ + str(round(rpdat.lnx_ss_refraction_falloff_exp * 100) / 100) + """; const float ss_refractionJitter = """ + str(round(rpdat.lnx_ss_refraction_jitter * 100) / 100) + """; """) if rpdat.lnx_ssrs: f.write( """const float ssrsRayStep = """ + str(round(rpdat.lnx_ssrs_ray_step * 100) / 100) + """; """) if rpdat.rp_volumetriclight: f.write( """const float volumAirTurbidity = """ + str(round(rpdat.lnx_volumetric_light_air_turbidity * 100) / 100) + """; const vec3 volumAirColor = vec3(""" + str(round(rpdat.lnx_volumetric_light_air_color[0] * 100) / 100) + """, """ + str(round(rpdat.lnx_volumetric_light_air_color[1] * 100) / 100) + """, """ + str(round(rpdat.lnx_volumetric_light_air_color[2] * 100) / 100) + """); const int volumSteps = """ + str(rpdat.lnx_volumetric_light_steps) + """; """) if rpdat.rp_autoexposure: f.write( """const float autoExposureStrength = """ + str(rpdat.lnx_autoexposure_strength) + """; const float autoExposureSpeed = """ + str(rpdat.lnx_autoexposure_speed) + """; """) # Compositor if rpdat.lnx_letterbox: f.write( """const float compoLetterboxSize = """ + str(round(rpdat.lnx_letterbox_size * 100) / 100) + """; const vec3 compoLetterboxColor = vec3(""" + str(round(rpdat.lnx_letterbox_color[0] * 100) / 100) + """, """ + str(round(rpdat.lnx_letterbox_color[1] * 100) / 100) + """, """ + str(round(rpdat.lnx_letterbox_color[2] * 100) / 100) + """); """) if rpdat.lnx_distort: f.write( """const float compoDistortStrength = """ + str(round(rpdat.lnx_distort_strength * 100) / 100) + """; """) if rpdat.lnx_grain: f.write( """const float compoGrainStrength = """ + str(round(rpdat.lnx_grain_strength * 100) / 100) + """; """) if rpdat.lnx_vignette: f.write( """const float compoVignetteStrength = """ + str(round(rpdat.lnx_vignette_strength * 100) / 100) + """; """) if rpdat.lnx_sharpen: f.write( """const float compoSharpenStrength = """ + str(round(rpdat.lnx_sharpen_strength * 100) / 100) + """; const float compoSharpenSize = """ + str(round(rpdat.lnx_sharpen_size * 100) / 100) + """; const vec3 compoSharpenColor = vec3(""" + str(round(rpdat.lnx_sharpen_color[0] * 100) / 100) + """, """ + str(round(rpdat.lnx_sharpen_color[1] * 100) / 100) + """, """ + str(round(rpdat.lnx_sharpen_color[2] * 100) / 100) + """); """) if lnx.utils.get_active_scene().view_settings.exposure != 0.0: f.write( """const float compoExposureStrength = """ + str(round(lnx.utils.get_active_scene().view_settings.exposure * 100) / 100) + """; """) if rpdat.lnx_fog: f.write( """const float compoFogAmountA = """ + str(round(rpdat.lnx_fog_amounta * 100) / 100) + """; const float compoFogAmountB = """ + str(round(rpdat.lnx_fog_amountb * 100) / 100) + """; const vec3 compoFogColor = vec3(""" + str(round(rpdat.lnx_fog_color[0] * 100) / 100) + """, """ + str(round(rpdat.lnx_fog_color[1] * 100) / 100) + """, """ + str(round(rpdat.lnx_fog_color[2] * 100) / 100) + """); """) if rpdat.lnx_lens_texture_masking: f.write( """const float compoCenterMinClip = """ + str(round(rpdat.lnx_lens_texture_masking_centerMinClip * 100) / 100) + """; const float compoCenterMaxClip = """ + str(round(rpdat.lnx_lens_texture_masking_centerMaxClip * 100) / 100) + """; const float compoLuminanceMin = """ + str(round(rpdat.lnx_lens_texture_masking_luminanceMin * 100) / 100) + """; const float compoLuminanceMax = """ + str(round(rpdat.lnx_lens_texture_masking_luminanceMax * 100) / 100) + """; const float compoBrightnessExponent = """ + str(round(rpdat.lnx_lens_texture_masking_brightnessExp * 100) / 100) + """; """) if rpdat.rp_chromatic_aberration: f.write( f"""const float compoChromaticStrength = {round(rpdat.lnx_chromatic_aberration_strength * 100) / 100}; const int compoChromaticSamples = {rpdat.lnx_chromatic_aberration_samples}; """) if rpdat.lnx_chromatic_aberration_type == "Spectral": f.write("const int compoChromaticType = 1;") else: f.write("const int compoChromaticType = 0;") focus_distance = 0.0 fstop = 0.0 if lnx.utils.get_active_scene().camera and lnx.utils.get_active_scene().camera.data.dof.use_dof: focus_distance = lnx.utils.get_active_scene().camera.data.dof.focus_distance fstop = lnx.utils.get_active_scene().camera.data.dof.aperture_fstop lens = lnx.utils.get_active_scene().camera.data.lens if focus_distance > 0.0: f.write( """const float compoDOFDistance = """ + str(round(focus_distance * 100) / 100) + """; const float compoDOFFstop = """ + str(round(fstop * 100) / 100) + """; const float compoDOFLength = """ + str(round(lens * 100) / 100) +"""; """) #160.0; if rpdat.rp_voxels != 'Off': f.write("""const ivec3 voxelgiResolution = ivec3(""" + str(rpdat.rp_voxelgi_resolution) + """, """ + str(rpdat.rp_voxelgi_resolution) + """, """ + str(rpdat.rp_voxelgi_resolution) + """); const int voxelgiClipmapCount = """ + str(rpdat.lnx_voxelgi_clipmap_count) + """; const float voxelgiOcc = """ + str(round(rpdat.lnx_voxelgi_occ * 100) / 100) + """; const float voxelgiVoxelSize = """ + str(round(rpdat.lnx_voxelgi_size * 1000) / 1000) + """; const float voxelgiStep = """ + str(round(rpdat.lnx_voxelgi_step * 1000) / 1000) + """; const float voxelgiRange = """ + str(round(rpdat.lnx_voxelgi_range * 100) / 100) + """; const float voxelgiOffset = """ + str(round(rpdat.lnx_voxelgi_offset * 1000) / 1000) + """; const float voxelgiAperture = """ + str(round(rpdat.lnx_voxelgi_aperture * 100) / 100) + """; const float voxelgiShad = """ + str(round(rpdat.lnx_voxelgi_shad * 100) / 100) + """; const float voxelgiEnv = """ + str(round(rpdat.lnx_voxelgi_env * 100) / 100) + """; """) if rpdat.rp_voxels == 'Voxel GI': f.write(""" 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 or '_SSS' in wrd.world_defs: f.write(f"const float sssWidth = {rpdat.lnx_sss_width / 10.0};\n") # Skinning if rpdat.lnx_skin == 'On': f.write( """const int skinMaxBones = """ + str(rpdat.lnx_skin_max_bones) + """; """) if '_Clusters' in wrd.world_defs: max_lights = "4" max_lights_clusters = "4" if rpdat.rp_shadowmap_atlas: max_lights = str(rpdat.rp_max_lights) max_lights_clusters = str(rpdat.rp_max_lights_cluster) # prevent max lights cluster being higher than max lights if (int(max_lights_clusters) > int(max_lights)): max_lights_clusters = max_lights f.write( """const int maxLights = """ + max_lights + """; const int maxLightsCluster = """ + max_lights_clusters + """; const float clusterNear = 3.0; """) f.write(add_compiledglsl + '\n') # External defined constants f.write("""#endif // _COMPILED_GLSL_ """) return lnx.utils.write_if_changed(inc_path, f.getvalue()) def write_traithx(class_path): wrd = bpy.data.worlds['Lnx'] # Split the haxe package syntax in components that will compose the path path_components = class_path.split('.') # extract the full file name (file + ext) from the components class_name = path_components[-1] # Create the absolute trait path (os-safe) package_path = os.sep.join([lnx.utils.get_fp(), 'Sources', lnx.utils.safestr(wrd.lnx_project_package)] + path_components[:-1]) if not os.path.exists(package_path): os.makedirs(package_path) package = '.'.join([lnx.utils.safestr(wrd.lnx_project_package)] + path_components[:-1]); with open(package_path + '/' + class_name + '.hx', 'w') as f: f.write( """package """ + package + """; class """ + class_name + """ extends iron.Trait { \tpublic function new() { \t\tsuper(); \t\t// notifyOnInit(function() { \t\t// }); \t\t// notifyOnUpdate(function() { \t\t// }); \t\t// notifyOnRemove(function() { \t\t// }); \t} } """) def write_canvasjson(canvas_name): canvas_path = lnx.utils.get_fp() + '/Bundled/canvas' if not os.path.exists(canvas_path): os.makedirs(canvas_path) with open(canvas_path + '/' + canvas_name + '.json', 'w') as f: f.write( """{ "name": "untitled", "x": 0.0, "y": 0.0, "width": 1280, "height": 720, "theme": "Default Light", "elements": [], "assets": [] }""")