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

@ -56,11 +56,12 @@ import bpy
from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import BoolProperty, EnumProperty, CollectionProperty from bpy.props import BoolProperty, EnumProperty, CollectionProperty
from bpy.types import Operator from bpy.types import Operator
from .msh_scene import create_scene from .msh_scene_utilities import create_scene
from .msh_scene_save import save_scene from .msh_scene_save import save_scene
from .msh_scene_read import read_scene from .msh_scene_read import read_scene
from .msh_material_properties import * from .msh_material_properties import *
from .msh_skeleton_properties import * from .msh_skeleton_properties import *
from .msh_collision_prim_properties import *
from .msh_to_blend import * from .msh_to_blend import *
from .zaa_to_blend import * from .zaa_to_blend import *
@ -189,6 +190,8 @@ def menu_func_import(self, context):
def register(): def register():
bpy.utils.register_class(CollisionPrimitiveProperties)
bpy.utils.register_class(MaterialProperties) bpy.utils.register_class(MaterialProperties)
bpy.utils.register_class(MaterialPropertiesPanel) bpy.utils.register_class(MaterialPropertiesPanel)
@ -201,11 +204,15 @@ def register():
bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
bpy.types.Material.swbf_msh = bpy.props.PointerProperty(type=MaterialProperties) bpy.types.Object.swbf_msh_coll_prim = bpy.props.PointerProperty(type=CollisionPrimitiveProperties)
bpy.types.Material.swbf_msh_mat = bpy.props.PointerProperty(type=MaterialProperties)
bpy.types.Armature.swbf_msh_skel = bpy.props.CollectionProperty(type=SkeletonProperties) bpy.types.Armature.swbf_msh_skel = bpy.props.CollectionProperty(type=SkeletonProperties)
def unregister(): def unregister():
bpy.utils.unregister_class(CollisionPrimitiveProperties)
bpy.utils.unregister_class(MaterialProperties) bpy.utils.unregister_class(MaterialProperties)
bpy.utils.unregister_class(MaterialPropertiesPanel) bpy.utils.unregister_class(MaterialPropertiesPanel)

View File

@ -1,5 +1,4 @@
""" Gathers the Blender objects from the current scene and returns them as a list of """ Converts currently active Action to an msh Animation """
Model objects. """
import bpy import bpy
import math import math
@ -11,30 +10,40 @@ from .msh_model_utilities import *
from .msh_utilities import * from .msh_utilities import *
from .msh_model_gather import * from .msh_model_gather import *
from .msh_skeleton_utilities import *
from .crc import to_crc from .crc import to_crc
'''
Convert the active Action into an Animation. When exported SWBF anims, there is the issue
that all bones in the anim must be in the skeleton/basepose anim. We guarantee this by
only keying bones if they are in the armature's preserved skeleton (swbf_msh_skel) and
adding dummy frames if the bones are not in the armature.
If a preserved skeleton is not present, we include only the keyed bones and add dummy frames for
the root (root_name)
'''
def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation:
if not armature.animation_data or not armature.animation_data.action:
raise RuntimeError("Cannot export animation data without an active Action on armature!")
action = armature.animation_data.action action = armature.animation_data.action
# Set of bones to include in SKL2/animation stuff # Set of bones to include in SKL2/animation stuff
keyable_bones : Set[str] = set() keyable_bones = get_real_BONES(armature)
msh_skel = armature.data.swbf_msh_skel
has_preserved_skel = len(msh_skel) > 0 # If it doesn't have a preserved skeleton, then we add the scene root.
# If it does have a preserved skeleton, any objects not animatable by blender (i.e. objects above the skeleton, scene root)
# will be included in the preserved skeleton
if not has_preserved_skeleton(armature):
keyable_bones.add(root_name)
if has_preserved_skel: # Subset of above bones to key with dummy frames (all bones not in armature)
for bone in msh_skel: dummy_bones = set([keyable_bone for keyable_bone in keyable_bones if keyable_bone not in armature.data.bones])
#print("Adding {} from preserved skel to exported skeleton".format(bone.name))
keyable_bones.add(bone.name)
elif action:
for group in action.groups:
#print("Adding {} from action groups to exported skeleton".format(group.name))
keyable_bones.add(group.name)
anim = Animation(); anim = Animation();
@ -51,41 +60,41 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation:
anim.end_index = num_frames - 1 anim.end_index = num_frames - 1
anim.bone_frames[root_crc] = ([], [])
for bone in armature.data.bones: for keyable_bone in keyable_bones:
if bone.name in keyable_bones: anim.bone_frames[to_crc(keyable_bone)] = ([], [])
anim.bone_frames[to_crc(bone.name)] = ([], [])
for frame in range(num_frames): for frame in range(num_frames):
frame_time = framerange.x + frame * increment frame_time = framerange.x + frame * increment
bpy.context.scene.frame_set(frame_time) bpy.context.scene.frame_set(frame_time)
for keyable_bone in keyable_bones:
rframe_dummy = RotationFrame(frame, convert_rotation_space(Quaternion())) bone_crc = to_crc(keyable_bone)
tframe_dummy = TranslationFrame(frame, Vector((0.0,0.0,0.0)))
anim.bone_frames[root_crc][0].append(tframe_dummy) if keyable_bone in dummy_bones:
anim.bone_frames[root_crc][1].append(rframe_dummy)
rframe = RotationFrame(frame, convert_rotation_space(Quaternion()))
tframe = TranslationFrame(frame, Vector((0.0,0.0,0.0)))
for bone in armature.pose.bones: else:
if bone.name not in keyable_bones: bone = armature.pose.bones[keyable_bone]
continue
transform = bone.matrix transform = bone.matrix
if bone.parent: if bone.parent:
transform = bone.parent.matrix.inverted() @ transform transform = bone.parent.matrix.inverted() @ transform
loc, rot, _ = transform.decompose() loc, rot, _ = transform.decompose()
rframe = RotationFrame(frame, convert_rotation_space(rot)) rframe = RotationFrame(frame, convert_rotation_space(rot))
tframe = TranslationFrame(frame, convert_vector_space(loc)) tframe = TranslationFrame(frame, convert_vector_space(loc))
anim.bone_frames[to_crc(bone.name)][0].append(tframe) anim.bone_frames[bone_crc][0].append(tframe)
anim.bone_frames[to_crc(bone.name)][1].append(rframe) anim.bone_frames[bone_crc][1].append(rframe)
return anim return anim

