Consolidate and update

This commit is contained in:
2026-10-27 15:58:22 -07:00
parent eb211da457
commit 6c13722fc6
42 changed files with 498 additions and 169 deletions

View File

@ -2724,6 +2724,8 @@ class LeenkxExporter:
self.extract_projection(o, proj)
else:
self.extract_ortho(o, proj)
if par_x == par_y:
o.pop('aspect', None)
o['frustum_culling'] = objref.lnx_frustum_culling
o['clear_color'] = self.get_camera_clear_color()
@ -3124,19 +3126,20 @@ class LeenkxExporter:
def export_world(self):
"""Exports the world of the current scene."""
for world in bpy.data.worlds:
if world is not None:
world_name = lnx.utils.safestr(lnx.utils.asset_name(world) if world.library else world.name)
world = self.scene.world
if world_name not in self.world_array:
self.world_array.append(world_name)
out_world = {'name': world_name}
if world is not None:
world_name = lnx.utils.safestr(lnx.utils.asset_name(world) if world.library else world.name)
self.post_export_world(world, out_world)
self.output['world_datas'].append(out_world)
if world_name not in self.world_array:
self.world_array.append(world_name)
out_world = {'name': world_name}
elif lnx.utils.get_rp().rp_background == 'World':
log.warn(f'Scene "{self.scene.name}" is missing a world, some render targets will not be cleared')
self.post_export_world(world, out_world)
self.output['world_datas'].append(out_world)
elif lnx.utils.get_rp().rp_background == 'World':
log.warn(f'Scene "{self.scene.name}" is missing a world, some render targets will not be cleared')
def export_objects(self, scene):
"""Exports all supported blender objects.
@ -3397,7 +3400,8 @@ class LeenkxExporter:
else:
self.extract_ortho(out_camera, proj)
out_camera['aspect'] = effective_aspect
if par_x != par_y:
out_camera['aspect'] = effective_aspect
self.output['camera_datas'].append(out_camera)

View File

@ -79,7 +79,7 @@ class MergeNode(LnxLogicTreeNode):
# Recreate all original inputs
array_nodes[str(id(newnode))] = newnode
for idx, input in enumerate(self.inputs):
bpy.ops.lnx.node_add_input('EXEC_DEFAULT', node_index=str(id(newnode)), socket_type='LnxNodeSocketAction')
newnode.add_input('LnxNodeSocketAction', f'Input {idx}')
for link in input.links:
node_tree.links.new(link.from_socket, newnode.inputs[idx])

View File

@ -278,7 +278,8 @@ def replace_all():
log.error(f'There were errors in the node update procedure, a detailed report has been written to {reportfile}')
bpy.ops.lnx.show_node_update_errors()
if not bpy.app.background:
bpy.ops.lnx.show_node_update_errors()
def node_compat_sdk2108():

View File

@ -232,8 +232,7 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha
wrd.world_defs += '_EnvClouds'
frag_write_clouds(world, frag)
if '_EnvSky' in world.world_defs or '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs or '_EnvCol' in world.world_defs or '_EnvClouds' in world.world_defs:
frag.add_uniform('float envmapStrength', link='_envmapStrength')
frag.add_uniform('float envmapStrength', link='_envmapStrength')
# Clear background color
if '_EnvCol' in world.world_defs:
@ -242,7 +241,6 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha
else:
if '_EnvTex' in world.world_defs and '_EnvLDR' in world.world_defs:
frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(2.2));')
frag.write('fragColor.rgb *= envmapStrength;')
if '_EnvClouds' in world.world_defs:
frag.write('if (pos.z > 0.0) fragColor.rgb = mix(fragColor.rgb, traceClouds(fragColor.rgb, pos), clamp(pos.z * 5.0, 0, 1));')

View File

@ -3,12 +3,14 @@ import bpy
import lnx.material.cycles as cycles
import lnx.material.mat_state as mat_state
import lnx.material.make_finalize as make_finalize
import lnx.material.make_mesh as make_mesh
import lnx.utils
if lnx.is_reload(__name__):
cycles = lnx.reload_module(cycles)
mat_state = lnx.reload_module(mat_state)
make_finalize = lnx.reload_module(make_finalize)
make_mesh = lnx.reload_module(make_mesh)
lnx.utils = lnx.reload_module(lnx.utils)
else:
lnx.enable_reload(__name__)
@ -64,14 +66,8 @@ def make(context_id):
frag.write_attrib(' if (abs(mpos.z) > 1.0) discard;')
frag.write_attrib(' vec2 texCoord = mpos.xy * 0.5 + 0.5;')
frag.write('vec3 basecol;')
frag.write('float roughness;')
frag.write('float metallic;')
frag.write('float occlusion;')
frag.write('float specular;')
frag.write('float opacity;')
frag.write('vec3 emissionCol;') # Declared to prevent compiler errors, but decals currently don't output any emission
frag.write('float ior;')
# Declared to prevent compiler errors, but decals currently don't output any emission
make_mesh._write_material_attribs_default(frag, parse_opacity=True)
cycles.parse(mat_state.nodes, con_decal, vert, frag, geom, tesc, tese)
frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));')

View File

@ -241,8 +241,7 @@ def make(context_id, rpasses, shadowmap=False, shadowmap_transparent=False):
vert.add_out('vec4 wvpposition')
vert.write('wvpposition = gl_Position;')
frag.write('float depth = (wvpposition.z / wvpposition.w) * 0.5 + 0.5;')
frag.write('vec3 color = basecol;')
frag.write('color *= 1.0 - opacity;')
frag.write('vec3 color = mix(vec3(1.0), basecol, opacity);')
frag.write('fragColor = vec4(color, depth);')
if parse_opacity and not shadowmap_transparent:

View File

@ -398,11 +398,14 @@ def make_forward_mobile(con_mesh):
return
is_shadows = '_ShadowMap' in wrd.world_defs
is_transparent_shadows = '_ShadowMapTransparent' in wrd.world_defs
is_shadows_atlas = '_ShadowMapAtlas' in wrd.world_defs
shadowmap_sun = 'shadowMap'
shadowmap_sun_tr = 'shadowMapTransparent'
if is_shadows_atlas:
is_single_atlas = '_SingleAtlas' in wrd.world_defs
shadowmap_sun = 'shadowMapAtlasSun' if not is_single_atlas else 'shadowMapAtlas'
shadowmap_sun_tr = 'shadowMapAtlasSunTransparent' if not is_single_atlas else 'shadowMapAtlasTransparent'
frag.add_uniform('vec2 smSizeUniform', '_shadowMapSize', included=True)
frag.write('vec3 direct = vec3(0.0);')
@ -417,6 +420,8 @@ def make_forward_mobile(con_mesh):
vert.write('lightPosition = LWVP * spos;')
frag.add_uniform('bool receiveShadow')
frag.add_uniform(f'sampler2DShadow {shadowmap_sun}')
if is_transparent_shadows:
frag.add_uniform(f'sampler2D {shadowmap_sun_tr}')
frag.add_uniform('float shadowsBias', '_sunShadowsBias')
frag.write('if (receiveShadow) {')
@ -427,14 +432,20 @@ def make_forward_mobile(con_mesh):
if is_shadows_atlas:
frag.add_uniform('vec4 tileBoundsSunArray[maxLights * shadowmapCascades]', '_tileBoundsSunArray', included=True)
frag.write('tileBounds = tileBoundsSunArray[0];')
frag.write(f'svisibility = shadowTestCascade({shadowmap_sun}, eye, wposition + n * shadowsBias * 10, shadowsBias, opacity != 1.0);')
frag.write(f'svisibility = shadowTestCascade({shadowmap_sun},')
if is_transparent_shadows:
frag.write(f'{shadowmap_sun_tr},')
frag.write('eye, wposition + n * shadowsBias * 10, shadowsBias')
if is_transparent_shadows:
frag.write(', opacity != 1.0')
frag.write(');')
else:
frag.write('if (lightPosition.w > 0.0) {')
frag.write(' vec3 lPos = lightPosition.xyz / lightPosition.w;')
if '_Legacy' in wrd.world_defs:
frag.write(f' svisibility = float(texture({shadowmap_sun}, vec2(lPos.xy)).r > lPos.z - shadowsBias, opacity != 1.0);')
frag.write(f' svisibility = vec3(float(texture({shadowmap_sun}, vec2(lPos.xy)).r > lPos.z - shadowsBias));')
else:
frag.write(f' svisibility = texture({shadowmap_sun}, vec3(lPos.xy, lPos.z - shadowsBias), opacity != 1.0).r;')
frag.write(f' svisibility = vec3(texture({shadowmap_sun}, vec3(lPos.xy, lPos.z - shadowsBias)).r);')
frag.write('}')
frag.write('}') # receiveShadow
frag.write('direct += basecol * sdotNL * sunCol * svisibility;')

View File

@ -80,9 +80,6 @@ def write(vert, particle_info=None, shadowmap=False):
vert.add_const('float', 'P_NORMAL_FACTOR', str(normal_factor))
if (shadowmap and normal_factor != 0.0):
vert.add_out('vec3 N')
if ramp_el_len != 0:
vert.add_const('float', 'P_SIZE_OVER_TIME_FACTOR', str(size_over_time_factor))
@ -156,6 +153,7 @@ def write(vert, particle_info=None, shadowmap=False):
vert.write('p_velocity.z += (fhash(gl_InstanceID + pd_random + 2 * pd[0][3]) * 2.0 / pd_size - 1.0 / pd_size) * pd[1][3];')
if normal_factor != 0.0:
vert.add_uniform('mat3 N', '_normalMatrix')
vert.write('if (P_NORMAL_FACTOR != 0.0) {')
vert.write(' vec3 local_normal_dir = normalize(ipos);')
vert.write(' vec3 world_emitter_normal = normalize(N * local_normal_dir);')

View File

@ -123,6 +123,8 @@ def set_socket_default(socket: NodeSocket, value: Any):
"""Set the socket's default value if it exists."""
if isinstance(socket, lnx.logicnode.lnx_sockets.LnxCustomSocket):
if socket.lnx_socket_type != 'NONE':
if socket.lnx_socket_type == 'INT' and isinstance(value, float):
value = int(value)
socket.default_value_raw = value
# Shader-type sockets don't have a default value

