Motion transfer setup #1

Manually merged
Sybren A. Stüvel merged 18 commits from cgtinker/powership:motion_transfer into main 2023-06-05 12:06:15 +02:00
2 changed files with 77 additions and 84 deletions
Showing only changes of commit e27f71abec - Show all commits

163
nodes.py
View File

@ -724,10 +724,8 @@ class RotateTowards(AbstractPowerShipNode):
# Set the rotation of the control
vec = Vector((destination - origin))
# vec.normalize()
rot = vec.to_track_quat(self.track, self.up)
cgtinker marked this conversation as resolved Outdated

default_value doesn't seem used.

`default_value` doesn't seem used.
rot.normalize()
self.outputs["Rotation"].default_value = rot
self.outputs["Rotation"].default_value = rot.normalized()
class OffsetRotation(AbstractPowerShipNode):
@ -776,20 +774,15 @@ class MapRange(AbstractPowerShipNode):
self.outputs["Result"].default_value = slope * val + offset

Why name it DEFAULT instead of UNSIGNED? And why is UNSIGNED the default value? Not saying that it should be the other one, just wondering about your thought process.

Why name it `DEFAULT` instead of `UNSIGNED`? And why is `UNSIGNED` the default value? Not saying that it should be the other one, just wondering about your thought process.

Usually an angle is not signed, you got to define something to sign it. That's why I thought unsigned angles should be the default.

I rechecked and to be honest, I neither knew that the "signed" vector method is only for 2D vectors nor how it actually signed an angle. So I guess I removed it and kept only the "unsigned" angle for now - my bad sorry. Kinda liked the idea of an easy to use signed angle.

So far, when I needed a signed angle, I ended up using a plane. Basically I've used the signed distance (based on the normal) from the plane to the destination of the vector and used the sign for the angle. This is possible with the current system without changes so I guess it's fine.

Usually an angle is not signed, you got to define something to sign it. That's why I thought unsigned angles should be the default. I rechecked and to be honest, I neither knew that the "signed" vector method is only for 2D vectors nor how it actually signed an angle. So I guess I removed it and kept only the "unsigned" angle for now - my bad sorry. Kinda liked the idea of an easy to use signed angle. So far, when I needed a signed angle, I ended up using a plane. Basically I've used the signed distance (based on the normal) from the plane to the destination of the vector and used the sign for the angle. This is possible with the current system without changes so I guess it's fine.
class RotationFromAngle(AbstractPowerShipNode):
bl_idname = "RotationFromAngle"
bl_label = "Rotation From Vector Angle"
class AngleFromVectors(AbstractPowerShipNode):
bl_idname = "AngleFromVectors"
bl_label = "Angle From Vectors"
bl_icon = "EMPTY_ARROWS"
axis: bpy.props.EnumProperty( # type: ignore
name="Axis",
items=_enum_up_axis_items,
)
angle_type: bpy.props.EnumProperty( # type: ignore
name="Type",
items=[
("DEFAULT", "Default", ""),
("DEFAULT", "Unsigned", ""),
("SIGNED", "Signed", ""),
],
)
@ -798,13 +791,12 @@ class RotationFromAngle(AbstractPowerShipNode):
self, context: bpy.types.Context, layout: bpy.types.UILayout
) -> None:
super().draw_buttons(context, layout)
layout.prop(self, "axis")
layout.prop(self, "angle_type")
def init(self, context):
self.add_optional_input_socket("NodeSocketVector", "U")
cgtinker marked this conversation as resolved Outdated

Don't compute length when you can use length_squared as well.

