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 106 additions and 142 deletions
Showing only changes of commit a024f31128 - Show all commits

View File

@ -80,8 +80,7 @@ def _on_frame_changed_post(
scene: bpy.types.Scene, depsgraph: bpy.types.Depsgraph
) -> None:
global _skip_next_autorun
if not _skip_next_autorun:
_skip_next_autorun = True
_skip_next_autorun = True
cgtinker marked this conversation as resolved

Since _skip_next_autorun is a boolean, the if condition can be removed.

Since `_skip_next_autorun` is a boolean, the `if` condition can be removed.
run_node_tree(scene, depsgraph)

245
nodes.py
View File

@ -3,8 +3,8 @@
import functools
from collections import deque
from copy import copy
from math import degrees, radians, sqrt
from typing import TypeVar, Callable, Optional, Iterable, Any
from math import degrees, radians
from typing import TypeVar, Callable, Optional, Iterable
import bpy
import nodeitems_utils
@ -271,7 +271,7 @@ class AbstractPowerShipNode(bpy.types.Node):
def _first_input_to_output(self) -> None:
"""Copy the first input's default value to the output, if the sockets are compatible."""
if not self.inputs or not self.outputs:
return
@ -580,25 +580,16 @@ class ToVector(AbstractPowerShipNode):
bl_icon = "EMPTY_ARROWS"
def init(self, context):
self.add_optional_input_socket("NodeSocketFloat", "X")
self.add_optional_input_socket("NodeSocketFloat", "Y")
self.add_optional_input_socket("NodeSocketFloat", "Z")
self.inputs.new("NodeSocketFloat", "X")
self.inputs.new("NodeSocketFloat", "Y")
self.inputs.new("NodeSocketFloat", "Z")
self.outputs.new("NodeSocketVector", "Vector")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
x = self._get_optional_input_value("X", float)
y = self._get_optional_input_value("Y", float)
z = self._get_optional_input_value("Z", float)
v = Vector((0.0, 0.0, 0.0))
if x is not None:
v.x = x
if y is not None:
v.y = y
if z is not None:
v.z = z
self.outputs["Vector"].default_value = v
x = self.inputs["X"].default_value or 0.0
y = self.inputs["Y"].default_value or 0.0
z = self.inputs["Z"].default_value or 0.0
self.outputs["Vector"].default_value = Vector((x, y, z))
class SplitVector(AbstractPowerShipNode):
@ -607,93 +598,88 @@ class SplitVector(AbstractPowerShipNode):
bl_icon = "EMPTY_ARROWS"
def init(self, context):
self.add_optional_input_socket("NodeSocketVector", "Vector")
self.inputs.new("NodeSocketVector", "Vector")
self.outputs.new("NodeSocketFloat", "X")
self.outputs.new("NodeSocketFloat", "Y")
self.outputs.new("NodeSocketFloat", "Z")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
v = self._get_optional_input_value("Vector", Vector)
v = self.inputs["Vector"].default_value
self.outputs["X"].default_value = v.x
self.outputs["Y"].default_value = v.y
self.outputs["Z"].default_value = v.z
class Distance(AbstractPowerShipNode):
""" Calculates distance between two points. """
"""Calculates distance between two points."""
bl_idname = "Distance"
bl_label = "Distance"
bl_icon = "EMPTY_ARROWS"
def init(self, context):
self.add_optional_input_socket("NodeSocketVector", "U")
self.add_optional_input_socket("NodeSocketVector", "V")
self.inputs.new("NodeSocketVector", "U")
self.inputs.new("NodeSocketVector", "V")
self.outputs.new("NodeSocketFloat", "Float")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
u = self._get_optional_input_value("U", Vector)
v = self._get_optional_input_value("V", Vector)
self.outputs["Float"].default_value = (u-v).length
u = self.inputs["U"].default_value
v = self.inputs["V"].default_value
self.outputs["Float"].default_value = (u - v).length
cgtinker marked this conversation as resolved

Does this calculate a normal vector? Or its length?

