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

161
nodes.py
View File

@ -724,10 +724,8 @@ class RotateTowards(AbstractPowerShipNode):
# Set the rotation of the control # Set the rotation of the control
vec = Vector((destination - origin)) vec = Vector((destination - origin))
# vec.normalize()
rot = vec.to_track_quat(self.track, self.up) 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.normalized()
self.outputs["Rotation"].default_value = rot
class OffsetRotation(AbstractPowerShipNode): class OffsetRotation(AbstractPowerShipNode):
@ -776,20 +774,15 @@ class MapRange(AbstractPowerShipNode):
self.outputs["Result"].default_value = slope * val + offset 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): class AngleFromVectors(AbstractPowerShipNode):
bl_idname = "RotationFromAngle" bl_idname = "AngleFromVectors"
bl_label = "Rotation From Vector Angle" bl_label = "Angle From Vectors"
bl_icon = "EMPTY_ARROWS" bl_icon = "EMPTY_ARROWS"
axis: bpy.props.EnumProperty( # type: ignore
name="Axis",
items=_enum_up_axis_items,
)
angle_type: bpy.props.EnumProperty( # type: ignore angle_type: bpy.props.EnumProperty( # type: ignore
name="Type", name="Type",
items=[ items=[
("DEFAULT", "Default", ""), ("DEFAULT", "Unsigned", ""),
("SIGNED", "Signed", ""), ("SIGNED", "Signed", ""),
], ],
) )
@ -798,13 +791,12 @@ class RotationFromAngle(AbstractPowerShipNode):
self, context: bpy.types.Context, layout: bpy.types.UILayout self, context: bpy.types.Context, layout: bpy.types.UILayout
) -> None: ) -> None:
super().draw_buttons(context, layout) super().draw_buttons(context, layout)
layout.prop(self, "axis")
layout.prop(self, "angle_type") layout.prop(self, "angle_type")
def init(self, context): def init(self, context):
self.add_optional_input_socket("NodeSocketVector", "U") 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.add_optional_input_socket("NodeSocketVector", "V")
self.outputs.new("NodeSocketQuaternion", "Rotation") self.outputs.new("NodeSocketFloat", "Angle")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None: def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
u = self._get_optional_input_value("U", Vector) u = self._get_optional_input_value("U", Vector)
@ -818,10 +810,7 @@ class RotationFromAngle(AbstractPowerShipNode):
else: else:
angle = u.angle(v) angle = u.angle(v)
m = Matrix.Rotation(angle, 3, self.axis) self.outputs["Angle"].default_value = angle
res = m.to_quaternion()
self.outputs["Rotation"].default_value = res
_enum_vector_math_operations = [ _enum_vector_math_operations = [
@ -1006,6 +995,7 @@ class SetBoneNode(AbstractPowerShipNode):
bone_mat_world: Matrix = arm_matrix @ bone.matrix bone_mat_world: Matrix = arm_matrix @ bone.matrix
loc, rot, scale = bone_mat_world.decompose() loc, rot, scale = bone_mat_world.decompose()
if control_location is not None: if control_location is not None:
loc = control_location 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: if control_rotation is not None:
@ -1013,24 +1003,27 @@ class SetBoneNode(AbstractPowerShipNode):
if control_scale is not None: if control_scale is not None:
scale = control_scale scale = control_scale
# 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: match self.space:
case "WORLD": case "WORLD":
bone_mat_world = Matrix.LocRotScale(loc, rot, scale) bone_mat_world = Matrix.LocRotScale(
loc, rot.normalized(), scale)
loc, rot, scale = bone_mat_world.decompose()
case "CHANNELS": 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]]
v_nil = Vector((0, 0, 0))
bone_rest_rot_scale = bone.bone.matrix_local.copy()
bone_rest_rot_scale.translation = v_nil bone_rest_rot_scale.translation = v_nil
bone_rest_rot = bone_rest_rot_scale.to_quaternion()
mat_rot_scale = Matrix.LocRotScale( 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) mat_loc = Matrix.Translation(loc)
bone_mat_world = mat_loc @ mat_rot_scale bone_mat_world = mat_loc @ mat_rot_scale
bone.matrix = arm_matrix.inverted() @ bone_mat_world bone.matrix = arm_matrix.inverted() @ bone_mat_world
loc, rot, scale = bone.matrix.decompose()
class TwoBoneIKNode(AbstractPowerShipNode): class TwoBoneIKNode(AbstractPowerShipNode):
@ -1237,61 +1230,61 @@ class ClampNode(AbstractPowerShipNode):
self.outputs["Result"].default_value = clamped self.outputs["Result"].default_value = clamped
class AbstractTwoValueMathNode(AbstractPowerShipNode): # class AbstractTwoValueMathNode(AbstractPowerShipNode):
def init(self, context: bpy.types.Context) -> None: # def init(self, context: bpy.types.Context) -> None:
self.inputs.new("NodeSocketFloat", "A") # self.inputs.new("NodeSocketFloat", "A")
self.inputs.new("NodeSocketFloat", "B") # self.inputs.new("NodeSocketFloat", "B")
self.outputs.new("NodeSocketFloat", "Result") # self.outputs.new("NodeSocketFloat", "Result")
#
def execute(self, depsgraph: bpy.types.Depsgraph) -> None: # def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
a = self._get_input_value("A", float) # a = self._get_input_value("A", float)
b = self._get_input_value("B", float) # b = self._get_input_value("B", float)
self.outputs["Result"].default_value = self.calculate(a, b) # self.outputs["Result"].default_value = self.calculate(a, b)
#
def calculate(self, a: float, b: float) -> float: # def calculate(self, a: float, b: float) -> float:
return 0 # return 0
#
#
class AddNode(AbstractTwoValueMathNode): # class AddNode(AbstractTwoValueMathNode):
"""Add two values""" # """Add two values"""
#
bl_idname = "AddNode" # bl_idname = "AddNode"
bl_label = "Add" # bl_label = "Add"
#
def calculate(self, a: float, b: float) -> float: # def calculate(self, a: float, b: float) -> float:
return a + b # return a + b
#
#
class SubtractNode(AbstractTwoValueMathNode): # class SubtractNode(AbstractTwoValueMathNode):
"""Subtract two values""" # """Subtract two values"""
#
bl_idname = "SubtractNode" # bl_idname = "SubtractNode"
bl_label = "Subtract" # bl_label = "Subtract"
#
def calculate(self, a: float, b: float) -> float: # def calculate(self, a: float, b: float) -> float:
return a - b # return a - b
#
#
class MultiplyNode(AbstractTwoValueMathNode): # class MultiplyNode(AbstractTwoValueMathNode):
"""Multiply two values""" # """Multiply two values"""
#
bl_idname = "MultiplyNode" # bl_idname = "MultiplyNode"
bl_label = "Multiply" # bl_label = "Multiply"
#
def calculate(self, a: float, b: float) -> float: # def calculate(self, a: float, b: float) -> float:
return a * b # return a * b
#
#
class DivideNode(AbstractTwoValueMathNode): # class DivideNode(AbstractTwoValueMathNode):
"""Divide two values; division by zero results in NaN""" # """Divide two values; division by zero results in NaN"""
#
bl_idname = "DivideNode" # bl_idname = "DivideNode"
bl_label = "Divide" # bl_label = "Divide"
#
def calculate(self, a: float, b: float) -> float: # def calculate(self, a: float, b: float) -> float:
if b == 0: # if b == 0:
return float("nan") # return float("nan")
return a / b # return a / b
def _on_num_sockets_change(self: "SequenceNode", context: bpy.types.Context) -> None: def _on_num_sockets_change(self: "SequenceNode", context: bpy.types.Context) -> None:
@ -1394,7 +1387,7 @@ node_categories = [
items=[ items=[
nodeitems_utils.NodeItem("RotateTowards"), nodeitems_utils.NodeItem("RotateTowards"),
nodeitems_utils.NodeItem("OffsetRotation"), nodeitems_utils.NodeItem("OffsetRotation"),
nodeitems_utils.NodeItem("RotationFromAngle"), nodeitems_utils.NodeItem("AngleFromVectors"),
nodeitems_utils.NodeItem("NormalFromPoints"), nodeitems_utils.NodeItem("NormalFromPoints"),
nodeitems_utils.NodeItem("SplitVector"), nodeitems_utils.NodeItem("SplitVector"),
nodeitems_utils.NodeItem("ToVector"), nodeitems_utils.NodeItem("ToVector"),
@ -1439,7 +1432,7 @@ classes = (
SequenceNode, SequenceNode,
# Math Nodes # Math Nodes
RotateTowards, RotateTowards,
RotationFromAngle, AngleFromVectors,
OffsetRotation, OffsetRotation,
NormalFromPoints, NormalFromPoints,
ToVector, ToVector,

Binary file not shown.