From f5c9e70d1acf7f26d1edf060c51997c657cc77c6 Mon Sep 17 00:00:00 2001 From: wuaieyo Date: Thu, 26 Jun 2025 03:05:11 +0200 Subject: [PATCH 1/2] 1. added local rotation so that if the source object is child then it would still align to the target object. 2. removed rotation output socket since is not really needed. --- .../leenkx/logicnode/SetLookAtRotationNode.hx | 60 ++++++++++++------- .../transform/LN_set_look_at_rotation.py | 11 +++- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/leenkx/Sources/leenkx/logicnode/SetLookAtRotationNode.hx b/leenkx/Sources/leenkx/logicnode/SetLookAtRotationNode.hx index 15fb2d7..d0f60b2 100644 --- a/leenkx/Sources/leenkx/logicnode/SetLookAtRotationNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetLookAtRotationNode.hx @@ -12,6 +12,7 @@ class SetLookAtRotationNode extends LogicNode { public var property2: String; // Use vector for source (true/false) public var property3: String; // Damping value (backward compatibility, now input socket) public var property4: String; // Disable rotation on aligning axis (true/false) + public var property5: String; // Use local space (true/false) // Store the calculated rotation for output var calculatedRotation: Quat = null; @@ -51,8 +52,8 @@ class SetLookAtRotationNode extends LogicNode { return; } - // Get source object's position - objectLoc = objectToUse.transform.loc; + // Get source object's WORLD position (important for child objects) + objectLoc = new Vec4(objectToUse.transform.worldx(), objectToUse.transform.worldy(), objectToUse.transform.worldz()); } // Determine if we're using a vector or an object as target @@ -74,8 +75,8 @@ class SetLookAtRotationNode extends LogicNode { return; } - // Get target object's position - targetLoc = targetObject.transform.loc; + // Get target object's WORLD position (important for child objects) + targetLoc = new Vec4(targetObject.transform.worldx(), targetObject.transform.worldy(), targetObject.transform.worldz()); } // Calculate direction to target @@ -122,6 +123,28 @@ class SetLookAtRotationNode extends LogicNode { calculatedRotation.fromEulerOrdered(eulerAngles, "XYZ"); } + // Convert world rotation to local rotation if local space is enabled and object has a parent + var targetRotation = new Quat(); + if (property5 == "true" && objectToUse.parent != null) { + // Get parent's world rotation + var parentWorldLoc = new Vec4(); + var parentWorldRot = new Quat(); + var parentWorldScale = new Vec4(); + objectToUse.parent.transform.world.decompose(parentWorldLoc, parentWorldRot, parentWorldScale); + + // Convert world rotation to local space by removing parent's rotation influence + // local_rotation = inverse(parent_world_rotation) * world_rotation + var invParentRot = new Quat().setFrom(parentWorldRot); + invParentRot.x = -invParentRot.x; + invParentRot.y = -invParentRot.y; + invParentRot.z = -invParentRot.z; + + targetRotation.multquats(invParentRot, calculatedRotation); + } else { + // No local space conversion needed, use world rotation directly + targetRotation.setFrom(calculatedRotation); + } + // Apply rotation with damping var dampingValue: Float = 0.0; @@ -141,17 +164,17 @@ class SetLookAtRotationNode extends LogicNode { // Higher damping = slower rotation (smaller step) var step = Math.max(0.001, (1.0 - dampingValue) * 0.2); // 0.001 to 0.2 range - // Get current rotation as quaternion - var currentRot = new Quat().setFrom(objectToUse.transform.rot); + // Get current local rotation as quaternion + var currentLocalRot = new Quat().setFrom(objectToUse.transform.rot); // Calculate the difference between current and target rotation var diffQuat = new Quat(); // q1 * inverse(q2) gives the rotation from q2 to q1 - var invCurrent = new Quat().setFrom(currentRot); + var invCurrent = new Quat().setFrom(currentLocalRot); invCurrent.x = -invCurrent.x; invCurrent.y = -invCurrent.y; invCurrent.z = -invCurrent.z; - diffQuat.multquats(calculatedRotation, invCurrent); + diffQuat.multquats(targetRotation, invCurrent); // Convert to axis-angle representation var axis = new Vec4(); @@ -163,15 +186,15 @@ class SetLookAtRotationNode extends LogicNode { // Create partial rotation quaternion var partialRot = new Quat().fromAxisAngle(axis, partialAngle); - // Apply this partial rotation to current - var newRot = new Quat(); - newRot.multquats(partialRot, currentRot); + // Apply this partial rotation to current local rotation + var newLocalRot = new Quat(); + newLocalRot.multquats(partialRot, currentLocalRot); - // Apply the new rotation - objectToUse.transform.rot.setFrom(newRot); + // Apply the new local rotation + objectToUse.transform.rot.setFrom(newLocalRot); } else { // No damping, apply instant rotation - objectToUse.transform.rot.setFrom(calculatedRotation); + objectToUse.transform.rot.setFrom(targetRotation); } objectToUse.transform.buildMatrix(); @@ -179,12 +202,5 @@ class SetLookAtRotationNode extends LogicNode { runOutput(0); } - // Getter method for output sockets - override function get(from: Int): Dynamic { - // Output index 1 is the rotation socket (global rotation) - if (from == 1) { - return calculatedRotation; - } - return null; - } + // No output sockets needed - this node only performs actions } diff --git a/leenkx/blender/lnx/logicnode/transform/LN_set_look_at_rotation.py b/leenkx/blender/lnx/logicnode/transform/LN_set_look_at_rotation.py index b2d76e8..4f8fbd2 100644 --- a/leenkx/blender/lnx/logicnode/transform/LN_set_look_at_rotation.py +++ b/leenkx/blender/lnx/logicnode/transform/LN_set_look_at_rotation.py @@ -29,6 +29,8 @@ class SetLookAtRotationNode(LnxLogicTreeNode): update=lambda self, context: self.update_sockets(context) ) + + damping: bpy.props.FloatProperty( name='Damping', description='Amount of damping for rotation (0.0 = instant, 1.0 = no movement)', @@ -74,6 +76,12 @@ class SetLookAtRotationNode(LnxLogicTreeNode): ('false', 'False', 'False')], name='Disable Rotation on Aligning Axis', default='false') + property5: HaxeEnumProperty( + 'property5', + items = [('true', 'True', 'True'), + ('false', 'False', 'False')], + name='Use Local Space', default='false') + def lnx_init(self, context): # Add inputs in standard order self.inputs.new('LnxNodeSocketAction', 'In') @@ -90,8 +98,6 @@ class SetLookAtRotationNode(LnxLogicTreeNode): # Add outputs self.add_output('LnxNodeSocketAction', 'Out') - # Add rotation output socket - self.add_output('LnxRotationSocket', 'Rotation') def draw_buttons(self, context, layout): # 1. Axis Selector @@ -114,6 +120,7 @@ class SetLookAtRotationNode(LnxLogicTreeNode): self.property2 = 'true' if self.use_source_vector else 'false' self.property3 = str(self.damping) # Keep for backward compatibility self.property4 = 'true' if self.disable_rotation_on_align_axis else 'false' + self.property5 = 'true' # Always use local space functionality # Store current object references before changing sockets self.save_object_references() From 647b73b746501bfb0affc4638f6d46f48bf53d9c Mon Sep 17 00:00:00 2001 From: wuaieyo Date: Mon, 30 Jun 2025 06:35:06 +0200 Subject: [PATCH 2/2] added Mouse Look node for FSP style movement of object like camera... --- .../Sources/leenkx/logicnode/MouseLookNode.hx | 232 ++++++++++++++++++ .../lnx/logicnode/input/LN_mouse_look.py | 101 ++++++++ 2 files changed, 333 insertions(+) create mode 100644 leenkx/Sources/leenkx/logicnode/MouseLookNode.hx create mode 100644 leenkx/blender/lnx/logicnode/input/LN_mouse_look.py diff --git a/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx b/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx new file mode 100644 index 0000000..edf6517 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/MouseLookNode.hx @@ -0,0 +1,232 @@ +package leenkx.logicnode; + +import iron.math.Vec4; +import iron.system.Input; +import iron.object.Object; +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; // 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 + + // 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; + + + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var bodyObject: Object = inputs[1].get(); + var headObject: Object = inputs[2].get(); + var sensitivity: FastFloat = inputs[3].get(); + var smoothing: FastFloat = inputs[4].get(); + + if (bodyObject == null) { + runOutput(0); + return; + } + + 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; + + // 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 + } + + // 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; + 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) + #if lnx_yaxisup + horizontalAxis.set(0, 0, 1); // Z axis for horizontal (yaw) + verticalAxis.set(1, 0, 0); // X axis for vertical (pitch) + #else + horizontalAxis.set(0, 0, 1); // Z axis for horizontal (yaw) + verticalAxis.set(1, 0, 0); // X axis for vertical (pitch) + #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) + } + + // Base scaling + var baseScale: FastFloat = 1500.0; + var finalScale = baseScale; + + // 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; + + // 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 + currentHorizontal += horizontalRotation; + if (currentHorizontal > maxHorizontal) { + horizontalRotation -= (currentHorizontal - maxHorizontal); + currentHorizontal = maxHorizontal; + } else if (currentHorizontal < -maxHorizontal) { + horizontalRotation -= (currentHorizontal + maxHorizontal); + currentHorizontal = -maxHorizontal; + } + } + + if (property5) { // Cap Up/Down + currentVertical += verticalRotation; + if (currentVertical > maxVertical) { + verticalRotation -= (currentVertical - maxVertical); + currentVertical = maxVertical; + } else if (currentVertical < -maxVertical) { + verticalRotation -= (currentVertical + maxVertical); + currentVertical = -maxVertical; + } + } + + // 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 + + // 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 + + // 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 + } + } + + 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 new file mode 100644 index 0000000..d33d111 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/input/LN_mouse_look.py @@ -0,0 +1,101 @@ +from lnx.logicnode.lnx_nodes import * + + +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 + """ + bl_idname = 'LNMouseLookNode' + bl_label = 'Mouse Look' + lnx_section = 'mouse' + lnx_version = 1 + + # Front axis property + 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') + + # Hide Locked property + property1: HaxeBoolProperty( + 'property1', + name='Hide Locked', + description='Automatically center and lock the mouse cursor', + default=True) + + # Invert X property + property2: HaxeBoolProperty( + 'property2', + name='Invert X', + description='Invert horizontal mouse movement', + default=False) + + # Invert Y property + property3: HaxeBoolProperty( + 'property3', + name='Invert Y', + description='Invert vertical mouse movement', + default=False) + + # Cap Left/Right property + property4: HaxeBoolProperty( + 'property4', + name='Cap Left / Right', + description='Limit horizontal rotation', + default=False) + + # Cap Up/Down property + property5: HaxeBoolProperty( + 'property5', + 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', + default=False) + + + + + + def lnx_init(self, context): + 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('LnxFloatSocket', 'Smoothing', default_value=0.0) + + self.add_output('LnxNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0', text='Front') + layout.prop(self, 'property1', text='Hide Locked') + + # 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) + + # Cap rotations section + col = layout.column(align=True) + col.prop(self, 'property4', text='Cap Left / Right') + col.prop(self, 'property5', text='Cap Up / Down') + + # Separator + layout.separator() + + # Enhancement strategies section + col = layout.column(align=True) + col.label(text="Enhancement Strategies:") + col.prop(self, 'property6', text='Resolution Adaptive') \ No newline at end of file