View File

@ -31,7 +31,7 @@ else:
lnx.enable_reload(__name__)
# Leenkx version
lnx_version = '2026.9'
lnx_version = '2026.10'
lnx_commit = '$Id: 6b2644d47db169cedd95593497cc283207d23a74 $'
LNX_TORRENT_DEFAULT_TRACKERS = [
@ -382,6 +382,27 @@ def init_properties():
('Hashlink', 'Hashlink', 'Hashlink')],
name="Runtime", description="Runtime to use when launching the game", default='Krom', update=assets.invalidate_shader_cache)
bpy.types.World.lnx_loadscreen = BoolProperty(name="Loading Screen", description="Show asset loading progress on published builds", default=True)
bpy.types.World.lnx_loadscreen_mode = EnumProperty(
items=[('Image', 'Image', 'Background image with a progress bar'),
('Simple', 'Simple', 'Background color with a progress bar')],
name="Loading Screen Mode", description="What the loading screen displays behind the progress bar", default='Image', update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_loadscreen_bgcolor = FloatVectorProperty(name="Loading Background Color", description="Background color shown behind the loading bar and image", default=(0.0, 0.165, 0.208, 1.0), min=0.0, max=1.0, subtype='COLOR', size=4, update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_loadscreen_image = StringProperty(name="Loading Screen Image", description="Background image shown behind the loading bar, leave as 'Default' to use the Leenkx logo wallpaper", default="Default", subtype="FILE_PATH", update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_loadscreen_fit = EnumProperty(
items=[('TopLeft', 'Top Left', 'Draw the image at its native size anchored to the top left'),
('TopCenter', 'Top Center', 'Draw the image at its native size anchored to the top center'),
('TopRight', 'Top Right', 'Draw the image at its native size anchored to the top right'),
('CenterLeft', 'Center Left', 'Draw the image at its native size anchored to the center left'),
('Center', 'Center', 'Draw the image at its native size centered on screen'),
('CenterRight', 'Center Right', 'Draw the image at its native size anchored to the center right'),
('BottomLeft', 'Bottom Left', 'Draw the image at its native size anchored to the bottom left'),
('BottomCenter', 'Bottom Center', 'Draw the image at its native size anchored to the bottom center'),
('BottomRight', 'Bottom Right', 'Draw the image at its native size anchored to the bottom right'),
('Contain', 'Contain', 'Scale the image to fit inside the screen, preserving aspect ratio'),
('Stretch', 'Stretch to Fit', 'Stretch the image to fill the screen, ignoring aspect ratio')],
name="Image Fit", description="How the loading screen image is positioned on screen", default='Center', update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_loadscreen_show_percentage = BoolProperty(name="Loading Screen Percentage", description="Draw the loading progress percentage above the progress bar", default=False, update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_loadscreen_color = FloatVectorProperty(name="Loading Bar Color", description="Color of the loading progress bar and percentage text", default=(0.1647, 0.8784, 0.8784, 1.0), min=0.0, max=1.0, subtype='COLOR', size=4)
bpy.types.World.lnx_vsync = BoolProperty(name="VSync", description="Vertical Synchronization", default=True, update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_dce = BoolProperty(name="DCE", description="Enable dead code elimination", default=False, update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_asset_compression = BoolProperty(name="Asset Compression", description="Enable scene data compression with LZ4. Warning: This will slow down export!", default=False, update=assets.invalidate_compiler_cache)
@ -763,8 +784,8 @@ def update_leenkx_world():
# This allows for seamless migration from earlier versions of Leenkx
for rp in wrd.lnx_rplist: # TODO: deprecated
if hasattr(rp, 'rp_gi') and rp.rp_gi != 'Off':
rp.rp_gi = 'Off'
rp.rp_voxels = rp.rp_gi
rp.rp_gi = 'Off'
# For some breaking changes we need to use a special update
# routine first before regularly replacing nodes

View File

@ -168,7 +168,7 @@ def update_preset(self, context):
rpdat.rp_compositornodes = True
rpdat.rp_volumetriclight = False
rpdat.rp_ssao = True
rpdat.rp_ssgi = True
rpdat.rp_ssgi = False
rpdat.lnx_ssrs = True
rpdat.lnx_micro_shadowing = True
rpdat.rp_ssr = True

View File

@ -1263,7 +1263,6 @@ class LNX_PT_ProjectFlagsPanel(bpy.types.Panel):
col = column_with_heading(layout, 'Runtime', align=True)
col.prop(wrd, 'lnx_live_patch')
col.prop(wrd, 'lnx_stream_scene')
col.prop(wrd, 'lnx_loadscreen')
col.prop(wrd, 'lnx_webview')
col.prop(wrd, 'lnx_write_config')
@ -1289,6 +1288,33 @@ class LNX_PT_ProjectFlagsPanel(bpy.types.Panel):
col.prop(wrd, 'lnx_copy_override')
col.operator('lnx.copy_to_bundled', icon='IMAGE_DATA')
class LNX_PT_ProjectFlagsLoadscreenPanel(bpy.types.Panel):
bl_label = "Loading Screen"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "render"
bl_options = {'DEFAULT_CLOSED'}
bl_parent_id = "LNX_PT_ProjectFlagsPanel"
def draw_header(self, context):
wrd = bpy.data.worlds['Lnx']
self.layout.prop(wrd, 'lnx_loadscreen', text='')
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
wrd = bpy.data.worlds['Lnx']
col = layout.column()
col.enabled = wrd.lnx_loadscreen
col.prop(wrd, 'lnx_loadscreen_mode')
col.prop(wrd, 'lnx_loadscreen_bgcolor')
col.prop(wrd, 'lnx_loadscreen_color')
if wrd.lnx_loadscreen_mode == 'Image':
col.prop(wrd, 'lnx_loadscreen_image')
col.prop(wrd, 'lnx_loadscreen_fit')
col.prop(wrd, 'lnx_loadscreen_show_percentage')
class LNX_PT_ProjectFlagsDebugConsolePanel(bpy.types.Panel):
bl_label = "Debug Console"
bl_space_type = "PROPERTIES"
@ -3017,9 +3043,9 @@ class LNX_OT_UpdateFileSDK(bpy.types.Operator):
wrd = bpy.data.worlds['Lnx']
# This allows for seamless migration from earlier versions of Leenkx
for rp in wrd.lnx_rplist: # TODO: deprecated
if rp.rp_gi != 'Off':
if hasattr(rp, 'rp_gi') and rp.rp_gi != 'Off':
rp.rp_voxels = rp.rp_gi
rp.rp_gi = 'Off'
rp.rp_voxelao = True
# Replace deprecated nodes
lnx.logicnode.replacement.replace_all()
@ -3265,6 +3291,7 @@ __REG_CLASSES = (
LNX_PT_LeenkxExporterWindowsSettingsPanel,
LNX_PT_LeenkxProjectPanel,
LNX_PT_ProjectFlagsPanel,
LNX_PT_ProjectFlagsLoadscreenPanel,
LNX_PT_ProjectFlagsDebugConsolePanel,
LNX_PT_ProjectWindowPanel,
LNX_PT_ProjectModulesPanel,

View File

@ -539,6 +539,12 @@ let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' +
if wrd.lnx_loadscreen:
assets.add_khafile_def('lnx_loadscreen')
custom_loadscreen = os.path.isfile(lnx.utils.get_fp() + '/Sources/' + wrd.lnx_project_package + '/LoadingScreen.hx')
if wrd.lnx_loadscreen_mode == 'Image' or custom_loadscreen:
loadscreen_image = bpy.path.abspath(wrd.lnx_loadscreen_image) if wrd.lnx_loadscreen_image else ''
if loadscreen_image == '' or not os.path.isfile(loadscreen_image):
loadscreen_image = os.path.join(sdk_path, 'leenkx', 'blender', 'theme', 'splash.png')
khafile.write(add_assets(loadscreen_image.replace('\\', '/'), use_data_dir=use_data_dir, rel_path=do_relpath_sdk))
if wrd.lnx_winresize or state.target == 'html5':
assets.add_khafile_def('lnx_resizable')
@ -742,6 +748,20 @@ class Main {
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;")
f.write("\n" + content_indent + "leenkx.system.Starter.assets = " + str([os.path.basename(path) for path in asset_references if 'Bundled' in path]) + ";")
if loadscreen_class == 'leenkx.trait.internal.LoadingScreen':
if wrd.lnx_loadscreen_mode == 'Image':
li = bpy.path.abspath(wrd.lnx_loadscreen_image) if wrd.lnx_loadscreen_image else ''
if li == '' or not os.path.isfile(li):
li = os.path.join(lnx.utils.get_sdk_path(), 'leenkx', 'blender', 'theme', 'splash.png')
f.write("\n" + content_indent + loadscreen_class + ".bgImageName = '" + os.path.basename(li) + "';")
f.write("\n" + content_indent + loadscreen_class + ".imageFit = '" + wrd.lnx_loadscreen_fit + "';")
else:
f.write("\n" + content_indent + loadscreen_class + ".bgImageName = '';")
c = wrd.lnx_loadscreen_color
f.write("\n" + content_indent + loadscreen_class + ".barColor = " + hex(0xff000000 + (int(c[0] * 255) << 16) + (int(c[1] * 255) << 8) + int(c[2] * 255)) + ";")
c = wrd.lnx_loadscreen_bgcolor
f.write("\n" + content_indent + loadscreen_class + ".bgColor = " + hex(0xff000000 + (int(c[0] * 255) << 16) + (int(c[1] * 255) << 8) + int(c[2] * 255)) + ";")
f.write("\n" + content_indent + loadscreen_class + ".showPercentage = " + ('true' if wrd.lnx_loadscreen_show_percentage else 'false') + ";")
scenes_list = [f"'{scene.name}'" for scene in bpy.data.scenes if getattr(scene, 'lnx_export', True)]
scenes_array = "[" + ", ".join(scenes_list) + "]"
@ -819,7 +839,7 @@ def write_indexhtml(w, h, is_publish):
<title>"""+html.escape( wrd.lnx_project_name)+"""</title>
"""+icon+"""
</head>
<body style='margin: 0; padding: 0'>
<body style='margin: 0; padding: 0; background: #001A22'>
""")
if rpdat.rp_stereo or wrd.lnx_winmode == 'Fullscreen':
f.write("""

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB