poikilos
3 years ago
2 changed files with 334 additions and 0 deletions
@ -0,0 +1,9 @@ |
|||||
|
|
||||
|
How to use a Blender script: |
||||
|
- Copy the entire script to the clipboard (open it in Geany or another editor, Edit, Select All, Edit, Copy) |
||||
|
- Open Blender (ensure the Blender version is in the range of versions recommended by the script's comments). |
||||
|
- Hover over the edge of the 3D view and right-click, then click "Split Vertically" |
||||
|
- Click the editor drop-down (looks like: #o while in 3D View), then choose "Text Editor" |
||||
|
- Click Text, New |
||||
|
- Paste the entire script (Edit, Paste) |
||||
|
- Follow the instructions at the top of the script. |
@ -0,0 +1,325 @@ |
|||||
|
|
||||
|
''' |
||||
|
Create actual bones from temporary bones that the |
||||
|
B3D importer creates. |
||||
|
|
||||
|
Requires: Blender (>=2.8) |
||||
|
|
||||
|
Assumes: |
||||
|
- x y and z scale, if not 1.0, must be all the same on the empties for this to work. |
||||
|
|
||||
|
Usage: |
||||
|
- Paste this script into a new text file in a text editor panel |
||||
|
(If you don't know how to get there, see the HowTo file in |
||||
|
the utilities/blender directory at |
||||
|
github.com/poikilos/EnlivenMinetest). |
||||
|
- Select an empty that has a mesh for a parent like a model imported from a B3D, or an empty that has no parent. |
||||
|
- "Text", "Run Script". |
||||
|
''' |
||||
|
import bpy |
||||
|
from mathutils import Vector, Quaternion |
||||
|
context = bpy.context |
||||
|
print("") |
||||
|
print("[ EnlivenMinetest/utilities/blender/hierarchy_of_empties_to_bones.py ] started") |
||||
|
|
||||
|
|
||||
|
def mat3_to_vec_roll(mat): |
||||
|
''' |
||||
|
Convert a mat3 to a tuple containing a vec and roll. |
||||
|
|
||||
|
Hendrix says that he re-ported the C code of blender to do this |
||||
|
after Emd4600 ported it |
||||
|
but for this function, he says, "this hasn't changed." |
||||
|
-[HENDRIX](https://blender.stackexchange.com/users/45904/hendrix). |
||||
|
<https://blender.stackexchange.com/a/90240>. Sep 14, 2017. |
||||
|
|
||||
|
Blender is [GPLv2](https://developer.blender.org/diffusion/B/browse/master/doc/license/GPL-license.txt) |
||||
|
--does that matter here? |
||||
|
''' |
||||
|
vec = mat.col[1] |
||||
|
vecmat = vec_roll_to_mat3(mat.col[1], 0) |
||||
|
vecmatinv = vecmat.inverted() |
||||
|
rollmat = vecmatinv * mat |
||||
|
roll = math.atan2(rollmat[0][2], rollmat[2][2]) |
||||
|
return vec, roll |
||||
|
|
||||
|
|
||||
|
def vec_roll_to_mat3(vec, roll): |
||||
|
''' |
||||
|
#port of the updated C function from armature.c |
||||
|
#https://developer.blender.org/T39470 |
||||
|
#note that C accesses columns first, so all matrix indices are swapped compared to the C version |
||||
|
-HENDRIX1 |
||||
|
|
||||
|
HENDRIX1 says that he re-ported the C code of blender to do this |
||||
|
after Emd4600 ported it |
||||
|
-[HENDRIX](https://blender.stackexchange.com/users/45904/hendrix). |
||||
|
<https://blender.stackexchange.com/a/90240>. Sep 14, 2017. |
||||
|
|
||||
|
Blender is [GPLv2](https://developer.blender.org/diffusion/B/browse/master/doc/license/GPL-license.txt) |
||||
|
--does that matter here? |
||||
|
''' |
||||
|
|
||||
|
nor = vec.normalized() |
||||
|
THETA_THRESHOLD_NEGY = 1.0e-9 |
||||
|
THETA_THRESHOLD_NEGY_CLOSE = 1.0e-5 |
||||
|
|
||||
|
#create a 3x3 matrix |
||||
|
bMatrix = mathutils.Matrix().to_3x3() |
||||
|
|
||||
|
theta = 1.0 + nor[1]; |
||||
|
|
||||
|
if (theta > THETA_THRESHOLD_NEGY_CLOSE) or ((nor[0] or nor[2]) and theta > THETA_THRESHOLD_NEGY): |
||||
|
|
||||
|
bMatrix[1][0] = -nor[0]; |
||||
|
bMatrix[0][1] = nor[0]; |
||||
|
bMatrix[1][1] = nor[1]; |
||||
|
bMatrix[2][1] = nor[2]; |
||||
|
bMatrix[1][2] = -nor[2]; |
||||
|
if theta > THETA_THRESHOLD_NEGY_CLOSE: |
||||
|
#If nor is far enough from -Y, apply the general case. |
||||
|
bMatrix[0][0] = 1 - nor[0] * nor[0] / theta; |
||||
|
bMatrix[2][2] = 1 - nor[2] * nor[2] / theta; |
||||
|
bMatrix[0][2] = bMatrix[2][0] = -nor[0] * nor[2] / theta; |
||||
|
|
||||
|
else: |
||||
|
#If nor is too close to -Y, apply the special case. |
||||
|
theta = nor[0] * nor[0] + nor[2] * nor[2]; |
||||
|
bMatrix[0][0] = (nor[0] + nor[2]) * (nor[0] - nor[2]) / -theta; |
||||
|
bMatrix[2][2] = -bMatrix[0][0]; |
||||
|
bMatrix[0][2] = bMatrix[2][0] = 2.0 * nor[0] * nor[2] / theta; |
||||
|
|
||||
|
else: |
||||
|
#If nor is -Y, simple symmetry by Z axis. |
||||
|
bMatrix = mathutils.Matrix().to_3x3() |
||||
|
bMatrix[0][0] = bMatrix[1][1] = -1.0; |
||||
|
|
||||
|
#Make Roll matrix |
||||
|
rMatrix = mathutils.Matrix.Rotation(roll, 3, nor) |
||||
|
|
||||
|
#Combine and output result |
||||
|
mat = rMatrix * bMatrix |
||||
|
return mat |
||||
|
|
||||
|
def getFirstChild(parentName): |
||||
|
for obj in bpy.data.objects: |
||||
|
if obj.parent is None: |
||||
|
continue |
||||
|
if obj.parent.name == parentName: |
||||
|
return obj |
||||
|
return None |
||||
|
|
||||
|
def getRealRotation_broken(cumulative_rotation, empty): |
||||
|
if cumulative_rotation is None: |
||||
|
cumulative_rotation = Quaternion((1, 0, 0, 0)) |
||||
|
# ^ default (no rotation) in Blender is (w, x, y, z) = (0, 0, 0, 1). |
||||
|
if (empty.parent is None) or (empty.parent.type != 'EMPTY'): |
||||
|
return cumulative_rotation |
||||
|
return getRealRotation(cumulative_rotation, empty.rotation_quaternion) @ empty.parent.rotation_quaternion |
||||
|
|
||||
|
|
||||
|
def getRealRotation(empty): |
||||
|
''' |
||||
|
This function exists to test ensuring correct order of operation because |
||||
|
HENDRIX1 replied to Cirno's cited post on blenderartists saying: |
||||
|
mat_armature = mat_local * parent_mat_local_0 * parent_mat_local_1 * … * parent_mat_local_n |
||||
|
The parent mats are the direct parent first, more removed parents in the end. |
||||
|
current_bone.transform(transform_quat.to_matrix()) |
||||
|
- HENDRIX1 <https://blenderartists.org/t/needed-help-with-creating-bones-in-python-using-position-and-rotation-data/1209120/2>. |
||||
|
''' |
||||
|
ancestor = empty.parent |
||||
|
if (ancestor is None) or (ancestor != 'EMPTY'): |
||||
|
return empty.rotation_quaternion |
||||
|
ancestor = empty.parent.parent |
||||
|
if (ancestor is None) or (ancestor != 'EMPTY'): |
||||
|
return empty.rotation_quaternion @ empty.parent.rotation_quaternion |
||||
|
ancestor = empty.parent.parent.parent |
||||
|
if (ancestor is None) or (ancestor != 'EMPTY'): |
||||
|
return empty.rotation_quaternion @ empty.parent.rotation_quaternion @ empty.parent.parent.rotation_quaternion |
||||
|
ancestor = empty.parent.parent.parent.parent |
||||
|
if (ancestor is None) or (ancestor != 'EMPTY'): |
||||
|
return empty.rotation_quaternion @ empty.parent.rotation_quaternion @ empty.parent.parent.rotation_quaternion @ empty.parent.parent.parent.rotation_quaternion |
||||
|
ancestor = empty.parent.parent.parent.parent.parent |
||||
|
if (ancestor is None) or (ancestor != 'EMPTY'): |
||||
|
return empty.rotation_quaternion @ empty.parent.rotation_quaternion @ empty.parent.parent.rotation_quaternion @ empty.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.rotation_quaternion |
||||
|
ancestor = empty.parent.parent.parent.parent.parent.parent |
||||
|
if (ancestor is None) or (ancestor != 'EMPTY'): |
||||
|
return empty.rotation_quaternion @ empty.parent.rotation_quaternion @ empty.parent.parent.rotation_quaternion @ empty.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.rotation_quaternion |
||||
|
ancestor = empty.parent.parent.parent.parent.parent.parent.parent |
||||
|
if (ancestor is None) or (ancestor != 'EMPTY'): |
||||
|
return empty.rotation_quaternion @ empty.parent.rotation_quaternion @ empty.parent.parent.rotation_quaternion @ empty.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.parent.rotation_quaternion |
||||
|
ancestor = empty.parent.parent.parent.parent.parent.parent.parent.parent |
||||
|
if (ancestor is None) or (ancestor != 'EMPTY'): |
||||
|
return empty.rotation_quaternion @ empty.parent.rotation_quaternion @ empty.parent.parent.rotation_quaternion @ empty.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.parent.parent.rotation_quaternion |
||||
|
ancestor = empty.parent.parent.parent.parent.parent.parent.parent.parent.parent |
||||
|
if (ancestor is None) or (ancestor != 'EMPTY'): |
||||
|
return empty.rotation_quaternion @ empty.parent.rotation_quaternion @ empty.parent.parent.rotation_quaternion @ empty.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.parent.parent.parent.rotation_quaternion |
||||
|
ancestor = empty.parent.parent.parent.parent.parent.parent.parent.parent.parent.parent |
||||
|
if (ancestor is None) or (ancestor != 'EMPTY'): |
||||
|
return empty.rotation_quaternion @ empty.parent.rotation_quaternion @ empty.parent.parent.rotation_quaternion @ empty.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.parent.parent.parent.rotation_quaternion @ empty.parent.parent.parent.parent.parent.parent.parent.parent.parent.rotation_quaternion |
||||
|
raise NotImplementedError("Fake recursion isn't implemented for hierarchies this deep.") |
||||
|
|
||||
|
def makeSameChildren(empty, armature, parent_bone, parent_empty, cumulative_rotation, cumulative_scale, depth=0): |
||||
|
''' |
||||
|
Make a new bone for the given empty, then do the same |
||||
|
recursively for each of the empty's children. |
||||
|
|
||||
|
This is based on Cirno's hard-coded armature creation code at <https://blenderartists.org/t/needed-help-with-creating-bones-in-python-using-position-and-rotation-data/1209120> |
||||
|
''' |
||||
|
name = empty.name |
||||
|
extI = empty.name.rfind(".") |
||||
|
ext = None |
||||
|
if extI > -1: |
||||
|
name = empty.name[:extI] |
||||
|
ext = empty.name[extI+1:] |
||||
|
print(depth*" "+"- "+name) |
||||
|
# ext != "Empty": |
||||
|
# print("Error: the must end with .Empty so the bone naming scheme won't interfere with the object naming scheme.") |
||||
|
# return |
||||
|
wm = empty.matrix_world |
||||
|
current_bone = armature.edit_bones.new(name) |
||||
|
|
||||
|
length = empty.empty_display_size |
||||
|
|
||||
|
# Get rotation difference of 2 points (2 "vectors"): |
||||
|
# |
||||
|
# - <https://docs.blender.org/api/current/mathutils.html?highlight=rotation_difference#mathutils.Vector.rotation_difference> |
||||
|
# - <https://docs.blender.org/api/blender_python_api_2_63_5/mathutils.html> |
||||
|
# - several operations are used in succession in the first code block but some may be only for generating test data |
||||
|
scale = None |
||||
|
transform_quat = None |
||||
|
if parent_bone is not None: |
||||
|
''' |
||||
|
It has a parent, so the **cumulative** transform of the empty is |
||||
|
necessary here to set the length correctly according to scale, |
||||
|
and to set rotation correctly. |
||||
|
TODO: There is no answer at <https://devtalk.blender.org/t/how-to-retrieve-accumulated-transformation-matrix-from-bone-deformation/7729> |
||||
|
- "EDIT: Posebone.matrix is in bonespace. Posebone.matrix rotation rotates armature up vector into bone vector." -Terry on https://blenderartists.org/t/how-to-global-pose-transforms-to-hierarchial-armature/548022/4 |
||||
|
''' |
||||
|
# parent_bone.tail = empty.location # Not always: there is often an offset |
||||
|
|
||||
|
# print("dir parent_bone: {}".format(dir(parent_bone))) |
||||
|
# print("dir parent_bone: {}".format(dir(parent_bone))) |
||||
|
parent_bone_tail = parent_bone.tail |
||||
|
parent_bone_quat_armature_space = Quaternion(parent_empty.rotation_quaternion) |
||||
|
print(depth*" "+" - parent_empty.name:{}".format(parent_empty.name)) |
||||
|
print(depth*" "+" - parent_bone.name:{}".format(parent_bone.name)) |
||||
|
|
||||
|
# create bone at armature origin and set its length |
||||
|
scale = cumulative_scale * empty.scale.z |
||||
|
current_bone.head = [0, 0, 0] |
||||
|
current_bone.tail = [0, 0, length * scale] |
||||
|
|
||||
|
# rotate bone |
||||
|
# print(depth*" "+" - empty.rotation_quaternion:{}".format(Quaternion(empty.rotation_quaternion))) |
||||
|
# print(depth*" "+" - parent_empty.rotation_quaternion:{}".format(Quaternion(parent_empty.rotation_quaternion))) |
||||
|
# ^ empty.rotation_quaternion is verified to be relative to parent |
||||
|
# ^ parent and child rotation being the same is ok even if they look different because rotation_quaternion is relative. |
||||
|
# parent_bone_quat_armature_space = Quaternion(parent_bone.rotation) |
||||
|
# ^ bone only has: matrix, roll, transform and properties that are not transform-related. |
||||
|
# The head and tail locations determine the visual appearance of having direction. |
||||
|
|
||||
|
current_bone_quat_parent_space = Quaternion(empty.rotation_quaternion) |
||||
|
# ^ rotation_quaternion is confirmed to be relative. |
||||
|
# Like matrices, quaternions can be multiplied to accumulate rotational values. |
||||
|
# Multiply parent rotation by the parent space rotation of the child: |
||||
|
# transform_quat = parent_bone_quat_armature_space @ current_bone_quat_parent_space |
||||
|
# transform_quat = current_bone_quat_parent_space @ cumulative_rotation |
||||
|
# transform_quat = Quaternion(empty.rotation_quaternion) @ Quaternion(empty.parent.rotation_quaternion) |
||||
|
transform_quat = getRealRotation(empty) |
||||
|
current_bone.transform(transform_quat.to_matrix()) |
||||
|
|
||||
|
# set position |
||||
|
# new_relative_loc = Quaternion(empty.location) |
||||
|
# ^ The empty.location is relative to the parent_empty's head |
||||
|
# but must be made relative the parent_bone's tail. |
||||
|
old_to_new = Vector(parent_bone.tail) - Vector(parent_empty.location) |
||||
|
new_relative_loc = Vector(empty.location) - old_to_new |
||||
|
# uhoh_if_nonzero = Vector(parent_bone.head) - Vector(parent_empty.location) |
||||
|
# print(depth*" "+" - uhoh_if_nonzero:{}".format(uhoh_if_nonzero)) |
||||
|
# ^ It is nonzero :( |
||||
|
print(depth*" "+" - empty.location:{}".format(empty.location)) |
||||
|
print(depth*" "+" - parent_bone.tail:{}".format(parent_bone.tail)) |
||||
|
print(depth*" "+" - old_to_new:{}".format(old_to_new)) |
||||
|
print(depth*" "+" - new_relative_loc:{}".format(new_relative_loc)) |
||||
|
# print(depth*" "+" - current_bone_offset:{}".format(current_bone_offset)) |
||||
|
current_bone.translate(Vector(new_relative_loc)) |
||||
|
|
||||
|
# connect |
||||
|
current_bone.parent = parent_bone |
||||
|
# current_bone.use_connect = True |
||||
|
''' |
||||
|
^ Bones are often not fully connected even though parented |
||||
|
in block-style models. |
||||
|
- current_bone.use_connect = True forces the base of the |
||||
|
bone to the location of the tail of the parent, |
||||
|
which will make often make the placement inaccurate. |
||||
|
''' |
||||
|
else: # first bone in chain |
||||
|
scale = cumulative_scale * empty.scale.z |
||||
|
transform_quat = empty.rotation_quaternion |
||||
|
current_bone.head = [0, 0, 0] |
||||
|
current_bone.tail = [0, 0, length * scale] |
||||
|
|
||||
|
# rotate bone |
||||
|
quat_armature_space = empty.rotation_quaternion |
||||
|
# current_bone.rotation_quaternion = empty.rotation_quaternion |
||||
|
# ^ AttributeError: 'EditBone' object has no attribute 'rotation_quaternion' |
||||
|
current_bone.transform(quat_armature_space.to_matrix()) |
||||
|
|
||||
|
# set position |
||||
|
current_bone.translate(Vector(empty.location)) |
||||
|
''' |
||||
|
try: |
||||
|
current_bone.translate(Vector(wm @ empty.location)) |
||||
|
except TypeError: |
||||
|
current_bone.translate(Vector(wm * empty.location)) |
||||
|
# ^ Blender < 2.8 matrix multiplication is * |
||||
|
''' |
||||
|
# parent_bone = current_bone |
||||
|
# parent_bone_tail = current_bone.tail |
||||
|
# parent_bone_quat_armature_space = quat_armature_space |
||||
|
if empty.children is not None: |
||||
|
for child in empty.children: |
||||
|
if child.type != 'EMPTY': |
||||
|
continue |
||||
|
makeSameChildren(child, armature, current_bone, empty, transform_quat, scale, depth=depth+1) |
||||
|
|
||||
|
def makeSameChildrenFromRootEmpty(obj): |
||||
|
# for obj in bpy.data.objects: |
||||
|
if obj.type != 'EMPTY': |
||||
|
print("non-Empty {} type: {} isn't compatible with this script".format(obj.name, obj.type)) |
||||
|
return |
||||
|
if (obj.parent is not None) and (obj.parent.type != 'MESH'): |
||||
|
print("WARNING: An object with a parent that isn't a mesh: {} type {} may not be compatible with this script".format(obj.name, obj.type)) |
||||
|
# return |
||||
|
|
||||
|
# Create a new armature if the empty has no parent. |
||||
|
print("{} parent: {}".format(obj.name, obj.parent)) |
||||
|
# print("dir: {}".format(dir(obj))) |
||||
|
print("location: {}".format(obj.location)) |
||||
|
|
||||
|
print("rotation_quaternion: {}".format(obj.rotation_quaternion)) |
||||
|
print("scale: {}".format(obj.scale)) |
||||
|
# See <https://blenderartists.org/t/ |
||||
|
# needed-help-with-creating-bones-in-python-using- |
||||
|
# position-and-rotation-data/1209120>: |
||||
|
armature = bpy.data.armatures.new("Armature") |
||||
|
armature.display_type = 'STICK' |
||||
|
rig = bpy.data.objects.new("Armature", armature) |
||||
|
context.scene.collection.objects.link(rig) |
||||
|
context.view_layer.objects.active = rig |
||||
|
bpy.ops.object.editmode_toggle() |
||||
|
print("hierarchy:") |
||||
|
makeSameChildren(obj, armature, None, None, None, 1.0) |
||||
|
bpy.ops.object.editmode_toggle() |
||||
|
# bpy.context.active_object = rig |
||||
|
|
||||
|
obj.select_set(state=False) |
||||
|
rig.select_set(state=True) |
||||
|
context.view_layer.objects.active = rig |
||||
|
# else: |
||||
|
# print("{} parent: {}".format(obj.name, obj.parent)) |
||||
|
|
||||
|
makeSameChildrenFromRootEmpty(context.view_layer.objects.active) |
||||
|
|
Loading…
Reference in new issue