This commit is contained in:
2026-02-24 17:35:26 -08:00
parent 1c3c30e6ce
commit d45c632dcd
28 changed files with 1982 additions and 97 deletions

View File

@ -595,6 +595,20 @@ class LeenkxExporter:
return space.region_3d.window_matrix, space.region_3d.is_perspective return space.region_3d.window_matrix, space.region_3d.is_perspective
return None, False 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): def write_bone_matrices(self, scene, action):
# profile_time = time.time() # profile_time = time.time()
begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) 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'] wrd = bpy.data.worlds['Lnx']
phys_enabled = wrd.lnx_physics != 'Disabled' 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 # Rigid body trait
if bobject.rigid_body is not None and phys_enabled: 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 wrd.lnx_physics != 'Disabled' and LeenkxExporter.export_physics:
if 'traits' not in self.output: if 'traits' not in self.output:
self.output['traits'] = [] 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 = { out_trait = {
'type': 'Script', 'type': 'Script',
@ -3204,7 +3218,7 @@ class LeenkxExporter:
LeenkxExporter.export_physics = True LeenkxExporter.export_physics = True
assets.add_khafile_def('lnx_physics_soft') 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'} out_trait = {'type': 'Script', 'class_name': 'leenkx.trait.physics.' + phys_pkg + '.SoftBody'}
# ClothModifier # ClothModifier
if modifier.type == 'CLOTH': if modifier.type == 'CLOTH':
@ -3228,7 +3242,7 @@ class LeenkxExporter:
def add_hook_mod(o, bobject: bpy.types.Object, target_name, group_name): def add_hook_mod(o, bobject: bpy.types.Object, target_name, group_name):
LeenkxExporter.export_physics = True 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'} out_trait = {'type': 'Script', 'class_name': 'leenkx.trait.physics.' + phys_pkg + '.PhysicsHook'}
verts = [] verts = []
@ -3254,7 +3268,7 @@ class LeenkxExporter:
return return
LeenkxExporter.export_physics = True 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 breaking_threshold = rbc.breaking_threshold if rbc.use_breaking else 0
trait = { trait = {

View File

@ -37,6 +37,7 @@ else:
_active_threads: Dict[threading.Thread, Callable] = {} _active_threads: Dict[threading.Thread, Callable] = {}
_last_poll_time = 0.0 _last_poll_time = 0.0
_consecutive_empty_polls = 0 _consecutive_empty_polls = 0
_last_render_engine = None
@persistent @persistent
def on_depsgraph_update_post(self): def on_depsgraph_update_post(self):
@ -141,6 +142,43 @@ def always() -> float:
return 0.5 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: def poll_threads() -> float:
""" """
@ -389,6 +427,7 @@ def register():
bpy.app.timers.register(always, persistent=True) bpy.app.timers.register(always, persistent=True)
bpy.app.timers.register(poll_threads, persistent=True) bpy.app.timers.register(poll_threads, persistent=True)
bpy.app.timers.register(check_render_engine, persistent=True)
if lnx.utils.get_fp() != '': if lnx.utils.get_fp() != '':
# TODO: On windows, on_load_post is not called when opening .blend file from explorer # TODO: On windows, on_load_post is not called when opening .blend file from explorer
@ -405,6 +444,7 @@ def register():
def unregister(): def unregister():
unload_py_libraries() unload_py_libraries()
bpy.app.timers.unregister(check_render_engine)
bpy.app.timers.unregister(poll_threads) bpy.app.timers.unregister(poll_threads)
bpy.app.timers.unregister(always) bpy.app.timers.unregister(always)

View File

@ -37,6 +37,8 @@ def register():
kmn.keymap_items.new('lnx.edit_group_tree', 'TAB', 'PRESS') 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('node.tree_path_parent', 'TAB', 'PRESS', ctrl=True)
kmn.keymap_items.new('lnx.ungroup_group_tree', 'G', 'PRESS', alt=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(): 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['lnx.edit_group_tree'])
kmn.keymap_items.remove(kmn.keymap_items['node.tree_path_parent']) 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.ungroup_group_tree'])
kmn.keymap_items.remove(kmn.keymap_items['lnx.frame_selected_nodes'])

View File

@ -1,12 +1,9 @@
import bpy, math, os, gpu, importlib import bpy, math, os, gpu, bgl, importlib
import numpy as np import numpy as np
from . import utility from . import utility
from fractions import Fraction from fractions import Fraction
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
if bpy.app.version < (4, 0, 0):
import bgl
def splitLogLuvAlphaAtlas(imageIn, outDir, quality): def splitLogLuvAlphaAtlas(imageIn, outDir, quality):
pass pass

View File

@ -1,9 +1,6 @@
import bpy, blf, os, gpu import bpy, blf, bgl, os, gpu
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
if bpy.app.version < (4, 0, 0):
import bgl
class ViewportDraw: class ViewportDraw:
def __init__(self, context, text): def __init__(self, context, text):

View File

@ -197,7 +197,10 @@ class CreateStyleNode(LnxLogicTreeNode):
properties += self.inputs[ind].name + ':' properties += self.inputs[ind].name + ':'
ind += 1 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) return self.get('property0', 60)

View File

@ -537,6 +537,40 @@ class LnxAddCallGroupNode(bpy.types.Operator):
return {'FINISHED'} 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): class LNX_PT_LogicGroupPanel(bpy.types.Panel):
bl_label = 'Leenkx Logic Group' bl_label = 'Leenkx Logic Group'
bl_idname = 'LNX_PT_LogicGroupPanel' bl_idname = 'LNX_PT_LogicGroupPanel'
@ -575,6 +609,7 @@ __REG_CLASSES = (
TreeVarNameConflictItem, TreeVarNameConflictItem,
LnxUngroupGroupTree, LnxUngroupGroupTree,
LnxAddCallGroupNode, LnxAddCallGroupNode,
LnxFrameSelectedNodes,
LNX_PT_LogicGroupPanel LNX_PT_LogicGroupPanel
) )
register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES)

