From 9b1393ea41a95f267a0cdd4b5395deff0808a6b9 Mon Sep 17 00:00:00 2001 From: niacdoial Date: Tue, 15 Jun 2021 23:06:43 +0200 Subject: [PATCH 1/5] Make rotation its own socket in logic nodes, add rotation-specific math node (part 1: conversion code not developed) --- Sources/armory/logicnode/GetRotationNode.hx | 41 ++--- Sources/armory/logicnode/LookAtNode.hx | 2 +- Sources/armory/logicnode/RotateObjectNode.hx | 37 ++-- Sources/armory/logicnode/RotationMathNode.hx | 90 ++++++++++ Sources/armory/logicnode/RotationNode.hx | 118 ++++++++++++ .../armory/logicnode/SeparateRotationNode.hx | 61 +++++++ .../armory/logicnode/SeparateTransformNode.hx | 2 +- Sources/armory/logicnode/SetRotationNode.hx | 28 +-- Sources/armory/logicnode/TransformNode.hx | 25 +-- blender/arm/logicnode/arm_sockets.py | 169 +++++++++++++++++- .../arm/logicnode/math/LN_rotation_math.py | 119 ++++++++++++ .../transform/LN_get_object_rotation.py | 14 +- blender/arm/logicnode/transform/LN_look_at.py | 4 +- .../logicnode/transform/LN_rotate_object.py | 35 +--- .../transform/LN_separate_rotation.py | 58 ++++++ .../transform/LN_separate_transform.py | 2 +- .../transform/LN_set_object_rotation.py | 33 +--- .../logicnode/transform/LN_test_rotation.py | 12 ++ .../arm/logicnode/transform/LN_transform.py | 4 +- .../LN_vector_to_object_orientation.py | 2 +- blender/arm/logicnode/variable/LN_rotation.py | 58 ++++++ blender/arm/make_logic.py | 2 + 22 files changed, 759 insertions(+), 157 deletions(-) create mode 100644 Sources/armory/logicnode/RotationMathNode.hx create mode 100644 Sources/armory/logicnode/RotationNode.hx create mode 100644 Sources/armory/logicnode/SeparateRotationNode.hx create mode 100644 blender/arm/logicnode/math/LN_rotation_math.py create mode 100644 blender/arm/logicnode/transform/LN_separate_rotation.py create mode 100644 blender/arm/logicnode/transform/LN_test_rotation.py create mode 100644 blender/arm/logicnode/variable/LN_rotation.py 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': From bd67667a6ed66b5697466faf937c0d4215ce4acd Mon Sep 17 00:00:00 2001 From: niacdoial Date: Thu, 12 Aug 2021 20:58:35 +0200 Subject: [PATCH 2/5] Added node replacement routines for previous commits (and fixed a couple bugs along the way) --- blender/arm/logicnode/arm_nodes.py | 92 +++-- blender/arm/logicnode/arm_sockets.py | 72 ++-- .../arm/logicnode/math/LN_quaternion_math.py | 381 +++++++++++++----- .../arm/logicnode/math/LN_rotation_math.py | 3 +- .../logicnode/math/LN_separate_quaternion.py | 33 +- .../transform/LN_get_object_rotation.py | 71 +++- blender/arm/logicnode/transform/LN_look_at.py | 22 +- .../logicnode/transform/LN_rotate_object.py | 59 ++- .../transform/LN_separate_rotation.py | 2 +- .../transform/LN_separate_transform.py | 33 +- .../transform/LN_set_object_rotation.py | 66 ++- .../arm/logicnode/transform/LN_transform.py | 33 +- .../arm/logicnode/variable/LN_quaternion.py | 59 ++- blender/arm/logicnode/variable/LN_rotation.py | 1 + 14 files changed, 750 insertions(+), 177 deletions(-) diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index 64c23121..877c01b3 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -121,7 +121,14 @@ class ArmNodeAddInputButton(bpy.types.Operator): def execute(self, context): global array_nodes inps = array_nodes[self.node_index].inputs - inps.new(self.socket_type, self.name_format.format(str(len(inps) + self.index_name_offset))) + + socket_types = self.socket_type.split(';') + name_formats = self.name_format.split(';') + assert len(socket_types)==len(name_formats) + + format_index = (len(inps) + self.index_name_offset) //len(socket_types) + for socket_type, name_format in zip(socket_types, name_formats): + inps.new(socket_type, name_format.format(str(format_index))) # Reset to default again for subsequent calls of this operator self.node_index = '' @@ -132,31 +139,34 @@ class ArmNodeAddInputButton(bpy.types.Operator): return{'FINISHED'} class ArmNodeAddInputValueButton(bpy.types.Operator): - """Add new input""" - bl_idname = 'arm.node_add_input_value' - bl_label = 'Add Input' - node_index: StringProperty(name='Node Index', default='') - socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') + """Add new input""" + bl_idname = 'arm.node_add_input_value' + bl_label = 'Add Input' + node_index: StringProperty(name='Node Index', default='') + socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') - def execute(self, context): - global array_nodes - inps = array_nodes[self.node_index].inputs - inps.new(self.socket_type, 'Value') - return{'FINISHED'} + def execute(self, context): + global array_nodes + inps = array_nodes[self.node_index].inputs + inps.new(self.socket_type, 'Value') + return{'FINISHED'} class ArmNodeRemoveInputButton(bpy.types.Operator): """Remove last input""" bl_idname = 'arm.node_remove_input' bl_label = 'Remove Input' node_index: StringProperty(name='Node Index', default='') + count: IntProperty(name='Number of inputs to remove', default=1, min=1) + min_inputs: IntProperty(name='Number of inputs to keep', default=0, min=0) def execute(self, context): global array_nodes node = array_nodes[self.node_index] inps = node.inputs - min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs - if len(inps) > min_inps: - inps.remove(inps.values()[-1]) + min_inps = self.min_inputs if not hasattr(node, 'min_inputs') else node.min_inputs + if len(inps) >= min_inps + self.count: + for _ in range(self.count): + inps.remove(inps.values()[-1]) return{'FINISHED'} class ArmNodeRemoveInputValueButton(bpy.types.Operator): @@ -164,13 +174,14 @@ class ArmNodeRemoveInputValueButton(bpy.types.Operator): bl_idname = 'arm.node_remove_input_value' bl_label = 'Remove Input' node_index: StringProperty(name='Node Index', default='') + target_name: StringProperty(name='Name of socket to remove', default='Value') def execute(self, context): global array_nodes node = array_nodes[self.node_index] inps = node.inputs min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs - if len(inps) > min_inps and inps[-1].name == 'Value': + if len(inps) > min_inps and inps[-1].name == self.target_name: inps.remove(inps.values()[-1]) return{'FINISHED'} @@ -187,7 +198,14 @@ class ArmNodeAddOutputButton(bpy.types.Operator): def execute(self, context): global array_nodes outs = array_nodes[self.node_index].outputs - outs.new(self.socket_type, self.name_format.format(str(len(outs) + self.index_name_offset))) + + socket_types = self.socket_type.split(';') + name_formats = self.name_format.split(';') + assert len(socket_types)==len(name_formats) + + format_index = (len(outs) + self.index_name_offset) //len(socket_types) + for socket_type, name_format in zip(socket_types, name_formats): + outs.new(socket_type, name_format.format(str(format_index))) # Reset to default again for subsequent calls of this operator self.node_index = '' @@ -202,14 +220,16 @@ class ArmNodeRemoveOutputButton(bpy.types.Operator): bl_idname = 'arm.node_remove_output' bl_label = 'Remove Output' node_index: StringProperty(name='Node Index', default='') + count: IntProperty(name='Number of outputs to remove', default=1, min=1) def execute(self, context): global array_nodes node = array_nodes[self.node_index] outs = node.outputs min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs - if len(outs) > min_outs: - outs.remove(outs.values()[-1]) + if len(outs) >= min_outs + self.count: + for _ in range(self.count): + outs.remove(outs.values()[-1]) return{'FINISHED'} class ArmNodeAddInputOutputButton(bpy.types.Operator): @@ -229,8 +249,21 @@ class ArmNodeAddInputOutputButton(bpy.types.Operator): node = array_nodes[self.node_index] inps = node.inputs outs = node.outputs - inps.new(self.in_socket_type, self.in_name_format.format(str(len(inps) + self.in_index_name_offset))) - outs.new(self.out_socket_type, self.out_name_format.format(str(len(outs)))) + + in_socket_types = self.in_socket_type.split(';') + in_name_formats = self.in_name_format.split(';') + assert len(in_socket_types)==len(in_name_formats) + + out_socket_types = self.out_socket_type.split(';') + out_name_formats = self.out_name_format.split(';') + assert len(out_socket_types)==len(out_name_formats) + + in_format_index = (len(outs) + self.index_name_offset) // len(in_socket_types) + out_format_index = len(outs) // len(out_socket_types) + for socket_type, name_format in zip(in_socket_types, in_name_formats): + inps.new(socket_type, name_format.format(str(in_format_index))) + for socket_type, name_format in zip(out_socket_types, out_name_formats): + outs.new(socket_type, name_format.format(str(out_format_index))) # Reset to default again for subsequent calls of this operator self.node_index = '' @@ -247,7 +280,9 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator): bl_idname = 'arm.node_remove_input_output' bl_label = 'Remove Input Output' node_index: StringProperty(name='Node Index', default='') - + in_count: IntProperty(name='Number of inputs to remove', default=1, min=1) + out_count: IntProperty(name='Number of inputs to remove', default=1, min=1) + def execute(self, context): global array_nodes node = array_nodes[self.node_index] @@ -255,10 +290,12 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator): outs = node.outputs min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs - if len(inps) > min_inps: - inps.remove(inps.values()[-1]) - if len(outs) > min_outs: - outs.remove(outs.values()[-1]) + if len(inps) >= min_inps + self.in_count: + for _ in range(self.in_count): + inps.remove(inps.values()[-1]) + if len(outs) >= min_outs + self.out_count: + for _ in range(self.out_count): + outs.remove(outs.values()[-1]) return{'FINISHED'} @@ -428,7 +465,10 @@ def deprecated(*alternatives: str, message=""): def wrapper(cls: ArmLogicTreeNode) -> ArmLogicTreeNode: cls.bl_label += ' (Deprecated)' - cls.bl_description = f'Deprecated. {cls.bl_description}' + if hasattr(cls, 'bl_description'): + cls.bl_description = f'Deprecated. {cls.bl_description}' + else: + cls.bl_description = 'Deprecated.' cls.bl_icon = 'ERROR' cls.arm_is_obsolete = True diff --git a/blender/arm/logicnode/arm_sockets.py b/blender/arm/logicnode/arm_sockets.py index b1dce886..a0f702fd 100644 --- a/blender/arm/logicnode/arm_sockets.py +++ b/blender/arm/logicnode/arm_sockets.py @@ -112,12 +112,16 @@ class ArmRotationSocket(ArmCustomSocket): 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 + @staticmethod + def convert_to_quaternion(part1,part2,param1,param2,param3): + """converts a representation of rotation into a quaternion. + ``part1`` is a vector, ``part2`` is a scalar or None, + ``param1`` is in ('Quat', 'EulerAngles', 'AxisAngle'), + ``param2`` is in ('Rad','Deg') for both EulerAngles and AxisAngle, + ``param3`` is a len-3 string like "XYZ", for EulerAngles """ + if param1 == 'Quat': + qx, qy, qz = part1[0], part1[1], part1[2] + qw = part2 # 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: @@ -127,49 +131,61 @@ class ArmRotationSocket(ArmCustomSocket): 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 + return mathutils.Vector((qx,qy,qz,qw)) + + elif param1 == 'AxisAngle': + if param2 == 'Deg': + angle = part2 * pi/180 else: - angle = self.default_value_s3 + angle = part2 cang, sang = cos(angle/2), sin(angle/2) - x = self.default_value_s0 - y = self.default_value_s1 - z = self.default_value_s2 + x,y,z = part1[0], part1[1], part1[2] 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)) + return mathutils.Vector((0.0,0.0,0.0,1.0)) else: - self.default_value_raw = mathutils.Vector(( + return 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 + else: # param1 == 'EulerAngles' + x,y,z = part1[0], part1[1], part1[2] + if param2 == 'Deg': + x *= pi/180 + y *= pi/180 + z *= pi/180 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]: + for direction in param3[::-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)) + return mathutils.Vector((qx,qy,qz,qw)) + + + def do_update_raw(self, context): + part1 = mathutils.Vector(( + self.default_value_s0, + self.default_value_s1, + self.default_value_s2, 1 + )) + part2 = self.default_value_s3 + + self.default_value_raw = self.convert_to_quaternion( + part1, + self.default_value_s3, + self.default_value_mode, + self.defualt_value_unit, + self.default_value_order + ) def draw(self, context, layout, node, text): diff --git a/blender/arm/logicnode/math/LN_quaternion_math.py b/blender/arm/logicnode/math/LN_quaternion_math.py index d1f91abc..10f13832 100644 --- a/blender/arm/logicnode/math/LN_quaternion_math.py +++ b/blender/arm/logicnode/math/LN_quaternion_math.py @@ -4,44 +4,46 @@ class QuaternionMathNode(ArmLogicTreeNode): """Mathematical operations on quaternions.""" bl_idname = 'LNQuaternionMathNode' bl_label = 'Quaternion Math' + bl_description = 'Mathematical operations that can be performed on rotations, when represented as quaternions specifically' arm_section = 'quaternions' - arm_version = 1 + arm_version = 2 - def get_bool(self): - return self.get('property1', False) - def set_bool(self, value): - self['property1'] = value - if value: - if ((self.property0 == 'Module') or (self.property0 == 'DotProduct') or (self.property0 == 'ToAxisAngle')) and (len(self.outputs) > 1): - self.outputs.remove(self.outputs.values()[-1]) # Module/DotProduct/ToAxisAngle - self.add_output('NodeSocketFloat', 'X') # Result X - self.add_output('NodeSocketFloat', 'Y') # Result Y - self.add_output('NodeSocketFloat', 'Z') # Result Z - self.add_output('NodeSocketFloat', 'W') # Result W - if (self.property0 == 'Module'): - self.add_output('NodeSocketFloat', 'Module') # Module - if (self.property0 == 'DotProduct'): - self.add_output('NodeSocketFloat', 'Scalar') # DotProduct - if (self.property0 == 'ToAxisAngle'): - self.add_output('NodeSocketFloat', 'To Axis Angle') # ToAxisAngle + def ensure_input_socket(self, socket_number, newclass, newname, default_value=None): + 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 + if self.inputs[socket_number].bl_idname == newclass: + default_value = self.inputs[socket_number].default_value + self.inputs.remove(self.inputs[socket_number]) else: - if ((self.property0 == 'Module') or (self.property0 == 'DotProduct') or (self.property0 == 'ToAxisAngle')) and (len(self.outputs) > 1): - self.outputs.remove(self.outputs.values()[-1]) # Module/DotProduct/ToAxisAngle - # Remove X, Y, Z, W - for i in range(4): - if len(self.outputs) > 1: - self.outputs.remove(self.outputs.values()[-1]) - else: - break - if (self.property0 == 'Module'): - self.add_output('NodeSocketFloat', 'Module') # Module - if (self.property0 == 'DotProduct'): - self.add_output('NodeSocketFloat', 'Scalar') # DotProduct - if (self.property0 == 'ToAxisAngle'): - self.add_output('NodeSocketFloat', 'To Axis Angle') # ToAxisAngle + source_socket = None + + + self.inputs.new(newclass, newname) + if default_value != None: + self.inputs[-1].default_value = default_value + 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]) - property1: BoolProperty(name='Separator Out', default=False, set=set_bool, get=get_bool) + 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) @staticmethod def get_enum_id_value(obj, prop_name, value): @@ -75,62 +77,40 @@ class QuaternionMathNode(ArmLogicTreeNode): # Checking the selection of another operation select_current = self.get_enum_id_value(self, 'property0', value) select_prev = self.property0 - if select_prev != select_current: - # Remove - count = 0 - if ((select_prev == 'Add') or (select_prev == 'Subtract') or (select_prev == 'Multiply') or (select_prev == 'DotProduct')) and ((select_current == 'Add') or (select_current == 'Subtract') or (select_current == 'Multiply') or (select_current == 'DotProduct')) or (((select_current == 'Lerp') or (select_current == 'Slerp')) and ((select_prev == 'Lerp') or (select_prev == 'Slerp'))): - count = len(self.inputs) - while (len(self.inputs) > count): - self.inputs.remove(self.inputs.values()[-1]) - if (select_prev == 'DotProduct') or (select_prev == 'ToAxisAngle') or (select_prev == 'Module'): - self.outputs.remove(self.outputs.values()[-1]) - - # Many arguments: Add, Subtract, DotProduct, Multiply, MultiplyFloat - if (self.get_count_in(select_current) == 0): - if (select_current == "MultiplyFloats"): - self.add_input('NodeSocketVector', 'Quaternion ' + str(len(self.inputs))) - self.add_input('NodeSocketFloat', 'Value ' + str(len(self.inputs))) - else: - while (len(self.inputs) < 2): - self.add_input('NodeSocketVector', 'Quaternion ' + str(len(self.inputs))) - if (select_current == 'DotProduct'): - self.add_output('NodeSocketFloat', 'Scalar') - - # 3 arguments: Lerp, Slerp, FromAxisAngle, FromEuler - if (self.get_count_in(select_current) == 3): - if (select_current == 'Lerp') or (select_current == 'Slerp'): - while (len(self.inputs) < 3): - self.add_input('NodeSocketVector', 'From') - self.add_input('NodeSocketVector', 'To') - self.add_input('NodeSocketFloat', 'T') - if (select_current == 'FromAxisAngle'): - self.add_input('NodeSocketVector', 'Quaternion') - self.add_input('NodeSocketVector', 'Axis') - self.add_input('NodeSocketFloat', 'Angle') - if (select_current == 'FromEuler'): - self.add_input('NodeSocketFloat', 'X') - self.add_input('NodeSocketFloat', 'Y') - self.add_input('NodeSocketFloat', 'Z') - - # 2 arguments: FromTo, FromMat, FromRotationMat, ToAxisAngle - if (self.get_count_in(select_current) == 2): - if (select_current == 'FromTo'): - self.add_input('NodeSocketVector', 'Vector ' + str(len(self.inputs))) - self.add_input('NodeSocketVector', 'Vector ' + str(len(self.inputs))) - if (select_current == 'FromMat') or (select_current == 'FromRotationMat'): - self.add_input('NodeSocketVector', 'Quaternion') - self.add_input('NodeSocketShader', 'Matrix') - if (select_current == 'ToAxisAngle'): - self.add_input('NodeSocketVector', 'Quaternion') - self.add_input('NodeSocketVector', 'Axis') - self.add_output('NodeSocketFloat', 'Angle') - # 1 argument: Module, Normalize, GetEuler - if (self.get_count_in(select_current) == 1): - self.add_input('NodeSocketVector', 'Quaternion') - if (select_current == 'Module'): - self.add_output('NodeSocketFloat', 'Module') + + if select_current in ('Add','Subtract','Multiply','DotProduct') \ + and select_prev in ('Add','Subtract','Multiply','DotProduct'): + pass # same as select_current==select_prev for the sockets + elif select_prev != select_current: + if select_current in ('Add','Subtract','Multiply','DotProduct'): + for i in range( max(len(self.inputs)//2 ,2) ): + self.ensure_input_socket(2*i, 'NodeSocketVector', 'Quaternion %d XYZ'%i) + self.ensure_input_socket(2*i+1, 'NodeSocketFloat', 'Quaternion %d W'%i, default_value=1.0) + if len(self.inputs)%1: + self.inputs.remove(self.inputs[len(self.inputs)-1]) + elif select_current == 'MultiplyFloats': + self.ensure_input_socket(0, 'NodeSocketVector', 'Quaternion XYZ') + self.ensure_input_socket(1, 'NodeSocketFloat', 'Quaternion W', default_value=1.0) + for i in range( max(len(self.inputs)-2 ,1) ): + self.ensure_input_socket(i+2, 'NodeSocketFloat', 'Value %d'%i) + elif select_current in ('Module', 'Normalize'): + self.ensure_input_socket(0, 'NodeSocketVector', 'Quaternion XYZ') + self.ensure_input_socket(1, 'NodeSocketFloat', 'Quaternion W', default_value=1.0) + while len(self.inputs)>2: + self.inputs.remove(self.inputs[2]) + else: + raise ValueError('Internal code of LNQuaternionMathNode failed to deal correctly with math operation "%s". Please report this to the developers.' %select_current) + + if select_current in ('Add','Subtract','Multiply','MultiplyFloats','Normalize'): + self.outputs[0].name = 'XYZ Out' + self.outputs[1].name = 'W Out' + else: + self.outputs[0].name = '[unused]' + self.outputs[1].name = 'Value Out' + self['property0'] = value + self['property0_proxy'] = value property0: EnumProperty( items = [('Add', 'Add', 'Add'), @@ -139,30 +119,35 @@ class QuaternionMathNode(ArmLogicTreeNode): ('Multiply', 'Multiply', 'Multiply'), ('MultiplyFloats', 'Multiply (Floats)', 'Multiply (Floats)'), ('Module', 'Module', 'Module'), - ('Normalize', 'Normalize', 'Normalize'), - ('Lerp', 'Lerp', 'Linearly interpolate'), - ('Slerp', 'Slerp', 'Spherical linear interpolation'), - ('FromTo', 'From To', 'From To'), - ('FromMat', 'From Mat', 'From Mat'), - ('FromRotationMat', 'From Rotation Mat', 'From Rotation Mat'), - ('ToAxisAngle', 'To Axis Angle', 'To Axis Angle'), - ('FromAxisAngle', 'From Axis Angle', 'From Axis Angle'), - ('FromEuler', 'From Euler', 'From Euler'), - ('GetEuler', 'To Euler', 'To Euler')], - name='', default='Add', set=set_enum, get=get_enum) + ('Normalize', 'Normalize', 'Normalize'), #], + # NOTE: the unused parts need to exist to be read from an old version from the node. + # this is so dumb… + ('Lerp', 'DO NOT USE',''), + ('Slerp', 'DO NOT USE',''), + ('FromTo', 'DO NOT USE',''), + ('FromMat', 'DO NOT USE',''), + ('FromRotationMat', 'DO NOT USE',''), + ('ToAxisAngle', 'DO NOT USE',''), + ('FromAxisAngle', 'DO NOT USE',''), + ('FromEuler', 'DO NOT USE',''), + ('GetEuler', 'DO NOT USE','')], + name='', default='Add') #, set=set_enum, get=get_enum) def __init__(self): + super(QuaternionMathNode, self).__init__() array_nodes[str(id(self))] = self def init(self, context): super(QuaternionMathNode, self).init(context) - self.add_input('NodeSocketVector', 'Quaternion 0', default_value=[0.0, 0.0, 0.0]) - self.add_input('NodeSocketVector', 'Quaternion 1', default_value=[0.0, 0.0, 0.0]) - self.add_output('NodeSocketVector', 'Result') + self.add_input('NodeSocketVector', 'Quaternion 0 XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_input('NodeSocketFloat', 'Quaternion 0 W', default_value=1) + self.add_input('NodeSocketVector', 'Quaternion 1 XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_input('NodeSocketFloat', 'Quaternion 1 W', default_value=1) + self.add_output('NodeSocketVector', 'Result XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_output('NodeSocketFloat', 'Result W', default_value=1) def draw_buttons(self, context, layout): - layout.prop(self, 'property1') # Separator Out - layout.prop(self, 'property0') # Operation + layout.prop(self, 'property0_proxy') # Operation # Buttons if (self.get_count_in(self.property0) == 0): row = layout.row(align=True) @@ -170,15 +155,201 @@ class QuaternionMathNode(ArmLogicTreeNode): op = column.operator('arm.node_add_input', text='Add Value', icon='PLUS', emboss=True) op.node_index = str(id(self)) if (self.property0 == 'Add') or (self.property0 == 'Subtract') or (self.property0 == 'Multiply') or (self.property0 == 'DotProduct'): - op.name_format = 'Quaternion {0}' + op.name_format = 'Quaternion {0} XYZ;Quaternion {0} W' else: op.name_format = 'Value {0}' if (self.property0 == "MultiplyFloats"): op.socket_type = 'NodeSocketFloat' else: - op.socket_type = 'NodeSocketVector' + op.socket_type = 'NodeSocketVector;NodeSocketFloat' column = row.column(align=True) op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) op.node_index = str(id(self)) + if self.property0 != "MultiplyFloats": + op.count = 2 + op.min_inputs = 4 + else: + op.min_inputs = 2 if len(self.inputs) == 2: column.enabled = False + + + + + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + ret=[] + if self.property0 == 'GetEuler': + newself = node_tree.nodes.new('LNSeparateRotationNode') + ret.append(newself) + newself.property0='EulerAngles' + newself.property2='XZY' + newself.property1='Rad' + + for link in self.inputs[0].links: # 0 or 1 + node_tree.links.new(link.from_socket, newself.inputs[0]) + elif self.property0 == 'ToEuler': + newself = node_tree.nodes.new('LNRotationNode') + ret.append(newself) + preconv = node_tree.nodes.new('LNVectorNode') + ret.append(preconv) + newself.property0='EulerAngles' + newself.property2='XZY' + newself.property1='Rad' + node_tree.links.new(preconv.outputs[0], newself.inputs[0]) + + preconv.inputs[0].default_value = self.inputs[0].default_value + for link in self.inputs[0].links: # 0 or 1 + node_tree.links.new(link.from_socket, preconv.inputs[0]) + preconv.inputs[1].default_value = self.inputs[1].default_value + for link in self.inputs[1].links: # 0 or 1 + node_tree.links.new(link.from_socket, preconv.inputs[1]) + preconv.inputs[2].default_value = self.inputs[2].default_value + for link in self.inputs[2].links: # 0 or 1 + node_tree.links.new(link.from_socket, preconv.inputs[2]) + elif self.property0 == 'ToAxisAngle': + newself = node_tree.nodes.new('LNSeparateRotationNode') + ret.append(newself) + newself.property0='AxisAngle' + newself.property1='Rad' + + for link in self.inputs[0].links: # 0 or 1 + node_tree.links.new(link.from_node, newself.inputs[0]) + elif self.property0 == 'FromAxisAngle': + newself = node_tree.nodes.new('LNRotationNode') + ret.append(newself) + newself.property0='AxisAngle' + newself.property1='Rad' + + newself.inputs[0].default_value = self.inputs[1].default_value + for link in self.inputs[1].links: # 0 or 1 + node_tree.links.new(link.from_node, newself.inputs[0]) + newself.inputs[1].default_value = self.inputs[2].default_value + for link in self.inputs[2].links: # 0 or 1 + node_tree.links.new(link.from_node, newself.inputs[1]) + elif self.property0 in ('FromMat','FromRotationMat'): + newself = node_tree.nodes.new('LNSeparateTransformNode') + ret.append(newself) + for link in self.inputs[1].links: # 0 or 1 + node_tree.links.new(link.from_node, newself.inputs[0]) + + elif self.property0 in ('Lerp','Slerp','FromTo'): + newself = node_tree.nodes.new('LNRotationMathNode') + ret.append(newself) + newself.property0 = self.property0 + for in1, in2 in zip(self.inputs, newself.inputs): + if in1.bl_idname in ('NodeSocketFloat', 'NodeSocketVector'): + in2.default_value = in1.default_value + for link in in1.links: + node_tree.links.new(link.from_socket, in2) + + else: + newself = node_tree.nodes.new('LNQuaternionMathNode') + ret.append(newself) + newself.property0 = self.property0 + + # convert the inputs… this is going to be hard lmao. + i_in_1 = 0 + i_in_2 = 0 + while i_in_1 < len(self.inputs): + in1 = self.inputs[i_in_1] + if in1.bl_idname == 'NodeSocketVector': + # quaternion input: now two sockets, not one. + convnode = node_tree.nodes.new('LNSeparateRotationNode') + convnode.property0 = 'Quaternion' + ret.append(convnode) + node_tree.links.new(convnode.outputs[0], newself.inputs[i_in_2]) + node_tree.links.new(convnode.outputs[1], newself.inputs[i_in_2+1]) + for link in in1.links: + node_tree.links.new(link.from_socket, convnode.inputs[0]) + i_in_2 +=1 + i_in_1 +=2 + elif in1.bl_idname == 'NodeSocketfloat': + for link in in1.links: + node_tree.links.new(link.from_socket, newself.inputs[i_in_2]) + else: + raise ValueError('get_replacement_node() for is not LNQuaternionMathNode V1->V2 is not prepared to deal with an input socket of type %s. This is a bug to report to the developers' %in1.bl_idname) + + # #### now that the input has been dealt with, let's deal with the output. + + if self.property0 in ('FromEuler','FromMat','FromRotationMat','FromAxisAngle','Lerp','Slerp','FromTo'): + # the new self returns a rotation + for link in self.outputs[0].links: + node_tree.links.new(newself.outputs[0], link.to_socket) + elif self.property0 in ('DotProduct','Module'): + # new self returns a float + for link in self.outputs[1 + 4*int(self.property1)].links: + node_tree.links.new(newself.outputs[1], link.to_socket) + elif self.property0 in ('GetEuler', 'ToAxisAngle'): + # new self returns misc. + for link in self.outputs[0].links: + node_tree.links.new(newself.outputs[0], link.to_socket) + if self.property0 == 'ToAxisAngle': + for link in self.outputs[1 + 4*int(self.property1)].links: + node_tree.links.new(newself.outputs[1], link.to_socket) + if self.property1: + xlinks = self.outputs[1].links + ylinks = self.outputs[2].links + zlinks = self.outputs[3].links + if len(xlinks)>0 or len(ylinks)>0 or len(zlinks)>0: + conv = node_tree.nodes.new('LNSeparateVectorNode') + ret.append(conv) + node_tree.links.new(newself.outputs[0], conv.inputs[0]) + for link in xlinks: + node_tree.links.new(conv.outputs[0], link.to_socket) + for link in ylinks: + node_tree.links.new(conv.outputs[1], link.to_socket) + for link in zlinks: + node_tree.links.new(conv.outputs[2], link.to_socket) + else: + # new self returns a proper quaternion XYZ/W + outlinks = self.outputs[0].links + if len(outlinks)>0: + conv = node_tree.nodes.new('LNRotationNode') + conv.property0='Quaternion' + ret.append(conv) + node_tree.links.new(newself.outputs[0], conv.inputs[0]) + node_tree.links.new(newself.outputs[1], conv.inputs[1]) + for link in outlinks: + node_tree.links.new(conv.outputs[0], link.to_socket) + if self.property1: + for link in self.outputs[4].links: # for W + node_tree.links.new(newself.outputs[1], link.to_socket) + xlinks = self.outputs[1].links + ylinks = self.outputs[2].links + zlinks = self.outputs[3].links + if len(xlinks)>0 or len(ylinks)>0 or len(zlinks)>0: + conv = node_tree.nodes.new('LNSeparateVectorNode') + ret.append(conv) + node_tree.links.new(newself.outputs[0], conv.inputs[0]) + for link in xlinks: + node_tree.links.new(conv.outputs[0], link.to_socket) + for link in ylinks: + node_tree.links.new(conv.outputs[1], link.to_socket) + for link in zlinks: + node_tree.links.new(conv.outputs[2], link.to_socket) + + return ret + + + # note: keep property1, so that it is actually readable for node conversion. + property1: BoolProperty(name='DEPRECATED', default=False) + + # this is the version of property0 that is shown in the interface, + # even though the real property0 is the one used elsewhere. + # NOTE FOR FUTURE MAINTAINERS: the value of this proxy property does **not** matter, only the value of property0 does. + property0_proxy: EnumProperty( + items = [('Add', 'Add', 'Add'), + ('Subtract', 'Subtract', 'Subtract'), + ('DotProduct', 'Dot Product', 'Dot Product'), + ('Multiply', 'Multiply', 'Multiply'), + ('MultiplyFloats', 'Multiply (Floats)', 'Multiply (Floats)'), + ('Module', 'Module', 'Module'), + ('Normalize', 'Normalize', 'Normalize')], + name='', default='Add', set=set_enum, get=get_enum) diff --git a/blender/arm/logicnode/math/LN_rotation_math.py b/blender/arm/logicnode/math/LN_rotation_math.py index 50d2abd3..dcf59883 100644 --- a/blender/arm/logicnode/math/LN_rotation_math.py +++ b/blender/arm/logicnode/math/LN_rotation_math.py @@ -4,6 +4,7 @@ class QuaternionMathNode(ArmLogicTreeNode): """Mathematical operations on rotations.""" bl_idname = 'LNRotationMathNode' bl_label = 'Rotation Math' + bl_description = 'Mathematical operations that can be performed on rotations, no matter their internal representation' arm_section = 'quaternions' arm_version = 1 @@ -97,7 +98,7 @@ class QuaternionMathNode(ArmLogicTreeNode): 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'), + #('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'), diff --git a/blender/arm/logicnode/math/LN_separate_quaternion.py b/blender/arm/logicnode/math/LN_separate_quaternion.py index fcd9abf6..21f18946 100644 --- a/blender/arm/logicnode/math/LN_separate_quaternion.py +++ b/blender/arm/logicnode/math/LN_separate_quaternion.py @@ -1,11 +1,14 @@ from arm.logicnode.arm_nodes import * +@deprecated(message='Do not use quaternion sockets') class SeparateQuaternionNode(ArmLogicTreeNode): """Splits the given quaternion into X, Y, Z and W.""" bl_idname = 'LNSeparateQuaternionNode' - bl_label = "Separate Quaternion" + bl_label = "Separate Quaternion (do not use: quaternions sockets have been phased out entirely)" + bl_description = "Separate a quaternion object (transported through a vector socket) into its four compoents." arm_section = 'quaternions' - arm_version = 1 + arm_version = 2 # deprecate + def init(self, context): super(SeparateQuaternionNode, self).init(context) @@ -15,3 +18,29 @@ class SeparateQuaternionNode(ArmLogicTreeNode): self.add_output('NodeSocketFloat', 'Y') self.add_output('NodeSocketFloat', 'Z') self.add_output('NodeSocketFloat', 'W') + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + # transition from version 1 to version 2[deprecated] + newself = node_tree.nodes.new('LNSeparateRotationNode') + separator = node_tree.nodes.new('LNSeparateVectorNode') + + newself.property0 = 'Quaternion' + newself.property1 = 'Rad' # bogus + newself.property2 = 'XYZ' # bogus + + for link in self.inputs[0].links: + node_tree.links.new(link.from_socket, newself.inputs[0]) + node_tree.links.new(newself.outputs[0], separator.inputs[0]) + for link in self.outputs[0].links: + node_tree.links.new(separator.outputs[0], link.to_socket) + for link in self.outputs[1].links: + node_tree.links.new(separator.outputs[1], link.to_socket) + for link in self.outputs[2].links: + node_tree.links.new(separator.outputs[2], link.to_socket) + for link in self.outputs[3].links: + node_tree.links.new(newself.outputs[1], link.to_socket) + return [newself, separator] diff --git a/blender/arm/logicnode/transform/LN_get_object_rotation.py b/blender/arm/logicnode/transform/LN_get_object_rotation.py index 9ba23553..86cce2f4 100644 --- a/blender/arm/logicnode/transform/LN_get_object_rotation.py +++ b/blender/arm/logicnode/transform/LN_get_object_rotation.py @@ -5,7 +5,7 @@ class GetRotationNode(ArmLogicTreeNode): bl_idname = 'LNGetRotationNode' bl_label = 'Get Object Rotation' arm_section = 'rotation' - arm_version = 1 + arm_version = 2 def init(self, context): super(GetRotationNode, self).init(context) @@ -19,3 +19,72 @@ class GetRotationNode(ArmLogicTreeNode): items = [('Local', 'Local', 'Local'), ('Global', 'Global', 'Global')], name='', default='Local') + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + + newself = self.id_data.nodes.new('LNGetRotationNode') + newself.property0 = 'Local' + newnodes = [newself] + + if len(self.outputs[0].links)>0: + # euler (radians) needed + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "EulerAngles" + converter.property1 = "Rad" + converter.property2 = "XZY" + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[0].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + if len(self.outputs[4].links)>0 or len(self.outputs[5].links)>0: + # quaternion needed + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "Quaternion" + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[4].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + for link in self.outputs[5].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + + if len(self.outputs[1].links)>0 or len(self.outputs[2].links)>0 or len(self.outputs[3].links)>0: + # axis/angle needed + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "AxisAngle" + converter.property1 = "Rad" + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[1].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + if len(self.outputs[3].links)==0 and len(self.outputs[2].links)==0: + pass + elif len(self.outputs[3].links)==0: + for link in self.outputs[2].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + elif len(self.outputs[2].links)==0: + converter.property1 = 'Deg' + for link in self.outputs[3].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + else: + for link in self.outputs[2].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "AxisAngle" + converter.property1 = "Deg" + converter.property2 = "XYZ" # bogus + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[3].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + + return newnodes diff --git a/blender/arm/logicnode/transform/LN_look_at.py b/blender/arm/logicnode/transform/LN_look_at.py index 87fdf03f..669d2f8a 100644 --- a/blender/arm/logicnode/transform/LN_look_at.py +++ b/blender/arm/logicnode/transform/LN_look_at.py @@ -5,7 +5,7 @@ class LookAtNode(ArmLogicTreeNode): bl_idname = 'LNLookAtNode' bl_label = 'Look At' arm_section = 'rotation' - arm_version = 1 + arm_version = 2 property0: EnumProperty( items = [('X', ' X', 'X'), @@ -25,3 +25,23 @@ class LookAtNode(ArmLogicTreeNode): def draw_buttons(self, context, layout): layout.prop(self, 'property0') + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + newself = self.id_data.nodes.new('LNLookAtNode') + converter = self.id_data.nodes.new('LNSeparateRotationNode') + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + converter.property0 = 'EulerAngles' + converter.property1 = 'Rad' + converter.property2 = 'XZY' + for link in self.outputs[0].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + return [newself, converter] diff --git a/blender/arm/logicnode/transform/LN_rotate_object.py b/blender/arm/logicnode/transform/LN_rotate_object.py index c49f68f0..b43cafc0 100644 --- a/blender/arm/logicnode/transform/LN_rotate_object.py +++ b/blender/arm/logicnode/transform/LN_rotate_object.py @@ -1,11 +1,12 @@ from arm.logicnode.arm_nodes import * +from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation class RotateObjectNode(ArmLogicTreeNode): """Rotates the given object.""" bl_idname = 'LNRotateObjectNode' bl_label = 'Rotate Object' arm_section = 'rotation' - arm_version = 1 + arm_version = 2 def init(self, context): super().init(context) @@ -20,5 +21,59 @@ class RotateObjectNode(ArmLogicTreeNode): property0: EnumProperty( 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')], + ('Global', 'Global/Parent F.O.R.', + 'Frame of reference oriented with the object\'s parent or the world')], name='', default='Local') + + + + + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + + newself = self.id_data.nodes.new('LNRotateObjectNode') + inputnode = self.id_data.nodes.new('LNRotationNode') + self.id_data.links.new(inputnode.outputs[0], newself.inputs[2]) + newself.inputs[1].default_value_raw = self.inputs[1].default_value_raw + inputnode.inputs[0].default_value = self.inputs[2].default_value + inputnode.inputs[1].default_value = self.inputs[3].default_value + + if len(self.inputs[0].links) >0: + self.id_data.links.new(self.inputs[0].links[0].from_socket, newself.inputs[0]) + if len(self.inputs[1].links) >0: + self.id_data.links.new(self.inputs[1].links[0].from_socket, newself.inputs[1]) + if len(self.inputs[2].links) >0: + self.id_data.links.new(self.inputs[2].links[0].from_socket, inputnode.inputs[0]) + if len(self.inputs[3].links) >0: + self.id_data.links.new(self.inputs[3].links[0].from_socket, inputnode.inputs[1]) + + # first, convert the default value + if self.property0 == 'Quaternion': + inputnode.property0 = 'Quaternion' + elif self.property0 == 'Euler Angles': + inputnode.property0 = 'EulerAngles' + inputnode.property1 = 'Rad' + inputnode.property2 = 'XZY' # legacy order + else: # starts with "Angle Axies" + inputnode.property0 = 'AxisAngle' + if 'Degrees' in self.property0: + inputnode.property1 = 'Deg' + else: + inputnode.property1 = 'Rad' + quat = Rotation.convert_to_quaternion( + self.inputs[2].default_value, + self.inputs[3].default_value, + inputnode.property0, + inputnode.property1, + inputnode.property2 + ) + newself.inputs[2].default_value_raw = quat + return [newself, inputnode] diff --git a/blender/arm/logicnode/transform/LN_separate_rotation.py b/blender/arm/logicnode/transform/LN_separate_rotation.py index 0111d5a0..2085951a 100644 --- a/blender/arm/logicnode/transform/LN_separate_rotation.py +++ b/blender/arm/logicnode/transform/LN_separate_rotation.py @@ -40,7 +40,7 @@ class SeparateRotationNode(ArmLogicTreeNode): property0: EnumProperty( items = [('EulerAngles', 'Euler Angles', 'Euler Angles'), ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), - ('Quat', 'Quaternion', 'Quaternion')], + ('Quaternion', 'Quaternion', 'Quaternion')], name='', default='EulerAngles', update=on_property_update) diff --git a/blender/arm/logicnode/transform/LN_separate_transform.py b/blender/arm/logicnode/transform/LN_separate_transform.py index 6ab0ade8..0a96747b 100644 --- a/blender/arm/logicnode/transform/LN_separate_transform.py +++ b/blender/arm/logicnode/transform/LN_separate_transform.py @@ -4,7 +4,7 @@ class SeparateTransformNode(ArmLogicTreeNode): """Separates the transform of the given object.""" bl_idname = 'LNSeparateTransformNode' bl_label = 'Separate Transform' - arm_version = 1 + arm_version = 2 def init(self, context): super(SeparateTransformNode, self).init(context) @@ -13,3 +13,34 @@ class SeparateTransformNode(ArmLogicTreeNode): self.add_output('NodeSocketVector', 'Location') self.add_output('ArmNodeSocketRotation', 'Rotation') self.add_output('NodeSocketVector', 'Scale') + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + newself = self.id_data.nodes.new('LNSeparateTransformNode') + for link in self.outputs[0].links: + self.id_data.links.new(newself.outputs[0], link.to_socket) + for link in self.outputs[2].links: + self.id_data.links.new(newself.outputs[2], link.to_socket) + for link in self.inputs[0].links: + self.id_data.links.new(link.from_socket, newself.inputs[0]) + + ret = [newself] + rot_links = self.outputs[1].links + if len(rot_links) >0: + converter = self.id_data.nodes.new('LNSeparateRotationNode') + ret.append(converter) + self.id_data.links.new(newself.outputs[1], converter.inputs[0]) + converter.property0 = 'EulerAngles' + converter.property1 = 'Rad' + converter.property2 = 'XZY' + for link in rot_links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + return ret diff --git a/blender/arm/logicnode/transform/LN_set_object_rotation.py b/blender/arm/logicnode/transform/LN_set_object_rotation.py index 555e3229..1223cdf8 100644 --- a/blender/arm/logicnode/transform/LN_set_object_rotation.py +++ b/blender/arm/logicnode/transform/LN_set_object_rotation.py @@ -1,11 +1,12 @@ from arm.logicnode.arm_nodes import * +from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation class SetRotationNode(ArmLogicTreeNode): """Sets the rotation of the given object.""" bl_idname = 'LNSetRotationNode' bl_label = 'Set Object Rotation' arm_section = 'rotation' - arm_version = 1 + arm_version = 2 def init(self, context): @@ -16,10 +17,61 @@ class SetRotationNode(ArmLogicTreeNode): self.add_output('ArmNodeSocketAction', 'Out') -# def draw_buttons(self, context, layout): -# layout.prop(self, 'property0') -# property0: EnumProperty( -# items = [('Local', 'Local', 'Local'), -# ('Global', 'Global', 'Global')], -# name='', default='Local') + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + + + newself = self.id_data.nodes.new('LNRotateObjectNode') + inputnode = self.id_data.nodes.new('LNRotationNode') + self.id_data.links.new(inputnode.outputs[0], newself.inputs[2]) + newself.inputs[1].default_value_raw = self.inputs[1].default_value_raw + inputnode.inputs[0].default_value = self.inputs[2].default_value + inputnode.inputs[1].default_value = self.inputs[3].default_value + + if len(self.inputs[0].links) >0: + self.id_data.links.new(self.inputs[0].links[0].from_socket, newself.inputs[0]) + if len(self.inputs[1].links) >0: + self.id_data.links.new(self.inputs[1].links[0].from_socket, newself.inputs[1]) + if len(self.inputs[2].links) >0: + self.id_data.links.new(self.inputs[2].links[0].from_socket, inputnode.inputs[0]) + if len(self.inputs[3].links) >0: + self.id_data.links.new(self.inputs[3].links[0].from_socket, inputnode.inputs[1]) + + # first, convert the default value + if self.property0 == 'Quaternion': + inputnode.property0 = 'Quaternion' + elif self.property0 == 'Euler Angles': + inputnode.property0 = 'EulerAngles' + inputnode.property1 = 'Rad' + inputnode.property2 = 'XZY' # legacy order + elif self.property0.startswith("Angle Axies "): + inputnode.property0 = 'AxisAngle' + if 'Degrees' in self.property0: + inputnode.property1 = 'Deg' + else: + inputnode.property1 = 'Rad' + else: + raise ValueError('nonsensical value {:s} for property0 in SetObjectRotationNode/V1. please report this to the devs.'.format(self.property0)) + quat = Rotation.convert_to_quaternion( + self.inputs[2].default_value, + self.inputs[3].default_value, + inputnode.property0, + inputnode.property1, + inputnode.property2 + ) + newself.inputs[2].default_value_raw = quat + return [newself, inputnode] + + # note: this is unused, but kept here so that the 'property0' field can be read during node replacement + property0: EnumProperty( + items = [('Euler Angles', '',''), + ('Angle Axies (Radians)', '', ''), + ('Angle Axies (Degrees)', '', ''), + ('Quaternion', '', '')], + name='', default='Euler Angles') diff --git a/blender/arm/logicnode/transform/LN_transform.py b/blender/arm/logicnode/transform/LN_transform.py index 1087caba..a4302b97 100644 --- a/blender/arm/logicnode/transform/LN_transform.py +++ b/blender/arm/logicnode/transform/LN_transform.py @@ -4,7 +4,7 @@ class TransformNode(ArmLogicTreeNode): """Stores the location, rotation and scale values as a transform.""" bl_idname = 'LNTransformNode' bl_label = 'Transform' - arm_version = 1 + arm_version = 2 def init(self, context): super(TransformNode, self).init(context) @@ -13,3 +13,34 @@ class TransformNode(ArmLogicTreeNode): self.add_input('NodeSocketVector', 'Scale', default_value=[1.0, 1.0, 1.0]) self.add_output('NodeSocketShader', 'Transform', is_var=True) + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + newself = self.id_data.nodes.new('LNTransformNode') + ret = [newself] + + for link in self.inputs[0].links: + self.id_data.links.new(link.from_socket, newself.inputs[0]) + for link in self.inputs[2].links: + self.id_data.links.new(link.from_socket, newself.inputs[2]) + for link in self.outputs[0].links: + self.id_data.links.new(newself.outputs[0], link.to_socket) + + links_rot = self.inputs[1].links + if len(links_rot) > 0: + converter = self.id_data.nodes.new('LNRotationNode') + self.id_data.links.new(converter.outputs[0], newself.inputs[1]) + converter.property0 = 'EulerAngles' + converter.property1 = 'Rad' + converter.property2 = 'XZY' + ret.append(converter) + for link in links_rot: + self.id_data.links.new(link.from_socket, converter.inputs[0]) + + return ret diff --git a/blender/arm/logicnode/variable/LN_quaternion.py b/blender/arm/logicnode/variable/LN_quaternion.py index 8f91465d..25a23a8f 100644 --- a/blender/arm/logicnode/variable/LN_quaternion.py +++ b/blender/arm/logicnode/variable/LN_quaternion.py @@ -1,11 +1,14 @@ from arm.logicnode.arm_nodes import * +from mathutils import Vector +@deprecated(message='Do not use quaternion sockets') class QuaternionNode(ArmLogicTreeNode): """TO DO.""" bl_idname = 'LNQuaternionNode' bl_label = 'Quaternion' + bl_label = 'Create a quaternion variable (transported through a vector socket)' arm_section = 'quaternions' - arm_version = 1 + arm_version = 2 # deprecate def init(self, context): super(QuaternionNode, self).init(context) @@ -17,3 +20,57 @@ class QuaternionNode(ArmLogicTreeNode): self.add_output('NodeSocketVector', 'Quaternion') self.add_output('NodeSocketVector', 'XYZ') self.add_output('NodeSocketFloat', 'W') + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + # transition from version 1 to version 2[deprecated] + + + newnodes = [] + + rawlinks = self.outputs[0].links + xyzlinks = self.outputs[1].links + wlinks = self.outputs[2].links + if len(rawlinks)>0 or len(xyzlinks)>0: + xyzcomb = node_tree.nodes.new('LNVectorNode') + newnodes.append(xyzcomb) + + xyzcomb.inputs[0].default_value = self.inputs[0].default_value + xyzcomb.inputs[1].default_value = self.inputs[1].default_value + xyzcomb.inputs[2].default_value = self.inputs[2].default_value + for link in self.inputs[0].links: + node_tree.links.new(link.from_socket, xyzcomb.inputs[0]) + for link in self.inputs[1].links: + node_tree.links.new(link.from_socket, xyzcomb.inputs[1]) + for link in self.inputs[2].links: + node_tree.links.new(link.from_socket, xyzcomb.inputs[2]) + + for link in xyzlinks: + node_tree.links.new(xyzcomb.outputs[0], link.to_socket) + if len(rawlinks)>0: + rotnode = node_tree.nodes.new('LNRotationNode') + newnodes.append(rotnode) + rotnode.property0 = 'Quaternion' + rotnode.inputs[0].default_value = Vector( + (self.inputs[0].default_value, + self.inputs[1].default_value, + self.inputs[2].default_value)) + rotnode.inputs[1].default_value = self.inputs[3].default_value + node_tree.links.new(xyzcomb.outputs[0], rotnode.inputs[0]) + for link in self.inputs[3].links: # 0 or 1 + node_tree.links.new(link.from_socket, rotnode.inputs[1]) + for link in rawlinks: + node_tree.links.new(rotnode.outputs[0], link.to_socket) + + if len(self.inputs[3].links)>0: + fromval = self.inputs[3].links[0].from_socket + for link in self.outputs[2].links: + node_tree.links.new(fromval, link.to_socket) + else: + for link in self.outputs[2].links: + link.to_socket.default_value = self.inputs[3].default_value + + return newnodes diff --git a/blender/arm/logicnode/variable/LN_rotation.py b/blender/arm/logicnode/variable/LN_rotation.py index ac832cea..a5ee0a2d 100644 --- a/blender/arm/logicnode/variable/LN_rotation.py +++ b/blender/arm/logicnode/variable/LN_rotation.py @@ -4,6 +4,7 @@ class RotationNode(ArmLogicTreeNode): """A rotation, created from one of its possible mathematical representations""" bl_idname = 'LNRotationNode' bl_label = 'Rotation' + bl_description = 'Create a Rotation object, describing the difference between two orientations (internally represented as a quaternion for efficiency)' #arm_section = 'rotation' arm_version = 1 From 1d0a6d7955912dcf4987f382d580aaf135ae5b7f Mon Sep 17 00:00:00 2001 From: niacdoial Date: Tue, 17 Aug 2021 19:29:35 +0200 Subject: [PATCH 3/5] Did the haxe part of the ongoing update. Also fixed a ton of bugs in the python part. NOTE: this requires a yet-to-be-done commit to Iron to work. --- .../armory/logicnode/QuaternionMathNode.hx | 187 +++++++----------- Sources/armory/logicnode/RotationMathNode.hx | 20 +- Sources/armory/logicnode/RotationNode.hx | 3 + .../armory/logicnode/SeparateRotationNode.hx | 2 +- Sources/armory/logicnode/SetRotationNode.hx | 2 + blender/arm/logicnode/arm_nodes.py | 9 +- blender/arm/logicnode/arm_sockets.py | 12 +- .../{variable => deprecated}/LN_quaternion.py | 0 .../LN_separate_quaternion.py | 0 .../arm/logicnode/math/LN_quaternion_math.py | 54 +++-- .../arm/logicnode/math/LN_rotation_math.py | 19 +- .../transform/LN_separate_rotation.py | 6 +- .../{transform => variable}/LN_transform.py | 0 13 files changed, 151 insertions(+), 163 deletions(-) rename blender/arm/logicnode/{variable => deprecated}/LN_quaternion.py (100%) rename blender/arm/logicnode/{math => deprecated}/LN_separate_quaternion.py (100%) rename blender/arm/logicnode/{transform => variable}/LN_transform.py (100%) diff --git a/Sources/armory/logicnode/QuaternionMathNode.hx b/Sources/armory/logicnode/QuaternionMathNode.hx index cb83f368..a2f21018 100644 --- a/Sources/armory/logicnode/QuaternionMathNode.hx +++ b/Sources/armory/logicnode/QuaternionMathNode.hx @@ -19,7 +19,7 @@ class QuaternionMathNode extends LogicNode { override function get(from: Int): Dynamic { switch (property0) { - // 1 argument: Module, Normalize, GetEuler + // 1 argument: Module, Normalize case "Module": { var q: Quat = inputs[0].get(); if (q == null) return null; @@ -32,159 +32,106 @@ class QuaternionMathNode extends LogicNode { res_q.setFrom(q); res_q = res_q.normalize(); } - case "GetEuler": { - var q: Quat = inputs[0].get(); - if (q == null) return null; - res_q.setFrom(q); - res_v = res_q.getEuler(); - } - // 2 arguments: FromTo, FromMat, FromRotationMat, ToAxisAngle - 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 "FromMat": { - var q: Quat = inputs[0].get(); - var m: Mat4 = inputs[1].get(); - if ((q == null) || (m == null)) return null; - res_q.setFrom(q); - res_q = res_q.fromMat(m); - } - case "FromRotationMat": { - var q: Quat = inputs[0].get(); - var m: Mat4 = inputs[1].get(); - if ((q == null) || (m == null)) return null; - res_q.setFrom(q); - res_q = res_q.fromRotationMat(m); - } - case "ToAxisAngle": { - var q: Quat = inputs[0].get(); - var v: Vec4 = inputs[1].get(); - if ((q == null) || (v == null)) return null; - res_q.setFrom(q); - res_f = res_q.toAxisAngle(v); - } - // # 3 arguments: Lerp, Slerp, FromAxisAngle, FromEuler - case "Lerp": { - var from: Quat = inputs[0].get(); - var to: Quat = inputs[1].get(); - var f: Float = inputs[2].get(); - if ((from == null) || (to == null)) return null; - res_q = res_q.lerp(from, to, f); - } - case "Slerp": { - var from: Quat = inputs[0].get(); - var to: Quat = inputs[1].get(); - var f: Float = inputs[2].get(); - if ((from == null) || (to == null)) return null; - res_q = res_q.slerp(from, to, f); - } - case "FromAxisAngle": { - var q: Quat = inputs[0].get(); - var axis: Vec4 = inputs[1].get(); - var angle: Float = inputs[2].get(); - if ((q == null) || (axis == null)) return null; - res_q.setFrom(q); - res_q = res_q.fromAxisAngle(axis, angle); - } - case "FromEuler": { - var x: Float = inputs[0].get(); - var y: Float = inputs[1].get(); - var z: Float = inputs[2].get(); - res_q = res_q.fromEuler(x, y, z); - } // Many arguments: Add, Subtract, DotProduct, Multiply case "Add": { - var q: Quat = inputs[0].get(); - if (q == null) return null; - res_q.setFrom(q); - var q2 = new Quat(); + res_v = inputs[0].get(); + res_f = inputs[1].get(); + if (res_v == null || res_f == null) return null; + + res_q.set(res_v.x, res_v.y, res_v.z, res_f); var i = 1; - while (i < inputs.length) { - q2 = inputs[i].get(); - if (q2 == null) return null; - res_q.add(q2); + while (2*i+1 < inputs.length) { + res_v = inputs[2*i].get(); + res_f = inputs[2*i+1].get(); + if (res_v == null || res_f == null) return null; + res_q.x += res_v.x; + res_q.y += res_v.y; + res_q.z += res_v.z; + res_q.w += res_f; i++; } } case "Subtract": { - var q: Quat = inputs[0].get(); - if (q == null) return null; - res_q.setFrom(q); - var q2 = new Quat(); + res_v = inputs[0].get(); + res_f = inputs[1].get(); + if (res_v == null || res_f == null) return null; + + res_q.set(res_v.x, res_v.y, res_v.z, res_f); var i = 1; - while (i < inputs.length) { - q2 = inputs[i].get(); - if (q2 == null) return null; - res_q.sub(q2); + while (2*i+1 < inputs.length) { + res_v = inputs[2*i].get(); + res_f = inputs[2*i+1].get(); + if (res_v == null || res_f == null) return null; + res_q.x -= res_v.x; + res_q.y -= res_v.y; + res_q.z -= res_v.z; + res_q.w -= res_f; i++; } } case "Multiply": { - var q: Quat = inputs[0].get(); - if (q == null) return null; - res_q.setFrom(q); - var q2 = new Quat(); + res_v = inputs[0].get(); + res_f = inputs[1].get(); + if (res_v == null || res_f == null) return null; + + res_q.set(res_v.x, res_v.y, res_v.z, res_f); var i = 1; - while (i < inputs.length) { - q2 = inputs[i].get(); - if (q2 == null) return null; - res_q.mult(q2); + while (2*i+1 < inputs.length) { + res_v = inputs[2*i].get(); + res_f = inputs[2*i+1].get(); + if (res_v == null || res_f == null) return null; + var temp_q = new Quat(res_v.x, res_v.y, res_v.z, res_f); + res_q.mult(temp_q); i++; } } case "MultiplyFloats": { - var q: Quat = inputs[0].get(); - if (q == null) return null; - res_q.setFrom(q); + res_v = inputs[0].get(); + res_f = inputs[1].get(); + if (res_v == null || res_f == null) return null; + + res_q.set(res_v.x, res_v.y, res_v.z, res_f); var f: Float = 1.0; - var i = 1; + var i = 2; while (i < inputs.length) { - f = inputs[i].get(); - res_q.scale(f); + f *= inputs[i].get(); + if (f == null) return null; i++; } + res_q.scale(f); } - case "DotProduct": { - var q: Quat = inputs[0].get(); - if (q == null) return null; - res_q.setFrom(q); - var q2 = new Quat(); + case "DotProduct": { // what this does with more than 2 terms is not *remotely* intuitive. Heck, you could consider it a footgun! + + res_v = inputs[0].get(); + var temp_f = inputs[1].get(); + if (res_v == null || temp_f == null) return null; + + res_q.set(res_v.x, res_v.y, res_v.z, temp_f); var i = 1; - while (i < inputs.length) { - q2 = inputs[i].get(); - if (q2 == null) return null; - res_f = res_q.dot(q2); + while (2*i+1 < inputs.length) { + res_v = inputs[2*i].get(); + temp_f = inputs[2*i+1].get(); + if (res_v == null || temp_f == null) return null; + var temp_q = new Quat(res_v.x, res_v.y, res_v.z, temp_f); + res_f = res_q.dot(temp_q); res_q.set(res_f, res_f, res_f, res_f); i++; } } } - // Return and check separator switch (from) { case 0: { - if (property0 == 'GetEuler') - return res_v; - else - return res_q; + return res_q; } case 1: - if (property1) { - return res_q.x; - } else { + if (property0 == "DotProduct" || property0 == "Module") { return res_f; + } else { + return null; } - case 2: - if (property1) return res_q.y; - case 3: - if (property1) return res_q.z; - case 4: - if (property1) return res_q.w; - case 5: - if (property1) return res_f; + default: { + return null; + } } - return null; } } \ No newline at end of file diff --git a/Sources/armory/logicnode/RotationMathNode.hx b/Sources/armory/logicnode/RotationMathNode.hx index 315c29ad..2af24da5 100644 --- a/Sources/armory/logicnode/RotationMathNode.hx +++ b/Sources/armory/logicnode/RotationMathNode.hx @@ -17,17 +17,21 @@ class RotationMathNode extends LogicNode { } override function get(from: Int): Dynamic { - var q: Quat = inputs[0].get(); - if (q==null) return null; + //var q: Quat = inputs[0].get(); + //if (q==null) return null; - var res_q: Quat = new Quat(); + //var res_q: Quat = new Quat(); switch (property0) { // 1 argument: Normalize, Inverse case "Normalize": { + var q: Quat = inputs[0].get(); + if (q==null) return null; res_q.setFrom(q); res_q = res_q.normalize(); } case "Inverse": { + var q: Quat = inputs[0].get(); + if (q==null) return null; 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; @@ -72,17 +76,19 @@ class RotationMathNode extends LogicNode { // # 3 arguments: Lerp, Slerp, FromAxisAngle, FromEuler case "Lerp": { //var from = q; + var from: Quat = inputs[0].get(); 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); + if ((from == null) || (f == null) || (to == null)) return null; + res_q = res_q.lerp(from, to, f); } case "Slerp": { //var from = q; + var from:Quat = inputs[0].get(); 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); + if ((from == null) || (f == null) || (to == null)) return null; + res_q = res_q.slerp(from, to, f); } } return res_q; diff --git a/Sources/armory/logicnode/RotationNode.hx b/Sources/armory/logicnode/RotationNode.hx index c3b07b95..893a1c78 100644 --- a/Sources/armory/logicnode/RotationNode.hx +++ b/Sources/armory/logicnode/RotationNode.hx @@ -38,6 +38,9 @@ class RotationNode extends LogicNode { //var inp0 = inputs[0].get(); //var inp //if (inputs[0].get()) + if (inputs.length == 0){ + return this.value; + } switch (property0){ case "Quaternion": { diff --git a/Sources/armory/logicnode/SeparateRotationNode.hx b/Sources/armory/logicnode/SeparateRotationNode.hx index a11af18a..0ce4f78c 100644 --- a/Sources/armory/logicnode/SeparateRotationNode.hx +++ b/Sources/armory/logicnode/SeparateRotationNode.hx @@ -50,7 +50,7 @@ class SeparateRotationNode extends LogicNode { case "Deg": return toDEG*aa_angle_cache; } } - case "Quat": + case "Quaternion": switch(from){ case 0: return new Vec4(q.x,q.y,q.z); case 1: return q.w; diff --git a/Sources/armory/logicnode/SetRotationNode.hx b/Sources/armory/logicnode/SetRotationNode.hx index 0f820d20..7126fbff 100644 --- a/Sources/armory/logicnode/SetRotationNode.hx +++ b/Sources/armory/logicnode/SetRotationNode.hx @@ -7,6 +7,8 @@ import armory.trait.physics.RigidBody; class SetRotationNode extends LogicNode { + public var property0: String; // UNUSED + public function new(tree: LogicTree) { super(tree); } diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index 877c01b3..a516d734 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -6,6 +6,7 @@ from typing import OrderedDict as ODict # Prevent naming conflicts import bpy.types from bpy.props import * from nodeitems_utils import NodeItem +from arm.logicnode.arm_sockets import ArmCustomSocket # Pass NodeReplacment forward to individual node modules that import arm_nodes from arm.logicnode.replacement import NodeReplacement @@ -83,7 +84,13 @@ class ArmLogicTreeNode(bpy.types.Node): socket = self.inputs.new(socket_type, socket_name) if default_value is not None: - socket.default_value = default_value + if isinstance(socket, ArmCustomSocket): + if socket.arm_socket_type != 'NONE': + socket.default_value_raw = default_value + else: + raise ValueError('specified a default value for an input node that doesn\'t accept one') + else: + socket.default_value = default_value if is_var and not socket.display_shape.endswith('_DOT'): socket.display_shape += '_DOT' diff --git a/blender/arm/logicnode/arm_sockets.py b/blender/arm/logicnode/arm_sockets.py index a0f702fd..8a165e98 100644 --- a/blender/arm/logicnode/arm_sockets.py +++ b/blender/arm/logicnode/arm_sockets.py @@ -97,7 +97,7 @@ class ArmRotationSocket(ArmCustomSocket): def on_mode_update(self, context): - if self.default_value_mode == 'Quat': + if self.default_value_mode == 'Quaternion': summ = abs(self.default_value_s0) summ+= abs(self.default_value_s1) summ+= abs(self.default_value_s2) @@ -116,10 +116,10 @@ class ArmRotationSocket(ArmCustomSocket): def convert_to_quaternion(part1,part2,param1,param2,param3): """converts a representation of rotation into a quaternion. ``part1`` is a vector, ``part2`` is a scalar or None, - ``param1`` is in ('Quat', 'EulerAngles', 'AxisAngle'), + ``param1`` is in ('Quaternion', 'EulerAngles', 'AxisAngle'), ``param2`` is in ('Rad','Deg') for both EulerAngles and AxisAngle, ``param3`` is a len-3 string like "XYZ", for EulerAngles """ - if param1 == 'Quat': + if param1=='Quaternion': qx, qy, qz = part1[0], part1[1], part1[2] qw = part2 # need to normalize the quaternion for a rotation (having it be 0 is not an option) @@ -183,7 +183,7 @@ class ArmRotationSocket(ArmCustomSocket): part1, self.default_value_s3, self.default_value_mode, - self.defualt_value_unit, + self.default_value_unit, self.default_value_order ) @@ -205,7 +205,7 @@ class ArmRotationSocket(ArmCustomSocket): 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': + elif self.default_value_mode == 'Quaternion': coll.prop(self, 'default_value_s0', text='X') coll.prop(self, 'default_value_s1', text='Y') coll.prop(self, 'default_value_s2', text='Z') @@ -223,7 +223,7 @@ class ArmRotationSocket(ArmCustomSocket): default_value_mode: EnumProperty( items=[('EulerAngles', 'Euler Angles', 'Euler Angles'), ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), - ('Quat', 'Quaternion', 'Quaternion')], + ('Quaternion', 'Quaternion', 'Quaternion')], name='', default='EulerAngles', update=on_mode_update) diff --git a/blender/arm/logicnode/variable/LN_quaternion.py b/blender/arm/logicnode/deprecated/LN_quaternion.py similarity index 100% rename from blender/arm/logicnode/variable/LN_quaternion.py rename to blender/arm/logicnode/deprecated/LN_quaternion.py diff --git a/blender/arm/logicnode/math/LN_separate_quaternion.py b/blender/arm/logicnode/deprecated/LN_separate_quaternion.py similarity index 100% rename from blender/arm/logicnode/math/LN_separate_quaternion.py rename to blender/arm/logicnode/deprecated/LN_separate_quaternion.py diff --git a/blender/arm/logicnode/math/LN_quaternion_math.py b/blender/arm/logicnode/math/LN_quaternion_math.py index 10f13832..ecc681d0 100644 --- a/blender/arm/logicnode/math/LN_quaternion_math.py +++ b/blender/arm/logicnode/math/LN_quaternion_math.py @@ -1,4 +1,5 @@ from arm.logicnode.arm_nodes import * +from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation class QuaternionMathNode(ArmLogicTreeNode): """Mathematical operations on quaternions.""" @@ -194,7 +195,7 @@ class QuaternionMathNode(ArmLogicTreeNode): for link in self.inputs[0].links: # 0 or 1 node_tree.links.new(link.from_socket, newself.inputs[0]) - elif self.property0 == 'ToEuler': + elif self.property0 == 'FromEuler': newself = node_tree.nodes.new('LNRotationNode') ret.append(newself) preconv = node_tree.nodes.new('LNVectorNode') @@ -220,7 +221,7 @@ class QuaternionMathNode(ArmLogicTreeNode): newself.property1='Rad' for link in self.inputs[0].links: # 0 or 1 - node_tree.links.new(link.from_node, newself.inputs[0]) + node_tree.links.new(link.from_socket, newself.inputs[0]) elif self.property0 == 'FromAxisAngle': newself = node_tree.nodes.new('LNRotationNode') ret.append(newself) @@ -229,22 +230,28 @@ class QuaternionMathNode(ArmLogicTreeNode): newself.inputs[0].default_value = self.inputs[1].default_value for link in self.inputs[1].links: # 0 or 1 - node_tree.links.new(link.from_node, newself.inputs[0]) + node_tree.links.new(link.from_socket, newself.inputs[0]) newself.inputs[1].default_value = self.inputs[2].default_value for link in self.inputs[2].links: # 0 or 1 - node_tree.links.new(link.from_node, newself.inputs[1]) + node_tree.links.new(link.from_socket, newself.inputs[1]) elif self.property0 in ('FromMat','FromRotationMat'): newself = node_tree.nodes.new('LNSeparateTransformNode') ret.append(newself) for link in self.inputs[1].links: # 0 or 1 - node_tree.links.new(link.from_node, newself.inputs[0]) + node_tree.links.new(link.from_socket, newself.inputs[0]) elif self.property0 in ('Lerp','Slerp','FromTo'): newself = node_tree.nodes.new('LNRotationMathNode') ret.append(newself) newself.property0 = self.property0 + for in1, in2 in zip(self.inputs, newself.inputs): - if in1.bl_idname in ('NodeSocketFloat', 'NodeSocketVector'): + if in2.bl_idname == 'ArmNodeSocketRotation': + in2.default_value_raw = Rotation.convert_to_quaternion( + in1.default_value,0, + 'EulerAngles','Rad','XZY' + ) + elif in1.bl_idname in ('NodeSocketFloat', 'NodeSocketVector'): in2.default_value = in1.default_value for link in in1.links: node_tree.links.new(link.from_socket, in2) @@ -264,15 +271,20 @@ class QuaternionMathNode(ArmLogicTreeNode): convnode = node_tree.nodes.new('LNSeparateRotationNode') convnode.property0 = 'Quaternion' ret.append(convnode) + if i_in_2 >= len(newself.inputs): + newself.ensure_input_socket(i_in_2, 'NodeSocketVector', 'Quaternion %d XYZ'%(i_in_1)) + newself.ensure_input_socket(i_in_2+1, 'NodeSocketFloat', 'Quaternion %d W'%(i_in_1), 1.0) node_tree.links.new(convnode.outputs[0], newself.inputs[i_in_2]) node_tree.links.new(convnode.outputs[1], newself.inputs[i_in_2+1]) for link in in1.links: node_tree.links.new(link.from_socket, convnode.inputs[0]) - i_in_2 +=1 - i_in_1 +=2 - elif in1.bl_idname == 'NodeSocketfloat': + i_in_2 +=2 + i_in_1 +=1 + elif in1.bl_idname == 'NodeSocketFloat': for link in in1.links: node_tree.links.new(link.from_socket, newself.inputs[i_in_2]) + i_in_1 +=1 + i_in_2 +=1 else: raise ValueError('get_replacement_node() for is not LNQuaternionMathNode V1->V2 is not prepared to deal with an input socket of type %s. This is a bug to report to the developers' %in1.bl_idname) @@ -281,7 +293,8 @@ class QuaternionMathNode(ArmLogicTreeNode): if self.property0 in ('FromEuler','FromMat','FromRotationMat','FromAxisAngle','Lerp','Slerp','FromTo'): # the new self returns a rotation for link in self.outputs[0].links: - node_tree.links.new(newself.outputs[0], link.to_socket) + out_sock_i = int( self.property0.endswith('Mat') ) + node_tree.links.new(newself.outputs[out_sock_i], link.to_socket) elif self.property0 in ('DotProduct','Module'): # new self returns a float for link in self.outputs[1 + 4*int(self.property1)].links: @@ -328,13 +341,22 @@ class QuaternionMathNode(ArmLogicTreeNode): conv = node_tree.nodes.new('LNSeparateVectorNode') ret.append(conv) node_tree.links.new(newself.outputs[0], conv.inputs[0]) - for link in xlinks: - node_tree.links.new(conv.outputs[0], link.to_socket) - for link in ylinks: - node_tree.links.new(conv.outputs[1], link.to_socket) - for link in zlinks: - node_tree.links.new(conv.outputs[2], link.to_socket) + for link in xlinks: + node_tree.links.new(conv.outputs[0], link.to_socket) + for link in ylinks: + node_tree.links.new(conv.outputs[1], link.to_socket) + for link in zlinks: + node_tree.links.new(conv.outputs[2], link.to_socket) + for node in ret: # update the labels on the node's displays + if node.bl_idname == 'LNSeparateRotationNode': + node.on_property_update(None) + elif node.bl_idname == 'LNRotationNode': + node.on_property_update(None) + elif node.bl_idname == 'LNRotationMathNode': + node.on_update_operation(None) + elif node.bl_idname == 'LNQuaternionMathNode': + node.set_enum(node.get_enum()) return ret diff --git a/blender/arm/logicnode/math/LN_rotation_math.py b/blender/arm/logicnode/math/LN_rotation_math.py index dcf59883..cbfe92e2 100644 --- a/blender/arm/logicnode/math/LN_rotation_math.py +++ b/blender/arm/logicnode/math/LN_rotation_math.py @@ -1,6 +1,7 @@ from arm.logicnode.arm_nodes import * +from mathutils import Vector -class QuaternionMathNode(ArmLogicTreeNode): +class RotationMathNode(ArmLogicTreeNode): """Mathematical operations on rotations.""" bl_idname = 'LNRotationMathNode' bl_label = 'Rotation Math' @@ -54,7 +55,7 @@ class QuaternionMathNode(ArmLogicTreeNode): for socket in sink_sockets: self.id_data.links.new(self.inputs[socket_number], socket) - def on_update_operation(self, context): + def on_property_update(self, context): # Checking the selection of another operation @@ -73,13 +74,13 @@ class QuaternionMathNode(ArmLogicTreeNode): 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") + self.ensure_input_socket(0, "NodeSocketVector", "From") + self.ensure_input_socket(1, "NodeSocketVector", "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") + self.replace_input_socket(1, "ArmNodeSocketRotation", "Rotation 2") if self.property0 == 'Compose': self.inputs[1].name = "Inner quaternion" # Float as argument 1: @@ -105,15 +106,15 @@ class QuaternionMathNode(ArmLogicTreeNode): ('FromTo', 'From To', 'From direction To direction'), #('FromRotationMat', 'From Rotation Mat', 'From Rotation Mat') ], - name='', default='Compose', update=on_update_operation) + name='', default='Compose', update=on_property_update) #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]) + super(RotationMathNode, self).init(context) + self.add_input('ArmNodeSocketRotation', 'Outer rotation', default_value=Vector((0.0, 0.0, 0.0, 1.0)) ) + self.add_input('ArmNodeSocketRotation', 'Inner rotation', default_value=Vector((0.0, 0.0, 0.0, 1.0))) self.add_output('ArmNodeSocketRotation', 'Result') def draw_buttons(self, context, layout): diff --git a/blender/arm/logicnode/transform/LN_separate_rotation.py b/blender/arm/logicnode/transform/LN_separate_rotation.py index 2085951a..9f1cfbde 100644 --- a/blender/arm/logicnode/transform/LN_separate_rotation.py +++ b/blender/arm/logicnode/transform/LN_separate_rotation.py @@ -17,13 +17,13 @@ class SeparateRotationNode(ArmLogicTreeNode): def on_property_update(self, context): """called by the EnumProperty, used to update the node socket labels""" - if self.property0 == "Quat": + if self.property0 == 'Quaternion': self.outputs[0].name = "Quaternion XYZ" self.outputs[1].name = "Quaternion W" - elif self.property0 == "EulerAngles": + elif self.property0 == 'EulerAngles': self.outputs[0].name = "Euler Angles" self.outputs[1].name = "[unused for Euler output]" - elif self.property0.startswith("AxisAngle"): + elif self.property0 == 'AxisAngle': self.outputs[0].name = "Axis" self.outputs[1].name = "Angle" else: diff --git a/blender/arm/logicnode/transform/LN_transform.py b/blender/arm/logicnode/variable/LN_transform.py similarity index 100% rename from blender/arm/logicnode/transform/LN_transform.py rename to blender/arm/logicnode/variable/LN_transform.py From a67452802d10ec48c17bf2940681a33267a256b0 Mon Sep 17 00:00:00 2001 From: niacdoial Date: Thu, 19 Aug 2021 11:31:12 +0200 Subject: [PATCH 4/5] React to commit 14d33ee0530e5db640e0fafb8604662333c1b066 in iron. --- Sources/armory/logicnode/RotationNode.hx | 4 ++-- Sources/armory/logicnode/SeparateRotationNode.hx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/armory/logicnode/RotationNode.hx b/Sources/armory/logicnode/RotationNode.hx index 893a1c78..362dc2df 100644 --- a/Sources/armory/logicnode/RotationNode.hx +++ b/Sources/armory/logicnode/RotationNode.hx @@ -69,7 +69,7 @@ class RotationNode extends LogicNode { vec.y *= toRAD; vec.z *= toRAD; } - this.value = Rotation.eulerToQuat(vec, property2); + this.value.fromEulerOrdered(vec, property2); } } default: { @@ -104,7 +104,7 @@ class RotationNode extends LogicNode { } case "EulerAngles": { if (input_length>0){ - var vec = Rotation.quatToEuler(value, property2); + var vec = value.toEulerOrdered(property2); if (property1=="Deg"){ vec.x /= toRAD; vec.y /= toRAD; diff --git a/Sources/armory/logicnode/SeparateRotationNode.hx b/Sources/armory/logicnode/SeparateRotationNode.hx index 0ce4f78c..acd8fce6 100644 --- a/Sources/armory/logicnode/SeparateRotationNode.hx +++ b/Sources/armory/logicnode/SeparateRotationNode.hx @@ -31,7 +31,7 @@ class SeparateRotationNode extends LogicNode { switch (property0) { case "EulerAngles": if (q!=this.input_cache) - euler_cache = Rotation.quatToEuler(q, property2); + euler_cache = q.toEulerOrdered(property2); if (from>0) return null; From 703c618c7df8da86957169f2c966f8dc11efb77c Mon Sep 17 00:00:00 2001 From: niacdoial Date: Sat, 21 Aug 2021 16:07:34 +0200 Subject: [PATCH 5/5] Misc. patches before merge [watch for amends] - fixed type/import bugs in RotationNode.hx and SeparateRotationNode.hx --- Sources/armory/logicnode/RotationNode.hx | 3 +-- Sources/armory/logicnode/SeparateRotationNode.hx | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/armory/logicnode/RotationNode.hx b/Sources/armory/logicnode/RotationNode.hx index 362dc2df..d9a0341b 100644 --- a/Sources/armory/logicnode/RotationNode.hx +++ b/Sources/armory/logicnode/RotationNode.hx @@ -3,7 +3,6 @@ package armory.logicnode; import iron.math.Vec4; import iron.math.Quat; import kha.FastFloat; -import iron.math.Rotation; class RotationNode extends LogicNode { @@ -104,7 +103,7 @@ class RotationNode extends LogicNode { } case "EulerAngles": { if (input_length>0){ - var vec = value.toEulerOrdered(property2); + var vec:Vec4 = value.toEulerOrdered(property2); if (property1=="Deg"){ vec.x /= toRAD; vec.y /= toRAD; diff --git a/Sources/armory/logicnode/SeparateRotationNode.hx b/Sources/armory/logicnode/SeparateRotationNode.hx index acd8fce6..0d366d79 100644 --- a/Sources/armory/logicnode/SeparateRotationNode.hx +++ b/Sources/armory/logicnode/SeparateRotationNode.hx @@ -3,7 +3,6 @@ package armory.logicnode; import kha.FastFloat; import iron.math.Quat; import iron.math.Vec4; -import iron.math.Rotation; class SeparateRotationNode extends LogicNode {