Don't compute `length` when you can use `length_squared` as well.
self.add_optional_input_socket("NodeSocketVector", "V")
self.outputs.new("NodeSocketQuaternion", "Rotation")
self.outputs.new("NodeSocketFloat", "Angle")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
u = self._get_optional_input_value("U", Vector)
@ -818,10 +810,7 @@ class RotationFromAngle(AbstractPowerShipNode):
else:
angle = u.angle(v)
m = Matrix.Rotation(angle, 3, self.axis)
res = m.to_quaternion()
self.outputs["Rotation"].default_value = res
self.outputs["Angle"].default_value = angle
_enum_vector_math_operations = [
@ -1006,6 +995,7 @@ class SetBoneNode(AbstractPowerShipNode):
bone_mat_world: Matrix = arm_matrix @ bone.matrix
loc, rot, scale = bone_mat_world.decompose()
if control_location is not None:
loc = control_location

in which case would rot not be normalized? I'd expect bone_mat_world.decompose() to return a unit quaternion. If you want to normalize the socket value, do that above with rot = control_rotation.normalized().

in which case would `rot` not be normalized? I'd expect `bone_mat_world.decompose()` to return a unit quaternion. If you want to normalize the socket value, do that above with `rot = control_rotation.normalized()`.

I think that can be removed. I was confused because the quaternion affected the scale which indicates that it's not a unit quaternion. This happened on longer bone chains - rounding the scale seemed to fix the issue so I guess it has been some floating point error.

I think that can be removed. I was confused because the quaternion affected the scale which indicates that it's not a unit quaternion. This happened on longer bone chains - rounding the scale seemed to fix the issue so I guess it has been some floating point error.
if control_rotation is not None:
@ -1013,24 +1003,27 @@ class SetBoneNode(AbstractPowerShipNode):
if control_scale is not None:
scale = control_scale
match self.space:
case "WORLD":
bone_mat_world = Matrix.LocRotScale(loc, rot, scale)
case "CHANNELS":
# Not sure what causes the scale to flip, however...
# The y/z-scale flips on update if multiple rotations are chained,
# this generates jitter when running const updates - flipping seems to temp fix it.
scale = [scale[i] for i in [0, 2, 1]]
# TODO: Fix jittering bone scale which happens
# esp. when multiple bones are parented to the rotated bone
# rounding helps but does not entirely fix the issue.
scale = [round(x, 4) for x in scale]
v_nil = Vector((0, 0, 0))
bone_rest_rot_scale = bone.bone.matrix_local.copy()
match self.space:
case "WORLD":
bone_mat_world = Matrix.LocRotScale(
loc, rot.normalized(), scale)
loc, rot, scale = bone_mat_world.decompose()
case "CHANNELS":
bone_rest_rot_scale.translation = v_nil
bone_rest_rot = bone_rest_rot_scale.to_quaternion()
mat_rot_scale = Matrix.LocRotScale(
v_nil, rot, scale) @ bone_rest_rot.to_matrix().to_4x4()
v_nil, rot, scale) @ bone_rest_rot_scale
mat_loc = Matrix.Translation(loc)
bone_mat_world = mat_loc @ mat_rot_scale
bone.matrix = arm_matrix.inverted() @ bone_mat_world
loc, rot, scale = bone.matrix.decompose()
class TwoBoneIKNode(AbstractPowerShipNode):
@ -1237,61 +1230,61 @@ class ClampNode(AbstractPowerShipNode):
self.outputs["Result"].default_value = clamped
class AbstractTwoValueMathNode(AbstractPowerShipNode):
def init(self, context: bpy.types.Context) -> None:
self.inputs.new("NodeSocketFloat", "A")
self.inputs.new("NodeSocketFloat", "B")
self.outputs.new("NodeSocketFloat", "Result")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
a = self._get_input_value("A", float)
b = self._get_input_value("B", float)
self.outputs["Result"].default_value = self.calculate(a, b)
def calculate(self, a: float, b: float) -> float:
return 0
class AddNode(AbstractTwoValueMathNode):
"""Add two values"""
bl_idname = "AddNode"
bl_label = "Add"
def calculate(self, a: float, b: float) -> float:
return a + b
class SubtractNode(AbstractTwoValueMathNode):
"""Subtract two values"""
bl_idname = "SubtractNode"
bl_label = "Subtract"
def calculate(self, a: float, b: float) -> float:
return a - b
class MultiplyNode(AbstractTwoValueMathNode):
"""Multiply two values"""
bl_idname = "MultiplyNode"
bl_label = "Multiply"
def calculate(self, a: float, b: float) -> float:
return a * b
class DivideNode(AbstractTwoValueMathNode):
"""Divide two values; division by zero results in NaN"""
bl_idname = "DivideNode"
bl_label = "Divide"
def calculate(self, a: float, b: float) -> float:
if b == 0:
return float("nan")
return a / b
# class AbstractTwoValueMathNode(AbstractPowerShipNode):
# def init(self, context: bpy.types.Context) -> None:
# self.inputs.new("NodeSocketFloat", "A")
# self.inputs.new("NodeSocketFloat", "B")
# self.outputs.new("NodeSocketFloat", "Result")
#
# def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
# a = self._get_input_value("A", float)
# b = self._get_input_value("B", float)
# self.outputs["Result"].default_value = self.calculate(a, b)
#
# def calculate(self, a: float, b: float) -> float:
# return 0
#
#
# class AddNode(AbstractTwoValueMathNode):
# """Add two values"""
#
# bl_idname = "AddNode"
# bl_label = "Add"
#
# def calculate(self, a: float, b: float) -> float:
# return a + b
#
#
# class SubtractNode(AbstractTwoValueMathNode):
# """Subtract two values"""
#
# bl_idname = "SubtractNode"
# bl_label = "Subtract"
#
# def calculate(self, a: float, b: float) -> float:
# return a - b
#
#
# class MultiplyNode(AbstractTwoValueMathNode):
# """Multiply two values"""
#
# bl_idname = "MultiplyNode"
# bl_label = "Multiply"
#
# def calculate(self, a: float, b: float) -> float:
# return a * b
#
#
# class DivideNode(AbstractTwoValueMathNode):
# """Divide two values; division by zero results in NaN"""
#
# bl_idname = "DivideNode"
# bl_label = "Divide"
#
# def calculate(self, a: float, b: float) -> float:
# if b == 0:
# return float("nan")
# return a / b
def _on_num_sockets_change(self: "SequenceNode", context: bpy.types.Context) -> None:
@ -1394,7 +1387,7 @@ node_categories = [
items=[
nodeitems_utils.NodeItem("RotateTowards"),
nodeitems_utils.NodeItem("OffsetRotation"),
nodeitems_utils.NodeItem("RotationFromAngle"),
nodeitems_utils.NodeItem("AngleFromVectors"),
nodeitems_utils.NodeItem("NormalFromPoints"),
nodeitems_utils.NodeItem("SplitVector"),
nodeitems_utils.NodeItem("ToVector"),
@ -1439,7 +1432,7 @@ classes = (
SequenceNode,
# Math Nodes
RotateTowards,
RotationFromAngle,
AngleFromVectors,
OffsetRotation,
NormalFromPoints,
ToVector,

Binary file not shown.