View File

@ -586,6 +586,298 @@ def play_done():
log.clear() log.clear()
live_patch.stop() 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(): def assets_done():
if state.proc_build == None: if state.proc_build == None:
return return
@ -761,6 +1053,17 @@ def build_success():
cmd.append(str(pid)) cmd.append(str(pid))
if wrd.lnx_audio == 'Disabled': if wrd.lnx_audio == 'Disabled':
cmd.append('--nosound') 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')): elif state.target.startswith(('windows-hl', 'linux-hl', 'macos-hl')):
log.info(f"Runtime Hashlink/C target: {state.target}") log.info(f"Runtime Hashlink/C target: {state.target}")

View File

@ -79,12 +79,6 @@ def add_world_defs():
assets.add_khafile_def('lnx_shadowmap_atlas_lod') 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))) 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: if rpdat.rp_autoexposure:
wrd.world_defs += '_AutoExposure' wrd.world_defs += '_AutoExposure'
@ -306,19 +300,25 @@ def build():
if rpdat.rp_supersampling == '4': if rpdat.rp_supersampling == '4':
assets.add_shader_pass('supersample_resolve') assets.add_shader_pass('supersample_resolve')
assets.add_khafile_def('rp_ssgi={0}'.format(rpdat.rp_ssgi)) if rpdat.rp_fsr1 != 'Off':
if rpdat.rp_ssgi != 'Off': assets.add_khafile_def('rp_fsr1')
if rpdat.rp_ssgi == 'SSAO': wrd.world_defs += '_FSR1_{0}'.format(rpdat.rp_fsr1)
wrd.world_defs += '_SSAO' assets.add_shader_pass('fsr1_easu_pass')
assets.add_shader_pass('ssao_pass') assets.add_shader_pass('fsr1_rcas_pass')
assets.add_shader_pass('blur_edge_pass')
elif rpdat.rp_ssgi == 'SSGI': if rpdat.rp_ssao:
wrd.world_defs += '_SSGI' assets.add_khafile_def('rp_ssao')
assets.add_shader_pass('ssgi_pass') wrd.world_defs += '_SSAO'
assets.add_shader_pass('blur_edge_pass') assets.add_shader_pass('ssao_pass')
else: assets.add_shader_pass('blur_edge_pass')
assets.add_shader_pass('ssgi_pass') if rpdat.lnx_ssao_half_res:
assets.add_shader_pass('blur_edge_pass') 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: if rpdat.lnx_ssgi_half_res:
assets.add_khafile_def('rp_ssgi_half') assets.add_khafile_def('rp_ssgi_half')
@ -362,6 +362,7 @@ def build():
assets.add_khafile_def('rp_stereo') assets.add_khafile_def('rp_stereo')
assets.add_khafile_def('lnx_vr') assets.add_khafile_def('lnx_vr')
wrd.world_defs += '_VR' wrd.world_defs += '_VR'
wrd.world_defs += '_VRStereo'
has_voxels = lnx.utils.voxel_support() has_voxels = lnx.utils.voxel_support()
if rpdat.rp_voxels != "Off" and has_voxels and rpdat.lnx_material_model == 'Full': if rpdat.rp_voxels != "Off" and has_voxels and rpdat.lnx_material_model == 'Full':
@ -421,7 +422,7 @@ def build():
wrd.world_defs += '_SSS' wrd.world_defs += '_SSS'
assets.add_shader_pass('sss_pass') 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') assets.add_shader_pass('downsample_depth')
if rpdat.rp_motionblur != 'Off': if rpdat.rp_motionblur != 'Off':

