diff --git a/Sources/armory/logicnode/GetRotationNode.hx b/Sources/armory/logicnode/GetRotationNode.hx index 87309fe7..4fb8537d 100644 --- a/Sources/armory/logicnode/GetRotationNode.hx +++ b/Sources/armory/logicnode/GetRotationNode.hx @@ -1,10 +1,13 @@ package armory.logicnode; import iron.object.Object; -import iron.math.Vec3; +import iron.math.Quat; +import iron.math.Vec4; class GetRotationNode extends LogicNode { + public var property0: String; + public function new(tree: LogicTree) { super(tree); } @@ -16,34 +19,16 @@ class GetRotationNode extends LogicNode { return null; } - var rot = object.transform.rot; - switch (from) { - case 0: - // euler angles - return object.transform.rot.getEuler(); - case 1: - // vector - var sqrtW = Math.sqrt(1 - (rot.w * rot.w)); - if (sqrtW == 0) { - return new Vec3(0, 0, 1); - } - return new Vec3(rot.x / sqrtW, rot.y / sqrtW, rot.z / sqrtW); - case 2: - // angle radians - var angle = 2 * Math.acos(rot.w); - return angle; - case 3: - // angle degrees - var angle = 2 * Math.acos(rot.w); - return angle * (180 / Math.PI); - case 4: - //quaternion xyz - return new Vec3(rot.x, rot.y, rot.z); - case 5: - //quaternion w - return rot.w; - } + switch(property0){ + case "Local": + return object.transform.rot; + case "Global":{ + var useless1 = new Vec4(); + var ret = new Quat(); + object.transform.world.decompose(useless1, ret, useless1); + return ret; + }} return null; } } diff --git a/Sources/armory/logicnode/LookAtNode.hx b/Sources/armory/logicnode/LookAtNode.hx index 25e37ee7..537e7c39 100644 --- a/Sources/armory/logicnode/LookAtNode.hx +++ b/Sources/armory/logicnode/LookAtNode.hx @@ -37,6 +37,6 @@ class LookAtNode extends LogicNode { v2.setFrom(vto).sub(vfrom).normalize(); q.fromTo(v1, v2); - return q.getEuler(); + return q; } } diff --git a/Sources/armory/logicnode/RotateObjectNode.hx b/Sources/armory/logicnode/RotateObjectNode.hx index fe3f5e38..b880cf06 100644 --- a/Sources/armory/logicnode/RotateObjectNode.hx +++ b/Sources/armory/logicnode/RotateObjectNode.hx @@ -2,13 +2,11 @@ package armory.logicnode; import iron.object.Object; import iron.math.Quat; -import iron.math.Vec4; import armory.trait.physics.RigidBody; class RotateObjectNode extends LogicNode { - public var property0 = "Euler Angles"; - var q = new Quat(); + public var property0 = "Local"; public function new(tree: LogicTree) { super(tree); @@ -16,32 +14,19 @@ class RotateObjectNode extends LogicNode { override function run(from: Int) { var object: Object = inputs[1].get(); - var vec: Vec4 = inputs[2].get(); + var q: Quat = inputs[2].get(); - // note: here, the next line is disabled because old versions of the node don't have a third input. - // when those old versions will be considered remove, feel free to uncomment that, and replace the other `inputs[3].get()` by `w` in this file. - //var w: Float = inputs[3].get(); + if (object == null || q == null) return; - if (object == null || vec == null) return; - - switch (property0) { - case "Euler Angles": - q.fromEuler(vec.x, vec.y, vec.z); - case "Angle Axies (Degrees)" | "Angle Axies (Radians)": - var angle: Float = inputs[3].get(); - 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, inputs[3].get()); - q.normalize(); + q.normalize(); + switch (property0){ + case "Local": + object.transform.rot.mult(q); + case "Global": + object.transform.rot.multquats(q, object.transform.rot); + // that function call (Quat.multquats) is weird: it both modifies the object, and returns `this` } - - object.transform.rot.mult(q); + object.transform.buildMatrix(); #if arm_physics diff --git a/Sources/armory/logicnode/RotationMathNode.hx b/Sources/armory/logicnode/RotationMathNode.hx new file mode 100644 index 00000000..315c29ad --- /dev/null +++ b/Sources/armory/logicnode/RotationMathNode.hx @@ -0,0 +1,90 @@ +package armory.logicnode; + +import iron.math.Quat; +import iron.math.Vec4; +import iron.math.Mat4; +import kha.FastFloat; + +class RotationMathNode extends LogicNode { + + public var property0: String; // Operation + var res_q = new Quat(); + var res_v = new Vec4(); + var res_f: FastFloat = 0.0; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var q: Quat = inputs[0].get(); + if (q==null) return null; + + var res_q: Quat = new Quat(); + switch (property0) { + // 1 argument: Normalize, Inverse + case "Normalize": { + res_q.setFrom(q); + res_q = res_q.normalize(); + } + case "Inverse": { + var modl = q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w; + modl = -1/modl; + res_q.w = -q.w*modl; + res_q.x = q.x*modl; + res_q.y = q.y*modl; + res_q.z = q.z*modl; + } + // 2 arguments: Compose, Amplify, FromTo, FromRotationMat, + case "FromTo": { + var v1: Vec4 = inputs[0].get(); + var v2: Vec4 = inputs[1].get(); + if ((v1 == null) || (v2 == null)) return null; + res_q.fromTo(v1, v2); + } + case "Compose": { + var v1: Quat = inputs[0].get(); + var v2: Quat = inputs[1].get(); + if ((v1 == null) || (v2 == null)) return null; + res_q.multquats(v1,v2); + } + case "Amplify": { + var v1: Quat = inputs[0].get(); + var v2: Float = inputs[1].get(); + if ((v1 == null) || (v2 == null)) return null; + res_q.setFrom(v1); + var fac2 = Math.sqrt(1- res_q.w*res_q.w); + if (fac2 > 0.001) { + var fac1 = v2*Math.acos(res_q.w); + res_q.w = Math.cos(fac1); + fac1 = Math.sin(fac1)/fac2; + res_q.x *= fac1; + res_q.y *= fac1; + res_q.z *= fac1; + } + } + //case "FromRotationMat": { + // var m: Mat4 = inputs[1].get(); + // if (m == null) return null; + + // res_q = res_q.fromMat(m); + //} + // # 3 arguments: Lerp, Slerp, FromAxisAngle, FromEuler + case "Lerp": { + //var from = q; + var to: Quat = inputs[1].get(); + var f: Float = inputs[2].get(); + if ((f == null) || (to == null)) return null; + res_q = res_q.lerp(q, to, f); + } + case "Slerp": { + //var from = q; + var to: Quat = inputs[1].get(); + var f: Float = inputs[2].get(); + if ((f == null) || (to == null)) return null; + res_q = res_q.slerp(q, to, f); + } + } + return res_q; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/RotationNode.hx b/Sources/armory/logicnode/RotationNode.hx new file mode 100644 index 00000000..c3b07b95 --- /dev/null +++ b/Sources/armory/logicnode/RotationNode.hx @@ -0,0 +1,118 @@ +package armory.logicnode; + +import iron.math.Vec4; +import iron.math.Quat; +import kha.FastFloat; +import iron.math.Rotation; + +class RotationNode extends LogicNode { + + static inline var toRAD: FastFloat = 0.017453292519943295; // 180/pi + + public var property0: String; // type of input (EulerAngles, AxisAngle, Quaternion) + public var property1: String; // angle unit (Deg, Rad) + public var property2: String; // euler order (XYZ, XZY, etc…) + + public var value: Quat; + //var input0_cache: Vec4 = new Vec4(); + //var input1_cache: Float = 0; + var input_length: Int = 0; + + public function new(tree: LogicTree, x: Null = null, + y: Null = null, + z: Null = null, + w: Null = null + ) { + super(tree); + this.value = new Quat(); + if (x!=null) this.value.set(x,y,z,w); + for (input in inputs) { + if (input !=null) + this.input_length +=1; + else + break; + } + } + + override function get(from: Int): Dynamic { + //var inp0 = inputs[0].get(); + //var inp + //if (inputs[0].get()) + + switch (property0){ + case "Quaternion": { + if (inputs[0]!=null && inputs[1]!=null) { + var vect: Vec4 = inputs[0].get(); + value.x = vect.x; + value.y = vect.y; + value.z = vect.z; + value.w = inputs[1].get(); + } + } + case "AxisAngle": { + if (inputs[0]!=null && inputs[1]!=null){ + var vec: Vec4 = inputs[0].get(); + var angle: FastFloat = inputs[1].get(); + if (property1=="Deg") + angle *= toRAD; + value.fromAxisAngle(vec, angle); + } + } + case "EulerAngles": { + if (inputs[0] != null){ + var vec: Vec4 = new Vec4().setFrom(inputs[0].get()); + if (property1=="Deg"){ + vec.x *= toRAD; + vec.y *= toRAD; + vec.z *= toRAD; + } + this.value = Rotation.eulerToQuat(vec, property2); + } + } + default: { + return property0; + } + } + return this.value; + } + + override function set(value: Dynamic) { + switch (property0){ + case "Quaternion": { + if (input_length>1) { + var vect = new Vec4(); + vect.x = value.x; + vect.y = value.y; + vect.z = value.z; + inputs[0].set(vect); + inputs[1].set(value.w); + } + } + case "AxisAngle": { + if (input_length>1){ + var vec = new Vec4(); + var angle = this.value.toAxisAngle(vec); + if (property1=="Deg") + angle /= toRAD; + inputs[0].set(vec); + inputs[1].set(angle); + + } + } + case "EulerAngles": { + if (input_length>0){ + var vec = Rotation.quatToEuler(value, property2); + if (property1=="Deg"){ + vec.x /= toRAD; + vec.y /= toRAD; + vec.z /= toRAD; + } + inputs[0].set(vec); + } + } + } + if (input_length > 0){ + // NYI + }else this.value=value; + } +} diff --git a/Sources/armory/logicnode/SeparateRotationNode.hx b/Sources/armory/logicnode/SeparateRotationNode.hx new file mode 100644 index 00000000..a11af18a --- /dev/null +++ b/Sources/armory/logicnode/SeparateRotationNode.hx @@ -0,0 +1,61 @@ +package armory.logicnode; + +import kha.FastFloat; +import iron.math.Quat; +import iron.math.Vec4; +import iron.math.Rotation; + +class SeparateRotationNode extends LogicNode { + + public var property0 = "EulerAngles"; // EulerAngles, AxisAngle, or Quat + public var property1 = "Rad"; // Rad or Deg + public var property2 = "XYZ"; + + static inline var toDEG:FastFloat = 57.29577951308232; // 180/pi + + var input_cache = new Quat(); + var euler_cache = new Vec4(); + var aa_axis_cache = new Vec4(); + var aa_angle_cache: Float = 0; + + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var q: Quat = inputs[0].get(); + if (q == null) return null; + q.normalize(); + + switch (property0) { + case "EulerAngles": + if (q!=this.input_cache) + euler_cache = Rotation.quatToEuler(q, property2); + if (from>0) + return null; + + switch (property1){ + case "Rad": return euler_cache; + case "Deg": return new Vec4(euler_cache.x*toDEG, euler_cache.y*toDEG, euler_cache.z*toDEG); + } + + case "AxisAngle": + if (q!=this.input_cache) + aa_angle_cache = q.toAxisAngle(aa_axis_cache); + switch (from){ + case 0: return aa_axis_cache; + case 1: switch(property1){ + case "Rad": return aa_angle_cache; + case "Deg": return toDEG*aa_angle_cache; + } + } + case "Quat": + switch(from){ + case 0: return new Vec4(q.x,q.y,q.z); + case 1: return q.w; + } + } + return null; + } +} diff --git a/Sources/armory/logicnode/SeparateTransformNode.hx b/Sources/armory/logicnode/SeparateTransformNode.hx index da0c43d4..ac4ea144 100644 --- a/Sources/armory/logicnode/SeparateTransformNode.hx +++ b/Sources/armory/logicnode/SeparateTransformNode.hx @@ -20,7 +20,7 @@ class SeparateTransformNode extends LogicNode { matrix.decompose(loc, rot, scale); if (from == 0) return loc; - else if (from == 1) return rot.getEuler(); + else if (from == 1) return rot; else return scale; } } diff --git a/Sources/armory/logicnode/SetRotationNode.hx b/Sources/armory/logicnode/SetRotationNode.hx index a5d85475..0f820d20 100644 --- a/Sources/armory/logicnode/SetRotationNode.hx +++ b/Sources/armory/logicnode/SetRotationNode.hx @@ -1,14 +1,12 @@ + package armory.logicnode; import iron.object.Object; import iron.math.Quat; -import iron.math.Vec4; import armory.trait.physics.RigidBody; class SetRotationNode extends LogicNode { - public var property0: String; - public function new(tree: LogicTree) { super(tree); } @@ -16,27 +14,13 @@ class SetRotationNode extends LogicNode { override function run(from: Int) { var object: Object = inputs[1].get(); if (object == null) return; - var vec: Vec4 = inputs[2].get(); - if (vec == null) return; - var w: Float = inputs[3].get(); + var q: Quat = inputs[2].get(); + if (q == null) return; - switch (property0) { - case "Euler Angles": - object.transform.rot.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); - 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(); - } + q.normalize(); + object.transform.rot = q; object.transform.buildMatrix(); + #if arm_physics var rigidBody = object.getTrait(RigidBody); if (rigidBody != null) { diff --git a/Sources/armory/logicnode/TransformNode.hx b/Sources/armory/logicnode/TransformNode.hx index 148309b2..f00c7af9 100644 --- a/Sources/armory/logicnode/TransformNode.hx +++ b/Sources/armory/logicnode/TransformNode.hx @@ -7,9 +7,9 @@ import iron.math.Quat; class TransformNode extends LogicNode { var value: Mat4 = Mat4.identity(); - static var q = new Quat(); - static var v1 = new Vec4(); - static var v2 = new Vec4(); + var q = new Quat(); + var v1 = new Vec4(); + var v2 = new Vec4(); public function new(tree: LogicTree) { super(tree); @@ -17,18 +17,21 @@ class TransformNode extends LogicNode { override function get(from: Int): Dynamic { var loc: Vec4 = inputs[0].get(); - var rot: Vec4 = inputs[1].get(); + var rot: Quat = new Quat().setFrom(inputs[1].get()); + rot.normalize(); var scale: Vec4 = inputs[2].get(); + if (loc == null && rot == null && scale == null) return this.value; if (loc == null || rot == null || scale == null) return null; - q.fromEuler(rot.x, rot.y, rot.z); - value.compose(loc, q, scale); - return value; + this.value.compose(loc, rot, scale); + return this.value; } override function set(value: Dynamic) { - cast(value, Mat4).decompose(v1, q, v2); - inputs[0].set(v1); - inputs[1].set(q.getEuler()); - inputs[2].set(v2); + if (inputs.length>0){ + cast(value, Mat4).decompose(v1, q, v2); + inputs[0].set(v1); + inputs[1].set(q); + inputs[2].set(v2); + }else this.value = value; } } diff --git a/blender/arm/logicnode/arm_sockets.py b/blender/arm/logicnode/arm_sockets.py index 91f98476..b1dce886 100644 --- a/blender/arm/logicnode/arm_sockets.py +++ b/blender/arm/logicnode/arm_sockets.py @@ -1,6 +1,8 @@ +from math import pi, cos, sin, sqrt import bpy -from bpy.props import PointerProperty +from bpy.props import PointerProperty, EnumProperty, FloatProperty, FloatVectorProperty from bpy.types import NodeSocket +import mathutils import arm.utils @@ -68,6 +70,169 @@ class ArmAnimActionSocket(ArmCustomSocket): def draw_color(self, context, node): return 0.8, 0.8, 0.8, 1 + +class ArmRotationSocket(ArmCustomSocket): + bl_idname = 'ArmNodeSocketRotation' + bl_label = 'Rotation Socket' + arm_socket_type = 'ROTATION' # the internal representation is a quaternion, AKA a '4D vector' (using mathutils.Vector((x,y,z,w))) + + def get_default_value(self): + if self.default_value_raw is None: + return Vector((0.0,0.0,0.0,1.0)) + else: + return self.default_value_raw + + def on_unit_update(self, context): + if self.default_value_unit == 'Rad': + fac = pi/180 # deg->rad conversion + else: + fac = 180/pi # rad->deg conversion + if self.default_value_mode == 'AxisAngle': + self.default_value_s3 *= fac + elif self.default_value_mode == 'EulerAngles': + self.default_value_s0 *= fac + self.default_value_s1 *= fac + self.default_value_s2 *= fac + self.do_update_raw(context) + + + def on_mode_update(self, context): + if self.default_value_mode == 'Quat': + summ = abs(self.default_value_s0) + summ+= abs(self.default_value_s1) + summ+= abs(self.default_value_s2) + summ+= abs(self.default_value_s3) + if summ<0.01: + self.default_value_s3 = 1.0 + elif self.default_value_mode == 'AxisAngle': + summ = abs(self.default_value_s0) + summ+= abs(self.default_value_s1) + summ+= abs(self.default_value_s2) + if summ<1E-5: + self.default_value_s3 = 0.0 + self.do_update_raw(context) + + def do_update_raw(self, context): + if self.default_value_mode == 'Quat': + qx = self.default_value_s0 + qy = self.default_value_s1 + qz = self.default_value_s2 + qw = self.default_value_s3 + # need to normalize the quaternion for a rotation (having it be 0 is not an option) + ql = sqrt(qx**2+qy**2+qz**2+qw**2) + if abs(ql)<1E-5: + qx, qy, qz, qw = 0.0,0.0,0.0,1.0 + else: + qx /= ql + qy /= ql + qz /= ql + qw /= ql + self.default_value_raw = mathutils.Vector((qx,qy,qz,qw)) + + elif self.default_value_mode == 'AxisAngle': + if self.default_value_unit == 'Deg': + angle = self.default_value_s3 * pi/180 + else: + angle = self.default_value_s3 + cang, sang = cos(angle/2), sin(angle/2) + x = self.default_value_s0 + y = self.default_value_s1 + z = self.default_value_s2 + veclen = sqrt(x**2+y**2+z**2) + if veclen<1E-5: + self.default_value_raw = mathutils.Vector((0.0,0.0,0.0,1.0)) + else: + self.default_value_raw = mathutils.Vector(( + x/veclen * sang, + y/veclen * sang, + z/veclen * sang, + cang + )) + else: + if self.default_value_unit == 'Deg': + x = self.default_value_s0 * pi/180 + y = self.default_value_s1 * pi/180 + z = self.default_value_s2 * pi/180 + else: + x = self.default_value_s0 + y = self.default_value_s1 + z = self.default_value_s2 + cx, sx = cos(x/2), sin(x/2) + cy, sy = cos(y/2), sin(y/2) + cz, sz = cos(z/2), sin(z/2) + + qw, qx, qy, qz = 1.0,0.0,0.0,0.0 + for direction in self.default_value_order[::-1]: + qwi, qxi,qyi,qzi = {'X': (cx,sx,0,0), 'Y': (cy,0,sy,0), 'Z': (cz,0,0,sz)}[direction] + + qw = qw*qwi -qx*qxi -qy*qyi -qz*qzi + qx = qx*qwi +qw*qxi +qy*qzi -qz*qyi + qy = qy*qwi +qw*qyi +qz*qxi -qx*qzi + qz = qz*qwi +qw*qzi +qx*qyi -qy*qxi + self.default_value_raw = mathutils.Vector((qx,qy,qz,qw)) + + + def draw(self, context, layout, node, text): + if (self.is_output or self.is_linked): + layout.label(text=self.name) + else: + coll1 = layout.column(align=True) + coll1.label(text=self.name) + bx=coll1.box() + coll = bx.column(align=True) + coll.prop(self, 'default_value_mode') + if self.default_value_mode in ('EulerAngles', 'AxisAngle'): + coll.prop(self, 'default_value_unit') + + if self.default_value_mode == 'EulerAngles': + coll.prop(self, 'default_value_order') + coll.prop(self, 'default_value_s0', text='X') + coll.prop(self, 'default_value_s1', text='Y') + coll.prop(self, 'default_value_s2', text='Z') + elif self.default_value_mode == 'Quat': + coll.prop(self, 'default_value_s0', text='X') + coll.prop(self, 'default_value_s1', text='Y') + coll.prop(self, 'default_value_s2', text='Z') + coll.prop(self, 'default_value_s3', text='W') + elif self.default_value_mode == 'AxisAngle': + coll.prop(self, 'default_value_s0', text='X') + coll.prop(self, 'default_value_s1', text='Y') + coll.prop(self, 'default_value_s2', text='Z') + coll.separator() + coll.prop(self, 'default_value_s3', text='Angle') + + def draw_color(self, context, node): + return 0.68, 0.22, 0.62, 1 + + default_value_mode: EnumProperty( + items=[('EulerAngles', 'Euler Angles', 'Euler Angles'), + ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), + ('Quat', 'Quaternion', 'Quaternion')], + name='', default='EulerAngles', + update=on_mode_update) + + default_value_unit: EnumProperty( + items=[('Deg', 'Degrees', 'Degrees'), + ('Rad', 'Radians', 'Radians')], + name='', default='Rad', + update=on_unit_update) + default_value_order: EnumProperty( + items=[('XYZ','XYZ','XYZ'), + ('XZY','XZY (legacy Armory euler order)','XZY (legacy Armory euler order)'), + ('YXZ','YXZ','YXZ'), + ('YZX','YZX','YZX'), + ('ZXY','ZXY','ZXY'), + ('ZYX','ZYX','ZYX')], + name='', default='XYZ' + ) + + default_value_s0: FloatProperty(update=do_update_raw) + default_value_s1: FloatProperty(update=do_update_raw) + default_value_s2: FloatProperty(update=do_update_raw) + default_value_s3: FloatProperty(update=do_update_raw) + + default_value_raw: FloatVectorProperty(size=4, default=(0,0,0,1)) + class ArmArraySocket(ArmCustomSocket): bl_idname = 'ArmNodeSocketArray' @@ -118,6 +283,7 @@ class ArmObjectSocket(ArmCustomSocket): def register(): bpy.utils.register_class(ArmActionSocket) bpy.utils.register_class(ArmAnimActionSocket) + bpy.utils.register_class(ArmRotationSocket) bpy.utils.register_class(ArmArraySocket) bpy.utils.register_class(ArmObjectSocket) @@ -126,4 +292,5 @@ def unregister(): bpy.utils.unregister_class(ArmObjectSocket) bpy.utils.unregister_class(ArmArraySocket) bpy.utils.unregister_class(ArmAnimActionSocket) + bpy.utils.unregister_class(ArmRotationSocket) bpy.utils.unregister_class(ArmActionSocket) diff --git a/blender/arm/logicnode/math/LN_rotation_math.py b/blender/arm/logicnode/math/LN_rotation_math.py new file mode 100644 index 00000000..50d2abd3 --- /dev/null +++ b/blender/arm/logicnode/math/LN_rotation_math.py @@ -0,0 +1,119 @@ +from arm.logicnode.arm_nodes import * + +class QuaternionMathNode(ArmLogicTreeNode): + """Mathematical operations on rotations.""" + bl_idname = 'LNRotationMathNode' + bl_label = 'Rotation Math' + arm_section = 'quaternions' + arm_version = 1 + + + @staticmethod + def get_count_in(operation_name): + return { + 'Inverse': 1, + 'Normalize': 1, + 'Compose': 2, + 'Amplify': 2, + 'FromTo': 2, + #'FromRotationMat': 2, + 'Lerp': 3, + 'Slerp': 3, + }.get(operation_name, 0) + + def ensure_input_socket(self, socket_number, newclass, newname): + while len(self.inputs) < socket_number: + self.inputs.new('NodeSocketFloat', 'BOGUS') + if len(self.inputs) > socket_number: + if len(self.inputs[socket_number].links) == 1: + source_socket = self.inputs[socket_number].links[0].from_socket + else: + source_socket = None + self.inputs.remove(self.inputs[socket_number]) + else: + source_socket = None + + + self.inputs.new(newclass, newname) + self.inputs.move(len(self.inputs)-1, socket_number) + if source_socket is not None: + self.id_data.links.new(source_socket, self.inputs[socket_number]) + + def ensure_output_socket(self, socket_number, newclass, newname): + sink_sockets = [] + while len(self.outputs) < socket_number: + self.outputs.new('NodeSocketFloat', 'BOGUS') + if len(self.outputs) > socket_number: + for link in self.inputs[socket_number].links: + sink_sockets.append(link.to_socket) + self.inputs.remove(self.inputs[socket_number]) + + self.inputs.new(newclass, newname) + self.inputs.move(len(self.inputs)-1, socket_number) + for socket in sink_sockets: + self.id_data.links.new(self.inputs[socket_number], socket) + + def on_update_operation(self, context): + # Checking the selection of another operation + + + # Rotation as argument 0: + if self.property0 in ('Inverse','Normalize','Amplify'): + self.ensure_input_socket(0, "ArmNodeSocketRotation", "Rotation") + self.ensure_input_socket(1, "NodeSocketFloat", "Amplification factor") + elif self.property0 in ('Slerp','Lerp','Compose'): + self.ensure_input_socket(0, "ArmNodeSocketRotation", "From") + self.ensure_input_socket(1, "ArmNodeSocketRotation", "To") + + if self.property0 == 'Compose': + self.inputs[0].name = 'Outer rotation' + self.inputs[1].name = 'Inner rotation' + else: + self.ensure_input_socket(2, "NodeSocketFloat", "Interpolation factor") + + elif self.property0 == 'FromTo': + self.ensure_input_socket(0, "ArmNodeSocketRotation", "From") + self.ensure_input_socket(1, "ArmNodeSocketRotation", "To") + + # Rotation as argument 1: + if self.property0 in ('Compose','Lerp','Slerp'): + if self.inputs[1].bl_idname != "ArmNodeSocketRotation": + self.replace_input_socket(1, "ArmNodeSocketRotation", "Quaternion 2") + if self.property0 == 'Compose': + self.inputs[1].name = "Inner quaternion" + # Float as argument 1: + if self.property0 == 'Amplify': + if self.inputs[1].bl_idname != 'NodeSocketFloat': + self.replace_input_socket(1, "NodeSocketFloat", "Amplification factor") + # Vector as argument 1: + #if self.property0 == 'FromRotationMat': + # # WHAT?? + # pass + + while len(self.inputs) > self.get_count_in(self.property0): + self.inputs.remove(self.inputs[len(self.inputs)-1]) + + + property0: EnumProperty( + items = [('Compose', 'Compose (multiply)', 'compose (multiply) two rotations. Note that order of the composition matters.'), + ('Amplify', 'Amplify (multiply by float)', 'Amplify or diminish the effect of a rotation'), + ('Normalize', 'Normalize', 'Normalize'), + ('Inverse', 'Get Inverse', 'from r, get the rotation r2 so that " r×r2=r2×r= " '), + ('Lerp', 'Lerp', 'Linearly interpolation'), + ('Slerp', 'Slerp', 'Spherical linear interpolation'), + ('FromTo', 'From To', 'From direction To direction'), + #('FromRotationMat', 'From Rotation Mat', 'From Rotation Mat') + ], + name='', default='Compose', update=on_update_operation) + + #def __init__(self): + # array_nodes[str(id(self))] = self + + def init(self, context): + super(QuaternionMathNode, self).init(context) + self.add_input('ArmNodeSocketRotation', 'Quaternion 0', default_value=[0.0, 0.0, 0.0]) + self.add_input('ArmNodeSocketRotation', 'Quaternion 1', default_value=[0.0, 0.0, 0.0]) + self.add_output('ArmNodeSocketRotation', 'Result') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') # Operation diff --git a/blender/arm/logicnode/transform/LN_get_object_rotation.py b/blender/arm/logicnode/transform/LN_get_object_rotation.py index ab14b0d0..9ba23553 100644 --- a/blender/arm/logicnode/transform/LN_get_object_rotation.py +++ b/blender/arm/logicnode/transform/LN_get_object_rotation.py @@ -10,10 +10,12 @@ class GetRotationNode(ArmLogicTreeNode): def init(self, context): super(GetRotationNode, self).init(context) self.add_input('ArmNodeSocketObject', 'Object') + self.add_output('ArmNodeSocketRotation', 'Rotation') - self.add_output('NodeSocketVector', 'Euler Angles') - self.add_output('NodeSocketVector', 'Vector') - self.add_output('NodeSocketFloat', 'Angle (Radians)') - self.add_output('NodeSocketFloat', 'Angle (Degrees)') - self.add_output('NodeSocketVector', 'Quaternion XYZ') - self.add_output('NodeSocketFloat', 'Quaternion W') + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + property0: EnumProperty( + items = [('Local', 'Local', 'Local'), + ('Global', 'Global', 'Global')], + name='', default='Local') diff --git a/blender/arm/logicnode/transform/LN_look_at.py b/blender/arm/logicnode/transform/LN_look_at.py index b1bdc31a..87fdf03f 100644 --- a/blender/arm/logicnode/transform/LN_look_at.py +++ b/blender/arm/logicnode/transform/LN_look_at.py @@ -1,7 +1,7 @@ from arm.logicnode.arm_nodes import * class LookAtNode(ArmLogicTreeNode): - """Converts the two given coordinates to a quaternion rotation.""" + """Returns *a* rotation that makes something look away from X,Y or Z, and instead look in the 'from->to' direction""" bl_idname = 'LNLookAtNode' bl_label = 'Look At' arm_section = 'rotation' @@ -21,7 +21,7 @@ class LookAtNode(ArmLogicTreeNode): self.add_input('NodeSocketVector', 'From Location') self.add_input('NodeSocketVector', 'To Location') - self.add_output('NodeSocketVector', 'Rotation') + self.add_output('ArmNodeSocketRotation', 'Rotation') def draw_buttons(self, context, layout): layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/transform/LN_rotate_object.py b/blender/arm/logicnode/transform/LN_rotate_object.py index 7b21abb4..c49f68f0 100644 --- a/blender/arm/logicnode/transform/LN_rotate_object.py +++ b/blender/arm/logicnode/transform/LN_rotate_object.py @@ -11,39 +11,14 @@ class RotateObjectNode(ArmLogicTreeNode): super().init(context) self.add_input('ArmNodeSocketAction', 'In') self.add_input('ArmNodeSocketObject', 'Object') - self.add_input('NodeSocketVector', 'Euler Angles') - self.add_input('NodeSocketFloat', 'Angle / W') + self.add_input('ArmNodeSocketRotation', 'Rotation') self.add_output('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') + 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) + items = [('Local', 'Local F.O.R.', 'Frame of reference oriented with the object'), + ('Global', 'Global/Parent F.O.R.', 'Frame of reference oriented with the object\'s parent or the world')], + name='', default='Local') diff --git a/blender/arm/logicnode/transform/LN_separate_rotation.py b/blender/arm/logicnode/transform/LN_separate_rotation.py new file mode 100644 index 00000000..0111d5a0 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_separate_rotation.py @@ -0,0 +1,58 @@ +from arm.logicnode.arm_nodes import * + +class SeparateRotationNode(ArmLogicTreeNode): + """Decompose a rotation into one of its mathematical representations""" + bl_idname = 'LNSeparateRotationNode' + bl_label = 'Separate Rotation' + arm_section = 'rotation' + arm_version = 1 + + def init(self, context): + super(SeparateRotationNode, self).init(context) + self.add_input('ArmNodeSocketRotation', 'Angle') + + self.add_output('NodeSocketVector', 'Euler Angles / Vector XYZ') + self.add_output('NodeSocketFloat', 'Angle / W') + + + def on_property_update(self, context): + """called by the EnumProperty, used to update the node socket labels""" + if self.property0 == "Quat": + self.outputs[0].name = "Quaternion XYZ" + self.outputs[1].name = "Quaternion W" + elif self.property0 == "EulerAngles": + self.outputs[0].name = "Euler Angles" + self.outputs[1].name = "[unused for Euler output]" + elif self.property0.startswith("AxisAngle"): + self.outputs[0].name = "Axis" + self.outputs[1].name = "Angle" + else: + raise ValueError('No nodesocket labels for current input mode: check self-consistancy of LN_separate_rotation.py') + + def draw_buttons(self, context, layout): + coll = layout.column(align=True) + coll.prop(self, 'property0') + if self.property0 in ('EulerAngles','AxisAngle'): + coll.prop(self, 'property1') + if self.property0=='EulerAngles': + coll.prop(self, 'property2') + + property0: EnumProperty( + items = [('EulerAngles', 'Euler Angles', 'Euler Angles'), + ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), + ('Quat', 'Quaternion', 'Quaternion')], + name='', default='EulerAngles', + update=on_property_update) + + property1: EnumProperty( + items=[('Deg', 'Degrees', 'Degrees'), + ('Rad', 'Radians', 'Radians')], + name='', default='Rad') + property2: EnumProperty( + items=[('XYZ','XYZ','XYZ'), + ('XZY','XZY (legacy Armory euler order)','XZY (legacy Armory euler order)'), + ('YXZ','YXZ','YXZ'), + ('YZX','YZX','YZX'), + ('ZXY','ZXY','ZXY'), + ('ZYX','ZYX','ZYX')], + name='', default='XYZ') diff --git a/blender/arm/logicnode/transform/LN_separate_transform.py b/blender/arm/logicnode/transform/LN_separate_transform.py index 5c834992..6ab0ade8 100644 --- a/blender/arm/logicnode/transform/LN_separate_transform.py +++ b/blender/arm/logicnode/transform/LN_separate_transform.py @@ -11,5 +11,5 @@ class SeparateTransformNode(ArmLogicTreeNode): self.add_input('NodeSocketShader', 'Transform') self.add_output('NodeSocketVector', 'Location') - self.add_output('NodeSocketVector', 'Rotation') + self.add_output('ArmNodeSocketRotation', 'Rotation') self.add_output('NodeSocketVector', 'Scale') diff --git a/blender/arm/logicnode/transform/LN_set_object_rotation.py b/blender/arm/logicnode/transform/LN_set_object_rotation.py index 4a59e161..555e3229 100644 --- a/blender/arm/logicnode/transform/LN_set_object_rotation.py +++ b/blender/arm/logicnode/transform/LN_set_object_rotation.py @@ -6,37 +6,20 @@ class SetRotationNode(ArmLogicTreeNode): bl_label = 'Set Object Rotation' arm_section = 'rotation' arm_version = 1 + def init(self, context): super(SetRotationNode, self).init(context) self.add_input('ArmNodeSocketAction', 'In') self.add_input('ArmNodeSocketObject', 'Object') - self.add_input('NodeSocketVector', 'Euler Angles / Vector XYZ') - self.add_input('NodeSocketFloat', 'Angle / W') + self.add_input('ArmNodeSocketRotation', 'Rotation') self.add_output('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') - 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) +# property0: EnumProperty( +# items = [('Local', 'Local', 'Local'), +# ('Global', 'Global', 'Global')], +# name='', default='Local') diff --git a/blender/arm/logicnode/transform/LN_test_rotation.py b/blender/arm/logicnode/transform/LN_test_rotation.py new file mode 100644 index 00000000..5b66910d --- /dev/null +++ b/blender/arm/logicnode/transform/LN_test_rotation.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class TestRotationNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNTestRotationNode' + bl_label = 'TEST NODE DO NOT USE' + arm_section = 'quaternions' + arm_version = 1 + + def init(self, context): + super(TestRotationNode, self).init(context) + self.add_input('ArmNodeSocketRotation', 'taste') diff --git a/blender/arm/logicnode/transform/LN_transform.py b/blender/arm/logicnode/transform/LN_transform.py index 002502a8..1087caba 100644 --- a/blender/arm/logicnode/transform/LN_transform.py +++ b/blender/arm/logicnode/transform/LN_transform.py @@ -9,7 +9,7 @@ class TransformNode(ArmLogicTreeNode): def init(self, context): super(TransformNode, self).init(context) self.add_input('NodeSocketVector', 'Location') - self.add_input('NodeSocketVector', 'Rotation') + self.add_input('ArmNodeSocketRotation', 'Rotation') self.add_input('NodeSocketVector', 'Scale', default_value=[1.0, 1.0, 1.0]) - self.add_output('NodeSocketShader', 'Transform') + self.add_output('NodeSocketShader', 'Transform', is_var=True) diff --git a/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py b/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py index e1402b6a..cb9ceaf9 100644 --- a/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py +++ b/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py @@ -10,7 +10,7 @@ class VectorToObjectOrientationNode(ArmLogicTreeNode): """ bl_idname = 'LNVectorToObjectOrientationNode' bl_label = 'Vector to Object Orientation' - arm_section = 'location' + arm_section = 'rotation' arm_version = 1 def init(self, context): diff --git a/blender/arm/logicnode/variable/LN_rotation.py b/blender/arm/logicnode/variable/LN_rotation.py new file mode 100644 index 00000000..ac832cea --- /dev/null +++ b/blender/arm/logicnode/variable/LN_rotation.py @@ -0,0 +1,58 @@ +from arm.logicnode.arm_nodes import * + +class RotationNode(ArmLogicTreeNode): + """A rotation, created from one of its possible mathematical representations""" + bl_idname = 'LNRotationNode' + bl_label = 'Rotation' + #arm_section = 'rotation' + arm_version = 1 + + def init(self, context): + super(RotationNode, self).init(context) + self.add_input('NodeSocketVector', 'Euler Angles / Vector XYZ') + self.add_input('NodeSocketFloat', 'Angle / W') + + self.add_output('ArmNodeSocketRotation', 'Out', is_var=True) + + def on_property_update(self, context): + """called by the EnumProperty, used to update the node socket labels""" + if self.property0 == "Quaternion": + self.inputs[0].name = "Quaternion XYZ" + self.inputs[1].name = "Quaternion W" + elif self.property0 == "EulerAngles": + self.inputs[0].name = "Euler Angles" + self.inputs[1].name = "[unused for Euler input]" + elif self.property0 == "AxisAngle": + self.inputs[0].name = "Axis" + self.inputs[1].name = "Angle" + else: + raise ValueError('No nodesocket labels for current input mode: check self-consistancy of LN_rotation.py') + + def draw_buttons(self, context, layout): + coll = layout.column(align=True) + coll.prop(self, 'property0') + if self.property0 in ('EulerAngles','AxisAngle'): + coll.prop(self, 'property1') + if self.property0=='EulerAngles': + coll.prop(self, 'property2') + + property0: EnumProperty( + items = [('EulerAngles', 'Euler Angles', 'Euler Angles'), + ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), + ('Quaternion', 'Quaternion', 'Quaternion')], + name='', default='EulerAngles', + update=on_property_update) + + property1: EnumProperty( + items=[('Deg', 'Degrees', 'Degrees'), + ('Rad', 'Radians', 'Radians')], + name='', default='Rad') + property2: EnumProperty( + items=[('XYZ','XYZ','XYZ'), + ('XZY','XZY (legacy Armory euler order)','XZY (legacy Armory euler order)'), + ('YXZ','YXZ','YXZ'), + ('YZX','YZX','YZX'), + ('ZXY','ZXY','ZXY'), + ('ZYX','ZYX','ZYX')], + name='', default='XYZ' + ) diff --git a/blender/arm/make_logic.py b/blender/arm/make_logic.py index 6ee7aac8..cbec6f34 100755 --- a/blender/arm/make_logic.py +++ b/blender/arm/make_logic.py @@ -294,6 +294,8 @@ def build_default_node(inp: bpy.types.NodeSocket): if inp_type == 'VECTOR': return f'new armory.logicnode.VectorNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]})' + elif inp_type == 'ROTATION': # a rotation is internally represented as a quaternion. + return f'new armory.logicnode.RotationNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]}, {default_value[3]})' elif inp_type == 'RGBA': return f'new armory.logicnode.ColorNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]}, {default_value[3]})' elif inp_type == 'RGB':