Make rotation its own socket in logic nodes, add rotation-specific math node

(part 1: conversion code not developed)
This commit is contained in:
niacdoial 2021-06-15 23:06:43 +02:00
parent 4ea5156f1e
commit 9b1393ea41
No known key found for this signature in database
GPG key ID: D2B9B9FE6D7B1C8E
22 changed files with 759 additions and 157 deletions

View file

@ -1,10 +1,13 @@
package armory.logicnode; package armory.logicnode;
import iron.object.Object; import iron.object.Object;
import iron.math.Vec3; import iron.math.Quat;
import iron.math.Vec4;
class GetRotationNode extends LogicNode { class GetRotationNode extends LogicNode {
public var property0: String;
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
super(tree); super(tree);
} }
@ -16,34 +19,16 @@ class GetRotationNode extends LogicNode {
return null; return null;
} }
var rot = object.transform.rot;
switch (from) { switch(property0){
case 0: case "Local":
// euler angles return object.transform.rot;
return object.transform.rot.getEuler(); case "Global":{
case 1: var useless1 = new Vec4();
// vector var ret = new Quat();
var sqrtW = Math.sqrt(1 - (rot.w * rot.w)); object.transform.world.decompose(useless1, ret, useless1);
if (sqrtW == 0) { return ret;
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;
}
return null; return null;
} }
} }

View file

@ -37,6 +37,6 @@ class LookAtNode extends LogicNode {
v2.setFrom(vto).sub(vfrom).normalize(); v2.setFrom(vto).sub(vfrom).normalize();
q.fromTo(v1, v2); q.fromTo(v1, v2);
return q.getEuler(); return q;
} }
} }

View file

@ -2,13 +2,11 @@ package armory.logicnode;
import iron.object.Object; import iron.object.Object;
import iron.math.Quat; import iron.math.Quat;
import iron.math.Vec4;
import armory.trait.physics.RigidBody; import armory.trait.physics.RigidBody;
class RotateObjectNode extends LogicNode { class RotateObjectNode extends LogicNode {
public var property0 = "Euler Angles"; public var property0 = "Local";
var q = new Quat();
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
super(tree); super(tree);
@ -16,32 +14,19 @@ class RotateObjectNode extends LogicNode {
override function run(from: Int) { override function run(from: Int) {
var object: Object = inputs[1].get(); 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. if (object == null || q == null) return;
// 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 || vec == null) return; q.normalize();
switch (property0){
switch (property0) { case "Local":
case "Euler Angles": object.transform.rot.mult(q);
q.fromEuler(vec.x, vec.y, vec.z); case "Global":
case "Angle Axies (Degrees)" | "Angle Axies (Radians)": object.transform.rot.multquats(q, object.transform.rot);
var angle: Float = inputs[3].get(); // that function call (Quat.multquats) is weird: it both modifies the object, and returns `this`
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();
} }
object.transform.rot.mult(q);
object.transform.buildMatrix(); object.transform.buildMatrix();
#if arm_physics #if arm_physics

View file

@ -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;
}
}

View file

@ -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<Float> = null,
y: Null<Float> = null,
z: Null<Float> = null,
w: Null<Float> = 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;
}
}

View file

@ -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;
}
}

View file

@ -20,7 +20,7 @@ class SeparateTransformNode extends LogicNode {
matrix.decompose(loc, rot, scale); matrix.decompose(loc, rot, scale);
if (from == 0) return loc; if (from == 0) return loc;
else if (from == 1) return rot.getEuler(); else if (from == 1) return rot;
else return scale; else return scale;
} }
} }

View file