View File

@ -18,3 +18,7 @@ if not lnx.is_reload(__name__):
is_export = False is_export = False
is_play = False is_play = False
is_publish = False is_publish = False
is_viewport = False
viewport_width = 1920
viewport_height = 1080
viewport_id = None

View File

@ -16,11 +16,13 @@
# #
import os import os
import shutil import shutil
import subprocess
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple
import bpy import bpy
import os
import lnx.assets import lnx.assets as assets
import lnx.log as log import lnx.log as log
import lnx.make_state import lnx.make_state
import lnx.material.cycles_functions as c_functions import lnx.material.cycles_functions as c_functions
@ -1003,7 +1005,12 @@ def make_texture(
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
max_size = int(wrd.lnx_max_texture_size) max_size = int(wrd.lnx_max_texture_size)
if max_size > 0 and image is not None: if max_size > 0 and image is not None:
original_filepath = filepath
filepath = resize_texture_if_needed(image, filepath, max_size) 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 # Link image path to assets
# TODO: Khamake converts .PNG to .jpg? Convert ext to lowercase on windows # TODO: Khamake converts .PNG to .jpg? Convert ext to lowercase on windows

View File

@ -1,4 +1,3 @@
from __future__ import annotations
import bpy import bpy
from bpy.types import NodeSocket 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)) 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 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]) 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 ek1 = mat_state.emission_type
mat_state.emission_type = mat_state.EmissionType.NO_EMISSION 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]) 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 ek2 = mat_state.emission_type
mat_state.needs_sss = sss_1 or sss_2
if state.parse_surface: if state.parse_surface:
state.out_basecol = '({0} * {3} + {1} * {2})'.format(bc1, bc2, fac_var, fac_inv_var) 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: def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, state: ParserState) -> None:
mat_state.emission_type = mat_state.EmissionType.NO_EMISSION 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]) 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 ek1 = mat_state.emission_type
mat_state.emission_type = mat_state.EmissionType.NO_EMISSION 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]) 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 ek2 = mat_state.emission_type
mat_state.needs_sss = sss_1 or sss_2
if state.parse_surface: if state.parse_surface:
state.out_basecol = '({0} + {1})'.format(bc1, bc2) state.out_basecol = '({0} + {1})'.format(bc1, bc2)
@ -82,6 +91,8 @@ if bpy.app.version < (2, 92, 0):
if state.parse_surface: if state.parse_surface:
c.write_normal(node.inputs[20]) c.write_normal(node.inputs[20])
state.out_basecol = c.parse_vector_input(node.inputs[0]) 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_metallic = c.parse_value_input(node.inputs[4])
state.out_specular = c.parse_value_input(node.inputs[5]) state.out_specular = c.parse_value_input(node.inputs[5])
state.out_roughness = c.parse_value_input(node.inputs[7]) 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: if state.parse_surface:
c.write_normal(node.inputs[22]) c.write_normal(node.inputs[22])
state.out_basecol = c.parse_vector_input(node.inputs[0]) 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_radius = c.parse_vector_input(node.inputs[2])
# subsurface_color = c.parse_vector_input(node.inputs[3]) # subsurface_color = c.parse_vector_input(node.inputs[3])
state.out_metallic = c.parse_value_input(node.inputs[6]) 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: if state.parse_surface:
c.write_normal(node.inputs[5]) c.write_normal(node.inputs[5])
state.out_basecol = c.parse_vector_input(node.inputs[0]) 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 = c.parse_value_input(node.inputs[7])
subsurface_radius = c.parse_vector_input(node.inputs[9]) subsurface_radius = c.parse_vector_input(node.inputs[9])
subsurface_color = c.parse_vector_input(node.inputs[8]) subsurface_color = c.parse_vector_input(node.inputs[8])
state.out_metallic = c.parse_value_input(node.inputs[1]) 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: 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]) state.out_roughness = c.parse_value_input(node.inputs[2])
# Prevent black material when metal = 1.0 and roughness = 0.0 # Prevent black material when metal = 1.0 and roughness = 0.0
try: 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: def parse_subsurfacescattering(node: bpy.types.ShaderNodeSubsurfaceScattering, out_socket: NodeSocket, state: ParserState) -> None:
if state.parse_surface: if state.parse_surface:
# Mark that this material needs SSS
mat_state.needs_sss = True
if bpy.app.version < (4, 1, 0): if bpy.app.version < (4, 1, 0):
c.write_normal(node.inputs[4]) c.write_normal(node.inputs[4])
else: else:

View File

