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

View file

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

View file

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

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);
if (from == 0) return loc;
else if (from == 1) return rot.getEuler();
else if (from == 1) return rot;
else return scale;
}
}

View file

@ -1,14 +1,12 @@
package armory.logicnode;
import iron.object.Object;
import iron.math.Quat;
import iron.math.Vec4;
import armory.trait.physics.RigidBody;
class SetRotationNode extends LogicNode {
public var property0: String;
public function new(tree: LogicTree) {
super(tree);
}
@ -16,27 +14,13 @@ class SetRotationNode extends LogicNode {
override function run(from: Int) {
var object: Object = inputs[1].get();
if (object == null) return;
var vec: Vec4 = inputs[2].get();
if (vec == null) return;
var w: Float = inputs[3].get();
var q: Quat = inputs[2].get();
if (q == null) return;
switch (property0) {
case "Euler Angles":
object.transform.rot.fromEuler(vec.x, vec.y, vec.z);
case "Angle Axies (Degrees)" | "Angle Axies (Radians)":
var angle: Float = w;
if (property0 == "Angle Axies (Degrees)") {
angle = angle * (Math.PI / 180);
}
var angleSin = Math.sin(angle / 2);
vec = vec.normalize();
var angleCos = Math.cos(angle / 2);
object.transform.rot = new Quat(vec.x * angleSin, vec.y * angleSin, vec.z * angleSin, angleCos);
case "Quaternion":
object.transform.rot = new Quat(vec.x, vec.y, vec.z, w);
object.transform.rot.normalize();
}
q.normalize();
object.transform.rot = q;
object.transform.buildMatrix();
#if arm_physics
var rigidBody = object.getTrait(RigidBody);
if (rigidBody != null) {

View file

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

View file

@ -1,6 +1,8 @@
from math import pi, cos, sin, sqrt
import bpy
from bpy.props import PointerProperty
from bpy.props import PointerProperty, EnumProperty, FloatProperty, FloatVectorProperty
from bpy.types import NodeSocket
import mathutils
import arm.utils
@ -68,6 +70,169 @@ class ArmAnimActionSocket(ArmCustomSocket):
def draw_color(self, context, node):
return 0.8, 0.8, 0.8, 1
class ArmRotationSocket(ArmCustomSocket):
bl_idname = 'ArmNodeSocketRotation'
bl_label = 'Rotation Socket'
arm_socket_type = 'ROTATION' # the internal representation is a quaternion, AKA a '4D vector' (using mathutils.Vector((x,y,z,w)))
def get_default_value(self):
if self.default_value_raw is None:
return Vector((0.0,0.0,0.0,1.0))
else:
return self.default_value_raw
def on_unit_update(self, context):
if self.default_value_unit == 'Rad':
fac = pi/180 # deg->rad conversion
else:
fac = 180/pi # rad->deg conversion
if self.default_value_mode == 'AxisAngle':
self.default_value_s3 *= fac
elif self.default_value_mode == 'EulerAngles':
self.default_value_s0 *= fac
self.default_value_s1 *= fac
self.default_value_s2 *= fac
self.do_update_raw(context)
def on_mode_update(self, context):
if self.default_value_mode == 'Quat':
summ = abs(self.default_value_s0)
summ+= abs(self.default_value_s1)
summ+= abs(self.default_value_s2)
summ+= abs(self.default_value_s3)
if summ<0.01:
self.default_value_s3 = 1.0
elif self.default_value_mode == 'AxisAngle':
summ = abs(self.default_value_s0)
summ+= abs(self.default_value_s1)
summ+= abs(self.default_value_s2)
if summ<1E-5:
self.default_value_s3 = 0.0
self.do_update_raw(context)
def do_update_raw(self, context):
if self.default_value_mode == 'Quat':
qx = self.default_value_s0
qy = self.default_value_s1
qz = self.default_value_s2
qw = self.default_value_s3
# need to normalize the quaternion for a rotation (having it be 0 is not an option)
ql = sqrt(qx**2+qy**2+qz**2+qw**2)
if abs(ql)<1E-5:
qx, qy, qz, qw = 0.0,0.0,0.0,1.0
else:
qx /= ql
qy /= ql
qz /= ql
qw /= ql
self.default_value_raw = mathutils.Vector((qx,qy,qz,qw))
elif self.default_value_mode == 'AxisAngle':
if self.default_value_unit == 'Deg':
angle = self.default_value_s3 * pi/180
else:
angle = self.default_value_s3
cang, sang = cos(angle/2), sin(angle/2)
x = self.default_value_s0
y = self.default_value_s1
z = self.default_value_s2
veclen = sqrt(x**2+y**2+z**2)
if veclen<1E-5:
self.default_value_raw = mathutils.Vector((0.0,0.0,0.0,1.0))
else:
self.default_value_raw = mathutils.Vector((
x/veclen * sang,
y/veclen * sang,
z/veclen * sang,
cang
))
else:
if self.default_value_unit == 'Deg':
x = self.default_value_s0 * pi/180
y = self.default_value_s1 * pi/180
z = self.default_value_s2 * pi/180
else:
x = self.default_value_s0
y = self.default_value_s1
z = self.default_value_s2
cx, sx = cos(x/2), sin(x/2)
cy, sy = cos(y/2), sin(y/2)
cz, sz = cos(z/2), sin(z/2)
qw, qx, qy, qz = 1.0,0.0,0.0,0.0
for direction in self.default_value_order[::-1]:
qwi, qxi,qyi,qzi = {'X': (cx,sx,0,0), 'Y': (cy,0,sy,0), 'Z': (cz,0,0,sz)}[direction]
qw = qw*qwi -qx*qxi -qy*qyi -qz*qzi
qx = qx*qwi +qw*qxi +qy*qzi -qz*qyi
qy = qy*qwi +qw*qyi +qz*qxi -qx*qzi
qz = qz*qwi +qw*qzi +qx*qyi -qy*qxi
self.default_value_raw = mathutils.Vector((qx,qy,qz,qw))
def draw(self, context, layout, node, text):
if (self.is_output or self.is_linked):
layout.label(text=self.name)
else:
coll1 = layout.column(align=True)
coll1.label(text=self.name)
bx=coll1.box()
coll = bx.column(align=True)
coll.prop(self, 'default_value_mode')
if self.default_value_mode in ('EulerAngles', 'AxisAngle'):
coll.prop(self, 'default_value_unit')
if self.default_value_mode == 'EulerAngles':
coll.prop(self, 'default_value_order')
coll.prop(self, 'default_value_s0', text='X')
coll.prop(self, 'default_value_s1', text='Y')
coll.prop(self, 'default_value_s2', text='Z')
elif self.default_value_mode == 'Quat':
coll.prop(self, 'default_value_s0', text='X')
coll.prop(self, 'default_value_s1', text='Y')
coll.prop(self, 'default_value_s2', text='Z')
coll.prop(self, 'default_value_s3', text='W')
elif self.default_value_mode == 'AxisAngle':
coll.prop(self, 'default_value_s0', text='X')
coll.prop(self, 'default_value_s1', text='Y')
coll.prop(self, 'default_value_s2', text='Z')
coll.separator()
coll.prop(self, 'default_value_s3', text='Angle')
def draw_color(self, context, node):
return 0.68, 0.22, 0.62, 1
default_value_mode: EnumProperty(
items=[('EulerAngles', 'Euler Angles', 'Euler Angles'),
('AxisAngle', 'Axis/Angle', 'Axis/Angle'),
('Quat', 'Quaternion', 'Quaternion')],
name='', default='EulerAngles',
update=on_mode_update)
default_value_unit: EnumProperty(
items=[('Deg', 'Degrees', 'Degrees'),
('Rad', 'Radians', 'Radians')],
name='', default='Rad',
update=on_unit_update)
default_value_order: EnumProperty(
items=[('XYZ','XYZ','XYZ'),
('XZY','XZY (legacy Armory euler order)','XZY (legacy Armory euler order)'),
('YXZ','YXZ','YXZ'),
('YZX','YZX','YZX'),
('ZXY','ZXY','ZXY'),
('ZYX','ZYX','ZYX')],
name='', default='XYZ'
)
default_value_s0: FloatProperty(update=do_update_raw)
default_value_s1: FloatProperty(update=do_update_raw)
default_value_s2: FloatProperty(update=do_update_raw)
default_value_s3: FloatProperty(update=do_update_raw)
default_value_raw: FloatVectorProperty(size=4, default=(0,0,0,1))
class ArmArraySocket(ArmCustomSocket):
bl_idname = 'ArmNodeSocketArray'
@ -118,6 +283,7 @@ class ArmObjectSocket(ArmCustomSocket):
def register():
bpy.utils.register_class(ArmActionSocket)
bpy.utils.register_class(ArmAnimActionSocket)
bpy.utils.register_class(ArmRotationSocket)
bpy.utils.register_class(ArmArraySocket)
bpy.utils.register_class(ArmObjectSocket)
@ -126,4 +292,5 @@ def unregister():
bpy.utils.unregister_class(ArmObjectSocket)
bpy.utils.unregister_class(ArmArraySocket)
bpy.utils.unregister_class(ArmAnimActionSocket)
bpy.utils.unregister_class(ArmRotationSocket)
bpy.utils.unregister_class(ArmActionSocket)

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):
super(GetRotationNode, self).init(context)
self.add_input('ArmNodeSocketObject', 'Object')
self.add_output('ArmNodeSocketRotation', 'Rotation')
self.add_output('NodeSocketVector', 'Euler Angles')
self.add_output('NodeSocketVector', 'Vector')
self.add_output('NodeSocketFloat', 'Angle (Radians)')
self.add_output('NodeSocketFloat', 'Angle (Degrees)')
self.add_output('NodeSocketVector', 'Quaternion XYZ')
self.add_output('NodeSocketFloat', 'Quaternion W')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
property0: EnumProperty(
items = [('Local', 'Local', 'Local'),
('Global', 'Global', 'Global')],
name='', default='Local')

View file

@ -1,7 +1,7 @@
from arm.logicnode.arm_nodes import *
class LookAtNode(ArmLogicTreeNode):
"""Converts the two given coordinates to a quaternion rotation."""
"""Returns *a* rotation that makes something look away from X,Y or Z, and instead look in the 'from->to' direction"""
bl_idname = 'LNLookAtNode'
bl_label = 'Look At'
arm_section = 'rotation'
@ -21,7 +21,7 @@ class LookAtNode(ArmLogicTreeNode):
self.add_input('NodeSocketVector', 'From Location')
self.add_input('NodeSocketVector', 'To Location')
self.add_output('NodeSocketVector', 'Rotation')
self.add_output('ArmNodeSocketRotation', 'Rotation')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')

View file

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

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_output('NodeSocketVector', 'Location')
self.add_output('NodeSocketVector', 'Rotation')
self.add_output('ArmNodeSocketRotation', 'Rotation')
self.add_output('NodeSocketVector', 'Scale')

View file

@ -6,37 +6,20 @@ class SetRotationNode(ArmLogicTreeNode):
bl_label = 'Set Object Rotation'
arm_section = 'rotation'
arm_version = 1
def init(self, context):
super(SetRotationNode, self).init(context)
self.add_input('ArmNodeSocketAction', 'In')
self.add_input('ArmNodeSocketObject', 'Object')
self.add_input('NodeSocketVector', 'Euler Angles / Vector XYZ')
self.add_input('NodeSocketFloat', 'Angle / W')
self.add_input('ArmNodeSocketRotation', 'Rotation')
self.add_output('ArmNodeSocketAction', 'Out')
def on_property_update(self, context):
"""called by the EnumProperty, used to update the node socket labels"""
if self.property0 == "Quaternion":
self.inputs[2].name = "Quaternion XYZ"
self.inputs[3].name = "Quaternion W"
elif self.property0 == "Euler Angles":
self.inputs[2].name = "Euler Angles"
self.inputs[3].name = "[unused for Euler input]"
elif self.property0.startswith("Angle Axies"):
self.inputs[2].name = "Axis"
self.inputs[3].name = "Angle"
else:
raise ValueError('No nodesocket labels for current input mode: check self-consistancy of action_set_rotation.py')
# def draw_buttons(self, context, layout):
# layout.prop(self, 'property0')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
property0: EnumProperty(
items = [('Euler Angles', 'Euler Angles', 'Euler Angles'),
('Angle Axies (Radians)', 'Angle Axies (Radians)', 'Angle Axies (Radians)'),
('Angle Axies (Degrees)', 'Angle Axies (Degrees)', 'Angle Axies (Degrees)'),
('Quaternion', 'Quaternion', 'Quaternion')],
name='', default='Euler Angles',
update=on_property_update)
# property0: EnumProperty(
# items = [('Local', 'Local', 'Local'),
# ('Global', 'Global', 'Global')],
# name='', default='Local')

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):
super(TransformNode, self).init(context)
self.add_input('NodeSocketVector', 'Location')
self.add_input('NodeSocketVector', 'Rotation')
self.add_input('ArmNodeSocketRotation', 'Rotation')
self.add_input('NodeSocketVector', 'Scale', default_value=[1.0, 1.0, 1.0])
self.add_output('NodeSocketShader', 'Transform')
self.add_output('NodeSocketShader', 'Transform', is_var=True)

View file

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

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':
return f'new armory.logicnode.VectorNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]})'
elif inp_type == 'ROTATION': # a rotation is internally represented as a quaternion.
return f'new armory.logicnode.RotationNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]}, {default_value[3]})'
elif inp_type == 'RGBA':
return f'new armory.logicnode.ColorNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]}, {default_value[3]})'
elif inp_type == 'RGB':