@ -1,14 +1,12 @@
package armory.logicnode; package armory.logicnode;
import iron.object.Object; import iron.object.Object;
import iron.math.Quat; import iron.math.Quat;
import iron.math.Vec4;
import armory.trait.physics.RigidBody; import armory.trait.physics.RigidBody;
class SetRotationNode extends LogicNode { class SetRotationNode extends LogicNode {
public var property0: String;
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
super(tree); super(tree);
} }
@ -16,27 +14,13 @@ class SetRotationNode extends LogicNode {
override function run(from: Int) { override function run(from: Int) {
var object: Object = inputs[1].get(); var object: Object = inputs[1].get();
if (object == null) return; if (object == null) return;
var vec: Vec4 = inputs[2].get(); var q: Quat = inputs[2].get();
if (vec == null) return; if (q == null) return;
var w: Float = inputs[3].get();
switch (property0) { q.normalize();
case "Euler Angles": object.transform.rot = q;
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();
}
object.transform.buildMatrix(); object.transform.buildMatrix();
#if arm_physics #if arm_physics
var rigidBody = object.getTrait(RigidBody); var rigidBody = object.getTrait(RigidBody);
if (rigidBody != null) { if (rigidBody != null) {

View file

@ -7,9 +7,9 @@ import iron.math.Quat;
class TransformNode extends LogicNode { class TransformNode extends LogicNode {
var value: Mat4 = Mat4.identity(); var value: Mat4 = Mat4.identity();
static var q = new Quat(); var q = new Quat();
static var v1 = new Vec4(); var v1 = new Vec4();
static var v2 = new Vec4(); var v2 = new Vec4();
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
super(tree); super(tree);
@ -17,18 +17,21 @@ class TransformNode extends LogicNode {
override function get(from: Int): Dynamic { override function get(from: Int): Dynamic {
var loc: Vec4 = inputs[0].get(); 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(); 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; if (loc == null || rot == null || scale == null) return null;
q.fromEuler(rot.x, rot.y, rot.z); this.value.compose(loc, rot, scale);
value.compose(loc, q, scale); return this.value;
return value;
} }
override function set(value: Dynamic) { override function set(value: Dynamic) {
cast(value, Mat4).decompose(v1, q, v2); if (inputs.length>0){
inputs[0].set(v1); cast(value, Mat4).decompose(v1, q, v2);
inputs[1].set(q.getEuler()); inputs[0].set(v1);
inputs[2].set(v2); inputs[1].set(q);
inputs[2].set(v2);
}else this.value = value;
} }
} }

View file

@ -1,6 +1,8 @@
from math import pi, cos, sin, sqrt
import bpy import bpy
from bpy.props import PointerProperty from bpy.props import PointerProperty, EnumProperty, FloatProperty, FloatVectorProperty
from bpy.types import NodeSocket from bpy.types import NodeSocket
import mathutils
import arm.utils import arm.utils
@ -68,6 +70,169 @@ class ArmAnimActionSocket(ArmCustomSocket):
def draw_color(self, context, node): def draw_color(self, context, node):
return 0.8, 0.8, 0.8, 1 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): class ArmArraySocket(ArmCustomSocket):
bl_idname = 'ArmNodeSocketArray' bl_idname = 'ArmNodeSocketArray'
@ -118,6 +283,7 @@ class ArmObjectSocket(ArmCustomSocket):
def register(): def register():
bpy.utils.register_class(ArmActionSocket) bpy.utils.register_class(ArmActionSocket)
bpy.utils.register_class(ArmAnimActionSocket) bpy.utils.register_class(ArmAnimActionSocket)
bpy.utils.register_class(ArmRotationSocket)
bpy.utils.register_class(ArmArraySocket) bpy.utils.register_class(ArmArraySocket)
bpy.utils.register_class(ArmObjectSocket) bpy.utils.register_class(ArmObjectSocket)
@ -126,4 +292,5 @@ def unregister():
bpy.utils.unregister_class(ArmObjectSocket) bpy.utils.unregister_class(ArmObjectSocket)
bpy.utils.unregister_class(ArmArraySocket) bpy.utils.unregister_class(ArmArraySocket)
bpy.utils.unregister_class(ArmAnimActionSocket) bpy.utils.unregister_class(ArmAnimActionSocket)
bpy.utils.unregister_class(ArmRotationSocket)
bpy.utils.unregister_class(ArmActionSocket) bpy.utils.unregister_class(ArmActionSocket)

View file

@ -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= <no rotation>" '),
('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

View file

@ -10,10 +10,12 @@ class GetRotationNode(ArmLogicTreeNode):
def init(self, context): def init(self, context):
super(GetRotationNode, self).init(context) super(GetRotationNode, self).init(context)
self.add_input('ArmNodeSocketObject', 'Object') self.add_input('ArmNodeSocketObject', 'Object')
self.add_output('ArmNodeSocketRotation', 'Rotation')
self.add_output('NodeSocketVector', 'Euler Angles') def draw_buttons(self, context, layout):
self.add_output('NodeSocketVector', 'Vector') layout.prop(self, 'property0')
self.add_output('NodeSocketFloat', 'Angle (Radians)')
self.add_output('NodeSocketFloat', 'Angle (Degrees)') property0: EnumProperty(
self.add_output('NodeSocketVector', 'Quaternion XYZ') items = [('Local', 'Local', 'Local'),
self.add_output('NodeSocketFloat', 'Quaternion W') ('Global', 'Global', 'Global')],
name='', default='Local')

View file

@ -1,7 +1,7 @@
from arm.logicnode.arm_nodes import * from arm.logicnode.arm_nodes import *
class LookAtNode(ArmLogicTreeNode): 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_idname = 'LNLookAtNode'
bl_label = 'Look At' bl_label = 'Look At'
arm_section = 'rotation' arm_section = 'rotation'
@ -21,7 +21,7 @@ class LookAtNode(ArmLogicTreeNode):
self.add_input('NodeSocketVector', 'From Location') self.add_input('NodeSocketVector', 'From Location')
self.add_input('NodeSocketVector', 'To Location') self.add_input('NodeSocketVector', 'To Location')
self.add_output('NodeSocketVector', 'Rotation') self.add_output('ArmNodeSocketRotation', 'Rotation')
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, 'property0') layout.prop(self, 'property0')

View file

@ -11,39 +11,14 @@ class RotateObjectNode(ArmLogicTreeNode):
super().init(context) super().init(context)
self.add_input('ArmNodeSocketAction', 'In') self.add_input('ArmNodeSocketAction', 'In')
self.add_input('ArmNodeSocketObject', 'Object') self.add_input('ArmNodeSocketObject', 'Object')
self.add_input('NodeSocketVector', 'Euler Angles') self.add_input('ArmNodeSocketRotation', 'Rotation')
self.add_input('NodeSocketFloat', 'Angle / W')
self.add_output('ArmNodeSocketAction', 'Out') 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): def draw_buttons(self, context, layout):
# this block is here to ensure backwards compatibility and warn the user. layout.prop(self, 'property0')
# delete it (only keep the "else" part) when the 'old version' of the node will be considered removed.
# (note: please also update the corresponding haxe file when doing so)
if len(self.inputs) < 4:
row = layout.row(align=True)
row.label(text="Node has been updated with armory 2020.09. Please consider deleting and recreating it.")
else:
layout.prop(self, 'property0')
property0: EnumProperty( property0: EnumProperty(
items = [('Euler Angles', 'Euler Angles', 'Euler Angles'), items = [('Local', 'Local F.O.R.', 'Frame of reference oriented with the object'),
('Angle Axies (Radians)', 'Angle Axies (Radians)', 'Angle Axies (Radians)'), ('Global', 'Global/Parent F.O.R.', 'Frame of reference oriented with the object\'s parent or the world')],
('Angle Axies (Degrees)', 'Angle Axies (Degrees)', 'Angle Axies (Degrees)'), name='', default='Local')
('Quaternion', 'Quaternion', 'Quaternion')],
name='', default='Euler Angles',
update = on_property_update)

View file

@ -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')

View file

@ -11,5 +11,5 @@ class SeparateTransformNode(ArmLogicTreeNode):
self.add_input('NodeSocketShader', 'Transform') self.add_input('NodeSocketShader', 'Transform')
self.add_output('NodeSocketVector', 'Location') self.add_output('NodeSocketVector', 'Location')
self.add_output('NodeSocketVector', 'Rotation') self.add_output('ArmNodeSocketRotation', 'Rotation')
self.add_output('NodeSocketVector', 'Scale') self.add_output('NodeSocketVector', 'Scale')

View file

@ -6,37 +6,20 @@ class SetRotationNode(ArmLogicTreeNode):
bl_label = 'Set Object Rotation' bl_label = 'Set Object Rotation'
arm_section = 'rotation' arm_section = 'rotation'
arm_version = 1 arm_version = 1
def init(self, context): def init(self, context):
super(SetRotationNode, self).init(context) super(SetRotationNode, self).init(context)
self.add_input('ArmNodeSocketAction', 'In') self.add_input('ArmNodeSocketAction', 'In')
self.add_input('ArmNodeSocketObject', 'Object') self.add_input('ArmNodeSocketObject', 'Object')
self.add_input('NodeSocketVector', 'Euler Angles / Vector XYZ') self.add_input('ArmNodeSocketRotation', 'Rotation')
self.add_input('NodeSocketFloat', 'Angle / W')
self.add_output('ArmNodeSocketAction', 'Out') self.add_output('ArmNodeSocketAction', 'Out')
def on_property_update(self, context): # def draw_buttons(self, context, layout):
"""called by the EnumProperty, used to update the node socket labels""" # layout.prop(self, 'property0')
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): # property0: EnumProperty(
layout.prop(self, 'property0') # items = [('Local', 'Local', 'Local'),
# ('Global', 'Global', 'Global')],
property0: EnumProperty( # name='', default='Local')
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)

View file

@ -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')

View file

@ -9,7 +9,7 @@ class TransformNode(ArmLogicTreeNode):
def init(self, context): def init(self, context):
super(TransformNode, self).init(context) super(TransformNode, self).init(context)
self.add_input('NodeSocketVector', 'Location') 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_input('NodeSocketVector', 'Scale', default_value=[1.0, 1.0, 1.0])
self.add_output('NodeSocketShader', 'Transform') self.add_output('NodeSocketShader', 'Transform', is_var=True)

View file

@ -10,7 +10,7 @@ class VectorToObjectOrientationNode(ArmLogicTreeNode):
""" """
bl_idname = 'LNVectorToObjectOrientationNode' bl_idname = 'LNVectorToObjectOrientationNode'
bl_label = 'Vector to Object Orientation' bl_label = 'Vector to Object Orientation'
arm_section = 'location' arm_section = 'rotation'
arm_version = 1 arm_version = 1
def init(self, context): def init(self, context):

View file

@ -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'
)

View file

@ -294,6 +294,8 @@ def build_default_node(inp: bpy.types.NodeSocket):
if inp_type == 'VECTOR': if inp_type == 'VECTOR':
return f'new armory.logicnode.VectorNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]})' 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': elif inp_type == 'RGBA':
return f'new armory.logicnode.ColorNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]}, {default_value[3]})' return f'new armory.logicnode.ColorNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]}, {default_value[3]})'
elif inp_type == 'RGB': elif inp_type == 'RGB':