View File

@ -0,0 +1,16 @@
""" IntProperty needed to keep track of Collision Primitives types that are imported without indicitive names.
Not sure I needed a PropertyGroup/what a leaner method would be. The prims shouldn't be renamed on import because
they are often referenced in ODFs.
Don't see a reason these should be exposed via a panel or need to be changed..."""
import bpy
from bpy.props import IntProperty
from bpy.types import PropertyGroup
class CollisionPrimitiveProperties(PropertyGroup):
prim_type: IntProperty(name="Primitive Type", default=-1)

View File

@ -18,6 +18,8 @@ class Rendertype(Enum):
NORMALMAPPED_ENVMAPPED = 26 NORMALMAPPED_ENVMAPPED = 26
NORMALMAPPED = 27 NORMALMAPPED = 27
NORMALMAPPED_TILED_ENVMAP = 29 NORMALMAPPED_TILED_ENVMAP = 29
# Placeholders to avoid crashes/import-export inconsistencies
OTHER_1 = 1 OTHER_1 = 1
OTHER_2 = 2 OTHER_2 = 2
OTHER_4 = 4 OTHER_4 = 4

View File

@ -24,10 +24,10 @@ def read_material(blender_material: bpy.types.Material) -> Material:
result = Material() result = Material()
if blender_material.swbf_msh is None: if blender_material.swbf_msh_mat is None:
return result return result
props = blender_material.swbf_msh props = blender_material.swbf_msh_mat
result.specular_color = props.specular_color.copy() result.specular_color = props.specular_color.copy()
result.rendertype = _read_material_props_rendertype(props) result.rendertype = _read_material_props_rendertype(props)

View File

@ -188,6 +188,7 @@ class MaterialProperties(PropertyGroup):
"distort the scene behind them. Should be a normal map " "distort the scene behind them. Should be a normal map "
"with '-forceformat v8u8' in it's '.tga.option' file.") "with '-forceformat v8u8' in it's '.tga.option' file.")
# Below props are for yet unsupported render types
data_value_0: IntProperty(name="", description="First data value") data_value_0: IntProperty(name="", description="First data value")
data_value_1: IntProperty(name="", description="Second data value") data_value_1: IntProperty(name="", description="Second data value")
@ -215,7 +216,7 @@ class MaterialPropertiesPanel(bpy.types.Panel):
layout = self.layout layout = self.layout
material_props = context.material.swbf_msh material_props = context.material.swbf_msh_mat
layout.prop(material_props, "rendertype") layout.prop(material_props, "rendertype")

