From 444a215e63a86fd7ac7500ecad6ea78232e31797 Mon Sep 17 00:00:00 2001 From: wuaieyo Date: Thu, 3 Jul 2025 05:27:24 +0200 Subject: [PATCH 01/12] made default resolution adaptive sensiticity because makes more sense, removed other things since no difference and dunno --- .../Sources/leenkx/logicnode/MouseLookNode.hx | 189 ++++++------------ .../lnx/logicnode/input/LN_mouse_look.py | 25 +-- 2 files changed, 68 insertions(+), 146 deletions(-) diff --git a/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx b/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx index edf6517..58fb70b 100644 --- a/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx +++ b/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx @@ -7,44 +7,24 @@ import kha.System; import kha.FastFloat; class MouseLookNode extends LogicNode { - // Note: This implementation works in degrees internally and converts to radians only when applying rotations - // Sub-pixel interpolation is always enabled for optimal precision - // Features: Resolution-adaptive scaling and precise low-sensitivity support + public var property0: String; + public var property1: Bool; + public var property2: Bool; + public var property3: Bool; + public var property4: Bool; + public var property5: Bool; + public var property7: Bool; - public var property0: String; // Front axis - public var property1: Bool; // Center Mouse - public var property2: Bool; // Invert X - public var property3: Bool; // Invert Y - public var property4: Bool; // Cap Left/Right - public var property5: Bool; // Cap Up/Down + var smoothX: Float = 0.0; + var smoothY: Float = 0.0; + var maxHorizontal: Float = Math.PI; + var maxVertical: Float = Math.PI / 2; + var currentHorizontal: Float = 0.0; + var currentVertical: Float = 0.0; + var baseResolutionWidth: Float = 1920.0; - // New strategy toggles - public var property6: Bool; // Resolution-Adaptive Scaling - - // Smoothing variables - var smoothX: FastFloat = 0.0; - var smoothY: FastFloat = 0.0; - - // Capping limits (in degrees) - var maxHorizontal: FastFloat = 180.0; // 180 degrees - var maxVertical: FastFloat = 90.0; // 90 degrees - - // Current accumulated rotations for capping - var currentHorizontal: FastFloat = 0.0; - var currentVertical: FastFloat = 0.0; - - // Sub-pixel interpolation accumulators - var accumulatedHorizontalRotation: FastFloat = 0.0; - var accumulatedVerticalRotation: FastFloat = 0.0; - var minimumRotationThreshold: FastFloat = 0.01; // degrees (was 0.0001 radians) - - // Frame rate independence removed - not applicable to mouse input - - // Resolution adaptive scaling - var baseResolutionWidth: FastFloat = 1920.0; - var baseResolutionHeight: FastFloat = 1080.0; - - + static inline var BASE_SCALE: Float = 1500.0; + static var RADIAN_SCALING_FACTOR: Float = Math.PI * 50.0 / 180.0; public function new(tree: LogicTree) { super(tree); @@ -63,114 +43,64 @@ class MouseLookNode extends LogicNode { var mouse = Input.getMouse(); - // Handle mouse centering/locking if (property1) { if (mouse.started() && !mouse.locked) { mouse.lock(); } } - // Only process if mouse is active if (!mouse.locked && !mouse.down()) { runOutput(0); return; } - // Get mouse movement deltas - var deltaX: FastFloat = mouse.movementX; - var deltaY: FastFloat = mouse.movementY; + var deltaX: Float = mouse.movementX; + var deltaY: Float = mouse.movementY; - // Note: Sensitivity will be applied later to preserve precision for small movements - - // Apply inversion if (property2) deltaX = -deltaX; if (property3) deltaY = -deltaY; - // Strategy 1: Resolution-Adaptive Scaling - var resolutionMultiplier: FastFloat = 1.0; - if (property6) { - var currentWidth = System.windowWidth(); - var currentHeight = System.windowHeight(); - resolutionMultiplier = (currentWidth / baseResolutionWidth) * (currentHeight / baseResolutionHeight); - resolutionMultiplier = Math.sqrt(resolutionMultiplier); // Take square root to avoid over-scaling - } + // Always apply resolution-adaptive scaling + var resolutionMultiplier: Float = System.windowWidth() / baseResolutionWidth; - // Frame Rate Independence disabled for mouse input - mouse deltas are inherently frame-rate independent - - // Apply smoothing if (smoothing > 0.0) { - var smoothFactor = 1.0 - Math.min(smoothing, 0.99); // Prevent complete smoothing - smoothX = smoothX * smoothing + deltaX * smoothFactor; - smoothY = smoothY * smoothing + deltaY * smoothFactor; + var smoothingFactor: Float = Math.min(smoothing, 0.99); + smoothX = smoothX * smoothingFactor + deltaX * (1.0 - smoothingFactor); + smoothY = smoothY * smoothingFactor + deltaY * (1.0 - smoothingFactor); deltaX = smoothX; deltaY = smoothY; } - // Determine rotation axes based on front axis setting var horizontalAxis = new Vec4(); var verticalAxis = new Vec4(); switch (property0) { - case "X": // X is front - horizontalAxis.set(0, 0, 1); // Z axis for horizontal (yaw) - verticalAxis.set(0, 1, 0); // Y axis for vertical (pitch) - case "Y": // Y is front (default) + case "X": + horizontalAxis.set(0, 0, 1); + verticalAxis.set(0, 1, 0); + case "Y": #if lnx_yaxisup - horizontalAxis.set(0, 0, 1); // Z axis for horizontal (yaw) - verticalAxis.set(1, 0, 0); // X axis for vertical (pitch) + horizontalAxis.set(0, 0, 1); + verticalAxis.set(1, 0, 0); #else - horizontalAxis.set(0, 0, 1); // Z axis for horizontal (yaw) - verticalAxis.set(1, 0, 0); // X axis for vertical (pitch) + horizontalAxis.set(0, 0, 1); + verticalAxis.set(1, 0, 0); #end - case "Z": // Z is front - horizontalAxis.set(0, 1, 0); // Y axis for horizontal (yaw) - verticalAxis.set(1, 0, 0); // X axis for vertical (pitch) + case "Z": + horizontalAxis.set(0, 1, 0); + verticalAxis.set(1, 0, 0); } - // Base scaling - var baseScale: FastFloat = 1500.0; - var finalScale = baseScale; + // Always apply resolution-adaptive scaling + var finalScale: Float = BASE_SCALE * resolutionMultiplier; - // Apply resolution scaling - if (property6) { - finalScale *= resolutionMultiplier; - } - - - - // Apply sensitivity scaling after all enhancement strategies to preserve precision deltaX *= sensitivity; deltaY *= sensitivity; - // Calculate rotation amounts (in degrees) - var horizontalRotation: FastFloat = (-deltaX / finalScale) * 180.0 / Math.PI; - var verticalRotation: FastFloat = (-deltaY / finalScale) * 180.0 / Math.PI; + var horizontalRotation: Float = (-deltaX / finalScale) * RADIAN_SCALING_FACTOR; + var verticalRotation: Float = (-deltaY / finalScale) * RADIAN_SCALING_FACTOR; - // Note: Frame rate independence removed for mouse input as mouse deltas - // are already frame-rate independent by nature. Mouse input represents - // instantaneous user intent, not time-based movement. - - // Strategy 2: Sub-Pixel Interpolation (always enabled) - accumulatedHorizontalRotation += horizontalRotation; - accumulatedVerticalRotation += verticalRotation; - - // Only apply rotation if accumulated amount exceeds threshold - if (Math.abs(accumulatedHorizontalRotation) >= minimumRotationThreshold) { - horizontalRotation = accumulatedHorizontalRotation; - accumulatedHorizontalRotation = 0.0; - } else { - horizontalRotation = 0.0; - } - - if (Math.abs(accumulatedVerticalRotation) >= minimumRotationThreshold) { - verticalRotation = accumulatedVerticalRotation; - accumulatedVerticalRotation = 0.0; - } else { - verticalRotation = 0.0; - } - - // Apply capping constraints - if (property4) { // Cap Left/Right + if (property4) { currentHorizontal += horizontalRotation; if (currentHorizontal > maxHorizontal) { horizontalRotation -= (currentHorizontal - maxHorizontal); @@ -181,7 +111,7 @@ class MouseLookNode extends LogicNode { } } - if (property5) { // Cap Up/Down + if (property5) { currentVertical += verticalRotation; if (currentVertical > maxVertical) { verticalRotation -= (currentVertical - maxVertical); @@ -192,39 +122,36 @@ class MouseLookNode extends LogicNode { } } - // Apply horizontal rotation to body (yaw) - if (Math.abs(horizontalRotation) > 0.01) { // 0.01 degrees threshold - bodyObject.transform.rotate(horizontalAxis, horizontalRotation * Math.PI / 180.0); // Convert degrees to radians + if (horizontalRotation != 0.0) { + bodyObject.transform.rotate(horizontalAxis, horizontalRotation); - // Sync physics if needed #if lnx_physics var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody); if (rigidBody != null) rigidBody.syncTransform(); #end } - // Apply vertical rotation to head (pitch) if head object is provided - if (headObject != null && Math.abs(verticalRotation) > 0.01) { // 0.01 degrees threshold - // For head rotation, use the head's local coordinate system - var headVerticalAxis = headObject.transform.world.right(); - headObject.transform.rotate(headVerticalAxis, verticalRotation * Math.PI / 180.0); // Convert degrees to radians + if (headObject != null && verticalRotation != 0.0) { + if (property7) { + // Local space rotation (recommended for FPS) + headObject.transform.rotate(verticalAxis, verticalRotation); + } else { + // World space rotation + var headVerticalAxis = headObject.transform.world.right(); + headObject.transform.rotate(headVerticalAxis, verticalRotation); + } - // Sync physics if needed #if lnx_physics var headRigidBody = headObject.getTrait(leenkx.trait.physics.RigidBody); if (headRigidBody != null) headRigidBody.syncTransform(); #end - } else if (headObject == null) { - // If no head object, apply vertical rotation to body as well - if (Math.abs(verticalRotation) > 0.01) { // 0.01 degrees threshold - bodyObject.transform.rotate(verticalAxis, verticalRotation * Math.PI / 180.0); // Convert degrees to radians - - // Sync physics if needed - #if lnx_physics - var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody); - if (rigidBody != null) rigidBody.syncTransform(); - #end - } + } else if (headObject == null && verticalRotation != 0.0) { + bodyObject.transform.rotate(verticalAxis, verticalRotation); + + #if lnx_physics + var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end } runOutput(0); diff --git a/leenkx/blender/lnx/logicnode/input/LN_mouse_look.py b/leenkx/blender/lnx/logicnode/input/LN_mouse_look.py index d33d111..11230cf 100644 --- a/leenkx/blender/lnx/logicnode/input/LN_mouse_look.py +++ b/leenkx/blender/lnx/logicnode/input/LN_mouse_look.py @@ -5,8 +5,7 @@ class MouseLookNode(LnxLogicTreeNode): """Controls object rotation based on mouse movement for FPS-style camera control. Features: - - Sub-pixel interpolation (always enabled) for optimal precision and smooth low-sensitivity movement - - Resolution-adaptive scaling for consistent feel across different screen resolutions + - Built-in resolution-adaptive scaling for consistent feel across different screen resolutions """ bl_idname = 'LNMouseLookNode' bl_label = 'Mouse Look' @@ -55,12 +54,12 @@ class MouseLookNode(LnxLogicTreeNode): name='Cap Up / Down', description='Limit vertical rotation', default=True) - - # Strategy toggles - property6: HaxeBoolProperty( - 'property6', - name='Resolution Adaptive', - description='Scale sensitivity based on screen resolution', + + # Head rotation space toggle + property7: HaxeBoolProperty( + 'property7', + name='Head Local Space', + description='Enable it if the Head is child of the Body to avoid weird rotation', default=False) @@ -72,7 +71,7 @@ class MouseLookNode(LnxLogicTreeNode): self.add_input('LnxNodeSocketObject', 'Body') self.add_input('LnxNodeSocketObject', 'Head') self.add_input('LnxFloatSocket', 'Sensitivity', default_value=0.5) - self.add_input('LnxFloatSocket', 'Smoothing', default_value=0.0) + self.add_input('LnxFactorSocket', 'Smoothing', default_value=0.0) self.add_output('LnxNodeSocketAction', 'Out') @@ -92,10 +91,6 @@ class MouseLookNode(LnxLogicTreeNode): col.prop(self, 'property4', text='Cap Left / Right') col.prop(self, 'property5', text='Cap Up / Down') - # Separator - layout.separator() - - # Enhancement strategies section + # Head behavior section col = layout.column(align=True) - col.label(text="Enhancement Strategies:") - col.prop(self, 'property6', text='Resolution Adaptive') \ No newline at end of file + col.prop(self, 'property7', text='Head Local Space') \ No newline at end of file From 57f0e937d0312eead3eed3ebf41030819ace793d Mon Sep 17 00:00:00 2001 From: wuaieyo Date: Tue, 8 Jul 2025 22:48:16 +0200 Subject: [PATCH 02/12] fixed properties numbering, comments and LNXfactor to LNXFloat --- .../Sources/leenkx/logicnode/MouseLookNode.hx | 148 +++++++++++---- .../lnx/logicnode/input/LN_mouse_look.py | 169 ++++++++++++------ 2 files changed, 227 insertions(+), 90 deletions(-) diff --git a/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx b/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx index 58fb70b..977432e 100644 --- a/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx +++ b/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx @@ -6,102 +6,162 @@ import iron.object.Object; import kha.System; import kha.FastFloat; +/** + * MouseLookNode - FPS-style mouse look camera controller + * + * This node provides smooth, resolution-independent mouse look functionality for + * first-person perspective controls. It supports separate body and head objects, + * allowing for realistic FPS camera movement where the body rotates horizontally + * and the head/camera rotates vertically. + * + * Key Features: + * - Resolution-adaptive scaling for consistent feel across different screen sizes + * - Configurable axis orientations (X, Y, Z as front) + * - Optional mouse cursor locking and hiding + * - Invertible X/Y axes + * - Rotation capping/limiting for both horizontal and vertical movement + * - Smoothing support for smoother camera movement + * - Physics integration with automatic rigid body synchronization + * - Support for both local and world space head rotation + */ class MouseLookNode extends LogicNode { - public var property0: String; - public var property1: Bool; - public var property2: Bool; - public var property3: Bool; - public var property4: Bool; - public var property5: Bool; - public var property7: Bool; + // Configuration properties (set from Blender node interface) + public var property0: String; // Front axis: "X", "Y", or "Z" + public var property1: Bool; // Hide Locked: auto-lock mouse cursor + public var property2: Bool; // Invert X: invert horizontal mouse movement + public var property3: Bool; // Invert Y: invert vertical mouse movement + public var property4: Bool; // Cap Left/Right: limit horizontal rotation + public var property5: Bool; // Cap Up/Down: limit vertical rotation + public var property6: Bool; // Head Local Space: use local space for head rotation - var smoothX: Float = 0.0; - var smoothY: Float = 0.0; - var maxHorizontal: Float = Math.PI; - var maxVertical: Float = Math.PI / 2; - var currentHorizontal: Float = 0.0; - var currentVertical: Float = 0.0; + // Smoothing state variables - maintain previous frame values for interpolation + var smoothX: Float = 0.0; // Smoothed horizontal mouse delta + var smoothY: Float = 0.0; // Smoothed vertical mouse delta + + // Rotation limits (in radians) + var maxHorizontal: Float = Math.PI; // Maximum horizontal rotation (180 degrees) + var maxVertical: Float = Math.PI / 2; // Maximum vertical rotation (90 degrees) + + // Current rotation tracking for capping calculations + var currentHorizontal: Float = 0.0; // Accumulated horizontal rotation + var currentVertical: Float = 0.0; // Accumulated vertical rotation + + // Resolution scaling reference - base resolution for consistent sensitivity var baseResolutionWidth: Float = 1920.0; - static inline var BASE_SCALE: Float = 1500.0; - static var RADIAN_SCALING_FACTOR: Float = Math.PI * 50.0 / 180.0; + // Sensitivity scaling constants + static inline var BASE_SCALE: Float = 1500.0; // Base sensitivity scale factor + static var RADIAN_SCALING_FACTOR: Float = Math.PI * 50.0 / 180.0; // Degrees to radians conversion with sensitivity scaling public function new(tree: LogicTree) { super(tree); } + /** + * Main execution function called every frame when the node is active + * + * Input connections: + * [0] - Action trigger (not used in current implementation) + * [1] - Body Object: the main object that rotates horizontally + * [2] - Head Object: optional object that rotates vertically (typically camera) + * [3] - Sensitivity: mouse sensitivity multiplier + * [4] - Smoothing: movement smoothing factor (0.0 = no smoothing, 0.99 = maximum smoothing) + */ override function run(from: Int) { + // Get input values from connected nodes var bodyObject: Object = inputs[1].get(); var headObject: Object = inputs[2].get(); var sensitivity: FastFloat = inputs[3].get(); var smoothing: FastFloat = inputs[4].get(); + // Early exit if no body object is provided if (bodyObject == null) { runOutput(0); return; } + // Get mouse input state var mouse = Input.getMouse(); + // Handle automatic mouse cursor locking for FPS controls if (property1) { if (mouse.started() && !mouse.locked) { - mouse.lock(); + mouse.lock(); // Center and hide cursor, enable unlimited movement } } + // Only process mouse look when cursor is locked or mouse button is held + // This prevents unwanted camera movement when UI elements are being used if (!mouse.locked && !mouse.down()) { runOutput(0); return; } + // Get raw mouse movement delta (pixels moved since last frame) var deltaX: Float = mouse.movementX; var deltaY: Float = mouse.movementY; - if (property2) deltaX = -deltaX; - if (property3) deltaY = -deltaY; + // Apply axis inversion if configured + if (property2) deltaX = -deltaX; // Invert horizontal movement + if (property3) deltaY = -deltaY; // Invert vertical movement - // Always apply resolution-adaptive scaling + // Calculate resolution-adaptive scaling to maintain consistent sensitivity + // across different screen resolutions. Higher resolutions will have proportionally + // higher scaling to compensate for increased pixel density. var resolutionMultiplier: Float = System.windowWidth() / baseResolutionWidth; + // Apply movement smoothing if enabled + // This creates a weighted average between current and previous movement values + // to reduce jittery camera movement, especially useful for low framerates if (smoothing > 0.0) { - var smoothingFactor: Float = Math.min(smoothing, 0.99); + var smoothingFactor: Float = Math.min(smoothing, 0.99); // Cap smoothing to prevent complete freeze smoothX = smoothX * smoothingFactor + deltaX * (1.0 - smoothingFactor); smoothY = smoothY * smoothingFactor + deltaY * (1.0 - smoothingFactor); deltaX = smoothX; deltaY = smoothY; } - var horizontalAxis = new Vec4(); - var verticalAxis = new Vec4(); + // Define rotation axes based on the configured front axis + // These determine which 3D axes are used for horizontal and vertical rotation + var horizontalAxis = new Vec4(); // Axis for left/right body rotation + var verticalAxis = new Vec4(); // Axis for up/down head rotation switch (property0) { - case "X": - horizontalAxis.set(0, 0, 1); - verticalAxis.set(0, 1, 0); - case "Y": + case "X": // X-axis forward (e.g., for side-scrolling or specific orientations) + horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation + verticalAxis.set(0, 1, 0); // Y-axis for vertical rotation + case "Y": // Y-axis forward (most common for 3D games) #if lnx_yaxisup - horizontalAxis.set(0, 0, 1); - verticalAxis.set(1, 0, 0); + // Y-up coordinate system (Blender default) + horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation + verticalAxis.set(1, 0, 0); // X-axis for vertical rotation #else - horizontalAxis.set(0, 0, 1); - verticalAxis.set(1, 0, 0); + // Z-up coordinate system + horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation + verticalAxis.set(1, 0, 0); // X-axis for vertical rotation #end - case "Z": - horizontalAxis.set(0, 1, 0); - verticalAxis.set(1, 0, 0); + case "Z": // Z-axis forward (top-down or specific orientations) + horizontalAxis.set(0, 1, 0); // Y-axis for horizontal rotation + verticalAxis.set(1, 0, 0); // X-axis for vertical rotation } - // Always apply resolution-adaptive scaling + // Calculate final sensitivity scaling combining base scale and resolution adaptation var finalScale: Float = BASE_SCALE * resolutionMultiplier; + // Apply user-defined sensitivity multiplier deltaX *= sensitivity; deltaY *= sensitivity; + // Convert pixel movement to rotation angles (radians) + // Negative values ensure natural movement direction (moving mouse right rotates right) var horizontalRotation: Float = (-deltaX / finalScale) * RADIAN_SCALING_FACTOR; var verticalRotation: Float = (-deltaY / finalScale) * RADIAN_SCALING_FACTOR; + // Apply horizontal rotation capping if enabled + // This prevents the character from rotating beyond specified limits if (property4) { currentHorizontal += horizontalRotation; + // Clamp rotation to maximum horizontal range and adjust current frame rotation if (currentHorizontal > maxHorizontal) { horizontalRotation -= (currentHorizontal - maxHorizontal); currentHorizontal = maxHorizontal; @@ -111,8 +171,11 @@ class MouseLookNode extends LogicNode { } } + // Apply vertical rotation capping if enabled + // This prevents looking too far up or down (like human neck limitations) if (property5) { currentVertical += verticalRotation; + // Clamp rotation to maximum vertical range and adjust current frame rotation if (currentVertical > maxVertical) { verticalRotation -= (currentVertical - maxVertical); currentVertical = maxVertical; @@ -122,38 +185,49 @@ class MouseLookNode extends LogicNode { } } + // Apply horizontal rotation to body object (character turning left/right) if (horizontalRotation != 0.0) { bodyObject.transform.rotate(horizontalAxis, horizontalRotation); + // Synchronize physics rigid body if present + // This ensures physics simulation stays in sync with visual transform #if lnx_physics var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody); if (rigidBody != null) rigidBody.syncTransform(); #end } + // Apply vertical rotation to head object (camera looking up/down) if (headObject != null && verticalRotation != 0.0) { - if (property7) { - // Local space rotation (recommended for FPS) + if (property6) { + // Local space rotation - recommended when head is a child of body + // This prevents gimbal lock and rotation inheritance issues headObject.transform.rotate(verticalAxis, verticalRotation); } else { - // World space rotation + // World space rotation - uses head object's current right vector + // More accurate for independent head objects but can cause issues with parenting var headVerticalAxis = headObject.transform.world.right(); headObject.transform.rotate(headVerticalAxis, verticalRotation); } + // Synchronize head physics rigid body if present #if lnx_physics var headRigidBody = headObject.getTrait(leenkx.trait.physics.RigidBody); if (headRigidBody != null) headRigidBody.syncTransform(); #end } else if (headObject == null && verticalRotation != 0.0) { + // Fallback: if no separate head object, apply vertical rotation to body + // This creates a simpler single-object camera control bodyObject.transform.rotate(verticalAxis, verticalRotation); + // Synchronize body physics rigid body #if lnx_physics var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody); if (rigidBody != null) rigidBody.syncTransform(); #end } + // Continue to next connected node in the logic tree runOutput(0); } } \ No newline at end of file diff --git a/leenkx/blender/lnx/logicnode/input/LN_mouse_look.py b/leenkx/blender/lnx/logicnode/input/LN_mouse_look.py index 11230cf..e2820d1 100644 --- a/leenkx/blender/lnx/logicnode/input/LN_mouse_look.py +++ b/leenkx/blender/lnx/logicnode/input/LN_mouse_look.py @@ -2,95 +2,158 @@ from lnx.logicnode.lnx_nodes import * class MouseLookNode(LnxLogicTreeNode): - """Controls object rotation based on mouse movement for FPS-style camera control. + """MouseLookNode - Blender UI interface for FPS-style mouse look camera controller + + This class defines the Blender node interface for the MouseLookNode logic node. + It creates the visual node that appears in Blender's logic tree editor and + defines all the properties that configure the mouse look behavior. + + The node provides controls for: + - Axis orientation configuration + - Mouse cursor behavior + - Movement inversion options + - Rotation limiting/capping + - Head rotation space behavior Features: - Built-in resolution-adaptive scaling for consistent feel across different screen resolutions + - Automatic physics synchronization for rigid bodies + - Support for both single-object and dual-object (body/head) setups """ - bl_idname = 'LNMouseLookNode' - bl_label = 'Mouse Look' - lnx_section = 'mouse' - lnx_version = 1 + + # Blender node identification + bl_idname = 'LNMouseLookNode' # Unique identifier for Blender's node system + bl_label = 'Mouse Look' # Display name in node menu and header + lnx_section = 'mouse' # Category section in node add menu + lnx_version = 1 # Node version for compatibility tracking - # Front axis property + # Property 0: Front Axis Configuration + # Determines which 3D axis represents the "forward" direction of the character/camera + # This affects how horizontal and vertical rotations are applied to the objects property0: HaxeEnumProperty( 'property0', - items=[('X', 'X Axis', 'X Axis as front'), - ('Y', 'Y Axis', 'Y Axis as front'), - ('Z', 'Z Axis', 'Z Axis as front')], - name='Front', default='Y') + items=[('X', 'X Axis', 'X Axis as front'), # X-forward (side-scrolling, specific orientations) + ('Y', 'Y Axis', 'Y Axis as front'), # Y-forward (most common for 3D games) + ('Z', 'Z Axis', 'Z Axis as front')], # Z-forward (top-down, specific orientations) + name='Front', + default='Y') # Y-axis is default as it's most common in 3D game development - # Hide Locked property + # Property 1: Automatic Mouse Cursor Management + # When enabled, automatically centers and locks the mouse cursor when mouse input starts + # This is essential for FPS games to prevent cursor from leaving game window property1: HaxeBoolProperty( 'property1', name='Hide Locked', - description='Automatically center and lock the mouse cursor', - default=True) + description='Automatically center and lock the mouse cursor when mouse input begins', + default=True) # Enabled by default for typical FPS behavior - # Invert X property + # Property 2: Horizontal Movement Inversion + # Allows users to invert horizontal mouse movement (left becomes right, right becomes left) + # Some players prefer inverted controls for consistency with flight simulators property2: HaxeBoolProperty( 'property2', name='Invert X', - description='Invert horizontal mouse movement', - default=False) + description='Invert horizontal mouse movement - moving mouse right turns character left', + default=False) # Most players expect non-inverted horizontal movement - # Invert Y property + # Property 3: Vertical Movement Inversion + # Allows users to invert vertical mouse movement (up becomes down, down becomes up) + # More commonly used than horizontal inversion, especially by flight sim players property3: HaxeBoolProperty( 'property3', name='Invert Y', - description='Invert vertical mouse movement', - default=False) + description='Invert vertical mouse movement - moving mouse up looks down', + default=False) # Most players expect non-inverted vertical movement - # Cap Left/Right property + # Property 4: Horizontal Rotation Limiting + # Prevents the character from rotating beyond specified horizontal limits + # Useful for fixed-perspective games or when character shouldn't turn completely around property4: HaxeBoolProperty( 'property4', name='Cap Left / Right', - description='Limit horizontal rotation', - default=False) + description='Limit horizontal rotation to prevent full 360-degree turns', + default=False) # Disabled by default - most FPS games allow full horizontal rotation - # Cap Up/Down property + # Property 5: Vertical Rotation Limiting + # Prevents looking too far up or down, simulating human neck movement limitations + # Essential for realistic FPS games to prevent disorienting over-rotation property5: HaxeBoolProperty( 'property5', name='Cap Up / Down', - description='Limit vertical rotation', - default=True) + description='Limit vertical rotation to simulate natural neck movement (±90 degrees)', + default=True) # Enabled by default for realistic FPS behavior - # Head rotation space toggle - property7: HaxeBoolProperty( - 'property7', + # Property 6: Head Rotation Space Mode + # Controls whether head rotation uses local or world space coordinates + # Critical for preventing rotation issues when head object is child of body object + property6: HaxeBoolProperty( + 'property6', name='Head Local Space', - description='Enable it if the Head is child of the Body to avoid weird rotation', - default=False) + description='Use local space for head rotation - enable when Head is child of Body to avoid gimbal lock', + default=False) # Disabled by default, enable when using parent-child object relationships - - - def lnx_init(self, context): + """Initialize the node's input and output sockets + + This method is called when the node is first created in Blender. + It defines all the connection points (sockets) that other nodes can connect to. + + Input Sockets: + - Action In: Execution flow input (when this node should run) + - Body: The main character/player object that rotates horizontally + - Head: Optional camera/head object that rotates vertically (can be child of Body) + - Sensitivity: Mouse sensitivity multiplier (0.5 = half sensitivity, 2.0 = double sensitivity) + - Smoothing: Movement smoothing factor (0.0 = no smoothing, higher = more smoothing) + + Output Sockets: + - Action Out: Execution flow output (continues to next node after processing) + """ + + # Execution flow input - connects from previous logic node self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxNodeSocketObject', 'Body') - self.add_input('LnxNodeSocketObject', 'Head') - self.add_input('LnxFloatSocket', 'Sensitivity', default_value=0.5) - self.add_input('LnxFactorSocket', 'Smoothing', default_value=0.0) - + + # Object inputs - require 3D objects from the scene + self.add_input('LnxNodeSocketObject', 'Body') # Main character object (required) + self.add_input('LnxNodeSocketObject', 'Head') # Camera/head object (optional) + + # Numeric inputs with sensible defaults + self.add_input('LnxFloatSocket', 'Sensitivity', default_value=0.5) # Medium sensitivity + self.add_input('LnxFloatSocket', 'Smoothing', default_value=0.0) # No smoothing by default + + # Execution flow output - connects to next logic node self.add_output('LnxNodeSocketAction', 'Out') def draw_buttons(self, context, layout): - layout.prop(self, 'property0', text='Front') - layout.prop(self, 'property1', text='Hide Locked') + """Draw the node's user interface in Blender's logic tree editor - # Invert XY section - col = layout.column(align=True) - col.label(text="Invert XY:") - row = col.row(align=True) - row.prop(self, 'property2', text='X', toggle=True) - row.prop(self, 'property3', text='Y', toggle=True) + This method creates the visual controls that appear on the node in Blender. + It organizes properties into logical groups for better usability. - # Cap rotations section - col = layout.column(align=True) - col.prop(self, 'property4', text='Cap Left / Right') - col.prop(self, 'property5', text='Cap Up / Down') + Args: + context: Blender context (current scene, selected objects, etc.) + layout: UI layout object for arranging interface elements + """ - # Head behavior section - col = layout.column(align=True) - col.prop(self, 'property7', text='Head Local Space') \ No newline at end of file + # Basic configuration section + layout.prop(self, 'property0', text='Front') # Front axis dropdown + layout.prop(self, 'property1', text='Hide Locked') # Mouse locking checkbox + + # Movement inversion controls section + # Group X and Y inversion together for logical organization + col = layout.column(align=True) # Create aligned column + col.label(text="Invert XY:") # Section header + row = col.row(align=True) # Create horizontal row within column + row.prop(self, 'property2', text='X', toggle=True) # X inversion toggle button + row.prop(self, 'property3', text='Y', toggle=True) # Y inversion toggle button + + # Rotation limiting controls section + # Group rotation caps together since they're related functionality + col = layout.column(align=True) # Create new aligned column + col.prop(self, 'property4', text='Cap Left / Right') # Horizontal capping checkbox + col.prop(self, 'property5', text='Cap Up / Down') # Vertical capping checkbox + + # Advanced head behavior section + # Separate advanced option that affects technical behavior + col = layout.column(align=True) # Create new aligned column + col.prop(self, 'property6', text='Head Local Space') # Head rotation space checkbox \ No newline at end of file From e922cc38e694a5675dda93dfecebf330ca73d07f Mon Sep 17 00:00:00 2001 From: Onek8 Date: Wed, 9 Jul 2025 23:17:55 +0000 Subject: [PATCH 03/12] Update leenkx/Shaders/std/shadows.glsl --- leenkx/Shaders/std/shadows.glsl | 145 ++++++++++++++++++-------------- 1 file changed, 84 insertions(+), 61 deletions(-) diff --git a/leenkx/Shaders/std/shadows.glsl b/leenkx/Shaders/std/shadows.glsl index d994eff..a1c01f4 100644 --- a/leenkx/Shaders/std/shadows.glsl +++ b/leenkx/Shaders/std/shadows.glsl @@ -58,7 +58,15 @@ vec2 sampleCube(vec3 dir, out int faceIndex) { } #endif -vec3 PCF(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, const vec2 uv, const float compare, const vec2 smSize, const bool transparent) { +vec3 PCF(sampler2DShadow shadowMap, + #ifdef _ShadowMapTransparent + sampler2D shadowMapTransparent, + #endif + const vec2 uv, const float compare, const vec2 smSize + #ifdef _ShadowMapTransparent + , const bool transparent + #endif + ) { vec3 result = vec3(0.0); result.x = texture(shadowMap, vec3(uv + (vec2(-1.0, -1.0) / smSize), compare)); result.x += texture(shadowMap, vec3(uv + (vec2(-1.0, 0.0) / smSize), compare)); @@ -71,11 +79,13 @@ vec3 PCF(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, const vec2 u result.x += texture(shadowMap, vec3(uv + (vec2(1.0, 1.0) / smSize), compare)); result = result.xxx / 9.0; + #ifdef _ShadowMapTransparent if (transparent == false) { vec4 shadowmap_transparent = texture(shadowMapTransparent, uv); if (shadowmap_transparent.a < compare) result *= shadowmap_transparent.rgb; } + #endif return result; } @@ -87,41 +97,15 @@ float lpToDepth(vec3 lp, const vec2 lightProj) { return zcomp * 0.5 + 0.5; } -#ifndef _ShadowMapAtlas -vec3 PCFCube(samplerCubeShadow shadowMapCube, samplerCube shadowMapCubeTransparent, vec3 lp, vec3 ml, float bias, vec2 lightProj, vec3 n, const bool transparent) { - const float s = shadowmapCubePcfSize; - float compare = lpToDepth(lp, lightProj) - bias * 1.5; - ml = ml + n * bias * 20; - #ifdef _InvY - ml.y = -ml.y; - #endif - - float shadowFactor = 0.0; - shadowFactor = texture(shadowMapCube, vec4(ml, compare)); - shadowFactor += texture(shadowMapCube, vec4(ml + vec3(s, s, s), compare)); - shadowFactor += texture(shadowMapCube, vec4(ml + vec3(-s, s, s), compare)); - shadowFactor += texture(shadowMapCube, vec4(ml + vec3(s, -s, s), compare)); - shadowFactor += texture(shadowMapCube, vec4(ml + vec3(s, s, -s), compare)); - shadowFactor += texture(shadowMapCube, vec4(ml + vec3(-s, -s, s), compare)); - shadowFactor += texture(shadowMapCube, vec4(ml + vec3(s, -s, -s), compare)); - shadowFactor += texture(shadowMapCube, vec4(ml + vec3(-s, s, -s), compare)); - shadowFactor += texture(shadowMapCube, vec4(ml + vec3(-s, -s, -s), compare)); - shadowFactor /= 9.0; - - vec3 result = vec3(shadowFactor); - - if (transparent == false) { - vec4 shadowmap_transparent = texture(shadowMapCubeTransparent, ml); - if (shadowmap_transparent.a < compare) - result *= shadowmap_transparent.rgb; - } - - return result; -} -#endif - -#ifdef _ShadowMapAtlas -vec3 PCFCube(samplerCubeShadow shadowMapCube, samplerCube shadowMapCubeTransparent, const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n, const bool transparent) { +vec3 PCFCube(samplerCubeShadow shadowMapCube, + #ifdef _ShadowMapTransparent + samplerCube shadowMapCubeTransparent, + #endif + const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n + #ifdef _ShadowMapTransparent + , const bool transparent + #endif + ) { const float s = shadowmapCubePcfSize; // TODO: incorrect... float compare = lpToDepth(lp, lightProj) - bias * 1.5; ml = ml + n * bias * 20; @@ -140,16 +124,18 @@ vec3 PCFCube(samplerCubeShadow shadowMapCube, samplerCube shadowMapCubeTranspare result.x += texture(shadowMapCube, vec4(ml + vec3(-s, -s, -s), compare)); result = result.xxx / 9.0; + #ifdef _ShadowMapTransparent if (transparent == false) { vec4 shadowmap_transparent = texture(shadowMapCubeTransparent, ml); if (shadowmap_transparent.a < compare) result *= shadowmap_transparent.rgb; } + #endif return result; } - +#ifdef _ShadowMapAtlas // transform "out-of-bounds" coordinates to the correct face/coordinate system // https://www.khronos.org/opengl/wiki/File:CubeMapAxes.png vec2 transformOffsetedUV(const int faceIndex, out int newFaceIndex, vec2 uv) { @@ -243,21 +229,31 @@ vec2 transformOffsetedUV(const int faceIndex, out int newFaceIndex, vec2 uv) { return uv; } -vec3 PCFFakeCube(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n, const int index, const bool transparent) { +vec3 PCFFakeCube(sampler2DShadow shadowMap, + #ifdef _ShadowMapTransparent + sampler2D shadowMapTransparent, + #endif + const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n, const int index + #ifdef _ShadowMapTransparent + , const bool transparent + #endif + ) { const vec2 smSize = smSizeUniform; // TODO: incorrect... const float compare = lpToDepth(lp, lightProj) - bias * 1.5; ml = ml + n * bias * 20; - int faceIndex = 0; const int lightIndex = index * 6; const vec2 uv = sampleCube(ml, faceIndex); - vec4 pointLightTile = pointLightDataArray[lightIndex + faceIndex]; // x: tile X offset, y: tile Y offset, z: tile size relative to atlas vec2 uvtiled = pointLightTile.z * uv + pointLightTile.xy; #ifdef _FlipY uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system #endif + if (any(lessThan(uvtiled, vec2(0.0))) || any(greaterThan(uvtiled, vec2(1.0)))) { + return vec3(1.0); // Handle edge cases by returning full light + } + vec3 result = vec3(0.0); result.x += texture(shadowMap, vec3(uvtiled, compare)); // soft shadowing @@ -270,14 +266,6 @@ vec3 PCFFakeCube(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, cons #endif result.x += texture(shadowMap, vec3(uvtiled, compare)); - uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, 1.0) / smSize))); - pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; - uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; - #ifdef _FlipY - uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system - #endif - result.x += texture(shadowMap, vec3(uvtiled, compare)); - uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(0.0, -1.0) / smSize))); pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; @@ -334,30 +322,47 @@ vec3 PCFFakeCube(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, cons uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system #endif + #ifdef _ShadowMapTransparent if (transparent == false) { vec4 shadowmap_transparent = texture(shadowMapTransparent, uvtiled); if (shadowmap_transparent.a < compare) result *= shadowmap_transparent.rgb; } + #endif return result; } #endif -vec3 shadowTest(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, const vec3 lPos, const float shadowsBias, const bool transparent) { +vec3 shadowTest(sampler2DShadow shadowMap, + #ifdef _ShadowMapTransparent + sampler2D shadowMapTransparent, + #endif + const vec3 lPos, const float shadowsBias + #ifdef _ShadowMapTransparent + , const bool transparent + #endif + ) { #ifdef _SMSizeUniform vec2 smSize = smSizeUniform; #else const vec2 smSize = shadowmapSize; #endif if (lPos.x < 0.0 || lPos.y < 0.0 || lPos.x > 1.0 || lPos.y > 1.0) return vec3(1.0); - return PCF(shadowMap, shadowMapTransparent, lPos.xy, lPos.z - shadowsBias, smSize, transparent); + return PCF(shadowMap, + #ifdef _ShadowMapTransparent + shadowMapTransparent, + #endif + lPos.xy, lPos.z - shadowsBias, smSize + #ifdef _ShadowMapTransparent + , transparent + #endif + ); } #ifdef _CSM mat4 getCascadeMat(const float d, out int casi, out int casIndex) { const int c = shadowmapCascades; - // Get cascade index // TODO: use bounding box slice selection instead of sphere const vec4 ci = vec4(float(c > 0), float(c > 1), float(c > 2), float(c > 3)); @@ -373,21 +378,26 @@ mat4 getCascadeMat(const float d, out int casi, out int casIndex) { float(d > casData[c * 4].z), float(d > casData[c * 4].w)); casi = int(min(dot(ci, comp), c)); - // Get cascade mat casIndex = casi * 4; - return mat4( casData[casIndex ], casData[casIndex + 1], casData[casIndex + 2], casData[casIndex + 3]); - // if (casIndex == 0) return mat4(casData[0], casData[1], casData[2], casData[3]); // .. } -vec3 shadowTestCascade(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, const vec3 eye, const vec3 p, const float shadowsBias, const bool transparent) { +vec3 shadowTestCascade(sampler2DShadow shadowMap, + #ifdef _ShadowMapTransparent + sampler2D shadowMapTransparent, + #endif + const vec3 eye, const vec3 p, const float shadowsBias + #ifdef _ShadowMapTransparent + , const bool transparent + #endif + ) { #ifdef _SMSizeUniform vec2 smSize = smSizeUniform; #else @@ -395,16 +405,22 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap, sampler2D shadowMapTransparent #endif const int c = shadowmapCascades; float d = distance(eye, p); - int casi; int casIndex; mat4 LWVP = getCascadeMat(d, casi, casIndex); - vec4 lPos = LWVP * vec4(p, 1.0); lPos.xyz /= lPos.w; vec3 visibility = vec3(1.0); - if (lPos.w > 0.0) visibility = PCF(shadowMap, shadowMapTransparent, lPos.xy, lPos.z - shadowsBias, smSize, transparent); + if (lPos.w > 0.0) visibility = PCF(shadowMap, + #ifdef _ShadowMapTransparent + shadowMapTransparent, + #endif + lPos.xy, lPos.z - shadowsBias, smSize + #ifdef _ShadowMapTransparent + , transparent + #endif + ); // Blend cascade // https://github.com/TheRealMJP/Shadows @@ -423,13 +439,20 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap, sampler2D shadowMapTransparent vec4 lPos2 = LWVP2 * vec4(p, 1.0); lPos2.xyz /= lPos2.w; vec3 visibility2 = vec3(1.0); - if (lPos2.w > 0.0) visibility2 = PCF(shadowMap, shadowMapTransparent, lPos2.xy, lPos2.z - shadowsBias, smSize, transparent); + if (lPos2.w > 0.0) visibility2 = PCF(shadowMap, + #ifdef _ShadowMapTransparent + shadowMapTransparent, + #endif + lPos.xy, lPos.z - shadowsBias, smSize + #ifdef _ShadowMapTransparent + , transparent + #endif + ); float lerpAmt = smoothstep(0.0, blendThres, splitDist); return mix(visibility2, visibility, lerpAmt); } return visibility; - // Visualize cascades // if (ci == 0) albedo.rgb = vec3(1.0, 0.0, 0.0); // if (ci == 4) albedo.rgb = vec3(0.0, 1.0, 0.0); @@ -437,4 +460,4 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap, sampler2D shadowMapTransparent // if (ci == 12) albedo.rgb = vec3(1.0, 1.0, 0.0); } #endif -#endif +#endif \ No newline at end of file From b9848cd2dc26dd4858483914d29e3b882f60fb41 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Wed, 9 Jul 2025 23:20:46 +0000 Subject: [PATCH 04/12] revert e922cc38e694a5675dda93dfecebf330ca73d07f revert Update leenkx/Shaders/std/shadows.glsl --- leenkx/Shaders/std/shadows.glsl | 145 ++++++++++++++------------------ 1 file changed, 61 insertions(+), 84 deletions(-) diff --git a/leenkx/Shaders/std/shadows.glsl b/leenkx/Shaders/std/shadows.glsl index a1c01f4..d994eff 100644 --- a/leenkx/Shaders/std/shadows.glsl +++ b/leenkx/Shaders/std/shadows.glsl @@ -58,15 +58,7 @@ vec2 sampleCube(vec3 dir, out int faceIndex) { } #endif -vec3 PCF(sampler2DShadow shadowMap, - #ifdef _ShadowMapTransparent - sampler2D shadowMapTransparent, - #endif - const vec2 uv, const float compare, const vec2 smSize - #ifdef _ShadowMapTransparent - , const bool transparent - #endif - ) { +vec3 PCF(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, const vec2 uv, const float compare, const vec2 smSize, const bool transparent) { vec3 result = vec3(0.0); result.x = texture(shadowMap, vec3(uv + (vec2(-1.0, -1.0) / smSize), compare)); result.x += texture(shadowMap, vec3(uv + (vec2(-1.0, 0.0) / smSize), compare)); @@ -79,13 +71,11 @@ vec3 PCF(sampler2DShadow shadowMap, result.x += texture(shadowMap, vec3(uv + (vec2(1.0, 1.0) / smSize), compare)); result = result.xxx / 9.0; - #ifdef _ShadowMapTransparent if (transparent == false) { vec4 shadowmap_transparent = texture(shadowMapTransparent, uv); if (shadowmap_transparent.a < compare) result *= shadowmap_transparent.rgb; } - #endif return result; } @@ -97,15 +87,41 @@ float lpToDepth(vec3 lp, const vec2 lightProj) { return zcomp * 0.5 + 0.5; } -vec3 PCFCube(samplerCubeShadow shadowMapCube, - #ifdef _ShadowMapTransparent - samplerCube shadowMapCubeTransparent, - #endif - const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n - #ifdef _ShadowMapTransparent - , const bool transparent - #endif - ) { +#ifndef _ShadowMapAtlas +vec3 PCFCube(samplerCubeShadow shadowMapCube, samplerCube shadowMapCubeTransparent, vec3 lp, vec3 ml, float bias, vec2 lightProj, vec3 n, const bool transparent) { + const float s = shadowmapCubePcfSize; + float compare = lpToDepth(lp, lightProj) - bias * 1.5; + ml = ml + n * bias * 20; + #ifdef _InvY + ml.y = -ml.y; + #endif + + float shadowFactor = 0.0; + shadowFactor = texture(shadowMapCube, vec4(ml, compare)); + shadowFactor += texture(shadowMapCube, vec4(ml + vec3(s, s, s), compare)); + shadowFactor += texture(shadowMapCube, vec4(ml + vec3(-s, s, s), compare)); + shadowFactor += texture(shadowMapCube, vec4(ml + vec3(s, -s, s), compare)); + shadowFactor += texture(shadowMapCube, vec4(ml + vec3(s, s, -s), compare)); + shadowFactor += texture(shadowMapCube, vec4(ml + vec3(-s, -s, s), compare)); + shadowFactor += texture(shadowMapCube, vec4(ml + vec3(s, -s, -s), compare)); + shadowFactor += texture(shadowMapCube, vec4(ml + vec3(-s, s, -s), compare)); + shadowFactor += texture(shadowMapCube, vec4(ml + vec3(-s, -s, -s), compare)); + shadowFactor /= 9.0; + + vec3 result = vec3(shadowFactor); + + if (transparent == false) { + vec4 shadowmap_transparent = texture(shadowMapCubeTransparent, ml); + if (shadowmap_transparent.a < compare) + result *= shadowmap_transparent.rgb; + } + + return result; +} +#endif + +#ifdef _ShadowMapAtlas +vec3 PCFCube(samplerCubeShadow shadowMapCube, samplerCube shadowMapCubeTransparent, const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n, const bool transparent) { const float s = shadowmapCubePcfSize; // TODO: incorrect... float compare = lpToDepth(lp, lightProj) - bias * 1.5; ml = ml + n * bias * 20; @@ -124,18 +140,16 @@ vec3 PCFCube(samplerCubeShadow shadowMapCube, result.x += texture(shadowMapCube, vec4(ml + vec3(-s, -s, -s), compare)); result = result.xxx / 9.0; - #ifdef _ShadowMapTransparent if (transparent == false) { vec4 shadowmap_transparent = texture(shadowMapCubeTransparent, ml); if (shadowmap_transparent.a < compare) result *= shadowmap_transparent.rgb; } - #endif return result; } -#ifdef _ShadowMapAtlas + // transform "out-of-bounds" coordinates to the correct face/coordinate system // https://www.khronos.org/opengl/wiki/File:CubeMapAxes.png vec2 transformOffsetedUV(const int faceIndex, out int newFaceIndex, vec2 uv) { @@ -229,31 +243,21 @@ vec2 transformOffsetedUV(const int faceIndex, out int newFaceIndex, vec2 uv) { return uv; } -vec3 PCFFakeCube(sampler2DShadow shadowMap, - #ifdef _ShadowMapTransparent - sampler2D shadowMapTransparent, - #endif - const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n, const int index - #ifdef _ShadowMapTransparent - , const bool transparent - #endif - ) { +vec3 PCFFakeCube(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n, const int index, const bool transparent) { const vec2 smSize = smSizeUniform; // TODO: incorrect... const float compare = lpToDepth(lp, lightProj) - bias * 1.5; ml = ml + n * bias * 20; + int faceIndex = 0; const int lightIndex = index * 6; const vec2 uv = sampleCube(ml, faceIndex); + vec4 pointLightTile = pointLightDataArray[lightIndex + faceIndex]; // x: tile X offset, y: tile Y offset, z: tile size relative to atlas vec2 uvtiled = pointLightTile.z * uv + pointLightTile.xy; #ifdef _FlipY uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system #endif - if (any(lessThan(uvtiled, vec2(0.0))) || any(greaterThan(uvtiled, vec2(1.0)))) { - return vec3(1.0); // Handle edge cases by returning full light - } - vec3 result = vec3(0.0); result.x += texture(shadowMap, vec3(uvtiled, compare)); // soft shadowing @@ -266,6 +270,14 @@ vec3 PCFFakeCube(sampler2DShadow shadowMap, #endif result.x += texture(shadowMap, vec3(uvtiled, compare)); + uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, 1.0) / smSize))); + pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; + uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; + #ifdef _FlipY + uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system + #endif + result.x += texture(shadowMap, vec3(uvtiled, compare)); + uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(0.0, -1.0) / smSize))); pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; @@ -322,47 +334,30 @@ vec3 PCFFakeCube(sampler2DShadow shadowMap, uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system #endif - #ifdef _ShadowMapTransparent if (transparent == false) { vec4 shadowmap_transparent = texture(shadowMapTransparent, uvtiled); if (shadowmap_transparent.a < compare) result *= shadowmap_transparent.rgb; } - #endif return result; } #endif -vec3 shadowTest(sampler2DShadow shadowMap, - #ifdef _ShadowMapTransparent - sampler2D shadowMapTransparent, - #endif - const vec3 lPos, const float shadowsBias - #ifdef _ShadowMapTransparent - , const bool transparent - #endif - ) { +vec3 shadowTest(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, const vec3 lPos, const float shadowsBias, const bool transparent) { #ifdef _SMSizeUniform vec2 smSize = smSizeUniform; #else const vec2 smSize = shadowmapSize; #endif if (lPos.x < 0.0 || lPos.y < 0.0 || lPos.x > 1.0 || lPos.y > 1.0) return vec3(1.0); - return PCF(shadowMap, - #ifdef _ShadowMapTransparent - shadowMapTransparent, - #endif - lPos.xy, lPos.z - shadowsBias, smSize - #ifdef _ShadowMapTransparent - , transparent - #endif - ); + return PCF(shadowMap, shadowMapTransparent, lPos.xy, lPos.z - shadowsBias, smSize, transparent); } #ifdef _CSM mat4 getCascadeMat(const float d, out int casi, out int casIndex) { const int c = shadowmapCascades; + // Get cascade index // TODO: use bounding box slice selection instead of sphere const vec4 ci = vec4(float(c > 0), float(c > 1), float(c > 2), float(c > 3)); @@ -378,26 +373,21 @@ mat4 getCascadeMat(const float d, out int casi, out int casIndex) { float(d > casData[c * 4].z), float(d > casData[c * 4].w)); casi = int(min(dot(ci, comp), c)); + // Get cascade mat casIndex = casi * 4; + return mat4( casData[casIndex ], casData[casIndex + 1], casData[casIndex + 2], casData[casIndex + 3]); + // if (casIndex == 0) return mat4(casData[0], casData[1], casData[2], casData[3]); // .. } -vec3 shadowTestCascade(sampler2DShadow shadowMap, - #ifdef _ShadowMapTransparent - sampler2D shadowMapTransparent, - #endif - const vec3 eye, const vec3 p, const float shadowsBias - #ifdef _ShadowMapTransparent - , const bool transparent - #endif - ) { +vec3 shadowTestCascade(sampler2DShadow shadowMap, sampler2D shadowMapTransparent, const vec3 eye, const vec3 p, const float shadowsBias, const bool transparent) { #ifdef _SMSizeUniform vec2 smSize = smSizeUniform; #else @@ -405,22 +395,16 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap, #endif const int c = shadowmapCascades; float d = distance(eye, p); + int casi; int casIndex; mat4 LWVP = getCascadeMat(d, casi, casIndex); + vec4 lPos = LWVP * vec4(p, 1.0); lPos.xyz /= lPos.w; vec3 visibility = vec3(1.0); - if (lPos.w > 0.0) visibility = PCF(shadowMap, - #ifdef _ShadowMapTransparent - shadowMapTransparent, - #endif - lPos.xy, lPos.z - shadowsBias, smSize - #ifdef _ShadowMapTransparent - , transparent - #endif - ); + if (lPos.w > 0.0) visibility = PCF(shadowMap, shadowMapTransparent, lPos.xy, lPos.z - shadowsBias, smSize, transparent); // Blend cascade // https://github.com/TheRealMJP/Shadows @@ -439,20 +423,13 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap, vec4 lPos2 = LWVP2 * vec4(p, 1.0); lPos2.xyz /= lPos2.w; vec3 visibility2 = vec3(1.0); - if (lPos2.w > 0.0) visibility2 = PCF(shadowMap, - #ifdef _ShadowMapTransparent - shadowMapTransparent, - #endif - lPos.xy, lPos.z - shadowsBias, smSize - #ifdef _ShadowMapTransparent - , transparent - #endif - ); + if (lPos2.w > 0.0) visibility2 = PCF(shadowMap, shadowMapTransparent, lPos2.xy, lPos2.z - shadowsBias, smSize, transparent); float lerpAmt = smoothstep(0.0, blendThres, splitDist); return mix(visibility2, visibility, lerpAmt); } return visibility; + // Visualize cascades // if (ci == 0) albedo.rgb = vec3(1.0, 0.0, 0.0); // if (ci == 4) albedo.rgb = vec3(0.0, 1.0, 0.0); @@ -460,4 +437,4 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap, // if (ci == 12) albedo.rgb = vec3(1.0, 1.0, 0.0); } #endif -#endif \ No newline at end of file +#endif From de6bf8a08aa46425c762390fea66635fef3123e2 Mon Sep 17 00:00:00 2001 From: wuaieyo Date: Fri, 11 Jul 2025 19:21:01 +0200 Subject: [PATCH 05/12] added last needed important rigid body settings in the blender RB leenkx settings for game engine ? like min max velocity,damping and lock translation and rotationboolean settings --- .../leenkx/trait/physics/bullet/RigidBody.hx | 113 +++++++++++++++++- leenkx/blender/lnx/exporter.py | 16 ++- leenkx/blender/lnx/props.py | 23 ++++ leenkx/blender/lnx/props_ui.py | 35 ++++++ 4 files changed, 182 insertions(+), 5 deletions(-) diff --git a/leenkx/Sources/leenkx/trait/physics/bullet/RigidBody.hx b/leenkx/Sources/leenkx/trait/physics/bullet/RigidBody.hx index a84d758..d4912bc 100644 --- a/leenkx/Sources/leenkx/trait/physics/bullet/RigidBody.hx +++ b/leenkx/Sources/leenkx/trait/physics/bullet/RigidBody.hx @@ -36,6 +36,18 @@ class RigidBody extends iron.Trait { var useDeactivation: Bool; var deactivationParams: Array; var ccd = false; // Continuous collision detection + // New velocity limiting properties + var linearVelocityMin: Float; + var linearVelocityMax: Float; + var angularVelocityMin: Float; + var angularVelocityMax: Float; + // New lock properties + var lockTranslationX: Bool; + var lockTranslationY: Bool; + var lockTranslationZ: Bool; + var lockRotationX: Bool; + var lockRotationY: Bool; + var lockRotationZ: Bool; public var group = 1; public var mask = 1; var trigger = false; @@ -120,7 +132,17 @@ class RigidBody extends iron.Trait { collisionMargin: 0.0, linearDeactivationThreshold: 0.0, angularDeactivationThrshold: 0.0, - deactivationTime: 0.0 + deactivationTime: 0.0, + linearVelocityMin: 0.0, + linearVelocityMax: 0.0, + angularVelocityMin: 0.0, + angularVelocityMax: 0.0, + lockTranslationX: false, + lockTranslationY: false, + lockTranslationZ: false, + lockRotationX: false, + lockRotationY: false, + lockRotationZ: false }; if (flags == null) flags = { @@ -139,6 +161,18 @@ class RigidBody extends iron.Trait { this.angularFactors = [params.angularFactorsX, params.angularFactorsY, params.angularFactorsZ]; this.collisionMargin = params.collisionMargin; this.deactivationParams = [params.linearDeactivationThreshold, params.angularDeactivationThrshold, params.deactivationTime]; + // New velocity limiting properties + this.linearVelocityMin = params.linearVelocityMin; + this.linearVelocityMax = params.linearVelocityMax; + this.angularVelocityMin = params.angularVelocityMin; + this.angularVelocityMax = params.angularVelocityMax; + // New lock properties + this.lockTranslationX = params.lockTranslationX; + this.lockTranslationY = params.lockTranslationY; + this.lockTranslationZ = params.lockTranslationZ; + this.lockRotationX = params.lockRotationX; + this.lockRotationY = params.lockRotationY; + this.lockRotationZ = params.lockRotationZ; this.animated = flags.animated; this.trigger = flags.trigger; this.ccd = flags.ccd; @@ -291,11 +325,25 @@ class RigidBody extends iron.Trait { } if (linearFactors != null) { - setLinearFactor(linearFactors[0], linearFactors[1], linearFactors[2]); + // Apply lock properties by overriding factors + var lx = linearFactors[0]; + var ly = linearFactors[1]; + var lz = linearFactors[2]; + if (lockTranslationX) lx = 0.0; + if (lockTranslationY) ly = 0.0; + if (lockTranslationZ) lz = 0.0; + setLinearFactor(lx, ly, lz); } if (angularFactors != null) { - setAngularFactor(angularFactors[0], angularFactors[1], angularFactors[2]); + // Apply lock properties by overriding factors + var ax = angularFactors[0]; + var ay = angularFactors[1]; + var az = angularFactors[2]; + if (lockRotationX) ax = 0.0; + if (lockRotationY) ay = 0.0; + if (lockRotationZ) az = 0.0; + setAngularFactor(ax, ay, az); } if (trigger) bodyColl.setCollisionFlags(bodyColl.getCollisionFlags() | CF_NO_CONTACT_RESPONSE); @@ -411,6 +459,55 @@ class RigidBody extends iron.Trait { var rbs = physics.getContacts(this); if (rbs != null) for (rb in rbs) for (f in onContact) f(rb); } + + // Apply velocity limiting if enabled + if (!animated && !staticObj) { + applyVelocityLimits(); + } + } + + function applyVelocityLimits() { + if (!ready) return; + + // Check linear velocity limits + if (linearVelocityMin > 0.0 || linearVelocityMax > 0.0) { + var velocity = getLinearVelocity(); + var speed = velocity.length(); + + if (linearVelocityMin > 0.0 && speed < linearVelocityMin) { + // Increase velocity to minimum + if (speed > 0.0) { + velocity.normalize(); + velocity.mult(linearVelocityMin); + setLinearVelocity(velocity.x, velocity.y, velocity.z); + } + } else if (linearVelocityMax > 0.0 && speed > linearVelocityMax) { + // Clamp velocity to maximum + velocity.normalize(); + velocity.mult(linearVelocityMax); + setLinearVelocity(velocity.x, velocity.y, velocity.z); + } + } + + // Check angular velocity limits + if (angularVelocityMin > 0.0 || angularVelocityMax > 0.0) { + var angularVel = getAngularVelocity(); + var angularSpeed = angularVel.length(); + + if (angularVelocityMin > 0.0 && angularSpeed < angularVelocityMin) { + // Increase angular velocity to minimum + if (angularSpeed > 0.0) { + angularVel.normalize(); + angularVel.mult(angularVelocityMin); + setAngularVelocity(angularVel.x, angularVel.y, angularVel.z); + } + } else if (angularVelocityMax > 0.0 && angularSpeed > angularVelocityMax) { + // Clamp angular velocity to maximum + angularVel.normalize(); + angularVel.mult(angularVelocityMax); + setAngularVelocity(angularVel.x, angularVel.y, angularVel.z); + } + } } public function disableCollision() { @@ -745,6 +842,16 @@ typedef RigidBodyParams = { var linearDeactivationThreshold: Float; var angularDeactivationThrshold: Float; var deactivationTime: Float; + var linearVelocityMin: Float; + var linearVelocityMax: Float; + var angularVelocityMin: Float; + var angularVelocityMax: Float; + var lockTranslationX: Bool; + var lockTranslationY: Bool; + var lockTranslationZ: Bool; + var lockRotationX: Bool; + var lockRotationY: Bool; + var lockRotationZ: Bool; } typedef RigidBodyFlags = { diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index 5be20d2..4fd6fec 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -2830,8 +2830,8 @@ class LeenkxExporter: deact_av = 0.0 deact_time = 0.0 body_params = {} - body_params['linearDamping'] = rb.linear_damping - body_params['angularDamping'] = rb.angular_damping + body_params['linearDamping'] = bobject.lnx_rb_linear_damping + body_params['angularDamping'] = bobject.lnx_rb_angular_damping body_params['linearFactorsX'] = lx body_params['linearFactorsY'] = ly body_params['linearFactorsZ'] = lz @@ -2843,6 +2843,18 @@ class LeenkxExporter: body_params['linearDeactivationThreshold'] = deact_lv body_params['angularDeactivationThrshold'] = deact_av body_params['deactivationTime'] = deact_time + # New velocity limit properties + body_params['linearVelocityMin'] = bobject.lnx_rb_linear_velocity_min + body_params['linearVelocityMax'] = bobject.lnx_rb_linear_velocity_max + body_params['angularVelocityMin'] = bobject.lnx_rb_angular_velocity_min + body_params['angularVelocityMax'] = bobject.lnx_rb_angular_velocity_max + # New lock properties + body_params['lockTranslationX'] = bobject.lnx_rb_lock_translation_x + body_params['lockTranslationY'] = bobject.lnx_rb_lock_translation_y + body_params['lockTranslationZ'] = bobject.lnx_rb_lock_translation_z + body_params['lockRotationX'] = bobject.lnx_rb_lock_rotation_x + body_params['lockRotationY'] = bobject.lnx_rb_lock_rotation_y + body_params['lockRotationZ'] = bobject.lnx_rb_lock_rotation_z body_flags = {} body_flags['animated'] = rb.kinematic body_flags['trigger'] = bobject.lnx_rb_trigger diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index 04cd05c..ce6d643 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -368,6 +368,29 @@ def init_properties(): default=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False), size=20, subtype='LAYER') + + # Linear velocity limits + bpy.types.Object.lnx_rb_linear_velocity_min = FloatProperty(name="Linear Velocity Min", description="Minimum linear velocity", default=0.0, min=0.0) + bpy.types.Object.lnx_rb_linear_velocity_max = FloatProperty(name="Linear Velocity Max", description="Maximum linear velocity", default=0.0, min=0.0) + + # Angular velocity limits + bpy.types.Object.lnx_rb_angular_velocity_min = FloatProperty(name="Angular Velocity Min", description="Minimum angular velocity", default=0.0, min=0.0) + bpy.types.Object.lnx_rb_angular_velocity_max = FloatProperty(name="Angular Velocity Max", description="Maximum angular velocity", default=0.0, min=0.0) + + # Damping controls + bpy.types.Object.lnx_rb_linear_damping = FloatProperty(name="Linear Damping", description="Linear damping factor", default=0.04, min=0.0, max=1.0) + bpy.types.Object.lnx_rb_angular_damping = FloatProperty(name="Angular Damping", description="Angular damping factor", default=0.1, min=0.0, max=1.0) + + # Lock translation axes + bpy.types.Object.lnx_rb_lock_translation_x = BoolProperty(name="Lock Translation X", description="Lock movement along X axis", default=False) + bpy.types.Object.lnx_rb_lock_translation_y = BoolProperty(name="Lock Translation Y", description="Lock movement along Y axis", default=False) + bpy.types.Object.lnx_rb_lock_translation_z = BoolProperty(name="Lock Translation Z", description="Lock movement along Z axis", default=False) + + # Lock rotation axes + bpy.types.Object.lnx_rb_lock_rotation_x = BoolProperty(name="Lock Rotation X", description="Lock rotation around X axis", default=False) + bpy.types.Object.lnx_rb_lock_rotation_y = BoolProperty(name="Lock Rotation Y", description="Lock rotation around Y axis", default=False) + bpy.types.Object.lnx_rb_lock_rotation_z = BoolProperty(name="Lock Rotation Z", description="Lock rotation around Z axis", default=False) + 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='') diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index 33b0a8c..6ff3211 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -240,6 +240,41 @@ class LNX_PT_PhysicsPropsPanel(bpy.types.Panel): layout.prop(obj, 'lnx_rb_linear_factor') layout.prop(obj, 'lnx_rb_angular_factor') layout.prop(obj, 'lnx_rb_angular_friction') + + # Linear Velocity section + layout.separator() + layout.label(text="Linear Velocity:") + layout.prop(obj, 'lnx_rb_linear_velocity_min') + layout.prop(obj, 'lnx_rb_linear_velocity_max') + + # Angular Velocity section + layout.separator() + layout.label(text="Angular Velocity:") + layout.prop(obj, 'lnx_rb_angular_velocity_min') + layout.prop(obj, 'lnx_rb_angular_velocity_max') + + # Damping section + layout.separator() + layout.label(text="Damping:") + layout.prop(obj, 'lnx_rb_linear_damping') + layout.prop(obj, 'lnx_rb_angular_damping') + + # Lock Translation section + layout.separator() + layout.label(text="Lock Translation:") + row = layout.row(align=True) + row.prop(obj, 'lnx_rb_lock_translation_x', text="X") + row.prop(obj, 'lnx_rb_lock_translation_y', text="Y") + row.prop(obj, 'lnx_rb_lock_translation_z', text="Z") + + # Lock Rotation section + layout.separator() + layout.label(text="Lock Rotation:") + row = layout.row(align=True) + row.prop(obj, 'lnx_rb_lock_rotation_x', text="X") + row.prop(obj, 'lnx_rb_lock_rotation_y', text="Y") + row.prop(obj, 'lnx_rb_lock_rotation_z', text="Z") + layout.prop(obj, 'lnx_rb_trigger') layout.prop(obj, 'lnx_rb_ccd') layout.prop(obj, 'lnx_rb_interpolate') From a1ee335c68a3ae973ec428ee9c3afb684ecc8a11 Mon Sep 17 00:00:00 2001 From: wuaieyo Date: Sun, 13 Jul 2025 04:23:51 +0200 Subject: [PATCH 06/12] removed the translation daping and rotation dampign because they would override the alreayd existedt under Dynamics panel --- leenkx/blender/lnx/exporter.py | 4 ++-- leenkx/blender/lnx/props.py | 3 --- leenkx/blender/lnx/props_ui.py | 5 ----- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index 4fd6fec..d7bf17c 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -2830,8 +2830,8 @@ class LeenkxExporter: deact_av = 0.0 deact_time = 0.0 body_params = {} - body_params['linearDamping'] = bobject.lnx_rb_linear_damping - body_params['angularDamping'] = bobject.lnx_rb_angular_damping + body_params['linearDamping'] = rb.linear_damping + body_params['angularDamping'] = rb.angular_damping body_params['linearFactorsX'] = lx body_params['linearFactorsY'] = ly body_params['linearFactorsZ'] = lz diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index ce6d643..7ab49fb 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -377,9 +377,6 @@ def init_properties(): bpy.types.Object.lnx_rb_angular_velocity_min = FloatProperty(name="Angular Velocity Min", description="Minimum angular velocity", default=0.0, min=0.0) bpy.types.Object.lnx_rb_angular_velocity_max = FloatProperty(name="Angular Velocity Max", description="Maximum angular velocity", default=0.0, min=0.0) - # Damping controls - bpy.types.Object.lnx_rb_linear_damping = FloatProperty(name="Linear Damping", description="Linear damping factor", default=0.04, min=0.0, max=1.0) - bpy.types.Object.lnx_rb_angular_damping = FloatProperty(name="Angular Damping", description="Angular damping factor", default=0.1, min=0.0, max=1.0) # Lock translation axes bpy.types.Object.lnx_rb_lock_translation_x = BoolProperty(name="Lock Translation X", description="Lock movement along X axis", default=False) diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index 6ff3211..cdb6b3f 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -253,11 +253,6 @@ class LNX_PT_PhysicsPropsPanel(bpy.types.Panel): layout.prop(obj, 'lnx_rb_angular_velocity_min') layout.prop(obj, 'lnx_rb_angular_velocity_max') - # Damping section - layout.separator() - layout.label(text="Damping:") - layout.prop(obj, 'lnx_rb_linear_damping') - layout.prop(obj, 'lnx_rb_angular_damping') # Lock Translation section layout.separator() From a65675ef752a924cdc7afd21c6e5275d840ff82d Mon Sep 17 00:00:00 2001 From: Onek8 Date: Tue, 15 Jul 2025 17:57:38 +0000 Subject: [PATCH 07/12] Update leenkx/blender/lnx/handlers.py --- leenkx/blender/lnx/handlers.py | 127 +++++++++++++++++++++++++++------ 1 file changed, 106 insertions(+), 21 deletions(-) diff --git a/leenkx/blender/lnx/handlers.py b/leenkx/blender/lnx/handlers.py index ef47589..5d62a25 100644 --- a/leenkx/blender/lnx/handlers.py +++ b/leenkx/blender/lnx/handlers.py @@ -2,7 +2,10 @@ import importlib import os import queue import sys +import threading +import time import types +from typing import Dict, Tuple, Callable, Set import bpy from bpy.app.handlers import persistent @@ -30,6 +33,10 @@ if lnx.is_reload(__name__): else: lnx.enable_reload(__name__) +# Module-level storage for active threads (eliminates re-queuing overhead) +_active_threads: Dict[threading.Thread, Callable] = {} +_last_poll_time = 0.0 +_consecutive_empty_polls = 0 @persistent def on_depsgraph_update_post(self): @@ -135,35 +142,113 @@ def always() -> float: def poll_threads() -> float: - """Polls the thread callback queue and if a thread has finished, it - is joined with the main thread and the corresponding callback is - executed in the main thread. """ + Improved thread polling with: + - No re-queuing overhead + - Batch processing of completed threads + - Adaptive timing based on activity + - Better memory management + - Simplified logic flow + """ + global _last_poll_time, _consecutive_empty_polls + current_time = time.time() + + # Process all new threads from queue at once (batch processing) + new_threads_added = 0 try: - thread, callback = make.thread_callback_queue.get(block=False) + while True: + thread, callback = make.thread_callback_queue.get(block=False) + _active_threads[thread] = callback + new_threads_added += 1 except queue.Empty: + pass + + # Early return if no active threads + if not _active_threads: + _consecutive_empty_polls += 1 + # Adaptive timing: longer intervals when consistently empty + if _consecutive_empty_polls > 10: + return 0.5 # Back off when no activity return 0.25 - if thread.is_alive(): - try: - make.thread_callback_queue.put((thread, callback), block=False) - except queue.Full: - return 0.5 - return 0.1 + + # Reset empty poll counter when we have active threads + _consecutive_empty_polls = 0 + + # Find completed threads (single pass, no re-queuing) + completed_threads = [] + for thread in list(_active_threads.keys()): + if not thread.is_alive(): + completed_threads.append(thread) + + # Batch process all completed threads + if completed_threads: + _process_completed_threads(completed_threads) + + # Adaptive timing based on activity level + active_count = len(_active_threads) + if active_count == 0: + return 0.25 + elif active_count <= 3: + return 0.05 # Medium frequency for low activity else: + return 0.01 # High frequency for high activity + +def _process_completed_threads(completed_threads: list) -> None: + """Process a batch of completed threads with robust error handling.""" + for thread in completed_threads: + callback = _active_threads.pop(thread) # Remove from tracking + try: - thread.join() + thread.join() # Should be instant since thread is dead callback() except Exception as e: - # If there is an exception, we can no longer return the time to - # the next call to this polling function, so to keep it running - # we re-register it and then raise the original exception. - try: - bpy.app.timers.unregister(poll_threads) - except ValueError: - pass - bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True) - # Quickly check if another thread has finished - return 0.01 + # Robust error recovery + _handle_callback_error(e) + continue # Continue processing other threads + + # Explicit cleanup for better memory management + del thread, callback + +def _handle_callback_error(exception: Exception) -> None: + """Centralized error handling with better recovery.""" + try: + # Try to unregister existing timer + bpy.app.timers.unregister(poll_threads) + except ValueError: + pass # Timer wasn't registered, that's fine + + # Re-register timer with slightly longer interval for stability + bpy.app.timers.register(poll_threads, first_interval=0.1, persistent=True) + + # Re-raise the original exception after ensuring timer continuity + raise exception + +def cleanup_polling_system() -> None: + """Optional cleanup function for proper shutdown.""" + global _active_threads, _consecutive_empty_polls + + # Wait for remaining threads to complete (with timeout) + for thread in list(_active_threads.keys()): + if thread.is_alive(): + thread.join(timeout=1.0) # 1 second timeout + + # Clear tracking structures + _active_threads.clear() + _consecutive_empty_polls = 0 + + # Unregister timer + try: + bpy.app.timers.unregister(poll_threads) + except ValueError: + pass + +def get_polling_stats() -> dict: + """Get statistics about the polling system for monitoring.""" + return { + 'active_threads': len(_active_threads), + 'consecutive_empty_polls': _consecutive_empty_polls, + 'thread_ids': [t.ident for t in _active_threads.keys()] + } loaded_py_libraries: dict[str, types.ModuleType] = {} From e9aae53be9a0646bfbd8956c72f930f46f8bb31d Mon Sep 17 00:00:00 2001 From: Onek8 Date: Tue, 15 Jul 2025 19:05:14 +0000 Subject: [PATCH 08/12] t3du - Fix attribute error rp_gi --- leenkx/blender/lnx/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index 7ab49fb..e8babe4 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -613,7 +613,7 @@ def update_leenkx_world(): if bpy.data.filepath != '' and (file_version < sdk_version or wrd.lnx_commit != lnx_commit): # This allows for seamless migration from earlier versions of Leenkx for rp in wrd.lnx_rplist: # TODO: deprecated - if rp.rp_gi != 'Off': + if hasattr(rp, 'rp_gi') and rp.rp_gi != 'Off': rp.rp_gi = 'Off' rp.rp_voxels = rp.rp_gi From 6afc209db72e41a31831a7e350721892e0523494 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Tue, 15 Jul 2025 22:06:11 +0000 Subject: [PATCH 09/12] Add leenkx/blender/lnx/logicnode/logic/LN_once.py --- leenkx/blender/lnx/logicnode/logic/LN_once.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 leenkx/blender/lnx/logicnode/logic/LN_once.py diff --git a/leenkx/blender/lnx/logicnode/logic/LN_once.py b/leenkx/blender/lnx/logicnode/logic/LN_once.py new file mode 100644 index 0000000..5bb9d02 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/logic/LN_once.py @@ -0,0 +1,15 @@ +from lnx.logicnode.lnx_nodes import * +import bpy + +class OnceNode(LnxLogicTreeNode): + bl_idname = 'LNOnceNode' + bl_label = 'Once' + lnx_version = 1 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'Run Once') + self.add_input('LnxNodeSocketAction', 'Reset') + + self.add_output('LnxNodeSocketAction', 'Out') + + From 82412dbf81ed8a2ab5ca7c44681b955fd57c8f16 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Tue, 15 Jul 2025 22:07:02 +0000 Subject: [PATCH 10/12] Add leenkx/Sources/leenkx/logicnode/OnceNode.hx --- leenkx/Sources/leenkx/logicnode/OnceNode.hx | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 leenkx/Sources/leenkx/logicnode/OnceNode.hx diff --git a/leenkx/Sources/leenkx/logicnode/OnceNode.hx b/leenkx/Sources/leenkx/logicnode/OnceNode.hx new file mode 100644 index 0000000..960c4f8 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/OnceNode.hx @@ -0,0 +1,23 @@ +package leenkx.logicnode; + +class OnceNode extends LogicNode { + + var triggered:Bool = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + if(from == 1){ + triggered = false; + return; + } + + if (!triggered) { + triggered = true; + runOutput(0); + } + } + +} From df4feac132c1f8dff4807d59f44449b4e52d85fe Mon Sep 17 00:00:00 2001 From: Onek8 Date: Wed, 16 Jul 2025 05:14:28 +0000 Subject: [PATCH 11/12] Add leenkx/blender/lnx/logicnode/object/LN_set_object_delayed_location.py --- .../object/LN_set_object_delayed_location.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 leenkx/blender/lnx/logicnode/object/LN_set_object_delayed_location.py diff --git a/leenkx/blender/lnx/logicnode/object/LN_set_object_delayed_location.py b/leenkx/blender/lnx/logicnode/object/LN_set_object_delayed_location.py new file mode 100644 index 0000000..7a40091 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/object/LN_set_object_delayed_location.py @@ -0,0 +1,28 @@ +from lnx.logicnode.lnx_nodes import * +import bpy +from bpy.props import BoolProperty + +class LNSetObjectDelayedLocationNode(LnxLogicTreeNode): + bl_idname = 'LNSetObjectDelayedLocationNode' + bl_label = 'Set Object Delayed Location' + lnx_section = 'props' + lnx_version = 1 + + use_local_space: BoolProperty( + name="Use Local Space", + description="Move follower in local space instead of world space", + default=False + ) + + def lnx_init(self, context): + self.inputs.new('LnxNodeSocketAction', 'In') + self.inputs.new('LnxNodeSocketObject', 'Source Object') + self.inputs.new('LnxNodeSocketObject', 'Target Object') + self.inputs.new('LnxFloatSocket', 'Delay') + self.outputs.new('LnxNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'use_local_space') + + def draw_label(self): + return "Set Object Delayed Location" From 28943f1522da937c797939b9e23b858c7ea9fb91 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Wed, 16 Jul 2025 05:15:59 +0000 Subject: [PATCH 12/12] Add leenkx/Sources/leenkx/logicnode/SetObjectDelayedLocationNode.hx --- .../logicnode/SetObjectDelayedLocationNode.hx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 leenkx/Sources/leenkx/logicnode/SetObjectDelayedLocationNode.hx diff --git a/leenkx/Sources/leenkx/logicnode/SetObjectDelayedLocationNode.hx b/leenkx/Sources/leenkx/logicnode/SetObjectDelayedLocationNode.hx new file mode 100644 index 0000000..ccc8b51 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/SetObjectDelayedLocationNode.hx @@ -0,0 +1,74 @@ +package leenkx.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import iron.math.Mat4; +import iron.system.Time; + +class SetObjectDelayedLocationNode extends LogicNode { + public var use_local_space: Bool = false; + + private var initialOffset: Vec4 = null; + private var targetPos: Vec4 = new Vec4(); + private var currentPos: Vec4 = new Vec4(); + private var deltaVec: Vec4 = new Vec4(); + private var tempVec: Vec4 = new Vec4(); + + private var lastParent: Object = null; + private var invParentMatrix: Mat4 = null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var follower: Object = inputs[1].get(); + var target: Object = inputs[2].get(); + var delay: Float = inputs[3].get(); + + if (follower == null || target == null || delay == null) return runOutput(0); + + if (initialOffset == null) { + initialOffset = new Vec4(); + var followerPos = follower.transform.world.getLoc(); + var targetPos = target.transform.world.getLoc(); + initialOffset.setFrom(followerPos); + initialOffset.sub(targetPos); + } + + targetPos.setFrom(target.transform.world.getLoc()); + currentPos.setFrom(follower.transform.world.getLoc()); + + tempVec.setFrom(targetPos).add(initialOffset); + + deltaVec.setFrom(tempVec).sub(currentPos); + + if (deltaVec.length() < 0.001 && delay < 0.01) { + runOutput(0); + return; + } + + if (delay == 0.0) { + currentPos.setFrom(tempVec); + } else { + var smoothFactor = Math.exp(-Time.delta / Math.max(0.0001, delay)); + currentPos.x = tempVec.x + (currentPos.x - tempVec.x) * smoothFactor; + currentPos.y = tempVec.y + (currentPos.y - tempVec.y) * smoothFactor; + currentPos.z = tempVec.z + (currentPos.z - tempVec.z) * smoothFactor; + } + if (use_local_space && follower.parent != null) { + if (follower.parent != lastParent || invParentMatrix == null) { + lastParent = follower.parent; + invParentMatrix = Mat4.identity(); + invParentMatrix.getInverse(follower.parent.transform.world); + } + tempVec.setFrom(currentPos); + tempVec.applymat(invParentMatrix); + follower.transform.loc.set(tempVec.x, tempVec.y, tempVec.z); + } else { + follower.transform.loc.set(currentPos.x, currentPos.y, currentPos.z); + } + follower.transform.buildMatrix(); + runOutput(0); + } +}