Merge pull request 'added Mouse Look node for FSP style movement of object like camera...' (#89) from wuaieyo/LNXSDK:main into main
Reviewed-on: #89
This commit is contained in:
		
							
								
								
									
										232
									
								
								leenkx/Sources/leenkx/logicnode/MouseLookNode.hx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								leenkx/Sources/leenkx/logicnode/MouseLookNode.hx
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
| 	} | ||||
| }  | ||||
							
								
								
									
										101
									
								
								leenkx/blender/lnx/logicnode/input/LN_mouse_look.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								leenkx/blender/lnx/logicnode/input/LN_mouse_look.py
									
									
									
									
									
										Normal file
									
								
							| @ -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')  | ||||
		Reference in New Issue
	
	Block a user