Does this calculate a normal vector? Or its length?
Review

The length.
I went through the comments and updated them (some were unrelated and happened due to closed eyes copy pasting...)

The length. I went through the comments and updated them (some were unrelated and happened due to closed eyes copy pasting...)
class NormalFromPoints(AbstractPowerShipNode):
""" Calculates normal from three points (plane). """
"""Calculates normal from three points (plane)."""
bl_idname = "NormalFromPoints"
bl_label = "Normal from Points"
bl_icon = "EMPTY_ARROWS"
def init(self, context):
self.add_optional_input_socket("NodeSocketVector", "U")
self.add_optional_input_socket("NodeSocketVector", "V")
self.add_optional_input_socket("NodeSocketVector", "W")
self.inputs.new("NodeSocketVector", "U")
self.inputs.new("NodeSocketVector", "V")
self.inputs.new("NodeSocketVector", "W")
self.outputs.new("NodeSocketVector", "Result")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
u = self._get_optional_input_value("U", Vector)
v = self._get_optional_input_value("V", Vector)
w = self._get_optional_input_value("W", Vector)
u = self.inputs["U"].default_value
v = self.inputs["V"].default_value
w = self.inputs["W"].default_value
a = v - u
b = w - u
normal = a.cross(b)
normal = a.cross(b).normalized()
self.outputs["Result"].default_value = normal
_enum_up_axis_items = (
('X', "X", ""),
('Y', "Y", ""),
('Z', "Z", ""),
("X", "X", ""),
("Y", "Y", ""),
("Z", "Z", ""),
)
_enum_track_axis_items = (
('X', "X", ""),
('Y', "Y", ""),
('Z', "Z", ""),
('-X', "-X", ""),
('-Y', "-Y", ""),
('-Z', "-Z", ""),
("X", "X", ""),
("Y", "Y", ""),
("Z", "Z", ""),
("-X", "-X", ""),
("-Y", "-Y", ""),
("-Z", "-Z", ""),
)
class RotateTowards(AbstractPowerShipNode):
""" Calculate the rotation from a vector with a track and up axis
based on the given origin, destination. """
"""Calculate the rotation from a vector with a track and up axis."""
bl_idname = "RotateTowards"
bl_label = "Rotate Towards"
bl_icon = "EMPTY_ARROWS"
track: bpy.props.EnumProperty( # type: ignore
name="Track",
items=_enum_track_axis_items,
default='X'
name="Track", items=_enum_track_axis_items, default="X"
)
up: bpy.props.EnumProperty( # type: ignore
name="Up",
items=_enum_up_axis_items,
default='Y'
name="Up", items=_enum_up_axis_items, default="Y"
)
def draw_buttons(
@ -704,34 +690,33 @@ class RotateTowards(AbstractPowerShipNode):
layout.prop(self, "track")
def init(self, context):
self.add_optional_input_socket("NodeSocketVector", "Origin")
self.add_optional_input_socket("NodeSocketVector", "Destination")
self.inputs.new("NodeSocketVector", "Vector")
self.inputs.new("NodeSocketVector", "RotateTo")
self.outputs.new("NodeSocketQuaternion", "Rotation")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
origin = self._get_optional_input_value("Origin", Vector)
destination = self._get_optional_input_value("Destination", Vector)
origin = self.inputs["Vector"].default_value
destination = self.inputs["RotateTo"].default_value
vec = Vector((destination - origin))
rot = vec.to_track_quat(self.track, self.up)
self.outputs["Rotation"].default_value = rot.normalized()
class OffsetRotation(AbstractPowerShipNode):
""" Offset a rotation. """
"""Offset a rotation."""
bl_idname = "OffsetRotation"
bl_label = "Offset Rotation"
bl_icon = "EMPTY_ARROWS"
default_value = Quaternion()
def init(self, context):
self.add_optional_input_socket("NodeSocketQuaternion", "Base")
self.add_optional_input_socket("NodeSocketQuaternion", "Offset")
self.inputs.new("NodeSocketQuaternion", "Base")
self.inputs.new("NodeSocketQuaternion", "Offset")
self.outputs.new("NodeSocketQuaternion", "Rotation")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
base = self._get_optional_input_value("Base", Quaternion)
offset = self._get_optional_input_value("Offset", Quaternion)
base = self.inputs["Base"].default_value
offset = self.inputs["Offset"].default_value
self.outputs["Rotation"].default_value = base @ offset
@ -741,19 +726,19 @@ class MapRange(AbstractPowerShipNode):
bl_icon = "EMPTY_ARROWS"
def init(self, context):
self.add_optional_input_socket("NodeSocketFloat", "Value")
self.add_optional_input_socket("NodeSocketFloat", "From Min")
self.add_optional_input_socket("NodeSocketFloat", "From Max")
self.add_optional_input_socket("NodeSocketFloat", "To Min")
self.add_optional_input_socket("NodeSocketFloat", "To Max")
self.inputs.new("NodeSocketFloat", "Value")
self.inputs.new("NodeSocketFloat", "From Min")
self.inputs.new("NodeSocketFloat", "From Max")
self.inputs.new("NodeSocketFloat", "To Min")
self.inputs.new("NodeSocketFloat", "To Max")
self.outputs.new("NodeSocketFloat", "Result")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
val = self._get_optional_input_value("Value", float)
fmin = self._get_optional_input_value("From Min", float)
fmax = self._get_optional_input_value("From Max", float)
tmin = self._get_optional_input_value("To Min", float)
tmax = self._get_optional_input_value("To Max", float)
val = self.inputs["Value"].default_value
fmin = self.inputs["From Min"].default_value
fmax = self.inputs["From Max"].default_value
tmin = self.inputs["To Min"].default_value
tmax = self.inputs["To Max"].default_value
factor = (tmax - tmin) / (fmax - fmin)
offset = tmin - factor * fmin
@ -761,52 +746,34 @@ class MapRange(AbstractPowerShipNode):
class AngleFromVectors(AbstractPowerShipNode):
""" Calculate the angle between two vectors. Output in degrees. """
"""Calculate the angle between two vectors. Output in degrees."""
bl_idname = "AngleFromVectors"
bl_label = "Angle From Vectors"
bl_icon = "EMPTY_ARROWS"
angle_type: bpy.props.EnumProperty( # type: ignore
name="Type",
items=[
("DEFAULT", "Unsigned", ""),
("SIGNED", "Signed", ""),
],
)
def draw_buttons(
self, context: bpy.types.Context, layout: bpy.types.UILayout
) -> None:
super().draw_buttons(context, layout)
layout.prop(self, "angle_type")
def init(self, context):
self.add_optional_input_socket("NodeSocketVector", "U")
self.add_optional_input_socket("NodeSocketVector", "V")
self.inputs.new("NodeSocketVector", "U")
self.inputs.new("NodeSocketVector", "V")
self.outputs.new("NodeSocketFloat", "Angle")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
u = self._get_optional_input_value("U", Vector)
v = self._get_optional_input_value("V", Vector)
signed = self.angle_type == 'SIGNED'
u = self.inputs["U"].default_value
v = self.inputs["V"].default_value
angle = 0
if not (u.length == 0 or v.length == 0):
if signed:
angle = u.angle_signed(v)
else:
angle = u.angle(v)
if not (u.length_squared == 0 or v.length_squared == 0):
angle = u.angle(v)
self.outputs["Angle"].default_value = degrees(angle)
_enum_vector_math_operations = [
('ADD', "Add", ""),
('SUBSTRACT', "Substract", ""),
('MULTIPLY', "Mutliply", ""),
('DIVIDE', "Divide", ""),
('CROSS', "Cross", ""),
("ADD", "Add", ""),
("SUBSTRACT", "Substract", ""),
("MULTIPLY", "Mutliply", ""),
("DIVIDE", "Divide", ""),
("CROSS", "Cross", ""),
]
@ -827,38 +794,36 @@ class VectorMath(AbstractPowerShipNode):
layout.prop(self, "operation")
def init(self, context):
self.add_optional_input_socket("NodeSocketVector", "U")
self.add_optional_input_socket("NodeSocketVector", "V")
self.inputs.new("NodeSocketVector", "U")
self.inputs.new("NodeSocketVector", "V")
self.outputs.new("NodeSocketVector", "Result")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
u = self._get_optional_input_value("U", Vector)
v = self._get_optional_input_value("V", Vector)
u = self.inputs["U"].default_value
v = self.inputs["V"].default_value
match self.operation:
case 'ADD':
res = u+v
case 'MULTIPLY':
res = u*v
case 'SUBSTRACT':
res = u-v
case 'DIVIDE':
res = Vector([x/y if y != 0.0 else float("nan")
for x, y in zip(u, v)])
case 'CROSS':
case "ADD":
res = u + v
case "MULTIPLY":
res = u * v
case "SUBSTRACT":
res = u - v
case "DIVIDE":
res = Vector((x / y if y != 0.0 else 0.0 for x, y in zip(u, v)))
case "CROSS":
res = u.cross(v)
case _:
print("Vector math operation not found:", self.operation)
raise ValueError
raise ValueError(f"Vector math operation not found: {self.operation}\n")
self.outputs["Result"].default_value = res
_enum_math_operations = [
('ADD', "Add", ""),
('SUBSTRACT', "Substract", ""),
('MULTIPLY', "Mutliply", ""),
('DIVIDE', "Divide", ""),
("ADD", "Add", ""),
("SUBSTRACT", "Substract", ""),
("MULTIPLY", "Mutliply", ""),
("DIVIDE", "Divide", ""),
]
@ -879,26 +844,25 @@ class Math(AbstractPowerShipNode):
layout.prop(self, "operation")
def init(self, context):
self.add_optional_input_socket("NodeSocketFloat", "U")
self.add_optional_input_socket("NodeSocketFloat", "V")
self.inputs.new("NodeSocketFloat", "U")
self.inputs.new("NodeSocketFloat", "V")
self.outputs.new("NodeSocketFloat", "Result")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
u = self._get_optional_input_value("U", float)
v = self._get_optional_input_value("V", float)
u = self.inputs["U"].default_value
v = self.inputs["V"].default_value
match self.operation:
case 'ADD':
res = u+v
case 'MULTIPLY':
res = u*v
case 'SUBSTRACT':
res = u-v
case 'DIVIDE':
res = u/v if v != 0 else float("nan")
case "ADD":
res = u + v
case "MULTIPLY":
res = u * v
case "SUBSTRACT":
res = u - v
case "DIVIDE":
res = u / v if v != 0 else 0
case _:
print("Math operation not found:", self.operation)
raise ValueError
raise ValueError(f"Math operation not found: {self.operation}\n")
self.outputs["Result"].default_value = res
@ -994,12 +958,13 @@ class SetBoneNode(AbstractPowerShipNode):
match self.space:
case "WORLD":
bone_mat_world = Matrix.LocRotScale(
loc, rot.normalized(), scale)
bone_mat_world = Matrix.LocRotScale(loc, rot, scale)
loc, rot, scale = bone_mat_world.decompose()
case "CHANNELS":
bone_rest_rot_scale.translation = v_nil
mat_rot_scale = Matrix.LocRotScale(v_nil, rot, scale) @ bone_rest_rot_scale
mat_rot_scale = (
Matrix.LocRotScale(v_nil, rot, scale) @ bone_rest_rot_scale
)
mat_loc = Matrix.Translation(loc)
bone_mat_world = mat_loc @ mat_rot_scale
@ -1318,7 +1283,7 @@ node_categories = [
nodeitems_utils.NodeItem("ClampNode"),
nodeitems_utils.NodeItem("ToEulerNode"),
nodeitems_utils.NodeItem("FromEulerNode"),
]
],
),
PowerShipNodeCategory(
"DEBUG",