@ -8,6 +8,7 @@ import lnx.log as log
import lnx.material.cycles as cycles import lnx.material.cycles as cycles
import lnx.material.make_shader as make_shader import lnx.material.make_shader as make_shader
import lnx.material.mat_batch as mat_batch 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.material.mat_utils as mat_utils
import lnx.node_utils import lnx.node_utils
import lnx.utils import lnx.utils
@ -17,6 +18,7 @@ if lnx.is_reload(__name__):
cycles = lnx.reload_module(cycles) cycles = lnx.reload_module(cycles)
make_shader = lnx.reload_module(make_shader) make_shader = lnx.reload_module(make_shader)
mat_batch = lnx.reload_module(mat_batch) mat_batch = lnx.reload_module(mat_batch)
mat_state = lnx.reload_module(mat_state)
mat_utils = lnx.reload_module(mat_utils) mat_utils = lnx.reload_module(mat_utils)
lnx.node_utils = lnx.reload_module(lnx.node_utils) lnx.node_utils = lnx.reload_module(lnx.node_utils)
lnx.utils = lnx.reload_module(lnx.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 shader_data_name = material.lnx_custom_material
bind_constants = {'mesh': []} bind_constants = {'mesh': []}
bind_textures = {'mesh': []} bind_textures = {'mesh': []}
mat_uses_sss = False
make_shader.make_instancing_and_skinning(material, mat_users) 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') 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'): 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 sd = shader_data.sd
else: 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 sd = shader_data.sd
sss_used = False 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': elif rpdat.rp_sss_state != 'Off':
const = {'name': 'materialID'} const = {'name': 'materialID'}
if needs_sss: # Use per-material SSS flag from shader build
if mat_uses_sss:
const['intValue'] = 2 const['intValue'] = 2
sss_used = True sss_used = True
if '_SSS' not in wrd.world_defs:
wrd.world_defs += '_SSS'
else: else:
const['intValue'] = 0 const['intValue'] = 0
c['bind_constants'].append(const) 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): 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 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 return False

View File

@ -107,7 +107,7 @@ def write(vert: shader.Shader, frag: shader.Shader):
if '_MicroShadowing' in wrd.world_defs and not is_mobile: if '_MicroShadowing' in wrd.world_defs and not is_mobile:
frag.write('\t, occlusion') frag.write('\t, occlusion')
if '_SSRS' in wrd.world_defs: 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('mat4 invVP', '_inverseViewProjectionMatrix')
frag.add_uniform('vec3 eye', '_cameraPosition') frag.add_uniform('vec3 eye', '_cameraPosition')
frag.write(', gbufferD, invVP, eye') frag.write(', gbufferD, invVP, eye')

View File

