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
3 changed files with 129 additions and 215 deletions
Showing only changes of commit fea3f043ed - Show all commits

View File

@ -146,6 +146,7 @@ def register() -> None:
def unregister() -> None: def unregister() -> None:
bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update_post) bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update_post)
bpy.app.handlers.frame_change_post.remove(_on_frame_changed_post)
del bpy.types.Scene.powership_mode del bpy.types.Scene.powership_mode

301
nodes.py
View File

@ -67,10 +67,7 @@ class PowerShipNodeTree(bpy.types.NodeTree):
def _prepare_nodes(self) -> None: def _prepare_nodes(self) -> None:
for node in self.nodes: for node in self.nodes:
try:
node.reset_run() node.reset_run()
except AttributeError:
print(node)
def _run_from_node( def _run_from_node(
self, depsgraph: bpy.types.Depsgraph, start_node: "AbstractPowerShipNode" self, depsgraph: bpy.types.Depsgraph, start_node: "AbstractPowerShipNode"
@ -234,7 +231,6 @@ class AbstractPowerShipNode(bpy.types.Node):
def run(self, depsgraph: bpy.types.Depsgraph) -> None: def run(self, depsgraph: bpy.types.Depsgraph) -> None:
assert not self.has_run, "a node can only run once, reset it first" assert not self.has_run, "a node can only run once, reset it first"
if self.mute: if self.mute:
# Skip execution of this node, it's muted. # Skip execution of this node, it's muted.
self._first_input_to_output() self._first_input_to_output()
@ -278,7 +274,6 @@ class AbstractPowerShipNode(bpy.types.Node):
def _first_input_to_output(self) -> None: def _first_input_to_output(self) -> None:
"""Copy the first input's default value to the output, if the sockets are compatible.""" """Copy the first input's default value to the output, if the sockets are compatible."""
if not self.inputs or not self.outputs: if not self.inputs or not self.outputs:
return return
@ -592,8 +587,6 @@ class ToVector(AbstractPowerShipNode):
bl_icon = "EMPTY_ARROWS" bl_icon = "EMPTY_ARROWS"
def init(self, context): def init(self, context):
self.add_execution_sockets()
self.add_optional_input_socket("NodeSocketFloat", "X") self.add_optional_input_socket("NodeSocketFloat", "X")
self.add_optional_input_socket("NodeSocketFloat", "Y") self.add_optional_input_socket("NodeSocketFloat", "Y")
self.add_optional_input_socket("NodeSocketFloat", "Z") self.add_optional_input_socket("NodeSocketFloat", "Z")
@ -621,8 +614,6 @@ class SplitVector(AbstractPowerShipNode):
bl_icon = "EMPTY_ARROWS" bl_icon = "EMPTY_ARROWS"
def init(self, context): def init(self, context):
self.add_execution_sockets()
self.add_optional_input_socket("NodeSocketVector", "Vector") self.add_optional_input_socket("NodeSocketVector", "Vector")
self.outputs.new("NodeSocketFloat", "X") self.outputs.new("NodeSocketFloat", "X")
self.outputs.new("NodeSocketFloat", "Y") self.outputs.new("NodeSocketFloat", "Y")
@ -630,10 +621,6 @@ class SplitVector(AbstractPowerShipNode):
def execute(self, depsgraph: bpy.types.Depsgraph) -> None: def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
v = self._get_optional_input_value("Vector", Vector) v = self._get_optional_input_value("Vector", Vector)
if not v:
return
self.outputs["X"].default_value = v.x self.outputs["X"].default_value = v.x
self.outputs["Y"].default_value = v.y self.outputs["Y"].default_value = v.y
self.outputs["Z"].default_value = v.z self.outputs["Z"].default_value = v.z
@ -647,8 +634,6 @@ class Distance(AbstractPowerShipNode):
bl_icon = "EMPTY_ARROWS" bl_icon = "EMPTY_ARROWS"
def init(self, context): def init(self, context):
self.add_execution_sockets()
self.add_optional_input_socket("NodeSocketVector", "U") self.add_optional_input_socket("NodeSocketVector", "U")
self.add_optional_input_socket("NodeSocketVector", "V") self.add_optional_input_socket("NodeSocketVector", "V")
self.outputs.new("NodeSocketFloat", "Float") self.outputs.new("NodeSocketFloat", "Float")
@ -671,8 +656,6 @@ class NormalFromPoints(AbstractPowerShipNode):
bl_icon = "EMPTY_ARROWS" bl_icon = "EMPTY_ARROWS"
def init(self, context): def init(self, context):
self.add_execution_sockets()
self.add_optional_input_socket("NodeSocketVector", "U") self.add_optional_input_socket("NodeSocketVector", "U")
self.add_optional_input_socket("NodeSocketVector", "V") self.add_optional_input_socket("NodeSocketVector", "V")
self.add_optional_input_socket("NodeSocketVector", "W") self.add_optional_input_socket("NodeSocketVector", "W")
@ -682,83 +665,26 @@ class NormalFromPoints(AbstractPowerShipNode):
u = self._get_optional_input_value("U", Vector) u = self._get_optional_input_value("U", Vector)
v = self._get_optional_input_value("V", Vector) v = self._get_optional_input_value("V", Vector)
w = self._get_optional_input_value("W", Vector) w = self._get_optional_input_value("W", Vector)
if not (u and v and w):
return
a = v - u a = v - u
b = w - u b = w - u
normal = a.cross(b) normal = a.cross(b)
self.outputs["Result"].default_value = normal self.outputs["Result"].default_value = normal
class UpAxisSocket(bpy.types.NodeSocket): _enum_up_axis_items = (
'''Custom node socket type'''
bl_idname = 'UpAxisSocket'
bl_label = "Up Axis Socket"
link_limit = 0
# Enum items list
axis_items = (
('X', "X", ""), ('X', "X", ""),
('Y', "Y", ""), ('Y', "Y", ""),
('Z', "Z", ""), ('Z', "Z", ""),
) )
# default_value = "UP"
default_value: bpy.props.EnumProperty( _enum_track_axis_items = (
name="Axis",
description="",
items=axis_items,
default='X',
)
# Optional function for drawing the socket input value
def draw(self, context, layout, node, text):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
layout.prop(self, "default_value", text=text)
def draw_color(self, context, node):
return (1.0, 0.4, 0.216, 0.5)
class TrackAxisSocket(bpy.types.NodeSocket):
'''Custom node socket type'''
bl_idname = 'TrackAxisSocket'
bl_label = "Track Axis Socket"
link_limit = 0
# Enum items list
axis_items = (
('X', "X", ""), ('X', "X", ""),
('Y', "Y", ""), ('Y', "Y", ""),
('Z', "Z", ""), ('Z', "Z", ""),
('-X', "-X", ""), ('-X', "-X", ""),
('-Y', "-Y", ""), ('-Y', "-Y", ""),
('-Z', "-Z", ""), ('-Z', "-Z", ""),
) )
# default_value = "UP"
default_value: bpy.props.EnumProperty(
name="Axis",
description="",
items=axis_items,
default='Y',
)
# Optional function for drawing the socket input value
def draw(self, context, layout, node, text):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
layout.prop(self, "default_value", text=text)
# Socket color
def draw_color(self, context, node):
return (1.0, 0.4, 0.216, 0.5)
class RotateTowards(AbstractPowerShipNode): class RotateTowards(AbstractPowerShipNode):
@ -768,12 +694,24 @@ class RotateTowards(AbstractPowerShipNode):
bl_label = "Rotate Towards" bl_label = "Rotate Towards"
bl_icon = "EMPTY_ARROWS" bl_icon = "EMPTY_ARROWS"
track: bpy.props.EnumProperty( # type: ignore
name="Track",
items=_enum_track_axis_items,
)
up: bpy.props.EnumProperty( # type: ignore
name="Up",
items=_enum_up_axis_items,
)
def draw_buttons(
self, context: bpy.types.Context, layout: bpy.types.UILayout
) -> None:
super().draw_buttons(context, layout)
layout.prop(self, "up")
layout.prop(self, "track")
def init(self, context): def init(self, context):
self.add_execution_sockets()
self.add_optional_input_socket("UpAxisSocket", "Up")
self.add_optional_input_socket("TrackAxisSocket", "Track")
self.add_optional_input_socket("NodeSocketVector", "Origin") self.add_optional_input_socket("NodeSocketVector", "Origin")
self.add_optional_input_socket("NodeSocketVector", "Destination") self.add_optional_input_socket("NodeSocketVector", "Destination")
self.outputs.new("NodeSocketQuaternion", "Rotation") self.outputs.new("NodeSocketQuaternion", "Rotation")
@ -781,16 +719,14 @@ class RotateTowards(AbstractPowerShipNode):
def execute(self, depsgraph: bpy.types.Depsgraph) -> None: def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
origin = self._get_optional_input_value("Origin", Vector) origin = self._get_optional_input_value("Origin", Vector)
destination = self._get_optional_input_value("Destination", Vector) destination = self._get_optional_input_value("Destination", Vector)
track = self._get_input_value("Track", str)
up = self._get_input_value("Up", str)
if not (origin and destination): if not (origin and destination):
return return Quaternion()
# Set the rotation of the control # Set the rotation of the control
vec = Vector((destination - origin)) vec = Vector((destination - origin))
vec.normalize() # vec.normalize()
rot = vec.to_track_quat(track, up) rot = vec.to_track_quat(self.track, self.up)
rot.normalize()
self.outputs["Rotation"].default_value = rot self.outputs["Rotation"].default_value = rot
@ -800,9 +736,9 @@ class OffsetRotation(AbstractPowerShipNode):
bl_idname = "OffsetRotation" bl_idname = "OffsetRotation"
bl_label = "Offset Rotation" bl_label = "Offset Rotation"
bl_icon = "EMPTY_ARROWS" bl_icon = "EMPTY_ARROWS"
default_value = Quaternion()
def init(self, context): def init(self, context):
self.add_execution_sockets()
self.add_optional_input_socket("NodeSocketQuaternion", "Base") self.add_optional_input_socket("NodeSocketQuaternion", "Base")
self.add_optional_input_socket("NodeSocketQuaternion", "Offset") self.add_optional_input_socket("NodeSocketQuaternion", "Offset")
self.outputs.new("NodeSocketQuaternion", "Rotation") self.outputs.new("NodeSocketQuaternion", "Rotation")
@ -810,10 +746,6 @@ class OffsetRotation(AbstractPowerShipNode):
def execute(self, depsgraph: bpy.types.Depsgraph) -> None: def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
base = self._get_optional_input_value("Base", Quaternion) base = self._get_optional_input_value("Base", Quaternion)
offset = self._get_optional_input_value("Offset", Quaternion) offset = self._get_optional_input_value("Offset", Quaternion)
if not (base and offset):
return
self.outputs["Rotation"].default_value = base @ offset self.outputs["Rotation"].default_value = base @ offset
@ -825,8 +757,6 @@ class MapRange(AbstractPowerShipNode):
bl_icon = "EMPTY_ARROWS" bl_icon = "EMPTY_ARROWS"
def init(self, context): def init(self, context):
self.add_execution_sockets()
self.add_optional_input_socket("NodeSocketFloat", "Value") self.add_optional_input_socket("NodeSocketFloat", "Value")
self.add_optional_input_socket("NodeSocketFloat", "From Min") self.add_optional_input_socket("NodeSocketFloat", "From Min")
self.add_optional_input_socket("NodeSocketFloat", "From Max") self.add_optional_input_socket("NodeSocketFloat", "From Max")
@ -841,55 +771,37 @@ class MapRange(AbstractPowerShipNode):
tmin = self._get_optional_input_value("Value", float) tmin = self._get_optional_input_value("Value", float)
tmax = self._get_optional_input_value("Value", float) tmax = self._get_optional_input_value("Value", float)
if not (val and fmin and fmax and tmin and tmax):
return
slope = (tmax - tmin) / (fmax - fmin) slope = (tmax - tmin) / (fmax - fmin)
offset = tmin - slope * fmin offset = tmin - slope * fmin
self.outputs["Result"].default_value = slope * val + offset self.outputs["Result"].default_value = slope * val + offset
class AngleTypeSocket(bpy.types.NodeSocket):
bl_idname = 'AngleTypeSocket'
bl_label = "Angle Type"
link_limit = 0
# Enum items list
ops_items = (
('SIGNED', "Signed", ""),
('UNSIGNED', "Default", ""),
)
# default_value = "UP"
default_value: bpy.props.EnumProperty(
name="Operation",
description="",
items=ops_items,
default='UNSIGNED',
)
# Optional function for drawing the socket input value
def draw(self, context, layout, node, text):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
layout.prop(self, "default_value", text=text)
# Socket color
def draw_color(self, context, node):
return (1.0, 0.4, 0.216, 0.5)
class RotationFromAngle(AbstractPowerShipNode): class RotationFromAngle(AbstractPowerShipNode):
bl_idname = "RotationFromAngle" bl_idname = "RotationFromAngle"
bl_label = "Rotation From Vector Angle" bl_label = "Rotation From Vector Angle"
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
name="Type",
items=[
("DEFAULT", "Default", ""),
("SIGNED", "Signed", ""),
],
)
def draw_buttons(
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): def init(self, context):
self.add_execution_sockets()
self.add_optional_input_socket("UpAxisSocket", "Axis")
self.add_optional_input_socket("AngleTypeSocket", "Type")
self.add_optional_input_socket("NodeSocketVector", "U") self.add_optional_input_socket("NodeSocketVector", "U")
self.add_optional_input_socket("NodeSocketVector", "V") self.add_optional_input_socket("NodeSocketVector", "V")
self.outputs.new("NodeSocketQuaternion", "Rotation") self.outputs.new("NodeSocketQuaternion", "Rotation")
@ -897,60 +809,28 @@ class RotationFromAngle(AbstractPowerShipNode):
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)
v = self._get_optional_input_value("V", Vector) v = self._get_optional_input_value("V", Vector)
axis = self._get_input_value("Axis", str) signed = self.angle_type == 'SIGNED'
angle_type = self._get_input_value("Type", str)
signed = angle_type == 'SIGNED'
if not (u and v):
return
angle = 0 angle = 0
if not (u.length == 0 or v.length == 0): if not (u.length == 0 or v.length == 0):
if signed: if signed:
angle = u.angle_signed(v) angle = u.angle_signed(v)
else: else:
angle = u.angle(v) angle = u.angle(v)
m = Matrix.Rotation(angle, 3, axis) m = Matrix.Rotation(angle, 3, self.axis)
res = m.to_quaternion() res = m.to_quaternion()
self.outputs["Rotation"].default_value = res self.outputs["Rotation"].default_value = res
class MathOperationSocket(bpy.types.NodeSocket): _enum_vector_math_operations = [
bl_idname = 'MathOperationSocket'
bl_label = "Operations"
link_limit = 0
# Enum items list
ops_items = (
('ADD', "Add", ""), ('ADD', "Add", ""),
('SUBSTRACT', "Substract", ""), ('SUBSTRACT', "Substract", ""),
('MULTIPLY', "Mutliply", ""), ('MULTIPLY', "Mutliply", ""),
('DIVIDE', "Divide", ""), ('DIVIDE', "Divide", ""),
('CROSS', "Cross", ""), ('CROSS', "Cross", ""),
) ]
# default_value = "UP"
default_value: bpy.props.EnumProperty(
name="Operation",
description="",
items=ops_items,
default='ADD',
)
# Optional function for drawing the socket input value
def draw(self, context, layout, node, text):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
layout.prop(self, "default_value", text=text)
# Socket color
def draw_color(self, context, node):
return (1.0, 0.4, 0.216, 0.5)
class VectorMath(AbstractPowerShipNode): class VectorMath(AbstractPowerShipNode):
@ -960,9 +840,18 @@ class VectorMath(AbstractPowerShipNode):
bl_label = "Vector Math" bl_label = "Vector Math"
bl_icon = "EMPTY_ARROWS" bl_icon = "EMPTY_ARROWS"
operation: bpy.props.EnumProperty( # type: ignore
name="Operation",
items=_enum_vector_math_operations,
)
def draw_buttons(
self, context: bpy.types.Context, layout: bpy.types.UILayout
) -> None:
super().draw_buttons(context, layout)
layout.prop(self, "operation")
def init(self, context): def init(self, context):
self.add_execution_sockets()
self.add_optional_input_socket("MathOperationSocket", "Operation")
self.add_optional_input_socket("NodeSocketVector", "U") self.add_optional_input_socket("NodeSocketVector", "U")
self.add_optional_input_socket("NodeSocketVector", "V") self.add_optional_input_socket("NodeSocketVector", "V")
self.outputs.new("NodeSocketVector", "Result") self.outputs.new("NodeSocketVector", "Result")
@ -970,12 +859,8 @@ class VectorMath(AbstractPowerShipNode):
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)
v = self._get_optional_input_value("V", Vector) v = self._get_optional_input_value("V", Vector)
mode = self._get_input_value("Operation", str)
if not (u and v): match self.operation:
return
match mode:
case 'ADD': case 'ADD':
res = u+v res = u+v
case 'MULTIPLY': case 'MULTIPLY':
@ -983,15 +868,24 @@ class VectorMath(AbstractPowerShipNode):
case 'SUBSTRACT': case 'SUBSTRACT':
res = u-v res = u-v
case 'DIVIDE': case 'DIVIDE':
res = Vector([x/y for x, y in zip(u, v)]) res = Vector([x/y if y != 0.0 else 0.0 for x, y in zip(u, v)])
case 'CROSS': case 'CROSS':
res = u.cross(v) res = u.cross(v)
case _: case _:
print("Vector math operation not found:", self.operation)
raise ValueError raise ValueError
self.outputs["Result"].default_value = res self.outputs["Result"].default_value = res
_enum_math_operations = [
('ADD', "Add", ""),
('SUBSTRACT', "Substract", ""),
('MULTIPLY', "Mutliply", ""),
('DIVIDE', "Divide", ""),
]
class Math(AbstractPowerShipNode): class Math(AbstractPowerShipNode):
"""Sets the location and/or rotation of the 3D cursor""" """Sets the location and/or rotation of the 3D cursor"""
@ -999,9 +893,18 @@ class Math(AbstractPowerShipNode):
bl_label = "Math" bl_label = "Math"
bl_icon = "EMPTY_ARROWS" bl_icon = "EMPTY_ARROWS"
operation: bpy.props.EnumProperty( # type: ignore
name="Operation",
items=_enum_math_operations,
)
def draw_buttons(
self, context: bpy.types.Context, layout: bpy.types.UILayout
) -> None:
super().draw_buttons(context, layout)
layout.prop(self, "operation")
def init(self, context): def init(self, context):
self.add_execution_sockets()
self.add_optional_input_socket("MathOperationSocket", "Operation")
self.add_optional_input_socket("NodeSocketFloat", "U") self.add_optional_input_socket("NodeSocketFloat", "U")
self.add_optional_input_socket("NodeSocketFloat", "V") self.add_optional_input_socket("NodeSocketFloat", "V")
self.outputs.new("NodeSocketFloat", "Result") self.outputs.new("NodeSocketFloat", "Result")
@ -1009,12 +912,8 @@ class Math(AbstractPowerShipNode):
def execute(self, depsgraph: bpy.types.Depsgraph) -> None: def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
u = self._get_optional_input_value("U", float) u = self._get_optional_input_value("U", float)
v = self._get_optional_input_value("V", float) v = self._get_optional_input_value("V", float)
mode = self._get_input_value("Operation", str)
if not (u and v): match self.operation:
return
match mode:
case 'ADD': case 'ADD':
res = u+v res = u+v
case 'MULTIPLY': case 'MULTIPLY':
@ -1022,9 +921,9 @@ class Math(AbstractPowerShipNode):
case 'SUBSTRACT': case 'SUBSTRACT':
res = u-v res = u-v
case 'DIVIDE': case 'DIVIDE':
res = u/v res = u/v if v != 0 else 0
case _: case _:
print("MODE NOT FOUND:", mode) print("Math operation not found:", self.operation)
raise ValueError raise ValueError
self.outputs["Result"].default_value = res self.outputs["Result"].default_value = res
@ -1062,6 +961,17 @@ class SetBoneNode(AbstractPowerShipNode):
bl_label = "Set Bone" bl_label = "Set Bone"
bl_icon = "BONE_DATA" bl_icon = "BONE_DATA"
space: bpy.props.EnumProperty( # type: ignore
name="Space",
items=_enum_control_space,
)
def draw_buttons(
self, context: bpy.types.Context, layout: bpy.types.UILayout
) -> None:
super().draw_buttons(context, layout)
layout.prop(self, "space")
def init(self, context): def init(self, context):
self.add_execution_sockets() self.add_execution_sockets()
@ -1103,13 +1013,20 @@ class SetBoneNode(AbstractPowerShipNode):
if control_scale is not None: if control_scale is not None:
scale = control_scale 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]]
v_nil = Vector((0, 0, 0)) v_nil = Vector((0, 0, 0))
bone_rest_rot_scale = bone.bone.matrix_local.copy() 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_scale v_nil, rot, scale) @ bone_rest_rot.to_matrix().to_4x4()
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
@ -1510,10 +1427,6 @@ classes = (
# Socket types: # Socket types:
NodeSocketExecute, NodeSocketExecute,
NodeSocketQuaternion, NodeSocketQuaternion,
MathOperationSocket,
TrackAxisSocket,
UpAxisSocket,
AngleTypeSocket,
# Nodes: # Nodes:
ForwardSolveNode, ForwardSolveNode,
BackwardSolveNode, BackwardSolveNode,

Binary file not shown.