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