@ -198,7 +198,7 @@ def make_deferred(con_mesh, rpasses):
rpdat = lnx.utils.get_rp() rpdat = lnx.utils.get_rp()
lnx_discard = mat_state.material.lnx_discard 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) make_base(con_mesh, parse_opacity=parse_opacity)
@ -213,6 +213,7 @@ def make_deferred(con_mesh, rpasses):
opac = '0.9999' # 1.0 - eps opac = '0.9999' # 1.0 - eps
frag.write('if (opacity < {0}) discard;'.format(opac)) frag.write('if (opacity < {0}) discard;'.format(opac))
frag.add_out(f'vec4 fragColor[GBUF_SIZE]') frag.add_out(f'vec4 fragColor[GBUF_SIZE]')
if '_gbuffer2' in wrd.world_defs: if '_gbuffer2' in wrd.world_defs:
@ -281,7 +282,7 @@ def make_deferred(con_mesh, rpasses):
frag.write('#endif') frag.write('#endif')
if '_SSRefraction' in wrd.world_defs or '_VoxelRefract' in wrd.world_defs: 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 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[0] = vec4(direct + indirect, packFloat2(occlusion, specular));')
frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);')
if rpdat.rp_ss_refraction or rpdat.lnx_voxelgi_refract: 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: else:
frag.add_out('vec4 fragColor[1]') frag.add_out('vec4 fragColor[1]')
@ -716,8 +717,12 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False):
else: else:
frag.write('vec3 indirect = envl;') 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: 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('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('indirect = (diffuse_indirect.rgb * albedo * (1.0 - F) + envl * (1.0 - diffuse_indirect.a)) * voxelgiDiff;')
frag.write('if (roughness < 1.0 && specular > 0.0) {') 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') frag.write(', true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight')
if '_VoxelShadow' in wrd.world_defs: if '_VoxelShadow' in wrd.world_defs:
frag.write(', voxels, voxelsSDF, clipmaps') frag.write(', voxels, voxelsSDF, clipmaps')
if '_Veloc' in wrd.world_defs or '_VoxelShadow' in wrd.world_defs:
frag.write(', velocity') frag.write(', velocity')
if '_MicroShadowing' in wrd.world_defs: if '_MicroShadowing' in wrd.world_defs:
frag.write(', occlusion') frag.write(', occlusion')
if '_SSRS' in wrd.world_defs: 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('mat4 invVP', '_inverseViewProjectionMatrix')
frag.add_uniform('vec3 eye', '_cameraPosition') frag.add_uniform('vec3 eye', '_cameraPosition')
frag.write(', gbufferD, invVP, eye') frag.write(', gbufferD, invVP, eye')

View File

@ -6,6 +6,8 @@ else:
lnx.enable_reload(__name__) lnx.enable_reload(__name__)
def morph_pos(vert): def morph_pos(vert):
if vert.has_attrib('vec2 texCoordMorph = morph * texUnpack;'):
return
rpdat = lnx.utils.get_rp() rpdat = lnx.utils.get_rp()
vert.add_include('compiled.inc') vert.add_include('compiled.inc')
vert.add_include('std/morph_target.glsl') vert.add_include('std/morph_target.glsl')
@ -22,6 +24,8 @@ def morph_pos(vert):
vert.write_attrib('spos.xyz /= posUnpack;') vert.write_attrib('spos.xyz /= posUnpack;')
def morph_nor(vert, is_bone, prep): 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('vec3 morphNor = vec3(0, 0, 0);')
vert.write_attrib('getMorphedNormal(texCoordMorph, vec3(nor.xy, pos.w), morphNor);') vert.write_attrib('getMorphedNormal(texCoordMorph, vec3(nor.xy, pos.w), morphNor);')
if not is_bone: if not is_bone:

View File

@ -18,7 +18,15 @@ else:
def make(context_id): 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) make_mesh.make_forward_base(con_refract, parse_opacity=True, transluc_pass=True)
vert = con_refract.vert vert = con_refract.vert
@ -51,14 +59,16 @@ def make(context_id):
frag.write('const uint matid = 0;') frag.write('const uint matid = 0;')
if rpdat.rp_renderer == 'Deferred': if rpdat.rp_renderer == 'Deferred':
frag.write('fragColor[0] = vec4(n.xy, roughness, packFloatInt16(metallic, matid));') frag.write('fragColor[0] = vec4(n.xy, roughness, 1.0);')
frag.write('fragColor[1] = vec4(direct + indirect, packFloat2(occlusion, specular));') frag.write('vec3 finalColor = direct + indirect;')
frag.write('fragColor[1] = vec4(finalColor * opacity, opacity);')
else: else:
frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') frag.write('vec3 finalColor = direct + indirect;')
frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') 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, 1.0 - opacity, gl_FragCoord.z, 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, packFloat2(basecol.r, basecol.g), basecol.b);')
make_finalize.make(con_refract) make_finalize.make(con_refract)

View File

@ -56,6 +56,9 @@ def build(material: Material, mat_users: Dict[Material, List[Object]], mat_lnxus
if mat_state.output_node is None: if mat_state.output_node is None:
# Place empty material output to keep compiler happy.. # Place empty material output to keep compiler happy..
mat_state.output_node = mat_state.nodes.new('ShaderNodeOutputMaterial') mat_state.output_node = mat_state.nodes.new('ShaderNodeOutputMaterial')
# reset for each material
mat_state.needs_sss = False
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
rpdat = lnx.utils.get_rp() 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' shader_data_path = lnx.utils.get_fp_build() + '/compiled/Shaders/' + shader_data_name + '.lnx'
assets.add_shader_data(shader_data_path) 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: 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: if shader is None or shader.is_linked:
return 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 # TODO: blend context
if rpass == 'mesh' and mat_state.material.lnx_blending: if rpass == 'mesh' and mat_state.material.lnx_blending:
rpass = 'blend' rpass = 'blend'

View File

@ -7,6 +7,9 @@ else:
def skin_pos(vert): def skin_pos(vert):
if vert.has_attrib('vec4 skinA;'):
return
vert.add_include('compiled.inc') vert.add_include('compiled.inc')
rpdat = lnx.utils.get_rp() rpdat = lnx.utils.get_rp()
@ -25,6 +28,11 @@ def skin_pos(vert):
def skin_nor(vert, is_morph, prep): 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() rpdat = lnx.utils.get_rp()
if(is_morph): if(is_morph):
vert.write_attrib(prep + 'wnormal = normalize(N * (morphNor + 2.0 * cross(skinA.xyz, cross(skinA.xyz, morphNor) + skinA.w * morphNor)));') vert.write_attrib(prep + 'wnormal = normalize(N * (morphNor + 2.0 * cross(skinA.xyz, cross(skinA.xyz, morphNor) + skinA.w * morphNor)));')

View File

@ -41,7 +41,7 @@ def make(context_id):
frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') 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('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('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[0] = vec4(premultipliedReflect.rgb * w, premultipliedReflect.a);')
frag.write('fragColor[1] = vec4(premultipliedReflect.a * w, 0.0, 0.0, 1.0);') frag.write('fragColor[1] = vec4(premultipliedReflect.a * w, 0.0, 0.0, 1.0);')

View File

@ -38,3 +38,4 @@ texture_grad = False # Sample textures using textureGrad()
con_mesh = None # Mesh context con_mesh = None # Mesh context
uses_instancing = False # Whether the current material has at least one user with instancing enabled uses_instancing = False # Whether the current material has at least one user with instancing enabled
emission_type = EmissionType.NO_EMISSION emission_type = EmissionType.NO_EMISSION
needs_sss = False

View File

@ -361,9 +361,14 @@ class Shader:
def write_header(self, s): def write_header(self, s):
self.header += s + '\n' 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' self.main_attribs += '\t' + s + '\n'
def has_attrib(self, s):
return s in self.main_attribs
def is_equal(self, sh): def is_equal(self, sh):
self.vstruct_to_vsin() self.vstruct_to_vsin()
return self.ins == sh.ins and \ return self.ins == sh.ins and \
@ -394,6 +399,25 @@ class Shader:
for e in vs: for e in vs:
self.add_in('vec' + self.data_size(e['data']) + ' ' + e['name']) 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): def get(self):
if self.noprocessing: if self.noprocessing:
return self.main return self.main

