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/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/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..2af24da5 --- /dev/null +++ b/Sources/armory/logicnode/RotationMathNode.hx @@ -0,0 +1,96 @@ +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": { + 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; + 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 from: Quat = inputs[0].get(); + var to: Quat = inputs[1].get(); + var f: Float = inputs[2].get(); + 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 ((from == null) || (f == null) || (to == null)) return null; + res_q = res_q.slerp(from, 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..d9a0341b --- /dev/null +++ b/Sources/armory/logicnode/RotationNode.hx @@ -0,0 +1,120 @@ +package armory.logicnode; + +import iron.math.Vec4; +import iron.math.Quat; +import kha.FastFloat; + +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()) + if (inputs.length == 0){ + return this.value; + } + + 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.fromEulerOrdered(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:Vec4 = value.toEulerOrdered(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..0d366d79 --- /dev/null +++ b/Sources/armory/logicnode/SeparateRotationNode.hx @@ -0,0 +1,60 @@ +package armory.logicnode; + +import kha.FastFloat; +import iron.math.Quat; +import iron.math.Vec4; + +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 = q.toEulerOrdered(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 "Quaternion": + 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..7126fbff 100644 --- a/Sources/armory/logicnode/SetRotationNode.hx +++ b/Sources/armory/logicnode/SetRotationNode.hx @@ -1,13 +1,13 @@ + 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 var property0: String; // UNUSED public function new(tree: LogicTree) { super(tree); @@ -16,27 +16,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_nodes.py b/blender/arm/logicnode/arm_nodes.py index 3b7834e8..785f1925 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 import arm # we cannot import arm.livepatch here or we have a circular import # Pass custom property types and NodeReplacement forward to individual @@ -169,7 +170,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: # should not happen anymore? + socket.default_value = default_value if is_var and not socket.display_shape.endswith('_DOT'): socket.display_shape += '_DOT' @@ -185,8 +192,12 @@ class ArmLogicTreeNode(bpy.types.Node): """ socket = self.outputs.new(socket_type, socket_name) + # FIXME: …a default_value on an output socket? Why is that a thing? if default_value is not None: - socket.default_value = default_value + 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') if is_var and not socket.display_shape.endswith('_DOT'): socket.display_shape += '_DOT' @@ -208,7 +219,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 = '' @@ -238,14 +256,17 @@ class ArmNodeRemoveInputButton(bpy.types.Operator): bl_label = 'Remove Input' bl_options = {'UNDO', 'INTERNAL'} 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): @@ -254,13 +275,14 @@ class ArmNodeRemoveInputValueButton(bpy.types.Operator): bl_label = 'Remove Input' bl_options = {'UNDO', 'INTERNAL'} 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'} @@ -278,7 +300,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 = '' @@ -294,14 +323,16 @@ class ArmNodeRemoveOutputButton(bpy.types.Operator): bl_label = 'Remove Output' bl_options = {'UNDO', 'INTERNAL'} 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): @@ -322,8 +353,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 = '' @@ -341,7 +385,9 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator): bl_label = 'Remove Input Output' bl_options = {'UNDO', 'INTERNAL'} 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] @@ -349,10 +395,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'} @@ -547,7 +595,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 0b004a18..944434b5 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 * from bpy.types import NodeSocket +import mathutils import arm.utils @@ -77,6 +79,190 @@ class ArmAnimActionSocket(ArmCustomSocket): def draw_color(self, context, node): return 0.8, 0.8, 0.8, 1 + +class ArmRotationSocket(ArmCustomSocket): + bl_idname = 'ArmRotationSocket' + 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 mathutils.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 == 'Quaternion': + 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) + + @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 ('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=='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) + 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 + return mathutils.Vector((qx,qy,qz,qw)) + + elif param1 == 'AxisAngle': + if param2 == 'Deg': + angle = part2 * pi/180 + else: + angle = part2 + cang, sang = cos(angle/2), sin(angle/2) + x,y,z = part1[0], part1[1], part1[2] + veclen = sqrt(x**2+y**2+z**2) + if veclen<1E-5: + return mathutils.Vector((0.0,0.0,0.0,1.0)) + else: + return mathutils.Vector(( + x/veclen * sang, + y/veclen * sang, + z/veclen * sang, + cang + )) + 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 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 + 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.default_value_unit, + self.default_value_order + ) + + + 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 == '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') + 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'), + ('Quaternion', '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( + name='Value', + description='Raw quaternion obtained for the default value of a ArmRotationSocket socket', + size=4, default=(0,0,0,1), + update = _on_update_socket + ) + class ArmArraySocket(ArmCustomSocket): bl_idname = 'ArmNodeSocketArray' @@ -226,6 +412,7 @@ class ArmObjectSocket(ArmCustomSocket): return 0.15, 0.55, 0.75, 1 + class ArmStringSocket(ArmCustomSocket): bl_idname = 'ArmStringSocket' bl_label = 'String Socket' @@ -292,10 +479,10 @@ def draw_socket_layout_split(socket: bpy.types.NodeSocket, layout: bpy.types.UIL if not socket.is_output and not socket.is_linked: layout.prop(socket, prop_name, text='') - REG_CLASSES = ( ArmActionSocket, ArmAnimActionSocket, + ArmRotationSocket, ArmArraySocket, ArmBoolSocket, ArmColorSocket, diff --git a/blender/arm/logicnode/deprecated/LN_quaternion.py b/blender/arm/logicnode/deprecated/LN_quaternion.py new file mode 100644 index 00000000..70c4ccb3 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_quaternion.py @@ -0,0 +1,75 @@ +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_description = 'Create a quaternion variable (transported through a vector socket)' + arm_section = 'quaternions' + arm_version = 2 # deprecate + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + self.add_input('ArmFloatSocket', 'Z') + self.add_input('ArmFloatSocket', 'W', default_value=1.0) + + self.add_output('ArmVectorSocket', 'Quaternion') + self.add_output('ArmVectorSocket', 'XYZ') + self.add_output('ArmVectorSocket', '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/deprecated/LN_separate_quaternion.py b/blender/arm/logicnode/deprecated/LN_separate_quaternion.py new file mode 100644 index 00000000..a07d20ae --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_separate_quaternion.py @@ -0,0 +1,45 @@ +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 (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 = 2 # deprecate + + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Quaternion') + + self.add_output('ArmFloatSocket', 'X') + self.add_output('ArmFloatSocket', 'Y') + self.add_output('ArmFloatSocket', 'Z') + self.add_output('ArmFloatSocket', '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/math/LN_quaternion_math.py b/blender/arm/logicnode/math/LN_quaternion_math.py index dfdece72..dcf2d164 100644 --- a/blender/arm/logicnode/math/LN_quaternion_math.py +++ b/blender/arm/logicnode/math/LN_quaternion_math.py @@ -1,47 +1,52 @@ from arm.logicnode.arm_nodes import * +from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation 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('ArmFloatSocket', 'X') # Result X - self.add_output('ArmFloatSocket', 'Y') # Result Y - self.add_output('ArmFloatSocket', 'Z') # Result Z - self.add_output('ArmFloatSocket', 'W') # Result W - if (self.property0 == 'Module'): - self.add_output('ArmFloatSocket', 'Module') # Module - if (self.property0 == 'DotProduct'): - self.add_output('ArmFloatSocket', 'Scalar') # DotProduct - if (self.property0 == 'ToAxisAngle'): - self.add_output('ArmFloatSocket', '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('ArmFloatSocket', '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 \ + and self.inputs[socket_number].arm_socket_type != 'NONE' + ): + default_value = self.inputs[socket_number].default_value_raw + 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('ArmFloatSocket', 'Module') # Module - if (self.property0 == 'DotProduct'): - self.add_output('ArmFloatSocket', 'Scalar') # DotProduct - if (self.property0 == 'ToAxisAngle'): - self.add_output('ArmFloatSocket', 'To Axis Angle') # ToAxisAngle + source_socket = None + + + self.inputs.new(newclass, newname) + if default_value != None: + self.inputs[-1].default_value_raw = 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('ArmFloatSocket', '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: HaxeBoolProperty('property1', 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,63 +80,43 @@ 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('ArmVectorSocket', 'Quaternion ' + str(len(self.inputs))) - self.add_input('ArmFloatSocket', 'Value ' + str(len(self.inputs))) - else: - while (len(self.inputs) < 2): - self.add_input('ArmVectorSocket', 'Quaternion ' + str(len(self.inputs))) - if (select_current == 'DotProduct'): - self.add_output('ArmFloatSocket', 'Scalar') + 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, 'ArmVectorSocket', 'Quaternion %d XYZ'%i) + self.ensure_input_socket(2*i+1, 'ArmFloatSocket', '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, 'ArmVectorSocket', 'Quaternion XYZ') + self.ensure_input_socket(1, 'ArmFloatSocket', 'Quaternion W', default_value=1.0) + for i in range( max(len(self.inputs)-2 ,1) ): + self.ensure_input_socket(i+2, 'ArmFloatSocket', 'Value %d'%i) + elif select_current in ('Module', 'Normalize'): + self.ensure_input_socket(0, 'ArmVectorSocket', 'Quaternion XYZ') + self.ensure_input_socket(1, 'ArmFloatSocket', '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) - # 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('ArmVectorSocket', 'From') - self.add_input('ArmVectorSocket', 'To') - self.add_input('ArmFloatSocket', 'T') - if (select_current == 'FromAxisAngle'): - self.add_input('ArmVectorSocket', 'Quaternion') - self.add_input('ArmVectorSocket', 'Axis') - self.add_input('ArmFloatSocket', 'Angle') - if (select_current == 'FromEuler'): - self.add_input('ArmFloatSocket', 'X') - self.add_input('ArmFloatSocket', 'Y') - self.add_input('ArmFloatSocket', 'Z') + 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' - # 2 arguments: FromTo, FromMat, FromRotationMat, ToAxisAngle - if (self.get_count_in(select_current) == 2): - if (select_current == 'FromTo'): - self.add_input('ArmVectorSocket', 'Vector ' + str(len(self.inputs))) - self.add_input('ArmVectorSocket', 'Vector ' + str(len(self.inputs))) - if (select_current == 'FromMat') or (select_current == 'FromRotationMat'): - self.add_input('ArmVectorSocket', 'Quaternion') - self.add_input('ArmDynamicSocket', 'Matrix') - if (select_current == 'ToAxisAngle'): - self.add_input('ArmVectorSocket', 'Quaternion') - self.add_input('ArmVectorSocket', 'Axis') - self.add_output('ArmFloatSocket', 'Angle') - - # 1 argument: Module, Normalize, GetEuler - if (self.get_count_in(select_current) == 1): - self.add_input('ArmVectorSocket', 'Quaternion') - if (select_current == 'Module'): - self.add_output('ArmFloatSocket', 'Module') self['property0'] = value + self['property0_proxy'] = value + + # this property swaperoo is kinda janky-looking, but necessary. + # Read more on LN_rotate_object.py property0: HaxeEnumProperty( 'property0', items = [('Add', 'Add', 'Add'), @@ -140,29 +125,44 @@ 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')], + ('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) + 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) + def __init__(self): + super(QuaternionMathNode, self).__init__() array_nodes[str(id(self))] = self def arm_init(self, context): - self.add_input('ArmVectorSocket', 'Quaternion 0', default_value=[0.0, 0.0, 0.0]) - self.add_input('ArmVectorSocket', 'Quaternion 1', default_value=[0.0, 0.0, 0.0]) - self.add_output('ArmVectorSocket', 'Result') + self.add_input('ArmVectorSocket', 'Quaternion 0 XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_input('ArmFloatSocket', 'Quaternion 0 W', default_value=1) + self.add_input('ArmVectorSocket', 'Quaternion 1 XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_input('ArmFloatSocket', 'Quaternion 1 W', default_value=1) + self.add_output('ArmVectorSocket', 'Result XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_output('ArmFloatSocket', '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 +170,210 @@ 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 = 'ArmFloatSocket' else: - op.socket_type = 'ArmVectorSocket' + op.socket_type = 'ArmVectorSocket;ArmFloatSocket' + 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 == 'FromEuler': + 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_socket, 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_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_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_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 in2.bl_idname == 'ArmRotationSocket': + in2.default_value_raw = Rotation.convert_to_quaternion( + in1.default_value,0, + 'EulerAngles','Rad','XZY' + ) + elif in1.bl_idname in ('ArmFloatSocket', 'ArmVectorSocket'): + 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 == 'ArmVectorSocket': + # quaternion input: now two sockets, not one. + 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, 'ArmVectorSocket', 'Quaternion %d XYZ'%(i_in_1)) + newself.ensure_input_socket(i_in_2+1, 'ArmFloatSocket', '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 +=2 + i_in_1 +=1 + elif in1.bl_idname == 'ArmFloatSocket': + 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) + + # #### 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: + 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: + 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) + + 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 + + + # note: keep property1, so that it is actually readable for node conversion. + property1: BoolProperty(name='DEPRECATED', default=False) 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..fea8e350 --- /dev/null +++ b/blender/arm/logicnode/math/LN_rotation_math.py @@ -0,0 +1,121 @@ +from arm.logicnode.arm_nodes import * +from mathutils import Vector + +class RotationMathNode(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 + + + @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('ArmFloatSocket', '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('ArmFloatSocket', '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_property_update(self, context): + # Checking the selection of another operation + + + # Rotation as argument 0: + if self.property0 in ('Inverse','Normalize','Amplify'): + self.ensure_input_socket(0, "ArmRotationSocket", "Rotation") + self.ensure_input_socket(1, "ArmFloatSocket", "Amplification factor") + elif self.property0 in ('Slerp','Lerp','Compose'): + self.ensure_input_socket(0, "ArmRotationSocket", "From") + self.ensure_input_socket(1, "ArmRotationSocket", "To") + + if self.property0 == 'Compose': + self.inputs[0].name = 'Outer rotation' + self.inputs[1].name = 'Inner rotation' + else: + self.ensure_input_socket(2, "ArmFloatSocket", "Interpolation factor") + + elif self.property0 == 'FromTo': + self.ensure_input_socket(0, "ArmVectorSocket", "From") + self.ensure_input_socket(1, "ArmVectorSocket", "To") + + # Rotation as argument 1: + if self.property0 in ('Compose','Lerp','Slerp'): + if self.inputs[1].bl_idname != "ArmRotationSocket": + self.replace_input_socket(1, "ArmRotationSocket", "Rotation 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 != 'ArmFloatSocket': + self.replace_input_socket(1, "ArmFloatSocket", "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: HaxeEnumProperty( + 'property0', + 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_property_update) + + #def __init__(self): + # array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmRotationSocket', 'Outer rotation', default_value=(0.0, 0.0, 0.0, 1.0) ) + self.add_input('ArmRotationSocket', 'Inner rotation', default_value=(0.0, 0.0, 0.0, 1.0) ) + self.add_output('ArmRotationSocket', 'Result') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') # Operation diff --git a/blender/arm/logicnode/math/LN_separate_quaternion.py b/blender/arm/logicnode/math/LN_separate_quaternion.py deleted file mode 100644 index d8b6f0cf..00000000 --- a/blender/arm/logicnode/math/LN_separate_quaternion.py +++ /dev/null @@ -1,16 +0,0 @@ -from arm.logicnode.arm_nodes import * - -class SeparateQuaternionNode(ArmLogicTreeNode): - """Splits the given quaternion into X, Y, Z and W.""" - bl_idname = 'LNSeparateQuaternionNode' - bl_label = "Separate Quaternion" - arm_section = 'quaternions' - arm_version = 1 - - def arm_init(self, context): - self.add_input('ArmVectorSocket', 'Quaternion') - - self.add_output('ArmFloatSocket', 'X') - self.add_output('ArmFloatSocket', 'Y') - self.add_output('ArmFloatSocket', 'Z') - self.add_output('ArmFloatSocket', 'W') diff --git a/blender/arm/logicnode/transform/LN_get_object_rotation.py b/blender/arm/logicnode/transform/LN_get_object_rotation.py index af3ba965..067211cf 100644 --- a/blender/arm/logicnode/transform/LN_get_object_rotation.py +++ b/blender/arm/logicnode/transform/LN_get_object_rotation.py @@ -5,14 +5,87 @@ class GetRotationNode(ArmLogicTreeNode): bl_idname = 'LNGetRotationNode' bl_label = 'Get Object Rotation' arm_section = 'rotation' - arm_version = 1 + arm_version = 2 def arm_init(self, context): self.add_input('ArmNodeSocketObject', 'Object') + self.add_output('ArmRotationSocket', 'Rotation') + - self.add_output('ArmVectorSocket', 'Euler Angles') - self.add_output('ArmVectorSocket', 'Vector') - self.add_output('ArmFloatSocket', 'Angle (Radians)') - self.add_output('ArmFloatSocket', 'Angle (Degrees)') - self.add_output('ArmVectorSocket', 'Quaternion XYZ') - self.add_output('ArmFloatSocket', 'Quaternion W') + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + property0: HaxeEnumProperty( + 'property0', + 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 f2bc1bde..79a3a02d 100644 --- a/blender/arm/logicnode/transform/LN_look_at.py +++ b/blender/arm/logicnode/transform/LN_look_at.py @@ -1,11 +1,11 @@ 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' - arm_version = 1 + arm_version = 2 property0: HaxeEnumProperty( 'property0', @@ -21,7 +21,27 @@ class LookAtNode(ArmLogicTreeNode): self.add_input('ArmVectorSocket', 'From Location') self.add_input('ArmVectorSocket', 'To Location') - self.add_output('ArmVectorSocket', 'Rotation') + self.add_output('ArmRotationSocket', 'Rotation') + 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 0e22ede6..aab97549 100644 --- a/blender/arm/logicnode/transform/LN_rotate_object.py +++ b/blender/arm/logicnode/transform/LN_rotate_object.py @@ -1,49 +1,105 @@ 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 arm_init(self, context): self.add_input('ArmNodeSocketAction', 'In') self.add_input('ArmNodeSocketObject', 'Object') - self.add_input('ArmVectorSocket', 'Euler Angles') - self.add_input('ArmFloatSocket', 'Angle / W') + self.add_input('ArmRotationSocket', '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 LN_rotate_object.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_proxy') + + # this property swaperoo is kinda janky-looking, but listen out: + # - when you reload an old file, the properties of loaded nodes can be mangled if the node class drops the property or the specific value within the property. + # -> to fix this, 'property0' needs to contain the old values so that node replacement can be done decently. + # - "but", I hear you ask, "why not make property0 a simple blender property, and create a property0v2 HaxeProperty to be bound to the haxe-time property0?" + # -> well, at the time of writing, a HaxeProperty's prop_name is only used for livepatching, not at initial setup, so a freshly-compiled game would get completely borked properties. + # solution: have a property0 HaxeProperty contain every possible value, and have a property0_proxy Property be in the UI. + + # NOTE FOR FUTURE MAINTAINERS: the value of the proxy property does **not** matter, only the value of property0 does. When eventually editing this class, you can safely drop the values in the proxy property, and *only* the proxy property. + + def on_proxyproperty_update(self, context=None): + self.property0 = self.property0_proxy + + property0_proxy: 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')], + name='', default='Local', + update = on_proxyproperty_update + ) property0: HaxeEnumProperty( 'property0', - 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=[('Euler Angles', 'NODE REPLACEMENT ONLY', ''), + ('Angle Axies (Radians)', 'NODE REPLACEMENT ONLY', ''), + ('Angle Axies (Degrees)', 'NODE REPLACEMENT ONLY', ''), + ('Quaternion', 'NODE REPLACEMENT ONLY', ''), + ('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') + + + + + + + + + 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 new file mode 100644 index 00000000..e751d9d2 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_separate_rotation.py @@ -0,0 +1,60 @@ +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 arm_init(self, context): + self.add_input('ArmRotationSocket', 'Angle') + + self.add_output('ArmVectorSocket', 'Euler Angles / Vector XYZ') + self.add_output('ArmFloatSocket', 'Angle / W') + + + def on_property_update(self, context): + """called by the EnumProperty, used to update the node socket labels""" + if self.property0 == 'Quaternion': + 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 == '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: HaxeEnumProperty( + 'property0', + items = [('EulerAngles', 'Euler Angles', 'Euler Angles'), + ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), + ('Quaternion', 'Quaternion', 'Quaternion')], + name='', default='EulerAngles', + update=on_property_update) + + property1: HaxeEnumProperty( + 'property1', + items=[('Deg', 'Degrees', 'Degrees'), + ('Rad', 'Radians', 'Radians')], + name='', default='Rad') + property2: HaxeEnumProperty( + 'property2', + 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 66cb1e7f..2770841a 100644 --- a/blender/arm/logicnode/transform/LN_separate_transform.py +++ b/blender/arm/logicnode/transform/LN_separate_transform.py @@ -4,11 +4,42 @@ class SeparateTransformNode(ArmLogicTreeNode): """Separates the transform of the given object.""" bl_idname = 'LNSeparateTransformNode' bl_label = 'Separate Transform' - arm_version = 1 + arm_version = 2 def arm_init(self, context): self.add_input('ArmDynamicSocket', 'Transform') self.add_output('ArmVectorSocket', 'Location') - self.add_output('ArmVectorSocket', 'Rotation') + self.add_output('ArmRotationSocket', 'Rotation') self.add_output('ArmVectorSocket', '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 6b7efbd9..2e2af8f5 100644 --- a/blender/arm/logicnode/transform/LN_set_object_rotation.py +++ b/blender/arm/logicnode/transform/LN_set_object_rotation.py @@ -1,42 +1,76 @@ 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 arm_init(self, context): self.add_input('ArmNodeSocketAction', 'In') self.add_input('ArmNodeSocketObject', 'Object') - self.add_input('ArmVectorSocket', 'Euler Angles / Vector XYZ') - self.add_input('ArmFloatSocket', 'Angle / W') + self.add_input('ArmRotationSocket', '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" + + 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('No nodesocket labels for current input mode: check self-consistancy of action_set_rotation.py') + 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] - def draw_buttons(self, context, layout): - layout.prop(self, 'property0') - - property0: HaxeEnumProperty( - 'property0', - 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) + # 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 deleted file mode 100644 index 7e7edd42..00000000 --- a/blender/arm/logicnode/transform/LN_transform.py +++ /dev/null @@ -1,14 +0,0 @@ -from arm.logicnode.arm_nodes import * - -class TransformNode(ArmLogicTreeNode): - """Stores the location, rotation and scale values as a transform.""" - bl_idname = 'LNTransformNode' - bl_label = 'Transform' - arm_version = 1 - - def arm_init(self, context): - self.add_input('ArmVectorSocket', 'Location') - self.add_input('ArmVectorSocket', 'Rotation') - self.add_input('ArmVectorSocket', 'Scale', default_value=[1.0, 1.0, 1.0]) - - self.add_output('ArmDynamicSocket', 'Transform') 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 80e5794d..c80fb367 100644 --- a/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py +++ b/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py @@ -9,7 +9,7 @@ class VectorToObjectOrientationNode(ArmLogicTreeNode): """ bl_idname = 'LNVectorToObjectOrientationNode' bl_label = 'Vector to Object Orientation' - arm_section = 'location' + arm_section = 'rotation' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/variable/LN_quaternion.py b/blender/arm/logicnode/variable/LN_quaternion.py deleted file mode 100644 index 7316dbab..00000000 --- a/blender/arm/logicnode/variable/LN_quaternion.py +++ /dev/null @@ -1,18 +0,0 @@ -from arm.logicnode.arm_nodes import * - -class QuaternionNode(ArmLogicTreeNode): - """TO DO.""" - bl_idname = 'LNQuaternionNode' - bl_label = 'Quaternion' - arm_section = 'quaternions' - arm_version = 1 - - def arm_init(self, context): - self.add_input('ArmFloatSocket', 'X') - self.add_input('ArmFloatSocket', 'Y') - self.add_input('ArmFloatSocket', 'Z') - self.add_input('ArmFloatSocket', 'W', default_value=1.0) - - self.add_output('ArmVectorSocket', 'Quaternion') - self.add_output('ArmVectorSocket', 'XYZ') - self.add_output('ArmFloatSocket', 'W') diff --git a/blender/arm/logicnode/variable/LN_rotation.py b/blender/arm/logicnode/variable/LN_rotation.py new file mode 100644 index 00000000..5eb7880d --- /dev/null +++ b/blender/arm/logicnode/variable/LN_rotation.py @@ -0,0 +1,61 @@ +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' + 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 + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Euler Angles / Vector XYZ') + self.add_input('ArmFloatSocket', 'Angle / W') + + self.add_output('ArmRotationSocket', '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: HaxeEnumProperty( + 'property0', + items = [('EulerAngles', 'Euler Angles', 'Euler Angles'), + ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), + ('Quaternion', 'Quaternion', 'Quaternion')], + name='', default='EulerAngles', + update=on_property_update) + + property1: HaxeEnumProperty( + 'property1', + items=[('Deg', 'Degrees', 'Degrees'), + ('Rad', 'Radians', 'Radians')], + name='', default='Rad') + property2: HaxeEnumProperty( + 'property2', + 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/variable/LN_transform.py b/blender/arm/logicnode/variable/LN_transform.py new file mode 100644 index 00000000..180b879f --- /dev/null +++ b/blender/arm/logicnode/variable/LN_transform.py @@ -0,0 +1,44 @@ +from arm.logicnode.arm_nodes import * + +class TransformNode(ArmLogicTreeNode): + """Stores the location, rotation and scale values as a transform.""" + bl_idname = 'LNTransformNode' + bl_label = 'Transform' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Location') + self.add_input('ArmRotationSocket', 'Rotation') + self.add_input('ArmVectorSocket', 'Scale', default_value=[1.0, 1.0, 1.0]) + self.add_output('ArmDynamicSocket', '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/make_logic.py b/blender/arm/make_logic.py index 13c1ae07..3ac1c0a2 100755 --- a/blender/arm/make_logic.py +++ b/blender/arm/make_logic.py @@ -308,6 +308,8 @@ def build_default_node(inp: bpy.types.NodeSocket): if inp_type == 'VECTOR': return f'new armory.logicnode.VectorNode(this, {default_value})' + elif inp_type == 'ROTATION': # a rotation is internally represented as a quaternion. + return f'new armory.logicnode.RotationNode(this, {default_value})' elif inp_type in ('RGB', 'RGBA'): return f'new armory.logicnode.ColorNode(this, {default_value})' elif inp_type == 'VALUE':