Skeleton related functions separated into msh_skeleton_utilities + CollisionPrimProperties added for imported primitives that don't have proper names + misc changes to avoid circular imports + minor refactors

This commit is contained in:
William Herald Snyder
2022-01-18 15:16:49 -05:00
parent bae32bdfe4
commit dce3f4e498
17 changed files with 469 additions and 347 deletions

View File

@@ -4,20 +4,23 @@
import bpy
import bmesh
import math
from enum import Enum
from typing import List, Set, Dict, Tuple
from itertools import zip_longest
from .msh_scene import Scene
from .msh_material_to_blend import *
from .msh_model import *
from .msh_model_utilities import *
from .msh_utilities import *
from .msh_model_gather import *
from .msh_skeleton_properties import *
from .msh_skeleton_utilities import *
from .msh_model_gather import get_is_model_hidden
from .crc import *
import os
# Extracts and applies anims in the scene to the currently selected armature
def extract_and_apply_anim(filename : str, scene : Scene):
@@ -100,170 +103,7 @@ def extract_and_apply_anim(filename : str, scene : Scene):
'''
Creates armature from the required nodes.
Assumes the required_skeleton is already sorted by parent.
Uses model_map to get the world matrix of each bone (hacky, see NOTE)
'''
def required_skeleton_to_armature(required_skeleton : List[Model], model_map : Dict[str, bpy.types.Object], msh_scene : Scene) -> bpy.types.Object:
armature = bpy.data.armatures.new("skeleton")
armature_obj = bpy.data.objects.new("skeleton", armature)
bpy.context.view_layer.active_layer_collection.collection.objects.link(armature_obj)
bones_set = set([model.name for model in required_skeleton])
armature_obj.select_set(True)
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
for bone in required_skeleton:
edit_bone = armature.edit_bones.new(bone.name)
if bone.parent and bone.parent in bones_set:
edit_bone.parent = armature.edit_bones[bone.parent]
'''
NOTE: I recall there being some rare issue with the get_world_matrix utility func.
Never bothered to figure it out and referencing the bone object's world mat always works.
Bone objects will be deleted later.
'''
bone_obj = model_map[bone.name]
edit_bone.matrix = bone_obj.matrix_world
edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,1.0,0.0))
bone_children = [b for b in get_model_children(bone, required_skeleton)]
'''
Perhaps we'll add an option for importing bones tip-to-tail, but that would
require preserving their original transforms as changing the tail position
changes the bones' transform...
'''
tail_pos = Vector()
if bone_children:
for bone_child in bone_children:
tail_pos += bone_obj.matrix_world.translation
tail_pos = tail_pos / len(bone_children)
edit_bone.length = .5 #(tail_pos - edit_bone.head).magnitude
else:
bone_length = .5# edit_bone.parent.length if edit_bone.parent is not None else .5
edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,bone_length,0.0))
bpy.ops.object.mode_set(mode='OBJECT')
armature_obj.select_set(True)
bpy.context.view_layer.update()
return armature_obj
'''
Ok, so this method is crucial. What this does is:
1) Find all nodes that are weighted to by skinned segments.
2) A node must be included in the armature if:
- It is in SKL2 and is not the scene root
- It is weighted to
- It has a parent and child that must be in the armature
'''
def extract_required_skeleton(scene: Scene) -> List[Model]:
# Will map Model names to Models in scene, for convenience
model_dict : Dict[str, Model] = {}
'''
Will contain hashes of all models that definitely need to be in the skeleton/armature.
We initialize it with the contents of SKL2 i.e. the nodes that are animated.
For now this includes the scene root, but that'll be excluded later.
'''
skeleton_hashes = set(scene.skeleton)
'''
We also need to add all nodes that are weighted to. These are not necessarily in
SKL2, as SKL2 seems to only reference nodes that are keyframed.
However, sometimes SKL2 is not included when it should be, but it can be mostly recovered
by checking which models are BONEs.
'''
for model in scene.models:
model_dict[model.name] = model
#if to_crc(model.name) in scene.skeleton:
# print("Skel model {} of type {} has parent {}".format(model.name, model.model_type, model.parent))
if model.model_type == ModelType.BONE:
skeleton_hashes.add(to_crc(model.name))
elif model.geometry:
for seg in model.geometry:
if seg.weights:
for weight_set in seg.weights:
for weight in weight_set:
model_weighted_to = scene.models[weight.bone]
if to_crc(model_weighted_to.name) not in skeleton_hashes:
skeleton_hashes.add(to_crc(model_weighted_to.name))
# The result of this function (to be sorted by parent)
required_skeleton_models = []
# Set of nodes to be included in required skeleton/were visited
visited_nodes = set()
'''
Here we add all skeleton nodes (except root) and any necessary ancestors to the armature.
- e.g. in bone_x/eff_x/eff_y, the effectors do not have to be in armature, as they are not ancestors of a bone
- but in bone_x/eff_x/eff_y/bone_y, they do.
'''
for bone in sort_by_parent(scene.models):
# make sure we exclude the scene root and any nodes irrelevant to the armature
if not bone.parent or to_crc(bone.name) not in skeleton_hashes:
continue
potential_bones = [bone]
visited_nodes.add(bone.name)
# Stacked transform will be needed if we decide to include an option for excluding effectors/roots
#stacked_transform = model_transform_to_matrix(bone.transform)
curr_ancestor = model_dict[bone.parent]
while True:
# If we hit a non-skin scene root, that means we just add the bone we started with, no ancestors.
if not curr_ancestor.parent and curr_ancestor.model_type != ModelType.SKIN:
required_skeleton_models.append(bone)
visited_nodes.add(bone.name)
break
# If we encounter another bone, a skin, or a previously visited object, we need to add the bone and its
# ancestors.
elif to_crc(curr_ancestor.name) in scene.skeleton or curr_ancestor.model_type == ModelType.SKIN or curr_ancestor.name in visited_nodes:
for potential_bone in potential_bones:
required_skeleton_models.append(potential_bone)
visited_nodes.add(potential_bone.name)
break
# Add ancestor to potential bones, update next ancestor
else:
if curr_ancestor.name not in visited_nodes:
potential_bones.insert(0, curr_ancestor)
curr_ancestor = model_dict[curr_ancestor.parent]
#stacked_transform = model_transform_to_matrix(curr_ancestor.transform) @ stacked_transform
return required_skeleton_models
# Create the msh hierachy. Armatures are not created here.
# Create the msh hierachy. Armatures are not created here. Much of this could use some optimization...
def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) -> Dict[str, bpy.types.Object]:
# This will be filled with model names -> Blender objects and returned
@@ -274,27 +114,32 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material])
for model in sorted_models:
new_obj = None
if model.model_type == ModelType.STATIC or model.model_type == ModelType.SKIN:
if model.model_type == ModelType.STATIC or model.model_type == ModelType.SKIN or model.model_type == ModelType.SHADOWVOLUME:
new_mesh = bpy.data.meshes.new(model.name)
verts = []
faces = []
offset = 0
mat_name = ""
full_texcoords = []
weights_offsets = {}
face_range_to_material_index = []
if model.geometry:
#if model.collisionprimitive is None:
# print(f"On model: {model.name}")
for i,seg in enumerate(model.geometry):
if i == 0:
mat_name = seg.material_name
verts += [tuple(convert_vector_space(v)) for v in seg.positions]
#if model.collisionprimitive is None:
# print("Importing segment with material: {} with and {} verts".format(seg.material_name, len(seg.positions)))
if seg.weights:
weights_offsets[offset] = seg.weights
@@ -303,6 +148,8 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material])
else:
full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))]
face_range_lower = len(faces)
if seg.triangles:
faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles]
else:
@@ -311,13 +158,17 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material])
face = tuple([offset + strip[j] for j in range(i,i+3)])
faces.append(face)
face_range_upper = len(faces)
face_range_to_material_index.append((face_range_lower, face_range_upper, i))
offset += len(seg.positions)
new_mesh.from_pydata(verts, [], faces)
new_mesh.update()
new_mesh.validate()
# If tex coords are present, add material and UV data
if full_texcoords:
edit_mesh = bmesh.new()
@@ -326,7 +177,12 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material])
uvlayer = edit_mesh.loops.layers.uv.verify()
for edit_mesh_face in edit_mesh.faces:
mesh_face = faces[edit_mesh_face.index]
face_index = edit_mesh_face.index
mesh_face = faces[face_index]
for frL, frU, ind in face_range_to_material_index:
if face_index >= frL and face_index < frU:
edit_mesh_face.material_index = ind
for i,loop in enumerate(edit_mesh_face.loops):
@@ -334,7 +190,8 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material])
loop[uvlayer].uv = tuple([texcoord.x, texcoord.y])
edit_mesh.to_mesh(new_mesh)
edit_mesh.free()
edit_mesh.free()
new_obj = bpy.data.objects.new(new_mesh.name, new_mesh)
@@ -348,21 +205,19 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material])
if index not in vertex_groups_indicies:
model_name = scene.models[index].name
#print("Adding new vertex group with index {} and model name {}".format(index, model_name))
vertex_groups_indicies[index] = new_obj.vertex_groups.new(name=model_name)
vertex_groups_indicies[index].add([offset + i], weight.weight, 'ADD')
'''
Assign Materials - will do per segment later...
'''
if mat_name:
material = materials_map[mat_name]
if new_obj.data.materials:
new_obj.data.materials[0] = material
else:
new_obj.data.materials.append(material)
'''
Assign Material slots
'''
if model.geometry:
for seg in model.geometry:
if seg.material_name:
material = materials_map[seg.material_name]
new_obj.data.materials.append(material)
else:
@@ -380,6 +235,9 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material])
new_obj.rotation_mode = "QUATERNION"
new_obj.rotation_quaternion = convert_rotation_space(model.transform.rotation)
if model.collisionprimitive is not None:
new_obj.swbf_msh_coll_prim.prim_type = model.collisionprimitive.shape.value
bpy.context.collection.objects.link(new_obj)
@@ -404,7 +262,7 @@ def extract_materials(folder_path: str, scene: Scene) -> Dict[str, bpy.types.Mat
texImage.image = bpy.data.images.load(diffuse_texture_path)
new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color'])
fill_material_props(material, new_mat.swbf_msh)
fill_material_props(material, new_mat.swbf_msh_mat)
extracted_materials[material_name] = new_mat
@@ -430,10 +288,10 @@ def extract_scene(filepath: str, scene: Scene):
armature = None if not skel else required_skeleton_to_armature(skel, model_map, scene)
if armature is not None:
preserved = armature.data.swbf_msh_skel
preserved_skel = armature.data.swbf_msh_skel
for model in scene.models:
if to_crc(model.name) in scene.skeleton:
entry = preserved.add()
if to_crc(model.name) in scene.skeleton or model.model_type == ModelType.BONE:
entry = preserved_skel.add()
entry.name = model.name