View File

@ -1,4 +1,4 @@
""" For finding textures and assigning material properties from entries in a Material """ """ For finding textures and assigning MaterialProperties from entries in a Material """
import bpy import bpy
from typing import Dict from typing import Dict

View File

@ -12,7 +12,11 @@ class ModelType(Enum):
CLOTH = 2 CLOTH = 2
BONE = 3 BONE = 3
STATIC = 4 STATIC = 4
SHADOWVOLUME = 6
# Maybe there are only for BF1 models (http://www.secretsociety.com/forum/downloads/BF1/BF1%20Mesh%20File%20Format.txt)?
# According to that link #3 is envelope, not bone, maybe that's for TCW or smthg
# CHILDSKIN = 5 # I didnt bother with these, never encountered one and they might need adjustments to vertex data
SHADOWVOLUME = 6 # Pretty common
class CollisionPrimitiveShape(Enum): class CollisionPrimitiveShape(Enum):
SPHERE = 0 SPHERE = 0

View File

@ -9,6 +9,7 @@ from itertools import zip_longest
from .msh_model import * from .msh_model import *
from .msh_model_utilities import * from .msh_model_utilities import *
from .msh_utilities import * from .msh_utilities import *
from .msh_skeleton_utilities import *
SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"} SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"}
MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"} MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"}
@ -59,7 +60,7 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
else: else:
if obj.parent is not None: if obj.parent is not None:
if obj.parent.type == "ARMATURE": if obj.parent.type == "ARMATURE":
model.parent = obj.parent.parent.name model.parent = obj.parent.parent.name if obj.parent.parent else ""
transform = obj.parent.matrix_local @ transform transform = obj.parent.matrix_local @ transform
else: else:
model.parent = obj.parent.name model.parent = obj.parent.name
@ -298,6 +299,9 @@ def get_collision_primitive(obj: bpy.types.Object) -> CollisionPrimitive:
return primitive return primitive
def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveShape: def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveShape:
""" Gets the CollisionPrimitiveShape of an object or raises an error if """ Gets the CollisionPrimitiveShape of an object or raises an error if
it can't. """ it can't. """
@ -311,8 +315,13 @@ def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveSh
if "box" in name or "cube" in name or "cuboid" in name: if "box" in name or "cube" in name or "cuboid" in name:
return CollisionPrimitiveShape.BOX return CollisionPrimitiveShape.BOX
return CollisionPrimitiveShape.BOX # arc170 fighter has examples of box colliders without proper naming
#raise RuntimeError(f"Object '{obj.name}' has no primitive type specified in it's name!") prim_type = obj.swbf_msh_coll_prim.prim_type
if prim_type in [item.value for item in CollisionPrimitiveShape]:
return CollisionPrimitiveShape(prim_type)
raise RuntimeError(f"Object '{obj.name}' has no primitive type specified in it's name!")
def check_for_bad_lod_suffix(obj: bpy.types.Object): def check_for_bad_lod_suffix(obj: bpy.types.Object):
""" Checks if the object has an LOD suffix that is known to be ignored by """ """ Checks if the object has an LOD suffix that is known to be ignored by """
@ -373,7 +382,9 @@ def select_objects(export_target: str) -> List[bpy.types.Object]:
def expand_armature(armature: bpy.types.Object) -> List[Model]: def expand_armature(armature: bpy.types.Object) -> List[Model]:
proper_BONES = get_real_BONES(armature)
bones: List[Model] = [] bones: List[Model] = []
for bone in armature.data.bones: for bone in armature.data.bones:
@ -398,7 +409,7 @@ def expand_armature(armature: bpy.types.Object) -> List[Model]:
local_translation, local_rotation, _ = transform.decompose() local_translation, local_rotation, _ = transform.decompose()
model.model_type = ModelType.BONE model.model_type = ModelType.BONE if bone.name in proper_BONES else ModelType.NULL
model.name = bone.name model.name = bone.name
model.transform.rotation = convert_rotation_space(local_rotation) model.transform.rotation = convert_rotation_space(local_rotation)
model.transform.translation = convert_vector_space(local_translation) model.transform.translation = convert_vector_space(local_translation)

View File

@ -26,14 +26,17 @@ def inject_dummy_data(model : Model):
model.geometry = [dummy_seg] model.geometry = [dummy_seg]
model.model_type = ModelType.STATIC model.model_type = ModelType.STATIC
def convert_vector_space_(vec: Vector) -> Vector: def convert_vector_space(vec: Vector) -> Vector:
return Vector((-vec.x, vec.z, vec.y)) return Vector((-vec.x, vec.z, vec.y))
def convert_rotation_space_(quat: Quaternion) -> Quaternion: def convert_scale_space(vec: Vector) -> Vector:
return Vector(vec.xzy)
def convert_rotation_space(quat: Quaternion) -> Quaternion:
return Quaternion((-quat.w, quat.x, -quat.z, -quat.y)) return Quaternion((-quat.w, quat.x, -quat.z, -quat.y))
def model_transform_to_matrix(transform: ModelTransform): def model_transform_to_matrix(transform: ModelTransform):
return Matrix.Translation(convert_vector_space_(transform.translation)) @ convert_rotation_space_(transform.rotation).to_matrix().to_4x4() return Matrix.Translation(convert_vector_space(transform.translation)) @ convert_rotation_space(transform.rotation).to_matrix().to_4x4()
def scale_segments(scale: Vector, segments: List[GeometrySegment]): def scale_segments(scale: Vector, segments: List[GeometrySegment]):
""" Scales are positions in the GeometrySegment list. """ """ Scales are positions in the GeometrySegment list. """
@ -145,13 +148,3 @@ def is_model_name_unused(name: str, models: List[Model]) -> bool:
return True return True
def convert_vector_space(vec: Vector) -> Vector:
return Vector((-vec.x, vec.z, vec.y))
def convert_scale_space(vec: Vector) -> Vector:
return Vector(vec.xzy)
def convert_rotation_space(quat: Quaternion) -> Quaternion:
return Quaternion((-quat.w, quat.x, -quat.z, -quat.y))

View File

@ -4,17 +4,14 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Dict from typing import List, Dict
from copy import copy from copy import copy
import bpy import bpy
from mathutils import Vector from mathutils import Vector
from .msh_model import Model, Animation, ModelType from .msh_model import Model, Animation, ModelType
from .msh_model_gather import gather_models
from .msh_model_utilities import sort_by_parent, has_multiple_root_models, reparent_model_roots, get_model_world_matrix, inject_dummy_data
from .msh_model_triangle_strips import create_models_triangle_strips
from .msh_material import * from .msh_material import *
from .msh_material_gather import gather_materials
from .msh_material_utilities import remove_unused_materials
from .msh_utilities import * from .msh_utilities import *
from .msh_anim_gather import extract_anim
@dataclass @dataclass
class SceneAABB: class SceneAABB:
@ -47,69 +44,4 @@ class Scene:
animation: Animation = None animation: Animation = None
skeleton: List[int] = field(default_factory=list) skeleton: List[int] = field(default_factory=list)
def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool, export_anim: bool) -> Scene:
""" Create a msh Scene from the active Blender scene. """
scene = Scene()
scene.name = bpy.context.scene.name
scene.materials = gather_materials()
scene.models, armature_obj = gather_models(apply_modifiers=apply_modifiers, export_target=export_target, skeleton_only=skel_only)
scene.models = sort_by_parent(scene.models)
if generate_triangle_strips:
scene.models = create_models_triangle_strips(scene.models)
else:
for model in scene.models:
if model.geometry:
for segment in model.geometry:
segment.triangle_strips = segment.triangles
if has_multiple_root_models(scene.models):
scene.models = reparent_model_roots(scene.models)
scene.materials = remove_unused_materials(scene.materials, scene.models)
root = scene.models[0]
if export_anim:
if armature_obj is not None:
scene.animation = extract_anim(armature_obj, root.name)
else:
raise Exception("Export Error: Could not find an armature object from which to export an animation!")
if skel_only and root.model_type == ModelType.NULL:
# For ZenAsset
inject_dummy_data(root)
return scene
def create_scene_aabb(scene: Scene) -> SceneAABB:
""" Create a SceneAABB for a Scene. """
global_aabb = SceneAABB()
for model in scene.models:
if model.geometry is None or model.hidden:
continue
model_world_matrix = get_model_world_matrix(model, scene.models)
model_aabb = SceneAABB()
for segment in model.geometry:
segment_aabb = SceneAABB()
for pos in segment.positions:
segment_aabb.integrate_position(model_world_matrix @ pos)
model_aabb.integrate_aabb(segment_aabb)
global_aabb.integrate_aabb(model_aabb)
return global_aabb

View File

@ -117,20 +117,10 @@ def read_scene(input_file, anim_only=False, debug=0) -> Scene:
if seg.weights: if seg.weights:
for weight_set in seg.weights: for weight_set in seg.weights:
for vweight in weight_set: for vweight in weight_set:
if vweight.bone in mndx_remap: if vweight.bone in mndx_remap:
vweight.bone = mndx_remap[vweight.bone] vweight.bone = mndx_remap[vweight.bone]
else: else:
vweight.bone = 0 vweight.bone = 0
# So in the new republic boba example, the weights aimed for bone_head instead map to sv_jettrooper...
#for key, val in mndx_remap.items():
#if scene.models[val].name == "bone_head" or scene.models[val].name == "sv_jettrooper":
#print("Key: {} is mapped to val: {}".format(key, val))
#print("Key: {}, val {} is model: {}".format(key, val, scene.models[val].name))
return scene return scene
@ -276,10 +266,6 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model:
else: else:
modl.skip_bytes(1) modl.skip_bytes(1)
global debug_level
if debug_level > 0:
print(modl.indent + "Read model " + model.name + " of type: " + str(model.model_type)[10:])
return model return model
@ -292,10 +278,6 @@ def _read_tran(tran: Reader) -> ModelTransform:
xform.rotation = tran.read_quat() xform.rotation = tran.read_quat()
xform.translation = tran.read_vec() xform.translation = tran.read_vec()
global debug_level
if debug_level > 0:
print(tran.indent + "Rot: {} Loc: {}".format(str(xform.rotation), str(xform.translation)))
return xform return xform
@ -341,6 +323,7 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
for _ in range(num_texcoords): for _ in range(num_texcoords):
geometry_seg.texcoords.append(Vector(uv0l.read_f32(2))) geometry_seg.texcoords.append(Vector(uv0l.read_f32(2)))
# TODO: Can't remember exact issue here...
elif next_header == "NDXL": elif next_header == "NDXL":
with segm.read_child() as ndxl: with segm.read_child() as ndxl:
@ -399,7 +382,8 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
geometry_seg.triangle_strips = strips geometry_seg.triangle_strips = strips
#if segm.read_u16 != 0: #trailing 0 bug https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP # TODO: Dont know how to handle trailing 0 bug yet: https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP
#if segm.read_u16 != 0:
# segm.skip_bytes(-2) # segm.skip_bytes(-2)
elif next_header == "WGHT": elif next_header == "WGHT":
@ -439,6 +423,8 @@ def _read_anm2(anm2: Reader) -> Animation:
# Dont even know what CYCL's data does. Tried playing # Dont even know what CYCL's data does. Tried playing
# with the values but didn't change anything in zenasset or ingame... # with the values but didn't change anything in zenasset or ingame...
# Besides num_anims, which is never > 1 for any SWBF1/2 mshs I've seen
''' '''
num_anims = cycl.read_u32() num_anims = cycl.read_u32()

View File

@ -2,7 +2,8 @@
from itertools import islice from itertools import islice
from typing import Dict from typing import Dict
from .msh_scene import Scene, create_scene_aabb from .msh_scene import Scene
from .msh_scene_utilities import create_scene_aabb
from .msh_model import * from .msh_model import *
from .msh_material import * from .msh_material import *
from .msh_writer import Writer from .msh_writer import Writer
@ -40,6 +41,7 @@ def save_scene(output_file, scene: Scene):
with hedr.create_child("SKL2") as skl2: with hedr.create_child("SKL2") as skl2:
_write_skl2(skl2, scene.animation) _write_skl2(skl2, scene.animation)
# Def not necessary, including anyways
with hedr.create_child("BLN2") as bln2: with hedr.create_child("BLN2") as bln2:
_write_bln2(bln2, scene.animation) _write_bln2(bln2, scene.animation)

View File

@ -0,0 +1,85 @@
""" Contains Scene object for representing a .msh file and the function to create one
from a Blender scene. """
from dataclasses import dataclass, field
from typing import List, Dict
from copy import copy
import bpy
from mathutils import Vector
from .msh_model import Model, Animation, ModelType
from .msh_scene import Scene, SceneAABB
from .msh_model_gather import gather_models
from .msh_model_utilities import sort_by_parent, has_multiple_root_models, reparent_model_roots, get_model_world_matrix, inject_dummy_data
from .msh_model_triangle_strips import create_models_triangle_strips
from .msh_material import *
from .msh_material_gather import gather_materials
from .msh_material_utilities import remove_unused_materials
from .msh_utilities import *
from .msh_anim_gather import extract_anim
def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool, export_anim: bool) -> Scene:
""" Create a msh Scene from the active Blender scene. """
scene = Scene()
scene.name = bpy.context.scene.name
scene.materials = gather_materials()
scene.models, armature_obj = gather_models(apply_modifiers=apply_modifiers, export_target=export_target, skeleton_only=skel_only)
scene.models = sort_by_parent(scene.models)
if generate_triangle_strips:
scene.models = create_models_triangle_strips(scene.models)
else:
for model in scene.models:
if model.geometry:
for segment in model.geometry:
segment.triangle_strips = segment.triangles
if has_multiple_root_models(scene.models):
scene.models = reparent_model_roots(scene.models)
scene.materials = remove_unused_materials(scene.materials, scene.models)
root = scene.models[0]
if export_anim:
if armature_obj is not None:
scene.animation = extract_anim(armature_obj, root.name)
else:
raise Exception("Export Error: Could not find an armature object from which to export an animation!")
if skel_only and root.model_type == ModelType.NULL:
# For ZenAsset
inject_dummy_data(root)
return scene
def create_scene_aabb(scene: Scene) -> SceneAABB:
""" Create a SceneAABB for a Scene. """
global_aabb = SceneAABB()
for model in scene.models:
if model.geometry is None or model.hidden:
continue
model_world_matrix = get_model_world_matrix(model, scene.models)
model_aabb = SceneAABB()
for segment in model.geometry:
segment_aabb = SceneAABB()
for pos in segment.positions:
segment_aabb.integrate_position(model_world_matrix @ pos)
model_aabb.integrate_aabb(segment_aabb)
global_aabb.integrate_aabb(model_aabb)
return global_aabb

View File

@ -1,10 +1,15 @@
""" Contains Blender properties and UI for .msh materials. """ """ Keeps track of exact skeleton when imported. Possibly needed for exporting skeleton-compatible animations. Will
probably be needed (with a matrix property) if we:
- add tip-to-tail adjustment and/or omit roots/effectors for imported skeletons to keep track of the original bone transforms
- add some sort of basepose-adjustment animation import option for already imported skeletons
I guess this might not need a panel, but I included it because the docs might need to reference it and
people may want to exclude certain bones without deleting keyframes.
"""
import bpy import bpy
from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatVectorProperty, IntProperty from bpy.props import StringProperty
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from .msh_material_ui_strings import *
from .msh_model import *
class SkeletonProperties(PropertyGroup): class SkeletonProperties(PropertyGroup):

View File

@ -0,0 +1,211 @@
""" Helpers for SWBF skeleton-armature mapping """
import bpy
import math
from typing import List, Set, Dict, Tuple
from .msh_scene import Scene
from .msh_model import *
from .msh_model_utilities import *
from .crc import *
def has_preserved_skeleton(armature : bpy.types.Armature):
return len(armature.data.swbf_msh_skel) > 0
'''Returns all bones that should be marked as BONE'''
def get_real_BONES(armature: bpy.types.Armature) -> Set[str]:
# First priority, add the names of the skeleton preserved on import
skel_props = armature.data.swbf_msh_skel
# Second, add all keyed bones
action = armature.animation_data.action if armature.animation_data else None
# Third, just add all bones in armature
# Set of bones to include
real_bones : Set[str] = set()
if len(skel_props) > 0:
for bone in skel_props:
#print(f"{bone.name} is a real BONE")
real_bones.add(bone.name)
elif action:
for group in armature.animation_data.action.groups:
#print(f"{group.name} is a real BONE")
real_bones.add(group.name)
else:
for bone in armature.data.bones:
#print(f"{bone.name} is a real BONE")
real_bones.add(bone.name)
return real_bones
'''
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
- has model_type == BONE
- is weighted to
- has a parent and child that must be in the armature
This may need a lot of adjustments, don't think I can prove it's validity but it has worked very well
and handles all stock + ZETools + Pandemic XSI exporter models I've tested
'''
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 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 or
# connecting bones tip-to-tail
#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

View File

@ -4,20 +4,23 @@
import bpy import bpy
import bmesh import bmesh
import math import math
from enum import Enum from enum import Enum
from typing import List, Set, Dict, Tuple from typing import List, Set, Dict, Tuple
from itertools import zip_longest
from .msh_scene import Scene from .msh_scene import Scene
from .msh_material_to_blend import * from .msh_material_to_blend import *
from .msh_model import * from .msh_model import *
from .msh_model_utilities import * from .msh_skeleton_utilities import *
from .msh_utilities import * from .msh_model_gather import get_is_model_hidden
from .msh_model_gather import *
from .msh_skeleton_properties import *
from .crc import * from .crc import *
import os import os
# Extracts and applies anims in the scene to the currently selected armature # Extracts and applies anims in the scene to the currently selected armature
def extract_and_apply_anim(filename : str, scene : Scene): def extract_and_apply_anim(filename : str, scene : Scene):
@ -100,170 +103,7 @@ def extract_and_apply_anim(filename : str, scene : Scene):
# Create the msh hierachy. Armatures are not created here. Much of this could use some optimization...
'''
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.
def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) -> Dict[str, bpy.types.Object]: 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 # 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: for model in sorted_models:
new_obj = None 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) new_mesh = bpy.data.meshes.new(model.name)
verts = [] verts = []
faces = [] faces = []
offset = 0 offset = 0
mat_name = ""
full_texcoords = [] full_texcoords = []
weights_offsets = {} weights_offsets = {}
face_range_to_material_index = []
if model.geometry: if model.geometry:
#if model.collisionprimitive is None:
# print(f"On model: {model.name}")
for i,seg in enumerate(model.geometry): 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] 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: if seg.weights:
weights_offsets[offset] = seg.weights weights_offsets[offset] = seg.weights
@ -303,6 +148,8 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material])
else: else:
full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))] full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))]
face_range_lower = len(faces)
if seg.triangles: if seg.triangles:
faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles] faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles]
else: 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)]) face = tuple([offset + strip[j] for j in range(i,i+3)])
faces.append(face) 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) offset += len(seg.positions)
new_mesh.from_pydata(verts, [], faces) new_mesh.from_pydata(verts, [], faces)
new_mesh.update() new_mesh.update()
new_mesh.validate() new_mesh.validate()
# If tex coords are present, add material and UV data
if full_texcoords: if full_texcoords:
edit_mesh = bmesh.new() 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() uvlayer = edit_mesh.loops.layers.uv.verify()
for edit_mesh_face in edit_mesh.faces: 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): 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]) loop[uvlayer].uv = tuple([texcoord.x, texcoord.y])
edit_mesh.to_mesh(new_mesh) edit_mesh.to_mesh(new_mesh)
edit_mesh.free() edit_mesh.free()
new_obj = bpy.data.objects.new(new_mesh.name, new_mesh) 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: if index not in vertex_groups_indicies:
model_name = scene.models[index].name 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] = new_obj.vertex_groups.new(name=model_name)
vertex_groups_indicies[index].add([offset + i], weight.weight, 'ADD') 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 Assign Material slots
else: '''
new_obj.data.materials.append(material) 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: 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_mode = "QUATERNION"
new_obj.rotation_quaternion = convert_rotation_space(model.transform.rotation) 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) 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) texImage.image = bpy.data.images.load(diffuse_texture_path)
new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) 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 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) armature = None if not skel else required_skeleton_to_armature(skel, model_map, scene)
if armature is not None: if armature is not None:
preserved = armature.data.swbf_msh_skel preserved_skel = armature.data.swbf_msh_skel
for model in scene.models: for model in scene.models:
if to_crc(model.name) in scene.skeleton: if to_crc(model.name) in scene.skeleton or model.model_type == ModelType.BONE:
entry = preserved.add() entry = preserved_skel.add()
entry.name = model.name entry.name = model.name