add new Set Look At Rotation Node for making logic nodes great again

This commit is contained in:
2025-06-24 19:47:03 +02:00
parent 06b003ecdb
commit 4055c979a1
3 changed files with 557 additions and 0 deletions

0
Krom/Krom Normal file → Executable file
View File

View File

@ -0,0 +1,190 @@
package leenkx.logicnode;
import iron.math.Vec4;
import iron.math.Quat;
import iron.math.Mat4;
import iron.object.Object;
class SetLookAtRotationNode extends LogicNode {
public var property0: String; // Axis to align
public var property1: String; // Use vector for target (true/false)
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)
// Store the calculated rotation for output
var calculatedRotation: Quat = null;
// Store the previous rotation for smooth interpolation
var previousRotation: Quat = null;
public function new(tree: LogicTree) {
super(tree);
previousRotation = new Quat();
}
override function run(from: Int): Void {
// Determine if we're using a vector or an object as source
var useSourceVector: Bool = property2 == "true";
var objectToUse: Object = null;
var objectLoc: Vec4 = null;
if (useSourceVector) {
// Use tree.object as the object to rotate
objectToUse = tree.object;
if (objectToUse == null) {
runOutput(0);
return;
}
// Get the source location directly
objectLoc = inputs[1].get();
if (objectLoc == null) {
runOutput(0);
return;
}
} else {
// Get the source object (or fallback to tree.object)
objectToUse = (inputs.length > 1 && inputs[1] != null) ? inputs[1].get() : tree.object;
if (objectToUse == null) {
runOutput(0);
return;
}
// Get source object's position
objectLoc = objectToUse.transform.loc;
}
// Determine if we're using a vector or an object as target
var useTargetVector: Bool = property1 == "true";
var targetLoc: Vec4 = null;
if (useTargetVector) {
// Get the target location directly
targetLoc = inputs[2].get();
if (targetLoc == null) {
runOutput(0);
return;
}
} else {
// Get the target object
var targetObject: Object = inputs[2].get();
if (targetObject == null) {
runOutput(0);
return;
}
// Get target object's position
targetLoc = targetObject.transform.loc;
}
// Calculate direction to target
var direction = new Vec4(
targetLoc.x - objectLoc.x,
targetLoc.y - objectLoc.y,
targetLoc.z - objectLoc.z
);
direction.normalize();
// Calculate target rotation based on selected axis
calculatedRotation = new Quat();
switch (property0) {
case "X":
calculatedRotation.fromTo(new Vec4(1, 0, 0), direction);
case "-X":
calculatedRotation.fromTo(new Vec4(-1, 0, 0), direction);
case "Y":
calculatedRotation.fromTo(new Vec4(0, 1, 0), direction);
case "-Y":
calculatedRotation.fromTo(new Vec4(0, -1, 0), direction);
case "Z":
calculatedRotation.fromTo(new Vec4(0, 0, 1), direction);
case "-Z":
calculatedRotation.fromTo(new Vec4(0, 0, -1), direction);
}
// If disable rotation on aligning axis is enabled, constrain the target rotation
if (property4 == "true") {
// Apply constraint to the target rotation BEFORE damping to avoid jiggling
var eulerAngles = calculatedRotation.toEulerOrdered("XYZ");
// Set the rotation around the selected axis to 0
switch (property0) {
case "X", "-X":
eulerAngles.x = 0.0;
case "Y", "-Y":
eulerAngles.y = 0.0;
case "Z", "-Z":
eulerAngles.z = 0.0;
}
// Convert back to quaternion
calculatedRotation.fromEulerOrdered(eulerAngles, "XYZ");
}
// Apply rotation with damping
var dampingValue: Float = 0.0;
// Try to get damping from input socket first (index 3), fallback to property
if (inputs.length > 3 && inputs[3] != null) {
var dampingInput: Dynamic = inputs[3].get();
if (dampingInput != null) {
dampingValue = dampingInput;
}
} else {
// Fallback to property for backward compatibility
dampingValue = Std.parseFloat(property3);
}
if (dampingValue > 0.0) {
// Create a fixed interpolation rate that never reaches exactly 1.0
// 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);
// 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);
invCurrent.x = -invCurrent.x;
invCurrent.y = -invCurrent.y;
invCurrent.z = -invCurrent.z;
diffQuat.multquats(calculatedRotation, invCurrent);
// Convert to axis-angle representation
var axis = new Vec4();
var angle = diffQuat.toAxisAngle(axis);
// Apply only a portion of this rotation (step)
var partialAngle = angle * step;
// 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 the new rotation
objectToUse.transform.rot.setFrom(newRot);
} else {
// No damping, apply instant rotation
objectToUse.transform.rot.setFrom(calculatedRotation);
}
objectToUse.transform.buildMatrix();
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;
}
}