View File

@ -205,7 +205,8 @@ def init_properties():
name="Physics", default='Auto', update=assets.invalidate_compiler_cache) name="Physics", default='Auto', update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_physics_engine = EnumProperty( bpy.types.World.lnx_physics_engine = EnumProperty(
items=[('Bullet', 'Bullet', 'Bullet'), items=[('Bullet', 'Bullet', 'Bullet'),
('Oimo', 'Oimo', 'Oimo')], ('Oimo', 'Oimo', 'Oimo'),
('Jolt', 'Jolt', 'Jolt')],
name="Physics Engine", default='Bullet', update=assets.invalidate_compiler_cache) name="Physics Engine", default='Bullet', update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_physics_fixed_step = FloatProperty( bpy.types.World.lnx_physics_fixed_step = FloatProperty(
name="Fixed Step", default=1/60, min=0, max=1, name="Fixed Step", default=1/60, min=0, max=1,

View File

@ -72,7 +72,8 @@ def update_preset(self, context):
rpdat.rp_antialiasing = 'SMAA' rpdat.rp_antialiasing = 'SMAA'
rpdat.rp_compositornodes = True rpdat.rp_compositornodes = True
rpdat.rp_volumetriclight = False rpdat.rp_volumetriclight = False
rpdat.rp_ssgi = 'SSAO' rpdat.rp_ssao = True
rpdat.rp_ssgi = False
rpdat.lnx_ssrs = False rpdat.lnx_ssrs = False
rpdat.lnx_micro_shadowing = False rpdat.lnx_micro_shadowing = False
rpdat.rp_ssr = False rpdat.rp_ssr = False
@ -110,7 +111,8 @@ def update_preset(self, context):
rpdat.rp_antialiasing = 'Off' rpdat.rp_antialiasing = 'Off'
rpdat.rp_compositornodes = False rpdat.rp_compositornodes = False
rpdat.rp_volumetriclight = False rpdat.rp_volumetriclight = False
rpdat.rp_ssgi = 'Off' rpdat.rp_ssao = False
rpdat.rp_ssgi = False
rpdat.lnx_ssrs = False rpdat.lnx_ssrs = False
rpdat.lnx_micro_shadowing = False rpdat.lnx_micro_shadowing = False
rpdat.rp_ssr = False rpdat.rp_ssr = False
@ -152,8 +154,9 @@ def update_preset(self, context):
rpdat.rp_antialiasing = 'TAA' rpdat.rp_antialiasing = 'TAA'
rpdat.rp_compositornodes = True rpdat.rp_compositornodes = True
rpdat.rp_volumetriclight = False rpdat.rp_volumetriclight = False
rpdat.rp_ssgi = 'SSGI' rpdat.rp_ssao = True
rpdat.lnx_ssrs = False rpdat.rp_ssgi = True
rpdat.lnx_ssrs = True
rpdat.lnx_micro_shadowing = True rpdat.lnx_micro_shadowing = True
rpdat.rp_ssr = True rpdat.rp_ssr = True
rpdat.rp_ss_refraction = True rpdat.rp_ss_refraction = True
@ -193,7 +196,8 @@ def update_preset(self, context):
rpdat.rp_antialiasing = 'Off' rpdat.rp_antialiasing = 'Off'
rpdat.rp_compositornodes = False rpdat.rp_compositornodes = False
rpdat.rp_volumetriclight = False rpdat.rp_volumetriclight = False
rpdat.rp_ssgi = 'Off' rpdat.rp_ssao = False
rpdat.rp_ssgi = False
rpdat.lnx_ssrs = False rpdat.lnx_ssrs = False
rpdat.lnx_micro_shadowing = False rpdat.lnx_micro_shadowing = False
rpdat.rp_ssr = False rpdat.rp_ssr = False
@ -380,6 +384,15 @@ class LnxRPListItem(bpy.types.PropertyGroup):
('2', '2', '2'), ('2', '2', '2'),
('4', '4', '4')], ('4', '4', '4')],
name="Super Sampling", description="Screen resolution multiplier", default='1', update=update_renderpath) 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( rp_antialiasing: EnumProperty(
items=[('Off', 'No AA', 'Off'), items=[('Off', 'No AA', 'Off'),
('FXAA', 'FXAA', 'FXAA'), ('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_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_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_ss_refraction: BoolProperty(name="SSRefraction", description="Screen space refractions", default=False, update=update_renderpath)
rp_ssgi: EnumProperty( rp_ssao: BoolProperty(name="SSAO", description="Screen space ambient occlusion", default=False, update=update_renderpath)
items=[('Off', 'No AO', 'Off'), rp_ssgi: BoolProperty(name="SSGI", description="Screen space global illumination", default=False, update=update_renderpath)
('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_bloom: BoolProperty(name="Bloom", description="Bloom processing", 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) lnx_bloom_follow_blender: BoolProperty(name="Use Blender Settings", description="Use Blender settings instead of Leenkx settings", default=True)
rp_motionblur: EnumProperty( 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_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_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_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_ssao_strength: FloatProperty(name="Strength", default=1.0, update=assets.invalidate_shader_cache)
lnx_ssgi_radius: FloatProperty(name="Radius", default=0.750, 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_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_samples: IntProperty(name="Samples", default=16, 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_half_res: BoolProperty(name="Half Res", description="Trace in half resolution", default=False, 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_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) 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)

View File

@ -385,6 +385,7 @@ class LNX_PT_WorldPropsPanel(bpy.types.Panel):
layout.prop(world, 'lnx_light_ies_texture') layout.prop(world, 'lnx_light_ies_texture')
layout.prop(world, 'lnx_light_clouds_texture') layout.prop(world, 'lnx_light_clouds_texture')
layout.separator()
layout.prop(world, 'lnx_use_clouds') layout.prop(world, 'lnx_use_clouds')
col = layout.column(align=True) col = layout.column(align=True)
col.enabled = world.lnx_use_clouds col.enabled = world.lnx_use_clouds
@ -1962,6 +1963,9 @@ class LNX_PT_RenderPathPostProcessPanel(bpy.types.Panel):
col = layout.column() col = layout.column()
col.prop(rpdat, "rp_antialiasing") col.prop(rpdat, "rp_antialiasing")
col.prop(rpdat, "rp_supersampling") 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 = layout.column()
col.prop(rpdat, 'lnx_rp_resolution') col.prop(rpdat, 'lnx_rp_resolution')
@ -1971,12 +1975,21 @@ class LNX_PT_RenderPathPostProcessPanel(bpy.types.Panel):
col.prop(rpdat, 'rp_dynres') col.prop(rpdat, 'rp_dynres')
layout.separator() 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 = layout.column()
col.prop(rpdat, "rp_ssgi") col.prop(rpdat, "rp_ssgi")
sub = col.column() 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_half_res')
#sub.prop(rpdat, 'lnx_ssgi_rays')
sub.prop(rpdat, 'lnx_ssgi_radius') sub.prop(rpdat, 'lnx_ssgi_radius')
sub.prop(rpdat, 'lnx_ssgi_strength') sub.prop(rpdat, 'lnx_ssgi_strength')
sub.prop(rpdat, 'lnx_ssgi_samples') sub.prop(rpdat, 'lnx_ssgi_samples')
@ -2880,10 +2893,10 @@ class LNX_PT_PhysicsProps(bpy.types.Panel):
layout.use_property_decorate = False layout.use_property_decorate = False
wrd = bpy.data.worlds['Lnx'] 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 = layout.row()
row.alert = True 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 = layout.column(align=False)
col.prop(wrd, "lnx_physics_fixed_step") col.prop(wrd, "lnx_physics_fixed_step")
@ -2906,10 +2919,10 @@ class LNX_PT_PhysicsDebugDrawingPanel(bpy.types.Panel):
layout.use_property_decorate = False layout.use_property_decorate = False
wrd = bpy.data.worlds['Lnx'] 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 = layout.row()
row.alert = True 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 = layout.column(align=False)
col.prop(wrd, "lnx_physics_dbg_draw_wireframe") col.prop(wrd, "lnx_physics_dbg_draw_wireframe")

File diff suppressed because it is too large Load Diff

View File

@ -154,6 +154,17 @@ project.addSources('Sources');
assets.add_khafile_def('lnx_oimo') assets.add_khafile_def('lnx_oimo')
if not os.path.exists('Libraries/oimo'): if not os.path.exists('Libraries/oimo'):
khafile.write(add_leenkx_library(sdk_path + '/lib/', 'oimo', rel_path=do_relpath_sdk)) 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: if export_navigation:
assets.add_khafile_def('lnx_navigation') assets.add_khafile_def('lnx_navigation')
@ -209,6 +220,8 @@ project.addSources('Sources');
if wrd.lnx_render_viewport: if wrd.lnx_render_viewport:
assets.add_khafile_def('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)) import_traits = list(set(import_traits))
for i in range(0, len(import_traits)): for i in range(0, len(import_traits)):
khafile.write("project.addParameter('" + import_traits[i] + "');\n") 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_msaa': int(rpdat.lnx_samples_per_pixel),
'window_scale': 1.0, 'window_scale': 1.0,
'rp_supersample': float(rpdat.rp_supersampling), '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_cube': rp_shadowmap_cube,
'rp_shadowmap_cascade': rp_shadowmap_cascade, '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_ssr': rpdat.rp_ssr != 'Off',
'rp_ss_refraction': rpdat.rp_ss_refraction != 'Off', 'rp_ss_refraction': rpdat.rp_ss_refraction != 'Off',
'rp_bloom': rpdat.rp_bloom != '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' 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( f.write(
"""const float ssaoRadius = """ + str(round(rpdat.lnx_ssgi_radius * 100) / 100) + """; """const float ssaoRadius = """ + str(round(rpdat.lnx_ssao_radius * 100) / 100) + """;
const float ssaoStrength = """ + str(round(rpdat.lnx_ssgi_strength * 100) / 100) + """; const float ssaoStrength = """ + str(round(rpdat.lnx_ssao_strength * 100) / 100) + """;
const float ssaoScale = """ + ("2.0" if rpdat.lnx_ssgi_half_res else "20.0") + """; 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( f.write(
"""const int ssgiSamples = """ + str(rpdat.lnx_ssgi_samples) + """; """const int ssgiSamples = """ + str(rpdat.lnx_ssgi_samples) + """;
const float ssgiRayStep = 0.005 * """ + str(round(rpdat.lnx_ssgi_step * 100) / 100) + """; 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 ssgiStrength = """ + str(round(rpdat.lnx_ssgi_strength * 100) / 100) + """;
const float ssgiRadius = """ + str(round(rpdat.lnx_ssgi_radius * 100) / 100) + """;
""") """)
if rpdat.rp_bloom: 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 voxelgiRefl = """ + str(round(rpdat.lnx_voxelgi_spec * 100) / 100) + """;
const float voxelgiRefr = """ + str(round(rpdat.lnx_voxelgi_refr * 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") f.write(f"const float sssWidth = {rpdat.lnx_sss_width / 10.0};\n")
# Skinning # Skinning

View File

@ -20,6 +20,7 @@ import lnx.props_camera_render_filter
import lnx.handlers import lnx.handlers
import lnx.utils import lnx.utils
import lnx.keymap import lnx.keymap
import lnx.render_engine
reload_started = 0 reload_started = 0
@ -49,6 +50,7 @@ if lnx.is_reload(__name__):
lnx.handlers = lnx.reload_module(lnx.handlers) lnx.handlers = lnx.reload_module(lnx.handlers)
lnx.utils = lnx.reload_module(lnx.utils) lnx.utils = lnx.reload_module(lnx.utils)
lnx.keymap = lnx.reload_module(lnx.keymap) lnx.keymap = lnx.reload_module(lnx.keymap)
lnx.render_engine = lnx.reload_module(lnx.render_engine)
else: else:
lnx.enable_reload(__name__) lnx.enable_reload(__name__)
@ -76,6 +78,7 @@ def register(local_sdk=False):
lnx.keymap.register() lnx.keymap.register()
lnx.handlers.register() lnx.handlers.register()
lnx.props_collision_filter_mask.register() lnx.props_collision_filter_mask.register()
lnx.render_engine.register()
lnx.handlers.post_register() lnx.handlers.post_register()
@ -104,3 +107,4 @@ def unregister():
lnx.props_properties.unregister() lnx.props_properties.unregister()
lnx.props_collision_filter_mask.unregister() lnx.props_collision_filter_mask.unregister()
lnx.props_camera_render_filter.unregister() lnx.props_camera_render_filter.unregister()
lnx.render_engine.unregister()