merge upstream

This commit is contained in:
2025-07-16 05:57:15 +00:00
11 changed files with 669 additions and 216 deletions

View File

@ -6,172 +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 {
// 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
// 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
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
// 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
// New strategy toggles
public var property6: Bool; // Resolution-Adaptive Scaling
// Smoothing variables
var smoothX: FastFloat = 0.0;
var smoothY: FastFloat = 0.0;
// 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)
// Capping limits (in degrees)
var maxHorizontal: FastFloat = 180.0; // 180 degrees
var maxVertical: FastFloat = 90.0; // 90 degrees
// Current rotation tracking for capping calculations
var currentHorizontal: Float = 0.0; // Accumulated horizontal rotation
var currentVertical: Float = 0.0; // Accumulated vertical rotation
// Current accumulated rotations for capping
var currentHorizontal: FastFloat = 0.0;
var currentVertical: FastFloat = 0.0;
// Resolution scaling reference - base resolution for consistent sensitivity
var baseResolutionWidth: Float = 1920.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;
// 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 mouse centering/locking
// 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 if mouse is active
// 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 mouse movement deltas
var deltaX: FastFloat = mouse.movementX;
var deltaY: FastFloat = mouse.movementY;
// Get raw mouse movement delta (pixels moved since last frame)
var deltaX: Float = mouse.movementX;
var deltaY: Float = mouse.movementY;
// Note: Sensitivity will be applied later to preserve precision for small movements
// Apply axis inversion if configured
if (property2) deltaX = -deltaX; // Invert horizontal movement
if (property3) deltaY = -deltaY; // Invert vertical movement
// Apply inversion
if (property2) deltaX = -deltaX;
if (property3) deltaY = -deltaY;
// 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;
// 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
// 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 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); // Cap smoothing to prevent complete freeze
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();
// 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": // 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": // 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); // Z axis for horizontal (yaw)
verticalAxis.set(1, 0, 0); // X axis for vertical (pitch)
// 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); // Z axis for horizontal (yaw)
verticalAxis.set(1, 0, 0); // X axis for vertical (pitch)
// 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": // 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": // 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
}
// Base scaling
var baseScale: FastFloat = 1500.0;
var finalScale = baseScale;
// Calculate final sensitivity scaling combining base scale and resolution adaptation
var finalScale: Float = BASE_SCALE * resolutionMultiplier;
// Apply resolution scaling
if (property6) {
finalScale *= resolutionMultiplier;
}
// Apply sensitivity scaling after all enhancement strategies to preserve precision
// Apply user-defined sensitivity multiplier
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;
// 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;
// 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
// 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;
@ -181,8 +171,11 @@ class MouseLookNode extends LogicNode {
}
}
if (property5) { // Cap Up/Down
// 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;
@ -192,41 +185,49 @@ 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
// Apply horizontal rotation to body object (character turning left/right)
if (horizontalRotation != 0.0) {
bodyObject.transform.rotate(horizontalAxis, horizontalRotation);
// Sync physics if needed
// 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 (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
// Apply vertical rotation to head object (camera looking up/down)
if (headObject != null && verticalRotation != 0.0) {
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 - 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);
}
// Sync physics if needed
// 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) {
// 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) {
// 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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -36,6 +36,18 @@ class RigidBody extends iron.Trait {
var useDeactivation: Bool;
var deactivationParams: Array<Float>;
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 = {