Motion transfer setup #1
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
/.venv
|
||||
/.vscode
|
||||
__pycache__
|
||||
*.DS_Store
|
||||
cgtinker marked this conversation as resolved
Outdated
|
||||
*.pyc
|
||||
*.blend1
|
||||
|
36
execute.py
@ -1,14 +1,14 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
is_first_load = "nodes" not in locals()
|
||||
import bpy
|
||||
from . import nodes
|
||||
is_first_load = "nodes" not in locals()
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
Please keep this block as-is. These changes will break the reloading mechanism. Please keep this block as-is. These changes will break the reloading mechanism.
Sybren A. Stüvel
commented
Please keep this block as-is. These changes will break the reloading mechanism. Please keep this block as-is. These changes will break the reloading mechanism.
Denys Hsu
commented
That's why it hasn't been updating! What kind of formatter are you using? (probably will add a few more nodes in the future if you don't mind) That's why it hasn't been updating!
My lsp destroyed the formatting a bit, doing my best to fix it – pylance has been fighting against mypy and no-one has been the winner I guess. Pep8 came to the rescue and yea well, here we are :)
What kind of formatter are you using? (probably will add a few more nodes in the future if you don't mind)
|
||||
|
||||
if not is_first_load:
|
||||
import sys
|
||||
|
||||
nodes = sys.modules[nodes.__name__]
|
||||
|
||||
import bpy
|
||||
|
||||
_skip_next_autorun = False
|
||||
|
||||
@ -50,7 +50,6 @@ def execute_tree(
|
||||
# The running of this tree will trigger another depsgraph update, which
|
||||
# should not trigger yet another execution.
|
||||
global _skip_next_autorun
|
||||
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
Please don’t include formatting changes like this. Formatting changes shouldn’t be in the same PR as functional changes. Please don’t include formatting changes like this. Formatting changes shouldn’t be in the same PR as functional changes.
|
||||
try:
|
||||
tree.run_event(depsgraph, mode)
|
||||
except Exception:
|
||||
@ -67,7 +66,6 @@ def _on_depsgraph_update_post(
|
||||
scene: bpy.types.Scene, depsgraph: bpy.types.Depsgraph
|
||||
) -> None:
|
||||
global _skip_next_autorun
|
||||
|
||||
if _skip_next_autorun:
|
||||
_skip_next_autorun = False
|
||||
return
|
||||
@ -85,6 +83,7 @@ def _on_depsgraph_update_post(
|
||||
return
|
||||
|
||||
cgtinker marked this conversation as resolved
Sybren A. Stüvel
commented
Since Since `_skip_next_autorun` is a boolean, the `if` condition can be removed.
|
||||
powership_mode = scene.powership_mode
|
||||
|
||||
if powership_mode == "AUTO":
|
||||
# TODO: don't use the global context here.
|
||||
powership_mode = _choose_auto_mode(bpy.context)
|
||||
@ -98,6 +97,34 @@ def _choose_auto_mode(context: bpy.types.Context) -> str:
|
||||
return "FORWARD"
|
||||
|
||||
|
||||
@bpy.app.handlers.persistent # type: ignore
|
||||
def _on_frame_changed_post(
|
||||
scene: bpy.types.Scene, depsgraph: bpy.types.Depsgraph
|
||||
) -> None:
|
||||
global _skip_next_autorun
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
This seems like the same logic as the depsgraph update callback. Maybe move it into a separate function and call that from both places? This seems like the same logic as the depsgraph update callback. Maybe move it into a separate function and call that from both places?
|
||||
if _skip_next_autorun:
|
||||
_skip_next_autorun = True
|
||||
|
||||
# updating on frame change to allow
|
||||
# keyframed animation influencing the rig
|
||||
for tree in bpy.data.node_groups:
|
||||
if tree.bl_idname != "PowerShipNodeTree":
|
||||
continue
|
||||
|
||||
if not tree.autorun:
|
||||
return
|
||||
|
||||
powership_mode = scene.powership_mode
|
||||
|
||||
if powership_mode == "AUTO":
|
||||
if bpy.context.object and bpy.context.object.mode == "POSE":
|
||||
powership_mode = "BACKWARD"
|
||||
else:
|
||||
powership_mode = "FORWARD"
|
||||
|
||||
execute_tree(tree, depsgraph, powership_mode)
|
||||
|
||||
|
||||
classes = (
|
||||
# Operators:
|
||||
PowerShip_OT_execute_tree,
|
||||
@ -114,6 +141,7 @@ def register() -> None:
|
||||
)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update_post)
|
||||
bpy.app.handlers.frame_change_post.append(_on_frame_changed_post)
|
||||
|
||||
|
||||
def unregister() -> None:
|
||||
|
536
nodes.py
@ -3,8 +3,8 @@
|
||||
import functools
|
||||
from collections import deque
|
||||
from copy import copy
|
||||
from math import degrees, radians
|
||||
from typing import TypeVar, Callable, Optional, Iterable
|
||||
from math import degrees, radians, sqrt
|
||||
from typing import TypeVar, Callable, Optional, Iterable, Any
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
Both Both `sqrt` and `Any` are unused.
|
||||
|
||||
import bpy
|
||||
import nodeitems_utils
|
||||
@ -67,7 +67,10 @@ class PowerShipNodeTree(bpy.types.NodeTree):
|
||||
|
||||
def _prepare_nodes(self) -> None:
|
||||
for node in self.nodes:
|
||||
try:
|
||||
node.reset_run()
|
||||
except AttributeError:
|
||||
print(node)
|
||||
|
||||
def _run_from_node(
|
||||
self, depsgraph: bpy.types.Depsgraph, start_node: "AbstractPowerShipNode"
|
||||
@ -83,7 +86,8 @@ class PowerShipNodeTree(bpy.types.NodeTree):
|
||||
|
||||
nodes_before = list(node.exec_order_prerequisites())
|
||||
if nodes_before:
|
||||
print(f" {len(nodes_before)} prerequisites, going there first")
|
||||
print(
|
||||
f" {len(nodes_before)} prerequisites, going there first")
|
||||
# There are nodes to execute before this one. Push them to the front of the queue.
|
||||
to_visit.extendleft(reversed(nodes_before))
|
||||
continue
|
||||
@ -169,10 +173,12 @@ class AbstractPowerShipNode(bpy.types.Node):
|
||||
self.add_execution_socket_output()
|
||||
|
||||
def add_execution_socket_input(self) -> None:
|
||||
self.inputs.new(NodeSocketExecute.bl_idname, NodeSocketExecute.bl_label)
|
||||
self.inputs.new(NodeSocketExecute.bl_idname,
|
||||
NodeSocketExecute.bl_label)
|
||||
|
||||
def add_execution_socket_output(self) -> None:
|
||||
self.outputs.new(NodeSocketExecute.bl_idname, NodeSocketExecute.bl_label)
|
||||
self.outputs.new(NodeSocketExecute.bl_idname,
|
||||
NodeSocketExecute.bl_label)
|
||||
|
||||
def add_optional_input_socket(
|
||||
self, typename: str, label: str
|
||||
@ -265,7 +271,8 @@ class AbstractPowerShipNode(bpy.types.Node):
|
||||
"""Return the connected socket value, or None if not connected."""
|
||||
input_socket = self.inputs[input_socket_name]
|
||||
for link in input_socket.links:
|
||||
convertedValue = expectType(link.from_socket.default_value) # type: ignore
|
||||
convertedValue = expectType(
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
Keep formatting changes out of the patch. You can use Keep formatting changes out of the patch. You can use `git gui` or some other tool to cherry-pick which lines you do (not) want to include in a commit. That way you can exclude such changes, commit the rest, then revert the unwanted formatting changes to get rid of them.
|
||||
link.from_socket.default_value) # type: ignore
|
||||
return convertedValue
|
||||
return None
|
||||
|
||||
@ -398,7 +405,8 @@ class GetBoneNode(AbstractPowerShipNode):
|
||||
bl_label = "Get Bone"
|
||||
bl_icon = "BONE_DATA"
|
||||
|
||||
head_tail: bpy.props.EnumProperty(name="Side", items=_bone_head_tail) # type: ignore
|
||||
head_tail: bpy.props.EnumProperty(
|
||||
name="Side", items=_bone_head_tail) # type: ignore
|
||||
|
||||
def draw_buttons(
|
||||
self, context: bpy.types.Context, layout: bpy.types.UILayout
|
||||
@ -440,7 +448,8 @@ class GetBoneNode(AbstractPowerShipNode):
|
||||
bone_rest_rot_scale = bone.bone.matrix_local.copy()
|
||||
bone_rest_rot_scale.translation = v_nil
|
||||
|
||||
_, rot, scale = (mat_world @ bone_rest_rot_scale.inverted()).decompose()
|
||||
_, rot, scale = (
|
||||
mat_world @ bone_rest_rot_scale.inverted()).decompose()
|
||||
|
||||
self.outputs["Location"].default_value = arm_matrix @ loc
|
||||
self.outputs["Rotation"].default_value = rot
|
||||
@ -536,7 +545,8 @@ class SetControlNode(AbstractPowerShipNode):
|
||||
|
||||
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
|
||||
control_location = self._get_optional_input_value("Location", Vector)
|
||||
control_rotation = self._get_optional_input_value("Rotation", Quaternion)
|
||||
control_rotation = self._get_optional_input_value(
|
||||
"Rotation", Quaternion)
|
||||
control_scale = self._get_optional_input_value("Scale", Vector)
|
||||
control_obj = self._get_input_value("Control", bpy.types.Object)
|
||||
|
||||
@ -576,6 +586,450 @@ class SetControlNode(AbstractPowerShipNode):
|
||||
return Matrix.Identity(4)
|
||||
|
||||
|
||||
class ToVector(AbstractPowerShipNode):
|
||||
bl_idname = "ToVector"
|
||||
bl_label = "To Vector"
|
||||
bl_icon = "EMPTY_ARROWS"
|
||||
|
||||
def init(self, context):
|
||||
self.add_execution_sockets()
|
||||
|
||||
self.add_optional_input_socket("NodeSocketFloat", "X")
|
||||
self.add_optional_input_socket("NodeSocketFloat", "Y")
|
||||
self.add_optional_input_socket("NodeSocketFloat", "Z")
|
||||
self.outputs.new("NodeSocketVector", "Vector")
|
||||
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
This can be simplified by using shortcutting (
This can be simplified by using shortcutting (`a or b` evaluates to `b` when `a` is falsey).
```py
x = self._get_optional_input_value("X", float) or 0.0
y = self._get_optional_input_value("Y", float) or 0.0
z = self._get_optional_input_value("Z", float) or 0.0
v = Vector((x, y, z))
```
|
||||
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:
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
Same as above, this could be a regular vector input socket. Same as above, this could be a regular vector input socket.
|
||||
v.z = z
|
||||
|
||||
self.outputs["Vector"].default_value = v
|
||||
|
||||
|
||||
class SplitVector(AbstractPowerShipNode):
|
||||
bl_idname = "SplitVector"
|
||||
bl_label = "Split Vector"
|
||||
bl_icon = "EMPTY_ARROWS"
|
||||
|
||||
def init(self, context):
|
||||
self.add_execution_sockets()
|
||||
|
||||
self.add_optional_input_socket("NodeSocketVector", "Vector")
|
||||
self.outputs.new("NodeSocketFloat", "X")
|
||||
self.outputs.new("NodeSocketFloat", "Y")
|
||||
self.outputs.new("NodeSocketFloat", "Z")
|
||||
|
||||
cgtinker marked this conversation as resolved
Sybren A. Stüvel
commented
Does this calculate a normal vector? Or its length? Does this calculate a normal vector? Or its length?
Denys Hsu
commented
The length. The length.
I went through the comments and updated them (some were unrelated and happened due to closed eyes copy pasting...)
|
||||
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
|
||||
v = self._get_optional_input_value("Vector", Vector)
|
||||
|
||||
if not v:
|
||||
return
|
||||
|
||||
self.outputs["X"].default_value = v.x
|
||||
self.outputs["Y"].default_value = v.y
|
||||
self.outputs["Z"].default_value = v.z
|
||||
|
||||
|
||||
class Distance(AbstractPowerShipNode):
|
||||
""" Calculate normal from three points. """
|
||||
|
||||
bl_idname = "Distance"
|
||||
bl_label = "Distance"
|
||||
bl_icon = "EMPTY_ARROWS"
|
||||
|
||||
def init(self, context):
|
||||
self.add_execution_sockets()
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
These should again be regular input sockets, as it makes no sense to do the computation without a full set of three points. These should again be regular input sockets, as it makes no sense to do the computation without a full set of three points.
|
||||
|
||||
self.add_optional_input_socket("NodeSocketVector", "U")
|
||||
self.add_optional_input_socket("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)
|
||||
|
||||
if not (u and v):
|
||||
return
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
This only outputs a normal vector when This only outputs a normal vector when `a` and `b` are perpendicular. Better to use `a.cross(b).normalized()`
|
||||
|
||||
self.outputs["Float"].default_value = (u-v).length
|
||||
|
||||
|
||||
class NormalFromPoints(AbstractPowerShipNode):
|
||||
""" Calculate normal from three points. """
|
||||
|
||||
bl_idname = "NormalFromPoints"
|
||||
bl_label = "Normal from Points"
|
||||
bl_icon = "EMPTY_ARROWS"
|
||||
|
||||
def init(self, context):
|
||||
self.add_execution_sockets()
|
||||
|
||||
self.add_optional_input_socket("NodeSocketVector", "U")
|
||||
self.add_optional_input_socket("NodeSocketVector", "V")
|
||||
self.add_optional_input_socket("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)
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
Follow PEP 257 (in this case, the initial line should be shorter, and the leading/trailing spaces should be removed). Same for other docstrings. Wellll.... try to follow PEP 257 but also keep in mind that Blender uses those docstrings for the tooltips, and thus they shouldn't end in a period. Stupid, I know... Follow PEP 257 (in this case, the initial line should be shorter, and the leading/trailing spaces should be removed). Same for other docstrings.
Wellll.... try to follow PEP 257 but also keep in mind that Blender uses those docstrings for the tooltips, and thus they shouldn't end in a period. Stupid, I know...
|
||||
w = self._get_optional_input_value("W", Vector)
|
||||
|
||||
if not (u and v and w):
|
||||
return
|
||||
|
||||
a = v - u
|
||||
b = w - u
|
||||
normal = a.cross(b)
|
||||
self.outputs["Result"].default_value = normal
|
||||
|
||||
|
||||
class UpAxisSocket(bpy.types.NodeSocket):
|
||||
'''Custom node socket type'''
|
||||
bl_idname = 'UpAxisSocket'
|
||||
bl_label = "Up Axis Socket"
|
||||
Sybren A. Stüvel
commented
Why is Why is `Y` the default up-vector, and not `Z`?
Denys Hsu
commented
I thought it was like that in the python interface (and I was wrong). I thought it was like that in the python interface (and I was wrong).
Just checked in _mathutils_Vector.c_, there the up axis is **Y** and the track axis is **Z**. Should we follow that convention or go for **Z** as up axis and **X** as track axis?
|
||||
link_limit = 0
|
||||
|
||||
# Enum items list
|
||||
axis_items = (
|
||||
('X', "X", ""),
|
||||
('Y', "Y", ""),
|
||||
('Z', "Z", ""),
|
||||
)
|
||||
# default_value = "UP"
|
||||
|
||||
default_value: bpy.props.EnumProperty(
|
||||
name="Axis",
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
I think these names could be improved for clarity. How about "Vector" and "Rotate To"? I think these names could be improved for clarity. How about "Vector" and "Rotate To"?
|
||||
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):
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
`default_value` doesn't seem used.
|
||||
'''Custom node socket type'''
|
||||
bl_idname = 'TrackAxisSocket'
|
||||
bl_label = "Track Axis Socket"
|
||||
link_limit = 0
|
||||
|
||||
# Enum items list
|
||||
axis_items = (
|
||||
('X', "X", ""),
|
||||
('Y', "Y", ""),
|
||||
('Z', "Z", ""),
|
||||
('-X', "-X", ""),
|
||||
('-Y', "-Y", ""),
|
||||
('-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):
|
||||
""" Calculate rotation from tangent (left / right), normal (forward) and binormal (up). """
|
||||
|
||||
bl_idname = "RotateTowards"
|
||||
bl_label = "Rotate Towards"
|
||||
bl_icon = "EMPTY_ARROWS"
|
||||
|
||||
def init(self, context):
|
||||
self.add_execution_sockets()
|
||||
|
||||
self.add_optional_input_socket("UpAxisSocket", "Up")
|
||||
self.add_optional_input_socket("TrackAxisSocket", "Track")
|
||||
Sybren A. Stüvel
commented
Why name it 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.
Denys Hsu
commented
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.
|
||||
|
||||
self.add_optional_input_socket("NodeSocketVector", "Origin")
|
||||
self.add_optional_input_socket("NodeSocketVector", "Destination")
|
||||
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)
|
||||
|
||||
track = self._get_input_value("Track", str)
|
||||
up = self._get_input_value("Up", str)
|
||||
if not (origin and destination):
|
||||
return
|
||||
|
||||
# Set the rotation of the control
|
||||
vec = Vector((destination - origin))
|
||||
vec.normalize()
|
||||
rot = vec.to_track_quat(track, up)
|
||||
self.outputs["Rotation"].default_value = rot
|
||||
|
||||
|
||||
class OffsetRotation(AbstractPowerShipNode):
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
Don't compute Don't compute `length` when you can use `length_squared` as well.
|
||||
""" Calculate rotation from tangent (left / right), normal (forward) and binormal (up). """
|
||||
|
||||
bl_idname = "OffsetRotation"
|
||||
bl_label = "Offset Rotation"
|
||||
bl_icon = "EMPTY_ARROWS"
|
||||
|
||||
def init(self, context):
|
||||
self.add_execution_sockets()
|
||||
self.add_optional_input_socket("NodeSocketQuaternion", "Base")
|
||||
self.add_optional_input_socket("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)
|
||||
|
||||
if not (base and offset):
|
||||
return
|
||||
|
||||
self.outputs["Rotation"].default_value = base @ offset
|
||||
|
||||
|
||||
class MapRange(AbstractPowerShipNode):
|
||||
"""Sets the location and/or rotation of the 3D cursor"""
|
||||
|
||||
bl_idname = "MapRange"
|
||||
bl_label = "Map Range"
|
||||
bl_icon = "EMPTY_ARROWS"
|
||||
|
||||
def init(self, context):
|
||||
self.add_execution_sockets()
|
||||
|
||||
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.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("Value", float)
|
||||
fmax = self._get_optional_input_value("Value", float)
|
||||
tmin = 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)
|
||||
offset = tmin - slope * fmin
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
This could be an interesting design discussion. I actually had a little design discussion with @nathanvegdahl about this, and until we have a 'debug mode' that can visualise where NaNs are produced, it's better to stick to regular floats. So in this case And another minor thing: the list comprehension can be replaced by a generator expression for a little bit of added performance and readability:
This could be an interesting design discussion. ~~Not something to do here -- this code is fine.~~ But in general, we have to decide how Powership is going to handle erroneous values. For example,. the `Vector.normalized()` function I suggested above will simply return `(0, 0, 0)` for zero vectors. I feel that maybe here it could also make sense to define `x / 0 → 0` and return as many usable values as possible.
I actually had a little design discussion with @nathanvegdahl about this, and until we have a 'debug mode' that can visualise where NaNs are produced, it's better to stick to regular floats. So in this case `x/y if y != 0.0 else 0.0`
And another minor thing: the list comprehension can be replaced by a generator expression for a little bit of added performance and readability:
```py
res = Vector(x/y if y != 0.0 else 0.0 for x, y in zip(u, v))
```
|
||||
self.outputs["Result"].default_value = slope * val + offset
|
||||
|
||||
|
||||
class AngleTypeSocket(bpy.types.NodeSocket):
|
||||
bl_idname = 'AngleTypeSocket'
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
Either log/print, or raise an exception. Don't do both, as one issue will be reported twice. Either log/print, or raise an exception. Don't do both, as one issue will be reported twice.
|
||||
bl_label = "Angle Type"
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
Don't use a class as exception, always instantiate it.
Don't use a class as exception, always instantiate it.
```py
raise ValueError(f"Vector math operation not found: {self.operation!r}")
```
|
||||
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):
|
||||
bl_idname = "RotationFromAngle"
|
||||
bl_label = "Rotation From Vector Angle"
|
||||
bl_icon = "EMPTY_ARROWS"
|
||||
|
||||
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", "V")
|
||||
self.outputs.new("NodeSocketQuaternion", "Rotation")
|
||||
|
||||
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
|
||||
u = self._get_optional_input_value("U", Vector)
|
||||
v = self._get_optional_input_value("V", Vector)
|
||||
axis = self._get_input_value("Axis", str)
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
nan → 0.0 nan → 0.0
|
||||
angle_type = self._get_input_value("Type", str)
|
||||
signed = angle_type == 'SIGNED'
|
||||
|
||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
Same as above. Same as above.
|
||||
if not (u and v):
|
||||
return
|
||||
|
||||
angle = 0
|
||||
|
||||
if not (u.length == 0 or v.length == 0):
|
||||
if signed:
|
||||
angle = u.angle_signed(v)
|
||||
else:
|
||||
angle = u.angle(v)
|
||||
|
||||
m = Matrix.Rotation(angle, 3, axis)
|
||||
res = m.to_quaternion()
|
||||
|
||||
self.outputs["Rotation"].default_value = res
|
||||
|
||||
|
||||
class MathOperationSocket(bpy.types.NodeSocket):
|
||||
bl_idname = 'MathOperationSocket'
|
||||
bl_label = "Operations"
|
||||
link_limit = 0
|
||||
|
||||
# Enum items list
|
||||
ops_items = (
|
||||
('ADD', "Add", ""),
|
||||
('SUBSTRACT', "Substract", ""),
|
||||
('MULTIPLY', "Mutliply", ""),
|
||||
('DIVIDE', "Divide", ""),
|
||||
('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):
|
||||
"""Sets the location and/or rotation of the 3D cursor"""
|
||||
|
||||
bl_idname = "VectorMath"
|
||||
bl_label = "Vector Math"
|
||||
bl_icon = "EMPTY_ARROWS"
|
||||
|
||||
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", "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)
|
||||
mode = self._get_input_value("Operation", str)
|
||||
|
||||
if not (u and v):
|
||||
return
|
||||
|
||||
match mode:
|
||||
case 'ADD':
|
||||
res = u+v
|
||||
case 'MULTIPLY':
|
||||
res = u*v
|
||||
case 'SUBSTRACT':
|
||||
res = u-v
|
||||
case 'DIVIDE':
|
||||
res = Vector([x/y for x, y in zip(u, v)])
|
||||
case 'CROSS':
|
||||
res = u.cross(v)
|
||||
case _:
|
||||
raise ValueError
|
||||
|
||||
self.outputs["Result"].default_value = res
|
||||
|
||||
|
||||
class Math(AbstractPowerShipNode):
|
||||
"""Sets the location and/or rotation of the 3D cursor"""
|
||||
|
||||
bl_idname = "Math"
|
||||
bl_label = "Math"
|
||||
bl_icon = "EMPTY_ARROWS"
|
||||
Sybren A. Stüvel
commented
in which case would 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()`.
Denys Hsu
commented
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.
|
||||
|
||||
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", "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)
|
||||
mode = self._get_input_value("Operation", str)
|
||||
|
||||
if not (u and v):
|
||||
return
|
||||
|
||||
match mode:
|
||||
case 'ADD':
|
||||
res = u+v
|
||||
case 'MULTIPLY':
|
||||
res = u*v
|
||||
case 'SUBSTRACT':
|
||||
res = u-v
|
||||
case 'DIVIDE':
|
||||
res = u/v
|
||||
case _:
|
||||
print("MODE NOT FOUND:", mode)
|
||||
raise ValueError
|
||||
|
||||
self.outputs["Result"].default_value = res
|
||||
|
||||
|
||||
class SetCursorNode(AbstractAlwaysExecuteNode):
|
||||
"""Sets the location and/or rotation of the 3D cursor"""
|
||||
|
||||
@ -590,7 +1044,8 @@ class SetCursorNode(AbstractAlwaysExecuteNode):
|
||||
|
||||
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
|
||||
control_location = self._get_optional_input_value("Location", Vector)
|
||||
control_rotation = self._get_optional_input_value("Rotation", Quaternion)
|
||||
control_rotation = self._get_optional_input_value(
|
||||
"Rotation", Quaternion)
|
||||
|
||||
cursor = depsgraph.scene.cursor
|
||||
if control_location is not None:
|
||||
@ -618,7 +1073,8 @@ class SetBoneNode(AbstractPowerShipNode):
|
||||
|
||||
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
|
||||
control_location = self._get_optional_input_value("Location", Vector)
|
||||
control_rotation = self._get_optional_input_value("Rotation", Quaternion)
|
||||
control_rotation = self._get_optional_input_value(
|
||||
"Rotation", Quaternion)
|
||||
control_scale = self._get_optional_input_value("Scale", Vector)
|
||||
|
||||
if not (control_location or control_rotation or control_scale):
|
||||
@ -651,7 +1107,8 @@ class SetBoneNode(AbstractPowerShipNode):
|
||||
bone_rest_rot_scale = bone.bone.matrix_local.copy()
|
||||
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
|
||||
@ -695,7 +1152,8 @@ class TwoBoneIKNode(AbstractPowerShipNode):
|
||||
|
||||
locator: Callable[[str, Vector], bpy.types.Object]
|
||||
if self._debug:
|
||||
locator = functools.partial(self._debug_locator, scene=depsgraph.scene)
|
||||
locator = functools.partial(
|
||||
self._debug_locator, scene=depsgraph.scene)
|
||||
else:
|
||||
locator = self._locator
|
||||
|
||||
@ -703,9 +1161,11 @@ class TwoBoneIKNode(AbstractPowerShipNode):
|
||||
target_loc = self._get_input_value("Target", Vector)
|
||||
target_ob = locator("temp-ik-target", target_loc)
|
||||
|
||||
pole_target_loc = self._get_optional_input_value("Pole Target", Vector)
|
||||
pole_target_loc = self._get_optional_input_value(
|
||||
"Pole Target", Vector)
|
||||
if pole_target_loc is not None:
|
||||
pole_target_ob = locator("temp-ik-pole-target", pole_target_loc)
|
||||
pole_target_ob = locator(
|
||||
"temp-ik-pole-target", pole_target_loc)
|
||||
|
||||
try:
|
||||
# Reuse constraint from previous run. This can be useful for
|
||||
@ -1015,14 +1475,24 @@ node_categories = [
|
||||
"MATH",
|
||||
"Math",
|
||||
items=[
|
||||
nodeitems_utils.NodeItem("AddNode"),
|
||||
nodeitems_utils.NodeItem("SubtractNode"),
|
||||
nodeitems_utils.NodeItem("MultiplyNode"),
|
||||
nodeitems_utils.NodeItem("DivideNode"),
|
||||
nodeitems_utils.NodeItem("RotateTowards"),
|
||||
nodeitems_utils.NodeItem("OffsetRotation"),
|
||||
nodeitems_utils.NodeItem("RotationFromAngle"),
|
||||
nodeitems_utils.NodeItem("NormalFromPoints"),
|
||||
nodeitems_utils.NodeItem("SplitVector"),
|
||||
nodeitems_utils.NodeItem("ToVector"),
|
||||
nodeitems_utils.NodeItem("Distance"),
|
||||
nodeitems_utils.NodeItem("MapRange"),
|
||||
nodeitems_utils.NodeItem("VectorMath"),
|
||||
nodeitems_utils.NodeItem("Math"),
|
||||
# nodeitems_utils.NodeItem("AddNode"),
|
||||
# nodeitems_utils.NodeItem("SubtractNode"),
|
||||
# nodeitems_utils.NodeItem("MultiplyNode"),
|
||||
# nodeitems_utils.NodeItem("DivideNode"),
|
||||
nodeitems_utils.NodeItem("ClampNode"),
|
||||
nodeitems_utils.NodeItem("ToEulerNode"),
|
||||
nodeitems_utils.NodeItem("FromEulerNode"),
|
||||
],
|
||||
]
|
||||
),
|
||||
PowerShipNodeCategory(
|
||||
"DEBUG",
|
||||
@ -1040,6 +1510,10 @@ classes = (
|
||||
# Socket types:
|
||||
NodeSocketExecute,
|
||||
NodeSocketQuaternion,
|
||||
MathOperationSocket,
|
||||
TrackAxisSocket,
|
||||
UpAxisSocket,
|
||||
AngleTypeSocket,
|
||||
# Nodes:
|
||||
ForwardSolveNode,
|
||||
BackwardSolveNode,
|
||||
@ -1050,13 +1524,24 @@ classes = (
|
||||
TwoBoneIKNode,
|
||||
SetCursorNode,
|
||||
SequenceNode,
|
||||
# Math Nodes
|
||||
RotateTowards,
|
||||
RotationFromAngle,
|
||||
OffsetRotation,
|
||||
NormalFromPoints,
|
||||
ToVector,
|
||||
SplitVector,
|
||||
Distance,
|
||||
MapRange,
|
||||
VectorMath,
|
||||
Math,
|
||||
ToEulerNode,
|
||||
FromEulerNode,
|
||||
ClampNode,
|
||||
AddNode,
|
||||
SubtractNode,
|
||||
MultiplyNode,
|
||||
DivideNode,
|
||||
# AddNode,
|
||||
# SubtractNode,
|
||||
# MultiplyNode,
|
||||
# DivideNode,
|
||||
# Operators:
|
||||
PowerShip_OT_rebuild_node,
|
||||
)
|
||||
@ -1065,7 +1550,8 @@ _register, _unregister = bpy.utils.register_classes_factory(classes)
|
||||
|
||||
def register() -> None:
|
||||
_register()
|
||||
nodeitems_utils.register_node_categories("POWERSHIP_NODES", node_categories)
|
||||
nodeitems_utils.register_node_categories(
|
||||
"POWERSHIP_NODES", node_categories)
|
||||
|
||||
|
||||
def unregister() -> None:
|
||||
|
Please don't add platform/IDE-specific files here. These can go into
.git/info/exclude
(which does the same but isn't tracked) or in your global gitignore file (if you want to have those available to all Git projects you work on).