From 3141d474e6b5d749efb37a800254abce53823d8a Mon Sep 17 00:00:00 2001 From: poikilos <7557867+poikilos@users.noreply.github.com> Date: Tue, 9 Nov 2021 17:13:08 -0500 Subject: [PATCH] Add a WIP version of the new script. Make a guide for the utilities/blender folder. --- utilities/blender/HowTo-BlenderScript.md | 9 + .../blender/hierarchy_of_empties_to_bones.py | 325 ++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 utilities/blender/HowTo-BlenderScript.md create mode 100644 utilities/blender/hierarchy_of_empties_to_bones.py diff --git a/utilities/blender/HowTo-BlenderScript.md b/utilities/blender/HowTo-BlenderScript.md new file mode 100644 index 0000000..05d1301 --- /dev/null +++ b/utilities/blender/HowTo-BlenderScript.md @@ -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. diff --git a/utilities/blender/hierarchy_of_empties_to_bones.py b/utilities/blender/hierarchy_of_empties_to_bones.py new file mode 100644 index 0000000..93e7c50 --- /dev/null +++ b/utilities/blender/hierarchy_of_empties_to_bones.py @@ -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). + . 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). + . 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 . + ''' + 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 + ''' + 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"): + # + # - + # - + # - 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 + - "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 : + 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) +