View File

@ -0,0 +1,367 @@
from lnx.logicnode.lnx_nodes import *
import bpy
class SetLookAtRotationNode(LnxLogicTreeNode):
"""Returns a rotation that makes an object look at a target object or location"""
bl_idname = 'LNSetLookAtRotationNode'
bl_label = 'Set Look At Rotation'
lnx_section = 'rotation'
lnx_version = 1
use_vector: bpy.props.BoolProperty(
name='Use Vector for Target Location',
description='Use a vector location instead of a target object',
default=False,
update=lambda self, context: self.update_sockets(context)
)
use_source_vector: bpy.props.BoolProperty(
name='Use Vector for Source',
description='Use a vector location instead of a source object',
default=False,
update=lambda self, context: self.update_sockets(context)
)
disable_rotation_on_align_axis: bpy.props.BoolProperty(
name='Disable Rotation on Aligning Axis',
description='Zero out the rotation on the aligning axis after look at is applied',
default=False,
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)',
default=0.0,
min=0.0,
max=1.0,
step=10,
precision=2,
update=lambda self, context: self.update_damping_socket(context)
)
# Store object references as custom properties
source_object_name: bpy.props.StringProperty(default="")
target_object_name: bpy.props.StringProperty(default="")
property0: HaxeEnumProperty(
'property0',
items = [('X', ' X', 'X'),
('-X', '-X', '-X'),
('Y', ' Y', 'Y'),
('-Y', '-Y', '-Y'),
('Z', ' Z', 'Z'),
('-Z', '-Z', '-Z')],
name='With', default='Z')
property1: HaxeEnumProperty(
'property1',
items = [('true', 'True', 'True'),
('false', 'False', 'False')],
name='Use Vector for Target Location', default='false')
property2: HaxeEnumProperty(
'property2',
items = [('true', 'True', 'True'),
('false', 'False', 'False')],
name='Use Vector for Source', default='false')
property3: bpy.props.StringProperty(name='Damping', default='0.0')
property4: HaxeEnumProperty(
'property4',
items = [('true', 'True', 'True'),
('false', 'False', 'False')],
name='Disable Rotation on Aligning Axis', default='false')
def lnx_init(self, context):
# Add inputs in standard order
self.inputs.new('LnxNodeSocketAction', 'In')
# Add the initial source input
self.inputs.new('LnxNodeSocketObject', 'Source Object')
# Add the initial target input
self.inputs.new('LnxNodeSocketObject', 'Target Object')
# Add damping input socket with default value
damping_socket = self.inputs.new('LnxFloatSocket', 'Damping')
damping_socket.default_value_raw = 0.0
# 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
layout.prop(self, 'property0')
# 2. 'Use Vector for Source' checkbox
layout.prop(self, 'use_source_vector')
# 3. 'Use Vector for Target Location' checkbox
layout.prop(self, 'use_vector')
# 4. 'Disable Rotation on Aligning Axis' checkbox
layout.prop(self, 'disable_rotation_on_align_axis')
# Note: Damping is now handled by the input socket
def update_sockets(self, context):
# Update the Haxe properties to match the Python properties
self.property1 = 'true' if self.use_vector else 'false'
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'
# Store current object references before changing sockets
self.save_object_references()
# Helper to find a socket by name
def find_input_socket(name):
for i, s in enumerate(self.inputs):
if s.name == name:
return i, s
return -1, None
# Ensure we have the 'In' socket at index 0
in_idx, in_socket = find_input_socket('In')
if in_idx == -1:
# If 'In' socket is missing, add it at index 0
self.inputs.new('LnxNodeSocketAction', 'In')
# Fixed indices for our sockets
SOURCE_INDEX = 1
TARGET_INDEX = 2
DAMPING_INDEX = 3
# Get current socket information
source_names = ['Source Object', 'Source Location']
target_names = ['Target Object', 'Target Location']
# Find current source and target sockets
source_idx = -1
source_socket = None
for name in source_names:
idx, socket = find_input_socket(name)
if idx != -1:
source_idx = idx
source_socket = socket
break
target_idx = -1
target_socket = None
for name in target_names:
idx, socket = find_input_socket(name)
if idx != -1:
target_idx = idx
target_socket = socket
break
# Expected types based on current settings
expected_source_name = 'Source Location' if self.use_source_vector else 'Source Object'
expected_source_type = 'LnxVectorSocket' if self.use_source_vector else 'LnxNodeSocketObject'
expected_target_name = 'Target Location' if self.use_vector else 'Target Object'
expected_target_type = 'LnxVectorSocket' if self.use_vector else 'LnxNodeSocketObject'
# Ensure we have exactly 4 sockets (In, Source, Target, Damping) in the correct order
while len(self.inputs) > 4:
# Remove any extra sockets
self.inputs.remove(self.inputs[-1])
# Make sure we have exactly 4 sockets
while len(self.inputs) < 4:
if len(self.inputs) == 0:
self.inputs.new('LnxNodeSocketAction', 'In')
elif len(self.inputs) == 1:
self.inputs.new(expected_source_type, expected_source_name)
elif len(self.inputs) == 2:
self.inputs.new(expected_target_type, expected_target_name)
elif len(self.inputs) == 3:
damping_socket = self.inputs.new('LnxFloatSocket', 'Damping')
damping_socket.default_value_raw = self.damping
# Now update the source socket if needed
if source_socket and source_socket.name != expected_source_name:
# Store links before removing
links = []
for link in source_socket.links:
links.append(link.from_socket)
# Get the index where this socket should be
correct_idx = SOURCE_INDEX
# Create the new socket at the correct position
self.inputs.remove(source_socket)
new_socket = self.inputs.new(expected_source_type, expected_source_name)
# Move the new socket to the correct position
if not new_socket.is_linked: # Only move if not linked
# Move the socket to the correct index
if correct_idx < len(self.inputs) - 1:
self.inputs.move(len(self.inputs) - 1, correct_idx)
# Restore links
if links and hasattr(context, 'space_data') and context.space_data and hasattr(context.space_data, 'edit_tree'):
for link_socket in links:
context.space_data.edit_tree.links.new(link_socket, new_socket)
# Update the target socket if needed
if target_socket and target_socket.name != expected_target_name:
# Store links before removing
links = []
for link in target_socket.links:
links.append(link.from_socket)
# Get the index where this socket should be
correct_idx = TARGET_INDEX
# Create the new socket at the correct position
self.inputs.remove(target_socket)
new_socket = self.inputs.new(expected_target_type, expected_target_name)
# Move the new socket to the correct position
if not new_socket.is_linked: # Only move if not linked
# Move the socket to the correct index
if correct_idx < len(self.inputs) - 1:
self.inputs.move(len(self.inputs) - 1, correct_idx)
# Restore links
if links and hasattr(context, 'space_data') and context.space_data and hasattr(context.space_data, 'edit_tree'):
for link_socket in links:
context.space_data.edit_tree.links.new(link_socket, new_socket)
# Make a final check to ensure the sockets are in the correct order
# This is a safety measure to ensure consistent UI
in_idx, in_socket = find_input_socket('In')
source_idx = -1
for name in source_names:
idx, _ = find_input_socket(name)
if idx != -1:
source_idx = idx
break
target_idx = -1
for name in target_names:
idx, _ = find_input_socket(name)
if idx != -1:
target_idx = idx
break
damping_idx, damping_socket = find_input_socket('Damping')
# If the order is wrong, fix it by recreating the sockets in the correct order
if not (in_idx == 0 and source_idx == 1 and target_idx == 2 and damping_idx == 3):
# Store all links
all_links = {}
for i, socket in enumerate(self.inputs):
# Store links
links = []
for link in socket.links:
links.append(link.from_socket)
all_links[socket.name] = links
# Clear all inputs
while len(self.inputs) > 0:
self.inputs.remove(self.inputs[0])
# Recreate in the correct order
in_socket = self.inputs.new('LnxNodeSocketAction', 'In')
source_socket = self.inputs.new(expected_source_type, expected_source_name)
target_socket = self.inputs.new(expected_target_type, expected_target_name)
damping_socket = self.inputs.new('LnxFloatSocket', 'Damping')
damping_socket.default_value_raw = self.damping
# Restore links
if hasattr(context, 'space_data') and context.space_data and hasattr(context.space_data, 'edit_tree'):
# Restore In links
if 'In' in all_links:
for link_socket in all_links['In']:
context.space_data.edit_tree.links.new(link_socket, in_socket)
# Restore Source links
source_links = []
for name in source_names:
if name in all_links:
source_links.extend(all_links[name])
for link_socket in source_links:
context.space_data.edit_tree.links.new(link_socket, source_socket)
# Restore Target links
target_links = []
for name in target_names:
if name in all_links:
target_links.extend(all_links[name])
for link_socket in target_links:
context.space_data.edit_tree.links.new(link_socket, target_socket)
# Restore Damping links
if 'Damping' in all_links:
for link_socket in all_links['Damping']:
context.space_data.edit_tree.links.new(link_socket, damping_socket)
# Restore object references after socket changes
self.restore_object_references()
def update_damping_socket(self, context):
"""Update the damping socket default value when the slider changes"""
for socket in self.inputs:
if socket.name == 'Damping' and hasattr(socket, 'default_value_raw'):
socket.default_value_raw = self.damping
break
def save_object_references(self):
"""Save object references to custom properties"""
# Find source and target object sockets
for socket in self.inputs:
if socket.name == 'Source Object' and socket.is_linked:
for link in socket.links:
if hasattr(link.from_node, 'item') and link.from_node.item:
self.source_object_name = link.from_node.item.name
if socket.name == 'Target Object' and socket.is_linked:
for link in socket.links:
if hasattr(link.from_node, 'item') and link.from_node.item:
self.target_object_name = link.from_node.item.name
def restore_object_references(self):
"""Restore object references from custom properties"""
# Only restore if we're not using vector inputs
if not self.use_source_vector:
# Find source object socket
for socket in self.inputs:
if socket.name == 'Source Object' and not socket.is_linked:
# Try to find the object in the scene
if self.source_object_name and self.source_object_name in bpy.context.scene.objects:
# Find the appropriate object node in the node tree
if hasattr(self, 'id_data') and hasattr(self.id_data, 'nodes'):
for node in self.id_data.nodes:
if (node.bl_idname == 'LNObjectNode' and
hasattr(node, 'item') and
node.item and
node.item.name == self.source_object_name):
# Create a link between the nodes
if hasattr(self.id_data, 'links'):
self.id_data.links.new(node.outputs[0], socket)
break
if not self.use_vector:
# Find target object socket
for socket in self.inputs:
if socket.name == 'Target Object' and not socket.is_linked:
# Try to find the object in the scene
if self.target_object_name and self.target_object_name in bpy.context.scene.objects:
# Find the appropriate object node in the node tree
if hasattr(self, 'id_data') and hasattr(self.id_data, 'nodes'):
for node in self.id_data.nodes:
if (node.bl_idname == 'LNObjectNode' and
hasattr(node, 'item') and
node.item and
node.item.name == self.target_object_name):
# Create a link between the nodes
if hasattr(self.id_data, 'links'):
self.id_data.links.new(node.outputs[0], socket)
break