From 17daabeb29dfb0a5cc8b4bef840b02b9f83f409d Mon Sep 17 00:00:00 2001 From: niacdoial Date: Sun, 30 Aug 2020 11:45:07 +0200 Subject: [PATCH] Improved quaternion and angle handling in logic nodes (+2bugfixes) - Made so all nodes outputting a quaternion object also output it as a XYZ (vector) + W (float) combination - Modernized the interface of the "Action/Rotate Object" node, to align on the newer "Action/Set Rotation" node interface ("Action/Rotate Object Along Axis" is now depreciated, but still usable) - Fixed a blender-side-only bug with the "Logic/Switch" node (...which technically could have lead to a compile-time problem if exploited the right way) - Fixed a bug on the "Action/Set Rotation" node: now, quaternion input is automatically normalized in order to avoid accidental scaling - Added a "Value/Separate Quaternion" node - Made so the names of some sockets change in the "Set Rotation" and "Rotate Object" nodes, so they adapt to those nodes' input types. (Same thing with "Value/Vector From Transform"'s output type) --- Sources/armory/logicnode/QuaternionNode.hx | 18 +++++++- Sources/armory/logicnode/RotateObjectNode.hx | 29 ++++++++++++- .../logicnode/SeparateQuaternionNode.hx | 23 ++++++++++ Sources/armory/logicnode/SetRotationNode.hx | 1 + .../logicnode/VectorFromTransformNode.hx | 42 ++++++++++++++----- blender/arm/logicnode/action_rotate_object.py | 35 +++++++++++++++- .../action_rotate_object_around_axis.py | 7 +++- blender/arm/logicnode/action_set_rotation.py | 29 +++++++++---- blender/arm/logicnode/logic_switch.py | 2 +- .../logicnode/value_separate_quaternion.py | 21 ++++++++++ .../logicnode/value_vector_from_transform.py | 34 +++++++++++---- blender/arm/logicnode/variable_quaternion.py | 2 + 12 files changed, 212 insertions(+), 31 deletions(-) create mode 100644 Sources/armory/logicnode/SeparateQuaternionNode.hx create mode 100644 blender/arm/logicnode/value_separate_quaternion.py diff --git a/Sources/armory/logicnode/QuaternionNode.hx b/Sources/armory/logicnode/QuaternionNode.hx index d92c4bfe..95880f4f 100644 --- a/Sources/armory/logicnode/QuaternionNode.hx +++ b/Sources/armory/logicnode/QuaternionNode.hx @@ -1,6 +1,7 @@ package armory.logicnode; import iron.math.Quat; +import iron.math.Vec4; class QuaternionNode extends LogicNode { @@ -22,7 +23,22 @@ class QuaternionNode extends LogicNode { value.y = inputs[1].get(); value.z = inputs[2].get(); value.w = inputs[3].get(); - return value; + value.normalize(); + switch (from){ + case 0: + return value; + case 1: + var value1 = new Vec4(); + value1.x = value.x; + value1.y = value.y; + value1.z = value.z; + value1.w = 0; // use 0 to avoid this vector being translated. + return value1; + case 2: + return value.w; + default: + return null; + } } override function set(value: Dynamic) { diff --git a/Sources/armory/logicnode/RotateObjectNode.hx b/Sources/armory/logicnode/RotateObjectNode.hx index 8525ac0c..ce3deb22 100644 --- a/Sources/armory/logicnode/RotateObjectNode.hx +++ b/Sources/armory/logicnode/RotateObjectNode.hx @@ -7,6 +7,7 @@ import armory.trait.physics.RigidBody; class RotateObjectNode extends LogicNode { + public var property0 = "uninitialized"; var q = new Quat(); public function new(tree: LogicTree) { @@ -16,10 +17,34 @@ class RotateObjectNode extends LogicNode { override function run(from: Int) { var object: Object = inputs[1].get(); var vec: Vec4 = inputs[2].get(); - + var w: Float = 0; + + // the next if/else block exist to ensure backwards compatibility with nodes that were created before armory 2020.09. + // delete it when the "old version" of this node will be considered removed from armory. + if (property0=="uninitialized") { + property0="Euler Angles"; + } + else{ + w = inputs[3].get(); + } if (object == null || vec == null) return; - q.fromEuler(vec.x, vec.y, vec.z); + switch (property0) { + case "Euler Angles": + q.fromEuler(vec.x, vec.y, vec.z); + case "Angle Axies (Degrees)" | "Angle Axies (Radians)": + var angle: Float = w; + if (property0 == "Angle Axies (Degrees)") { + angle = angle * (Math.PI / 180); + } + var angleSin = Math.sin(angle / 2); + vec = vec.normalize(); + var angleCos = Math.cos(angle / 2); + q = new Quat(vec.x * angleSin, vec.y * angleSin, vec.z * angleSin, angleCos); + case "Quaternion": + q = new Quat(vec.x, vec.y, vec.z, w); + q.normalize(); + } object.transform.rot.mult(q); object.transform.buildMatrix(); diff --git a/Sources/armory/logicnode/SeparateQuaternionNode.hx b/Sources/armory/logicnode/SeparateQuaternionNode.hx new file mode 100644 index 00000000..fac7b69e --- /dev/null +++ b/Sources/armory/logicnode/SeparateQuaternionNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.math.Quat; +import kha.FastFloat; + +class SeparateQuaternionNode extends LogicNode { + var q:Quat = null; + + public function new(tree:LogicTree) { super(tree); } + + override function get(from:Int):Dynamic{ + q = inputs[0].get(); + if (from==0) + return q.x; + else if (from==1) + return q.y; + else if (from==2) + return q.z; + else + return q.w; + + } +} diff --git a/Sources/armory/logicnode/SetRotationNode.hx b/Sources/armory/logicnode/SetRotationNode.hx index 6665fe61..a5d85475 100644 --- a/Sources/armory/logicnode/SetRotationNode.hx +++ b/Sources/armory/logicnode/SetRotationNode.hx @@ -34,6 +34,7 @@ class SetRotationNode extends LogicNode { object.transform.rot = new Quat(vec.x * angleSin, vec.y * angleSin, vec.z * angleSin, angleCos); case "Quaternion": object.transform.rot = new Quat(vec.x, vec.y, vec.z, w); + object.transform.rot.normalize(); } object.transform.buildMatrix(); #if arm_physics diff --git a/Sources/armory/logicnode/VectorFromTransformNode.hx b/Sources/armory/logicnode/VectorFromTransformNode.hx index 939f05af..da0d3271 100644 --- a/Sources/armory/logicnode/VectorFromTransformNode.hx +++ b/Sources/armory/logicnode/VectorFromTransformNode.hx @@ -2,6 +2,7 @@ package armory.logicnode; import iron.math.Quat; import iron.math.Mat4; +import iron.math.Vec4; class VectorFromTransformNode extends LogicNode { @@ -16,16 +17,37 @@ class VectorFromTransformNode extends LogicNode { if (m == null) return null; - switch (property0) { - case "Up": - return m.up(); - case "Right": - return m.right(); - case "Look": - return m.look(); - case "Quaternion": - var q = new Quat(); - return q.fromMat(m); + switch(from) { + case 0: + switch (property0) { + case "Up": + return m.up(); + case "Right": + return m.right(); + case "Look": + return m.look(); + case "Quaternion": + var q = new Quat(); + q.fromMat(m); + return q.normalize(); + } + case 1: + if (property0 == "Quaternion") { + var q = new Quat(); + q.fromMat(m); + q.normalize(); + var v = new Vec4(); + v.x = q.x; v.y = q.y; v.z = q.z; + v.w = 0; //prevent vector translation + return v; + } + case 2: + if (property0 == "Quaternion") { + var q = new Quat(); + q.fromMat(m); + q.normalize(); + return q.w; + } } return null; diff --git a/blender/arm/logicnode/action_rotate_object.py b/blender/arm/logicnode/action_rotate_object.py index 6cf3dd74..7f5de209 100644 --- a/blender/arm/logicnode/action_rotate_object.py +++ b/blender/arm/logicnode/action_rotate_object.py @@ -12,7 +12,40 @@ class RotateObjectNode(Node, ArmLogicTreeNode): def init(self, context): self.inputs.new('ArmNodeSocketAction', 'In') self.inputs.new('ArmNodeSocketObject', 'Object') - self.inputs.new('NodeSocketVector', 'Vector') + self.inputs.new('NodeSocketVector', 'Euler Angles') + self.inputs.new('NodeSocketFloat', 'Angle / W') self.outputs.new('ArmNodeSocketAction', 'Out') + + def on_property_update(self, context): + """ called by the EnumProperty, used to update the node socket labels""" + if self.property0 == "Quaternion": + self.inputs[2].name = "Quaternion XYZ" + self.inputs[3].name = "Quaternion W" + elif self.property0 == "Euler Angles": + self.inputs[2].name = "Euler Angles" + self.inputs[3].name = "[unused for Euler input]" + elif self.property0.startswith("Angle Axies"): + self.inputs[2].name = "Axis" + self.inputs[3].name = "Angle" + else: + raise ValueError('No nodesocket labels for current input mode: check self-consistancy of action_set_rotation.py') + + def draw_buttons(self, context, layout): + # this block is here to ensure backwards compatibility and warn the user. + # delete it (only keep the "else" part) when the 'old version' of the node will be considered removed. + # (note: please also update the corresponding haxe file when doing so) + if len(self.inputs) < 4: + row = layout.row(align=True) + row.label(text="Node has been updated with armory 2020.09. Please consider deleting and recreating it.") + else: + layout.prop(self, 'property0') + + property0: EnumProperty( + items = [('Euler Angles', 'Euler Angles', 'Euler Angles'), + ('Angle Axies (Radians)', 'Angle Axies (Radians)', 'Angle Axies (Radians)'), + ('Angle Axies (Degrees)', 'Angle Axies (Degrees)', 'Angle Axies (Degrees)'), + ('Quaternion', 'Quaternion', 'Quaternion')], + name='', default='Euler Angles', + update = on_property_update) add_node(RotateObjectNode, category='Action') diff --git a/blender/arm/logicnode/action_rotate_object_around_axis.py b/blender/arm/logicnode/action_rotate_object_around_axis.py index 0020f854..ed14e795 100644 --- a/blender/arm/logicnode/action_rotate_object_around_axis.py +++ b/blender/arm/logicnode/action_rotate_object_around_axis.py @@ -7,7 +7,8 @@ class RotateObjectAroundAxisNode(Node, ArmLogicTreeNode): '''Rotate object around axis node''' bl_idname = 'LNRotateObjectAroundAxisNode' bl_label = 'Rotate Object Around Axis' - bl_icon = 'NONE' + bl_description = 'Rotate Object Around Axis (Depreciated: use "Rotate Object")' + bl_icon = 'ERROR' def init(self, context): self.inputs.new('ArmNodeSocketAction', 'In') @@ -16,5 +17,9 @@ class RotateObjectAroundAxisNode(Node, ArmLogicTreeNode): self.inputs[-1].default_value = [0, 0, 1] self.inputs.new('NodeSocketFloat', 'Angle') self.outputs.new('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + row.label(text='Depreciated. Consider using "Rotate Object"') add_node(RotateObjectAroundAxisNode, category='Action') diff --git a/blender/arm/logicnode/action_set_rotation.py b/blender/arm/logicnode/action_set_rotation.py index 0d56dca2..dd1cd6f6 100644 --- a/blender/arm/logicnode/action_set_rotation.py +++ b/blender/arm/logicnode/action_set_rotation.py @@ -9,21 +9,36 @@ class SetRotationNode(Node, ArmLogicTreeNode): bl_label = 'Set Rotation' bl_icon = 'NONE' - property0: EnumProperty( - items = [('Euler Angles', 'Euler Angles', 'Euler Angles'), - ('Angle Axies (Radians)', 'Angle Axies (Radians)', 'Angle Axies (Radians)'), - ('Angle Axies (Degrees)', 'Angle Axies (Degrees)', 'Angle Axies (Degrees)'), - ('Quaternion', 'Quaternion', 'Quaternion')], - name='', default='Euler Angles') - def init(self, context): self.inputs.new('ArmNodeSocketAction', 'In') self.inputs.new('ArmNodeSocketObject', 'Object') self.inputs.new('NodeSocketVector', 'Euler Angles / Vector XYZ') self.inputs.new('NodeSocketFloat', 'Angle / W') self.outputs.new('ArmNodeSocketAction', 'Out') + + def on_property_update(self, context): + """called by the EnumProperty, used to update the node socket labels""" + if self.property0 == "Quaternion": + self.inputs[2].name = "Quaternion XYZ" + self.inputs[3].name = "Quaternion W" + elif self.property0 == "Euler Angles": + self.inputs[2].name = "Euler Angles" + self.inputs[3].name = "[unused for Euler input]" + elif self.property0.startswith("Angle Axies"): + self.inputs[2].name = "Axis" + self.inputs[3].name = "Angle" + else: + raise ValueError('No nodesocket labels for current input mode: check self-consistancy of action_set_rotation.py') def draw_buttons(self, context, layout): layout.prop(self, 'property0') + + property0: EnumProperty( + items = [('Euler Angles', 'Euler Angles', 'Euler Angles'), + ('Angle Axies (Radians)', 'Angle Axies (Radians)', 'Angle Axies (Radians)'), + ('Angle Axies (Degrees)', 'Angle Axies (Degrees)', 'Angle Axies (Degrees)'), + ('Quaternion', 'Quaternion', 'Quaternion')], + name='', default='Euler Angles', + update=on_property_update) add_node(SetRotationNode, category='Action') diff --git a/blender/arm/logicnode/logic_switch.py b/blender/arm/logicnode/logic_switch.py index b52f6889..10a3ff1a 100644 --- a/blender/arm/logicnode/logic_switch.py +++ b/blender/arm/logicnode/logic_switch.py @@ -8,7 +8,7 @@ class SwitchNode(Node, ArmLogicTreeNode): bl_idname = 'LNSwitchNode' bl_label = 'Switch' bl_icon = 'NONE' - min_inputs = 1 + min_inputs = 2 min_outputs = 1 def __init__(self): diff --git a/blender/arm/logicnode/value_separate_quaternion.py b/blender/arm/logicnode/value_separate_quaternion.py new file mode 100644 index 00000000..dd5bb4fd --- /dev/null +++ b/blender/arm/logicnode/value_separate_quaternion.py @@ -0,0 +1,21 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + + +class SeparateQuaternionNode(Node, ArmLogicTreeNode): + + bl_idname = 'LNSeparateQuaternionNode' + bl_label = "Separate Quaternion" + bl_icon = 'NONE' + + def init(self, context): + self.inputs.new('NodeSocketVector', 'Quaternion') + self.outputs.new('NodeSocketFloat', 'X') + self.outputs.new('NodeSocketFloat', 'Y') + self.outputs.new('NodeSocketFloat', 'Z') + self.outputs.new('NodeSocketFloat', 'W') + + +add_node(SeparateQuaternionNode, 'Value') diff --git a/blender/arm/logicnode/value_vector_from_transform.py b/blender/arm/logicnode/value_vector_from_transform.py index cb8ce0f6..6178349d 100644 --- a/blender/arm/logicnode/value_vector_from_transform.py +++ b/blender/arm/logicnode/value_vector_from_transform.py @@ -8,18 +8,36 @@ class VectorFromTransformNode(Node, ArmLogicTreeNode): bl_idname = 'LNVectorFromTransformNode' bl_label = 'Vector From Transform' bl_icon = 'NONE' + + def init(self, context): + self.inputs.new('NodeSocketShader', 'Transform') + self.outputs.new('NodeSocketVector', 'Vector') + self.outputs.new('NodeSocketVector', 'Quaternion XYZ') + self.outputs.new('NodeSocketFloat', 'Quaternion W') + + def on_property_update(self, context): + """called by the EnumProperty, used to update the node socket labels""" + # note: the conditions on len(self.outputs) are take in account "old version" (pre-2020.9) nodes, which only have one output + if self.property0 == "Quaternion": + self.outputs[0].name = "Quaternion" + if len(self.outputs) > 1: + self.outputs[1].name = "Quaternion XYZ" + self.outputs[2].name = "Quaternion W" + else: + self.outputs[0].name = "Vector" + if len(self.outputs) > 1: + self.outputs[1].name = "[quaternion only]" + self.outputs[2].name = "[quaternion only]" + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + property0: EnumProperty( items = [('Up', 'Up', 'Up'), ('Right', 'Right', 'Right'), ('Look', 'Look', 'Look'), ('Quaternion', 'Quaternion', 'Quaternion')], - name='', default='Look') - - def init(self, context): - self.inputs.new('NodeSocketShader', 'Transform') - self.outputs.new('NodeSocketVector', 'Vector') - - def draw_buttons(self, context, layout): - layout.prop(self, 'property0') + name='', default='Look', + update=on_property_update) add_node(VectorFromTransformNode, category='Value') diff --git a/blender/arm/logicnode/variable_quaternion.py b/blender/arm/logicnode/variable_quaternion.py index f4e7d2b5..a4d74f88 100644 --- a/blender/arm/logicnode/variable_quaternion.py +++ b/blender/arm/logicnode/variable_quaternion.py @@ -17,5 +17,7 @@ class QuaternionNode(Node, ArmLogicTreeNode): self.inputs[-1].default_value = 1.0 self.outputs.new('NodeSocketVector', 'Quaternion') + self.outputs.new('NodeSocketVector', 'XYZ') + self.outputs.new('NodeSocketFloat', 'W') add_node(QuaternionNode, category='Variable')