forked from LeenkxTeam/LNXSDK
Repe [T3DU] and Moises Jpelaez updates
This commit is contained in:
@ -55,6 +55,16 @@ def reset():
|
||||
shader_cons['voxel_frag'] = []
|
||||
shader_cons['voxel_geom'] = []
|
||||
|
||||
def reset_shader_cons():
|
||||
# Reset shader comparison arrays to prevent cross-scene shader merging
|
||||
global shader_cons
|
||||
shader_cons['mesh_vert'] = []
|
||||
shader_cons['depth_vert'] = []
|
||||
shader_cons['depth_frag'] = []
|
||||
shader_cons['voxel_vert'] = []
|
||||
shader_cons['voxel_frag'] = []
|
||||
shader_cons['voxel_geom'] = []
|
||||
|
||||
def add(asset_file):
|
||||
global assets
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -133,7 +133,7 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec
|
||||
# Shape keys UV are exported separately, so reduce UV count by 1
|
||||
num_uv_layers -= 1
|
||||
morph_uv_index = self.get_morph_uv_index(bobject.data)
|
||||
has_tex = self.get_export_uvs(export_mesh) or num_uv_layers > 0 # TODO FIXME: this should use an `and` instead of `or`. Workaround to completely ignore if the mesh has the `export_uvs` flag. Only checking the `uv_layers` to bypass issues with materials in linked objects.
|
||||
has_tex = self.get_export_uvs(export_mesh) or num_uv_layers > 0 # FIXME: this should use an `and` instead of `or`. Workaround to completely ignore if the mesh has the `export_uvs` flag. Only checking the `uv_layers` to bypass issues with materials in linked objects.
|
||||
if self.has_baked_material(bobject, export_mesh.materials):
|
||||
has_tex = True
|
||||
has_tex1 = has_tex and num_uv_layers > 1
|
||||
@ -177,16 +177,16 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec
|
||||
for v in lay0.data:
|
||||
if abs(v.uv[0]) > maxdim:
|
||||
maxdim = abs(v.uv[0])
|
||||
if abs(v.uv[1]) > maxdim:
|
||||
maxdim = abs(v.uv[1])
|
||||
if abs(1.0 - v.uv[1]) > maxdim:
|
||||
maxdim = abs(1.0 - v.uv[1])
|
||||
if has_tex1:
|
||||
lay1 = uv_layers[t1map]
|
||||
for v in lay1.data:
|
||||
if abs(v.uv[0]) > maxdim:
|
||||
maxdim = abs(v.uv[0])
|
||||
maxdim_uvlayer = lay1
|
||||
if abs(v.uv[1]) > maxdim:
|
||||
maxdim = abs(v.uv[1])
|
||||
if abs(1.0 - v.uv[1]) > maxdim:
|
||||
maxdim = abs(1.0 - v.uv[1])
|
||||
maxdim_uvlayer = lay1
|
||||
if has_morph_target:
|
||||
morph_data = np.empty(num_verts * 2, dtype='<f4')
|
||||
@ -195,8 +195,8 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec
|
||||
if abs(v.uv[0]) > maxdim:
|
||||
maxdim = abs(v.uv[0])
|
||||
maxdim_uvlayer = lay2
|
||||
if abs(v.uv[1]) > maxdim:
|
||||
maxdim = abs(v.uv[1])
|
||||
if abs(1.0 - v.uv[1]) > maxdim:
|
||||
maxdim = abs(1.0 - v.uv[1])
|
||||
maxdim_uvlayer = lay2
|
||||
if maxdim > 1:
|
||||
o['scale_tex'] = maxdim
|
||||
|
||||
@ -344,7 +344,7 @@ def apply_materials(load_atlas=0):
|
||||
else:
|
||||
mat.node_tree.links.new(lightmapNode.outputs[0], mixNode.inputs[6]) #Connect lightmap node to mixnode
|
||||
mat.node_tree.links.new(baseColorNode.outputs[0], mixNode.inputs[7]) #Connect basecolor to pbr node
|
||||
mat.node_tree.links.new(mixNode.outputs[0], mainNode.inputs[2]) #Connect mixnode to pbr node
|
||||
mat.node_tree.links.new(mixNode.outputs[2], mainNode.inputs[0]) #Connect mixnode to pbr node
|
||||
if not scene.TLM_EngineProperties.tlm_target == "vertex":
|
||||
mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode
|
||||
|
||||
|
||||
@ -811,7 +811,7 @@ def set_settings():
|
||||
|
||||
print(bpy.app.version)
|
||||
|
||||
if bpy.app.version[0] == 3 or byp.app.version[0] == 4:
|
||||
if bpy.app.version[0] == 3 or bpy.app.version[0] == 4:
|
||||
if cycles.device == "GPU":
|
||||
scene.cycles.tile_size = 256
|
||||
else:
|
||||
|
||||
144
leenkx/blender/lnx/linked_utils.py
Normal file
144
leenkx/blender/lnx/linked_utils.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""Utilities for handling linked blend files in Leenkx exports."""
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
import bpy
|
||||
|
||||
import lnx
|
||||
|
||||
if lnx.is_reload(__name__):
|
||||
pass
|
||||
else:
|
||||
lnx.enable_reload(__name__)
|
||||
|
||||
|
||||
def is_linked(bdata) -> bool:
|
||||
"""Check if a data block is linked from an external library."""
|
||||
if bdata is None:
|
||||
return False
|
||||
return bdata.library is not None
|
||||
|
||||
|
||||
def get_library_name(bdata) -> Optional[str]:
|
||||
"""Get the library filename for a linked data block."""
|
||||
if bdata is None or bdata.library is None:
|
||||
return None
|
||||
return bdata.library.name
|
||||
|
||||
|
||||
def get_library_path(bdata) -> Optional[Path]:
|
||||
"""Get the absolute path to the library .blend file."""
|
||||
if bdata is None or bdata.library is None:
|
||||
return None
|
||||
return Path(bpy.path.abspath(bdata.library.filepath))
|
||||
|
||||
|
||||
def asset_name(bdata) -> Optional[str]:
|
||||
"""Get qualified asset name with library suffix for linked data."""
|
||||
if bdata is None:
|
||||
return None
|
||||
name = bdata.name
|
||||
if bdata.library is not None:
|
||||
name += '_' + bdata.library.name
|
||||
return name
|
||||
|
||||
|
||||
def get_source_path(bdata) -> Optional[Path]:
|
||||
"""Get Sources folder path for a linked data block's project."""
|
||||
lib_path = get_library_path(bdata)
|
||||
if lib_path is None:
|
||||
return None
|
||||
sources_path = lib_path.parent / 'Sources'
|
||||
if sources_path.exists() and sources_path.is_dir():
|
||||
return sources_path
|
||||
return None
|
||||
|
||||
|
||||
class TransformEvaluator:
|
||||
"""Context manager for evaluating linked object transforms (Blender 4.2+ workaround)."""
|
||||
|
||||
def __init__(self, bobject: bpy.types.Object, scene: bpy.types.Scene,
|
||||
depsgraph: bpy.types.Depsgraph):
|
||||
self.bobject = bobject
|
||||
self.scene = scene
|
||||
self.depsgraph = depsgraph
|
||||
self._temp_collection: Optional[bpy.types.Collection] = None
|
||||
self._evaluated_obj: Optional[bpy.types.Object] = None
|
||||
self._is_linked = False
|
||||
|
||||
def __enter__(self) -> 'TransformEvaluator':
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
self._is_linked = self.bobject.name not in self.scene.collection.children
|
||||
if self._is_linked:
|
||||
self._temp_collection = bpy.data.collections.new("_lnx_temp_eval")
|
||||
bpy.context.scene.collection.children.link(self._temp_collection)
|
||||
self._temp_collection.objects.link(self.bobject)
|
||||
temp_depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
self._evaluated_obj = self.bobject.evaluated_get(temp_depsgraph)
|
||||
else:
|
||||
self._evaluated_obj = self.bobject.evaluated_get(self.depsgraph)
|
||||
else:
|
||||
self._evaluated_obj = self.bobject
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self._is_linked and self._temp_collection is not None:
|
||||
try:
|
||||
self._temp_collection.objects.unlink(self.bobject)
|
||||
bpy.context.scene.collection.children.unlink(self._temp_collection)
|
||||
bpy.data.collections.remove(self._temp_collection)
|
||||
except Exception:
|
||||
pass
|
||||
self._temp_collection = None
|
||||
return False
|
||||
|
||||
@property
|
||||
def evaluated_object(self) -> bpy.types.Object:
|
||||
return self._evaluated_obj
|
||||
|
||||
@property
|
||||
def matrix_local(self):
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
return self._evaluated_obj.matrix_local.copy()
|
||||
return self.bobject.matrix_local
|
||||
|
||||
|
||||
@contextmanager
|
||||
def evaluated_mesh(bobject: bpy.types.Object, scene: bpy.types.Scene,
|
||||
depsgraph: bpy.types.Depsgraph, apply_modifiers: bool = True):
|
||||
"""Context manager for mesh export with Blender 4.2+ linked object workaround."""
|
||||
temp_collection = None
|
||||
is_linked = False
|
||||
|
||||
try:
|
||||
if apply_modifiers and bpy.app.version >= (4, 2, 0):
|
||||
is_linked = bobject.name not in scene.collection.children
|
||||
if is_linked:
|
||||
temp_collection = bpy.data.collections.new("_lnx_temp_mesh_eval")
|
||||
bpy.context.scene.collection.children.link(temp_collection)
|
||||
temp_collection.objects.link(bobject)
|
||||
|
||||
temp_depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
bobject_eval = bobject.evaluated_get(temp_depsgraph)
|
||||
yield bobject_eval, temp_depsgraph
|
||||
|
||||
finally:
|
||||
if is_linked and temp_collection is not None:
|
||||
try:
|
||||
temp_collection.objects.unlink(bobject)
|
||||
bpy.context.scene.collection.children.unlink(temp_collection)
|
||||
bpy.data.collections.remove(temp_collection)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def discover_linked_sources() -> Dict[str, Path]:
|
||||
"""Discover Sources folders from all linked libraries."""
|
||||
sources: Dict[str, Path] = {}
|
||||
for lib in bpy.data.libraries:
|
||||
lib_path = Path(bpy.path.abspath(lib.filepath))
|
||||
sources_path = lib_path.parent / 'Sources'
|
||||
if sources_path.exists() and sources_path.is_dir():
|
||||
sources[lib.name] = sources_path
|
||||
return sources
|
||||
@ -28,6 +28,8 @@ def init_categories():
|
||||
lnx_nodes.add_category('Logic', icon='OUTLINER', section="basic",
|
||||
description="Logic nodes are used to control execution flow using branching, loops, gates etc.")
|
||||
lnx_nodes.add_category('Event', icon='INFO', section="basic")
|
||||
lnx_nodes.add_category('Signal', icon='LINKED', section="basic",
|
||||
description="Signal nodes provide type-safe, instance-based event communication between traits.")
|
||||
lnx_nodes.add_category('Input', icon='GREASEPENCIL', section="basic")
|
||||
lnx_nodes.add_category('Native', icon='MEMORY', section="basic",
|
||||
description="The Native category contains nodes which interact with the system (Input/Output functionality, etc.) or Haxe.")
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
class GetTilesheetFlipNode(LnxLogicTreeNode):
|
||||
"""Returns the flip state of the tilesheet.
|
||||
|
||||
@output Flip X: Whether the sprite is flipped horizontally.
|
||||
@output Flip Y: Whether the sprite is flipped vertically.
|
||||
"""
|
||||
bl_idname = 'LNGetTilesheetFlipNode'
|
||||
bl_label = 'Get Tilesheet Flip'
|
||||
lnx_version = 1
|
||||
lnx_section = 'tilesheet'
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketObject', 'Object')
|
||||
|
||||
self.add_output('LnxBoolSocket', 'Flip X')
|
||||
self.add_output('LnxBoolSocket', 'Flip Y')
|
||||
@ -2,8 +2,8 @@ from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
class GetTilesheetStateNode(LnxLogicTreeNode):
|
||||
"""Returns the information about the current tilesheet of the given object.
|
||||
|
||||
@output Active Tilesheet: Current active tilesheet.
|
||||
|
||||
@output Tilesheet: Tilesheet name.
|
||||
|
||||
@output Active Action: Current action in the tilesheet.
|
||||
|
||||
@ -15,23 +15,28 @@ class GetTilesheetStateNode(LnxLogicTreeNode):
|
||||
"""
|
||||
bl_idname = 'LNGetTilesheetStateNode'
|
||||
bl_label = 'Get Tilesheet State'
|
||||
lnx_version = 2
|
||||
lnx_version = 4
|
||||
lnx_section = 'tilesheet'
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketObject', 'Object')
|
||||
|
||||
self.add_output('LnxStringSocket', 'Active Tilesheet')
|
||||
self.add_output('LnxStringSocket', 'Tilesheet')
|
||||
self.add_output('LnxStringSocket', 'Active Action')
|
||||
self.add_output('LnxIntSocket', 'Frame')
|
||||
self.add_output('LnxIntSocket', 'Absolute Frame')
|
||||
self.add_output('LnxBoolSocket', 'Is Paused')
|
||||
|
||||
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
|
||||
if self.lnx_version not in (0, 1):
|
||||
raise LookupError()
|
||||
|
||||
return NodeReplacement(
|
||||
'LNGetTilesheetStateNode', self.lnx_version, 'LNGetTilesheetStateNode', 2,
|
||||
in_socket_mapping={}, out_socket_mapping={0:1, 1:3, 2:4}
|
||||
)
|
||||
if self.lnx_version in (0, 1):
|
||||
return NodeReplacement(
|
||||
'LNGetTilesheetStateNode', self.lnx_version, 'LNGetTilesheetStateNode', 4,
|
||||
in_socket_mapping={}, out_socket_mapping={0: 1, 1: 3, 2: 4}
|
||||
)
|
||||
elif self.lnx_version in (2, 3):
|
||||
# Version 2 and 3 have same outputs, just rename Material to Tilesheet
|
||||
return NodeReplacement(
|
||||
'LNGetTilesheetStateNode', self.lnx_version, 'LNGetTilesheetStateNode', 4,
|
||||
in_socket_mapping={0: 0}, out_socket_mapping={0: 0, 1: 1, 2: 2, 3: 3, 4: 4}
|
||||
)
|
||||
raise LookupError()
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
class PlayTilesheetNode(LnxLogicTreeNode):
|
||||
class PlayTilesheetActionNode(LnxLogicTreeNode):
|
||||
"""Plays the given tilesheet action."""
|
||||
bl_idname = 'LNPlayTilesheetNode'
|
||||
bl_label = 'Play Tilesheet'
|
||||
bl_idname = 'LNPlayTilesheetActionNode'
|
||||
bl_label = 'Play Tilesheet Action'
|
||||
lnx_version = 1
|
||||
lnx_section = 'tilesheet'
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxNodeSocketObject', 'Object')
|
||||
self.add_input('LnxStringSocket', 'Name')
|
||||
self.add_input('LnxStringSocket', 'Action')
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
self.add_output('LnxNodeSocketAction', 'Done')
|
||||
@ -1,16 +1,15 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
class SetActiveTilesheetNode(LnxLogicTreeNode):
|
||||
"""Set the active tilesheet."""
|
||||
bl_idname = 'LNSetActiveTilesheetNode'
|
||||
bl_label = 'Set Active Tilesheet'
|
||||
class SetTilesheetActionNode(LnxLogicTreeNode):
|
||||
"""Sets the tilesheet action for the given object."""
|
||||
bl_idname = 'LNSetTilesheetActionNode'
|
||||
bl_label = 'Set Tilesheet Action'
|
||||
lnx_version = 1
|
||||
lnx_section = 'tilesheet'
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxNodeSocketObject', 'Object')
|
||||
self.add_input('LnxStringSocket', 'Tilesheet')
|
||||
self.add_input('LnxStringSocket', 'Action')
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
@ -0,0 +1,21 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
class SetTilesheetFlipNode(LnxLogicTreeNode):
|
||||
"""Set the flip state of the tilesheet for UV-based sprite flipping.
|
||||
This is useful for billboarded sprites where mesh scaling cannot be used.
|
||||
|
||||
@input Flip X: Flip the sprite horizontally.
|
||||
@input Flip Y: Flip the sprite vertically.
|
||||
"""
|
||||
bl_idname = 'LNSetTilesheetFlipNode'
|
||||
bl_label = 'Set Tilesheet Flip'
|
||||
lnx_version = 1
|
||||
lnx_section = 'tilesheet'
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxNodeSocketObject', 'Object')
|
||||
self.add_input('LnxBoolSocket', 'Flip X')
|
||||
self.add_input('LnxBoolSocket', 'Flip Y')
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
54
leenkx/blender/lnx/logicnode/draw/LN_draw_image_render.py
Normal file
54
leenkx/blender/lnx/logicnode/draw/LN_draw_image_render.py
Normal file
@ -0,0 +1,54 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
|
||||
class DrawImageRenderNode(LnxLogicTreeNode):
|
||||
"""Draws an image render target.
|
||||
|
||||
@input Draw: Activate to draw the image on this frame. The input must
|
||||
be (indirectly) called from an `On Render2D` node.
|
||||
|
||||
@input In: Activate to retrieve the imaga render target.
|
||||
@input Camera: the render target image of the camera.
|
||||
@input Color: The color that the image's pixels are multiplied with.
|
||||
@input Left/Center/Right: Horizontal anchor point of the image.
|
||||
|
||||
0 = Left, 1 = Center, 2 = Right
|
||||
@input Top/Middle/Bottom: Vertical anchor point of the image.
|
||||
|
||||
0 = Top, 1 = Middle, 2 = Bottom
|
||||
@input X/Y: Position of the anchor point in pixels.
|
||||
@input Width/Height: Size of the image in pixels.
|
||||
@input sX/Y: Position of the sub anchor point in pixels.
|
||||
@input sWidth/sHeight: Size of the sub image in pixels.
|
||||
@input Angle: Rotation angle in radians. Image will be rotated cloclwiswe
|
||||
at the anchor point.
|
||||
@input Render2D: include Render 2D draws.
|
||||
|
||||
@output Out: Activated after the image has been drawn.
|
||||
|
||||
@see [`kha.graphics2.Graphics.drawImage()`](http://kha.tech/api/kha/graphics2/Graphics.html#drawImage).
|
||||
"""
|
||||
bl_idname = 'LNDrawImageRenderNode'
|
||||
bl_label = 'Draw Image Render'
|
||||
lnx_section = 'draw'
|
||||
lnx_version = 1
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'Draw')
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxNodeSocketObject', 'Camera')
|
||||
self.add_input('LnxColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0])
|
||||
self.add_input('LnxIntSocket', '0/1/2 = Left/Center/Right', default_value=0)
|
||||
self.add_input('LnxIntSocket', '0/1/2 = Top/Middle/Bottom', default_value=0)
|
||||
self.add_input('LnxFloatSocket', 'X')
|
||||
self.add_input('LnxFloatSocket', 'Y')
|
||||
self.add_input('LnxFloatSocket', 'Width')
|
||||
self.add_input('LnxFloatSocket', 'Height')
|
||||
self.add_input('LnxFloatSocket', 'sX')
|
||||
self.add_input('LnxFloatSocket', 'sY')
|
||||
self.add_input('LnxFloatSocket', 'sWidth')
|
||||
self.add_input('LnxFloatSocket', 'sHeight')
|
||||
self.add_input('LnxFloatSocket', 'Angle')
|
||||
self.add_input('LnxBoolSocket', 'Render2D')
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
@ -12,9 +12,9 @@ class DrawSubImageNode(LnxLogicTreeNode):
|
||||
@input Top/Middle/Bottom: Vertical anchor point of the image.
|
||||
0 = Top, 1 = Middle, 2 = Bottom
|
||||
@input X/Y: Position of the anchor point in pixels.
|
||||
@input Width/Height: Size of the sub image in pixels.
|
||||
@input sX/Y: Position of the sub anchor point in pixels.
|
||||
@input sWidth/Height: Size of the image in pixels.
|
||||
@input Width/Height: Size of the image in pixels.
|
||||
@input sX/sY: Position of the sub anchor point in pixels.
|
||||
@input sWidth/sHeight: Size of the sub image in pixels.
|
||||
@input Angle: Rotation angle in radians. Image will be rotated cloclwiswe
|
||||
at the anchor point.
|
||||
@output Out: Activated after the image has been drawn.
|
||||
|
||||
37
leenkx/blender/lnx/logicnode/draw/LN_draw_to_image.py
Normal file
37
leenkx/blender/lnx/logicnode/draw/LN_draw_to_image.py
Normal file
@ -0,0 +1,37 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
|
||||
class DrawToImageNode(LnxLogicTreeNode):
|
||||
"""Writes the given draw image to the given file. If the image
|
||||
already exists, the existing content of the image is overwritten.
|
||||
|
||||
@input Image File: the name of the image
|
||||
@input Color: The color that the image's pixels are multiplied with.
|
||||
@input Width: width of the image file.
|
||||
@input Height: heigth of the image file.
|
||||
@input sX: sub position of first x pixel of the sub image (0 for start).
|
||||
@input sY: sub position of first y pixel of the sub image (0 for start).
|
||||
@input sWidth: width of the sub image.
|
||||
@input sHeight: height of the sub image.
|
||||
|
||||
WARNING: Calling getPixels() on a renderTarget with non-standard non-POT dimensions
|
||||
can cause a system crash. Ensure renderTarget resolution is a power of two
|
||||
(e.g., 256x256) or a common standard resolution (e.g., 1920x1080).
|
||||
"""
|
||||
bl_idname = 'LNDrawToImageNode'
|
||||
bl_label = 'Draw To Image'
|
||||
lnx_section = 'draw'
|
||||
lnx_version = 1
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxStringSocket', 'Image File')
|
||||
self.add_input('LnxColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0])
|
||||
self.add_input('LnxIntSocket', 'Width')
|
||||
self.add_input('LnxIntSocket', 'Height')
|
||||
self.add_input('LnxIntSocket', 'sX')
|
||||
self.add_input('LnxIntSocket', 'sY')
|
||||
self.add_input('LnxIntSocket', 'sWidth')
|
||||
self.add_input('LnxIntSocket', 'sHeight')
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
69
leenkx/blender/lnx/logicnode/draw/LN_draw_to_screen.py
Normal file
69
leenkx/blender/lnx/logicnode/draw/LN_draw_to_screen.py
Normal file
@ -0,0 +1,69 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
import bpy
|
||||
|
||||
|
||||
class DrawToScreenNode(LnxLogicTreeNode):
|
||||
"""Draws a Render Target image to screen.
|
||||
|
||||
@input Draw: Activate to draw the Render Target image to screen. The input must
|
||||
be (indirectly) called from an `On Render2D` node.
|
||||
@input In: Activate to get the Render Target image of the render 2d draws.
|
||||
@input Draw Width/Height: Size of the Render Target image in pixels.
|
||||
@input Image: The filename of the image.
|
||||
@input Color: The color that the image's pixels are multiplied with.
|
||||
@input Left/Center/Right: Horizontal anchor point of the image.
|
||||
|
||||
0 = Left, 1 = Center, 2 = Right
|
||||
@input Top/Middle/Bottom: Vertical anchor point of the image.
|
||||
|
||||
0 = Top, 1 = Middle, 2 = Bottom
|
||||
@input X/Y: Position of the anchor point in pixels.
|
||||
@input Width/Height: Size of the sub image in pixels.
|
||||
@input sX/Y: Position of the sub anchor point in pixels.
|
||||
@input sWidth/Height: Size of the image in pixels.
|
||||
@input Angle: Rotation angle in radians. Image will be rotated cloclwiswe
|
||||
at the anchor point.
|
||||
@input Clear Image: Clear the image before drawing to it
|
||||
|
||||
@output Out: Activated after the image has been drawn.
|
||||
@output Draw: Input for the render 2d draws.
|
||||
|
||||
@see [`kha.graphics2.Graphics.drawImage()`](http://kha.tech/api/kha/graphics2/Graphics.html#drawImage).
|
||||
"""
|
||||
bl_idname = 'LNDrawToScreenNode'
|
||||
bl_label = 'Draw To Screen'
|
||||
lnx_section = 'draw'
|
||||
lnx_version = 2
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'Draw')
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxIntSocket', 'Draw Width')
|
||||
self.add_input('LnxIntSocket', 'Draw Height')
|
||||
self.add_input('LnxColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0])
|
||||
self.add_input('LnxIntSocket', '0/1/2 = Left/Center/Right', default_value=0)
|
||||
self.add_input('LnxIntSocket', '0/1/2 = Top/Middle/Bottom', default_value=0)
|
||||
self.add_input('LnxFloatSocket', 'X')
|
||||
self.add_input('LnxFloatSocket', 'Y')
|
||||
self.add_input('LnxFloatSocket', 'Width')
|
||||
self.add_input('LnxFloatSocket', 'Height')
|
||||
self.add_input('LnxFloatSocket', 'sX')
|
||||
self.add_input('LnxFloatSocket', 'sY')
|
||||
self.add_input('LnxFloatSocket', 'sWidth')
|
||||
self.add_input('LnxFloatSocket', 'sHeight')
|
||||
self.add_input('LnxFloatSocket', 'Angle')
|
||||
self.add_input('LnxBoolSocket', 'Clear Image')
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
self.add_output('LnxNodeSocketAction', 'Draw')
|
||||
|
||||
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
|
||||
if self.lnx_version not in (0, 1):
|
||||
raise LookupError()
|
||||
|
||||
return NodeReplacement(
|
||||
"LNDrawToScreenNode", self.lnx_version,
|
||||
"LNDrawToScreenNode", 2,
|
||||
in_socket_mapping={0:1, 1:2, 2:3, 3:4, 4:5, 5:6, 6:7, 7:8, 8:9, 9:10, 10:11, 11:12, 12:13, 13:14, 14:15},
|
||||
out_socket_mapping={0:0},
|
||||
)
|
||||
@ -4,6 +4,7 @@ class OnUpdateNode(LnxLogicTreeNode):
|
||||
"""Activates the output on every frame.
|
||||
|
||||
@option Update: (default) activates the output every frame.
|
||||
@option Fixed Update: activates the output at a fixed time step.
|
||||
@option Late Update: activates the output after all non-late updates are calculated.
|
||||
@option Physics Pre-Update: activates the output before calculating the physics.
|
||||
Only available when using a physics engine."""
|
||||
@ -13,6 +14,7 @@ class OnUpdateNode(LnxLogicTreeNode):
|
||||
property0: HaxeEnumProperty(
|
||||
'property0',
|
||||
items = [('Update', 'Update', 'Update'),
|
||||
('Fixed Update', 'Fixed Update', 'Fixed Update'),
|
||||
('Late Update', 'Late Update', 'Late Update'),
|
||||
('Physics Pre-Update', 'Physics Pre-Update', 'Physics Pre-Update')],
|
||||
name='On', default='Update')
|
||||
|
||||
@ -88,7 +88,7 @@ class LnxAnimActionSocket(LnxCustomSocket):
|
||||
default_value_raw: PointerProperty(name='Action', type=bpy.types.Action, update=_on_update_socket)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
super(LnxAnimActionSocket, self).__init__(*args, **kwargs)
|
||||
if self.default_value_get is not None:
|
||||
self.default_value_raw = self.default_value_get
|
||||
self.default_value_get = None
|
||||
@ -510,7 +510,7 @@ class LnxObjectSocket(LnxCustomSocket):
|
||||
default_value_raw: PointerProperty(name='Object', type=bpy.types.Object, update=_on_update_socket)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
super(LnxObjectSocket, self).__init__(*args, **kwargs)
|
||||
if self.default_value_get is not None:
|
||||
self.default_value_raw = self.default_value_get
|
||||
self.default_value_get = None
|
||||
|
||||
@ -15,8 +15,8 @@ class SelectOutputNode(LnxLogicTreeNode):
|
||||
lnx_version = 1
|
||||
min_outputs = 2
|
||||
|
||||
def __init__(self):
|
||||
super(SelectOutputNode, self).__init__()
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SelectOutputNode, self).__init__(*args, **kwargs)
|
||||
array_nodes[self.get_id_str()] = self
|
||||
|
||||
def lnx_init(self, context):
|
||||
|
||||
@ -29,7 +29,7 @@ class CameraSetNode(LnxLogicTreeNode):
|
||||
if self.property0 == 'DoF F-Stop':
|
||||
self.add_input('LnxFloatSocket', 'DoF F-Stop', default_value=128.0)#8
|
||||
if self.property0 == 'Tonemapping':
|
||||
self.add_input('LnxIntSocket', 'Tonemapping', default_value=5)#9
|
||||
self.add_input('LnxIntSocket', 'Tonemapping', default_value=0)#9
|
||||
if self.property0 == 'Distort':
|
||||
self.add_input('LnxFloatSocket', 'Distort', default_value=2.0)#10
|
||||
if self.property0 == 'Film Grain':
|
||||
@ -75,12 +75,12 @@ class CameraSetNode(LnxLogicTreeNode):
|
||||
layout.label(text="1: Filmic2")
|
||||
layout.label(text="2: Reinhard")
|
||||
layout.label(text="3: Uncharted2")
|
||||
layout.label(text="5: Agx")
|
||||
layout.label(text="6: None")
|
||||
layout.label(text="5: None")
|
||||
|
||||
layout.prop(self, 'property0')
|
||||
|
||||
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
|
||||
if self.lnx_version not in range(0, 4):
|
||||
if self.lnx_version not in range(0, 6):
|
||||
raise LookupError()
|
||||
elif self.lnx_version == 1:
|
||||
newnode = node_tree.nodes.new('LNCameraSetNode')
|
||||
|
||||
@ -17,7 +17,8 @@ class ProbabilisticIndexNode(LnxLogicTreeNode):
|
||||
|
||||
num_choices: IntProperty(default=0, min=0)
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProbabilisticIndexNode, self).__init__(*args, **kwargs)
|
||||
array_nodes[str(id(self))] = self
|
||||
|
||||
def lnx_init(self, context):
|
||||
|
||||
45
leenkx/blender/lnx/logicnode/signal/LN_emit_signal.py
Normal file
45
leenkx/blender/lnx/logicnode/signal/LN_emit_signal.py
Normal file
@ -0,0 +1,45 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
|
||||
class EmitSignalNode(LnxLogicTreeNode):
|
||||
"""Emits a Signal with optional arguments.
|
||||
|
||||
Connect a Signal instance to the Signal input. When this node is activated,
|
||||
it calls emit() on the Signal, passing any connected arguments to all
|
||||
connected OnSignal nodes.
|
||||
|
||||
Use 'Add Arg' to add input sockets for passing data to listeners.
|
||||
|
||||
@seeNode Signal
|
||||
@seeNode On Signal"""
|
||||
|
||||
bl_idname = 'LNEmitSignalNode'
|
||||
bl_label = 'Emit Signal'
|
||||
lnx_version = 1
|
||||
lnx_section = 'signal'
|
||||
min_inputs = 2
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EmitSignalNode, self).__init__(*args, **kwargs)
|
||||
array_nodes[str(id(self))] = self
|
||||
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxDynamicSocket', 'Signal')
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
row = layout.row(align=True)
|
||||
op = row.operator('lnx.node_add_input', text='Add Arg', icon='PLUS', emboss=True)
|
||||
op.node_index = str(id(self))
|
||||
op.socket_type = 'LnxDynamicSocket'
|
||||
op.name_format = "Arg {0}"
|
||||
op.index_name_offset = -1
|
||||
column = row.column(align=True)
|
||||
op = column.operator('lnx.node_remove_input', text='', icon='X', emboss=True)
|
||||
op.node_index = str(id(self))
|
||||
if len(self.inputs) == self.min_inputs:
|
||||
column.enabled = False
|
||||
25
leenkx/blender/lnx/logicnode/signal/LN_global_signal.py
Normal file
25
leenkx/blender/lnx/logicnode/signal/LN_global_signal.py
Normal file
@ -0,0 +1,25 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
|
||||
class GlobalSignalNode(LnxLogicTreeNode):
|
||||
"""Gets or creates a global Signal by name.
|
||||
|
||||
Global Signals are stored in a static registry and can be accessed from
|
||||
any logic tree in the scene. Provide a unique name to identify the signal.
|
||||
|
||||
Use this for communication between different objects or logic trees without
|
||||
needing to pass Signal references directly.
|
||||
|
||||
@seeNode Signal
|
||||
@seeNode On Signal
|
||||
@seeNode Emit Signal"""
|
||||
|
||||
bl_idname = 'LNGlobalSignalNode'
|
||||
bl_label = 'Global Signal'
|
||||
lnx_version = 1
|
||||
lnx_section = 'signal'
|
||||
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxStringSocket', 'Property')
|
||||
self.add_output('LnxDynamicSocket', 'Signal')
|
||||
44
leenkx/blender/lnx/logicnode/signal/LN_on_signal.py
Normal file
44
leenkx/blender/lnx/logicnode/signal/LN_on_signal.py
Normal file
@ -0,0 +1,44 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
|
||||
class OnSignalNode(LnxLogicTreeNode):
|
||||
"""Activates the output when the given Signal emits.
|
||||
|
||||
Connect a Signal instance to the input. When that Signal emits,
|
||||
the output is activated and emitted arguments are available on
|
||||
the dynamic output sockets.
|
||||
|
||||
Use 'Add Arg' to add output sockets for receiving emitted data.
|
||||
|
||||
@seeNode Signal
|
||||
@seeNode Emit Signal"""
|
||||
|
||||
bl_idname = 'LNOnSignalNode'
|
||||
bl_label = 'On Signal'
|
||||
lnx_version = 1
|
||||
lnx_section = 'signal'
|
||||
min_outputs = 1
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OnSignalNode, self).__init__(*args, **kwargs)
|
||||
array_nodes[str(id(self))] = self
|
||||
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxDynamicSocket', 'Signal')
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
row = layout.row(align=True)
|
||||
op = row.operator('lnx.node_add_output', text='Add Arg', icon='PLUS', emboss=True)
|
||||
op.node_index = str(id(self))
|
||||
op.socket_type = 'LnxDynamicSocket'
|
||||
op.name_format = "Arg {0}"
|
||||
op.index_name_offset = 0
|
||||
column = row.column(align=True)
|
||||
op = column.operator('lnx.node_remove_output', text='', icon='X', emboss=True)
|
||||
op.node_index = str(id(self))
|
||||
if len(self.outputs) == self.min_outputs:
|
||||
column.enabled = False
|
||||
27
leenkx/blender/lnx/logicnode/signal/LN_signal.py
Normal file
27
leenkx/blender/lnx/logicnode/signal/LN_signal.py
Normal file
@ -0,0 +1,27 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
|
||||
class SignalNode(LnxLogicTreeNode):
|
||||
"""Creates a new Signal or references an existing Signal from an object's property.
|
||||
|
||||
**Standalone Mode (default):**
|
||||
Creates a new Signal instance that can be connected to OnSignal and EmitSignal nodes.
|
||||
The Signal is stored in the LogicTree and persists for the lifetime of the trait.
|
||||
|
||||
**Reference Mode:**
|
||||
When Object and Property inputs are connected, retrieves an existing Signal
|
||||
from a Haxe trait's property using reflection.
|
||||
|
||||
@seeNode On Signal
|
||||
@seeNode Emit Signal"""
|
||||
|
||||
bl_idname = 'LNSignalNode'
|
||||
bl_label = 'Signal'
|
||||
lnx_version = 1
|
||||
lnx_section = 'signal'
|
||||
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketObject', 'Object')
|
||||
self.add_input('LnxStringSocket', 'Property')
|
||||
self.add_output('LnxDynamicSocket', 'Signal')
|
||||
3
leenkx/blender/lnx/logicnode/signal/__init__.py
Normal file
3
leenkx/blender/lnx/logicnode/signal/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from lnx.logicnode.lnx_nodes import add_node_section
|
||||
|
||||
add_node_section(name='signal', category='Signal')
|
||||
@ -0,0 +1,28 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
class SetFirstPersonControllerNode(LnxLogicTreeNode):
|
||||
"""Config Visual"""
|
||||
bl_idname = 'LNSetFirstPersonControllerNode'
|
||||
bl_label = 'Set FirstPersonControllerSettings'
|
||||
lnx_section = 'props'
|
||||
lnx_version = 1
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxNodeSocketObject', 'Object')
|
||||
|
||||
"""Config de la camara"""
|
||||
self.add_input('LnxFloatSocket', 'RotationSpeed')
|
||||
self.add_input('LnxFloatSocket', 'MaxPitch')
|
||||
self.add_input('LnxFloatSocket', 'MinPitch')
|
||||
|
||||
self.add_input('LnxFloatSocket', 'MoveSpeed')
|
||||
self.add_input('LnxFloatSocket', 'RunSpeed')
|
||||
|
||||
self.add_input('LnxBoolSocket', 'EnableJump')
|
||||
self.add_input('LnxBoolSocket', 'EnableAllowAirJump')
|
||||
self.add_input('LnxBoolSocket', 'EnableRun')
|
||||
self.add_input('LnxBoolSocket', 'EnableStamina')
|
||||
self.add_input('LnxBoolSocket', 'EnableFatigue')
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
@ -0,0 +1,28 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
class SetOverheadPersonControllerNode(LnxLogicTreeNode):
|
||||
"""Config Visual"""
|
||||
bl_idname = 'LNSetOverheadPersonControllerNode'
|
||||
bl_label = 'Set OverheadPersonControllerSettings'
|
||||
lnx_section = 'props'
|
||||
lnx_version = 2
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
|
||||
self.add_input('LnxNodeSocketObject', 'Object')
|
||||
|
||||
"""Suavizado"""
|
||||
self.add_input('LnxBoolSocket', 'EnableSmoothTrack')
|
||||
self.add_input('LnxFloatSocket', 'SmoothSpeed')
|
||||
|
||||
self.add_input('LnxFloatSocket', 'MoveSpeed')
|
||||
self.add_input('LnxFloatSocket', 'RunSpeed')
|
||||
|
||||
self.add_input('LnxBoolSocket', 'EnableJump')
|
||||
self.add_input('LnxBoolSocket', 'EnableAllowAirJump')
|
||||
self.add_input('LnxBoolSocket', 'EnableRun')
|
||||
self.add_input('LnxBoolSocket', 'EnableStamina')
|
||||
self.add_input('LnxBoolSocket', 'EnableFatigue')
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
15
leenkx/blender/lnx/logicnode/transform/LN_replace_object.py
Normal file
15
leenkx/blender/lnx/logicnode/transform/LN_replace_object.py
Normal file
@ -0,0 +1,15 @@
|
||||
from lnx.logicnode.lnx_nodes import *
|
||||
|
||||
class ReplaceObjectNode(LnxLogicTreeNode):
|
||||
"""Replace location and rotation between two objects"""
|
||||
bl_idname = 'LNReplaceObjectNode'
|
||||
bl_label = 'Replace Object'
|
||||
lnx_version = 2
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxNodeSocketObject', 'Base')
|
||||
self.add_input('LnxNodeSocketObject', 'Replace')
|
||||
self.add_input('LnxBoolSocket', 'Invert')
|
||||
self.add_input('LnxBoolSocket', 'Scale')
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
@ -118,10 +118,12 @@ def remove_readonly(func, path, excinfo):
|
||||
func(path)
|
||||
|
||||
|
||||
appended_scenes = []
|
||||
linked_blend_paths = []
|
||||
linked_scenes = []
|
||||
|
||||
def load_external_blends():
|
||||
global appended_scenes
|
||||
global linked_scenes
|
||||
global linked_blend_paths
|
||||
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
if not hasattr(wrd, 'lnx_external_blends_path'):
|
||||
@ -146,22 +148,24 @@ def load_external_blends():
|
||||
with bpy.data.libraries.load(blend_path, link=True) as (data_from, data_to):
|
||||
data_to.scenes = list(data_from.scenes)
|
||||
|
||||
linked_blend_paths.append(blend_path)
|
||||
|
||||
for scn in data_to.scenes:
|
||||
if scn is not None and scn not in appended_scenes:
|
||||
# make name unique with file name
|
||||
scn.name += "_" + filename.replace(".blend", "")
|
||||
appended_scenes.append(scn)
|
||||
if scn is not None and scn not in linked_scenes:
|
||||
linked_scenes.append(scn)
|
||||
|
||||
log.info(f"Loaded external blend: {blend_path}")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to load external blend {blend_path}: {e}")
|
||||
|
||||
def clear_external_scenes():
|
||||
global appended_scenes
|
||||
if not appended_scenes:
|
||||
global linked_blend_paths
|
||||
global linked_scenes
|
||||
|
||||
if not linked_scenes and not linked_blend_paths:
|
||||
return
|
||||
|
||||
for scn in appended_scenes:
|
||||
for scn in linked_scenes:
|
||||
try:
|
||||
bpy.data.scenes.remove(scn, do_unlink=True)
|
||||
except Exception as e:
|
||||
@ -169,7 +173,7 @@ def clear_external_scenes():
|
||||
|
||||
for lib in list(bpy.data.libraries):
|
||||
try:
|
||||
if lib.users == 0:
|
||||
if lib.users == 0 or lib.filepath in linked_blend_paths:
|
||||
bpy.data.libraries.remove(lib)
|
||||
except Exception as e:
|
||||
log.error(f"Failed to remove library {lib.name}: {e}")
|
||||
@ -179,7 +183,8 @@ def clear_external_scenes():
|
||||
except Exception as e:
|
||||
log.error(f"Failed to purge orphan data: {e}")
|
||||
|
||||
appended_scenes = []
|
||||
linked_scenes = []
|
||||
linked_blend_paths = []
|
||||
|
||||
def export_data(fp, sdk_path):
|
||||
state.is_exporting = True
|
||||
@ -190,6 +195,11 @@ def export_data(fp, sdk_path):
|
||||
|
||||
|
||||
def export_data_impl(fp, sdk_path):
|
||||
# Reload all libraries to retrieve updated data without needing to restart Blender
|
||||
for lib in bpy.data.libraries:
|
||||
lib.reload()
|
||||
log.info(f"Reloaded: {lib.filepath}")
|
||||
|
||||
load_external_blends()
|
||||
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
@ -263,8 +273,10 @@ def export_data_impl(fp, sdk_path):
|
||||
|
||||
for scene in bpy.data.scenes:
|
||||
if scene.lnx_export:
|
||||
# Reset shader comparison arrays to prevent cross-scene shader merging
|
||||
assets.reset_shader_cons()
|
||||
ext = '.lz4' if LeenkxExporter.compress_enabled else '.lnx'
|
||||
asset_path = build_dir + '/compiled/Assets/' + lnx.utils.safestr(scene.name) + ext
|
||||
asset_path = build_dir + '/compiled/Assets/' + lnx.utils.safestr(scene.name + "_" + os.path.basename(scene.library.filepath).replace(".blend", "") if scene.library else scene.name) + ext
|
||||
LeenkxExporter.export_scene(bpy.context, asset_path, scene=scene, depsgraph=depsgraph)
|
||||
if LeenkxExporter.export_physics:
|
||||
physics_found = True
|
||||
|
||||
@ -149,7 +149,7 @@ def add_world_defs():
|
||||
wrd.world_defs += '_Clusters'
|
||||
assets.add_khafile_def('lnx_clusters')
|
||||
|
||||
if '_Rad' in wrd.world_defs and '_Brdf' not in wrd.world_defs:
|
||||
if ('_Rad' in wrd.world_defs or ('_EnvCol' in wrd.world_defs and rpdat.lnx_material_model == 'Full')) and '_Brdf' not in wrd.world_defs:
|
||||
wrd.world_defs += '_Brdf'
|
||||
assets.add_khafile_def("lnx_brdf")
|
||||
|
||||
@ -369,6 +369,8 @@ def build():
|
||||
assets.add_khafile_def('rp_voxels={0}'.format(rpdat.rp_voxels))
|
||||
assets.add_khafile_def('rp_voxelgi_resolution={0}'.format(rpdat.rp_voxelgi_resolution))
|
||||
assets.add_khafile_def('rp_voxelgi_resolution_z={0}'.format(rpdat.rp_voxelgi_resolution_z))
|
||||
else:
|
||||
assets.add_khafile_def('rp_voxels=Off')
|
||||
|
||||
if rpdat.lnx_rp_resolution == 'Custom':
|
||||
assets.add_khafile_def('rp_resolution_filter={0}'.format(rpdat.lnx_rp_resolution_filter))
|
||||
|
||||
@ -31,6 +31,29 @@ callback = None
|
||||
shader_datas = []
|
||||
|
||||
|
||||
def add_world_def(world: bpy.types.World, define: str):
|
||||
if define not in world.world_defs:
|
||||
world.world_defs += define
|
||||
|
||||
|
||||
def add_global_def(define: str):
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
if define not in wrd.world_defs:
|
||||
wrd.world_defs += define
|
||||
|
||||
|
||||
def add_irradiance_defs(world: bpy.types.World, rpdat):
|
||||
if rpdat.lnx_irradiance and rpdat.lnx_material_model != 'Solid':
|
||||
add_world_def(world, '_Irr')
|
||||
add_global_def('_Irr')
|
||||
assets.add_khafile_def("lnx_irradiance")
|
||||
|
||||
|
||||
def mark_color_environment(world: bpy.types.World):
|
||||
add_world_def(world, '_EnvCol')
|
||||
add_global_def('_EnvCol')
|
||||
|
||||
|
||||
def build():
|
||||
"""Builds world shaders for all exported worlds."""
|
||||
global shader_datas
|
||||
@ -38,6 +61,10 @@ def build():
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
rpdat = lnx.utils.get_rp()
|
||||
|
||||
if rpdat is None:
|
||||
log.error("No render path found. Please ensure a valid render path is selected.")
|
||||
return
|
||||
|
||||
mobile_mat = rpdat.lnx_material_model == 'Mobile' or rpdat.lnx_material_model == 'Solid'
|
||||
envpath = os.path.join(lnx.utils.get_fp_build(), 'compiled', 'Assets', 'envmaps')
|
||||
|
||||
@ -170,7 +197,7 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha
|
||||
|
||||
# film_transparent, do not render
|
||||
if bpy.context.scene is not None and bpy.context.scene.render.film_transparent:
|
||||
world.world_defs += '_EnvCol'
|
||||
mark_color_environment(world)
|
||||
frag.add_uniform('vec3 backgroundCol', link='_backgroundCol')
|
||||
frag.write('fragColor.rgb = backgroundCol;')
|
||||
return
|
||||
@ -191,15 +218,11 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha
|
||||
|
||||
# No world nodes/no output node, use background color
|
||||
if not is_parsed:
|
||||
solid_mat = rpdat.lnx_material_model == 'Solid'
|
||||
if rpdat.lnx_irradiance and not solid_mat:
|
||||
world.world_defs += '_Irr'
|
||||
assets.add_khafile_def("lnx_irradiance")
|
||||
add_irradiance_defs(world, rpdat)
|
||||
col = world.color
|
||||
world.lnx_envtex_color = [col[0], col[1], col[2], 1.0]
|
||||
world.lnx_envtex_strength = 1.0
|
||||
world.world_defs += '_EnvCol'
|
||||
assets.add_khafile_def("lnx_envcol")
|
||||
mark_color_environment(world)
|
||||
|
||||
# Clouds enabled
|
||||
if rpdat.lnx_clouds and world.lnx_use_clouds:
|
||||
@ -209,12 +232,13 @@ 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 '_EnvClouds' in world.world_defs:
|
||||
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')
|
||||
|
||||
# Clear background color
|
||||
if '_EnvCol' in world.world_defs:
|
||||
frag.write('fragColor.rgb = backgroundCol;')
|
||||
frag.add_uniform('vec3 backgroundCol', link='_backgroundCol')
|
||||
frag.write('fragColor.rgb = backgroundCol * envmapStrength;')
|
||||
|
||||
elif '_EnvTex' in world.world_defs and '_EnvLDR' in world.world_defs:
|
||||
frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(2.2));')
|
||||
@ -273,15 +297,11 @@ def parse_world_output(world: bpy.types.World, node_output: bpy.types.Node, frag
|
||||
|
||||
|
||||
def parse_surface(world: bpy.types.World, node_surface: bpy.types.Node, frag: Shader):
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
rpdat = lnx.utils.get_rp()
|
||||
solid_mat = rpdat.lnx_material_model == 'Solid'
|
||||
|
||||
if node_surface.type in ('BACKGROUND', 'EMISSION'):
|
||||
# Append irradiance define
|
||||
if rpdat.lnx_irradiance and not solid_mat:
|
||||
wrd.world_defs += '_Irr'
|
||||
assets.add_khafile_def("lnx_irradiance")
|
||||
add_irradiance_defs(world, rpdat)
|
||||
|
||||
# Extract environment strength
|
||||
# Todo: follow/parse strength input
|
||||
@ -292,12 +312,8 @@ def parse_surface(world: bpy.types.World, node_surface: bpy.types.Node, frag: Sh
|
||||
frag.write(f'fragColor.rgb = {out};')
|
||||
|
||||
if not node_surface.inputs[0].is_linked:
|
||||
solid_mat = rpdat.lnx_material_model == 'Solid'
|
||||
if rpdat.lnx_irradiance and not solid_mat:
|
||||
world.world_defs += '_Irr'
|
||||
assets.add_khafile_def("lnx_irradiance")
|
||||
world.lnx_envtex_color = node_surface.inputs[0].default_value
|
||||
world.lnx_envtex_strength = 1.0
|
||||
mark_color_environment(world)
|
||||
|
||||
else:
|
||||
log.warn(f'World node type {node_surface.type} must not be connected to the world output node!')
|
||||
|
||||
@ -321,29 +321,18 @@ def parse_shader(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> Tuple[st
|
||||
'MIX_SHADER',
|
||||
'ADD_SHADER',
|
||||
'BSDF_PRINCIPLED',
|
||||
'PRINCIPLED_BSDF',
|
||||
'BSDF_DIFFUSE',
|
||||
'DIFFUSE_BSDF',
|
||||
'BSDF_GLOSSY',
|
||||
'GLOSSY_BSDF',
|
||||
'BSDF_SHEEN',
|
||||
'SHEEN_BSDF',
|
||||
'AMBIENT_OCCLUSION',
|
||||
'BSDF_ANISOTROPIC',
|
||||
'ANISOTROPIC_BSDF',
|
||||
'EMISSION',
|
||||
'BSDF_GLASS',
|
||||
'GLASS_BSDF',
|
||||
'BSDF_REFRACTION',
|
||||
'REFRACTION_BSDF',
|
||||
'HOLDOUT',
|
||||
'SUBSURFACE_SCATTERING',
|
||||
'BSDF_TRANSLUCENT',
|
||||
'TRANSLUCENT_BSDF',
|
||||
'BSDF_TRANSPARENT',
|
||||
'TRANSPARENT_BSDF',
|
||||
'BSDF_VELVET',
|
||||
'VELVET_BSDF',
|
||||
)
|
||||
|
||||
state.reset_outs()
|
||||
@ -377,7 +366,7 @@ def parse_shader(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> Tuple[st
|
||||
mat_state.emission_type = mat_state.EmissionType.SHADED
|
||||
if state.parse_opacity:
|
||||
state.out_opacity = parse_value_input(node.inputs[1])
|
||||
state.out_ior = 1.450;
|
||||
state.out_ior = 1.450
|
||||
else:
|
||||
return parse_group(node, socket)
|
||||
|
||||
@ -394,6 +383,21 @@ def parse_shader(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> Tuple[st
|
||||
return state.get_outs()
|
||||
|
||||
|
||||
# Use an array of socket names for compatibility across Blender versions
|
||||
def get_vector_input(node: bpy.types.Node, socket_names: Tuple[str, ...]) -> vec3str:
|
||||
for name in socket_names:
|
||||
if name in node.inputs:
|
||||
try:
|
||||
return parse_vector_input(node.inputs[name])
|
||||
except Exception:
|
||||
log.warn(f'Failed to parse input "{name}" on node "{node.name}"')
|
||||
else:
|
||||
# FIXME: Fallback to default value if the node isn't found
|
||||
log.warn(f'Input "{name}" not found on node "{node.name}", returning default None')
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def parse_displacement_input(inp):
|
||||
if inp.is_linked:
|
||||
l = inp.links[0]
|
||||
@ -504,6 +508,20 @@ def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str:
|
||||
return "vec3(0, 0, 0)"
|
||||
|
||||
|
||||
# Use an array of socket names for compatibility across Blender versions
|
||||
def get_value_input(node: bpy.types.Node, socket_names: Tuple[str, ...]) -> floatstr:
|
||||
for name in socket_names:
|
||||
if name in node.inputs:
|
||||
try:
|
||||
return parse_value_input(node.inputs[name])
|
||||
except Exception:
|
||||
log.warn(f'Failed to parse input "{name}" on node "{node.name}"')
|
||||
else:
|
||||
# FIXME: Fallback to default value if the node isn't found
|
||||
log.warn(f'Input "{name}" not found on node "{node.name}", returning default 1.0')
|
||||
return '1.0'
|
||||
|
||||
|
||||
def parse_normal_map_color_input(inp, strength_input=None):
|
||||
frag = state.frag
|
||||
|
||||
@ -731,7 +749,7 @@ def store_var_name(node: bpy.types.Node) -> str:
|
||||
return name + '_store'
|
||||
|
||||
|
||||
def texture_store(node, tex, tex_name, to_linear=False, tex_link=None, default_value=None, is_lnx_mat_param=None):
|
||||
def texture_store(node, tex, tex_name, to_linear=False, unpremultiply=False, tex_link=None, default_value=None, is_lnx_mat_param=None):
|
||||
curshader = state.curshader
|
||||
|
||||
tex_store = store_var_name(node)
|
||||
@ -770,6 +788,9 @@ def texture_store(node, tex, tex_name, to_linear=False, tex_link=None, default_v
|
||||
else:
|
||||
curshader.write('vec4 {0} = texture({1}, {2}.xy);'.format(tex_store, tex_name, uv_name))
|
||||
|
||||
if unpremultiply:
|
||||
curshader.write('if ({0}.a > 0.0) {0}.rgb /= {0}.a;'.format(tex_store))
|
||||
|
||||
if to_linear:
|
||||
curshader.write('{0}.rgb = pow({0}.rgb, vec3(2.2));'.format(tex_store))
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
str_tex_proc = """
|
||||
// <https://www.shadertoy.com/view/4dS3Wd>
|
||||
// By Morgan McGuire @morgan3d, http://graphicscodex.com
|
||||
float hash_f(const float n) { return fract(sin(n) * 1e4); }
|
||||
float hash_f(const vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); }
|
||||
float hash_f(const vec3 co){ return fract(sin(dot(co.xyz, vec3(12.9898,78.233,52.8265)) * 24.384) * 43758.5453); }
|
||||
// Hash functions by Dave Hoskins
|
||||
// <https://www.shadertoy.com/view/4djSRW>
|
||||
float hash_f(float p) { p = fract(p * 0.1031); p *= p + 33.33; p *= p + p; return fract(p); }
|
||||
float hash_f(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); }
|
||||
float hash_f(vec3 p3) { p3 = fract(p3 * 0.1031); p3 += dot(p3, p3.zyx + 31.32); return fract((p3.x + p3.y) * p3.z); }
|
||||
|
||||
float noise(const vec3 x) {
|
||||
const vec3 step = vec3(110, 241, 171);
|
||||
@ -418,11 +418,12 @@ float tex_brick_blender_f(vec3 co,
|
||||
|
||||
|
||||
str_tex_wave = """
|
||||
float tex_wave_f(const vec3 p, const int type, const int profile, const float dist, const float detail, const float detail_scale) {
|
||||
float tex_wave_f(const vec3 p, const int type, const int profile, const float dist, const float detail, const float detail_scale, const float phase_offset) {
|
||||
float n;
|
||||
if(type == 0) n = (p.x + p.y + p.z) * 9.5;
|
||||
else n = length(p) * 13.0;
|
||||
if(dist != 0.0) n += dist * fractal_noise(p * detail_scale, detail) * 2.0 - 1.0;
|
||||
n += phase_offset;
|
||||
if(profile == 0) { return 0.5 + 0.5 * sin(n - PI); }
|
||||
else {
|
||||
n /= 2.0 * PI;
|
||||
|
||||
@ -83,37 +83,28 @@ def parse_clamp(node: bpy.types.ShaderNodeClamp, out_socket: bpy.types.NodeSocke
|
||||
|
||||
|
||||
def parse_valtorgb(node: bpy.types.ShaderNodeValToRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
|
||||
# Alpha (TODO: make ColorRamp calculation vec4-based and split afterwards)
|
||||
if out_socket == node.outputs[1]:
|
||||
return '1.0'
|
||||
|
||||
input_fac: bpy.types.NodeSocket = node.inputs[0]
|
||||
alpha_out = out_socket == node.outputs[1]
|
||||
|
||||
fac: str = c.parse_value_input(input_fac) if input_fac.is_linked else c.to_vec1(input_fac.default_value)
|
||||
interp = node.color_ramp.interpolation
|
||||
elems = node.color_ramp.elements
|
||||
|
||||
|
||||
if len(elems) == 1:
|
||||
if alpha_out:
|
||||
return c.to_vec1(elems[0].color[3]) # Return alpha from the color
|
||||
else:
|
||||
return c.to_vec3(elems[0].color) # Return RGB
|
||||
|
||||
name_prefix = c.node_name(node.name).upper()
|
||||
|
||||
if alpha_out:
|
||||
cols_var = name_prefix + '_ALPHAS'
|
||||
else:
|
||||
cols_var = name_prefix + '_COLS'
|
||||
return c.to_vec3(elems[0].color)
|
||||
|
||||
# Write color array
|
||||
# The last entry is included twice so that the interpolation
|
||||
# between indices works (no out of bounds error)
|
||||
cols_var = c.node_name(node.name).upper() + '_COLS'
|
||||
|
||||
if state.current_pass == ParserPass.REGULAR:
|
||||
if alpha_out:
|
||||
cols_entries = ', '.join(f'{elem.color[3]}' for elem in elems)
|
||||
# Add last value twice to avoid out of bounds access
|
||||
cols_entries += f', {elems[len(elems) - 1].color[3]}'
|
||||
state.curshader.add_const("float", cols_var, cols_entries, array_size=len(elems) + 1)
|
||||
else:
|
||||
# Create array of RGB values for color output
|
||||
cols_entries = ', '.join(f'vec3({elem.color[0]}, {elem.color[1]}, {elem.color[2]})' for elem in elems)
|
||||
cols_entries += f', vec3({elems[len(elems) - 1].color[0]}, {elems[len(elems) - 1].color[1]}, {elems[len(elems) - 1].color[2]})'
|
||||
state.curshader.add_const("vec3", cols_var, cols_entries, array_size=len(elems) + 1)
|
||||
cols_entries = ', '.join(f'vec3({elem.color[0]}, {elem.color[1]}, {elem.color[2]})' for elem in elems)
|
||||
cols_entries += f', vec3({elems[len(elems) - 1].color[0]}, {elems[len(elems) - 1].color[1]}, {elems[len(elems) - 1].color[2]})'
|
||||
state.curshader.add_const("vec3", cols_var, cols_entries, array_size=len(elems) + 1)
|
||||
|
||||
fac_var = c.node_name(node.name) + '_fac' + state.get_parser_pass_suffix()
|
||||
state.curshader.write(f'float {fac_var} = {fac};')
|
||||
@ -131,22 +122,22 @@ def parse_valtorgb(node: bpy.types.ShaderNodeValToRGB, out_socket: bpy.types.Nod
|
||||
|
||||
# Linear interpolation
|
||||
else:
|
||||
# Write factor array - same for both color and alpha
|
||||
facs_var = name_prefix + '_FACS'
|
||||
# Write factor array
|
||||
facs_var = c.node_name(node.name).upper() + '_FACS'
|
||||
if state.current_pass == ParserPass.REGULAR:
|
||||
facs_entries = ', '.join(str(elem.position) for elem in elems)
|
||||
# Add one more entry at the rightmost position to avoid out of bounds access
|
||||
# Add one more entry at the rightmost position so that the
|
||||
# interpolation between indices works (no out of bounds error)
|
||||
facs_entries += ', 1.0'
|
||||
state.curshader.add_const("float", facs_var, facs_entries, array_size=len(elems) + 1)
|
||||
|
||||
# Calculation for interpolation position
|
||||
# Mix color
|
||||
prev_stop_fac = f'{facs_var}[{index_var}]'
|
||||
next_stop_fac = f'{facs_var}[{index_var} + 1]'
|
||||
prev_stop_col = f'{cols_var}[{index_var}]'
|
||||
next_stop_col = f'{cols_var}[{index_var} + 1]'
|
||||
rel_pos = f'({fac_var} - {prev_stop_fac}) * (1.0 / ({next_stop_fac} - {prev_stop_fac}))'
|
||||
|
||||
# Use mix function for both alpha and color outputs (mix works on floats too)
|
||||
|
||||
return f'mix({prev_stop_col}, {next_stop_col}, max({rel_pos}, 0.0))'
|
||||
|
||||
if bpy.app.version > (3, 2, 0):
|
||||
|
||||
@ -248,7 +248,7 @@ def parse_objectinfo(node: bpy.types.ShaderNodeObjectInfo, out_socket: bpy.types
|
||||
|
||||
|
||||
def parse_particleinfo(node: bpy.types.ShaderNodeParticleInfo, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
|
||||
particles_on = lnx.utils.get_rp().lnx_particles == 'On'
|
||||
particles_on = lnx.utils.get_rp().lnx_particles == 'GPU'
|
||||
|
||||
# Index
|
||||
if out_socket == node.outputs[0]:
|
||||
@ -310,27 +310,23 @@ def parse_texcoord(node: bpy.types.ShaderNodeTexCoord, out_socket: bpy.types.Nod
|
||||
return 'vec3(0.0)'
|
||||
state.con.add_elem('tex', 'short2norm')
|
||||
state.dxdy_varying_input_value = True
|
||||
|
||||
return 'vec3(texCoord.x, 1.0 - texCoord.y, 0.0)'
|
||||
elif out_socket == node.outputs[3]: # Object
|
||||
state.dxdy_varying_input_value = True
|
||||
return 'mposition'
|
||||
elif out_socket == node.outputs[4]: # Camera
|
||||
state.curshader.add_uniform('mat4 V', link='_viewMatrix')
|
||||
if not state.frag.contains('vec3 viewPosition;'):
|
||||
state.frag.write_init('vec3 viewPosition = (V * vec4(wposition, 1.0)).xyz;')
|
||||
state.dxdy_varying_input_value = True
|
||||
return 'viewPosition'
|
||||
return 'vec3(0.0)' # 'vposition'
|
||||
elif out_socket == node.outputs[5]: # Window
|
||||
# TODO: Don't use gl_FragCoord here, it uses different axes on different graphics APIs
|
||||
state.frag.add_uniform('vec2 screenSize', link='_screenSize')
|
||||
state.dxdy_varying_input_value = True
|
||||
return f'vec3(gl_FragCoord.xy / screenSize, 0.0)'
|
||||
elif out_socket == node.outputs[6]: # Reflection
|
||||
state.curshader.add_uniform('vec3 eye', link='_cameraPosition')
|
||||
if not state.frag.contains('vec3 reflectionVector;'):
|
||||
state.frag.write_init('vec3 reflectionVector = reflect(normalize(wposition - eye), normalize(n));')
|
||||
state.dxdy_varying_input_value = True
|
||||
return 'reflectionVector'
|
||||
if state.context == ParserContext.WORLD:
|
||||
state.dxdy_varying_input_value = True
|
||||
return 'n'
|
||||
return 'vec3(0.0)'
|
||||
|
||||
|
||||
def parse_uvmap(node: bpy.types.ShaderNodeUVMap, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
|
||||
|
||||
@ -85,6 +85,8 @@ def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket,
|
||||
state.out_opacity = '({0} * 0.5 + {1} * 0.5)'.format(opac1, opac2)
|
||||
state.out_ior = '({0} * 0.5 + {1} * 0.5)'.format(ior1, ior2)
|
||||
|
||||
# TODO: Refactor using c.get_*_input()
|
||||
|
||||
|
||||
if bpy.app.version < (2, 92, 0):
|
||||
def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None:
|
||||
@ -224,12 +226,6 @@ if bpy.app.version < (4, 1, 0):
|
||||
c.write_normal(node.inputs[2])
|
||||
state.out_basecol = c.parse_vector_input(node.inputs[0])
|
||||
state.out_roughness = c.parse_value_input(node.inputs[1])
|
||||
# Prevent black material when metal = 1.0 and roughness = 0.0
|
||||
try:
|
||||
if float(state.out_roughness) < 0.00101:
|
||||
state.out_roughness = '0.001'
|
||||
except ValueError:
|
||||
pass
|
||||
state.out_metallic = '1.0'
|
||||
else:
|
||||
def parse_bsdfglossy(node: bpy.types.ShaderNodeBsdfAnisotropic, out_socket: NodeSocket, state: ParserState) -> None:
|
||||
@ -237,12 +233,6 @@ else:
|
||||
c.write_normal(node.inputs[4])
|
||||
state.out_basecol = c.parse_vector_input(node.inputs[0])
|
||||
state.out_roughness = c.parse_value_input(node.inputs[1])
|
||||
# Prevent black material when metal = 1.0 and roughness = 0.0
|
||||
try:
|
||||
if float(state.out_roughness) < 0.00101:
|
||||
state.out_roughness = '0.001'
|
||||
except ValueError:
|
||||
pass
|
||||
state.out_metallic = '1.0'
|
||||
|
||||
|
||||
|
||||
@ -28,12 +28,11 @@ if lnx.is_reload(__name__):
|
||||
else:
|
||||
lnx.enable_reload(__name__)
|
||||
|
||||
|
||||
def parse_tex_brick(node: bpy.types.ShaderNodeTexBrick, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
|
||||
state.curshader.add_function(c_functions.str_tex_brick_blender)
|
||||
|
||||
if node.inputs[0].is_linked:
|
||||
co = c.parse_vector_input(node.inputs[0])
|
||||
if node.inputs['Vector'].is_linked:
|
||||
co = c.get_vector_input(node, ['Vector'])
|
||||
else:
|
||||
co = 'bposition'
|
||||
|
||||
@ -42,23 +41,23 @@ def parse_tex_brick(node: bpy.types.ShaderNodeTexBrick, out_socket: bpy.types.No
|
||||
squash_amount = node.squash
|
||||
squash_frequency = node.squash_frequency
|
||||
|
||||
col1 = c.parse_vector_input(node.inputs[1])
|
||||
col2 = c.parse_vector_input(node.inputs[2])
|
||||
col3 = c.parse_vector_input(node.inputs[3])
|
||||
scale = c.parse_value_input(node.inputs[4])
|
||||
mortar_size = c.parse_value_input(node.inputs[5])
|
||||
mortar_smooth = c.parse_value_input(node.inputs[6])
|
||||
bias = c.parse_value_input(node.inputs[7])
|
||||
brick_width = c.parse_value_input(node.inputs[8])
|
||||
row_height = c.parse_value_input(node.inputs[9])
|
||||
#res = f'tex_brick({co} * {scale}, {col1}, {col2}, {col3})'
|
||||
col1 = c.get_vector_input(node, ['Color1'])
|
||||
col2 = c.get_vector_input(node, ['Color2'])
|
||||
mortar = c.get_vector_input(node, ['Mortar'])
|
||||
scale = c.get_value_input(node, ['Scale'])
|
||||
mortar_size = c.get_value_input(node, ['Mortar Size'])
|
||||
mortar_smooth = c.get_value_input(node, ['Mortar Smooth'])
|
||||
bias = c.get_value_input(node, ['Bias'])
|
||||
brick_width = c.get_value_input(node, ['Brick Width'])
|
||||
row_height = c.get_value_input(node, ['Row Height'])
|
||||
#res = f'tex_brick({co} * {scale}, {col1}, {col2}, {mortar})'
|
||||
|
||||
# Color
|
||||
if out_socket == node.outputs[0]:
|
||||
res = f'tex_brick_blender({co}, {col1}, {col2}, {col3}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})'
|
||||
if out_socket == node.outputs['Color']:
|
||||
res = f'tex_brick_blender({co}, {col1}, {col2}, {mortar}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})'
|
||||
# Fac
|
||||
else:
|
||||
res = f'tex_brick_blender_f({co}, {col1}, {col2}, {col3}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})'
|
||||
res = f'tex_brick_blender_f({co}, {col1}, {col2}, {mortar}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})'
|
||||
|
||||
return res
|
||||
|
||||
@ -66,28 +65,28 @@ def parse_tex_brick(node: bpy.types.ShaderNodeTexBrick, out_socket: bpy.types.No
|
||||
def parse_tex_checker(node: bpy.types.ShaderNodeTexChecker, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
|
||||
state.curshader.add_function(c_functions.str_tex_checker)
|
||||
|
||||
if node.inputs[0].is_linked:
|
||||
co = c.parse_vector_input(node.inputs[0])
|
||||
if node.inputs['Vector'].is_linked:
|
||||
co = c.get_vector_input(node, ['Vector'])
|
||||
else:
|
||||
co = 'bposition'
|
||||
|
||||
scale = c.get_value_input(node, ['Scale'])
|
||||
|
||||
# Color
|
||||
if out_socket == node.outputs[0]:
|
||||
col1 = c.parse_vector_input(node.inputs[1])
|
||||
col2 = c.parse_vector_input(node.inputs[2])
|
||||
scale = c.parse_value_input(node.inputs[3])
|
||||
if out_socket == node.outputs['Color']:
|
||||
col1 = c.get_vector_input(node, ['Color1'])
|
||||
col2 = c.get_vector_input(node, ['Color2'])
|
||||
res = f'tex_checker({co}, {col1}, {col2}, {scale})'
|
||||
# Fac
|
||||
else:
|
||||
scale = c.parse_value_input(node.inputs[3])
|
||||
res = 'tex_checker_f({0}, {1})'.format(co, scale)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def parse_tex_gradient(node: bpy.types.ShaderNodeTexGradient, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
|
||||
if node.inputs[0].is_linked:
|
||||
co = c.parse_vector_input(node.inputs[0])
|
||||
if node.inputs['Vector'].is_linked:
|
||||
co = c.get_vector_input(node, ['Vector'])
|
||||
else:
|
||||
co = 'bposition'
|
||||
|
||||
@ -108,7 +107,7 @@ def parse_tex_gradient(node: bpy.types.ShaderNodeTexGradient, out_socket: bpy.ty
|
||||
f = f'max(1.0 - sqrt({co}.x * {co}.x + {co}.y * {co}.y + {co}.z * {co}.z), 0.0)'
|
||||
|
||||
# Color
|
||||
if out_socket == node.outputs[0]:
|
||||
if out_socket == node.outputs['Color']:
|
||||
res = f'vec3(clamp({f}, 0.0, 1.0))'
|
||||
# Fac
|
||||
else:
|
||||
@ -119,7 +118,7 @@ def parse_tex_gradient(node: bpy.types.ShaderNodeTexGradient, out_socket: bpy.ty
|
||||
|
||||
def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
|
||||
# Color or Alpha output
|
||||
use_color_out = out_socket == node.outputs[0]
|
||||
use_color_out = out_socket == node.outputs['Color']
|
||||
|
||||
if state.context == ParserContext.OBJECT:
|
||||
tex_store = c.store_var_name(node)
|
||||
@ -147,11 +146,12 @@ def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.No
|
||||
state.curshader.write_textures += 1
|
||||
if node.lnx_material_param and tex['file'] is not None:
|
||||
tex_default_file = tex['file']
|
||||
unpremultiply = node.image is not None and node.image.alpha_mode != 'CHANNEL_PACKED'
|
||||
if use_color_out:
|
||||
to_linear = node.image is not None and node.image.colorspace_settings.name == 'sRGB'
|
||||
res = f'{c.texture_store(node, tex, tex_name, to_linear, tex_link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)}.rgb'
|
||||
res = f'{c.texture_store(node, tex, tex_name, to_linear, unpremultiply, tex_link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)}.rgb'
|
||||
else:
|
||||
res = f'{c.texture_store(node, tex, tex_name, tex_link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)}.a'
|
||||
res = f'{c.texture_store(node, tex, tex_name, unpremultiply, tex_link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)}.a'
|
||||
state.curshader.write_textures -= 1
|
||||
return res
|
||||
|
||||
@ -162,8 +162,8 @@ def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.No
|
||||
'file': ''
|
||||
}
|
||||
if use_color_out:
|
||||
return '{0}.rgb'.format(c.texture_store(node, tex, tex_name, to_linear=False, tex_link=tex_link, is_lnx_mat_param=is_lnx_mat_param))
|
||||
return '{0}.a'.format(c.texture_store(node, tex, tex_name, to_linear=True, tex_link=tex_link, is_lnx_mat_param=is_lnx_mat_param))
|
||||
return '{0}.rgb'.format(c.texture_store(node, tex, tex_name, to_linear=False, unpremultiply=False, tex_link=tex_link, is_lnx_mat_param=is_lnx_mat_param))
|
||||
return '{0}.a'.format(c.texture_store(node, tex, tex_name, to_linear=True, unpremultiply=False, tex_link=tex_link, is_lnx_mat_param=is_lnx_mat_param))
|
||||
|
||||
# Pink color for missing texture
|
||||
else:
|
||||
@ -240,15 +240,15 @@ def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.No
|
||||
def parse_tex_magic(node: bpy.types.ShaderNodeTexMagic, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
|
||||
state.curshader.add_function(c_functions.str_tex_magic)
|
||||
|
||||
if node.inputs[0].is_linked:
|
||||
co = c.parse_vector_input(node.inputs[0])
|
||||
if node.inputs['Vector'].is_linked:
|
||||
co = c.get_vector_input(node, ['Vector'])
|
||||
else:
|
||||
co = 'bposition'
|
||||
|
||||
scale = c.parse_value_input(node.inputs[1])
|
||||
scale = c.get_value_input(node, ['Scale'])
|
||||
|
||||
# Color
|
||||
if out_socket == node.outputs[0]:
|
||||
if out_socket == node.outputs['Color']:
|
||||
res = f'tex_magic({co} * {scale} * 4.0)'
|
||||
# Fac
|
||||
else:
|
||||
@ -260,17 +260,17 @@ if bpy.app.version < (4, 1, 0):
|
||||
def parse_tex_musgrave(node: bpy.types.ShaderNodeTexMusgrave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
|
||||
state.curshader.add_function(c_functions.str_tex_musgrave)
|
||||
|
||||
if node.inputs[0].is_linked:
|
||||
co = c.parse_vector_input(node.inputs[0])
|
||||
if node.inputs['Vector'].is_linked:
|
||||
co = c.get_vector_input(node, ['Vector'])
|
||||
else:
|
||||
co = 'bposition'
|
||||
|
||||
scale = c.parse_value_input(node.inputs['Scale'])
|
||||
detail = c.parse_value_input(node.inputs[3])
|
||||
distortion = c.parse_value_input(node.inputs[4])
|
||||
|
||||
res = f'tex_musgrave_f({co} * {scale} * 0.5, {detail}, {distortion})'
|
||||
|
||||
scale = c.get_value_input(node, ['Scale'])
|
||||
detail = c.get_value_input(node, ['Detail'])
|
||||
dimension = c.get_value_input(node, ['Dimension'])
|
||||
|
||||
res = f'tex_musgrave_f({co} * {scale} * 0.5, {detail}, {dimension})' # FIXME: a `distortion` is applied instead of a `dimension`
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@ -280,28 +280,30 @@ def parse_tex_noise(node: bpy.types.ShaderNodeTexNoise, out_socket: bpy.types.No
|
||||
c.assets_add(os.path.join(lnx.utils.get_sdk_path(), 'leenkx', 'Assets', 'noise256.png'))
|
||||
c.assets_add_embedded_data('noise256.png')
|
||||
state.curshader.add_uniform('sampler2D snoise256', link='$noise256.png')
|
||||
if node.inputs[0].is_linked:
|
||||
co = c.parse_vector_input(node.inputs[0])
|
||||
if node.inputs['Vector'].is_linked:
|
||||
co = c.get_vector_input(node, ['Vector'])
|
||||
else:
|
||||
co = 'bposition'
|
||||
scale = c.parse_value_input(node.inputs[2])
|
||||
detail = c.parse_value_input(node.inputs[3])
|
||||
roughness = c.parse_value_input(node.inputs[4])
|
||||
distortion = c.parse_value_input(node.inputs[5])
|
||||
scale = c.get_value_input(node, ['Scale'])
|
||||
detail = c.get_value_input(node, ['Detail'])
|
||||
roughness = c.get_value_input(node, ['Roughness'])
|
||||
distortion = c.get_value_input(node, ['Distortion'])
|
||||
if bpy.app.version >= (4, 1, 0):
|
||||
if node.noise_type == "FBM":
|
||||
state.curshader.add_function(c_functions.str_tex_musgrave)
|
||||
if out_socket == node.outputs[1]:
|
||||
if out_socket == node.outputs['Color']:
|
||||
res = 'vec3(tex_musgrave_f({0} * {1}, {2}, {3}), tex_musgrave_f({0} * {1} + 120.0, {2}, {3}), tex_musgrave_f({0} * {1} + 168.0, {2}, {3}))'.format(co, scale, detail, distortion)
|
||||
else:
|
||||
res = f'tex_musgrave_f({co} * {scale} * 1.0, {detail}, {distortion})'
|
||||
else:
|
||||
if out_socket == node.outputs[1]:
|
||||
if out_socket == node.outputs['Color']:
|
||||
res = 'vec3(tex_noise({0} * {1},{2},{3}), tex_noise({0} * {1} + 120.0,{2},{3}), tex_noise({0} * {1} + 168.0,{2},{3}))'.format(co, scale, detail, distortion)
|
||||
else:
|
||||
res = 'tex_noise({0} * {1},{2},{3})'.format(co, scale, detail, distortion)
|
||||
if node.normalize:
|
||||
res = f'(1.0 - ({res}))'
|
||||
else:
|
||||
if out_socket == node.outputs[1]:
|
||||
if out_socket == node.outputs['Color']:
|
||||
res = 'vec3(tex_noise({0} * {1},{2},{3}), tex_noise({0} * {1} + 120.0,{2},{3}), tex_noise({0} * {1} + 168.0,{2},{3}))'.format(co, scale, detail, distortion)
|
||||
else:
|
||||
res = 'tex_noise({0} * {1},{2},{3})'.format(co, scale, detail, distortion)
|
||||
@ -311,7 +313,7 @@ if bpy.app.version < (5, 0, 0):
|
||||
def parse_tex_pointdensity(node: bpy.types.ShaderNodeTexPointDensity, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
|
||||
# Pass through
|
||||
# Color
|
||||
if out_socket == node.outputs[0]:
|
||||
if out_socket == node.outputs['Color']:
|
||||
return c.to_vec3([0.0, 0.0, 0.0])
|
||||
# Density
|
||||
else:
|
||||
@ -546,21 +548,24 @@ def parse_tex_voronoi(node: bpy.types.ShaderNodeTexVoronoi, out_socket: bpy.type
|
||||
m = 2
|
||||
elif node.distance == 'MINKOWSKI':
|
||||
m = 3
|
||||
# TODO: Add node.distance == 'MANHATHAN'
|
||||
# Add node.feature
|
||||
# Add node.voronoi_dimensions
|
||||
|
||||
c.write_procedurals()
|
||||
state.curshader.add_function(c_functions.str_tex_voronoi)
|
||||
|
||||
if node.inputs[0].is_linked:
|
||||
co = c.parse_vector_input(node.inputs[0])
|
||||
if node.inputs['Vector'].is_linked:
|
||||
co = c.get_vector_input(node, ['Vector'])
|
||||
else:
|
||||
co = 'bposition'
|
||||
|
||||
scale = c.parse_value_input(node.inputs[2])
|
||||
exp = c.parse_value_input(node.inputs[4])
|
||||
randomness = c.parse_value_input(node.inputs[5])
|
||||
scale = c.get_value_input(node, ['Scale'])
|
||||
exp = c.get_value_input(node, ['Exponent'])
|
||||
randomness = c.get_value_input(node, ['Randomness'])
|
||||
|
||||
# Color or Position
|
||||
if out_socket == node.outputs[1] or out_socket == node.outputs[2]:
|
||||
if out_socket == node.outputs['Color'] or out_socket == node.outputs['Position']:
|
||||
res = 'tex_voronoi({0}, {1}, {2}, {3}, {4}, {5})'.format(co, randomness, m, outp, scale, exp)
|
||||
# Distance
|
||||
else:
|
||||
@ -572,14 +577,16 @@ def parse_tex_voronoi(node: bpy.types.ShaderNodeTexVoronoi, out_socket: bpy.type
|
||||
def parse_tex_wave(node: bpy.types.ShaderNodeTexWave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
|
||||
c.write_procedurals()
|
||||
state.curshader.add_function(c_functions.str_tex_wave)
|
||||
if node.inputs[0].is_linked:
|
||||
co = c.parse_vector_input(node.inputs[0])
|
||||
if node.inputs['Vector'].is_linked:
|
||||
co = c.get_vector_input(node, ['Vector'])
|
||||
else:
|
||||
co = 'bposition'
|
||||
scale = c.parse_value_input(node.inputs[1])
|
||||
distortion = c.parse_value_input(node.inputs[2])
|
||||
detail = c.parse_value_input(node.inputs[3])
|
||||
detail_scale = c.parse_value_input(node.inputs[4])
|
||||
scale = c.get_value_input(node, ['Scale'])
|
||||
distortion = c.get_value_input(node, ['Distortion'])
|
||||
detail = c.get_value_input(node, ['Detail'])
|
||||
detail_scale = c.get_value_input(node, ['Detail Scale'])
|
||||
phase_offset = c.get_value_input(node, ['Phase Offset'])
|
||||
|
||||
if node.wave_profile == 'SIN':
|
||||
wave_profile = 0
|
||||
else:
|
||||
@ -590,10 +597,10 @@ def parse_tex_wave(node: bpy.types.ShaderNodeTexWave, out_socket: bpy.types.Node
|
||||
wave_type = 1
|
||||
|
||||
# Color
|
||||
if out_socket == node.outputs[0]:
|
||||
res = 'vec3(tex_wave_f({0} * {1},{2},{3},{4},{5},{6}))'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale)
|
||||
if out_socket == node.outputs['Color']:
|
||||
res = 'vec3(tex_wave_f({0} * {1},{2},{3},{4},{5},{6},{7}))'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale, phase_offset)
|
||||
# Fac
|
||||
else:
|
||||
res = 'tex_wave_f({0} * {1},{2},{3},{4},{5},{6})'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale)
|
||||
res = 'tex_wave_f({0} * {1},{2},{3},{4},{5},{6},{7})'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale, phase_offset)
|
||||
|
||||
return res
|
||||
|
||||
@ -25,8 +25,8 @@ else:
|
||||
|
||||
|
||||
def parse_curvevec(node: bpy.types.ShaderNodeVectorCurve, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
|
||||
fac = c.parse_value_input(node.inputs[0])
|
||||
vec = c.parse_vector_input(node.inputs[1])
|
||||
fac = c.get_value_input(node, ['Fac'])
|
||||
vec = c.get_vector_input(node, ['Vector'])
|
||||
curves = node.mapping.curves
|
||||
name = c.node_name(node.name)
|
||||
# mapping.curves[0].points[0].handle_type # bezier curve
|
||||
@ -42,19 +42,17 @@ def parse_bump(node: bpy.types.ShaderNodeBump, out_socket: bpy.types.NodeSocket,
|
||||
return 'vec3(0.0)'
|
||||
|
||||
# Interpolation strength
|
||||
strength = c.parse_value_input(node.inputs[0])
|
||||
# Height multiplier
|
||||
# distance = c.parse_value_input(node.inputs[1])
|
||||
height = c.parse_value_input(node.inputs[2])
|
||||
strength = c.get_value_input(node, ['Strength'])
|
||||
# distance = c.get_value_input(node, ['Distance'])
|
||||
height = c.get_value_input(node, ['Height'])
|
||||
# normal = c.get_vector_input(node, ['Normal'])
|
||||
|
||||
state.current_pass = ParserPass.DX_SCREEN_SPACE
|
||||
height_dx = c.parse_value_input(node.inputs[2])
|
||||
height_dx = c.get_value_input(node, ['Height'])
|
||||
state.current_pass = ParserPass.DY_SCREEN_SPACE
|
||||
height_dy = c.parse_value_input(node.inputs[2])
|
||||
height_dy = c.get_value_input(node, ['Height'])
|
||||
state.current_pass = ParserPass.REGULAR
|
||||
|
||||
# nor = c.parse_vector_input(node.inputs[3])
|
||||
|
||||
if height_dx != height or height_dy != height:
|
||||
tangent = f'{c.dfdx_fine("wposition")} + n * ({height_dx} - {height})'
|
||||
bitangent = f'{c.dfdy_fine("wposition")} + n * ({height_dy} - {height})'
|
||||
@ -79,11 +77,12 @@ def parse_bump(node: bpy.types.ShaderNodeBump, out_socket: bpy.types.NodeSocket,
|
||||
|
||||
|
||||
def parse_mapping(node: bpy.types.ShaderNodeMapping, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
|
||||
# TODO: Add support for "Normal" type
|
||||
# Only "Point", "Texture" and "Vector" types supported for now..
|
||||
# More information about the order of operations for this node:
|
||||
# https://docs.blender.org/manual/en/latest/render/shader_nodes/vector/mapping.html#properties
|
||||
|
||||
input_vector: bpy.types.NodeSocket = node.inputs[0]
|
||||
input_vector: bpy.types.NodeSocket = node.inputs['Vector']
|
||||
input_location: bpy.types.NodeSocket = node.inputs['Location']
|
||||
input_rotation: bpy.types.NodeSocket = node.inputs['Rotation']
|
||||
input_scale: bpy.types.NodeSocket = node.inputs['Scale']
|
||||
@ -145,44 +144,48 @@ def parse_normal(node: bpy.types.ShaderNodeNormal, out_socket: bpy.types.NodeSoc
|
||||
return nor1
|
||||
|
||||
elif out_socket == node.outputs['Dot']:
|
||||
nor2 = c.parse_vector_input(node.inputs["Normal"])
|
||||
nor2 = c.get_vector_input(node, ["Normal"])
|
||||
return f'dot({nor1}, {nor2})'
|
||||
|
||||
|
||||
def parse_normalmap(node: bpy.types.ShaderNodeNormalMap, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
|
||||
if state.curshader == state.tese:
|
||||
return c.parse_vector_input(node.inputs[1])
|
||||
return c.get_vector_input(node, ["Normal"])
|
||||
else:
|
||||
# TODO:
|
||||
# space = node.space
|
||||
# map = node.uv_map
|
||||
# Color
|
||||
c.parse_normal_map_color_input(node.inputs[1], node.inputs[0])
|
||||
c.parse_normal_map_color_input(node.inputs['Color'], node.inputs['Strength'])
|
||||
return 'n'
|
||||
|
||||
|
||||
def parse_vectortransform(node: bpy.types.ShaderNodeVectorTransform, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
|
||||
# type = node.vector_type
|
||||
# TODO:
|
||||
# vector_type = node.vector_type
|
||||
# conv_from = node.convert_from
|
||||
# conv_to = node.convert_to
|
||||
# Pass through
|
||||
return c.parse_vector_input(node.inputs[0])
|
||||
return c.get_vector_input(node, ['Vector'])
|
||||
|
||||
|
||||
def parse_displacement(node: bpy.types.ShaderNodeDisplacement, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
|
||||
height = c.parse_value_input(node.inputs[0])
|
||||
midlevel = c.parse_value_input(node.inputs[1])
|
||||
scale = c.parse_value_input(node.inputs[2])
|
||||
nor = c.parse_vector_input(node.inputs[3])
|
||||
# TODO:
|
||||
# space = node.space
|
||||
height = c.get_value_input(node, ['Height'])
|
||||
midlevel = c.get_value_input(node, ['Midlevel'])
|
||||
scale = c.get_value_input(node, ['Scale'])
|
||||
nor = c.get_vector_input(node, ['Normal'])
|
||||
return f'(vec3({height}) * {scale})'
|
||||
|
||||
def parse_vectorrotate(node: bpy.types.ShaderNodeVectorRotate, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
|
||||
|
||||
type = node.rotation_type
|
||||
input_vector: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[0])
|
||||
input_center: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[1])
|
||||
input_axis: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[2])
|
||||
input_angle: bpy.types.NodeSocket = c.parse_value_input(node.inputs[3])
|
||||
input_rotation: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[4])
|
||||
input_vector: bpy.types.NodeSocket = c.get_vector_input(node, ['Vector'])
|
||||
input_center: bpy.types.NodeSocket = c.get_vector_input(node, ['Center'])
|
||||
input_axis: bpy.types.NodeSocket = c.get_vector_input(node, ['Axis'])
|
||||
input_angle: bpy.types.NodeSocket = c.get_value_input(node, ['Angle'])
|
||||
input_rotation: bpy.types.NodeSocket = c.get_vector_input(node, ['Rotation'])
|
||||
|
||||
if node.invert:
|
||||
input_invert = "0"
|
||||
|
||||
@ -1,100 +1,212 @@
|
||||
from typing import Optional
|
||||
|
||||
|
||||
|
||||
import lnx.material.cycles as cycles
|
||||
|
||||
import lnx.material.mat_state as mat_state
|
||||
|
||||
import lnx.material.make_skin as make_skin
|
||||
|
||||
import lnx.material.make_particle as make_particle
|
||||
|
||||
import lnx.material.make_inst as make_inst
|
||||
|
||||
import lnx.material.make_tess as make_tess
|
||||
|
||||
import lnx.material.make_morph_target as make_morph_target
|
||||
|
||||
from lnx.material.shader import Shader, ShaderContext
|
||||
|
||||
import lnx.utils
|
||||
|
||||
|
||||
|
||||
if lnx.is_reload(__name__):
|
||||
|
||||
cycles = lnx.reload_module(cycles)
|
||||
|
||||
mat_state = lnx.reload_module(mat_state)
|
||||
|
||||
make_skin = lnx.reload_module(make_skin)
|
||||
|
||||
make_particle = lnx.reload_module(make_particle)
|
||||
|
||||
make_inst = lnx.reload_module(make_inst)
|
||||
|
||||
make_tess = lnx.reload_module(make_tess)
|
||||
|
||||
make_morph_target = lnx.reload_module(make_morph_target)
|
||||
|
||||
lnx.material.shader = lnx.reload_module(lnx.material.shader)
|
||||
|
||||
from lnx.material.shader import Shader, ShaderContext
|
||||
|
||||
lnx.utils = lnx.reload_module(lnx.utils)
|
||||
|
||||
else:
|
||||
|
||||
lnx.enable_reload(__name__)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def write_vertpos(vert):
|
||||
|
||||
billboard = mat_state.material.lnx_billboard
|
||||
|
||||
particle = mat_state.material.lnx_particle_flag
|
||||
|
||||
# Particles
|
||||
|
||||
if particle:
|
||||
if lnx.utils.get_rp().lnx_particles == 'On':
|
||||
|
||||
if lnx.utils.get_rp().lnx_particles == 'GPU':
|
||||
|
||||
make_particle.write(vert, particle_info=cycles.particle_info)
|
||||
|
||||
# Billboards
|
||||
|
||||
if billboard == 'spherical':
|
||||
|
||||
vert.add_uniform('mat4 WV', '_worldViewMatrix')
|
||||
|
||||
vert.add_uniform('mat4 P', '_projectionMatrix')
|
||||
|
||||
vert.write('gl_Position = P * (WV * vec4(0.0, 0.0, spos.z, 1.0) + vec4(spos.x, spos.y, 0.0, 0.0));')
|
||||
|
||||
else:
|
||||
|
||||
vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix')
|
||||
|
||||
vert.write('gl_Position = WVP * spos;')
|
||||
|
||||
else:
|
||||
|
||||
# Billboards
|
||||
|
||||
if billboard == 'spherical':
|
||||
|
||||
vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrixSphere')
|
||||
|
||||
elif billboard == 'cylindrical':
|
||||
|
||||
vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrixCylinder')
|
||||
|
||||
else: # off
|
||||
|
||||
vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix')
|
||||
|
||||
vert.write('gl_Position = WVP * spos;')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def write_norpos(con_mesh: ShaderContext, vert: Shader, declare=False, write_nor=True):
|
||||
|
||||
is_bone = con_mesh.is_elem('bone')
|
||||
|
||||
is_morph = con_mesh.is_elem('morph')
|
||||
|
||||
if is_morph:
|
||||
|
||||
make_morph_target.morph_pos(vert)
|
||||
|
||||
if is_bone:
|
||||
|
||||
make_skin.skin_pos(vert)
|
||||
|
||||
if write_nor:
|
||||
|
||||
prep = 'vec3 ' if declare else ''
|
||||
|
||||
if is_morph:
|
||||
|
||||
make_morph_target.morph_nor(vert, is_bone, prep)
|
||||
|
||||
if is_bone:
|
||||
|
||||
make_skin.skin_nor(vert, is_morph, prep)
|
||||
|
||||
if not is_morph and not is_bone:
|
||||
|
||||
vert.write_attrib(prep + 'wnormal = normalize(N * vec3(nor.xy, pos.w));')
|
||||
|
||||
if con_mesh.is_elem('ipos'):
|
||||
|
||||
make_inst.inst_pos(con_mesh, vert)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def write_tex_coords(con_mesh: ShaderContext, vert: Shader, frag: Shader, tese: Optional[Shader]):
|
||||
|
||||
rpdat = lnx.utils.get_rp()
|
||||
|
||||
|
||||
|
||||
if con_mesh.is_elem('tex'):
|
||||
|
||||
vert.add_out('vec2 texCoord')
|
||||
|
||||
vert.add_uniform('float texUnpack', link='_texUnpack')
|
||||
|
||||
if mat_state.material.lnx_tilesheet_flag:
|
||||
|
||||
if mat_state.material.lnx_particle_flag and rpdat.lnx_particles == 'On':
|
||||
|
||||
make_particle.write_tilesheet(vert)
|
||||
|
||||
else:
|
||||
|
||||
vert.add_uniform('vec2 tilesheetOffset', '_tilesheetOffset')
|
||||
vert.write_attrib('texCoord = tex * texUnpack + tilesheetOffset;')
|
||||
|
||||
vert.add_uniform('vec2 tilesheetFlip', '_tilesheetFlip')
|
||||
|
||||
vert.add_uniform('vec2 tilesheetTiles', '_tilesheetTiles')
|
||||
|
||||
vert.write_attrib('vec2 tileSize = vec2(1.0 / tilesheetTiles.x, 1.0 / tilesheetTiles.y);')
|
||||
|
||||
vert.write_attrib('vec2 tileUV = tex * texUnpack;')
|
||||
|
||||
vert.write_attrib('tileUV.x = mix(tileUV.x, tileSize.x - tileUV.x, tilesheetFlip.x);')
|
||||
|
||||
vert.write_attrib('tileUV.y = mix(tileUV.y, tileSize.y - tileUV.y, tilesheetFlip.y);')
|
||||
|
||||
vert.write_attrib('texCoord = tileUV + tilesheetOffset;')
|
||||
|
||||
else:
|
||||
|
||||
vert.write_attrib('texCoord = tex * texUnpack;')
|
||||
|
||||
|
||||
|
||||
if tese is not None:
|
||||
|
||||
tese.write_pre = True
|
||||
|
||||
make_tess.interpolate(tese, 'texCoord', 2, declare_out=frag.contains('texCoord'))
|
||||
|
||||
tese.write_pre = False
|
||||
|
||||
|
||||
|
||||
if con_mesh.is_elem('tex1'):
|
||||
|
||||
vert.add_out('vec2 texCoord1')
|
||||
|
||||
vert.add_uniform('float texUnpack', link='_texUnpack')
|
||||
|
||||
vert.write_attrib('texCoord1 = tex1 * texUnpack;')
|
||||
|
||||
if tese is not None:
|
||||
|
||||
tese.write_pre = True
|
||||
|
||||
make_tess.interpolate(tese, 'texCoord1', 2, declare_out=frag.contains('texCoord1'))
|
||||
|
||||
tese.write_pre = False
|
||||
|
||||
|
||||
@ -112,7 +112,7 @@ def make(context_id, rpasses, shadowmap=False, shadowmap_transparent=False):
|
||||
make_inst.inst_pos(con_depth, vert)
|
||||
|
||||
rpdat = lnx.utils.get_rp()
|
||||
if mat_state.material.lnx_particle_flag and rpdat.lnx_particles == 'On':
|
||||
if mat_state.material.lnx_particle_flag and rpdat.lnx_particles == 'GPU':
|
||||
make_particle.write(vert, shadowmap=shadowmap)
|
||||
|
||||
if is_disp:
|
||||
|
||||
@ -58,6 +58,7 @@ def make(context_id, rpasses):
|
||||
con['alpha_blend_destination'] = mat.lnx_blending_destination_alpha
|
||||
con['alpha_blend_operation'] = mat.lnx_blending_operation_alpha
|
||||
con['depth_write'] = False
|
||||
con['compare_mode'] = mat.lnx_compare_mode
|
||||
elif particle:
|
||||
pass
|
||||
# Depth prepass was performed, exclude mat with depth read that
|
||||
@ -500,7 +501,13 @@ def make_forward_solid(con_mesh):
|
||||
vert.add_uniform('float texUnpack', link='_texUnpack')
|
||||
if mat_state.material.lnx_tilesheet_flag:
|
||||
vert.add_uniform('vec2 tilesheetOffset', '_tilesheetOffset')
|
||||
vert.write('texCoord = tex * texUnpack + tilesheetOffset;')
|
||||
vert.add_uniform('vec2 tilesheetFlip', '_tilesheetFlip')
|
||||
vert.add_uniform('vec2 tilesheetTiles', '_tilesheetTiles')
|
||||
vert.write('vec2 tileSize = vec2(1.0 / tilesheetTiles.x, 1.0 / tilesheetTiles.y);')
|
||||
vert.write('vec2 tileUV = tex * texUnpack;')
|
||||
vert.write('tileUV.x = mix(tileUV.x, tileSize.x - tileUV.x, tilesheetFlip.x);')
|
||||
vert.write('tileUV.y = mix(tileUV.y, tileSize.y - tileUV.y, tilesheetFlip.y);')
|
||||
vert.write('texCoord = tileUV + tilesheetOffset;')
|
||||
else:
|
||||
vert.write('texCoord = tex * texUnpack;')
|
||||
|
||||
@ -571,7 +578,7 @@ def make_forward(con_mesh):
|
||||
frag.write('fragColor[0].rgb = tonemapFilmic(fragColor[0].rgb);')
|
||||
|
||||
# Particle opacity
|
||||
if mat_state.material.lnx_particle_flag and lnx.utils.get_rp().lnx_particles == 'On' and mat_state.material.lnx_particle_fade:
|
||||
if mat_state.material.lnx_particle_flag and lnx.utils.get_rp().lnx_particles == 'GPU' and mat_state.material.lnx_particle_fade:
|
||||
frag.write('fragColor[0].rgb *= p_fade;')
|
||||
|
||||
|
||||
@ -695,10 +702,10 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False):
|
||||
if '_Brdf' in wrd.world_defs:
|
||||
frag.write('envl.rgb *= 1.0 - F;')
|
||||
if '_Rad' in wrd.world_defs:
|
||||
frag.write('envl += prefilteredColor * F;')
|
||||
frag.write('envl += prefilteredColor * F * 1.5;')
|
||||
elif '_EnvCol' in wrd.world_defs:
|
||||
frag.add_uniform('vec3 backgroundCol', link='_backgroundCol')
|
||||
frag.write('envl += backgroundCol * F;')
|
||||
frag.write('envl += backgroundCol * F * 1.5;')
|
||||
|
||||
frag.add_uniform('float envmapStrength', link='_envmapStrength')
|
||||
frag.write('envl *= envmapStrength * occlusion;')
|
||||
|
||||
@ -43,7 +43,7 @@ def write(vert, particle_info=None, shadowmap=False):
|
||||
for tex_slot in psettings.texture_slots:
|
||||
if not tex_slot: break
|
||||
if not tex_slot.use_map_size: break # TODO: check also for other influences
|
||||
if tex_slot.texture and tex_slot.texture.use_color_ramp:
|
||||
if tex_slot.texture.use_color_ramp:
|
||||
if tex_slot.texture.color_ramp and tex_slot.texture.color_ramp.elements:
|
||||
ramp_el_len = len(tex_slot.texture.color_ramp.elements.items())
|
||||
for element in tex_slot.texture.color_ramp.elements:
|
||||
@ -250,7 +250,7 @@ def write(vert, particle_info=None, shadowmap=False):
|
||||
vert.write('wnormal = normalize(rotate_around(wnormal, r_angle));')
|
||||
|
||||
# Particle fade
|
||||
if mat_state.material.lnx_particle_flag and lnx.utils.get_rp().lnx_particles == 'On' and mat_state.material.lnx_particle_fade:
|
||||
if mat_state.material.lnx_particle_flag and lnx.utils.get_rp().lnx_particles == 'GPU' and mat_state.material.lnx_particle_fade:
|
||||
vert.add_out('float p_fade')
|
||||
vert.write('p_fade = sin(min((p_age / 2) * 3.141592, 3.141592));')
|
||||
|
||||
|
||||
@ -93,7 +93,7 @@ def make_gi(context_id):
|
||||
|
||||
# Voxelized particles
|
||||
particle = mat_state.material.lnx_particle_flag
|
||||
if particle and rpdat.lnx_particles == 'On':
|
||||
if particle and rpdat.lnx_particles == 'GPU':
|
||||
# make_particle.write(vert, particle_info=cycles.particle_info)
|
||||
frag.write_pre = True
|
||||
frag.write('const float p_index = 0;')
|
||||
|
||||
@ -59,6 +59,7 @@ def get_signature(mat, object: bpy.types.Object):
|
||||
sign += mat.lnx_billboard
|
||||
sign += '_skin' if lnx.utils.export_bone_data(object) else '0'
|
||||
sign += '_morph' if lnx.utils.export_morph_targets(object) else '0'
|
||||
# sign += '_tilesheet' if mat.lnx_tilesheet_flag else '0'
|
||||
return sign
|
||||
|
||||
def traverse_tree2(node, ar):
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import bpy
|
||||
import lnx.utils
|
||||
|
||||
if lnx.is_reload(__name__):
|
||||
@ -286,7 +287,8 @@ class Shader:
|
||||
ar[0] = 'floats'
|
||||
ar[1] = ar[1].split('[', 1)[0]
|
||||
elif ar[0] == 'mat4' and '[' in ar[1]:
|
||||
ar[0] = 'floats'
|
||||
if not (ar[1].startswith('LWVPSpot') and not '_Clusters' in bpy.data.worlds['Lnx'].world_defs): #HACK: do not convert mat4 to floats when using single spot lights
|
||||
ar[0] = 'floats'
|
||||
ar[1] = ar[1].split('[', 1)[0]
|
||||
self.context.add_constant(ar[0], ar[1], link=link, default_value=default_value, is_lnx_mat_param=is_lnx_mat_param)
|
||||
if top:
|
||||
|
||||
@ -31,7 +31,7 @@ else:
|
||||
lnx.enable_reload(__name__)
|
||||
|
||||
# Leenkx version
|
||||
lnx_version = '2025.1'
|
||||
lnx_version = '2026.9'
|
||||
lnx_commit = '$Id: 6b2644d47db169cedd95593497cc283207d23a74 $'
|
||||
|
||||
def get_project_html5_copy(self):
|
||||
@ -151,7 +151,10 @@ def init_properties():
|
||||
bpy.types.World.lnx_project_version_autoinc = BoolProperty(name="Auto-increment Build Number", description="Auto-increment build number", default=True, update=assets.invalidate_compiler_cache)
|
||||
bpy.types.World.lnx_project_bundle = StringProperty(name="Bundle", description="Exported project bundle", default="org.leenkx3d", update=assets.invalidate_compiler_cache, set=set_project_bundle, get=get_project_bundle)
|
||||
# External Blend Files
|
||||
bpy.types.World.lnx_external_blends_path = StringProperty(name="External Blends", description="Directory containing external blend files to include in export", default="", subtype='DIR_PATH', update=assets.invalidate_compiler_cache)
|
||||
if bpy.app.version >= (4, 5, 0):
|
||||
bpy.types.World.lnx_external_blends_path = StringProperty(name="External Blends", description="Directory containing external blend files to include in export", default="", subtype='DIR_PATH', update=assets.invalidate_compiler_cache, options={'PATH_SUPPORTS_BLEND_RELATIVE'})
|
||||
else:
|
||||
bpy.types.World.lnx_external_blends_path = StringProperty(name="External Blends", description="Directory containing external blend files to include in export", default="", subtype='DIR_PATH', update=assets.invalidate_compiler_cache)
|
||||
# Android Settings
|
||||
bpy.types.World.lnx_project_android_sdk_min = IntProperty(name="Minimal Version SDK", description="Minimal Version Android SDK", default=23, min=14, max=30, update=assets.invalidate_compiler_cache)
|
||||
bpy.types.World.lnx_project_android_sdk_target = IntProperty(name="Target Version SDK", description="Target Version Android SDK", default=26, min=26, max=30, update=assets.invalidate_compiler_cache)
|
||||
@ -196,8 +199,14 @@ def init_properties():
|
||||
bpy.types.World.lnx_project_win_build_cpu = IntProperty(name="CPU Count", description="Specifies the maximum number of concurrent processes to use when building", default=1, min=1, max=multiprocessing.cpu_count())
|
||||
bpy.types.World.lnx_project_win_build_open = BoolProperty(name="Open Build Directory", description="Open the build directory after successfully assemble", default=False)
|
||||
|
||||
bpy.types.World.lnx_project_icon = StringProperty(name="Icon (PNG)", description="Exported project icon, must be a PNG image", default="", subtype="FILE_PATH", update=assets.invalidate_compiler_cache)
|
||||
bpy.types.World.lnx_project_root = StringProperty(name="Root", description="Set root folder for linked assets", default="", subtype="DIR_PATH", update=assets.invalidate_compiler_cache)
|
||||
if bpy.app.version >= (4, 5, 0):
|
||||
bpy.types.World.lnx_project_icon = StringProperty(name="Icon (PNG)", description="Exported project icon, must be a PNG image", default="", subtype="FILE_PATH", update=assets.invalidate_compiler_cache, options={'PATH_SUPPORTS_BLEND_RELATIVE'})
|
||||
else:
|
||||
bpy.types.World.lnx_project_icon = StringProperty(name="Icon (PNG)", description="Exported project icon, must be a PNG image", default="", subtype="FILE_PATH", update=assets.invalidate_compiler_cache)
|
||||
if bpy.app.version >= (4, 5, 0):
|
||||
bpy.types.World.lnx_project_root = StringProperty(name="Root", description="Set root folder for linked assets", default="", subtype="DIR_PATH", update=assets.invalidate_compiler_cache, options={'PATH_SUPPORTS_BLEND_RELATIVE'})
|
||||
else:
|
||||
bpy.types.World.lnx_project_root = StringProperty(name="Root", description="Set root folder for linked assets", default="", subtype="DIR_PATH", update=assets.invalidate_compiler_cache)
|
||||
bpy.types.World.lnx_physics = EnumProperty(
|
||||
items=[('Disabled', 'Disabled', 'Disabled'),
|
||||
('Auto', 'Auto', 'Auto'),
|
||||
@ -418,9 +427,6 @@ def init_properties():
|
||||
|
||||
bpy.types.Object.lnx_relative_physics_constraint = BoolProperty(name="Relative Physics Constraint", description="Add physics constraint relative to the parent object or collection when spawned", default=False)
|
||||
bpy.types.Object.lnx_animation_enabled = BoolProperty(name="Animation", description="Enable skinning & timeline animation", default=True)
|
||||
bpy.types.Object.lnx_tilesheet = StringProperty(name="Tilesheet", description="Set tilesheet animation", default='')
|
||||
bpy.types.Object.lnx_tilesheet_action = StringProperty(name="Tilesheet Action", description="Set startup action", default='')
|
||||
bpy.types.Object.lnx_use_custom_tilesheet_node = BoolProperty(name="Use custom tilesheet node", description="Use custom tilesheet shader node", default=False)
|
||||
# For speakers
|
||||
bpy.types.Speaker.lnx_play_on_start = BoolProperty(name="Play on Start", description="Play this sound automatically", default=False)
|
||||
bpy.types.Speaker.lnx_loop = BoolProperty(name="Loop", description="Loop this sound", default=False)
|
||||
@ -616,8 +622,31 @@ def init_properties():
|
||||
description="Particles have independent transform updates following emitter compared to a static baked particle system used if emitters dont generally move around.",
|
||||
default=True
|
||||
)
|
||||
bpy.types.ParticleSettings.lnx_is_unique = BoolProperty(name="Is Unique", description="Make this particle system look different each time it starts", default=False)
|
||||
bpy.types.ParticleSettings.lnx_is_unique = BoolProperty(name="Is Unique", description="Make this particle system look different each time it starts. Only affects GPU particles. Default behavior for CPU particles", default=False)
|
||||
bpy.types.ParticleSettings.lnx_local_coords = BoolProperty(name="Local Coords", description="Keep spawned particles parented to their emitter. Only affects CPU particles. Default behavior for GPU particles at the moment", default=False)
|
||||
bpy.types.ParticleSettings.lnx_loop = BoolProperty(name="Loop", description="Loop this particle system", default=False)
|
||||
bpy.types.ParticleSettings.lnx_use_rotations = BoolProperty(name="Use Rotations", description="Enable particle rotations", default=False)
|
||||
bpy.types.ParticleSettings.lnx_rotation_mode = EnumProperty(
|
||||
name="Rotation Mode",
|
||||
description="Rotation orientation mode",
|
||||
items=[
|
||||
('NONE', "None", "None"),
|
||||
('NOR', "Normal", "Normal"),
|
||||
('NOR_TAN', "Normal-Tangent", "Normal-Tangent"),
|
||||
('VEL', "Velocity/Hair", "Velocity/Hair"),
|
||||
('GLOB_X', "Global X", "Global X"),
|
||||
('GLOB_Y', "Global Y", "Global Y"),
|
||||
('GLOB_Z', "Global Z", "Global Z"),
|
||||
('OB_X', "Object X", "Object X"),
|
||||
('OB_Y', "Object Y", "Object Y"),
|
||||
('OB_Z', "Object Z", "Object Z"),
|
||||
],
|
||||
default='NONE'
|
||||
)
|
||||
bpy.types.ParticleSettings.lnx_rotation_factor_random = FloatProperty(name="Rotation Random", description="Random rotation factor", default=0.0, min=0.0, max=1.0)
|
||||
bpy.types.ParticleSettings.lnx_phase_factor = FloatProperty(name="Phase", description="Rotation phase factor", default=0.0, min=0.0, max=1.0)
|
||||
bpy.types.ParticleSettings.lnx_phase_factor_random = FloatProperty(name="Phase Random", description="Random rotation phase factor", default=0.0, min=0.0, max=1.0)
|
||||
bpy.types.ParticleSettings.lnx_use_dynamic_rotation = BoolProperty(name="Dynamic Rotation", description="Enable dynamic rotation updates", default=False)
|
||||
bpy.types.ParticleSettings.lnx_count_mult = FloatProperty(name="Multiply Count", description="Multiply particle count when rendering in Leenkx", default=1.0)
|
||||
# Actions
|
||||
bpy.types.Action.lnx_root_motion_pos = BoolProperty(name="Root Motion Position", description="Enable position root motion", default=False)
|
||||
|
||||
@ -66,7 +66,7 @@ def update_preset(self, context):
|
||||
rpdat.rp_stereo = False
|
||||
rpdat.rp_voxelgi_resolution = '32'
|
||||
rpdat.lnx_voxelgi_size = 0.125
|
||||
rpdat.rp_voxels = 'Voxel GI'
|
||||
rpdat.rp_voxels = 'Off'
|
||||
rpdat.rp_render_to_texture = True
|
||||
rpdat.rp_supersampling = '1'
|
||||
rpdat.rp_antialiasing = 'SMAA'
|
||||
@ -142,7 +142,7 @@ def update_preset(self, context):
|
||||
rpdat.rp_hdr = True
|
||||
rpdat.rp_background = 'World'
|
||||
rpdat.rp_stereo = False
|
||||
rpdat.rp_voxels = 'Voxel GI'
|
||||
rpdat.rp_voxels = 'Off'
|
||||
rpdat.rp_voxelgi_resolution = '64'
|
||||
rpdat.lnx_voxelgi_size = 0.125
|
||||
rpdat.lnx_voxelgi_step = 0.01
|
||||
@ -444,7 +444,7 @@ class LnxRPListItem(bpy.types.PropertyGroup):
|
||||
rp_draw_order: EnumProperty(
|
||||
items=[('Auto', 'Auto', 'Auto'),
|
||||
('Distance', 'Distance', 'Distance'),
|
||||
('Shader', 'Shader', 'Shader')],
|
||||
('Index', 'Index', 'Index')],
|
||||
name='Draw Order', description='Sort objects', default='Auto', update=assets.invalidate_compiled_data)
|
||||
rp_depth_texture: BoolProperty(name="Depth Texture", description="Current render-path state", default=False)
|
||||
rp_depth_texture_state: EnumProperty(
|
||||
@ -669,9 +669,10 @@ class LnxRPListItem(bpy.types.PropertyGroup):
|
||||
('Off', 'Off', 'Off')],
|
||||
name='Shape key', description='Enable shape keys', default='On', update=assets.invalidate_shader_cache)
|
||||
lnx_particles: EnumProperty(
|
||||
items=[('On', 'On', 'On'),
|
||||
items=[('GPU', 'GPU', 'GPU'),
|
||||
('CPU', 'CPU', 'CPU'),
|
||||
('Off', 'Off', 'Off')],
|
||||
name='Particles', description='Enable particle simulation', default='On', update=assets.invalidate_shader_cache)
|
||||
name='Particles', description='Enable particle simulation', default='GPU', update=assets.invalidate_shader_cache)
|
||||
# Material override flags
|
||||
lnx_culling: BoolProperty(name="Culling", default=True)
|
||||
lnx_two_sided_area_light: BoolProperty(name="Two-Sided Area Light", description="Emit light from both faces of area plane", default=False, update=assets.invalidate_shader_cache)
|
||||
|
||||
@ -1,211 +1,120 @@
|
||||
import bpy
|
||||
from bpy.props import *
|
||||
|
||||
|
||||
class LnxTilesheetEventListItem(bpy.types.PropertyGroup):
|
||||
"""An event triggered on a specific frame within a tilesheet action."""
|
||||
name: StringProperty(
|
||||
name="Event Name",
|
||||
description="Name of the event to trigger",
|
||||
default="event")
|
||||
|
||||
frame_prop: IntProperty(
|
||||
name="Frame",
|
||||
description="Frame number when this event triggers",
|
||||
default=0,
|
||||
min=0)
|
||||
|
||||
|
||||
class LnxTilesheetActionListItem(bpy.types.PropertyGroup):
|
||||
"""An action (animation sequence) within a tilesheet with per-action properties."""
|
||||
name: StringProperty(
|
||||
name="Name",
|
||||
description="A name for this item",
|
||||
description="Name of this tilesheet action",
|
||||
default="Untitled")
|
||||
|
||||
start_prop: IntProperty(
|
||||
name="Start",
|
||||
description="A name for this item",
|
||||
description="Starting frame index",
|
||||
default=0)
|
||||
|
||||
end_prop: IntProperty(
|
||||
name="End",
|
||||
description="A name for this item",
|
||||
description="Ending frame index",
|
||||
default=0)
|
||||
|
||||
loop_prop: BoolProperty(
|
||||
name="Loop",
|
||||
description="A name for this item",
|
||||
description="Whether this action should loop",
|
||||
default=True)
|
||||
|
||||
class LNX_UL_TilesheetActionList(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
# We could write some code to decide which icon to use here...
|
||||
custom_icon = 'OBJECT_DATAMODE'
|
||||
|
||||
# Make sure your code supports all 3 layout types
|
||||
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||
layout.prop(item, "name", text="", emboss=False, icon=custom_icon)
|
||||
|
||||
elif self.layout_type in {'GRID'}:
|
||||
layout.alignment = 'CENTER'
|
||||
layout.label(text="", icon = custom_icon)
|
||||
|
||||
class LnxTilesheetActionListNewItem(bpy.types.Operator):
|
||||
# Add a new item to the list
|
||||
bl_idname = "lnx_tilesheetactionlist.new_item"
|
||||
bl_label = "Add a new item"
|
||||
|
||||
def execute(self, context):
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index]
|
||||
trait.lnx_tilesheetactionlist.add()
|
||||
trait.lnx_tilesheetactionlist_index = len(trait.lnx_tilesheetactionlist) - 1
|
||||
return{'FINISHED'}
|
||||
|
||||
class LnxTilesheetActionListDeleteItem(bpy.types.Operator):
|
||||
"""Delete the selected item from the list"""
|
||||
bl_idname = "lnx_tilesheetactionlist.delete_item"
|
||||
bl_label = "Deletes an item"
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
"""Enable if there's something in the list"""
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
if len(wrd.lnx_tilesheetlist) == 0:
|
||||
return False
|
||||
trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index]
|
||||
return len(trait.lnx_tilesheetactionlist) > 0
|
||||
|
||||
def execute(self, context):
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index]
|
||||
list = trait.lnx_tilesheetactionlist
|
||||
index = trait.lnx_tilesheetactionlist_index
|
||||
|
||||
list.remove(index)
|
||||
|
||||
if index > 0:
|
||||
index = index - 1
|
||||
|
||||
trait.lnx_tilesheetactionlist_index = index
|
||||
return{'FINISHED'}
|
||||
|
||||
class LnxTilesheetActionListMoveItem(bpy.types.Operator):
|
||||
"""Move an item in the list"""
|
||||
bl_idname = "lnx_tilesheetactionlist.move_item"
|
||||
bl_label = "Move an item in the list"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
direction: EnumProperty(
|
||||
items=(
|
||||
('UP', 'Up', ""),
|
||||
('DOWN', 'Down', "")
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
"""Enable if there's something in the list"""
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
if len(wrd.lnx_tilesheetlist) == 0:
|
||||
return False
|
||||
trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index]
|
||||
return len(trait.lnx_tilesheetactionlist) > 0
|
||||
|
||||
def move_index(self):
|
||||
# Move index of an item render queue while clamping it
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index]
|
||||
index = trait.lnx_tilesheetactionlist_index
|
||||
list_length = len(trait.lnx_tilesheetactionlist) - 1
|
||||
new_index = 0
|
||||
|
||||
if self.direction == 'UP':
|
||||
new_index = index - 1
|
||||
elif self.direction == 'DOWN':
|
||||
new_index = index + 1
|
||||
|
||||
new_index = max(0, min(new_index, list_length))
|
||||
trait.lnx_tilesheetactionlist.move(index, new_index)
|
||||
trait.lnx_tilesheetactionlist_index = new_index
|
||||
|
||||
def execute(self, context):
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index]
|
||||
list = trait.lnx_tilesheetactionlist
|
||||
index = trait.lnx_tilesheetactionlist_index
|
||||
|
||||
if self.direction == 'DOWN':
|
||||
neighbor = index + 1
|
||||
self.move_index()
|
||||
|
||||
elif self.direction == 'UP':
|
||||
neighbor = index - 1
|
||||
self.move_index()
|
||||
else:
|
||||
return{'CANCELLED'}
|
||||
return{'FINISHED'}
|
||||
|
||||
class LnxTilesheetListItem(bpy.types.PropertyGroup):
|
||||
name: StringProperty(
|
||||
name="Name",
|
||||
description="A name for this item",
|
||||
default="Untitled")
|
||||
|
||||
tilesx_prop: IntProperty(
|
||||
name="Tiles X",
|
||||
description="A name for this item",
|
||||
default=0)
|
||||
description="Number of horizontal tiles for this action",
|
||||
default=1,
|
||||
min=1)
|
||||
|
||||
tilesy_prop: IntProperty(
|
||||
name="Tiles Y",
|
||||
description="A name for this item",
|
||||
default=0)
|
||||
description="Number of vertical tiles for this action",
|
||||
default=1,
|
||||
min=1)
|
||||
|
||||
framerate_prop: FloatProperty(
|
||||
framerate_prop: IntProperty(
|
||||
name="Frame Rate",
|
||||
description="A name for this item",
|
||||
default=4.0)
|
||||
description="Animation frame rate for this action (frames per second)",
|
||||
default=4,
|
||||
min=1)
|
||||
|
||||
lnx_tilesheetactionlist: CollectionProperty(type=LnxTilesheetActionListItem)
|
||||
lnx_tilesheetactionlist_index: IntProperty(name="Index for lnx_tilesheetactionlist", default=0)
|
||||
mesh_prop: StringProperty(
|
||||
name="Mesh",
|
||||
description="Optional mesh data to swap to when playing this action (brings its own material/texture/UVs)",
|
||||
default="")
|
||||
|
||||
class LNX_UL_TilesheetList(bpy.types.UIList):
|
||||
# Events list for this action
|
||||
events: CollectionProperty(type=LnxTilesheetEventListItem)
|
||||
events_index: IntProperty(name="Event Index", default=0)
|
||||
|
||||
class LNX_UL_TilesheetActionList(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
# We could write some code to decide which icon to use here...
|
||||
custom_icon = 'OBJECT_DATAMODE'
|
||||
custom_icon = 'PLAY'
|
||||
|
||||
# Make sure your code supports all 3 layout types
|
||||
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||
layout.prop(item, "name", text="", emboss=False, icon=custom_icon)
|
||||
|
||||
elif self.layout_type in {'GRID'}:
|
||||
layout.alignment = 'CENTER'
|
||||
layout.label(text="", icon=custom_icon)
|
||||
|
||||
class LnxTilesheetListNewItem(bpy.types.Operator):
|
||||
"""Add a new item to the list"""
|
||||
bl_idname = "lnx_tilesheetlist.new_item"
|
||||
bl_label = "Add a new item"
|
||||
class LnxTilesheetActionListNewItem(bpy.types.Operator):
|
||||
"""Add a new action to the tilesheet"""
|
||||
bl_idname = "lnx_tilesheetactionlist.new_item"
|
||||
bl_label = "Add Action"
|
||||
|
||||
def execute(self, context):
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
wrd.lnx_tilesheetlist.add()
|
||||
wrd.lnx_tilesheetlist_index = len(wrd.lnx_tilesheetlist) - 1
|
||||
return{'FINISHED'}
|
||||
obj = context.object
|
||||
if obj is None:
|
||||
return {'CANCELLED'}
|
||||
obj.lnx_tilesheet_actionlist.add()
|
||||
obj.lnx_tilesheet_actionlist_index = len(obj.lnx_tilesheet_actionlist) - 1
|
||||
return {'FINISHED'}
|
||||
|
||||
class LnxTilesheetListDeleteItem(bpy.types.Operator):
|
||||
"""Delete the selected item from the list"""
|
||||
bl_idname = "lnx_tilesheetlist.delete_item"
|
||||
bl_label = "Deletes an item"
|
||||
class LnxTilesheetActionListDeleteItem(bpy.types.Operator):
|
||||
"""Delete the selected action from the tilesheet"""
|
||||
bl_idname = "lnx_tilesheetactionlist.delete_item"
|
||||
bl_label = "Delete Action"
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
""" Enable if there's something in the list """
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
return len(wrd.lnx_tilesheetlist) > 0
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
return obj is not None and len(obj.lnx_tilesheet_actionlist) > 0
|
||||
|
||||
def execute(self, context):
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
list = wrd.lnx_tilesheetlist
|
||||
index = wrd.lnx_tilesheetlist_index
|
||||
obj = context.object
|
||||
action_list = obj.lnx_tilesheet_actionlist
|
||||
index = obj.lnx_tilesheet_actionlist_index
|
||||
|
||||
list.remove(index)
|
||||
action_list.remove(index)
|
||||
|
||||
if index > 0:
|
||||
index = index - 1
|
||||
|
||||
wrd.lnx_tilesheetlist_index = index
|
||||
return{'FINISHED'}
|
||||
obj.lnx_tilesheet_actionlist_index = index
|
||||
return {'FINISHED'}
|
||||
|
||||
class LnxTilesheetListMoveItem(bpy.types.Operator):
|
||||
"""Move an item in the list"""
|
||||
bl_idname = "lnx_tilesheetlist.move_item"
|
||||
bl_label = "Move an item in the list"
|
||||
class LnxTilesheetActionListMoveItem(bpy.types.Operator):
|
||||
"""Move an action in the list"""
|
||||
bl_idname = "lnx_tilesheetactionlist.move_item"
|
||||
bl_label = "Move Action"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
direction: EnumProperty(
|
||||
@ -215,56 +124,266 @@ class LnxTilesheetListMoveItem(bpy.types.Operator):
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
""" Enable if there's something in the list. """
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
return len(wrd.lnx_tilesheetlist) > 0
|
||||
|
||||
def move_index(self):
|
||||
# Move index of an item render queue while clamping it
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
index = wrd.lnx_tilesheetlist_index
|
||||
list_length = len(wrd.lnx_tilesheetlist) - 1
|
||||
new_index = 0
|
||||
|
||||
if self.direction == 'UP':
|
||||
new_index = index - 1
|
||||
elif self.direction == 'DOWN':
|
||||
new_index = index + 1
|
||||
|
||||
new_index = max(0, min(new_index, list_length))
|
||||
wrd.lnx_tilesheetlist.move(index, new_index)
|
||||
wrd.lnx_tilesheetlist_index = new_index
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
return obj is not None and len(obj.lnx_tilesheet_actionlist) > 0
|
||||
|
||||
def execute(self, context):
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
list = wrd.lnx_tilesheetlist
|
||||
index = wrd.lnx_tilesheetlist_index
|
||||
obj = context.object
|
||||
action_list = obj.lnx_tilesheet_actionlist
|
||||
index = obj.lnx_tilesheet_actionlist_index
|
||||
list_length = len(action_list) - 1
|
||||
|
||||
if self.direction == 'DOWN':
|
||||
neighbor = index + 1
|
||||
self.move_index()
|
||||
if self.direction == 'UP':
|
||||
new_index = max(0, index - 1)
|
||||
else: # DOWN
|
||||
new_index = min(list_length, index + 1)
|
||||
|
||||
elif self.direction == 'UP':
|
||||
neighbor = index - 1
|
||||
self.move_index()
|
||||
action_list.move(index, new_index)
|
||||
obj.lnx_tilesheet_actionlist_index = new_index
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class LnxTilesheetEventListNewItem(bpy.types.Operator):
|
||||
"""Add a new event to the current action"""
|
||||
bl_idname = "lnx_tilesheetactionlist.new_event"
|
||||
bl_label = "Add Event"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
return obj is not None and len(obj.lnx_tilesheet_actionlist) > 0
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
if obj.lnx_tilesheet_actionlist_index < 0:
|
||||
return {'CANCELLED'}
|
||||
action = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index]
|
||||
action.events.add()
|
||||
action.events_index = len(action.events) - 1
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class LnxTilesheetEventListDeleteItem(bpy.types.Operator):
|
||||
"""Delete the selected event from the current action"""
|
||||
bl_idname = "lnx_tilesheetactionlist.delete_event"
|
||||
bl_label = "Delete Event"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
if obj is None or len(obj.lnx_tilesheet_actionlist) == 0:
|
||||
return False
|
||||
if obj.lnx_tilesheet_actionlist_index < 0:
|
||||
return False
|
||||
action = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index]
|
||||
return len(action.events) > 0
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
action = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index]
|
||||
events = action.events
|
||||
index = action.events_index
|
||||
|
||||
events.remove(index)
|
||||
|
||||
if index > 0:
|
||||
action.events_index = index - 1
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class LnxTilesheetSlice(bpy.types.Operator):
|
||||
"""Slice the UV map based on tile dimensions - scales UVs to fit one tile"""
|
||||
bl_idname = "lnx_tilesheet.slice"
|
||||
bl_label = "Slice"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
if obj is None or obj.type != 'MESH':
|
||||
return False
|
||||
if len(obj.lnx_tilesheet_actionlist) == 0:
|
||||
return False
|
||||
if obj.lnx_tilesheet_actionlist_index < 0:
|
||||
return False
|
||||
action = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index]
|
||||
# Check if action has a mesh specified, and that mesh has UVs
|
||||
if action.mesh_prop != '':
|
||||
mesh_data = bpy.data.meshes.get(action.mesh_prop)
|
||||
if mesh_data is None or not mesh_data.uv_layers:
|
||||
return False
|
||||
else:
|
||||
return{'CANCELLED'}
|
||||
return{'FINISHED'}
|
||||
# Fall back to object's own mesh
|
||||
if not obj.data.uv_layers:
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
||||
if obj.lnx_tilesheet_actionlist_index < 0:
|
||||
self.report({'ERROR'}, "No action selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
action = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index]
|
||||
tiles_x = action.tilesx_prop
|
||||
tiles_y = action.tilesy_prop
|
||||
|
||||
if tiles_x < 1 or tiles_y < 1:
|
||||
self.report({'ERROR'}, "Tiles X and Y must be at least 1")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Get mesh from action's mesh_prop, or fall back to object's mesh
|
||||
if action.mesh_prop != '':
|
||||
mesh = bpy.data.meshes.get(action.mesh_prop)
|
||||
if mesh is None:
|
||||
self.report({'ERROR'}, f"Mesh '{action.mesh_prop}' not found")
|
||||
return {'CANCELLED'}
|
||||
mesh_name = action.mesh_prop
|
||||
else:
|
||||
mesh = obj.data
|
||||
mesh_name = obj.data.name
|
||||
|
||||
if not mesh.uv_layers:
|
||||
self.report({'ERROR'}, f"Mesh '{mesh_name}' has no UV layers")
|
||||
return {'CANCELLED'}
|
||||
|
||||
uv_layer = mesh.uv_layers.active.data
|
||||
|
||||
# Calculate target tile size
|
||||
tile_width = 1.0 / tiles_x
|
||||
tile_height = 1.0 / tiles_y
|
||||
|
||||
# Find current UV bounding box
|
||||
min_u = min_v = float('inf')
|
||||
max_u = max_v = float('-inf')
|
||||
|
||||
for loop_uv in uv_layer:
|
||||
min_u = min(min_u, loop_uv.uv[0])
|
||||
max_u = max(max_u, loop_uv.uv[0])
|
||||
min_v = min(min_v, loop_uv.uv[1])
|
||||
max_v = max(max_v, loop_uv.uv[1])
|
||||
|
||||
current_width = max_u - min_u
|
||||
current_height = max_v - min_v
|
||||
|
||||
if current_width == 0 or current_height == 0:
|
||||
self.report({'ERROR'}, f"UV map on '{mesh_name}' has zero dimensions")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Scale and position UVs to fit in first tile (0,0) to (tile_width, tile_height)
|
||||
for loop_uv in uv_layer:
|
||||
# Normalize to 0-1 range
|
||||
norm_u = (loop_uv.uv[0] - min_u) / current_width
|
||||
norm_v = (loop_uv.uv[1] - min_v) / current_height
|
||||
# Scale to tile size
|
||||
loop_uv.uv[0] = norm_u * tile_width
|
||||
loop_uv.uv[1] = norm_v * tile_height
|
||||
|
||||
mesh.update()
|
||||
|
||||
self.report({'INFO'}, f"UVs sliced to {tiles_x}x{tiles_y} grid (tile size: {tile_width:.3f} x {tile_height:.3f})")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class LNX_PT_TilesheetPanel(bpy.types.Panel):
|
||||
bl_label = "Leenkx Tilesheet"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object is not None and context.object.type == 'MESH'
|
||||
|
||||
def draw_header(self, context):
|
||||
obj = context.object
|
||||
self.layout.prop(obj, "lnx_tilesheet_enabled", text="")
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
obj = context.object
|
||||
|
||||
layout.enabled = obj.lnx_tilesheet_enabled
|
||||
|
||||
# Start action dropdown
|
||||
layout.prop_search(obj, "lnx_tilesheet_default_action", obj, "lnx_tilesheet_actionlist", text="Start Action")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(obj, "lnx_tilesheet_flipx")
|
||||
row.prop(obj, "lnx_tilesheet_flipy")
|
||||
|
||||
# Actions list
|
||||
layout.separator()
|
||||
layout.label(text="Actions")
|
||||
rows = 2
|
||||
if len(obj.lnx_tilesheet_actionlist) > 1:
|
||||
rows = 4
|
||||
row = layout.row()
|
||||
row.template_list("LNX_UL_TilesheetActionList", "The_List", obj, "lnx_tilesheet_actionlist", obj, "lnx_tilesheet_actionlist_index", rows=rows)
|
||||
col = row.column(align=True)
|
||||
col.operator("lnx_tilesheetactionlist.new_item", icon='ADD', text="")
|
||||
col.operator("lnx_tilesheetactionlist.delete_item", icon='REMOVE', text="")
|
||||
|
||||
if len(obj.lnx_tilesheet_actionlist) > 1:
|
||||
col.separator()
|
||||
op = col.operator("lnx_tilesheetactionlist.move_item", icon='TRIA_UP', text="")
|
||||
op.direction = 'UP'
|
||||
op = col.operator("lnx_tilesheetactionlist.move_item", icon='TRIA_DOWN', text="")
|
||||
op.direction = 'DOWN'
|
||||
|
||||
# Selected action details (per-action properties)
|
||||
if obj.lnx_tilesheet_actionlist_index >= 0 and len(obj.lnx_tilesheet_actionlist) > 0:
|
||||
adat = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index]
|
||||
box = layout.box()
|
||||
# Grid dimensions
|
||||
row = box.row()
|
||||
row.use_property_split = False
|
||||
row.prop(adat, "tilesx_prop")
|
||||
row.prop(adat, "tilesy_prop")
|
||||
row = box.row()
|
||||
row.operator("lnx_tilesheet.slice", text="Slice")
|
||||
# Frame range
|
||||
row = box.row()
|
||||
row.use_property_split = False
|
||||
row.prop(adat, "start_prop")
|
||||
row.prop(adat, "end_prop")
|
||||
# Framerate and loop
|
||||
box.prop(adat, "framerate_prop")
|
||||
box.prop(adat, "loop_prop")
|
||||
# Optional mesh (dropdown from bpy.data.meshes)
|
||||
box.prop_search(adat, "mesh_prop", bpy.data, "meshes", text="Mesh")
|
||||
|
||||
# Events section
|
||||
box.separator()
|
||||
box.label(text="Events")
|
||||
row = box.row()
|
||||
col = row.column()
|
||||
for i, evt in enumerate(adat.events):
|
||||
evt_row = col.row(align=True)
|
||||
evt_row.prop(evt, "name", text="")
|
||||
evt_row.prop(evt, "frame_prop", text="Frame")
|
||||
row = box.row(align=True)
|
||||
row.operator("lnx_tilesheetactionlist.new_event", icon='ADD', text="Add Event")
|
||||
row.operator("lnx_tilesheetactionlist.delete_event", icon='REMOVE', text="Remove Event")
|
||||
|
||||
|
||||
__REG_CLASSES = (
|
||||
LnxTilesheetEventListItem,
|
||||
LnxTilesheetActionListItem,
|
||||
LNX_UL_TilesheetActionList,
|
||||
LnxTilesheetActionListNewItem,
|
||||
LnxTilesheetActionListDeleteItem,
|
||||
LnxTilesheetActionListMoveItem,
|
||||
|
||||
LnxTilesheetListItem,
|
||||
LNX_UL_TilesheetList,
|
||||
LnxTilesheetListNewItem,
|
||||
LnxTilesheetListDeleteItem,
|
||||
LnxTilesheetListMoveItem,
|
||||
LnxTilesheetEventListNewItem,
|
||||
LnxTilesheetEventListDeleteItem,
|
||||
LnxTilesheetSlice,
|
||||
LNX_PT_TilesheetPanel,
|
||||
)
|
||||
__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES)
|
||||
|
||||
@ -272,5 +391,28 @@ __reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES)
|
||||
def register():
|
||||
__reg_classes()
|
||||
|
||||
bpy.types.World.lnx_tilesheetlist = CollectionProperty(type=LnxTilesheetListItem)
|
||||
bpy.types.World.lnx_tilesheetlist_index = IntProperty(name="Index for lnx_tilesheetlist", default=0)
|
||||
# Tilesheet properties on Object (one tilesheet per object)
|
||||
bpy.types.Object.lnx_tilesheet_enabled = BoolProperty(
|
||||
name="Tilesheet",
|
||||
description="Enable tilesheet animation for this object",
|
||||
default=False)
|
||||
|
||||
bpy.types.Object.lnx_tilesheet_default_action = StringProperty(
|
||||
name="Start Action",
|
||||
description="Start action to play on spawn",
|
||||
default="")
|
||||
|
||||
bpy.types.Object.lnx_tilesheet_flipx = BoolProperty(
|
||||
name="Flip X",
|
||||
description="Flip the tilesheet horizontally",
|
||||
default=False)
|
||||
|
||||
bpy.types.Object.lnx_tilesheet_flipy = BoolProperty(
|
||||
name="Flip Y",
|
||||
description="Flip the tilesheet vertically",
|
||||
default=False)
|
||||
|
||||
bpy.types.Object.lnx_tilesheet_actionlist = CollectionProperty(type=LnxTilesheetActionListItem)
|
||||
bpy.types.Object.lnx_tilesheet_actionlist_index = IntProperty(
|
||||
name="Action Index",
|
||||
default=0)
|
||||
|
||||
@ -29,7 +29,7 @@ else:
|
||||
|
||||
ICON_HAXE = ui_icons.get_id('haxe')
|
||||
ICON_NODES = 'NODETREE'
|
||||
ICON_CANVAS = 'NODE_COMPOSITING'
|
||||
ICON_CANVAS = 'WINDOW'
|
||||
ICON_BUNDLED = ui_icons.get_id('bundle')
|
||||
ICON_WASM = ui_icons.get_id('wasm')
|
||||
|
||||
@ -131,7 +131,7 @@ class LNX_UL_TraitList(bpy.types.UIList):
|
||||
elif item.type_prop == "WebAssembly":
|
||||
custom_icon_value = ICON_WASM
|
||||
elif item.type_prop == "UI Canvas":
|
||||
custom_icon = "NODE_COMPOSITING"
|
||||
custom_icon = "WINDOW"
|
||||
elif item.type_prop == "Bundled Script":
|
||||
custom_icon_value = ICON_BUNDLED
|
||||
elif item.type_prop == "Logic Nodes":
|
||||
@ -996,7 +996,7 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b
|
||||
row.operator("lnx.new_canvas", icon="FILE_NEW").is_object = is_object
|
||||
column = row.column(align=True)
|
||||
column.enabled = item.canvas_name_prop != ''
|
||||
column.operator("lnx.edit_canvas", icon="NODE_COMPOSITING").is_object = is_object
|
||||
column.operator("lnx.edit_canvas", icon="WINDOW").is_object = is_object
|
||||
|
||||
refresh_op = "lnx.refresh_object_scripts" if is_object else "lnx.refresh_scripts"
|
||||
row.operator(refresh_op, text="Refresh", icon="FILE_REFRESH")
|
||||
|
||||
@ -16,8 +16,7 @@ PROP_TYPE_ICONS = {
|
||||
"CameraObject": "CAMERA_DATA",
|
||||
"LightObject": "LIGHT_DATA",
|
||||
"MeshObject": "MESH_DATA",
|
||||
"SpeakerObject": "OUTLINER_DATA_SPEAKER",
|
||||
"TSceneFormat": "SCENE_DATA"
|
||||
"SpeakerObject": "OUTLINER_DATA_SPEAKER"
|
||||
}
|
||||
|
||||
|
||||
@ -61,8 +60,7 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup):
|
||||
("CameraObject", "Camera Object", "Camera Object Type"),
|
||||
("LightObject", "Light Object", "Light Object Type"),
|
||||
("MeshObject", "Mesh Object", "Mesh Object Type"),
|
||||
("SpeakerObject", "Speaker Object", "Speaker Object Type"),
|
||||
("TSceneFormat", "Scene", "Scene Type")),
|
||||
("SpeakerObject", "Speaker Object", "Speaker Object Type")),
|
||||
name="Type",
|
||||
description="The type of this property",
|
||||
default="String")
|
||||
@ -74,7 +72,6 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup):
|
||||
value_vec3: FloatVectorProperty(name="Value", size=3)
|
||||
value_vec4: FloatVectorProperty(name="Value", size=4)
|
||||
value_object: PointerProperty(name="Value", type=bpy.types.Object, poll=filter_objects)
|
||||
value_scene: PointerProperty(name="Value", type=bpy.types.Scene)
|
||||
else:
|
||||
type: EnumProperty(
|
||||
items=(
|
||||
@ -90,8 +87,7 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup):
|
||||
("CameraObject", "Camera Object", "Camera Object Type"),
|
||||
("LightObject", "Light Object", "Light Object Type"),
|
||||
("MeshObject", "Mesh Object", "Mesh Object Type"),
|
||||
("SpeakerObject", "Speaker Object", "Speaker Object Type"),
|
||||
("TSceneFormat", "Scene", "Scene Type")),
|
||||
("SpeakerObject", "Speaker Object", "Speaker Object Type")),
|
||||
name="Type",
|
||||
description="The type of this property",
|
||||
default="String",
|
||||
@ -104,7 +100,6 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup):
|
||||
value_vec3: FloatVectorProperty(name="Value", size=3, override={"LIBRARY_OVERRIDABLE"})
|
||||
value_vec4: FloatVectorProperty(name="Value", size=4, override={"LIBRARY_OVERRIDABLE"})
|
||||
value_object: PointerProperty(name="Value", type=bpy.types.Object, poll=filter_objects, override={"LIBRARY_OVERRIDABLE"})
|
||||
value_scene: PointerProperty(name="Value", type=bpy.types.Scene, override={"LIBRARY_OVERRIDABLE"})
|
||||
|
||||
def set_value(self, val):
|
||||
# Would require way too much effort, so it's out of scope here.
|
||||
@ -153,10 +148,6 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup):
|
||||
if self.value_object is not None:
|
||||
return self.value_object.name
|
||||
return ""
|
||||
if self.type == "TSceneFormat":
|
||||
if self.value_scene is not None:
|
||||
return self.value_scene.name
|
||||
return ""
|
||||
|
||||
|
||||
return self.value_string
|
||||
@ -176,8 +167,6 @@ class LNX_UL_PropList(bpy.types.UIList):
|
||||
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||
if item.type.endswith("Object"):
|
||||
sp.prop_search(item, "value_object", context.scene, "objects", text="", icon=custom_icon)
|
||||
elif item.type.endswith("TSceneFormat"):
|
||||
sp.prop_search(item, "value_scene", bpy.data, "scenes", text="", icon=custom_icon)
|
||||
else:
|
||||
use_emboss = item.type in ("Bool", "String")
|
||||
sp.prop(item, item_value_ref, text="", emboss=use_emboss)
|
||||
|
||||
@ -92,16 +92,6 @@ class LNX_PT_ObjectPropsPanel(bpy.types.Panel):
|
||||
|
||||
if obj.type == 'MESH':
|
||||
layout.prop(obj, 'lnx_instanced')
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
layout.prop_search(obj, "lnx_tilesheet", wrd, "lnx_tilesheetlist", text="Tilesheet")
|
||||
if obj.lnx_tilesheet != '':
|
||||
selected_ts = None
|
||||
for ts in wrd.lnx_tilesheetlist:
|
||||
if ts.name == obj.lnx_tilesheet:
|
||||
selected_ts = ts
|
||||
break
|
||||
layout.prop_search(obj, "lnx_tilesheet_action", selected_ts, "lnx_tilesheetactionlist", text="Action")
|
||||
layout.prop(obj, "lnx_use_custom_tilesheet_node")
|
||||
|
||||
# Properties list
|
||||
lnx.props_properties.draw_properties(layout, obj)
|
||||
@ -227,6 +217,7 @@ class LNX_PT_ParticlesPropsPanel(bpy.types.Panel):
|
||||
layout.prop(obj.settings, 'lnx_auto_start')
|
||||
layout.prop(obj.settings, 'lnx_dynamic_emitter')
|
||||
layout.prop(obj.settings, 'lnx_is_unique')
|
||||
layout.prop(obj.settings, 'lnx_local_coords')
|
||||
layout.prop(obj.settings, 'lnx_loop')
|
||||
layout.prop(obj.settings, 'lnx_count_mult')
|
||||
|
||||
@ -864,11 +855,13 @@ class LNX_PT_LeenkxPlayerPanel(bpy.types.Panel):
|
||||
row.operator("lnx.play", icon="PLAY")
|
||||
else:
|
||||
if bpy.app.version < (3, 0, 0):
|
||||
row.operator("lnx.stop", icon="CANCEL", text="")
|
||||
elif bpy.app.version > (3, 0, 0) and bpy.app.version < (4, 3, 2):
|
||||
row.operator("lnx.stop", icon="SEQUENCE_COLOR_01", text="")
|
||||
row.operator("lnx.stop", icon="CANCEL")
|
||||
elif bpy.app.version > (3, 0, 0) and bpy.app.version < (4, 5, 0):
|
||||
row.operator("lnx.stop", icon="SEQUENCE_COLOR_01")
|
||||
elif bpy.app.version >= (4, 5, 0):
|
||||
row.operator("lnx.stop", icon="STRIP_COLOR_01")
|
||||
else:
|
||||
row.operator("lnx.stop", icon="EVENT_MEDIASTOP", text="")
|
||||
row.operator("lnx.stop", icon="EVENT_MEDIASTOP")
|
||||
row.operator("lnx.clean_menu", icon="BRUSH_DATA")
|
||||
|
||||
col = layout.box().column()
|
||||
@ -1369,6 +1362,7 @@ class LeenkxStopButton(bpy.types.Operator):
|
||||
elif state.proc_build != None:
|
||||
state.proc_build.terminate()
|
||||
state.proc_build = None
|
||||
make.clear_external_scenes()
|
||||
|
||||
lnx.write_probes.check_last_cmft_time()
|
||||
|
||||
@ -1550,11 +1544,13 @@ class LNX_PT_TopbarPanel(bpy.types.Panel):
|
||||
row.operator("lnx.play", icon="PLAY", text="")
|
||||
else:
|
||||
if bpy.app.version < (3, 0, 0):
|
||||
row.operator("lnx.stop", icon="CANCEL", text="")
|
||||
elif bpy.app.version > (3, 0, 0) and bpy.app.version < (4, 3, 2):
|
||||
row.operator("lnx.stop", icon="SEQUENCE_COLOR_01", text="")
|
||||
row.operator("lnx.stop", icon="CANCEL")
|
||||
elif bpy.app.version > (3, 0, 0) and bpy.app.version < (4, 5, 0):
|
||||
row.operator("lnx.stop", icon="SEQUENCE_COLOR_01")
|
||||
elif bpy.app.version >= (4, 5, 0):
|
||||
row.operator("lnx.stop", icon="STRIP_COLOR_01")
|
||||
else:
|
||||
row.operator("lnx.stop", icon="EVENT_MEDIASTOP", text="")
|
||||
row.operator("lnx.stop", icon="EVENT_MEDIASTOP")
|
||||
row.operator("lnx.clean_menu", icon="BRUSH_DATA", text="")
|
||||
row.operator("lnx.open_editor", icon="DESKTOP", text="")
|
||||
row.operator("lnx.open_project_folder", icon="FILE_FOLDER", text="")
|
||||
@ -2435,64 +2431,6 @@ class LNX_PT_TerrainPanel(bpy.types.Panel):
|
||||
layout.operator('lnx.generate_terrain')
|
||||
layout.prop(scn, 'lnx_terrain_object')
|
||||
|
||||
class LNX_PT_TilesheetPanel(bpy.types.Panel):
|
||||
bl_label = "Leenkx Tilesheet"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "material"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
wrd = bpy.data.worlds['Lnx']
|
||||
|
||||
rows = 2
|
||||
if len(wrd.lnx_tilesheetlist) > 1:
|
||||
rows = 4
|
||||
row = layout.row()
|
||||
row.template_list("LNX_UL_TilesheetList", "The_List", wrd, "lnx_tilesheetlist", wrd, "lnx_tilesheetlist_index", rows=rows)
|
||||
col = row.column(align=True)
|
||||
col.operator("lnx_tilesheetlist.new_item", icon='ADD', text="")
|
||||
col.operator("lnx_tilesheetlist.delete_item", icon='REMOVE', text="")
|
||||
|
||||
if len(wrd.lnx_tilesheetlist) > 1:
|
||||
col.separator()
|
||||
op = col.operator("lnx_tilesheetlist.move_item", icon='TRIA_UP', text="")
|
||||
op.direction = 'UP'
|
||||
op = col.operator("lnx_tilesheetlist.move_item", icon='TRIA_DOWN', text="")
|
||||
op.direction = 'DOWN'
|
||||
|
||||
if wrd.lnx_tilesheetlist_index >= 0 and len(wrd.lnx_tilesheetlist) > 0:
|
||||
dat = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index]
|
||||
layout.prop(dat, "tilesx_prop")
|
||||
layout.prop(dat, "tilesy_prop")
|
||||
layout.prop(dat, "framerate_prop")
|
||||
|
||||
layout.label(text='Actions')
|
||||
rows = 2
|
||||
if len(dat.lnx_tilesheetactionlist) > 1:
|
||||
rows = 4
|
||||
row = layout.row()
|
||||
row.template_list("LNX_UL_TilesheetList", "The_List", dat, "lnx_tilesheetactionlist", dat, "lnx_tilesheetactionlist_index", rows=rows)
|
||||
col = row.column(align=True)
|
||||
col.operator("lnx_tilesheetactionlist.new_item", icon='ADD', text="")
|
||||
col.operator("lnx_tilesheetactionlist.delete_item", icon='REMOVE', text="")
|
||||
|
||||
if len(dat.lnx_tilesheetactionlist) > 1:
|
||||
col.separator()
|
||||
op = col.operator("lnx_tilesheetactionlist.move_item", icon='TRIA_UP', text="")
|
||||
op.direction = 'UP'
|
||||
op = col.operator("lnx_tilesheetactionlist.move_item", icon='TRIA_DOWN', text="")
|
||||
op.direction = 'DOWN'
|
||||
|
||||
if dat.lnx_tilesheetactionlist_index >= 0 and len(dat.lnx_tilesheetactionlist) > 0:
|
||||
adat = dat.lnx_tilesheetactionlist[dat.lnx_tilesheetactionlist_index]
|
||||
layout.prop(adat, "start_prop")
|
||||
layout.prop(adat, "end_prop")
|
||||
layout.prop(adat, "loop_prop")
|
||||
|
||||
class LnxPrintTraitsButton(bpy.types.Operator):
|
||||
bl_idname = 'lnx.print_traits'
|
||||
bl_label = 'Print All Traits'
|
||||
@ -3058,7 +2996,6 @@ __REG_CLASSES = (
|
||||
LNX_PT_LodPanel,
|
||||
LnxGenTerrainButton,
|
||||
LNX_PT_TerrainPanel,
|
||||
LNX_PT_TilesheetPanel,
|
||||
LnxPrintTraitsButton,
|
||||
LNX_PT_MaterialNodePanel,
|
||||
LNX_OT_UpdateFileSDK,
|
||||
|
||||
@ -380,7 +380,7 @@ def get_haxe_path():
|
||||
if get_os() == 'win':
|
||||
return get_kha_path() + '/Tools/windows_x64/haxe.exe'
|
||||
elif get_os() == 'mac':
|
||||
return get_kha_path() + '/Tools/macos/haxe'
|
||||
return get_kha_path() + '/Tools/macos_x64/haxe'
|
||||
else:
|
||||
return get_kha_path() + '/Tools/linux_x64/haxe'
|
||||
|
||||
@ -484,7 +484,7 @@ def fetch_script_props(filename: str):
|
||||
|
||||
# Property type is annotated
|
||||
if p_type is not None:
|
||||
if p_type.startswith("iron.object.") or p_type == "iron.data.SceneFormat.TSceneFormat":
|
||||
if p_type.startswith("iron.object."):
|
||||
p_type = p_type[12:]
|
||||
elif p_type.startswith("iron.math."):
|
||||
p_type = p_type[10:]
|
||||
@ -562,7 +562,7 @@ def get_type_default_value(prop_type: str):
|
||||
if prop_type == "Float":
|
||||
return 0.0
|
||||
if prop_type == "String" or prop_type in (
|
||||
"Object", "CameraObject", "LightObject", "MeshObject", "SpeakerObject", "TSceneFormat"):
|
||||
"Object", "CameraObject", "LightObject", "MeshObject", "SpeakerObject"):
|
||||
return ""
|
||||
if prop_type == "Bool":
|
||||
return False
|
||||
@ -804,13 +804,13 @@ def get_haxe_json_string(d: dict) -> str:
|
||||
return s
|
||||
|
||||
def asset_name(bdata):
|
||||
if bdata == None:
|
||||
return None
|
||||
s = bdata.name
|
||||
# Append library name if linked
|
||||
if bdata.library is not None:
|
||||
s += '_' + bdata.library.name
|
||||
return s
|
||||
"""Get the qualified asset name, with library suffix for linked data.
|
||||
|
||||
For local assets, returns just the name.
|
||||
For linked assets, returns 'name_libraryname' to ensure uniqueness.
|
||||
"""
|
||||
import lnx.linked_utils as linked_utils
|
||||
return linked_utils.asset_name(bdata)
|
||||
|
||||
def asset_path(s):
|
||||
"""Remove leading '//'"""
|
||||
@ -868,7 +868,7 @@ def check_blender_version(op: bpy.types.Operator):
|
||||
"""Check whether the Blender version is supported by Leenkx,
|
||||
if not, report in UI.
|
||||
"""
|
||||
if bpy.app.version[:2] not in [(4, 4), (4, 2), (3, 6), (3, 3)]:
|
||||
if bpy.app.version[:2] not in [(4, 5), (4, 4), (4, 2), (3, 6), (3, 3)]:
|
||||
op.report({'INFO'}, 'INFO: For Leenkx to work correctly, use a Blender LTS version')
|
||||
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import glob
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
@ -10,6 +11,7 @@ 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
|
||||
@ -17,6 +19,7 @@ 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)
|
||||
@ -24,6 +27,96 @@ 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)
|
||||
@ -70,6 +163,9 @@ def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, expo
|
||||
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()
|
||||
@ -78,13 +174,47 @@ def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, expo
|
||||
# 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) + """');
|
||||
|
||||
project.addSources('Sources');
|
||||
""")
|
||||
# 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'):
|
||||
@ -247,8 +377,7 @@ project.addSources('Sources');
|
||||
shaders_path = os.path.relpath(shaders_path, project_path).replace('\\', '/')
|
||||
khafile.write('project.addShaders("' + shaders_path + '", { noprocessing: true, noembed: ' + str(noembed).lower() + ' });\n')
|
||||
|
||||
# Move assets for published game to /data folder
|
||||
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')
|
||||
# Add define for data directory usage
|
||||
if use_data_dir:
|
||||
assets.add_khafile_def('lnx_data_dir')
|
||||
|
||||
@ -353,6 +482,10 @@ project.addSources('Sources');
|
||||
|
||||
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')
|
||||
@ -369,9 +502,6 @@ project.addSources('Sources');
|
||||
if wrd.lnx_winresize or state.target == 'html5':
|
||||
assets.add_khafile_def('lnx_resizable')
|
||||
|
||||
if get_winmode(wrd.lnx_winmode) == 1 and state.target.startswith('html5'):
|
||||
assets.add_khafile_def('kha_html5_disable_automatic_size_adjust')
|
||||
|
||||
# if bpy.data.scenes[0].unit_settings.system_rotation == 'DEGREES':
|
||||
# assets.add_khafile_def('lnx_degrees')
|
||||
|
||||
@ -381,9 +511,6 @@ project.addSources('Sources');
|
||||
for d in assets.khafile_defs:
|
||||
khafile.write("project.addDefine('" + d + "');\n")
|
||||
|
||||
for p in assets.khafile_params:
|
||||
khafile.write("project.addParameter('" + p + "');\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)))
|
||||
@ -424,7 +551,10 @@ project.addSources('Sources');
|
||||
khafile.write("project.targetOptions.ios.bundle = '{0}';\n".format(lnx.utils.safestr(bundle)))
|
||||
|
||||
if wrd.lnx_project_icon != '':
|
||||
shutil.copy(bpy.path.abspath(wrd.lnx_project_icon), project_path + '/icon.png')
|
||||
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())
|
||||
@ -494,10 +624,18 @@ def write_mainhx(scene_name, resx, resy, is_play, is_publish):
|
||||
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;\n""")
|
||||
package;
|
||||
""")
|
||||
|
||||
# Write hook imports
|
||||
if hooks['imports']:
|
||||
f.write(hooks['imports'])
|
||||
|
||||
f.write("""
|
||||
class Main {
|
||||
@ -511,10 +649,9 @@ class Main {
|
||||
public static inline var voxelgiVoxelSize = """ + str(round(rpdat.lnx_voxelgi_size * 100) / 100) + """;""")
|
||||
|
||||
if rpdat.rp_bloom:
|
||||
if bpy.app.version <= (4, 2, 4):
|
||||
f.write(f"public static var bloomRadius = {bpy.context.scene.eevee.bloom_radius if rpdat.lnx_bloom_follow_blender else rpdat.lnx_bloom_radius};")
|
||||
else:
|
||||
f.write(f"public static var bloomRadius = {rpdat.lnx_bloom_radius};")
|
||||
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("""
|
||||
@ -523,49 +660,71 @@ class Main {
|
||||
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("""
|
||||
iron.object.BoneAnimation.skinMaxBones = """ + str(rpdat.lnx_skin_max_bones) + """;""")
|
||||
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("""
|
||||
iron.object.LightObject.cascadeCount = """ + str(rpdat.rp_shadowmap_cascades) + """;
|
||||
iron.object.LightObject.cascadeSplitFactor = """ + str(rpdat.lnx_shadowmap_split) + """;""")
|
||||
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("""
|
||||
iron.object.LightObject.cascadeBounds = """ + str(rpdat.lnx_shadowmap_bounds) + """;""")
|
||||
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("""
|
||||
leenkx.system.Starter.numAssets = """ + str(len(asset_references)) + """;
|
||||
leenkx.system.Starter.drawLoading = """ + loadscreen_class + """.render;""")
|
||||
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("""
|
||||
leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;""")
|
||||
f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;")
|
||||
elif wrd.lnx_canvas_img_scaling_quality == 'high':
|
||||
f.write("""
|
||||
leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.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("""
|
||||
leenkx.system.Starter.main(
|
||||
'""" + lnx.utils.safestr(scene_name) + scene_ext + """',
|
||||
""" + str(winmode) + """,
|
||||
""" + ('true' if wrd.lnx_winresize else 'false') + """,
|
||||
""" + ('true' if wrd.lnx_winminimize else 'false') + """,
|
||||
""" + ('true' if (wrd.lnx_winresize and wrd.lnx_winmaximize) else 'false') + """,
|
||||
""" + str(resx) + """,
|
||||
""" + str(resy) + """,
|
||||
""" + str(int(rpdat.lnx_samples_per_pixel)) + """,
|
||||
""" + ('true' if wrd.lnx_vsync else 'false') + """,
|
||||
""" + pathpack + """.renderpath.RenderPathCreator.get
|
||||
);
|
||||
}
|
||||
}""")
|
||||
|
||||
@ -584,28 +743,27 @@ def write_indexhtml(w, h, is_publish):
|
||||
"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>""")
|
||||
<meta charset='utf-8'/>""")
|
||||
if rpdat.rp_stereo or wrd.lnx_winmode == 'Fullscreen':
|
||||
f.write("""
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
""")
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=no'>""")
|
||||
f.write("""
|
||||
<title>"""+html.escape( wrd.lnx_project_name)+"""</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0;">
|
||||
<body style='margin: 0; padding: 0'>
|
||||
""")
|
||||
if rpdat.rp_stereo or wrd.lnx_winmode == 'Fullscreen':
|
||||
f.write("""
|
||||
<canvas style="object-fit: contain; min-width: 100%; max-width: 100%; max-height: 100vh; min-height: 100vh; display: block;" id='khanvas' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas>
|
||||
<canvas style='object-fit: contain; min-width: 100%; max-width: 100%; max-height: 100vh; min-height: 100vh; display: block;' id='khanvas' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas>
|
||||
""")
|
||||
else:
|
||||
if wrd.lnx_winmode != 'Headless':
|
||||
f.write("""
|
||||
<p align="center"><canvas align="center" style="outline: none;" id='khanvas' width='""" + str(w) + """' height='""" + str(h) + """' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas></p>
|
||||
<p align='center'><canvas align='center' style='outline: none;' id='khanvas' width='""" + str(w) + """' height='""" + str(h) + """' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas></p>
|
||||
""")
|
||||
else:
|
||||
f.write("""
|
||||
<canvas align="center" style="display: none;" id='khanvas' width='""" + str(w) + """' height='""" + str(h) + """' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas>
|
||||
<canvas align='center' style='display: none;' id='khanvas' width='""" + str(w) + """' height='""" + str(h) + """' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas>
|
||||
<script>
|
||||
// Quick solution for headless mode only for HTML target
|
||||
window.onload = function() {
|
||||
@ -614,7 +772,7 @@ def write_indexhtml(w, h, is_publish):
|
||||
</script>
|
||||
""")
|
||||
f.write("""
|
||||
<script src='kha.js'></script>
|
||||
<script type='text/javascript' src='kha.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
@ -707,16 +865,13 @@ const float ssgiRadius = """ + str(round(rpdat.lnx_ssgi_radius * 100) / 100) + "
|
||||
""")
|
||||
|
||||
if rpdat.rp_bloom:
|
||||
follow_blender = rpdat.lnx_bloom_follow_blender
|
||||
|
||||
follow_blender = rpdat.lnx_bloom_follow_blender if bpy.app.version < (4, 3, 0) else False
|
||||
|
||||
eevee_settings = bpy.context.scene.eevee
|
||||
if bpy.app.version <= (4, 2, 4):
|
||||
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
|
||||
else:
|
||||
threshold = rpdat.lnx_bloom_threshold
|
||||
strength = rpdat.lnx_bloom_strength
|
||||
knee = rpdat.lnx_bloom_knee
|
||||
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) + """;
|
||||
|
||||
@ -160,7 +160,7 @@ def write_probes(image_filepath: str, disable_hdr: bool, from_srgb: bool, cached
|
||||
kraffiti_path = kha_path + '/Kinc/Tools/windows_x64/kraffiti.exe'
|
||||
elif lnx.utils.get_os() == 'mac':
|
||||
cmft_path = '"' + sdk_path + '/lib/leenkx_tools/cmft/cmft-osx"'
|
||||
kraffiti_path = '"' + kha_path + '/Kinc/Tools/macos/kraffiti"'
|
||||
kraffiti_path = '"' + kha_path + '/Kinc/Tools/macos_x64/kraffiti"'
|
||||
else:
|
||||
cmft_path = '"' + sdk_path + '/lib/leenkx_tools/cmft/cmft-linux64"'
|
||||
kraffiti_path = '"' + kha_path + '/Kinc/Tools/linux_x64/kraffiti"'
|
||||
@ -439,8 +439,10 @@ def write_sky_irradiance(base_name):
|
||||
|
||||
def write_color_irradiance(base_name, col):
|
||||
"""Constant color irradiance"""
|
||||
# Adjust to Cycles
|
||||
irradiance_floats = [col[0] * 1.13, col[1] * 1.13, col[2] * 1.13]
|
||||
# Match shIrradiance()'s c4 factor so a constant color environment
|
||||
# decodes back to the same diffuse radiance at strength 1.
|
||||
sh_l00 = 1.0 / 0.886227
|
||||
irradiance_floats = [col[0] * sh_l00, col[1] * sh_l00, col[2] * sh_l00]
|
||||
for i in range(0, 24):
|
||||
irradiance_floats.append(0.0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user