From 647b73b746501bfb0affc4638f6d46f48bf53d9c Mon Sep 17 00:00:00 2001 From: wuaieyo Date: Mon, 30 Jun 2025 06:35:06 +0200 Subject: [PATCH] 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