From 043127c1f8f1aebe535c85c3ed6422cf6ae6d8ab Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Sun, 11 Oct 2020 12:08:11 -0400 Subject: [PATCH 01/39] anim seed --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 29e4cdf..1fb1485 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ - +.DS_Store *.msh # Created by https://www.gitignore.io/api/python,visualstudiocode From 603068b5b5256b97fb50bfdae31590b5d7968c59 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Sun, 11 Oct 2020 12:11:15 -0400 Subject: [PATCH 02/39] Hardcoded spa1_prop_door amims test --- addons/io_scene_swbf_msh/crc.py | 142 +++++++++++++++++++ addons/io_scene_swbf_msh/msh_anim_gather.py | 48 +++++++ addons/io_scene_swbf_msh/msh_model.py | 11 +- addons/io_scene_swbf_msh/msh_model_gather.py | 7 +- addons/io_scene_swbf_msh/msh_scene.py | 6 +- addons/io_scene_swbf_msh/msh_scene_save.py | 57 +++++++- 6 files changed, 265 insertions(+), 6 deletions(-) create mode 100644 addons/io_scene_swbf_msh/crc.py create mode 100644 addons/io_scene_swbf_msh/msh_anim_gather.py diff --git a/addons/io_scene_swbf_msh/crc.py b/addons/io_scene_swbf_msh/crc.py new file mode 100644 index 0000000..8b59617 --- /dev/null +++ b/addons/io_scene_swbf_msh/crc.py @@ -0,0 +1,142 @@ +from struct import pack + + +class CRCError(Exception): + def __init__(self, val): + self.val = val + + def __str__(self): + return '{0}'.format(self.val) + +# CRC lookup table. +TABLE_32 = ( + 0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9, + 0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005, + 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61, + 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD, + 0x4C11DB70, 0x48D0C6C7, 0x4593E01E, 0x4152FDA9, + 0x5F15ADAC, 0x5BD4B01B, 0x569796C2, 0x52568B75, + 0x6A1936C8, 0x6ED82B7F, 0x639B0DA6, 0x675A1011, + 0x791D4014, 0x7DDC5DA3, 0x709F7B7A, 0x745E66CD, + 0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039, + 0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5, + 0xBE2B5B58, 0xBAEA46EF, 0xB7A96036, 0xB3687D81, + 0xAD2F2D84, 0xA9EE3033, 0xA4AD16EA, 0xA06C0B5D, + 0xD4326D90, 0xD0F37027, 0xDDB056FE, 0xD9714B49, + 0xC7361B4C, 0xC3F706FB, 0xCEB42022, 0xCA753D95, + 0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1, + 0xE13EF6F4, 0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D, + 0x34867077, 0x30476DC0, 0x3D044B19, 0x39C556AE, + 0x278206AB, 0x23431B1C, 0x2E003DC5, 0x2AC12072, + 0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16, + 0x018AEB13, 0x054BF6A4, 0x0808D07D, 0x0CC9CDCA, + 0x7897AB07, 0x7C56B6B0, 0x71159069, 0x75D48DDE, + 0x6B93DDDB, 0x6F52C06C, 0x6211E6B5, 0x66D0FB02, + 0x5E9F46BF, 0x5A5E5B08, 0x571D7DD1, 0x53DC6066, + 0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA, + 0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E, + 0xBFA1B04B, 0xBB60ADFC, 0xB6238B25, 0xB2E29692, + 0x8AAD2B2F, 0x8E6C3698, 0x832F1041, 0x87EE0DF6, + 0x99A95DF3, 0x9D684044, 0x902B669D, 0x94EA7B2A, + 0xE0B41DE7, 0xE4750050, 0xE9362689, 0xEDF73B3E, + 0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2, + 0xC6BCF05F, 0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686, + 0xD5B88683, 0xD1799B34, 0xDC3ABDED, 0xD8FBA05A, + 0x690CE0EE, 0x6DCDFD59, 0x608EDB80, 0x644FC637, + 0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB, + 0x4F040D56, 0x4BC510E1, 0x46863638, 0x42472B8F, + 0x5C007B8A, 0x58C1663D, 0x558240E4, 0x51435D53, + 0x251D3B9E, 0x21DC2629, 0x2C9F00F0, 0x285E1D47, + 0x36194D42, 0x32D850F5, 0x3F9B762C, 0x3B5A6B9B, + 0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF, + 0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623, + 0xF12F560E, 0xF5EE4BB9, 0xF8AD6D60, 0xFC6C70D7, + 0xE22B20D2, 0xE6EA3D65, 0xEBA91BBC, 0xEF68060B, + 0xD727BBB6, 0xD3E6A601, 0xDEA580D8, 0xDA649D6F, + 0xC423CD6A, 0xC0E2D0DD, 0xCDA1F604, 0xC960EBB3, + 0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7, + 0xAE3AFBA2, 0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B, + 0x9B3660C6, 0x9FF77D71, 0x92B45BA8, 0x9675461F, + 0x8832161A, 0x8CF30BAD, 0x81B02D74, 0x857130C3, + 0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640, + 0x4E8EE645, 0x4A4FFBF2, 0x470CDD2B, 0x43CDC09C, + 0x7B827D21, 0x7F436096, 0x7200464F, 0x76C15BF8, + 0x68860BFD, 0x6C47164A, 0x61043093, 0x65C52D24, + 0x119B4BE9, 0x155A565E, 0x18197087, 0x1CD86D30, + 0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC, + 0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088, + 0x2497D08D, 0x2056CD3A, 0x2D15EBE3, 0x29D4F654, + 0xC5A92679, 0xC1683BCE, 0xCC2B1D17, 0xC8EA00A0, + 0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB, 0xDBEE767C, + 0xE3A1CBC1, 0xE760D676, 0xEA23F0AF, 0xEEE2ED18, + 0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4, + 0x89B8FD09, 0x8D79E0BE, 0x803AC667, 0x84FBDBD0, + 0x9ABC8BD5, 0x9E7D9662, 0x933EB0BB, 0x97FFAD0C, + 0xAFB010B1, 0xAB710D06, 0xA6322BDF, 0xA2F33668, + 0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4 +) + +# Used to calculate the lowercase CRC. +TOLOWER = ( + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, + 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, + 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, + 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, + 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, + 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, + 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, + 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, + 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, + 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, + 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, + 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, + 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff +) + + +def return_lowest_bits(n): + '''Simulate unsigned behavior.''' + return n & 0xFFFFFFFF + + +def crc(string): + '''Calculate the Zero CRC from string and return it as number.''' + crc_ = 0 + crc_ = return_lowest_bits(~crc_) + if string: + for char in string: + ind = (crc_ >> 24) + ind = ind ^ TOLOWER[ord(char)] + crc_ = return_lowest_bits(crc_ << 8) ^ TABLE_32[ind] + return return_lowest_bits(~crc_) + + +def strcrc(string): + '''Calculate the Zero CRC and return it in a structure + usable in .msh files.''' + return pack(' List[Animation]: + + anim_data = Animation(); + + action = armature.animation_data.action + + framerange = action.frame_range + increment = (framerange.y - framerange.x) / 20.0 + offset = framerange.x; + + anim_data.bone_transforms[armature.parent.name] = [] + for bone in armature.data.bones: + anim_data.bone_transforms[bone.name] = [] + + for frame in range(21): + frame_time = offset + frame * increment + bpy.context.scene.frame_set(frame_time) + + anim_data.bone_transforms[armature.parent.name].append(ModelTransform()) #for testing + + for bone in armature.pose.bones: + xform = ModelTransform() + xform.translation = convert_vector_space(bone.location) + xform.rotation = convert_rotation_space(bone.rotation_quaternion) + + anim_data.bone_transforms[bone.name].append(xform) + + return [anim_data] + + + + + + diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 589a274..794a7f4 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -2,7 +2,7 @@ saved to a .msh file. """ from dataclasses import dataclass, field -from typing import List +from typing import List, Dict from enum import Enum from mathutils import Vector, Quaternion @@ -65,3 +65,12 @@ class Model: geometry: List[GeometrySegment] = None collisionprimitive: CollisionPrimitive = None + +@dataclass +class Animation: + """ Class representing 'CYCL' + 'KFR3' sections in a .msh file """ + + name: str = "open" + anim_type: str = "HardSkinned" + bone_transforms: Dict[str, List[ModelTransform]] = field(default_factory=dict) + diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index a55a5ef..567aa2f 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -10,7 +10,7 @@ from .msh_model import * from .msh_model_utilities import * from .msh_utilities import * -SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"} +SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE", "ARMATURE"} MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"} MAX_MSH_VERTEX_COUNT = 32767 @@ -44,7 +44,10 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: model.transform.translation = convert_vector_space(local_translation) if obj.parent is not None: - model.parent = obj.parent.name + if obj.parent.type == "Armature": + model.parent = obj.parent.parent.name + else: + model.parent = obj.parent.name if obj.type in MESH_OBJECT_TYPES: mesh = obj.to_mesh() diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index 6c6f7fd..05debe8 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -6,7 +6,7 @@ from typing import List, Dict from copy import copy import bpy from mathutils import Vector -from .msh_model import Model +from .msh_model import Model, Animation 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 from .msh_model_triangle_strips import create_models_triangle_strips @@ -14,6 +14,7 @@ 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 * @dataclass class SceneAABB: @@ -43,6 +44,7 @@ class Scene: name: str = "Scene" materials: Dict[str, Material] = field(default_factory=dict) models: List[Model] = field(default_factory=list) + anims: List[Animation] = field(default_factory=list) def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str) -> Scene: """ Create a msh Scene from the active Blender scene. """ @@ -69,6 +71,8 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t scene.materials = remove_unused_materials(scene.materials, scene.models) + scene.anims = gather_animdata(bpy.context.scene.objects["Armature"]) + return scene def create_scene_aabb(scene: Scene) -> SceneAABB: diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 71914a0..b851713 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -8,6 +8,8 @@ from .msh_material import * from .msh_writer import Writer from .msh_utilities import * +from .crc import * + def save_scene(output_file, scene: Scene): """ Saves scene to the supplied file. """ @@ -26,6 +28,10 @@ def save_scene(output_file, scene: Scene): with msh2.create_child("MODL") as modl: _write_modl(modl, model, index, material_index) + with hedr.create_child("ANM2") as anm2: #simple for now + for anim in scene.anims: + _write_anm2(anm2, anim) + with hedr.create_child("CL1L"): pass @@ -34,8 +40,8 @@ def _write_sinf(sinf: Writer, scene: Scene): name.write_string(scene.name) with sinf.create_child("FRAM") as fram: - fram.write_i32(0, 1) - fram.write_f32(29.97003) + fram.write_i32(0, 20) #test values + fram.write_f32(10.0) #test values with sinf.create_child("BBOX") as bbox: aabb = create_scene_aabb(scene) @@ -189,3 +195,50 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for index in islice(strip, 2, len(strip)): strp.write_u16(index) + + +def _write_anm2(anm2: Writer, anim: Animation): + + with anm2.create_child("CYCL") as cycl: + + cycl.write_u32(1) + cycl.write_string(anim.name) + + for _ in range(64 - (len(anim.name) + 1)): + cycl.write_u8(0) + + cycl.write_f32(10.0) #test framerate + cycl.write_u32(0) #what does play style refer to? + cycl.write_u32(0, 20) #first frame indices + + + with anm2.create_child("KFR3") as kfr3: + + kfr3.write_u32(len(anim.bone_transforms.keys())) + + for boneName in anim.bone_transforms.keys(): + kfr3.write_u32(crc(boneName)) + kfr3.write_u32(0) #what is keyframe type? + + kfr3.write_u32(21, 21) #basic testing + + print(boneName) + + for i, xform in enumerate(anim.bone_transforms[boneName]): + kfr3.write_u32(i) + kfr3.write_f32(xform.translation.x, xform.translation.y, xform.translation.z) + + print(xform.translation) + + for i, xform in enumerate(anim.bone_transforms[boneName]): + kfr3.write_u32(i) + kfr3.write_f32(xform.rotation.x, xform.rotation.y, xform.rotation.z, xform.rotation.w) + + + + + + + + + From 8dea6fac49286f3a290707830f313677eb4b89a0 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Sun, 11 Oct 2020 20:52:34 -0400 Subject: [PATCH 03/39] Hardcoded open override exporting, coord sys conversions need adjustment --- addons/io_scene_swbf_msh/msh_anim_gather.py | 13 +++++++++++-- addons/io_scene_swbf_msh/msh_model.py | 3 ++- addons/io_scene_swbf_msh/msh_model_gather.py | 3 ++- addons/io_scene_swbf_msh/msh_scene_save.py | 4 ---- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 0b97e97..e525042 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -34,11 +34,20 @@ def gather_animdata(armature: bpy.types.Armature) -> List[Animation]: for bone in armature.pose.bones: xform = ModelTransform() - xform.translation = convert_vector_space(bone.location) + + + vt = convert_vector_space(bone.location); + + xform.translation = Vector((vt.x * -1.0, vt.y, vt.z)) xform.rotation = convert_rotation_space(bone.rotation_quaternion) + + ''' + xform.translation = bone.location + xform.rotation = bone.rotation_quaternion anim_data.bone_transforms[bone.name].append(xform) - + ''' + return [anim_data] diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 794a7f4..a855c0c 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -2,7 +2,7 @@ saved to a .msh file. """ from dataclasses import dataclass, field -from typing import List, Dict +from typing import List, Dict, Tuple from enum import Enum from mathutils import Vector, Quaternion @@ -34,6 +34,7 @@ class GeometrySegment: material_name: str = "" positions: List[Vector] = field(default_factory=list) + weights: List[Tuple[int, float]] = field(default_factory=List) normals: List[Vector] = field(default_factory=list) colors: List[List[float]] = None texcoords: List[Vector] = field(default_factory=list) diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 567aa2f..8933304 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -44,7 +44,7 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: model.transform.translation = convert_vector_space(local_translation) if obj.parent is not None: - if obj.parent.type == "Armature": + if obj.parent.type == "ARMATURE": model.parent = obj.parent.parent.name else: model.parent = obj.parent.name @@ -167,6 +167,7 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: return new_index + for tri in mesh.loop_triangles: polygons[tri.material_index].add(tri.polygon_index) segments[tri.material_index].triangles.append([ diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index b851713..afc5af4 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -222,14 +222,10 @@ def _write_anm2(anm2: Writer, anim: Animation): kfr3.write_u32(21, 21) #basic testing - print(boneName) - for i, xform in enumerate(anim.bone_transforms[boneName]): kfr3.write_u32(i) kfr3.write_f32(xform.translation.x, xform.translation.y, xform.translation.z) - print(xform.translation) - for i, xform in enumerate(anim.bone_transforms[boneName]): kfr3.write_u32(i) kfr3.write_f32(xform.rotation.x, xform.rotation.y, xform.rotation.z, xform.rotation.w) From 2599a7203e92bab74f41998a63bca9cbf0f6639e Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Mon, 12 Oct 2020 09:45:21 -0400 Subject: [PATCH 04/39] Weighting start --- addons/io_scene_swbf_msh/msh_model.py | 2 +- addons/io_scene_swbf_msh/msh_model_gather.py | 30 ++++++++++++++++++-- addons/io_scene_swbf_msh/msh_scene_save.py | 3 ++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index a855c0c..6e0e13d 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -34,7 +34,7 @@ class GeometrySegment: material_name: str = "" positions: List[Vector] = field(default_factory=list) - weights: List[Tuple[int, float]] = field(default_factory=List) + weights: List[Tuple[int, float]] = None normals: List[Vector] = field(default_factory=list) colors: List[List[float]] = None texcoords: List[Vector] = field(default_factory=list) diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 8933304..b2656c3 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -22,6 +22,11 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: parents = create_parents_set() models_list: List[Model] = [] + model_indices: Dict[str, int] = {} + + for i, uneval_obj in enumerate(select_objects(export_target)): + if not (uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents): + model_indices[uneval_obj.name] = i for uneval_obj in select_objects(export_target): if uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents: @@ -49,9 +54,15 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: else: model.parent = obj.parent.name + if obj.type in MESH_OBJECT_TYPES: mesh = obj.to_mesh() - model.geometry = create_mesh_geometry(mesh) + + vgroups_to_indices = {} + for i, vgroup in enumerate(obj.vertex_groups): + vgroups_to_indices[i] = model_indices[vgroup.name] + + model.geometry = create_mesh_geometry(mesh, vgroups_to_indices) obj.to_mesh_clear() _, _, world_scale = obj.matrix_world.decompose() @@ -83,7 +94,7 @@ def create_parents_set() -> Set[str]: return parents -def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: +def create_mesh_geometry(mesh: bpy.types.Mesh, vgrps_to_indices : Dict[int, int]) -> List[GeometrySegment]: """ Creates a list of GeometrySegment objects from a Blender mesh. Does NOT create triangle strips in the GeometrySegment however. """ @@ -104,6 +115,10 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: for segment in segments: segment.colors = [] + if vgrps_to_indices: + for segment in segments: + segment.weights = [] + for segment, material in zip(segments, mesh.materials): segment.material_name = material.name @@ -157,6 +172,14 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: segment.positions.append(convert_vector_space(mesh.vertices[vertex_index].co)) segment.normals.append(convert_vector_space(vertex_normal)) + if vgrps_to_indices is not None: + for i,grp_el in mesh.vertices[vertex_index].groups: + segment.weights.append(tuple(vgrps_to_indices[grp_el.group], grp_el.weight)) + if i > 3: + break + while (i < 3): + segment.weights.append(tuple(0,0.0)) + if mesh.uv_layers.active is None: segment.texcoords.append(Vector((0.0, 0.0))) else: @@ -185,7 +208,8 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: def get_model_type(obj: bpy.types.Object) -> ModelType: """ Get the ModelType for a Blender object. """ - # TODO: Skinning support, etc + if obj.parent_type == "ARMATURE": + return ModelType.SKIN if obj.type in MESH_OBJECT_TYPES: return ModelType.STATIC diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index afc5af4..dfdec59 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -129,6 +129,9 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str for segment in model.geometry: with geom.create_child("SEGM") as segm: _write_segm(segm, segment, material_index) + if model.type = ModelType.SKIN: + with modl.create_child("ENVL") as envl: + if model.collisionprimitive is not None: with modl.create_child("SWCI") as swci: From 1892a1cdbd3818a5b897a72048e17f30d193011c Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Wed, 14 Oct 2020 16:06:04 -0400 Subject: [PATCH 05/39] Minor envelope progress --- addons/io_scene_swbf_msh/msh_anim_gather.py | 8 +- addons/io_scene_swbf_msh/msh_model.py | 7 +- addons/io_scene_swbf_msh/msh_model_gather.py | 78 +++++++++++++------- addons/io_scene_swbf_msh/msh_scene.py | 17 ++++- addons/io_scene_swbf_msh/msh_scene_save.py | 28 ++++--- 5 files changed, 93 insertions(+), 45 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index e525042..631839f 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -40,14 +40,14 @@ def gather_animdata(armature: bpy.types.Armature) -> List[Animation]: xform.translation = Vector((vt.x * -1.0, vt.y, vt.z)) xform.rotation = convert_rotation_space(bone.rotation_quaternion) - - ''' + + ''' xform.translation = bone.location xform.rotation = bone.rotation_quaternion anim_data.bone_transforms[bone.name].append(xform) - ''' - + ''' + return [anim_data] diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 6e0e13d..31f04a0 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -34,11 +34,11 @@ class GeometrySegment: material_name: str = "" positions: List[Vector] = field(default_factory=list) - weights: List[Tuple[int, float]] = None normals: List[Vector] = field(default_factory=list) colors: List[List[float]] = None texcoords: List[Vector] = field(default_factory=list) - # TODO: Skin support. + + weights: List[Tuple[int, float]] = None polygons: List[List[int]] = field(default_factory=list) triangles: List[List[int]] = field(default_factory=list) @@ -67,11 +67,12 @@ class Model: geometry: List[GeometrySegment] = None collisionprimitive: CollisionPrimitive = None + vgroups_to_modelnames_map : Dict[int, str] = None + @dataclass class Animation: """ Class representing 'CYCL' + 'KFR3' sections in a .msh file """ name: str = "open" - anim_type: str = "HardSkinned" bone_transforms: Dict[str, List[ModelTransform]] = field(default_factory=dict) diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index b2656c3..7b6e7fd 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -4,7 +4,7 @@ import bpy import math from enum import Enum -from typing import List, Set, Dict, Tuple +from typing import List, Set, Dict, Tuple, Set from itertools import zip_longest from .msh_model import * from .msh_model_utilities import * @@ -22,11 +22,7 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: parents = create_parents_set() models_list: List[Model] = [] - model_indices: Dict[str, int] = {} - - for i, uneval_obj in enumerate(select_objects(export_target)): - if not (uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents): - model_indices[uneval_obj.name] = i + skeleton: bpy.types.Armature = None for uneval_obj in select_objects(export_target): if uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents: @@ -50,19 +46,23 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: if obj.parent is not None: if obj.parent.type == "ARMATURE": - model.parent = obj.parent.parent.name - else: - model.parent = obj.parent.name + skeleton = obj.parent + + parent_bone_name = obj.parent_bone + if parent_bone_name == "": + model.parent = obj.parent.parent + else: + model.parent = parent_bone_name + + if model.model_type == ModelType.SKIN: + model.vgroups_to_modelnames_map = {} + for i, vgroup in enumerate(obj.vertex_groups): + vgroups_to_modelnames_map[i] = vgroup.name if obj.type in MESH_OBJECT_TYPES: - mesh = obj.to_mesh() - - vgroups_to_indices = {} - for i, vgroup in enumerate(obj.vertex_groups): - vgroups_to_indices[i] = model_indices[vgroup.name] - - model.geometry = create_mesh_geometry(mesh, vgroups_to_indices) + mesh = obj.to_mesh() + model.geometry = create_mesh_geometry(mesh, model.model_type == ModelType.SKIN) obj.to_mesh_clear() _, _, world_scale = obj.matrix_world.decompose() @@ -80,6 +80,30 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: models_list.append(model) + + for bone in skeleton.data.bones: + + model = Model() + model.name = bone.name + model.model_type = ModelType.NULL + model.hidden = False + + local_translation, local_rotation, _ = bone.matrix_local.decompose() + model.transform.rotation = convert_rotation_space(local_rotation) + model.transform.translation = convert_vector_space(local_translation) + + parent_name = bone.parent + if parent_name is not None: + model.parent = parent_name + else: + if skeleton.parent is not None: + model.parent = skeleton.parent.name + else: + model.parent = None + + models_list.append(model) + + return models_list def create_parents_set() -> Set[str]: @@ -94,7 +118,7 @@ def create_parents_set() -> Set[str]: return parents -def create_mesh_geometry(mesh: bpy.types.Mesh, vgrps_to_indices : Dict[int, int]) -> List[GeometrySegment]: +def create_mesh_geometry(mesh: bpy.types.Mesh, is_skinned : bool) -> List[GeometrySegment]: """ Creates a list of GeometrySegment objects from a Blender mesh. Does NOT create triangle strips in the GeometrySegment however. """ @@ -115,10 +139,6 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, vgrps_to_indices : Dict[int, int] for segment in segments: segment.colors = [] - if vgrps_to_indices: - for segment in segments: - segment.weights = [] - for segment, material in zip(segments, mesh.materials): segment.material_name = material.name @@ -172,13 +192,15 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, vgrps_to_indices : Dict[int, int] segment.positions.append(convert_vector_space(mesh.vertices[vertex_index].co)) segment.normals.append(convert_vector_space(vertex_normal)) - if vgrps_to_indices is not None: - for i,grp_el in mesh.vertices[vertex_index].groups: - segment.weights.append(tuple(vgrps_to_indices[grp_el.group], grp_el.weight)) - if i > 3: - break - while (i < 3): - segment.weights.append(tuple(0,0.0)) + if is_skinned: + for i,grp_el in mesh.vertices[vertex_index].groups: + segment.weights.append(tuple(grp_el.group, grp_el.weight)) + print("Adding weight to group {grp_el.group} of value {grp_el.weight}") + if i > 3: #will have to look into aramture/skin settings for limiting envolopes to 4 weights... + break + while i < 3: + segment.weights.append(tuple(0,0.0)) + i+=1 if mesh.uv_layers.active is None: segment.texcoords.append(Vector((0.0, 0.0))) diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index 05debe8..7fa5930 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -46,6 +46,7 @@ class Scene: models: List[Model] = field(default_factory=list) anims: List[Animation] = field(default_factory=list) + def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str) -> Scene: """ Create a msh Scene from the active Blender scene. """ @@ -69,9 +70,23 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t if has_multiple_root_models(scene.models): scene.models = reparent_model_roots(scene.models) + + #now that we've collected all models, we should remap WGHT indices... + names_to_indices = {} + for i,model in enumerate(scene.models): + names_to_indices[model.name] = i; + + for model in scene.models: + if model.model_type == ModelType.SKIN: + for segment in model.geometry: + for i in range(len(segment.weights)): + vgroup_index = segment.weights[i][0] + segment.weights[i][0] = names_to_indices[model.vgroups_to_modelnames_map[vgroup_index]] + + scene.materials = remove_unused_materials(scene.materials, scene.models) - scene.anims = gather_animdata(bpy.context.scene.objects["Armature"]) + #scene.anims = gather_animdata(bpy.context.scene.objects["Armature"]) return scene diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index dfdec59..2fd0945 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -26,11 +26,11 @@ def save_scene(output_file, scene: Scene): for index, model in enumerate(scene.models): with msh2.create_child("MODL") as modl: - _write_modl(modl, model, index, material_index) + _write_modl(modl, model, index, material_index, scene) - with hedr.create_child("ANM2") as anm2: #simple for now - for anim in scene.anims: - _write_anm2(anm2, anim) + #with hedr.create_child("ANM2") as anm2: #simple for now + # for anim in scene.anims: + # _write_anm2(anm2, anim) with hedr.create_child("CL1L"): pass @@ -103,7 +103,7 @@ def _write_matd(matd: Writer, material_name: str, material: Material): with matd.create_child("TX3D") as tx3d: tx3d.write_string(material.texture3) -def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str, int]): +def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str, int], scene: Scene): with modl.create_child("MTYP") as mtyp: mtyp.write_u32(model.model_type.value) @@ -129,9 +129,12 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str for segment in model.geometry: with geom.create_child("SEGM") as segm: _write_segm(segm, segment, material_index) - if model.type = ModelType.SKIN: - with modl.create_child("ENVL") as envl: - + + if model.model_type == ModelType.SKIN: + with modl.create_child("ENVL") as envl: + envl.write_u32(len(scene.models)) + for i in range(len(scene.models)): + envl.write_u32(i) if model.collisionprimitive is not None: with modl.create_child("SWCI") as swci: @@ -162,6 +165,13 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for normal in segment.normals: nrml.write_f32(normal.x, normal.y, normal.z) + if segment.weights is not None: + with segm.create_child("WGHT") as wght: + wght.write_u32(len(segment.weights) / 4) + for weight in segment.weights: + wght.write_u32(weight[0]) + wght.write_f32(weight[1]) + if segment.colors is not None: with segm.create_child("CLRL") as clrl: clrl.write_u32(len(segment.colors)) @@ -207,7 +217,7 @@ def _write_anm2(anm2: Writer, anim: Animation): cycl.write_u32(1) cycl.write_string(anim.name) - for _ in range(64 - (len(anim.name) + 1)): + for _ in range(63 - len(anim.name)): cycl.write_u8(0) cycl.write_f32(10.0) #test framerate From f426237785722a71a7b0ea36ba847a9f4706eafd Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Thu, 15 Oct 2020 14:44:17 -0500 Subject: [PATCH 06/39] Rotation fix (I think) --- addons/io_scene_swbf_msh/msh_anim_gather.py | 19 ++++++++----------- addons/io_scene_swbf_msh/msh_model_gather.py | 18 +++++++++++++++--- addons/io_scene_swbf_msh/msh_scene.py | 4 +++- addons/io_scene_swbf_msh/msh_scene_save.py | 13 ++++++++----- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 631839f..86de953 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -33,20 +33,17 @@ def gather_animdata(armature: bpy.types.Armature) -> List[Animation]: anim_data.bone_transforms[armature.parent.name].append(ModelTransform()) #for testing for bone in armature.pose.bones: + xform = ModelTransform() - - - vt = convert_vector_space(bone.location); - - xform.translation = Vector((vt.x * -1.0, vt.y, vt.z)) + xform.translation = convert_vector_space(bone.location) + xform.translation.x *= -1.0 xform.rotation = convert_rotation_space(bone.rotation_quaternion) - - ''' - xform.translation = bone.location - xform.rotation = bone.rotation_quaternion - + xform.rotation.x *= -1.0 + xform.rotation.y *= -1.0 + xform.rotation.z *= -1.0 + anim_data.bone_transforms[bone.name].append(xform) - ''' + return [anim_data] diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 7b6e7fd..dd952f4 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -33,6 +33,9 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: else: obj = uneval_obj + if uneval_obj.type == "ARMATURE": + continue + check_for_bad_lod_suffix(obj) local_translation, local_rotation, _ = obj.matrix_local.decompose() @@ -44,8 +47,13 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: model.transform.rotation = convert_rotation_space(local_rotation) model.transform.translation = convert_vector_space(local_translation) + print("Adding model: " + model.name) + if obj.parent is not None: if obj.parent.type == "ARMATURE": + + model.parent = "DummyRoot" + skeleton = obj.parent parent_bone_name = obj.parent_bone @@ -80,18 +88,20 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: models_list.append(model) - + for bone in skeleton.data.bones: model = Model() model.name = bone.name - model.model_type = ModelType.NULL + model.model_type = ModelType.BONE model.hidden = False local_translation, local_rotation, _ = bone.matrix_local.decompose() model.transform.rotation = convert_rotation_space(local_rotation) model.transform.translation = convert_vector_space(local_translation) + print("Adding bone: " + model.name) + parent_name = bone.parent if parent_name is not None: model.parent = parent_name @@ -102,6 +112,8 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: model.parent = None models_list.append(model) + + return models_list @@ -231,7 +243,7 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, is_skinned : bool) -> List[Geomet def get_model_type(obj: bpy.types.Object) -> ModelType: """ Get the ModelType for a Blender object. """ if obj.parent_type == "ARMATURE": - return ModelType.SKIN + return ModelType.SKIN if obj.type in MESH_OBJECT_TYPES: return ModelType.STATIC diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index 7fa5930..f19afa2 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -72,6 +72,7 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t #now that we've collected all models, we should remap WGHT indices... + ''' names_to_indices = {} for i,model in enumerate(scene.models): names_to_indices[model.name] = i; @@ -82,11 +83,12 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t for i in range(len(segment.weights)): vgroup_index = segment.weights[i][0] segment.weights[i][0] = names_to_indices[model.vgroups_to_modelnames_map[vgroup_index]] + ''' scene.materials = remove_unused_materials(scene.materials, scene.models) - #scene.anims = gather_animdata(bpy.context.scene.objects["Armature"]) + scene.anims = gather_animdata(bpy.context.scene.objects["Armature"]) return scene diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 2fd0945..78c786c 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -28,9 +28,9 @@ def save_scene(output_file, scene: Scene): with msh2.create_child("MODL") as modl: _write_modl(modl, model, index, material_index, scene) - #with hedr.create_child("ANM2") as anm2: #simple for now - # for anim in scene.anims: - # _write_anm2(anm2, anim) + with hedr.create_child("ANM2") as anm2: #simple for now + for anim in scene.anims: + _write_anm2(anm2, anim) with hedr.create_child("CL1L"): pass @@ -129,12 +129,13 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str for segment in model.geometry: with geom.create_child("SEGM") as segm: _write_segm(segm, segment, material_index) - + ''' if model.model_type == ModelType.SKIN: with modl.create_child("ENVL") as envl: envl.write_u32(len(scene.models)) for i in range(len(scene.models)): envl.write_u32(i) + ''' if model.collisionprimitive is not None: with modl.create_child("SWCI") as swci: @@ -165,12 +166,14 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for normal in segment.normals: nrml.write_f32(normal.x, normal.y, normal.z) + ''' if segment.weights is not None: with segm.create_child("WGHT") as wght: wght.write_u32(len(segment.weights) / 4) for weight in segment.weights: wght.write_u32(weight[0]) wght.write_f32(weight[1]) + ''' if segment.colors is not None: with segm.create_child("CLRL") as clrl: @@ -217,7 +220,7 @@ def _write_anm2(anm2: Writer, anim: Animation): cycl.write_u32(1) cycl.write_string(anim.name) - for _ in range(63 - len(anim.name)): + for _ in range(64 - (len(anim.name) + 1)): cycl.write_u8(0) cycl.write_f32(10.0) #test framerate From dac3ade7a4bae013afc25cd26a7bc3e474d6a349 Mon Sep 17 00:00:00 2001 From: SleepKiller Date: Fri, 16 Oct 2020 13:40:27 +1300 Subject: [PATCH 07/39] initial vertex weights implementation --- addons/io_scene_swbf_msh/msh_model.py | 13 ++++- addons/io_scene_swbf_msh/msh_model_gather.py | 57 ++++++++++++++++++-- addons/io_scene_swbf_msh/msh_scene_save.py | 33 +++++++++++- 3 files changed, 94 insertions(+), 9 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 589a274..b92fe76 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -27,17 +27,24 @@ class ModelTransform: translation: Vector = field(default_factory=Vector) rotation: Quaternion = field(default_factory=Quaternion) +@dataclass +class VertexWeight: + """ Class representing a vertex weight in a .msh file. """ + + weight: float = 1.0 + bone: int = 0 + @dataclass class GeometrySegment: """ Class representing a 'SEGM' section in a .msh file. """ - material_name: str = "" + material_name: str = field(default_factory=str) positions: List[Vector] = field(default_factory=list) normals: List[Vector] = field(default_factory=list) colors: List[List[float]] = None texcoords: List[Vector] = field(default_factory=list) - # TODO: Skin support. + weights: List[List[VertexWeight]] = None polygons: List[List[int]] = field(default_factory=list) triangles: List[List[int]] = field(default_factory=list) @@ -63,5 +70,7 @@ class Model: transform: ModelTransform = field(default_factory=ModelTransform) + bone_map: List[str] = None + geometry: List[GeometrySegment] = None collisionprimitive: CollisionPrimitive = None diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index a55a5ef..946303b 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -34,6 +34,9 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: check_for_bad_lod_suffix(obj) + if obj.type == "ARMATURE": + models_list += expand_armature(obj) + local_translation, local_rotation, _ = obj.matrix_local.decompose() model = Model() @@ -48,7 +51,7 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: if obj.type in MESH_OBJECT_TYPES: mesh = obj.to_mesh() - model.geometry = create_mesh_geometry(mesh) + model.geometry = create_mesh_geometry(mesh, obj.vertex_groups) obj.to_mesh_clear() _, _, world_scale = obj.matrix_world.decompose() @@ -64,6 +67,9 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: if get_is_collision_primitive(obj): model.collisionprimitive = get_collision_primitive(obj) + if obj.vertex_groups: + model.bone_map = [group.name for group in obj.vertex_groups] + models_list.append(model) return models_list @@ -80,7 +86,7 @@ def create_parents_set() -> Set[str]: return parents -def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: +def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[GeometrySegment]: """ Creates a list of GeometrySegment objects from a Blender mesh. Does NOT create triangle strips in the GeometrySegment however. """ @@ -93,7 +99,7 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: material_count = max(len(mesh.materials), 1) segments: List[GeometrySegment] = [GeometrySegment() for i in range(material_count)] - vertex_cache: List[Dict[Tuple[float], int]] = [dict() for i in range(material_count)] + vertex_cache = [dict() for i in range(material_count)] vertex_remap: List[Dict[Tuple[int, int], int]] = [dict() for i in range(material_count)] polygons: List[Set[int]] = [set() for i in range(material_count)] @@ -101,6 +107,10 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: for segment in segments: segment.colors = [] + if has_weights: + for segment in segments: + segment.weights = [] + for segment, material in zip(segments, mesh.materials): segment.material_name = material.name @@ -139,6 +149,11 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: for v in mesh.vertex_colors.active.data[loop_index].color: yield v + if segment.weights is not None: + for v in mesh.vertices[vertex_index].groups: + yield v.group + yield v.weight + vertex_cache_entry = tuple(get_cache_vertex()) cached_vertex_index = cache.get(vertex_cache_entry, vertex_cache_miss_index) @@ -162,6 +177,11 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: if segment.colors is not None: segment.colors.append(list(mesh.vertex_colors.active.data[loop_index].color)) + if segment.weights is not None: + groups = mesh.vertices[vertex_index].groups + + segment.weights.append([VertexWeight(v.weight, v.group) for v in groups]) + return new_index for tri in mesh.loop_triangles: @@ -181,10 +201,12 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: def get_model_type(obj: bpy.types.Object) -> ModelType: """ Get the ModelType for a Blender object. """ - # TODO: Skinning support, etc if obj.type in MESH_OBJECT_TYPES: - return ModelType.STATIC + if obj.vertex_groups: + return ModelType.SKIN + else: + return ModelType.STATIC return ModelType.NULL @@ -320,6 +342,31 @@ def select_objects(export_target: str) -> List[bpy.types.Object]: return objects + parents +def expand_armature(obj: bpy.types.Object) -> List[Model]: + bones: List[Model] = [] + + for bone in obj.data.bones: + model = Model() + + transform = bone.matrix_local + + if bone.parent: + transform = bone.parent.matrix_local.inverted() @ transform + model.parent = bone.parent.name + else: + model.parent = obj.name + + local_translation, local_rotation, _ = transform.decompose() + + model.model_type = ModelType.BONE + model.name = bone.name + model.transform.rotation = convert_rotation_space(local_rotation) + model.transform.translation = convert_vector_space(local_translation) + + bones.append(model) + + return bones + def convert_vector_space(vec: Vector) -> Vector: return Vector((-vec.x, vec.z, vec.y)) diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 71914a0..b70f766 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -17,6 +17,7 @@ def save_scene(output_file, scene: Scene): with msh2.create_child("SINF") as sinf: _write_sinf(sinf, scene) + model_index: Dict[str, int] = {model.name:i for i, model in enumerate(scene.models)} material_index: Dict[str, int] = {} with msh2.create_child("MATL") as matl: @@ -24,7 +25,7 @@ def save_scene(output_file, scene: Scene): for index, model in enumerate(scene.models): with msh2.create_child("MODL") as modl: - _write_modl(modl, model, index, material_index) + _write_modl(modl, model, index, material_index, model_index) with hedr.create_child("CL1L"): pass @@ -97,7 +98,7 @@ def _write_matd(matd: Writer, material_name: str, material: Material): with matd.create_child("TX3D") as tx3d: tx3d.write_string(material.texture3) -def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str, int]): +def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str, int], model_index: Dict[str, int]): with modl.create_child("MTYP") as mtyp: mtyp.write_u32(model.model_type.value) @@ -124,6 +125,10 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str with geom.create_child("SEGM") as segm: _write_segm(segm, segment, material_index) + if model.bone_map: + with geom.create_child("ENVL") as envl: + _write_envl(envl, model, model_index) + if model.collisionprimitive is not None: with modl.create_child("SWCI") as swci: swci.write_u32(model.collisionprimitive.shape.value) @@ -147,6 +152,10 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for position in segment.positions: posl.write_f32(position.x, position.y, position.z) + if segment.weights: + with segm.create_child("WGHT") as wght: + _write_wght(wght, segment.weights) + with segm.create_child("NRML") as nrml: nrml.write_u32(len(segment.normals)) @@ -189,3 +198,23 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for index in islice(strip, 2, len(strip)): strp.write_u16(index) + +def _write_wght(wght: Writer, weights: List[List[VertexWeight]]): + wght.write_u32(len(weights)) + + for weight_list in weights: + weight_list += [VertexWeight(0.0, 0)] * 4 + weight_list = sorted(weight_list, key=lambda w: w.weight, reverse=True) + weight_list = weight_list[:4] + + total_weight = max(sum(map(lambda w: w.weight, weight_list)), 1e-5) + + for weight in weight_list: + wght.write_i32(weight.bone) + wght.write_f32(weight.weight / total_weight) + +def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]): + envl.write_u32(len(model.bone_map)) + + for bone_name in model.bone_map: + envl.write_u32(model_index[bone_name]) From ff3a517312029c03bcc3e292b1739f7ee3695417 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Fri, 16 Oct 2020 13:56:03 -0500 Subject: [PATCH 08/39] Menu anim export option, minor reorg --- addons/io_scene_swbf_msh/__init__.py | 12 ++++- addons/io_scene_swbf_msh/msh_anim_gather.py | 34 +++++++++--- addons/io_scene_swbf_msh/msh_model.py | 2 +- addons/io_scene_swbf_msh/msh_model_gather.py | 13 ++--- .../io_scene_swbf_msh/msh_model_utilities.py | 23 ++++++++ addons/io_scene_swbf_msh/msh_scene_save.py | 54 +++++++++++-------- 6 files changed, 98 insertions(+), 40 deletions(-) diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index 200b48f..2d25e73 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -97,14 +97,24 @@ class ExportMSH(Operator, ExportHelper): default=True ) + export_anims: BoolProperty( + name="Export Animations", + description="Action export test", + default=False + ) + + def execute(self, context): + with open(self.filepath, 'wb') as output_file: save_scene( output_file=output_file, scene=create_scene( generate_triangle_strips=self.generate_triangle_strips, apply_modifiers=self.apply_modifiers, - export_target=self.export_target)) + export_target=self.export_target), + separate_anims=self.export_anims + ) return {'FINISHED'} diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 86de953..bf7ef8c 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -35,12 +35,8 @@ def gather_animdata(armature: bpy.types.Armature) -> List[Animation]: for bone in armature.pose.bones: xform = ModelTransform() - xform.translation = convert_vector_space(bone.location) - xform.translation.x *= -1.0 - xform.rotation = convert_rotation_space(bone.rotation_quaternion) - xform.rotation.x *= -1.0 - xform.rotation.y *= -1.0 - xform.rotation.z *= -1.0 + xform.translation = to_skeleton_vector_space(bone.location) + xform.rotation = to_skeleton_rotation_space(bone.rotation_quaternion) anim_data.bone_transforms[bone.name].append(xform) @@ -48,6 +44,32 @@ def gather_animdata(armature: bpy.types.Armature) -> List[Animation]: return [anim_data] +def get_basepose(armature: bpy.types.Armature) -> Animation: + + anim_data = Animation(); + anim_data.name = "basepose" + + bpy.context.scene.frame_set(0.0) + + anim_data.bone_transforms[armature.parent.name] = [] + for bone in armature.data.bones: + anim_data.bone_transforms[bone.name] = [] + + for frame in range(2): + + anim_data.bone_transforms[armature.parent.name].append(ModelTransform()) #for testing + + for bone in armature.pose.bones: + + xform = ModelTransform() + xform.translation = to_skeleton_vector_space(bone.location) + xform.rotation = to_skeleton_rotation_space(bone.rotation_quaternion) + + anim_data.bone_transforms[bone.name].append(xform) + + + return anim_data + diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index cdcc2e4..0af2932 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -80,6 +80,6 @@ class Model: class Animation: """ Class representing 'CYCL' + 'KFR3' sections in a .msh file """ - name: str = "open" + name: str = "wiggle" bone_transforms: Dict[str, List[ModelTransform]] = field(default_factory=dict) diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 2119dbc..c189e5d 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -36,6 +36,7 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: if obj.type == "ARMATURE": models_list += expand_armature(obj) + continue local_translation, local_rotation, _ = obj.matrix_local.decompose() @@ -47,6 +48,7 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: model.transform.translation = convert_vector_space(local_translation) if obj.type in MESH_OBJECT_TYPES: + mesh = obj.to_mesh() model.geometry = create_mesh_geometry(mesh, obj.vertex_groups) obj.to_mesh_clear() @@ -353,7 +355,7 @@ def expand_armature(obj: bpy.types.Object) -> List[Model]: transform = bone.parent.matrix_local.inverted() @ transform model.parent = bone.parent.name else: - model.parent = obj.name + model.parent = "DummyRoot" # obj.name local_translation, local_rotation, _ = transform.decompose() @@ -365,12 +367,3 @@ def expand_armature(obj: bpy.types.Object) -> List[Model]: bones.append(model) return bones - -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)) diff --git a/addons/io_scene_swbf_msh/msh_model_utilities.py b/addons/io_scene_swbf_msh/msh_model_utilities.py index 1547697..e33e9f4 100644 --- a/addons/io_scene_swbf_msh/msh_model_utilities.py +++ b/addons/io_scene_swbf_msh/msh_model_utilities.py @@ -114,3 +114,26 @@ def is_model_name_unused(name: str, models: List[Model]) -> bool: return False 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)) + + +def to_skeleton_vector_space(vec : Vector): + vnew = convert_vector_space(vec) + vnew.x *= -1.0 + return vnew + +def to_skeleton_rotation_space(quat : Quaternion): + qnew = convert_rotation_space(quat) + qnew.x *= -1.0 + qnew.y *= -1.0 + qnew.z *= -1.0 + return qnew diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index a049bf1..dc3a2ae 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -10,7 +10,7 @@ from .msh_utilities import * from .crc import * -def save_scene(output_file, scene: Scene): +def save_scene(output_file, scene: Scene, separate_anims: bool): """ Saves scene to the supplied file. """ with Writer(file=output_file, chunk_id="HEDR") as hedr: @@ -26,12 +26,15 @@ def save_scene(output_file, scene: Scene): material_index = _write_matl_and_get_material_index(matl, scene) for index, model in enumerate(scene.models): + if separate_anims and (model.model_type not in {ModelType.NULL, ModelType.BONE}): + continue with msh2.create_child("MODL") as modl: _write_modl(modl, model, index, material_index, model_index) - with hedr.create_child("ANM2") as anm2: #simple for now - for anim in scene.anims: - _write_anm2(anm2, anim) + if separate_anims: + with hedr.create_child("ANM2") as anm2: #simple for now + for anim in scene.anims: + _write_anm2(anm2, anim) with hedr.create_child("CL1L"): pass @@ -205,7 +208,32 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for index in islice(strip, 2, len(strip)): strp.write_u16(index) +''' +SKINNING CHUNKS +''' +def _write_wght(wght: Writer, weights: List[List[VertexWeight]]): + wght.write_u32(len(weights)) + for weight_list in weights: + weight_list += [VertexWeight(0.0, 0)] * 4 + weight_list = sorted(weight_list, key=lambda w: w.weight, reverse=True) + weight_list = weight_list[:4] + + total_weight = max(sum(map(lambda w: w.weight, weight_list)), 1e-5) + + for weight in weight_list: + wght.write_i32(weight.bone) + wght.write_f32(weight.weight / total_weight) + +def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]): + envl.write_u32(len(model.bone_map)) + + for bone_name in model.bone_map: + envl.write_u32(model_index[bone_name]) + +''' +ANIMATION CHUNKS +''' def _write_anm2(anm2: Writer, anim: Animation): with anm2.create_child("CYCL") as cycl: @@ -240,22 +268,4 @@ def _write_anm2(anm2: Writer, anim: Animation): kfr3.write_f32(xform.rotation.x, xform.rotation.y, xform.rotation.z, xform.rotation.w) -def _write_wght(wght: Writer, weights: List[List[VertexWeight]]): - wght.write_u32(len(weights)) - for weight_list in weights: - weight_list += [VertexWeight(0.0, 0)] * 4 - weight_list = sorted(weight_list, key=lambda w: w.weight, reverse=True) - weight_list = weight_list[:4] - - total_weight = max(sum(map(lambda w: w.weight, weight_list)), 1e-5) - - for weight in weight_list: - wght.write_i32(weight.bone) - wght.write_f32(weight.weight / total_weight) - -def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]): - envl.write_u32(len(model.bone_map)) - - for bone_name in model.bone_map: - envl.write_u32(model_index[bone_name]) From 152b22feb942e3b2736a88d29598f4ae354b5c26 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Tue, 20 Oct 2020 14:28:53 -0400 Subject: [PATCH 09/39] MSH scene reading draft, no blender conversion yet --- addons/io_scene_swbf_msh/__init__.py | 37 ++- addons/io_scene_swbf_msh/msh_reader.py | 118 ++++++++ addons/io_scene_swbf_msh/msh_scene_read.py | 308 +++++++++++++++++++++ addons/io_scene_swbf_msh/msh_utilities.py | 12 + 4 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 addons/io_scene_swbf_msh/msh_reader.py create mode 100644 addons/io_scene_swbf_msh/msh_scene_read.py diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index 200b48f..6165685 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -53,11 +53,12 @@ if "bpy" in locals(): # End of stuff taken from glTF import bpy -from bpy_extras.io_utils import ExportHelper +from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy.props import BoolProperty, EnumProperty from bpy.types import Operator from .msh_scene import create_scene from .msh_scene_save import save_scene +from .msh_scene_read import read_scene from .msh_material_properties import * class ExportMSH(Operator, ExportHelper): @@ -108,16 +109,47 @@ class ExportMSH(Operator, ExportHelper): return {'FINISHED'} + # Only needed if you want to add into a dynamic menu def menu_func_export(self, context): self.layout.operator(ExportMSH.bl_idname, text="SWBF msh (.msh)") + + + + +class ImportMSH(Operator, ImportHelper): + """ Import an SWBF .msh file. """ + + bl_idname = "swbf_msh.import" + bl_label = "Import SWBF .msh File" + filename_ext = ".msh" + + filter_glob: StringProperty( + default="*.msh", + options={'HIDDEN'}, + maxlen=255, # Max internal buffer length, longer would be clamped. + ) + + def execute(self, context): + with open(self.filepath, 'rb') as input_file: + read_scene(input_file) + return {'FINISHED'} + +def menu_func_import(self, context): + self.layout.operator(ImportMSH.bl_idname, text="SWBF msh (.msh)") + + + + def register(): bpy.utils.register_class(MaterialProperties) bpy.utils.register_class(MaterialPropertiesPanel) bpy.utils.register_class(ExportMSH) + bpy.utils.register_class(ImportMSH) bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.Material.swbf_msh = bpy.props.PointerProperty(type=MaterialProperties) @@ -125,7 +157,10 @@ def unregister(): bpy.utils.unregister_class(MaterialProperties) bpy.utils.unregister_class(MaterialPropertiesPanel) bpy.utils.unregister_class(ExportMSH) + bpy.utils.unregister_class(ImportMSH) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + if __name__ == "__main__": register() diff --git a/addons/io_scene_swbf_msh/msh_reader.py b/addons/io_scene_swbf_msh/msh_reader.py new file mode 100644 index 0000000..c652683 --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_reader.py @@ -0,0 +1,118 @@ + +import io +import struct + +class Reader: + def __init__(self, file, chunk_id: str, parent=None, indent=0): + self.file = file + self.size: int = 0 + self.size_pos = None + self.parent = parent + self.header = chunk_id + self.indent = " " * indent + + + def __enter__(self): + self.size_pos = self.file.tell() + self.file.seek(4,1) #skip header, will add check later + self.size = self.read_u32() + + padding_length = 4 - (self.size % 4) if self.size % 4 > 0 else 0 + self.end_pos = self.size_pos + padding_length + self.size + 8 + + print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) + + return self + + + def __exit__(self, exc_type, exc_value, traceback): + if self.size > self.MAX_SIZE: + raise OverflowError(f".msh file overflowed max size. size = {self.size} MAX_SIZE = {self.MAX_SIZE}") + + print(self.indent + "End " + self.header) + self.file.seek(self.end_pos) + + + + def read_bytes(self,num_bytes): + return self.file.read(num_bytes) + + + def read_string(self): + last_byte = self.read_bytes(1) + result = b'' + while last_byte[0] != 0x0: + result += last_byte + last_byte = self.read_bytes(1) + + return result.decode("utf-8") + + def read_i8(self, num=1): + buf = self.read_bytes(num) + result = struct.unpack(f"<{num}b", buf) + return result[0] if num == 1 else result + + def read_u8(self, num=1): + buf = self.read_bytes(num) + result = struct.unpack(f"<{num}B", buf) + return result[0] if num == 1 else result + + def read_i16(self, num=1): + buf = self.read_bytes(num * 2) + result = struct.unpack(f"<{num}h", buf) + return result[0] if num == 1 else result + + def read_u16(self, num=1): + buf = self.read_bytes(num * 2) + result = struct.unpack(f"<{num}H", buf) + return result[0] if num == 1 else result + + def read_i32(self, num=1): + buf = self.read_bytes(num * 4) + result = struct.unpack(f"<{num}i", buf) + return result[0] if num == 1 else result + + def read_u32(self, num=1): + buf = self.read_bytes(num * 4) + result = struct.unpack(f"<{num}I", buf) + return result[0] if num == 1 else result + + def read_f32(self, num=1): + buf = self.read_bytes(num * 4) + result = struct.unpack(f"<{num}f", buf) + return result[0] if num == 1 else result + + + + def read_child(self, child_id: str): + child = Reader(self.file, chunk_id=child_id, parent=self, indent=int(len(self.indent) / 2) + 1) + return child + + + def skip_bytes(self,num): + self.file.seek(num,1) + + + def peak_next_header(self): + buf = self.read_bytes(4); + self.file.seek(-4,1) + return buf.decode("utf-8") + + + def could_have_child(self): + return self.end_pos - self.file.tell() >= 8 + + + + MAX_SIZE: int = 2147483647 - 8 + +''' +with open("/Users/will/Desktop/spacedoortest/spa1_prop_impdoor.msh", "rb") as tst_stream: + with Reader(tst_stream, "HEDR") as hedr: + print(hedr.peak_next_header()) +''' + + + + + diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py new file mode 100644 index 0000000..cae2d15 --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -0,0 +1,308 @@ +""" Contains functions for saving a Scene to a .msh file. """ + +from itertools import islice +from typing import Dict +from .msh_scene import Scene +from .msh_model import * +from .msh_material import * +from .msh_reader import Reader +from .msh_utilities import * + +from .crc import * + +def read_scene(input_file) -> Scene: + + scene = Scene() + scene.models = [] + + with Reader(file=input_file, chunk_id="HEDR") as hedr: + with hedr.read_child("MSH2") as msh2: + + with msh2.read_child("SINF") as sinf: + pass + + materials_list: List[str] = [] + + with msh2.read_child("MATL") as matl: + materials_list = _read_matl_and_get_materials_list(matl) + + while ("MODL" in msh2.peak_next_header()): + with msh2.read_child("MODL") as modl: + scene.models.append(_read_modl(modl, materials_list)) + + mats_dict = {} + for i,mat in enumerate(materials_list): + mats_dict["Material" + str(i)] = mat + + scene.materials = mats_dict + + #with hedr.read_child("ANM2") as anm2: #simple for now + # for anim in scene.anims: + # _write_anm2(anm2, anim) + + #with hedr.read_child("CL1L"): + # pass + + + + return scene + + +def _read_matl_and_get_materials_list(matl: Reader) -> List[str]: + materials_list: List[str] = [] + + num_mats = matl.read_u32() + + for _ in range(num_mats): + with matl.read_child("MATD") as matd: + materials_list.append(_read_matd(matd)) + + return materials_list + + + +def _read_matd(matd: Reader) -> Material: + + mat = Material() + + while matd.could_have_child(): + + next_header = matd.peak_next_header() + + if "NAME" in next_header: + with matd.read_child("NAME") as name: + mat.name = name.read_string() + print(matd.indent + "Got a new material: " + mat.name) + + elif "DATA" in next_header: + with matd.read_child("DATA") as data: + data.read_f32(4) # Diffuse Color (Seams to get ignored by modelmunge) + mat.specular_color = data.read_f32(4) + data.read_f32(4) # Ambient Color (Seams to get ignored by modelmunge and Zero(?)) + data.read_f32() # Specular Exponent/Decay (Gets ignored by RedEngine in SWBFII for all known materials) + + elif "ATRB" in next_header: + with matd.read_child("ATRB") as atrb: + mat.flags = atrb.read_u8() + mat.rendertype = atrb.read_u8() + mat.data = atrb.read_u8(2) + + elif "TX0D" in next_header: + with matd.read_child("TX0D") as tx0d: + mat.texture0 = tx0d.read_string() + + elif "TX1D" in next_header: + with matd.read_child("TX1D") as tx1d: + mat.texture1 = tx1d.read_string() + + elif "TX2D" in next_header: + with matd.read_child("TX2D") as tx2d: + mat.texture2 = tx2d.read_string() + + elif "TX3D" in next_header: + with matd.read_child("TX3D") as tx3d: + mat.texture3 = tx3d.read_string() + + else: + matd.skip_bytes(4) + + return mat + + +def _read_modl(modl: Reader, materials_list: List[str]) -> Model: + + model = Model() + + while modl.could_have_child(): + + next_header = modl.peak_next_header() + + if "MTYP" in next_header: + with modl.read_child("MTYP") as mtyp: + model.model_type = mtyp.read_u32() + + elif "MNDX" in next_header: + with modl.read_child("MNDX") as mndx: + pass + + elif "NAME" in next_header: + with modl.read_child("NAME") as name: + model.name = name.read_string() + print(modl.indent + "New model: " + model.name) + + elif "PRNT" in next_header: + with modl.read_child("PRNT") as prnt: + model.parent = prnt.read_string() + + elif "FLGS" in next_header: + with modl.read_child("FLGS") as flgs: + model.hidden = flgs.read_u32() + + elif "TRAN" in next_header: + with modl.read_child("TRAN") as tran: + model.transform = _read_tran(tran) + + elif "GEOM" in next_header: + model.geometry = [] + with modl.read_child("GEOM") as geom: + + next_header_modl = geom.peak_next_header() + + if "SEGM" in next_header_modl: + with geom.read_child("SEGM") as segm: + model.geometry.append(_read_segm(segm, materials_list)) + ''' + if model.model_type == ModelType.SKIN: + with modl.read_child("ENVL") as envl: + envl.write_u32(len(scene.models)) + for i in range(len(scene.models)): + envl.write_u32(i) + ''' + elif "SWCI" in next_header: + prim = CollisionPrimitive() + with modl.read_child("SWCI") as swci: + prim.shape.value = swci.read_u32() + prim.radius = swci.read_f32() + prim.height = swci.read_f32() + prim.length = swci.read_f32() + model.collisionprimitive = prim + + else: + with modl.read_child("NULL") as unknown: + pass + + + +def _read_tran(tran: Reader) -> ModelTransform: + + xform = ModelTransform() + + tran.skip_bytes(4 * 3) #ignore scale + xform.rotation = Quaternion(tran.read_f32(4)) + xform.position = Vector(tran.read_f32(3)) + + return xform + + + +def _read_segm(segm: Reader, materials_list: List[str]) -> GeometrySegment: + + geometry_seg = GeometrySegment() + + while segm.could_have_child(): + + next_header = segm.peak_next_header() + + if "MATI" in next_header: + with segm.read_child("MATI") as mati: + geometry_seg.material_name = materials_list[mati.read_u32()] + + elif "POSL" in next_header: + with segm.read_child("POSL") as posl: + num_positions = posl.read_u32() + + for _ in range(num_positions): + geometry_seg.positions.append(Vector(posl.read_f32(3))) + + elif "NRML" in next_header: + with segm.read_child("NRML") as nrml: + num_normals = nrml.read_u32() + + for _ in range(num_positions): + geometry_seg.normals.append(Vector(nrml.read_f32(3))) + + elif "WGHT" in next_header: + geometry_seg.weights = [] + + with segm.read_child("WGHT") as wght: + num_boneweights = wght.read_u32() + + for _ in range(num_boneweights): + geometry_seg.weights.append((wght.read_u32(), wght.read_f32())) + + elif "CLRL" in next_header: + geometry_seg.colors = [] + + with segm.read_child("CLRL") as clrl: + num_colors = clrl.read_u32() + + for _ in range(num_colors): + geometry_seg.colors += unpack_color(clrl.read_u32()) + + elif "UV0L" in next_header: + with segm.read_child("UV0L") as uv0l: + num_texcoords = uv0l.read_u32() + + for _ in range(num_texcoords): + geometry_seg.texcoords.append(Vector(uv0l.read_f32(2))) + + elif "NDXL" in next_header: + with segm.read_child("NDXL") as ndxl: + num_polygons = ndxl.read_u32() + + for _ in range(num_polygons): + polygon = ndxl.read_u16(ndxl.read_u16()) + geometry_seg.polygons.append(polygon) + + elif "NDXT" in next_header: + with segm.read_child("NDXT") as ndxt: + num_tris = ndxt.read_u32() + + for _ in range(num_tris): + geometry_seg.triangles.append(ndxt.read_u16(3)) + + elif "STRP" in next_header: + with segm.read_child("STRP") as strp: + pass + + if segm.read_u16 != 0: #trailing 0 bug + segm.skip_bytes(-2) + + else: + with segm.read_child("NULL") as unknown: + pass + + return geometry_seg + + + +''' + + +def _write_anm2(anm2: Writer, anim: Animation): + + with anm2.read_child("CYCL") as cycl: + + cycl.write_u32(1) + cycl.write_string(anim.name) + + for _ in range(63 - len(anim.name)): + cycl.write_u8(0) + + cycl.write_f32(10.0) #test framerate + cycl.write_u32(0) #what does play style refer to? + cycl.write_u32(0, 20) #first frame indices + + + with anm2.read_child("KFR3") as kfr3: + + kfr3.write_u32(len(anim.bone_transforms.keys())) + + for boneName in anim.bone_transforms.keys(): + kfr3.write_u32(crc(boneName)) + kfr3.write_u32(0) #what is keyframe type? + + kfr3.write_u32(21, 21) #basic testing + + for i, xform in enumerate(anim.bone_transforms[boneName]): + kfr3.write_u32(i) + kfr3.write_f32(xform.translation.x, xform.translation.y, xform.translation.z) + + for i, xform in enumerate(anim.bone_transforms[boneName]): + kfr3.write_u32(i) + kfr3.write_f32(xform.rotation.x, xform.rotation.y, xform.rotation.z, xform.rotation.w) + + +''' + + diff --git a/addons/io_scene_swbf_msh/msh_utilities.py b/addons/io_scene_swbf_msh/msh_utilities.py index 7715383..701933f 100644 --- a/addons/io_scene_swbf_msh/msh_utilities.py +++ b/addons/io_scene_swbf_msh/msh_utilities.py @@ -1,6 +1,7 @@ """ Misc utilities. """ from mathutils import Vector +from typing import List def add_vec(l: Vector, r: Vector) -> Vector: return Vector(v0 + v1 for v0, v1 in zip(l, r)) @@ -29,3 +30,14 @@ def pack_color(color) -> int: packed |= (int(color[3] * 255.0 + 0.5) << 24) return packed + +def unpack_color(color: int) -> List[float]: + + mask = int(0x000000ff) + + r = (color & (mask << 16)) / 255.0 + g = (color & (mask << 8)) / 255.0 + b = (color & mask) / 255.0 + a = (color & (mask << 24)) / 255.0 + + return [r,g,b,a] From 1cc6a8d08d04f8e0d808567380862627a7baf148 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Mon, 26 Oct 2020 10:20:33 -0400 Subject: [PATCH 10/39] Non-import changes removed --- addons/io_scene_swbf_msh/msh_anim_gather.py | 57 ---------------- addons/io_scene_swbf_msh/msh_model.py | 15 +---- addons/io_scene_swbf_msh/msh_model_gather.py | 64 ++---------------- addons/io_scene_swbf_msh/msh_scene.py | 21 +----- addons/io_scene_swbf_msh/msh_scene_save.py | 70 ++------------------ 5 files changed, 14 insertions(+), 213 deletions(-) delete mode 100644 addons/io_scene_swbf_msh/msh_anim_gather.py diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py deleted file mode 100644 index 631839f..0000000 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ /dev/null @@ -1,57 +0,0 @@ -""" Gathers the Blender objects from the current scene and returns them as a list of - Model objects. """ - -import bpy -import math -from enum import Enum -from typing import List, Set, Dict, Tuple -from itertools import zip_longest -from .msh_model import * -from .msh_model_utilities import * -from .msh_utilities import * -from .msh_model_gather import * - - -def gather_animdata(armature: bpy.types.Armature) -> List[Animation]: - - anim_data = Animation(); - - action = armature.animation_data.action - - framerange = action.frame_range - increment = (framerange.y - framerange.x) / 20.0 - offset = framerange.x; - - anim_data.bone_transforms[armature.parent.name] = [] - for bone in armature.data.bones: - anim_data.bone_transforms[bone.name] = [] - - for frame in range(21): - frame_time = offset + frame * increment - bpy.context.scene.frame_set(frame_time) - - anim_data.bone_transforms[armature.parent.name].append(ModelTransform()) #for testing - - for bone in armature.pose.bones: - xform = ModelTransform() - - - vt = convert_vector_space(bone.location); - - xform.translation = Vector((vt.x * -1.0, vt.y, vt.z)) - xform.rotation = convert_rotation_space(bone.rotation_quaternion) - - ''' - xform.translation = bone.location - xform.rotation = bone.rotation_quaternion - - anim_data.bone_transforms[bone.name].append(xform) - ''' - - return [anim_data] - - - - - - diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 31f04a0..589a274 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -2,7 +2,7 @@ saved to a .msh file. """ from dataclasses import dataclass, field -from typing import List, Dict, Tuple +from typing import List from enum import Enum from mathutils import Vector, Quaternion @@ -37,8 +37,7 @@ class GeometrySegment: normals: List[Vector] = field(default_factory=list) colors: List[List[float]] = None texcoords: List[Vector] = field(default_factory=list) - - weights: List[Tuple[int, float]] = None + # TODO: Skin support. polygons: List[List[int]] = field(default_factory=list) triangles: List[List[int]] = field(default_factory=list) @@ -66,13 +65,3 @@ class Model: geometry: List[GeometrySegment] = None collisionprimitive: CollisionPrimitive = None - - vgroups_to_modelnames_map : Dict[int, str] = None - -@dataclass -class Animation: - """ Class representing 'CYCL' + 'KFR3' sections in a .msh file """ - - name: str = "open" - bone_transforms: Dict[str, List[ModelTransform]] = field(default_factory=dict) - diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 7b6e7fd..a55a5ef 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -4,13 +4,13 @@ import bpy import math from enum import Enum -from typing import List, Set, Dict, Tuple, Set +from typing import List, Set, Dict, Tuple from itertools import zip_longest from .msh_model import * from .msh_model_utilities import * from .msh_utilities import * -SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE", "ARMATURE"} +SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"} MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"} MAX_MSH_VERTEX_COUNT = 32767 @@ -22,7 +22,6 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: parents = create_parents_set() models_list: List[Model] = [] - skeleton: bpy.types.Armature = None for uneval_obj in select_objects(export_target): if uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents: @@ -45,24 +44,11 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: model.transform.translation = convert_vector_space(local_translation) if obj.parent is not None: - if obj.parent.type == "ARMATURE": - skeleton = obj.parent - - parent_bone_name = obj.parent_bone - if parent_bone_name == "": - model.parent = obj.parent.parent - else: - model.parent = parent_bone_name - - if model.model_type == ModelType.SKIN: - model.vgroups_to_modelnames_map = {} - for i, vgroup in enumerate(obj.vertex_groups): - vgroups_to_modelnames_map[i] = vgroup.name - + model.parent = obj.parent.name if obj.type in MESH_OBJECT_TYPES: - mesh = obj.to_mesh() - model.geometry = create_mesh_geometry(mesh, model.model_type == ModelType.SKIN) + mesh = obj.to_mesh() + model.geometry = create_mesh_geometry(mesh) obj.to_mesh_clear() _, _, world_scale = obj.matrix_world.decompose() @@ -80,30 +66,6 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: models_list.append(model) - - for bone in skeleton.data.bones: - - model = Model() - model.name = bone.name - model.model_type = ModelType.NULL - model.hidden = False - - local_translation, local_rotation, _ = bone.matrix_local.decompose() - model.transform.rotation = convert_rotation_space(local_rotation) - model.transform.translation = convert_vector_space(local_translation) - - parent_name = bone.parent - if parent_name is not None: - model.parent = parent_name - else: - if skeleton.parent is not None: - model.parent = skeleton.parent.name - else: - model.parent = None - - models_list.append(model) - - return models_list def create_parents_set() -> Set[str]: @@ -118,7 +80,7 @@ def create_parents_set() -> Set[str]: return parents -def create_mesh_geometry(mesh: bpy.types.Mesh, is_skinned : bool) -> List[GeometrySegment]: +def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: """ Creates a list of GeometrySegment objects from a Blender mesh. Does NOT create triangle strips in the GeometrySegment however. """ @@ -192,16 +154,6 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, is_skinned : bool) -> List[Geomet segment.positions.append(convert_vector_space(mesh.vertices[vertex_index].co)) segment.normals.append(convert_vector_space(vertex_normal)) - if is_skinned: - for i,grp_el in mesh.vertices[vertex_index].groups: - segment.weights.append(tuple(grp_el.group, grp_el.weight)) - print("Adding weight to group {grp_el.group} of value {grp_el.weight}") - if i > 3: #will have to look into aramture/skin settings for limiting envolopes to 4 weights... - break - while i < 3: - segment.weights.append(tuple(0,0.0)) - i+=1 - if mesh.uv_layers.active is None: segment.texcoords.append(Vector((0.0, 0.0))) else: @@ -212,7 +164,6 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, is_skinned : bool) -> List[Geomet return new_index - for tri in mesh.loop_triangles: polygons[tri.material_index].add(tri.polygon_index) segments[tri.material_index].triangles.append([ @@ -230,8 +181,7 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, is_skinned : bool) -> List[Geomet def get_model_type(obj: bpy.types.Object) -> ModelType: """ Get the ModelType for a Blender object. """ - if obj.parent_type == "ARMATURE": - return ModelType.SKIN + # TODO: Skinning support, etc if obj.type in MESH_OBJECT_TYPES: return ModelType.STATIC diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index 7fa5930..6c6f7fd 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -6,7 +6,7 @@ from typing import List, Dict from copy import copy import bpy from mathutils import Vector -from .msh_model import Model, Animation +from .msh_model import Model 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 from .msh_model_triangle_strips import create_models_triangle_strips @@ -14,7 +14,6 @@ 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 * @dataclass class SceneAABB: @@ -44,8 +43,6 @@ class Scene: name: str = "Scene" materials: Dict[str, Material] = field(default_factory=dict) models: List[Model] = field(default_factory=list) - anims: List[Animation] = field(default_factory=list) - def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str) -> Scene: """ Create a msh Scene from the active Blender scene. """ @@ -70,24 +67,8 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t if has_multiple_root_models(scene.models): scene.models = reparent_model_roots(scene.models) - - #now that we've collected all models, we should remap WGHT indices... - names_to_indices = {} - for i,model in enumerate(scene.models): - names_to_indices[model.name] = i; - - for model in scene.models: - if model.model_type == ModelType.SKIN: - for segment in model.geometry: - for i in range(len(segment.weights)): - vgroup_index = segment.weights[i][0] - segment.weights[i][0] = names_to_indices[model.vgroups_to_modelnames_map[vgroup_index]] - - scene.materials = remove_unused_materials(scene.materials, scene.models) - #scene.anims = gather_animdata(bpy.context.scene.objects["Armature"]) - return scene def create_scene_aabb(scene: Scene) -> SceneAABB: diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 2fd0945..71914a0 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -8,8 +8,6 @@ from .msh_material import * from .msh_writer import Writer from .msh_utilities import * -from .crc import * - def save_scene(output_file, scene: Scene): """ Saves scene to the supplied file. """ @@ -26,11 +24,7 @@ def save_scene(output_file, scene: Scene): for index, model in enumerate(scene.models): with msh2.create_child("MODL") as modl: - _write_modl(modl, model, index, material_index, scene) - - #with hedr.create_child("ANM2") as anm2: #simple for now - # for anim in scene.anims: - # _write_anm2(anm2, anim) + _write_modl(modl, model, index, material_index) with hedr.create_child("CL1L"): pass @@ -40,8 +34,8 @@ def _write_sinf(sinf: Writer, scene: Scene): name.write_string(scene.name) with sinf.create_child("FRAM") as fram: - fram.write_i32(0, 20) #test values - fram.write_f32(10.0) #test values + fram.write_i32(0, 1) + fram.write_f32(29.97003) with sinf.create_child("BBOX") as bbox: aabb = create_scene_aabb(scene) @@ -103,7 +97,7 @@ def _write_matd(matd: Writer, material_name: str, material: Material): with matd.create_child("TX3D") as tx3d: tx3d.write_string(material.texture3) -def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str, int], scene: Scene): +def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str, int]): with modl.create_child("MTYP") as mtyp: mtyp.write_u32(model.model_type.value) @@ -129,12 +123,6 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str for segment in model.geometry: with geom.create_child("SEGM") as segm: _write_segm(segm, segment, material_index) - - if model.model_type == ModelType.SKIN: - with modl.create_child("ENVL") as envl: - envl.write_u32(len(scene.models)) - for i in range(len(scene.models)): - envl.write_u32(i) if model.collisionprimitive is not None: with modl.create_child("SWCI") as swci: @@ -165,13 +153,6 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for normal in segment.normals: nrml.write_f32(normal.x, normal.y, normal.z) - if segment.weights is not None: - with segm.create_child("WGHT") as wght: - wght.write_u32(len(segment.weights) / 4) - for weight in segment.weights: - wght.write_u32(weight[0]) - wght.write_f32(weight[1]) - if segment.colors is not None: with segm.create_child("CLRL") as clrl: clrl.write_u32(len(segment.colors)) @@ -208,46 +189,3 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for index in islice(strip, 2, len(strip)): strp.write_u16(index) - - -def _write_anm2(anm2: Writer, anim: Animation): - - with anm2.create_child("CYCL") as cycl: - - cycl.write_u32(1) - cycl.write_string(anim.name) - - for _ in range(63 - len(anim.name)): - cycl.write_u8(0) - - cycl.write_f32(10.0) #test framerate - cycl.write_u32(0) #what does play style refer to? - cycl.write_u32(0, 20) #first frame indices - - - with anm2.create_child("KFR3") as kfr3: - - kfr3.write_u32(len(anim.bone_transforms.keys())) - - for boneName in anim.bone_transforms.keys(): - kfr3.write_u32(crc(boneName)) - kfr3.write_u32(0) #what is keyframe type? - - kfr3.write_u32(21, 21) #basic testing - - for i, xform in enumerate(anim.bone_transforms[boneName]): - kfr3.write_u32(i) - kfr3.write_f32(xform.translation.x, xform.translation.y, xform.translation.z) - - for i, xform in enumerate(anim.bone_transforms[boneName]): - kfr3.write_u32(i) - kfr3.write_f32(xform.rotation.x, xform.rotation.y, xform.rotation.z, xform.rotation.w) - - - - - - - - - From fb072f8d59dcb7f12ef9abdd654c19d3dba08b30 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Mon, 19 Oct 2020 22:18:08 -0500 Subject: [PATCH 11/39] Working for complex skinned exports, still crashes ZE, menus need work --- addons/io_scene_swbf_msh/__init__.py | 18 +++-- addons/io_scene_swbf_msh/msh_anim_gather.py | 71 ++++++------------ addons/io_scene_swbf_msh/msh_model.py | 6 +- addons/io_scene_swbf_msh/msh_model_gather.py | 29 +++++-- .../io_scene_swbf_msh/msh_model_utilities.py | 14 +--- addons/io_scene_swbf_msh/msh_scene.py | 12 +-- addons/io_scene_swbf_msh/msh_scene_save.py | 75 ++++++++++++++++--- 7 files changed, 135 insertions(+), 90 deletions(-) diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index 2d25e73..8b97f68 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -97,9 +97,15 @@ class ExportMSH(Operator, ExportHelper): default=True ) - export_anims: BoolProperty( - name="Export Animations", - description="Action export test", + export_animated: BoolProperty( + name="Export Animated Object", + description="Always check if the object will be animated.", + default=False + ) + + export_skeleton_only: BoolProperty( + name="Export Skeleton", + description="Check if you intend to export skeleton data only.", default=False ) @@ -112,8 +118,10 @@ class ExportMSH(Operator, ExportHelper): scene=create_scene( generate_triangle_strips=self.generate_triangle_strips, apply_modifiers=self.apply_modifiers, - export_target=self.export_target), - separate_anims=self.export_anims + export_target=self.export_target, + skel_only=self.export_skeleton_only + ), + is_animated=self.export_animated ) return {'FINISHED'} diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index bf7ef8c..794d094 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -12,65 +12,42 @@ from .msh_utilities import * from .msh_model_gather import * -def gather_animdata(armature: bpy.types.Armature) -> List[Animation]: - - anim_data = Animation(); +def extract_anim(armature: bpy.types.Armature) -> Animation: action = armature.animation_data.action + anim = Animation(); + + if not action: + framerange = Vector((0.0,1.0)) + else: + framerange = action.frame_range - framerange = action.frame_range - increment = (framerange.y - framerange.x) / 20.0 - offset = framerange.x; + num_frames = math.floor(framerange.y - framerange.x) + 1 + increment = (framerange.y - framerange.x) / (num_frames - 1) - anim_data.bone_transforms[armature.parent.name] = [] + anim.end_index = num_frames - 1 + for bone in armature.data.bones: - anim_data.bone_transforms[bone.name] = [] + anim.bone_transforms[bone.name] = [] - for frame in range(21): - frame_time = offset + frame * increment + for frame in range(num_frames): + + frame_time = framerange.x + frame * increment bpy.context.scene.frame_set(frame_time) - anim_data.bone_transforms[armature.parent.name].append(ModelTransform()) #for testing - for bone in armature.pose.bones: - xform = ModelTransform() - xform.translation = to_skeleton_vector_space(bone.location) - xform.rotation = to_skeleton_rotation_space(bone.rotation_quaternion) - - anim_data.bone_transforms[bone.name].append(xform) - - - return [anim_data] + transform = bone.matrix - -def get_basepose(armature: bpy.types.Armature) -> Animation: - - anim_data = Animation(); - anim_data.name = "basepose" - - bpy.context.scene.frame_set(0.0) - - anim_data.bone_transforms[armature.parent.name] = [] - for bone in armature.data.bones: - anim_data.bone_transforms[bone.name] = [] - - for frame in range(2): - - anim_data.bone_transforms[armature.parent.name].append(ModelTransform()) #for testing - - for bone in armature.pose.bones: + if bone.parent: + transform = bone.parent.matrix.inverted() @ transform + + loc, rot, _ = transform.decompose() xform = ModelTransform() - xform.translation = to_skeleton_vector_space(bone.location) - xform.rotation = to_skeleton_rotation_space(bone.rotation_quaternion) - - anim_data.bone_transforms[bone.name].append(xform) - - - return anim_data - - - + xform.rotation = convert_rotation_space(rot) + xform.translation = convert_vector_space(loc) + anim.bone_transforms[bone.name].append(xform) + return anim diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 0af2932..10673de 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -80,6 +80,10 @@ class Model: class Animation: """ Class representing 'CYCL' + 'KFR3' sections in a .msh file """ - name: str = "wiggle" + name: str = "fullanimation" bone_transforms: Dict[str, List[ModelTransform]] = field(default_factory=dict) + + framerate: float = 10.0 + start_index : int = 0 + end_index : int = 0 diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index c189e5d..5550c44 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -14,7 +14,7 @@ SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"} MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"} MAX_MSH_VERTEX_COUNT = 32767 -def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: +def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool) -> List[Model]: """ Gathers the Blender objects from the current scene and returns them as a list of Model objects. """ @@ -36,15 +36,26 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: if obj.type == "ARMATURE": models_list += expand_armature(obj) - continue - local_translation, local_rotation, _ = obj.matrix_local.decompose() + if skeleton_only: + continue model = Model() model.name = obj.name model.model_type = get_model_type(obj) model.hidden = get_is_model_hidden(obj) - model.transform.rotation = convert_rotation_space(local_rotation) + + transform = obj.matrix_local + + if obj.parent_bone: + model.parent = obj.parent_bone + transform = obj.parent.data.bones[obj.parent_bone].matrix_local.inverted() @ transform + else: + if obj.parent is not None: + model.parent = obj.parent.name + + local_translation, local_rotation, _ = transform.decompose() + model.transform.rotation = convert_rotation_space(local_rotation) model.transform.translation = convert_vector_space(local_translation) if obj.type in MESH_OBJECT_TYPES: @@ -126,11 +137,11 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet if use_smooth_normal or mesh.use_auto_smooth: if mesh.has_custom_normals: - vertex_normal = mesh.loops[loop_index].normal + vertex_normal = Vector( mesh.loops[loop_index].normal ) else: - vertex_normal = mesh.vertices[vertex_index].normal + vertex_normal = Vector( mesh.vertices[vertex_index].normal ) else: - vertex_normal = face_normal + vertex_normal = Vector(face_normal) def get_cache_vertex(): yield mesh.vertices[vertex_index].co.x @@ -343,6 +354,8 @@ def select_objects(export_target: str) -> List[bpy.types.Object]: return objects + parents + + def expand_armature(obj: bpy.types.Object) -> List[Model]: bones: List[Model] = [] @@ -355,7 +368,7 @@ def expand_armature(obj: bpy.types.Object) -> List[Model]: transform = bone.parent.matrix_local.inverted() @ transform model.parent = bone.parent.name else: - model.parent = "DummyRoot" # obj.name + model.parent = obj.name local_translation, local_rotation, _ = transform.decompose() diff --git a/addons/io_scene_swbf_msh/msh_model_utilities.py b/addons/io_scene_swbf_msh/msh_model_utilities.py index e33e9f4..b0f7c49 100644 --- a/addons/io_scene_swbf_msh/msh_model_utilities.py +++ b/addons/io_scene_swbf_msh/msh_model_utilities.py @@ -3,6 +3,8 @@ from typing import List from .msh_model import * from .msh_utilities import * +import mathutils +import math from mathutils import Vector, Matrix def scale_segments(scale: Vector, segments: List[GeometrySegment]): @@ -125,15 +127,3 @@ def convert_scale_space(vec: Vector) -> Vector: def convert_rotation_space(quat: Quaternion) -> Quaternion: return Quaternion((-quat.w, quat.x, -quat.z, -quat.y)) - -def to_skeleton_vector_space(vec : Vector): - vnew = convert_vector_space(vec) - vnew.x *= -1.0 - return vnew - -def to_skeleton_rotation_space(quat : Quaternion): - qnew = convert_rotation_space(quat) - qnew.x *= -1.0 - qnew.y *= -1.0 - qnew.z *= -1.0 - return qnew diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index 459004e..8a813ed 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -47,7 +47,7 @@ class Scene: anims: List[Animation] = field(default_factory=list) -def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str) -> Scene: +def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool) -> Scene: """ Create a msh Scene from the active Blender scene. """ scene = Scene() @@ -56,7 +56,7 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t scene.materials = gather_materials() - scene.models = gather_models(apply_modifiers=apply_modifiers, export_target=export_target) + scene.models = 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: @@ -70,13 +70,15 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t if has_multiple_root_models(scene.models): scene.models = reparent_model_roots(scene.models) - scene.materials = remove_unused_materials(scene.materials, scene.models) - - scene.anims = gather_animdata(bpy.context.scene.objects["Armature"]) + + #creates a dummy basepose if no Action is selected + if "Armature" in bpy.context.scene.objects.keys(): + scene.anims = [extract_anim(bpy.context.scene.objects["Armature"])] return scene + def create_scene_aabb(scene: Scene) -> SceneAABB: """ Create a SceneAABB for a Scene. """ diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index dc3a2ae..685df7f 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -10,7 +10,7 @@ from .msh_utilities import * from .crc import * -def save_scene(output_file, scene: Scene, separate_anims: bool): +def save_scene(output_file, scene: Scene, is_animated: bool): """ Saves scene to the supplied file. """ with Writer(file=output_file, chunk_id="HEDR") as hedr: @@ -26,12 +26,19 @@ def save_scene(output_file, scene: Scene, separate_anims: bool): material_index = _write_matl_and_get_material_index(matl, scene) for index, model in enumerate(scene.models): - if separate_anims and (model.model_type not in {ModelType.NULL, ModelType.BONE}): - continue + + print(model.name) + with msh2.create_child("MODL") as modl: _write_modl(modl, model, index, material_index, model_index) - if separate_anims: + if is_animated: + with hedr.create_child("SKL2") as skl2: + _write_skl2(skl2, scene) + + with hedr.create_child("BLN2") as bln2: + _write_bln2(bln2, scene) + with hedr.create_child("ANM2") as anm2: #simple for now for anim in scene.anims: _write_anm2(anm2, anim) @@ -44,8 +51,15 @@ def _write_sinf(sinf: Writer, scene: Scene): name.write_string(scene.name) with sinf.create_child("FRAM") as fram: - fram.write_i32(0, 20) #test values - fram.write_f32(10.0) #test values + min_index = 0 + max_index = 1 + + if scene.anims and len(scene.anims) > 0: + min_index = min([anim.start_index for anim in scene.anims]) + max_index = min([anim.end_index for anim in scene.anims]) + + fram.write_i32(min_index, max_index) + fram.write_f32(10.0) with sinf.create_child("BBOX") as bbox: aabb = create_scene_aabb(scene) @@ -130,6 +144,12 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str if model.geometry is not None: with modl.create_child("GEOM") as geom: + + with geom.create_child("BBOX") as bbox: + bbox.write_f32(0.0, 0.0, 0.0, 1.0) + bbox.write_f32(0, 0, 0) + bbox.write_f32(1.0,1.0,1.0,2.0) + for segment in model.geometry: with geom.create_child("SEGM") as segm: _write_segm(segm, segment, material_index) @@ -168,7 +188,7 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str with segm.create_child("NRML") as nrml: nrml.write_u32(len(segment.normals)) - for normal in segment.normals: + for i,normal in enumerate(segment.normals): nrml.write_f32(normal.x, normal.y, normal.z) if segment.colors is not None: @@ -214,6 +234,8 @@ SKINNING CHUNKS def _write_wght(wght: Writer, weights: List[List[VertexWeight]]): wght.write_u32(len(weights)) + print("Writing WGHT: ") + for weight_list in weights: weight_list += [VertexWeight(0.0, 0)] * 4 weight_list = sorted(weight_list, key=lambda w: w.weight, reverse=True) @@ -221,18 +243,46 @@ def _write_wght(wght: Writer, weights: List[List[VertexWeight]]): total_weight = max(sum(map(lambda w: w.weight, weight_list)), 1e-5) + print_str = "" + for weight in weight_list: + + print_str += "({}, {}) ".format(weight.bone, weight.weight / total_weight) + wght.write_i32(weight.bone) wght.write_f32(weight.weight / total_weight) + print(" {}".format(print_str)) + def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]): envl.write_u32(len(model.bone_map)) + print("Writing ENVL: ") + for bone_name in model.bone_map: + print(" {:10} Index: {}".format(bone_name, model_index[bone_name])) envl.write_u32(model_index[bone_name]) ''' -ANIMATION CHUNKS +SKELETON CHUNKS +''' +def _write_bln2(bln2: Writer, scene: Scene): + bones = scene.anims[0].bone_transforms.keys() + bln2.write_u32(len(bones)) + + for boneName in bones: + bln2.write_u32(crc(boneName), 0) + +def _write_skl2(skl2: Writer, scene: Scene): + bones = scene.anims[0].bone_transforms.keys() + skl2.write_u32(len(bones)) + + for boneName in bones: + skl2.write_u32(crc(boneName), 0) #default values + skl2.write_f32(1.0, 0.0, 0.0) #from docs + +''' +ANIMATION CHUNK ''' def _write_anm2(anm2: Writer, anim: Animation): @@ -241,12 +291,12 @@ def _write_anm2(anm2: Writer, anim: Animation): cycl.write_u32(1) cycl.write_string(anim.name) - for _ in range(63 - len(anim.name) ): + for _ in range(63 - len(anim.name)): cycl.write_u8(0) - cycl.write_f32(10.0) #test framerate + cycl.write_f32(anim.framerate) cycl.write_u32(0) #what does play style refer to? - cycl.write_u32(0, 20) #first frame indices + cycl.write_u32(anim.start_index, anim.end_index) #first frame indices with anm2.create_child("KFR3") as kfr3: @@ -257,7 +307,8 @@ def _write_anm2(anm2: Writer, anim: Animation): kfr3.write_u32(crc(boneName)) kfr3.write_u32(0) #what is keyframe type? - kfr3.write_u32(21, 21) #basic testing + num_frames = 1 + anim.end_index - anim.start_index + kfr3.write_u32(num_frames, num_frames) #basic testing for i, xform in enumerate(anim.bone_transforms[boneName]): kfr3.write_u32(i) From b8afa1ed105fb29e721a934bad2ddd1bf9b6c4f9 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Fri, 30 Oct 2020 16:59:54 -0400 Subject: [PATCH 12/39] Models with basic materials importing correctly --- addons/io_scene_swbf_msh/__init__.py | 3 +- addons/io_scene_swbf_msh/msh_material.py | 5 +- addons/io_scene_swbf_msh/msh_scene_read.py | 130 +++++++-------------- addons/io_scene_swbf_msh/msh_to_blend.py | 124 ++++++++++++++++++++ 4 files changed, 171 insertions(+), 91 deletions(-) create mode 100644 addons/io_scene_swbf_msh/msh_to_blend.py diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index 6165685..e432118 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -60,6 +60,7 @@ from .msh_scene import create_scene from .msh_scene_save import save_scene from .msh_scene_read import read_scene from .msh_material_properties import * +from .msh_to_blend import * class ExportMSH(Operator, ExportHelper): """ Export the current scene as a SWBF .msh file. """ @@ -133,7 +134,7 @@ class ImportMSH(Operator, ImportHelper): def execute(self, context): with open(self.filepath, 'rb') as input_file: - read_scene(input_file) + extract_scene(read_scene(input_file)) return {'FINISHED'} def menu_func_import(self, context): diff --git a/addons/io_scene_swbf_msh/msh_material.py b/addons/io_scene_swbf_msh/msh_material.py index ed7867b..560e3bb 100644 --- a/addons/io_scene_swbf_msh/msh_material.py +++ b/addons/io_scene_swbf_msh/msh_material.py @@ -32,8 +32,9 @@ class MaterialFlags(Flag): @dataclass class Material: - """ Data class representing a .msh material. - Intended to be stored in a dictionary so name is missing. """ + """ Data class representing a .msh material.""" + + name: str = "" specular_color: Color = Color((1.0, 1.0, 1.0)) rendertype: Rendertype = Rendertype.NORMAL diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index cae2d15..9faa78f 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -1,4 +1,4 @@ -""" Contains functions for saving a Scene to a .msh file. """ +""" Contains functions for extracting a scene from a .msh file""" from itertools import islice from typing import Dict @@ -14,42 +14,52 @@ def read_scene(input_file) -> Scene: scene = Scene() scene.models = [] + scene.materials = {} with Reader(file=input_file, chunk_id="HEDR") as hedr: - with hedr.read_child("MSH2") as msh2: - with msh2.read_child("SINF") as sinf: - pass + while hedr.could_have_child(): - materials_list: List[str] = [] + next_header = hedr.peak_next_header() - with msh2.read_child("MATL") as matl: - materials_list = _read_matl_and_get_materials_list(matl) + if "MSH2" in next_header: - while ("MODL" in msh2.peak_next_header()): - with msh2.read_child("MODL") as modl: - scene.models.append(_read_modl(modl, materials_list)) + with hedr.read_child("MSH2") as msh2: - mats_dict = {} - for i,mat in enumerate(materials_list): - mats_dict["Material" + str(i)] = mat + materials_list = [] - scene.materials = mats_dict + while (msh2.could_have_child()): - #with hedr.read_child("ANM2") as anm2: #simple for now - # for anim in scene.anims: - # _write_anm2(anm2, anim) + next_header = msh2.peak_next_header() - #with hedr.read_child("CL1L"): - # pass + if "SINF" in next_header: + with msh2.read_child("SINF") as sinf: + pass + elif "MATL" in next_header: + with msh2.read_child("MATL") as matl: + materials_list += _read_matl_and_get_materials_list(matl) + for i,mat in enumerate(materials_list): + scene.materials[mat.name] = mat + elif "MODL" in next_header: + while ("MODL" in msh2.peak_next_header()): + with msh2.read_child("MODL") as modl: + scene.models.append(_read_modl(modl, materials_list)) + + else: + with hedr.read_child("NULL") as unknown: + pass + + else: + with hedr.read_child("NULL") as unknown: + pass return scene -def _read_matl_and_get_materials_list(matl: Reader) -> List[str]: - materials_list: List[str] = [] +def _read_matl_and_get_materials_list(matl: Reader) -> List[Material]: + materials_list: List[Material] = [] num_mats = matl.read_u32() @@ -72,7 +82,6 @@ def _read_matd(matd: Reader) -> Material: if "NAME" in next_header: with matd.read_child("NAME") as name: mat.name = name.read_string() - print(matd.indent + "Got a new material: " + mat.name) elif "DATA" in next_header: with matd.read_child("DATA") as data: @@ -109,7 +118,7 @@ def _read_matd(matd: Reader) -> Material: return mat -def _read_modl(modl: Reader, materials_list: List[str]) -> Model: +def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: model = Model() @@ -119,7 +128,7 @@ def _read_modl(modl: Reader, materials_list: List[str]) -> Model: if "MTYP" in next_header: with modl.read_child("MTYP") as mtyp: - model.model_type = mtyp.read_u32() + model.model_type = ModelType(mtyp.read_u32()) elif "MNDX" in next_header: with modl.read_child("MNDX") as mndx: @@ -128,7 +137,6 @@ def _read_modl(modl: Reader, materials_list: List[str]) -> Model: elif "NAME" in next_header: with modl.read_child("NAME") as name: model.name = name.read_string() - print(modl.indent + "New model: " + model.name) elif "PRNT" in next_header: with modl.read_child("PRNT") as prnt: @@ -151,13 +159,7 @@ def _read_modl(modl: Reader, materials_list: List[str]) -> Model: if "SEGM" in next_header_modl: with geom.read_child("SEGM") as segm: model.geometry.append(_read_segm(segm, materials_list)) - ''' - if model.model_type == ModelType.SKIN: - with modl.read_child("ENVL") as envl: - envl.write_u32(len(scene.models)) - for i in range(len(scene.models)): - envl.write_u32(i) - ''' + elif "SWCI" in next_header: prim = CollisionPrimitive() with modl.read_child("SWCI") as swci: @@ -171,6 +173,7 @@ def _read_modl(modl: Reader, materials_list: List[str]) -> Model: with modl.read_child("NULL") as unknown: pass + return model def _read_tran(tran: Reader) -> ModelTransform: @@ -178,14 +181,15 @@ def _read_tran(tran: Reader) -> ModelTransform: xform = ModelTransform() tran.skip_bytes(4 * 3) #ignore scale - xform.rotation = Quaternion(tran.read_f32(4)) - xform.position = Vector(tran.read_f32(3)) + + rot = tran.read_f32(4) + xform.rotation = Quaternion((rot[3], rot[0], rot[1], rot[2])) + xform.translation = Vector(tran.read_f32(3)) return xform - -def _read_segm(segm: Reader, materials_list: List[str]) -> GeometrySegment: +def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: geometry_seg = GeometrySegment() @@ -195,7 +199,7 @@ def _read_segm(segm: Reader, materials_list: List[str]) -> GeometrySegment: if "MATI" in next_header: with segm.read_child("MATI") as mati: - geometry_seg.material_name = materials_list[mati.read_u32()] + geometry_seg.material_name = materials_list[mati.read_u32()].name elif "POSL" in next_header: with segm.read_child("POSL") as posl: @@ -211,15 +215,6 @@ def _read_segm(segm: Reader, materials_list: List[str]) -> GeometrySegment: for _ in range(num_positions): geometry_seg.normals.append(Vector(nrml.read_f32(3))) - elif "WGHT" in next_header: - geometry_seg.weights = [] - - with segm.read_child("WGHT") as wght: - num_boneweights = wght.read_u32() - - for _ in range(num_boneweights): - geometry_seg.weights.append((wght.read_u32(), wght.read_f32())) - elif "CLRL" in next_header: geometry_seg.colors = [] @@ -255,7 +250,7 @@ def _read_segm(segm: Reader, materials_list: List[str]) -> GeometrySegment: with segm.read_child("STRP") as strp: pass - if segm.read_u16 != 0: #trailing 0 bug + if segm.read_u16 != 0: #trailing 0 bug https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP segm.skip_bytes(-2) else: @@ -265,44 +260,3 @@ def _read_segm(segm: Reader, materials_list: List[str]) -> GeometrySegment: return geometry_seg - -''' - - -def _write_anm2(anm2: Writer, anim: Animation): - - with anm2.read_child("CYCL") as cycl: - - cycl.write_u32(1) - cycl.write_string(anim.name) - - for _ in range(63 - len(anim.name)): - cycl.write_u8(0) - - cycl.write_f32(10.0) #test framerate - cycl.write_u32(0) #what does play style refer to? - cycl.write_u32(0, 20) #first frame indices - - - with anm2.read_child("KFR3") as kfr3: - - kfr3.write_u32(len(anim.bone_transforms.keys())) - - for boneName in anim.bone_transforms.keys(): - kfr3.write_u32(crc(boneName)) - kfr3.write_u32(0) #what is keyframe type? - - kfr3.write_u32(21, 21) #basic testing - - for i, xform in enumerate(anim.bone_transforms[boneName]): - kfr3.write_u32(i) - kfr3.write_f32(xform.translation.x, xform.translation.y, xform.translation.z) - - for i, xform in enumerate(anim.bone_transforms[boneName]): - kfr3.write_u32(i) - kfr3.write_f32(xform.rotation.x, xform.rotation.y, xform.rotation.z, xform.rotation.w) - - -''' - - diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py new file mode 100644 index 0000000..a5c3495 --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -0,0 +1,124 @@ +""" Gathers the Blender objects from the current scene and returns them as a list of + Model objects. """ + +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_model import * +from .msh_model_utilities import * +from .msh_utilities import * +from .msh_model_gather import * + + + +def extract_models(models, materials_map): + + model_map = {} + + for model in sort_by_parent(models): + + if model.model_type != ModelType.STATIC: + new_obj = bpy.data.objects.new(model.name, None) + new_obj.empty_display_size = 1 + new_obj.empty_display_type = 'PLAIN_AXES' + + else: + new_mesh = bpy.data.meshes.new(model.name) + verts = [] + faces = [] + offset = 0 + + mat_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] + faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles] + + offset += len(seg.positions) + + new_mesh.from_pydata(verts, [], faces) + new_mesh.update() + new_mesh.validate() + + + edit_mesh = bmesh.new() + edit_mesh.from_mesh(new_mesh) + + uvlayer = edit_mesh.loops.layers.uv.verify() + + for edit_mesh_face in edit_mesh.faces: + mesh_face = faces[edit_mesh_face.index] + + for i,loop in enumerate(edit_mesh_face.loops): + texcoord = seg.texcoords[mesh_face[i]] + loop[uvlayer].uv = tuple([texcoord.x, texcoord.y]) + + edit_mesh.to_mesh(new_mesh) + edit_mesh.free() + + + new_obj = bpy.data.objects.new(new_mesh.name, new_mesh) + + ''' + 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) + + + + model_map[model.name] = new_obj + + if model.parent: + new_obj.parent = model_map[model.parent] + + new_obj.location = convert_vector_space(model.transform.translation) + new_obj.rotation_mode = "QUATERNION" + new_obj.rotation_quaternion = convert_rotation_space(model.transform.rotation) + + bpy.context.collection.objects.link(new_obj) + + + +def extract_materials(scene: Scene) -> Dict[str,bpy.types.Material]: + + extracted_materials = {} + + for material_name in scene.materials.keys(): + + new_mat = bpy.data.materials.new(name=material_name) + new_mat.use_nodes = True + bsdf = new_mat.node_tree.nodes["Principled BSDF"] + texImage = new_mat.node_tree.nodes.new('ShaderNodeTexImage') + texImage.image = bpy.data.images.load("/Users/will/Desktop/grad.png") + new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) + + extracted_materials[material_name] = new_mat + + return extracted_materials + + + +def extract_scene(scene: Scene): + + matmap = extract_materials(scene) + extract_models(scene.models, matmap) + + + + + From 706c32431d450bfebf42031dd826e496dca8032f Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Sun, 1 Nov 2020 11:48:07 -0500 Subject: [PATCH 13/39] msh_reader refined, more chunks implemented --- addons/io_scene_swbf_msh/msh_model.py | 1 + addons/io_scene_swbf_msh/msh_reader.py | 23 ++---- addons/io_scene_swbf_msh/msh_scene.py | 2 + addons/io_scene_swbf_msh/msh_scene_read.py | 82 +++++++++++++--------- addons/io_scene_swbf_msh/msh_to_blend.py | 29 ++++---- 5 files changed, 71 insertions(+), 66 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 589a274..4f1e29b 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -12,6 +12,7 @@ class ModelType(Enum): CLOTH = 2 BONE = 3 STATIC = 4 + SHADOWVOLUME = 6 class CollisionPrimitiveShape(Enum): SPHERE = 0 diff --git a/addons/io_scene_swbf_msh/msh_reader.py b/addons/io_scene_swbf_msh/msh_reader.py index c652683..3203955 100644 --- a/addons/io_scene_swbf_msh/msh_reader.py +++ b/addons/io_scene_swbf_msh/msh_reader.py @@ -3,18 +3,17 @@ import io import struct class Reader: - def __init__(self, file, chunk_id: str, parent=None, indent=0): + def __init__(self, file, parent=None, indent=0): self.file = file self.size: int = 0 self.size_pos = None self.parent = parent - self.header = chunk_id - self.indent = " " * indent + self.indent = " " * indent #for print debugging def __enter__(self): self.size_pos = self.file.tell() - self.file.seek(4,1) #skip header, will add check later + self.header = self.read_bytes(4).decode("utf-8") self.size = self.read_u32() padding_length = 4 - (self.size % 4) if self.size % 4 > 0 else 0 @@ -84,8 +83,8 @@ class Reader: - def read_child(self, child_id: str): - child = Reader(self.file, chunk_id=child_id, parent=self, indent=int(len(self.indent) / 2) + 1) + def read_child(self): + child = Reader(self.file, parent=self, indent=int(len(self.indent) / 2) + 1) return child @@ -103,16 +102,4 @@ class Reader: return self.end_pos - self.file.tell() >= 8 - MAX_SIZE: int = 2147483647 - 8 - -''' -with open("/Users/will/Desktop/spacedoortest/spa1_prop_impdoor.msh", "rb") as tst_stream: - with Reader(tst_stream, "HEDR") as hedr: - print(hedr.peak_next_header()) -''' - - - - - diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index 6c6f7fd..53266be 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -44,6 +44,8 @@ class Scene: materials: Dict[str, Material] = field(default_factory=dict) models: List[Model] = field(default_factory=list) + skeleton: List[int] = field(default_factory=list) + def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str) -> Scene: """ Create a msh Scene from the active Blender scene. """ diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index 9faa78f..36657f8 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -16,7 +16,7 @@ def read_scene(input_file) -> Scene: scene.models = [] scene.materials = {} - with Reader(file=input_file, chunk_id="HEDR") as hedr: + with Reader(file=input_file) as hedr: while hedr.could_have_child(): @@ -24,7 +24,7 @@ def read_scene(input_file) -> Scene: if "MSH2" in next_header: - with hedr.read_child("MSH2") as msh2: + with hedr.read_child() as msh2: materials_list = [] @@ -33,26 +33,37 @@ def read_scene(input_file) -> Scene: next_header = msh2.peak_next_header() if "SINF" in next_header: - with msh2.read_child("SINF") as sinf: + with msh2.read_child() as sinf: pass elif "MATL" in next_header: - with msh2.read_child("MATL") as matl: + with msh2.read_child() as matl: materials_list += _read_matl_and_get_materials_list(matl) for i,mat in enumerate(materials_list): scene.materials[mat.name] = mat elif "MODL" in next_header: while ("MODL" in msh2.peak_next_header()): - with msh2.read_child("MODL") as modl: + with msh2.read_child() as modl: scene.models.append(_read_modl(modl, materials_list)) else: - with hedr.read_child("NULL") as unknown: + with hedr.read_child() as unknown: pass + elif "SKL2" in next_header: + with hedr.read_child() as skl2: + num_bones = skl2.read_u32() + scene.skeleton = [skl2.read_u32(5)[0] for i in range(num_bones)] + print("Skeleton models: ") + for crc_hash in scene.skeleton: + for model in scene.models: + if crc_hash == crc(model.name): + print("\t" + model.name + " with type: " + str(model.model_type)) + + else: - with hedr.read_child("NULL") as unknown: + with hedr.read_child() as unknown: pass return scene @@ -64,7 +75,7 @@ def _read_matl_and_get_materials_list(matl: Reader) -> List[Material]: num_mats = matl.read_u32() for _ in range(num_mats): - with matl.read_child("MATD") as matd: + with matl.read_child() as matd: materials_list.append(_read_matd(matd)) return materials_list @@ -80,36 +91,36 @@ def _read_matd(matd: Reader) -> Material: next_header = matd.peak_next_header() if "NAME" in next_header: - with matd.read_child("NAME") as name: + with matd.read_child() as name: mat.name = name.read_string() elif "DATA" in next_header: - with matd.read_child("DATA") as data: + with matd.read_child() as data: data.read_f32(4) # Diffuse Color (Seams to get ignored by modelmunge) mat.specular_color = data.read_f32(4) data.read_f32(4) # Ambient Color (Seams to get ignored by modelmunge and Zero(?)) data.read_f32() # Specular Exponent/Decay (Gets ignored by RedEngine in SWBFII for all known materials) elif "ATRB" in next_header: - with matd.read_child("ATRB") as atrb: + with matd.read_child() as atrb: mat.flags = atrb.read_u8() mat.rendertype = atrb.read_u8() mat.data = atrb.read_u8(2) elif "TX0D" in next_header: - with matd.read_child("TX0D") as tx0d: + with matd.read_child() as tx0d: mat.texture0 = tx0d.read_string() elif "TX1D" in next_header: - with matd.read_child("TX1D") as tx1d: + with matd.read_child() as tx1d: mat.texture1 = tx1d.read_string() elif "TX2D" in next_header: - with matd.read_child("TX2D") as tx2d: + with matd.read_child() as tx2d: mat.texture2 = tx2d.read_string() elif "TX3D" in next_header: - with matd.read_child("TX3D") as tx3d: + with matd.read_child() as tx3d: mat.texture3 = tx3d.read_string() else: @@ -127,50 +138,50 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: next_header = modl.peak_next_header() if "MTYP" in next_header: - with modl.read_child("MTYP") as mtyp: + with modl.read_child() as mtyp: model.model_type = ModelType(mtyp.read_u32()) elif "MNDX" in next_header: - with modl.read_child("MNDX") as mndx: + with modl.read_child() as mndx: pass elif "NAME" in next_header: - with modl.read_child("NAME") as name: + with modl.read_child() as name: model.name = name.read_string() elif "PRNT" in next_header: - with modl.read_child("PRNT") as prnt: + with modl.read_child() as prnt: model.parent = prnt.read_string() elif "FLGS" in next_header: - with modl.read_child("FLGS") as flgs: + with modl.read_child() as flgs: model.hidden = flgs.read_u32() elif "TRAN" in next_header: - with modl.read_child("TRAN") as tran: + with modl.read_child() as tran: model.transform = _read_tran(tran) elif "GEOM" in next_header: model.geometry = [] - with modl.read_child("GEOM") as geom: + with modl.read_child() as geom: next_header_modl = geom.peak_next_header() if "SEGM" in next_header_modl: - with geom.read_child("SEGM") as segm: + with geom.read_child() as segm: model.geometry.append(_read_segm(segm, materials_list)) elif "SWCI" in next_header: prim = CollisionPrimitive() - with modl.read_child("SWCI") as swci: - prim.shape.value = swci.read_u32() + with modl.read_child() as swci: + prim.shape = CollisionPrimitiveShape(swci.read_u32()) prim.radius = swci.read_f32() prim.height = swci.read_f32() prim.length = swci.read_f32() model.collisionprimitive = prim else: - with modl.read_child("NULL") as unknown: + with modl.read_child() as unknown: pass return model @@ -198,18 +209,18 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: next_header = segm.peak_next_header() if "MATI" in next_header: - with segm.read_child("MATI") as mati: + with segm.read_child() as mati: geometry_seg.material_name = materials_list[mati.read_u32()].name elif "POSL" in next_header: - with segm.read_child("POSL") as posl: + with segm.read_child() as posl: num_positions = posl.read_u32() for _ in range(num_positions): geometry_seg.positions.append(Vector(posl.read_f32(3))) elif "NRML" in next_header: - with segm.read_child("NRML") as nrml: + with segm.read_child() as nrml: num_normals = nrml.read_u32() for _ in range(num_positions): @@ -218,21 +229,21 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: elif "CLRL" in next_header: geometry_seg.colors = [] - with segm.read_child("CLRL") as clrl: + with segm.read_child() as clrl: num_colors = clrl.read_u32() for _ in range(num_colors): geometry_seg.colors += unpack_color(clrl.read_u32()) elif "UV0L" in next_header: - with segm.read_child("UV0L") as uv0l: + with segm.read_child() as uv0l: num_texcoords = uv0l.read_u32() for _ in range(num_texcoords): geometry_seg.texcoords.append(Vector(uv0l.read_f32(2))) elif "NDXL" in next_header: - with segm.read_child("NDXL") as ndxl: + with segm.read_child() as ndxl: num_polygons = ndxl.read_u32() for _ in range(num_polygons): @@ -240,23 +251,24 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: geometry_seg.polygons.append(polygon) elif "NDXT" in next_header: - with segm.read_child("NDXT") as ndxt: + with segm.read_child() as ndxt: num_tris = ndxt.read_u32() for _ in range(num_tris): geometry_seg.triangles.append(ndxt.read_u16(3)) elif "STRP" in next_header: - with segm.read_child("STRP") as strp: + with segm.read_child() as strp: pass if segm.read_u16 != 0: #trailing 0 bug https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP segm.skip_bytes(-2) else: - with segm.read_child("NULL") as unknown: + with segm.read_child() as unknown: pass return geometry_seg + diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index a5c3495..ac3e933 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -12,21 +12,18 @@ from .msh_model import * from .msh_model_utilities import * from .msh_utilities import * from .msh_model_gather import * +from .crc import * -def extract_models(models, materials_map): +def extract_models(scene: Scene, materials_map): model_map = {} - for model in sort_by_parent(models): + for model in sort_by_parent(scene.models): - if model.model_type != ModelType.STATIC: - new_obj = bpy.data.objects.new(model.name, None) - new_obj.empty_display_size = 1 - new_obj.empty_display_type = 'PLAIN_AXES' - - else: + if model.model_type == ModelType.STATIC: + new_mesh = bpy.data.meshes.new(model.name) verts = [] faces = [] @@ -67,10 +64,10 @@ def extract_models(models, materials_map): new_obj = bpy.data.objects.new(new_mesh.name, new_mesh) + ''' Assign Materials - will do per segment later... ''' - if mat_name: material = materials_map[mat_name] @@ -78,6 +75,12 @@ def extract_models(models, materials_map): new_obj.data.materials[0] = material else: new_obj.data.materials.append(material) + + else: + + new_obj = bpy.data.objects.new(model.name, None) + new_obj.empty_display_size = 1 + new_obj.empty_display_type = 'PLAIN_AXES' @@ -104,7 +107,7 @@ def extract_materials(scene: Scene) -> Dict[str,bpy.types.Material]: new_mat.use_nodes = True bsdf = new_mat.node_tree.nodes["Principled BSDF"] texImage = new_mat.node_tree.nodes.new('ShaderNodeTexImage') - texImage.image = bpy.data.images.load("/Users/will/Desktop/grad.png") + texImage.image = bpy.data.images.load("/Users/will/Desktop/grad.jpg") new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) extracted_materials[material_name] = new_mat @@ -114,9 +117,9 @@ def extract_materials(scene: Scene) -> Dict[str,bpy.types.Material]: def extract_scene(scene: Scene): - - matmap = extract_materials(scene) - extract_models(scene.models, matmap) + return None + #matmap = extract_materials(scene) + #extract_models(scene, matmap) From 20ad9a48d5a0660e6d56a32299d9c66a31ef85da Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Mon, 23 Nov 2020 10:15:22 -0500 Subject: [PATCH 14/39] ZE crash fixes --- addons/io_scene_swbf_msh/msh_anim_gather.py | 2 + addons/io_scene_swbf_msh/msh_model.py | 2 +- addons/io_scene_swbf_msh/msh_model_gather.py | 8 +++- addons/io_scene_swbf_msh/msh_scene_save.py | 44 ++++++++------------ 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 794d094..43e2363 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -27,6 +27,7 @@ def extract_anim(armature: bpy.types.Armature) -> Animation: anim.end_index = num_frames - 1 + anim.bone_transforms["DummyRoot"] = [] for bone in armature.data.bones: anim.bone_transforms[bone.name] = [] @@ -35,6 +36,7 @@ def extract_anim(armature: bpy.types.Armature) -> Animation: frame_time = framerange.x + frame * increment bpy.context.scene.frame_set(frame_time) + anim.bone_transforms["DummyRoot"].append(ModelTransform()) for bone in armature.pose.bones: transform = bone.matrix diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 10673de..fbf5dc8 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -83,7 +83,7 @@ class Animation: name: str = "fullanimation" bone_transforms: Dict[str, List[ModelTransform]] = field(default_factory=dict) - framerate: float = 10.0 + framerate: float = 29.97 start_index : int = 0 end_index : int = 0 diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 5550c44..dd1b8ba 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -36,6 +36,7 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool if obj.type == "ARMATURE": models_list += expand_armature(obj) + continue if skeleton_only: continue @@ -52,7 +53,10 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool transform = obj.parent.data.bones[obj.parent_bone].matrix_local.inverted() @ transform else: if obj.parent is not None: - model.parent = obj.parent.name + if obj.parent.type == "ARMATURE": + model.parent = obj.parent.parent.name + else: + model.parent = obj.parent.name local_translation, local_rotation, _ = transform.decompose() model.transform.rotation = convert_rotation_space(local_rotation) @@ -368,7 +372,7 @@ def expand_armature(obj: bpy.types.Object) -> List[Model]: transform = bone.parent.matrix_local.inverted() @ transform model.parent = bone.parent.name else: - model.parent = obj.name + model.parent = "tst_prop" local_translation, local_rotation, _ = transform.decompose() diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 685df7f..7dfb017 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -48,7 +48,7 @@ def save_scene(output_file, scene: Scene, is_animated: bool): def _write_sinf(sinf: Writer, scene: Scene): with sinf.create_child("NAME") as name: - name.write_string(scene.name) + name.write_string("spa1_prop_impdoor_rig") with sinf.create_child("FRAM") as fram: min_index = 0 @@ -58,8 +58,8 @@ def _write_sinf(sinf: Writer, scene: Scene): min_index = min([anim.start_index for anim in scene.anims]) max_index = min([anim.end_index for anim in scene.anims]) - fram.write_i32(min_index, max_index) - fram.write_f32(10.0) + fram.write_i32(0, 1) + fram.write_f32(29.97) with sinf.create_child("BBOX") as bbox: aabb = create_scene_aabb(scene) @@ -100,7 +100,7 @@ def _write_matd(matd: Writer, material_name: str, material: Material): data.write_f32(1.0, 1.0, 1.0, 1.0) # Diffuse Color (Seams to get ignored by modelmunge) data.write_f32(material.specular_color[0], material.specular_color[1], material.specular_color[2], 1.0) - data.write_f32(0.0, 0.0, 0.0, 1.0) # Ambient Color (Seams to get ignored by modelmunge and Zero(?)) + data.write_f32(1.0, 1.0, 1.0, 1.0) # Ambient Color (Seams to get ignored by modelmunge and Zero(?)) data.write_f32(50.0) # Specular Exponent/Decay (Gets ignored by RedEngine in SWBFII for all known materials) with matd.create_child("ATRB") as atrb: atrb.write_u8(material.flags.value) @@ -135,9 +135,13 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str with modl.create_child("PRNT") as prnt: prnt.write_string(model.parent) - if model.hidden: - with modl.create_child("FLGS") as flgs: - flgs.write_u32(1) + if model.model_type != ModelType.NULL and model.model_type != ModelType.BONE: + if model.hidden or "Dummy" in model.name or "hp_" in model.name: + with modl.create_child("FLGS") as flgs: + flgs.write_u32(1) + for seg in model.geometry: + seg.texcoords = None + with modl.create_child("TRAN") as tran: _write_tran(tran, model.transform) @@ -198,11 +202,12 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for color in segment.colors: clrl.write_u32(pack_color(color)) - with segm.create_child("UV0L") as uv0l: - uv0l.write_u32(len(segment.texcoords)) + if segment.texcoords is not None: + with segm.create_child("UV0L") as uv0l: + uv0l.write_u32(len(segment.texcoords)) - for texcoord in segment.texcoords: - uv0l.write_f32(texcoord.x, texcoord.y) + for texcoord in segment.texcoords: + uv0l.write_f32(texcoord.x, texcoord.y) with segm.create_child("NDXL") as ndxl: ndxl.write_u32(len(segment.polygons)) @@ -234,8 +239,6 @@ SKINNING CHUNKS def _write_wght(wght: Writer, weights: List[List[VertexWeight]]): wght.write_u32(len(weights)) - print("Writing WGHT: ") - for weight_list in weights: weight_list += [VertexWeight(0.0, 0)] * 4 weight_list = sorted(weight_list, key=lambda w: w.weight, reverse=True) @@ -243,24 +246,13 @@ def _write_wght(wght: Writer, weights: List[List[VertexWeight]]): total_weight = max(sum(map(lambda w: w.weight, weight_list)), 1e-5) - print_str = "" - for weight in weight_list: - - print_str += "({}, {}) ".format(weight.bone, weight.weight / total_weight) - - wght.write_i32(weight.bone) - wght.write_f32(weight.weight / total_weight) - - print(" {}".format(print_str)) + wght.write_i32(weight.bone) + wght.write_f32(weight.weight / total_weight) def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]): envl.write_u32(len(model.bone_map)) - - print("Writing ENVL: ") - for bone_name in model.bone_map: - print(" {:10} Index: {}".format(bone_name, model_index[bone_name])) envl.write_u32(model_index[bone_name]) ''' From 049803f750d96d802533663a12e5b673919b79c0 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Sun, 1 Nov 2020 18:11:26 -0500 Subject: [PATCH 15/39] Reader class simplified --- addons/io_scene_swbf_msh/__init__.py | 3 +- addons/io_scene_swbf_msh/msh_model.py | 4 +- addons/io_scene_swbf_msh/msh_scene_read.py | 84 ++++++++++++++++++---- addons/io_scene_swbf_msh/msh_to_blend.py | 44 ++++++++---- 4 files changed, 104 insertions(+), 31 deletions(-) diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index e432118..cab7f18 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -62,6 +62,7 @@ from .msh_scene_read import read_scene from .msh_material_properties import * from .msh_to_blend import * + class ExportMSH(Operator, ExportHelper): """ Export the current scene as a SWBF .msh file. """ @@ -134,7 +135,7 @@ class ImportMSH(Operator, ImportHelper): def execute(self, context): with open(self.filepath, 'rb') as input_file: - extract_scene(read_scene(input_file)) + extract_scene(self.filepath, read_scene(input_file)) return {'FINISHED'} def menu_func_import(self, context): diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 4f1e29b..c23c5c5 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -16,9 +16,9 @@ class ModelType(Enum): class CollisionPrimitiveShape(Enum): SPHERE = 0 - # ELLIPSOID = 1 + ELLIPSOID = 1 CYLINDER = 2 - # MESH = 3 + MESH = 3 BOX = 4 @dataclass diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index 36657f8..b774d5a 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -10,6 +10,8 @@ from .msh_utilities import * from .crc import * +envls = [] + def read_scene(input_file) -> Scene: scene = Scene() @@ -52,20 +54,27 @@ def read_scene(input_file) -> Scene: pass elif "SKL2" in next_header: - with hedr.read_child() as skl2: - num_bones = skl2.read_u32() - scene.skeleton = [skl2.read_u32(5)[0] for i in range(num_bones)] - print("Skeleton models: ") - for crc_hash in scene.skeleton: - for model in scene.models: - if crc_hash == crc(model.name): - print("\t" + model.name + " with type: " + str(model.model_type)) + with hedr.read_child() as skl2: + num_bones = skl2.read_u32() + scene.skeleton = [skl2.read_u32(5)[0] for i in range(num_bones)] + print("Skeleton models: ") + for crc_hash in scene.skeleton: + for model in scene.models: + if crc_hash == crc(model.name): + print("\t" + model.name + " with type: " + str(model.model_type)) + elif "ANM2" in next_header: + with hedr.read_child() as anm2: + _read_anm2(anm2, scene.models) else: - with hedr.read_child() as unknown: + with hedr.read_child() as null: pass + print("Models indexed by ENVLs: ") + for envl_index in set(envls): + print("\t" + scene.models[envl_index].name) + return scene @@ -164,12 +173,23 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: elif "GEOM" in next_header: model.geometry = [] with modl.read_child() as geom: + while geom.could_have_child(): + next_header_geom = geom.peak_next_header() - next_header_modl = geom.peak_next_header() + if "SEGM" in next_header_geom: + with geom.read_child() as segm: + model.geometry.append(_read_segm(segm, materials_list)) - if "SEGM" in next_header_modl: - with geom.read_child() as segm: - model.geometry.append(_read_segm(segm, materials_list)) + elif "ENVL" in next_header_geom: + with geom.read_child() as envl: + global envls + num_indicies = envl.read_u32() + print("reading ENVL with " + str(num_indicies) + " indicies") + envls += [envl.read_u32() for _ in range(num_indicies)] + + else: + with geom.read_child() as null: + pass elif "SWCI" in next_header: prim = CollisionPrimitive() @@ -191,7 +211,7 @@ def _read_tran(tran: Reader) -> ModelTransform: xform = ModelTransform() - tran.skip_bytes(4 * 3) #ignore scale + tran.skip_bytes(12) #ignore scale rot = tran.read_f32(4) xform.rotation = Quaternion((rot[3], rot[0], rot[1], rot[2])) @@ -264,6 +284,10 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: if segm.read_u16 != 0: #trailing 0 bug https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP segm.skip_bytes(-2) + elif "WGHT" in next_header: + with segm.read_child() as null: + pass + else: with segm.read_child() as unknown: pass @@ -272,3 +296,35 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: +def _read_anm2(anm2: Reader, models): + + hash_dict = {} + for model in models: + hash_dict[crc(model.name)] = model.name + + while anm2.could_have_child(): + + next_header = anm2.peak_next_header() + + if "CYCL" in next_header: + with anm2.read_child() as cycl: + pass + + elif "KFR3" in next_header: + with anm2.read_child() as kfr3: + + num_bones = kfr3.read_u32() + + for _ in range(num_bones): + + kfr3.read_u32() + + frametype = kfr3.read_u32() + + num_loc_frames = kfr3.read_u32() + num_rot_frames = kfr3.read_u32() + + kfr3.skip_bytes(16 * num_loc_frames + 20 * num_rot_frames) + + + diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index ac3e933..c2715ce 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -14,6 +14,8 @@ from .msh_utilities import * from .msh_model_gather import * from .crc import * +import os + def extract_models(scene: Scene, materials_map): @@ -21,9 +23,13 @@ def extract_models(scene: Scene, materials_map): model_map = {} for model in sort_by_parent(scene.models): + new_obj = None + + if model.name.startswith("p_") or "collision" in model.name: + continue + + if model.model_type == ModelType.STATIC or model.model_type == ModelType.SKIN: - if model.model_type == ModelType.STATIC: - new_mesh = bpy.data.meshes.new(model.name) verts = [] faces = [] @@ -42,10 +48,11 @@ def extract_models(scene: Scene, materials_map): offset += len(seg.positions) new_mesh.from_pydata(verts, [], faces) + new_mesh.update() new_mesh.validate() - + ''' edit_mesh = bmesh.new() edit_mesh.from_mesh(new_mesh) @@ -59,12 +66,12 @@ def extract_models(scene: Scene, materials_map): 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) - ''' Assign Materials - will do per segment later... ''' @@ -97,7 +104,7 @@ def extract_models(scene: Scene, materials_map): -def extract_materials(scene: Scene) -> Dict[str,bpy.types.Material]: +def extract_materials(folder_path: str, scene: Scene) -> Dict[str,bpy.types.Material]: extracted_materials = {} @@ -106,9 +113,16 @@ def extract_materials(scene: Scene) -> Dict[str,bpy.types.Material]: new_mat = bpy.data.materials.new(name=material_name) new_mat.use_nodes = True bsdf = new_mat.node_tree.nodes["Principled BSDF"] - texImage = new_mat.node_tree.nodes.new('ShaderNodeTexImage') - texImage.image = bpy.data.images.load("/Users/will/Desktop/grad.jpg") - new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) + + tex_path_def = os.path.join(folder_path, scene.materials[material_name].texture0) + tex_path_alt = os.path.join(folder_path, "PC", scene.materials[material_name].texture0) + + tex_path = tex_path_def if os.path.exists(tex_path_def) else tex_path_alt + + if os.path.exists(tex_path): + texImage = new_mat.node_tree.nodes.new('ShaderNodeTexImage') + texImage.image = bpy.data.images.load(tex_path) + new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) extracted_materials[material_name] = new_mat @@ -116,10 +130,12 @@ def extract_materials(scene: Scene) -> Dict[str,bpy.types.Material]: -def extract_scene(scene: Scene): - return None - #matmap = extract_materials(scene) - #extract_models(scene, matmap) +def extract_scene(filepath: str, scene: Scene): + + folder = os.path.join(os.path.dirname(filepath),"") + + matmap = extract_materials(folder,scene) + extract_models(scene, matmap) From 8273e01167d92a53e55ed186eb95225b2b4b3a70 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Wed, 25 Nov 2020 23:10:14 -0500 Subject: [PATCH 16/39] Correct skeleton imports, though what to do with re-parenting and skeleton impurities such as effector nulls still uncertain --- addons/io_scene_swbf_msh/msh_model.py | 4 +- .../io_scene_swbf_msh/msh_model_utilities.py | 10 ++ addons/io_scene_swbf_msh/msh_reader.py | 4 +- addons/io_scene_swbf_msh/msh_scene_read.py | 28 +++- addons/io_scene_swbf_msh/msh_to_blend.py | 150 ++++++++++++++++-- 5 files changed, 170 insertions(+), 26 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index c23c5c5..bb052eb 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -2,7 +2,7 @@ saved to a .msh file. """ from dataclasses import dataclass, field -from typing import List +from typing import List, Tuple from enum import Enum from mathutils import Vector, Quaternion @@ -44,6 +44,8 @@ class GeometrySegment: triangles: List[List[int]] = field(default_factory=list) triangle_strips: List[List[int]] = None + weights: List[List[Tuple[int, float]]] = None + @dataclass class CollisionPrimitive: """ Class representing a 'SWCI' section in a .msh file. """ diff --git a/addons/io_scene_swbf_msh/msh_model_utilities.py b/addons/io_scene_swbf_msh/msh_model_utilities.py index 1547697..98ae83e 100644 --- a/addons/io_scene_swbf_msh/msh_model_utilities.py +++ b/addons/io_scene_swbf_msh/msh_model_utilities.py @@ -5,6 +5,16 @@ from .msh_model import * from .msh_utilities import * from mathutils import Vector, Matrix + +def convert_vector_space_(vec: Vector) -> Vector: + return Vector((-vec.x, vec.z, vec.y)) + +def convert_rotation_space_(quat: Quaternion) -> Quaternion: + return Quaternion((-quat.w, quat.x, -quat.z, -quat.y)) + +def model_transform_to_matrix(transform: ModelTransform): + 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]): """ Scales are positions in the GeometrySegment list. """ diff --git a/addons/io_scene_swbf_msh/msh_reader.py b/addons/io_scene_swbf_msh/msh_reader.py index 3203955..f81a077 100644 --- a/addons/io_scene_swbf_msh/msh_reader.py +++ b/addons/io_scene_swbf_msh/msh_reader.py @@ -19,7 +19,7 @@ class Reader: padding_length = 4 - (self.size % 4) if self.size % 4 > 0 else 0 self.end_pos = self.size_pos + padding_length + self.size + 8 - print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) + #print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) return self @@ -28,7 +28,7 @@ class Reader: if self.size > self.MAX_SIZE: raise OverflowError(f".msh file overflowed max size. size = {self.size} MAX_SIZE = {self.MAX_SIZE}") - print(self.indent + "End " + self.header) + #print(self.indent + "End " + self.header) self.file.seek(self.end_pos) diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index b774d5a..b4d6462 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -57,11 +57,12 @@ def read_scene(input_file) -> Scene: with hedr.read_child() as skl2: num_bones = skl2.read_u32() scene.skeleton = [skl2.read_u32(5)[0] for i in range(num_bones)] - print("Skeleton models: ") + #print("Skeleton models: ") for crc_hash in scene.skeleton: for model in scene.models: if crc_hash == crc(model.name): - print("\t" + model.name + " with type: " + str(model.model_type)) + pass + #print("\t" + model.name + " with type: " + str(model.model_type)) elif "ANM2" in next_header: with hedr.read_child() as anm2: @@ -70,11 +71,7 @@ def read_scene(input_file) -> Scene: else: with hedr.read_child() as null: pass - - print("Models indexed by ENVLs: ") - for envl_index in set(envls): - print("\t" + scene.models[envl_index].name) - + return scene @@ -285,9 +282,24 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: segm.skip_bytes(-2) elif "WGHT" in next_header: - with segm.read_child() as null: + with segm.read_child() as wght: pass + ''' + geometry_seg.weights = [] + num_weights = wght.read_u32() + + for _ in range(num_weights): + weight_set = [] + for _ in range(4): + index = wght.read_u32() + value = wght.read_f32() + + if value > 0.000001: + weight_set.append((index,value)) + + geometry_seg.weights.append(weight_set) + ''' else: with segm.read_child() as unknown: pass diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index c2715ce..a768cc5 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -18,6 +18,102 @@ import os + +def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): + + 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) + armature_obj.select_set(True) + + bpy.context.view_layer.objects.active = armature_obj + bpy.ops.object.mode_set(mode='EDIT') + + for bone in refined_skeleton: + + edit_bone = armature.edit_bones.new(bone.name) + + if bone.parent: + edit_bone.parent = armature.edit_bones[bone.parent] + + edit_bone.head = model_map[bone.name].matrix_world.translation + + bone_children = [b for b in get_model_children(bone, refined_skeleton)] + + if len(bone_children) > 0: + edit_bone.tail = Vector((0.0,0.0,0.0)) + for bone_child in bone_children: + edit_bone.tail += model_map[bone_child.name].matrix_world.translation + edit_bone.tail = edit_bone.tail / len(bone_children) + else: + edit_bone.tail = model_map[bone.name].matrix_world @ Vector((-0.2,0.0,0.0)) + + + bpy.ops.object.mode_set(mode='OBJECT') + armature_obj.select_set(True) + bpy.context.view_layer.update() + + + + + + +def extract_refined_skeleton(scene: Scene): + + model_dict = {} + children_dict = {} + skeleton_models = [] + + for model in scene.models: + model_dict[model.name] = model + children_dict[model.name] = [] + + for model in scene.models: + if model.parent: + children_dict[model.parent].append(model) + + if crc(model.name) in scene.skeleton: + skeleton_models.append(model) + + refined_skeleton_models = [] + + for bone in skeleton_models: + + if bone.parent: + + curr_ancestor = model_dict[bone.parent] + stacked_transform = model_transform_to_matrix(bone.transform) + + while True: + + if crc(curr_ancestor.name) in scene.skeleton or "dummyroot" in curr_ancestor.name.lower(): + new_model = Model() + new_model.name = bone.name + new_model.parent = curr_ancestor.name if "dummyroot" not in curr_ancestor.name.lower() else "" + + loc, rot, _ = stacked_transform.decompose() + + new_model.transform.rotation = rot + new_model.transform.translation = loc + + refined_skeleton_models.append(new_model) + break + + else: + curr_ancestor = model_dict[curr_ancestor.parent] + stacked_transform = model_transform_to_matrix(curr_ancestor.transform) @ stacked_transform + + return sort_by_parent(refined_skeleton_models) + + + + + + + + + def extract_models(scene: Scene, materials_map): model_map = {} @@ -25,7 +121,13 @@ def extract_models(scene: Scene, materials_map): for model in sort_by_parent(scene.models): new_obj = None - if model.name.startswith("p_") or "collision" in model.name: + if "bone_l_toe" in model.name: + loc = get_model_world_matrix(model, scene.models).translation + print("World bone_l_toe: " + str(loc)) + + + + if model.name.startswith("p_") or "collision" in model.name or model.name.startswith("c_") or model.name.startswith("sv_"): continue if model.model_type == ModelType.STATIC or model.model_type == ModelType.SKIN: @@ -37,37 +139,47 @@ def extract_models(scene: Scene, materials_map): mat_name = "" + full_texcoords = [] + 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 seg.texcoords is not None: + full_texcoords += seg.texcoords + else: + full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))] + faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles] offset += len(seg.positions) new_mesh.from_pydata(verts, [], faces) - new_mesh.update() new_mesh.validate() - ''' - edit_mesh = bmesh.new() - edit_mesh.from_mesh(new_mesh) + + if len(full_texcoords) > 0: - uvlayer = edit_mesh.loops.layers.uv.verify() + edit_mesh = bmesh.new() + edit_mesh.from_mesh(new_mesh) - for edit_mesh_face in edit_mesh.faces: - mesh_face = faces[edit_mesh_face.index] + uvlayer = edit_mesh.loops.layers.uv.verify() - for i,loop in enumerate(edit_mesh_face.loops): - texcoord = seg.texcoords[mesh_face[i]] - loop[uvlayer].uv = tuple([texcoord.x, texcoord.y]) + for edit_mesh_face in edit_mesh.faces: + mesh_face = faces[edit_mesh_face.index] - edit_mesh.to_mesh(new_mesh) - edit_mesh.free() - ''' + for i,loop in enumerate(edit_mesh_face.loops): + + texcoord = full_texcoords[mesh_face[i]] + loop[uvlayer].uv = tuple([texcoord.x, texcoord.y]) + + edit_mesh.to_mesh(new_mesh) + edit_mesh.free() + new_obj = bpy.data.objects.new(new_mesh.name, new_mesh) @@ -102,6 +214,8 @@ def extract_models(scene: Scene, materials_map): bpy.context.collection.objects.link(new_obj) + return model_map + def extract_materials(folder_path: str, scene: Scene) -> Dict[str,bpy.types.Material]: @@ -135,7 +249,13 @@ def extract_scene(filepath: str, scene: Scene): folder = os.path.join(os.path.dirname(filepath),"") matmap = extract_materials(folder,scene) - extract_models(scene, matmap) + + model_map = extract_models(scene, matmap) + + + skel = extract_refined_skeleton(scene) + refined_skeleton_to_armature(skel, model_map) + From aa62fd47ea7d410c9e203f9b23a66e5dcd0bdb3d Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Sat, 28 Nov 2020 21:42:27 -0500 Subject: [PATCH 17/39] Vertex groups importing properly. ENVL indicies refer to each model's MNDX, which start from 1, not 0/don't necessarily refer to order in file... --- addons/io_scene_swbf_msh/msh_scene_read.py | 41 ++++++++++++++++++---- addons/io_scene_swbf_msh/msh_to_blend.py | 37 +++++++++++++------ 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index b4d6462..a2563f9 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -12,6 +12,8 @@ from .crc import * envls = [] +model_counter = 0 + def read_scene(input_file) -> Scene: scene = Scene() @@ -71,6 +73,11 @@ def read_scene(input_file) -> Scene: else: with hedr.read_child() as null: pass + + for envl in envls: + #print("Envelope: ") + for index in envl: + pass#print("\t" + scene.models[index].name) return scene @@ -149,7 +156,10 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: elif "MNDX" in next_header: with modl.read_child() as mndx: - pass + index = mndx.read_u32() + global model_counter + print("Encountered model {} with index {}".format(model_counter, index)) + model_counter += 1 elif "NAME" in next_header: with modl.read_child() as name: @@ -169,7 +179,10 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: elif "GEOM" in next_header: model.geometry = [] + envelope = [] + with modl.read_child() as geom: + while geom.could_have_child(): next_header_geom = geom.peak_next_header() @@ -179,14 +192,22 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: elif "ENVL" in next_header_geom: with geom.read_child() as envl: - global envls num_indicies = envl.read_u32() - print("reading ENVL with " + str(num_indicies) + " indicies") - envls += [envl.read_u32() for _ in range(num_indicies)] + envelope += [envl.read_u32() - 1 for _ in range(num_indicies)] else: with geom.read_child() as null: pass + if envelope: + global envls + envls.append(envelope) + + for seg in model.geometry: + if seg.weights: + for weight_set in seg.weights: + for i in range(len(weight_set)): + weight = weight_set[i] + weight_set[i] = (envelope[weight[0]], weight[1]) elif "SWCI" in next_header: prim = CollisionPrimitive() @@ -282,24 +303,30 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: segm.skip_bytes(-2) elif "WGHT" in next_header: + print("===================================") with segm.read_child() as wght: - pass - ''' + geometry_seg.weights = [] num_weights = wght.read_u32() for _ in range(num_weights): weight_set = [] + print_str = "" for _ in range(4): index = wght.read_u32() value = wght.read_f32() + print_str += "({}, {}) ".format(index,value) + if value > 0.000001: weight_set.append((index,value)) + #print(print_str) + geometry_seg.weights.append(weight_set) - ''' + print("===================================") + else: with segm.read_child() as unknown: pass diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index a768cc5..49be9f4 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -41,7 +41,7 @@ def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): bone_children = [b for b in get_model_children(bone, refined_skeleton)] - if len(bone_children) > 0: + if bone_children: edit_bone.tail = Vector((0.0,0.0,0.0)) for bone_child in bone_children: edit_bone.tail += model_map[bone_child.name].matrix_world.translation @@ -50,6 +50,7 @@ def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): edit_bone.tail = model_map[bone.name].matrix_world @ Vector((-0.2,0.0,0.0)) + bpy.ops.object.mode_set(mode='OBJECT') armature_obj.select_set(True) bpy.context.view_layer.update() @@ -121,12 +122,6 @@ def extract_models(scene: Scene, materials_map): for model in sort_by_parent(scene.models): new_obj = None - if "bone_l_toe" in model.name: - loc = get_model_world_matrix(model, scene.models).translation - print("World bone_l_toe: " + str(loc)) - - - if model.name.startswith("p_") or "collision" in model.name or model.name.startswith("c_") or model.name.startswith("sv_"): continue @@ -141,6 +136,8 @@ def extract_models(scene: Scene, materials_map): full_texcoords = [] + weights_offsets = {} + for i,seg in enumerate(model.geometry): if i == 0: @@ -148,6 +145,9 @@ def extract_models(scene: Scene, materials_map): verts += [tuple(convert_vector_space(v)) for v in seg.positions] + if seg.weights: + weights_offsets[offset] = seg.weights + if seg.texcoords is not None: full_texcoords += seg.texcoords else: @@ -162,7 +162,7 @@ def extract_models(scene: Scene, materials_map): new_mesh.validate() - if len(full_texcoords) > 0: + if full_texcoords: edit_mesh = bmesh.new() edit_mesh.from_mesh(new_mesh) @@ -180,10 +180,21 @@ def extract_models(scene: Scene, materials_map): edit_mesh.to_mesh(new_mesh) edit_mesh.free() - - new_obj = bpy.data.objects.new(new_mesh.name, new_mesh) + + for offset in weights_offsets: + vertex_groups_indicies = {} + for i, weight_set in enumerate(weights_offsets[offset]): + for weight in weight_set: + index = weight[0] + + if index not in vertex_groups_indicies: + model_name = scene.models[index].name + vertex_groups_indicies[index] = new_obj.vertex_groups.new(name=model_name) + + vertex_groups_indicies[index].add([offset + i], weight[1], 'ADD') + ''' Assign Materials - will do per segment later... ''' @@ -250,6 +261,12 @@ def extract_scene(filepath: str, scene: Scene): matmap = extract_materials(folder,scene) + if scene.skeleton: + #print("Skeleton models: ") + for model in scene.models: + if crc(model.name) in scene.skeleton: + pass#print("\tName: " + model.name + " Parent: " + model.parent) + model_map = extract_models(scene, matmap) From a83c74ebf7f02dd2e90fb19ec2ae86206f33c996 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Sun, 29 Nov 2020 15:45:32 -0500 Subject: [PATCH 18/39] Better anim abstraction --- addons/io_scene_swbf_msh/msh_anim_gather.py | 28 +++++++++++---------- addons/io_scene_swbf_msh/msh_model.py | 18 +++++++++++-- addons/io_scene_swbf_msh/msh_scene_save.py | 26 +++++++++---------- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 1da0003..f959a19 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -27,19 +27,23 @@ def extract_anim(armature: bpy.types.Armature) -> Animation: anim.end_index = num_frames - 1 - anim.bone_transforms["DummyRoot"] = [] + anim.bone_frames["DummyRoot"] = ([], []) for bone in armature.data.bones: - anim.bone_transforms[bone.name] = [] + anim.bone_frames[bone.name] = ([], []) for frame in range(num_frames): - - #if frame % 10 == 0: - # print("Sample frame {}:".format(frame)) - + frame_time = framerange.x + frame * increment bpy.context.scene.frame_set(frame_time) - anim.bone_transforms["DummyRoot"].append(ModelTransform()) + + rframe_dummy = RotationFrame(frame, convert_rotation_space(Quaternion())) + tframe_dummy = TranslationFrame(frame, Vector((0.0,0.0,0.0))) + + anim.bone_frames["DummyRoot"][0].append(tframe_dummy) + anim.bone_frames["DummyRoot"][1].append(rframe_dummy) + + for bone in armature.pose.bones: transform = bone.matrix @@ -49,13 +53,11 @@ def extract_anim(armature: bpy.types.Armature) -> Animation: loc, rot, _ = transform.decompose() - xform = ModelTransform() - xform.rotation = convert_rotation_space(rot) - xform.translation = convert_vector_space(loc) + rframe = RotationFrame(frame, convert_rotation_space(rot)) + tframe = TranslationFrame(frame, convert_vector_space(loc)) - #if frame % 10 == 0: - # print("\t{:10}: loc {:15} rot {:15}".format(bone.name, vec_to_str(xform.translation), quat_to_str(xform.rotation))) + anim.bone_frames[bone.name][0].append(tframe) + anim.bone_frames[bone.name][1].append(rframe) - anim.bone_transforms[bone.name].append(xform) return anim diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index fbf5dc8..af75efd 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -76,14 +76,28 @@ class Model: geometry: List[GeometrySegment] = None collisionprimitive: CollisionPrimitive = None + +@dataclass +class RotationFrame: + + index : int = 0 + rotation : Quaternion = field(default_factory=Quaternion) + + +@dataclass +class TranslationFrame: + + index : int = 0 + translation : Vector = field(default_factory=Vector) + + @dataclass class Animation: """ Class representing 'CYCL' + 'KFR3' sections in a .msh file """ name: str = "fullanimation" - bone_transforms: Dict[str, List[ModelTransform]] = field(default_factory=dict) + bone_frames: Dict[str, Tuple[List[TranslationFrame], List[RotationFrame]]] = field(default_factory=dict) framerate: float = 29.97 start_index : int = 0 end_index : int = 0 - diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 6507b17..ed7a75b 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -248,14 +248,14 @@ def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]): SKELETON CHUNKS ''' def _write_bln2(bln2: Writer, scene: Scene): - bones = scene.anims[0].bone_transforms.keys() + bones = scene.anims[0].bone_frames.keys() bln2.write_u32(len(bones)) for boneName in bones: bln2.write_u32(crc(boneName), 0) def _write_skl2(skl2: Writer, scene: Scene): - bones = scene.anims[0].bone_transforms.keys() + bones = scene.anims[0].bone_frames.keys() skl2.write_u32(len(bones)) for boneName in bones: @@ -282,22 +282,20 @@ def _write_anm2(anm2: Writer, anim: Animation): with anm2.create_child("KFR3") as kfr3: - kfr3.write_u32(len(anim.bone_transforms.keys())) + kfr3.write_u32(len(anim.bone_frames)) - for boneName in anim.bone_transforms.keys(): + for boneName in anim.bone_frames: kfr3.write_u32(crc(boneName)) kfr3.write_u32(0) #what is keyframe type? - num_frames = 1 + anim.end_index - anim.start_index - kfr3.write_u32(num_frames, num_frames) #basic testing - - for i, xform in enumerate(anim.bone_transforms[boneName]): - kfr3.write_u32(i) - kfr3.write_f32(xform.translation.x, xform.translation.y, xform.translation.z) - - for i, xform in enumerate(anim.bone_transforms[boneName]): - kfr3.write_u32(i) - kfr3.write_f32(xform.rotation.x, xform.rotation.y, xform.rotation.z, xform.rotation.w) + translation_frames, rotation_frames = anim.bone_frames[boneName] + kfr3.write_u32(len(translation_frames), len(rotation_frames)) + for frame in translation_frames: + kfr3.write_u32(frame.index) + kfr3.write_f32(frame.translation.x, frame.translation.y, frame.translation.z) + for frame in rotation_frames: + kfr3.write_u32(frame.index) + kfr3.write_f32(frame.rotation.x, frame.rotation.y, frame.rotation.z, frame.rotation.w) From 791a033d087c43b38504be0bb5c54210d803afa5 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Sun, 29 Nov 2020 18:32:17 -0500 Subject: [PATCH 19/39] DummyRoot data injection for ZA, matrix_local-with-bone-parent bug workaround, ENVL indicies start from one (fixing ZE bug), auto transform application for children of bones in a skeleton used for skinning done but commented out for further decision --- addons/io_scene_swbf_msh/msh_anim_gather.py | 8 +++- addons/io_scene_swbf_msh/msh_model_gather.py | 45 ++++++++++++++----- .../io_scene_swbf_msh/msh_model_utilities.py | 20 +++++++++ addons/io_scene_swbf_msh/msh_scene.py | 4 ++ addons/io_scene_swbf_msh/msh_scene_save.py | 25 +++-------- addons/io_scene_swbf_msh/msh_utilities.py | 7 +++ 6 files changed, 79 insertions(+), 30 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 43e2363..1da0003 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -32,7 +32,10 @@ def extract_anim(armature: bpy.types.Armature) -> Animation: anim.bone_transforms[bone.name] = [] for frame in range(num_frames): - + + #if frame % 10 == 0: + # print("Sample frame {}:".format(frame)) + frame_time = framerange.x + frame * increment bpy.context.scene.frame_set(frame_time) @@ -50,6 +53,9 @@ def extract_anim(armature: bpy.types.Armature) -> Animation: xform.rotation = convert_rotation_space(rot) xform.translation = convert_vector_space(loc) + #if frame % 10 == 0: + # print("\t{:10}: loc {:15} rot {:15}".format(bone.name, vec_to_str(xform.translation), quat_to_str(xform.rotation))) + anim.bone_transforms[bone.name].append(xform) return anim diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index dd1b8ba..ea1fe03 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -38,23 +38,37 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool models_list += expand_armature(obj) continue - if skeleton_only: - continue - model = Model() model.name = obj.name - model.model_type = get_model_type(obj) + model.model_type = get_model_type(obj, skeleton_only) model.hidden = get_is_model_hidden(obj) transform = obj.matrix_local + transform_reset = Matrix.Identity(4) if obj.parent_bone: model.parent = obj.parent_bone - transform = obj.parent.data.bones[obj.parent_bone].matrix_local.inverted() @ transform + + # matrix_local, when called on an armature child also parented to a bone, appears to be broken. + # At the very least, the results contradict the docs... + armature_relative_transform = obj.parent.matrix_world.inverted() @ obj.matrix_world + bone_relative_transform = obj.parent.data.bones[obj.parent_bone].matrix_local.inverted() @ armature_relative_transform + + transform = bone_relative_transform + + ''' + # Since the transforms of direct bone children are discarded by ZEngine (but not ZEditor), we apply the transform + # before geometry extraction, then apply the inversion after. + if obj.type in MESH_OBJECT_TYPES: + obj.data.transform(bone_relative_transform) + transform_reset = bone_relative_transform.inverted() + transform = Matrix.Identity(4) + ''' else: if obj.parent is not None: if obj.parent.type == "ARMATURE": model.parent = obj.parent.parent.name + transform = obj.parent.matrix_local @ transform else: model.parent = obj.parent.name @@ -63,6 +77,7 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool model.transform.translation = convert_vector_space(local_translation) if obj.type in MESH_OBJECT_TYPES: + mesh = obj.to_mesh() model.geometry = create_mesh_geometry(mesh, obj.vertex_groups) obj.to_mesh_clear() @@ -70,21 +85,25 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool _, _, world_scale = obj.matrix_world.decompose() world_scale = convert_scale_space(world_scale) scale_segments(world_scale, model.geometry) - + for segment in model.geometry: if len(segment.positions) > MAX_MSH_VERTEX_COUNT: raise RuntimeError(f"Object '{obj.name}' has resulted in a .msh geometry segment that has " f"more than {MAX_MSH_VERTEX_COUNT} vertices! Split the object's mesh up " f"and try again!") + if obj.vertex_groups: + model.bone_map = [group.name for group in obj.vertex_groups] + + obj.data.transform(transform_reset) + if get_is_collision_primitive(obj): model.collisionprimitive = get_collision_primitive(obj) - if obj.vertex_groups: - model.bone_map = [group.name for group in obj.vertex_groups] models_list.append(model) + return models_list @@ -215,10 +234,10 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet return segments -def get_model_type(obj: bpy.types.Object) -> ModelType: +def get_model_type(obj: bpy.types.Object, skel_only: bool) -> ModelType: """ Get the ModelType for a Blender object. """ - if obj.type in MESH_OBJECT_TYPES: + if obj.type in MESH_OBJECT_TYPES and not skel_only: if obj.vertex_groups: return ModelType.SKIN else: @@ -372,7 +391,11 @@ def expand_armature(obj: bpy.types.Object) -> List[Model]: transform = bone.parent.matrix_local.inverted() @ transform model.parent = bone.parent.name else: - model.parent = "tst_prop" + model.parent = obj.parent.name + for child_obj in obj.children: + if child_obj.vertex_groups and not get_is_model_hidden(obj) and not obj.parent_bone: + model.parent = child_obj.name + local_translation, local_rotation, _ = transform.decompose() diff --git a/addons/io_scene_swbf_msh/msh_model_utilities.py b/addons/io_scene_swbf_msh/msh_model_utilities.py index b0f7c49..eec1450 100644 --- a/addons/io_scene_swbf_msh/msh_model_utilities.py +++ b/addons/io_scene_swbf_msh/msh_model_utilities.py @@ -7,6 +7,26 @@ import mathutils import math from mathutils import Vector, Matrix + +def inject_dummy_data(model : Model): + """ Adds a triangle and material to the scene root when exporting skeletons to satisfy ZenAsset. """ + model.hidden = True + + dummy_seg = GeometrySegment() + dummy_seg.material_name = "" + + dummy_seg.positions = [Vector((0.0,0.1,0.0)), Vector((0.1,0.0,0.0)), Vector((0.0,0.0,0.1))] + dummy_seg.normals = [Vector((0.0,1.0,0.0)), Vector((1.0,0.0,0.0)), Vector((0.0,0.0,1.0))] + dummy_seg.texcoords = [Vector((0.1,0.1)), Vector((0.2,0.2)), Vector((0.3,0.3))] + tri = [[0,1,2]] + dummy_seg.triangles = tri + dummy_seg.polygons = tri + dummy_seg.triangle_strips = tri + + model.geometry = [dummy_seg] + model.model_type = ModelType.STATIC + + def scale_segments(scale: Vector, segments: List[GeometrySegment]): """ Scales are positions in the GeometrySegment list. """ diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index 8a813ed..2f37f9a 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -76,6 +76,10 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t if "Armature" in bpy.context.scene.objects.keys(): scene.anims = [extract_anim(bpy.context.scene.objects["Armature"])] + root = scene.models[0] + if skel_only and root.model_type == ModelType.NULL: + inject_dummy_data(root) + return scene diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 7dfb017..6507b17 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -19,7 +19,7 @@ def save_scene(output_file, scene: Scene, is_animated: bool): with msh2.create_child("SINF") as sinf: _write_sinf(sinf, scene) - model_index: Dict[str, int] = {model.name:i for i, model in enumerate(scene.models)} + model_index: Dict[str, int] = {model.name:(i+1) for i, model in enumerate(scene.models)} material_index: Dict[str, int] = {} with msh2.create_child("MATL") as matl: @@ -27,7 +27,7 @@ def save_scene(output_file, scene: Scene, is_animated: bool): for index, model in enumerate(scene.models): - print(model.name) + #print("Name: {:.10}, Pos: {:15}, Rot: {:15}, Parent: {}".format(model.name, vec_to_str(model.transform.translation), quat_to_str(model.transform.rotation), model.parent)) with msh2.create_child("MODL") as modl: _write_modl(modl, model, index, material_index, model_index) @@ -48,16 +48,9 @@ def save_scene(output_file, scene: Scene, is_animated: bool): def _write_sinf(sinf: Writer, scene: Scene): with sinf.create_child("NAME") as name: - name.write_string("spa1_prop_impdoor_rig") + name.write_string(scene.name) with sinf.create_child("FRAM") as fram: - min_index = 0 - max_index = 1 - - if scene.anims and len(scene.anims) > 0: - min_index = min([anim.start_index for anim in scene.anims]) - max_index = min([anim.end_index for anim in scene.anims]) - fram.write_i32(0, 1) fram.write_f32(29.97) @@ -126,7 +119,7 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str mtyp.write_u32(model.model_type.value) with modl.create_child("MNDX") as mndx: - mndx.write_u32(index) + mndx.write_u32(index + 1) with modl.create_child("NAME") as name: name.write_string(model.name) @@ -135,13 +128,9 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str with modl.create_child("PRNT") as prnt: prnt.write_string(model.parent) - if model.model_type != ModelType.NULL and model.model_type != ModelType.BONE: - if model.hidden or "Dummy" in model.name or "hp_" in model.name: - with modl.create_child("FLGS") as flgs: - flgs.write_u32(1) - for seg in model.geometry: - seg.texcoords = None - + if model.hidden: + with modl.create_child("FLGS") as flgs: + flgs.write_u32(1) with modl.create_child("TRAN") as tran: _write_tran(tran, model.transform) diff --git a/addons/io_scene_swbf_msh/msh_utilities.py b/addons/io_scene_swbf_msh/msh_utilities.py index 7715383..5a3fa6e 100644 --- a/addons/io_scene_swbf_msh/msh_utilities.py +++ b/addons/io_scene_swbf_msh/msh_utilities.py @@ -2,6 +2,13 @@ from mathutils import Vector + +def vec_to_str(vec): + return "({:.4},{:.4},{:.4})".format(vec.x,vec.y,vec.z) + +def quat_to_str(quat): + return "({:.4},{:.4},{:.4},{:.4})".format(quat.x, quat.y, quat.z, quat.w) + def add_vec(l: Vector, r: Vector) -> Vector: return Vector(v0 + v1 for v0, v1 in zip(l, r)) From 7db0591cc0f4c0b64ea37fc0809f5e5532ca7d82 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Sun, 22 Nov 2020 00:24:20 -0500 Subject: [PATCH 20/39] Armature extraction more robust (looks at skinning requirements, not just SKL2), automatic skin-skeleton reparenting, gave bone tails default epsilon position --- addons/io_scene_swbf_msh/msh_scene_read.py | 45 ++++----- addons/io_scene_swbf_msh/msh_to_blend.py | 104 ++++++++++++++++----- 2 files changed, 96 insertions(+), 53 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index a2563f9..2ebf4d1 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -10,16 +10,18 @@ from .msh_utilities import * from .crc import * -envls = [] - model_counter = 0 + def read_scene(input_file) -> Scene: scene = Scene() scene.models = [] scene.materials = {} + global model_counter + model_counter = 0 + with Reader(file=input_file) as hedr: while hedr.could_have_child(): @@ -59,13 +61,7 @@ def read_scene(input_file) -> Scene: with hedr.read_child() as skl2: num_bones = skl2.read_u32() scene.skeleton = [skl2.read_u32(5)[0] for i in range(num_bones)] - #print("Skeleton models: ") - for crc_hash in scene.skeleton: - for model in scene.models: - if crc_hash == crc(model.name): - pass - #print("\t" + model.name + " with type: " + str(model.model_type)) - + elif "ANM2" in next_header: with hedr.read_child() as anm2: _read_anm2(anm2, scene.models) @@ -74,10 +70,12 @@ def read_scene(input_file) -> Scene: with hedr.read_child() as null: pass - for envl in envls: - #print("Envelope: ") - for index in envl: - pass#print("\t" + scene.models[index].name) + if scene.skeleton: + print("Skeleton models: ") + for model in scene.models: + if crc(model.name) in scene.skeleton: + print("\t" + model.name) + return scene @@ -156,9 +154,9 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: elif "MNDX" in next_header: with modl.read_child() as mndx: - index = mndx.read_u32() global model_counter - print("Encountered model {} with index {}".format(model_counter, index)) + if mndx.read_u32() - 1 != model_counter: + print("MODEL INDEX DIDNT MATCH COUNTER!") model_counter += 1 elif "NAME" in next_header: @@ -198,9 +196,6 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: else: with geom.read_child() as null: pass - if envelope: - global envls - envls.append(envelope) for seg in model.geometry: if seg.weights: @@ -219,9 +214,11 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: model.collisionprimitive = prim else: - with modl.read_child() as unknown: + with modl.read_child() as null: pass + print("Reading model " + model.name + " of type: " + str(model.model_type)[10:]) + return model @@ -303,32 +300,24 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: segm.skip_bytes(-2) elif "WGHT" in next_header: - print("===================================") with segm.read_child() as wght: geometry_seg.weights = [] - num_weights = wght.read_u32() for _ in range(num_weights): weight_set = [] - print_str = "" for _ in range(4): index = wght.read_u32() value = wght.read_f32() - print_str += "({}, {}) ".format(index,value) - if value > 0.000001: weight_set.append((index,value)) - #print(print_str) - geometry_seg.weights.append(weight_set) - print("===================================") else: - with segm.read_child() as unknown: + with segm.read_child() as null: pass return geometry_seg diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index 49be9f4..dc1ed09 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -18,6 +18,10 @@ import os +#def import_anim(scene : Scene): + + + def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): @@ -42,19 +46,20 @@ def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): bone_children = [b for b in get_model_children(bone, refined_skeleton)] if bone_children: - edit_bone.tail = Vector((0.0,0.0,0.0)) + edit_bone.tail = Vector((-0.00001,0.0,0.0)) for bone_child in bone_children: edit_bone.tail += model_map[bone_child.name].matrix_world.translation - edit_bone.tail = edit_bone.tail / len(bone_children) + edit_bone.tail = edit_bone.tail / len(bone_children) else: edit_bone.tail = model_map[bone.name].matrix_world @ Vector((-0.2,0.0,0.0)) - bpy.ops.object.mode_set(mode='OBJECT') armature_obj.select_set(True) bpy.context.view_layer.update() + return armature_obj + @@ -63,20 +68,28 @@ def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): def extract_refined_skeleton(scene: Scene): model_dict = {} - children_dict = {} skeleton_models = [] + for model in scene.models: model_dict[model.name] = model - children_dict[model.name] = [] + if 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[0]] + + if crc(model_weighted_to.name) not in scene.skeleton: + scene.skeleton.append(crc(model_weighted_to.name)) + print("Found additional bone: " + model_weighted_to.name) + for model in scene.models: - if model.parent: - children_dict[model.parent].append(model) - if crc(model.name) in scene.skeleton: skeleton_models.append(model) + refined_skeleton_models = [] for bone in skeleton_models: @@ -88,10 +101,11 @@ def extract_refined_skeleton(scene: Scene): while True: - if crc(curr_ancestor.name) in scene.skeleton or "dummyroot" in curr_ancestor.name.lower(): + if crc(curr_ancestor.name) in scene.skeleton or curr_ancestor.name == scene.models[0].name: new_model = Model() new_model.name = bone.name - new_model.parent = curr_ancestor.name if "dummyroot" not in curr_ancestor.name.lower() else "" + print("Adding {} to refined skeleton...".format(bone.name)) + new_model.parent = curr_ancestor.name if curr_ancestor.name != scene.models[0].name else "" loc, rot, _ = stacked_transform.decompose() @@ -122,9 +136,6 @@ def extract_models(scene: Scene, materials_map): for model in sort_by_parent(scene.models): new_obj = None - if model.name.startswith("p_") or "collision" in model.name or model.name.startswith("c_") or model.name.startswith("sv_"): - continue - if model.model_type == ModelType.STATIC or model.model_type == ModelType.SKIN: new_mesh = bpy.data.meshes.new(model.name) @@ -183,8 +194,9 @@ def extract_models(scene: Scene, materials_map): new_obj = bpy.data.objects.new(new_mesh.name, new_mesh) + vertex_groups_indicies = {} + for offset in weights_offsets: - vertex_groups_indicies = {} for i, weight_set in enumerate(weights_offsets[offset]): for weight in weight_set: index = weight[0] @@ -213,7 +225,6 @@ def extract_models(scene: Scene, materials_map): new_obj.empty_display_type = 'PLAIN_AXES' - model_map[model.name] = new_obj if model.parent: @@ -225,6 +236,7 @@ def extract_models(scene: Scene, materials_map): bpy.context.collection.objects.link(new_obj) + return model_map @@ -258,20 +270,62 @@ def extract_materials(folder_path: str, scene: Scene) -> Dict[str,bpy.types.Mate def extract_scene(filepath: str, scene: Scene): folder = os.path.join(os.path.dirname(filepath),"") - - matmap = extract_materials(folder,scene) - - if scene.skeleton: - #print("Skeleton models: ") - for model in scene.models: - if crc(model.name) in scene.skeleton: - pass#print("\tName: " + model.name + " Parent: " + model.parent) + matmap = extract_materials(folder, scene) model_map = extract_models(scene, matmap) - skel = extract_refined_skeleton(scene) - refined_skeleton_to_armature(skel, model_map) + armature = refined_skeleton_to_armature(skel, model_map) + + for model in scene.models: + reparent_obj = None + if model.model_type == ModelType.SKIN: + + if model.parent: + reparent_obj = model_map[model.parent] + + skin_obj = model_map[model.name] + skin_obj.select_set(True) + armature.select_set(True) + bpy.context.view_layer.objects.active = armature + + bpy.ops.object.parent_clear(type='CLEAR') + bpy.ops.object.parent_set(type='ARMATURE') + + skin_obj.select_set(False) + armature.select_set(False) + bpy.context.view_layer.objects.active = None + + if reparent_obj is not None: + armature.select_set(True) + reparent_obj.select_set(True) + bpy.context.view_layer.objects.active = reparent_obj + bpy.ops.object.parent_set(type='OBJECT') + + armature.select_set(False) + reparent_obj.select_set(False) + bpy.context.view_layer.objects.active = None + + + if armature is not None: + for bone in armature.data.bones: + model_map[bone.name].select_set(True) + bpy.ops.object.delete() + + for model in scene.models: + if model.name in bpy.data.objects: + if model.hidden and len(bpy.data.objects[model.name].children) == 0: + bpy.data.objects[model.name].hide_set(True) + + + + + + + + + + From 617118bfd80291392185246ba086d28088295d58 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Sun, 29 Nov 2020 20:10:16 -0500 Subject: [PATCH 21/39] Name agnostic armature handling, better export options --- addons/io_scene_swbf_msh/__init__.py | 16 +++++------ addons/io_scene_swbf_msh/msh_anim_gather.py | 8 +++--- addons/io_scene_swbf_msh/msh_model_gather.py | 22 ++++----------- addons/io_scene_swbf_msh/msh_scene.py | 17 ++++++++---- addons/io_scene_swbf_msh/msh_scene_save.py | 29 ++++++++++---------- 5 files changed, 43 insertions(+), 49 deletions(-) diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index 8b97f68..69edf37 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -97,15 +97,15 @@ class ExportMSH(Operator, ExportHelper): default=True ) - export_animated: BoolProperty( - name="Export Animated Object", - description="Always check if the object will be animated.", + export_with_animation: BoolProperty( + name="Export With Animation", + description="Includes animation data extracted from the action currently set on armature.", default=False ) - export_skeleton_only: BoolProperty( - name="Export Skeleton", - description="Check if you intend to export skeleton data only.", + export_as_skeleton: BoolProperty( + name="Export Objects As Skeleton", + description="Check if you intend to export skeleton data for consumption by ZenAsset.", default=False ) @@ -119,9 +119,9 @@ class ExportMSH(Operator, ExportHelper): generate_triangle_strips=self.generate_triangle_strips, apply_modifiers=self.apply_modifiers, export_target=self.export_target, - skel_only=self.export_skeleton_only + skel_only=self.export_as_skeleton, + export_anim=self.export_with_animation ), - is_animated=self.export_animated ) return {'FINISHED'} diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index f959a19..0129587 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -12,7 +12,7 @@ from .msh_utilities import * from .msh_model_gather import * -def extract_anim(armature: bpy.types.Armature) -> Animation: +def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: action = armature.animation_data.action anim = Animation(); @@ -27,7 +27,7 @@ def extract_anim(armature: bpy.types.Armature) -> Animation: anim.end_index = num_frames - 1 - anim.bone_frames["DummyRoot"] = ([], []) + anim.bone_frames[root_name] = ([], []) for bone in armature.data.bones: anim.bone_frames[bone.name] = ([], []) @@ -40,8 +40,8 @@ def extract_anim(armature: bpy.types.Armature) -> Animation: rframe_dummy = RotationFrame(frame, convert_rotation_space(Quaternion())) tframe_dummy = TranslationFrame(frame, Vector((0.0,0.0,0.0))) - anim.bone_frames["DummyRoot"][0].append(tframe_dummy) - anim.bone_frames["DummyRoot"][1].append(rframe_dummy) + anim.bone_frames[root_name][0].append(tframe_dummy) + anim.bone_frames[root_name][1].append(rframe_dummy) for bone in armature.pose.bones: diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index ea1fe03..b0a6df8 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -14,7 +14,7 @@ SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"} MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"} MAX_MSH_VERTEX_COUNT = 32767 -def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool) -> List[Model]: +def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool) -> Tuple[List[Model], bpy.types.Object]: """ Gathers the Blender objects from the current scene and returns them as a list of Model objects. """ @@ -23,6 +23,8 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool models_list: List[Model] = [] + armature_found = None + for uneval_obj in select_objects(export_target): if uneval_obj.type in SKIPPED_OBJECT_TYPES and uneval_obj.name not in parents: continue @@ -36,6 +38,7 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool if obj.type == "ARMATURE": models_list += expand_armature(obj) + armature_found = obj continue model = Model() @@ -44,7 +47,6 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool model.hidden = get_is_model_hidden(obj) transform = obj.matrix_local - transform_reset = Matrix.Identity(4) if obj.parent_bone: model.parent = obj.parent_bone @@ -52,18 +54,8 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool # matrix_local, when called on an armature child also parented to a bone, appears to be broken. # At the very least, the results contradict the docs... armature_relative_transform = obj.parent.matrix_world.inverted() @ obj.matrix_world - bone_relative_transform = obj.parent.data.bones[obj.parent_bone].matrix_local.inverted() @ armature_relative_transform + transform = obj.parent.data.bones[obj.parent_bone].matrix_local.inverted() @ armature_relative_transform - transform = bone_relative_transform - - ''' - # Since the transforms of direct bone children are discarded by ZEngine (but not ZEditor), we apply the transform - # before geometry extraction, then apply the inversion after. - if obj.type in MESH_OBJECT_TYPES: - obj.data.transform(bone_relative_transform) - transform_reset = bone_relative_transform.inverted() - transform = Matrix.Identity(4) - ''' else: if obj.parent is not None: if obj.parent.type == "ARMATURE": @@ -94,8 +86,6 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool if obj.vertex_groups: model.bone_map = [group.name for group in obj.vertex_groups] - obj.data.transform(transform_reset) - if get_is_collision_primitive(obj): model.collisionprimitive = get_collision_primitive(obj) @@ -104,7 +94,7 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool models_list.append(model) - return models_list + return (models_list, armature_found) def create_parents_set() -> Set[str]: diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index 2f37f9a..090a3f4 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -44,10 +44,10 @@ class Scene: name: str = "Scene" materials: Dict[str, Material] = field(default_factory=dict) models: List[Model] = field(default_factory=list) - anims: List[Animation] = field(default_factory=list) + animation: Animation = None -def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool) -> Scene: +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() @@ -56,7 +56,7 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t scene.materials = gather_materials() - scene.models = gather_models(apply_modifiers=apply_modifiers, export_target=export_target, skeleton_only=skel_only) + 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: @@ -72,12 +72,17 @@ def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_t scene.materials = remove_unused_materials(scene.materials, scene.models) - #creates a dummy basepose if no Action is selected - if "Armature" in bpy.context.scene.objects.keys(): - scene.anims = [extract_anim(bpy.context.scene.objects["Armature"])] 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 diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index ed7a75b..f35e5c9 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -10,7 +10,7 @@ from .msh_utilities import * from .crc import * -def save_scene(output_file, scene: Scene, is_animated: bool): +def save_scene(output_file, scene: Scene): """ Saves scene to the supplied file. """ with Writer(file=output_file, chunk_id="HEDR") as hedr: @@ -32,16 +32,15 @@ def save_scene(output_file, scene: Scene, is_animated: bool): with msh2.create_child("MODL") as modl: _write_modl(modl, model, index, material_index, model_index) - if is_animated: + if scene.animation is not None: with hedr.create_child("SKL2") as skl2: - _write_skl2(skl2, scene) + _write_skl2(skl2, scene.animation) with hedr.create_child("BLN2") as bln2: - _write_bln2(bln2, scene) + _write_bln2(bln2, scene.animation) - with hedr.create_child("ANM2") as anm2: #simple for now - for anim in scene.anims: - _write_anm2(anm2, anim) + with hedr.create_child("ANM2") as anm2: #simple for now + _write_anm2(anm2, scene.animation) with hedr.create_child("CL1L"): pass @@ -247,23 +246,23 @@ def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]): ''' SKELETON CHUNKS ''' -def _write_bln2(bln2: Writer, scene: Scene): - bones = scene.anims[0].bone_frames.keys() +def _write_bln2(bln2: Writer, anim: Animation): + bones = anim.bone_frames.keys() bln2.write_u32(len(bones)) for boneName in bones: bln2.write_u32(crc(boneName), 0) -def _write_skl2(skl2: Writer, scene: Scene): - bones = scene.anims[0].bone_frames.keys() - skl2.write_u32(len(bones)) +def _write_skl2(skl2: Writer, anim: Animation): + bones = anim.bone_frames.keys() + skl2.write_u32(len(bones)) for boneName in bones: - skl2.write_u32(crc(boneName), 0) #default values - skl2.write_f32(1.0, 0.0, 0.0) #from docs + skl2.write_u32(crc(boneName), 0) #default values from docs + skl2.write_f32(1.0, 0.0, 0.0) ''' -ANIMATION CHUNK +ANIMATION CHUNKS ''' def _write_anm2(anm2: Writer, anim: Animation): From 440a3e7300ff7ce7792c58250b2f3ad3a044e88d Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Thu, 3 Dec 2020 23:15:25 -0500 Subject: [PATCH 22/39] MSH reader no longer reads up to the next multiple of 4, since files exported with ZETools seem not to abide by that rule. It doesn't matter if files are exported with this behavior, since the reading code now skips bytes until a known header is found, instead of just reading the next header blindly. Teancum's highsinger and xizor now importing just fine. --- addons/io_scene_swbf_msh/msh_reader.py | 4 +--- addons/io_scene_swbf_msh/msh_to_blend.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_reader.py b/addons/io_scene_swbf_msh/msh_reader.py index c9941bf..25528f9 100644 --- a/addons/io_scene_swbf_msh/msh_reader.py +++ b/addons/io_scene_swbf_msh/msh_reader.py @@ -16,9 +16,7 @@ class Reader: self.size_pos = self.file.tell() self.header = self.read_bytes(4).decode("utf-8") self.size = self.read_u32() - - padding_length = 4 - (self.size % 4) if self.size % 4 > 0 else 0 - self.end_pos = self.size_pos + padding_length + self.size + 8 + self.end_pos = self.size_pos + self.size + 8 if self.debug: print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index 0492eec..9f530ea 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -83,7 +83,6 @@ def extract_refined_skeleton(scene: Scene): if crc(model_weighted_to.name) not in scene.skeleton: scene.skeleton.append(crc(model_weighted_to.name)) - print("Found additional bone: " + model_weighted_to.name) for model in scene.models: if crc(model.name) in scene.skeleton: @@ -170,7 +169,6 @@ def extract_models(scene: Scene, materials_map): for strip in seg.triangle_strips: for i in range(len(strip) - 2): face = tuple([offset + strip[j] for j in range(i,i+3)]) - print("strip face: " + str(face)) faces.append(face) offset += len(seg.positions) From 30bf326b9e2ec1401d08895ee80a73a1176baac7 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Fri, 4 Dec 2020 01:22:58 -0500 Subject: [PATCH 23/39] Awfully slow (need to abandon bpy.ops) but correct bone parenting --- addons/io_scene_swbf_msh/msh_scene_read.py | 2 + addons/io_scene_swbf_msh/msh_to_blend.py | 61 +++++++++++++++++++--- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index 6bdcfc7..ab8d274 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -254,6 +254,8 @@ def _read_tran(tran: Reader) -> ModelTransform: xform.rotation = Quaternion((rot[3], rot[0], rot[1], rot[2])) xform.translation = Vector(tran.read_f32(3)) + print(tran.indent + "Rot: {} Loc: {}".format(str(xform.rotation), str(xform.translation))) + return xform diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index 9f530ea..7cee0af 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -20,6 +20,47 @@ import os #def import_anim(scene : Scene): +def parent_object_to_bone(obj, armature, bone_name): + + bpy.ops.object.select_all(action='DESELECT') + armature.select_set(True) + bpy.context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + armature.data.edit_bones.active = armature.data.edit_bones[bone_name] + bpy.ops.object.mode_set(mode='OBJECT') + + + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = None + + + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM") + bpy.context.view_layer.objects.active = None + + + obj.select_set(True) + armature.select_set(True) + bpy.context.view_layer.objects.active = armature + + bpy.ops.object.mode_set(mode='POSE') + + armature.pose.bones[bone_name].bone.select = True + + bpy.ops.object.parent_set(type="BONE") + armature.pose.bones[bone_name].bone.select = False + + + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + + + + + @@ -301,7 +342,18 @@ def extract_scene(filepath: str, scene: Scene): armature.select_set(False) bpy.context.view_layer.objects.active = None - if reparent_obj is not None: + print("About to parent to bones....") + + if armature is not None: + for bone in armature.data.bones: + for model in scene.models: + if model.parent in armature.data.bones and model.name not in armature.data.bones: + parent_object_to_bone(model_map[model.name], armature, model.parent) + + print("Done parenting to bones") + ''' + if reparent_obj is not None and armature.name != reparent_obj.name: + armature.select_set(True) reparent_obj.select_set(True) bpy.context.view_layer.objects.active = reparent_obj @@ -312,17 +364,12 @@ def extract_scene(filepath: str, scene: Scene): bpy.context.view_layer.objects.active = None - if armature is not None: - for bone in armature.data.bones: - model_map[bone.name].select_set(True) - bpy.ops.object.delete() - for model in scene.models: if model.name in bpy.data.objects: obj = bpy.data.objects[model.name] if get_is_model_hidden(obj) and len(obj.children) == 0: obj.hide_set(True) - + ''' From c0c978af8bf1926dd0e9464e33130334b0cb9d48 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Sat, 5 Dec 2020 00:53:59 -0500 Subject: [PATCH 24/39] Bone parenting fixed, anm2 reading, menu option for anim import --- addons/io_scene_swbf_msh/__init__.py | 15 ++++- addons/io_scene_swbf_msh/msh_anim_gather.py | 14 +++-- addons/io_scene_swbf_msh/msh_model.py | 2 +- addons/io_scene_swbf_msh/msh_reader.py | 8 +++ addons/io_scene_swbf_msh/msh_scene_read.py | 68 ++++++++++++--------- addons/io_scene_swbf_msh/msh_scene_save.py | 12 ++-- addons/io_scene_swbf_msh/msh_to_blend.py | 45 ++++---------- 7 files changed, 89 insertions(+), 75 deletions(-) diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index 530b654..387c0f2 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -151,9 +151,22 @@ class ImportMSH(Operator, ImportHelper): maxlen=255, # Max internal buffer length, longer would be clamped. ) + animation_only: BoolProperty( + name="Import Animation Only", + description="Import animation and append as a new action to currently selected armature.", + default=False + ) + + def execute(self, context): with open(self.filepath, 'rb') as input_file: - extract_scene(self.filepath, read_scene(input_file)) + scene = read_scene(input_file, self.animation_only) + + if not self.animation_only: + extract_scene(self.filepath, scene) + else: + extract_and_apply_anim(self.filepath, scene) + return {'FINISHED'} def menu_func_import(self, context): diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 0129587..40b90a2 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -17,6 +17,8 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: action = armature.animation_data.action anim = Animation(); + root_crc = crc(root_name) + if not action: framerange = Vector((0.0,1.0)) else: @@ -27,9 +29,9 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: anim.end_index = num_frames - 1 - anim.bone_frames[root_name] = ([], []) + anim.bone_frames[root_crc] = ([], []) for bone in armature.data.bones: - anim.bone_frames[bone.name] = ([], []) + anim.bone_frames[crc(bone.name)] = ([], []) for frame in range(num_frames): @@ -40,8 +42,8 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: rframe_dummy = RotationFrame(frame, convert_rotation_space(Quaternion())) tframe_dummy = TranslationFrame(frame, Vector((0.0,0.0,0.0))) - anim.bone_frames[root_name][0].append(tframe_dummy) - anim.bone_frames[root_name][1].append(rframe_dummy) + anim.bone_frames[root_crc][0].append(tframe_dummy) + anim.bone_frames[root_crc][1].append(rframe_dummy) for bone in armature.pose.bones: @@ -56,8 +58,8 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: rframe = RotationFrame(frame, convert_rotation_space(rot)) tframe = TranslationFrame(frame, convert_vector_space(loc)) - anim.bone_frames[bone.name][0].append(tframe) - anim.bone_frames[bone.name][1].append(rframe) + anim.bone_frames[crc(bone.name)][0].append(tframe) + anim.bone_frames[crc(bone.name)][1].append(rframe) return anim diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index a1df175..e2d624b 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -98,7 +98,7 @@ class Animation: """ Class representing 'CYCL' + 'KFR3' sections in a .msh file """ name: str = "fullanimation" - bone_frames: Dict[str, Tuple[List[TranslationFrame], List[RotationFrame]]] = field(default_factory=dict) + bone_frames: Dict[int, Tuple[List[TranslationFrame], List[RotationFrame]]] = field(default_factory=dict) framerate: float = 29.97 start_index : int = 0 diff --git a/addons/io_scene_swbf_msh/msh_reader.py b/addons/io_scene_swbf_msh/msh_reader.py index 25528f9..2840013 100644 --- a/addons/io_scene_swbf_msh/msh_reader.py +++ b/addons/io_scene_swbf_msh/msh_reader.py @@ -84,6 +84,14 @@ class Reader: return result[0] if num == 1 else result + def read_quat(self): + rot = self.read_f32(4) + return Quaternion((rot[3], rot[0], rot[1], rot[2])) + + def read_vec(self): + return Vector(self.read_f32(3)) + + def read_child(self): child = Reader(self.file, parent=self, indent=int(len(self.indent) / 2) + 1, debug=self.debug) diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index ab8d274..d8b68d2 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -15,7 +15,7 @@ model_counter = 0 mndx_remap = {} -def read_scene(input_file) -> Scene: +def read_scene(input_file, anim_only=False) -> Scene: scene = Scene() scene.models = [] @@ -36,30 +36,31 @@ def read_scene(input_file) -> Scene: if "MSH2" in next_header: with hedr.read_child() as msh2: + + if not anim_only: + materials_list = [] - materials_list = [] + while (msh2.could_have_child()): - while (msh2.could_have_child()): + next_header = msh2.peak_next_header() - next_header = msh2.peak_next_header() + if "SINF" in next_header: + with msh2.read_child() as sinf: + pass - if "SINF" in next_header: - with msh2.read_child() as sinf: - pass + elif "MATL" in next_header: + with msh2.read_child() as matl: + materials_list += _read_matl_and_get_materials_list(matl) + for i,mat in enumerate(materials_list): + scene.materials[mat.name] = mat - elif "MATL" in next_header: - with msh2.read_child() as matl: - materials_list += _read_matl_and_get_materials_list(matl) - for i,mat in enumerate(materials_list): - scene.materials[mat.name] = mat + elif "MODL" in next_header: + while ("MODL" in msh2.peak_next_header()): + with msh2.read_child() as modl: + scene.models.append(_read_modl(modl, materials_list)) - elif "MODL" in next_header: - while ("MODL" in msh2.peak_next_header()): - with msh2.read_child() as modl: - scene.models.append(_read_modl(modl, materials_list)) - - else: - msh2.skip_bytes(1) + else: + msh2.skip_bytes(1) elif "SKL2" in next_header: with hedr.read_child() as skl2: @@ -68,7 +69,7 @@ def read_scene(input_file) -> Scene: elif "ANM2" in next_header: with hedr.read_child() as anm2: - _read_anm2(anm2, scene.models) + scene.animation = _read_anm2(anm2) else: hedr.skip_bytes(1) @@ -250,9 +251,8 @@ def _read_tran(tran: Reader) -> ModelTransform: tran.skip_bytes(12) #ignore scale - rot = tran.read_f32(4) - xform.rotation = Quaternion((rot[3], rot[0], rot[1], rot[2])) - xform.translation = Vector(tran.read_f32(3)) + xform.rotation = tran.read_quat() + xform.translation = tran.read_vec() print(tran.indent + "Rot: {} Loc: {}".format(str(xform.rotation), str(xform.translation))) @@ -381,11 +381,9 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: -def _read_anm2(anm2: Reader, models): +def _read_anm2(anm2: Reader) -> Animation: - hash_dict = {} - for model in models: - hash_dict[crc(model.name)] = model.name + anim = Animation() while anm2.could_have_child(): @@ -402,14 +400,26 @@ def _read_anm2(anm2: Reader, models): for _ in range(num_bones): - kfr3.read_u32() + bone_crc = kfr3.read_u32() + + frames = ([],[]) frametype = kfr3.read_u32() num_loc_frames = kfr3.read_u32() num_rot_frames = kfr3.read_u32() - kfr3.skip_bytes(16 * num_loc_frames + 20 * num_rot_frames) + for i in range(num_loc_frames): + frames[0].append(TranslationFrame(kfr3.read_u32(), kfr3.read_vec())) + + for i in range(num_rot_frames): + frames[1].append(RotationFrame(kfr3.read_u32(), kfr3.read_quat())) + + anim.bone_frames[bone_crc] = frames + else: + anm2.skip_bytes(1) + + return anim diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 9e65fbf..bb28ddc 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -251,15 +251,15 @@ def _write_bln2(bln2: Writer, anim: Animation): bones = anim.bone_frames.keys() bln2.write_u32(len(bones)) - for boneName in bones: - bln2.write_u32(crc(boneName), 0) + for bone_crc in bones: + bln2.write_u32(bone_crc, 0) def _write_skl2(skl2: Writer, anim: Animation): bones = anim.bone_frames.keys() skl2.write_u32(len(bones)) - for boneName in bones: - skl2.write_u32(crc(boneName), 0) #default values from docs + for bone_crc in bones: + skl2.write_u32(bone_crc, 0) #default values from docs skl2.write_f32(1.0, 0.0, 0.0) ''' @@ -284,8 +284,8 @@ def _write_anm2(anm2: Writer, anim: Animation): kfr3.write_u32(len(anim.bone_frames)) - for boneName in anim.bone_frames: - kfr3.write_u32(crc(boneName)) + for bone_crc in anim.bone_frames: + kfr3.write_u32(bone_crc) kfr3.write_u32(0) #what is keyframe type? translation_frames, rotation_frames = anim.bone_frames[boneName] diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index 7cee0af..d28c116 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -18,44 +18,25 @@ import os -#def import_anim(scene : Scene): + + + + + def parent_object_to_bone(obj, armature, bone_name): - bpy.ops.object.select_all(action='DESELECT') - armature.select_set(True) - bpy.context.view_layer.objects.active = armature - bpy.ops.object.mode_set(mode='EDIT') - armature.data.edit_bones.active = armature.data.edit_bones[bone_name] - bpy.ops.object.mode_set(mode='OBJECT') + worldmat = obj.matrix_world + obj.parent = None + obj.parent = armature + obj.parent_type = 'BONE' + obj.parent_bone = bone_name - bpy.ops.object.select_all(action='DESELECT') - bpy.context.view_layer.objects.active = None + obj.matrix_basis = Matrix() + obj.matrix_parent_inverse = Matrix() - - obj.select_set(True) - bpy.context.view_layer.objects.active = obj - - bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM") - bpy.context.view_layer.objects.active = None - - - obj.select_set(True) - armature.select_set(True) - bpy.context.view_layer.objects.active = armature - - bpy.ops.object.mode_set(mode='POSE') - - armature.pose.bones[bone_name].bone.select = True - - bpy.ops.object.parent_set(type="BONE") - armature.pose.bones[bone_name].bone.select = False - - - - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.select_all(action='DESELECT') + obj.matrix_world = worldmat From 57909f758f1c9f9ed1c840c11224d05e0bb4aa79 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Sun, 6 Dec 2020 04:11:12 -0500 Subject: [PATCH 25/39] Animations importing with expected rotation conversion issues --- addons/io_scene_swbf_msh/msh_anim_gather.py | 2 + addons/io_scene_swbf_msh/msh_reader.py | 1 + addons/io_scene_swbf_msh/msh_scene.py | 5 +- addons/io_scene_swbf_msh/msh_scene_save.py | 2 +- addons/io_scene_swbf_msh/msh_to_blend.py | 60 +++++++++++++++++++-- 5 files changed, 62 insertions(+), 8 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 40b90a2..100285d 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -11,6 +11,8 @@ from .msh_model_utilities import * from .msh_utilities import * from .msh_model_gather import * +from .crc import crc + def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: diff --git a/addons/io_scene_swbf_msh/msh_reader.py b/addons/io_scene_swbf_msh/msh_reader.py index 2840013..c629b47 100644 --- a/addons/io_scene_swbf_msh/msh_reader.py +++ b/addons/io_scene_swbf_msh/msh_reader.py @@ -1,6 +1,7 @@ import io import struct +from mathutils import Vector, Quaternion class Reader: def __init__(self, file, parent=None, indent=0, debug=False): diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index ab29a67..755a365 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -6,14 +6,15 @@ from typing import List, Dict from copy import copy import bpy from mathutils import Vector -from .msh_model import Model, Animation +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 +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 @dataclass class SceneAABB: diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index bb28ddc..7ca557c 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -288,7 +288,7 @@ def _write_anm2(anm2: Writer, anim: Animation): kfr3.write_u32(bone_crc) kfr3.write_u32(0) #what is keyframe type? - translation_frames, rotation_frames = anim.bone_frames[boneName] + translation_frames, rotation_frames = anim.bone_frames[bone_crc] kfr3.write_u32(len(translation_frames), len(rotation_frames)) diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index d28c116..9d50f97 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -18,8 +18,63 @@ import os +def extract_and_apply_anim(filename, scene): + + arma = bpy.context.view_layer.objects.active + + if arma.type != 'ARMATURE': + raise Exception("Select an armature to attach the imported animation to!") + + if scene.animation is None: + raise Exception("No animation found in msh file") + + else: + head, tail = os.path.split(filename) + anim_name = tail.split(".")[0] + action = bpy.data.actions.new(anim_name) + + if not arma.animation_data: + arma.animation_data_create() + + for bone in arma.pose.bones: + if crc(bone.name) in scene.animation.bone_frames: + print("Inserting anim data for bone: {}".format(bone.name)) + + bone_local_mat = arma.data.bones[bone.name].matrix_local + + translation_frames, rotation_frames = scene.animation.bone_frames[crc(bone.name)] + + loc_data_path = "pose.bones[\"{}\"].location".format(bone.name) + rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name) + + fcurve_rot_w = action.fcurves.new(rot_data_path, index=0) + fcurve_rot_x = action.fcurves.new(rot_data_path, index=1) + fcurve_rot_y = action.fcurves.new(rot_data_path, index=2) + fcurve_rot_z = action.fcurves.new(rot_data_path, index=3) + + for frame in rotation_frames: + i = frame.index + q = (bone_local_mat @ convert_rotation_space(frame.rotation).to_matrix().to_4x4()).to_quaternion() + + fcurve_rot_w.keyframe_points.insert(i,q.w) + fcurve_rot_x.keyframe_points.insert(i,q.x) + fcurve_rot_y.keyframe_points.insert(i,q.y) + fcurve_rot_z.keyframe_points.insert(i,q.z) + fcurve_loc_x = action.fcurves.new(loc_data_path, index=0) + fcurve_loc_y = action.fcurves.new(loc_data_path, index=1) + fcurve_loc_z = action.fcurves.new(loc_data_path, index=2) + + for frame in translation_frames: + i = frame.index + t = bone_local_mat @ convert_vector_space(Vector((0.0,0.0,0.0))) + + fcurve_loc_x.keyframe_points.insert(i,t.x) + fcurve_loc_y.keyframe_points.insert(i,t.y) + fcurve_loc_z.keyframe_points.insert(i,t.z) + + arma.animation_data.action = action @@ -40,11 +95,6 @@ def parent_object_to_bone(obj, armature, bone_name): - - - - - def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): armature = bpy.data.armatures.new("skeleton") From 49f89a1fde0c390cf4a189721b11efe07218e366 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Sun, 6 Dec 2020 00:19:58 -0500 Subject: [PATCH 26/39] Animations importing correctly for most models, though some (acklay and human) are very wrong and others have one or two incorrect bones (at-at). Bones are imported as is and not made to look pretty in order to preserve animation ease. Letting users import cleaned up skeletons while preserving original transforms via some sort of context-menu addon might be worthwhile... --- addons/io_scene_swbf_msh/msh_to_blend.py | 71 +++++++++++++++-------- addons/io_scene_swbf_msh/msh_utilities.py | 2 +- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index 9d50f97..14779e7 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -36,11 +36,20 @@ def extract_and_apply_anim(filename, scene): if not arma.animation_data: arma.animation_data_create() + bone_bind_poses = {} + for bone in arma.data.bones: + local_mat = bone.matrix_local + if bone.parent: + local_mat = bone.parent.matrix_local.inverted() @ local_mat + bone_bind_poses[bone.name] = local_mat + + for bone in arma.pose.bones: if crc(bone.name) in scene.animation.bone_frames: - print("Inserting anim data for bone: {}".format(bone.name)) + #print("Inserting anim data for bone: {}".format(bone.name)) - bone_local_mat = arma.data.bones[bone.name].matrix_local + + bone_local_mat = bone_bind_poses[bone.name] translation_frames, rotation_frames = scene.animation.bone_frames[crc(bone.name)] @@ -52,15 +61,20 @@ def extract_and_apply_anim(filename, scene): fcurve_rot_y = action.fcurves.new(rot_data_path, index=2) fcurve_rot_z = action.fcurves.new(rot_data_path, index=3) + print("\nBone name: " + bone.name) + print("\tRot: {}".format(quat_to_str(rotation_frames[0].rotation))) + for frame in rotation_frames: i = frame.index - q = (bone_local_mat @ convert_rotation_space(frame.rotation).to_matrix().to_4x4()).to_quaternion() + q = (bone_local_mat.inverted() @ convert_rotation_space(frame.rotation).to_matrix().to_4x4()).to_quaternion() fcurve_rot_w.keyframe_points.insert(i,q.w) fcurve_rot_x.keyframe_points.insert(i,q.x) fcurve_rot_y.keyframe_points.insert(i,q.y) fcurve_rot_z.keyframe_points.insert(i,q.z) + print("\tLoc: {}".format(vec_to_str(translation_frames[0].translation))) + fcurve_loc_x = action.fcurves.new(loc_data_path, index=0) fcurve_loc_y = action.fcurves.new(loc_data_path, index=1) @@ -68,7 +82,7 @@ def extract_and_apply_anim(filename, scene): for frame in translation_frames: i = frame.index - t = bone_local_mat @ convert_vector_space(Vector((0.0,0.0,0.0))) + t = convert_vector_space(frame.translation) - bone_local_mat.translation fcurve_loc_x.keyframe_points.insert(i,t.x) fcurve_loc_y.keyframe_points.insert(i,t.y) @@ -113,10 +127,19 @@ def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): if bone.parent: edit_bone.parent = armature.edit_bones[bone.parent] - edit_bone.head = model_map[bone.name].matrix_world.translation + bone_obj = model_map[bone.name] + #edit_bone.head = bone_obj.matrix_world.translation + + edit_bone.matrix = bone_obj.matrix_world + edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,1.0,0.0)) + #edit_bone.length = 1 + + + ''' bone_children = [b for b in get_model_children(bone, refined_skeleton)] + if bone_children: edit_bone.tail = Vector((-0.00001,0.0,0.0)) for bone_child in bone_children: @@ -124,6 +147,7 @@ def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): edit_bone.tail = edit_bone.tail / len(bone_children) else: edit_bone.tail = model_map[bone.name].matrix_world @ Vector((-0.2,0.0,0.0)) + ''' bpy.ops.object.mode_set(mode='OBJECT') @@ -220,30 +244,31 @@ def extract_models(scene: Scene, materials_map): weights_offsets = {} - for i,seg in enumerate(model.geometry): + if model.geometry: + for i,seg in enumerate(model.geometry): - if i == 0: - mat_name = seg.material_name + 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 seg.weights: - weights_offsets[offset] = seg.weights + if seg.weights: + weights_offsets[offset] = seg.weights - if seg.texcoords is not None: - full_texcoords += seg.texcoords - else: - full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))] + if seg.texcoords is not None: + full_texcoords += seg.texcoords + else: + full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))] - if seg.triangles: - faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles] - else: - for strip in seg.triangle_strips: - for i in range(len(strip) - 2): - face = tuple([offset + strip[j] for j in range(i,i+3)]) - faces.append(face) + if seg.triangles: + faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles] + else: + for strip in seg.triangle_strips: + for i in range(len(strip) - 2): + face = tuple([offset + strip[j] for j in range(i,i+3)]) + faces.append(face) - offset += len(seg.positions) + offset += len(seg.positions) new_mesh.from_pydata(verts, [], faces) new_mesh.update() diff --git a/addons/io_scene_swbf_msh/msh_utilities.py b/addons/io_scene_swbf_msh/msh_utilities.py index 1c7424f..6a35578 100644 --- a/addons/io_scene_swbf_msh/msh_utilities.py +++ b/addons/io_scene_swbf_msh/msh_utilities.py @@ -8,7 +8,7 @@ def vec_to_str(vec): return "({:.4},{:.4},{:.4})".format(vec.x,vec.y,vec.z) def quat_to_str(quat): - return "({:.4},{:.4},{:.4},{:.4})".format(quat.x, quat.y, quat.z, quat.w) + return "({:.4},{:.4},{:.4},{:.4})".format(quat.w, quat.x, quat.y, quat.z) def add_vec(l: Vector, r: Vector) -> Vector: return Vector(v0 + v1 for v0, v1 in zip(l, r)) From b749e475367d5d8ea2df292977953ec8e7c5b0b0 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Thu, 10 Dec 2020 23:47:45 -0500 Subject: [PATCH 27/39] Correct pose-relative transforms for impure skeletons --- addons/io_scene_swbf_msh/msh_scene_read.py | 81 ++++---- addons/io_scene_swbf_msh/msh_to_blend.py | 220 +++++++++++++-------- 2 files changed, 181 insertions(+), 120 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index d8b68d2..ba68a10 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -33,7 +33,7 @@ def read_scene(input_file, anim_only=False) -> Scene: next_header = hedr.peak_next_header() - if "MSH2" in next_header: + if next_header == "MSH2": with hedr.read_child() as msh2: @@ -44,30 +44,29 @@ def read_scene(input_file, anim_only=False) -> Scene: next_header = msh2.peak_next_header() - if "SINF" in next_header: + if next_header == "SINF": with msh2.read_child() as sinf: pass - elif "MATL" in next_header: + elif next_header == "MATL": with msh2.read_child() as matl: materials_list += _read_matl_and_get_materials_list(matl) for i,mat in enumerate(materials_list): scene.materials[mat.name] = mat - elif "MODL" in next_header: - while ("MODL" in msh2.peak_next_header()): - with msh2.read_child() as modl: - scene.models.append(_read_modl(modl, materials_list)) + elif next_header == "MODL": + with msh2.read_child() as modl: + scene.models.append(_read_modl(modl, materials_list)) else: msh2.skip_bytes(1) - elif "SKL2" in next_header: + elif next_header == "SKL2": with hedr.read_child() as skl2: num_bones = skl2.read_u32() scene.skeleton = [skl2.read_u32(5)[0] for i in range(num_bones)] - elif "ANM2" in next_header: + elif next_header == "ANM2": with hedr.read_child() as anm2: scene.animation = _read_anm2(anm2) @@ -118,36 +117,36 @@ def _read_matd(matd: Reader) -> Material: next_header = matd.peak_next_header() - if "NAME" in next_header: + if next_header == "NAME": with matd.read_child() as name: mat.name = name.read_string() - elif "DATA" in next_header: + elif next_header == "DATA": with matd.read_child() as data: data.read_f32(4) # Diffuse Color (Seams to get ignored by modelmunge) mat.specular_color = data.read_f32(4) data.read_f32(4) # Ambient Color (Seams to get ignored by modelmunge and Zero(?)) data.read_f32() # Specular Exponent/Decay (Gets ignored by RedEngine in SWBFII for all known materials) - elif "ATRB" in next_header: + elif next_header == "ATRB": with matd.read_child() as atrb: mat.flags = atrb.read_u8() mat.rendertype = atrb.read_u8() mat.data = atrb.read_u8(2) - elif "TX0D" in next_header: + elif next_header == "TX0D": with matd.read_child() as tx0d: mat.texture0 = tx0d.read_string() - elif "TX1D" in next_header: + elif next_header == "TX1D": with matd.read_child() as tx1d: mat.texture1 = tx1d.read_string() - elif "TX2D" in next_header: + elif next_header == "TX2D": with matd.read_child() as tx2d: mat.texture2 = tx2d.read_string() - elif "TX3D" in next_header: + elif next_header == "TX3D": with matd.read_child() as tx3d: mat.texture3 = tx3d.read_string() @@ -165,11 +164,11 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: next_header = modl.peak_next_header() - if "MTYP" in next_header: + if next_header == "MTYP": with modl.read_child() as mtyp: model.model_type = ModelType(mtyp.read_u32()) - elif "MNDX" in next_header: + elif next_header == "MNDX": with modl.read_child() as mndx: index = mndx.read_u32() @@ -181,23 +180,23 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: model_counter += 1 - elif "NAME" in next_header: + elif next_header == "NAME": with modl.read_child() as name: model.name = name.read_string() - elif "PRNT" in next_header: + elif next_header == "PRNT": with modl.read_child() as prnt: model.parent = prnt.read_string() - elif "FLGS" in next_header: + elif next_header == "FLGS": with modl.read_child() as flgs: model.hidden = flgs.read_u32() - elif "TRAN" in next_header: + elif next_header == "TRAN": with modl.read_child() as tran: model.transform = _read_tran(tran) - elif "GEOM" in next_header: + elif next_header == "GEOM": model.geometry = [] envelope = [] @@ -206,11 +205,11 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: while geom.could_have_child(): next_header_geom = geom.peak_next_header() - if "SEGM" in next_header_geom: + if next_header_geom == "SEGM": with geom.read_child() as segm: model.geometry.append(_read_segm(segm, materials_list)) - elif "ENVL" in next_header_geom: + elif next_header_geom == "ENVL": with geom.read_child() as envl: num_indicies = envl.read_u32() envelope += [envl.read_u32() for _ in range(num_indicies)] @@ -228,7 +227,7 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: index = vertex_weight.bone weight_set[i] = VertexWeight(vertex_weight.weight, envelope[vertex_weight.bone]) - elif "SWCI" in next_header: + elif next_header == "SWCI": prim = CollisionPrimitive() with modl.read_child() as swci: prim.shape = CollisionPrimitiveShape(swci.read_u32()) @@ -267,25 +266,25 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: next_header = segm.peak_next_header() - if "MATI" in next_header: + if next_header == "MATI": with segm.read_child() as mati: geometry_seg.material_name = materials_list[mati.read_u32()].name - elif "POSL" in next_header: + elif next_header == "POSL": with segm.read_child() as posl: num_positions = posl.read_u32() for _ in range(num_positions): geometry_seg.positions.append(Vector(posl.read_f32(3))) - elif "NRML" in next_header: + elif next_header == "NRML": with segm.read_child() as nrml: num_normals = nrml.read_u32() for _ in range(num_positions): geometry_seg.normals.append(Vector(nrml.read_f32(3))) - elif "CLRL" in next_header: + elif next_header == "CLRL": geometry_seg.colors = [] with segm.read_child() as clrl: @@ -294,14 +293,14 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: for _ in range(num_colors): geometry_seg.colors += unpack_color(clrl.read_u32()) - elif "UV0L" in next_header: + elif next_header == "UV0L": with segm.read_child() as uv0l: num_texcoords = uv0l.read_u32() for _ in range(num_texcoords): geometry_seg.texcoords.append(Vector(uv0l.read_f32(2))) - elif "NDXL" in next_header: + elif next_header == "NDXL": with segm.read_child() as ndxl: num_polygons = ndxl.read_u32() @@ -309,14 +308,14 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: polygon = ndxl.read_u16(ndxl.read_u16()) geometry_seg.polygons.append(polygon) - elif "NDXT" in next_header: + elif next_header == "NDXT": with segm.read_child() as ndxt: num_tris = ndxt.read_u32() for _ in range(num_tris): geometry_seg.triangles.append(ndxt.read_u16(3)) - elif "STRP" in next_header: + elif next_header == "STRP": strips : List[List[int]] = [] with segm.read_child() as strp: @@ -357,7 +356,7 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: #if segm.read_u16 != 0: #trailing 0 bug https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP # segm.skip_bytes(-2) - elif "WGHT" in next_header: + elif next_header == "WGHT": with segm.read_child() as wght: geometry_seg.weights = [] @@ -389,11 +388,19 @@ def _read_anm2(anm2: Reader) -> Animation: next_header = anm2.peak_next_header() - if "CYCL" in next_header: + if next_header == "CYCL": with anm2.read_child() as cycl: pass - elif "KFR3" in next_header: + ''' + num_anims = cycl.read_u32() + + for _ in range(num_anims): + cycl.skip_bytes(64) + print("CYCL play style {}".format(cycl.read_u32(4)[1])) + ''' + + elif next_header == "KFR3": with anm2.read_child() as kfr3: num_bones = kfr3.read_u32() diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index 14779e7..cf65bb2 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -20,75 +20,94 @@ import os def extract_and_apply_anim(filename, scene): - arma = bpy.context.view_layer.objects.active + arma = bpy.context.view_layer.objects.active - if arma.type != 'ARMATURE': - raise Exception("Select an armature to attach the imported animation to!") + if arma.type != 'ARMATURE': + raise Exception("Select an armature to attach the imported animation to!") - if scene.animation is None: - raise Exception("No animation found in msh file") - - else: - head, tail = os.path.split(filename) - anim_name = tail.split(".")[0] - action = bpy.data.actions.new(anim_name) + if scene.animation is None: + raise Exception("No animation found in msh file!") + + else: + head, tail = os.path.split(filename) + anim_name = tail.split(".")[0] + action = bpy.data.actions.new(anim_name) - if not arma.animation_data: - arma.animation_data_create() - - bone_bind_poses = {} - for bone in arma.data.bones: - local_mat = bone.matrix_local - if bone.parent: - local_mat = bone.parent.matrix_local.inverted() @ local_mat - bone_bind_poses[bone.name] = local_mat + if not arma.animation_data: + arma.animation_data_create() - for bone in arma.pose.bones: - if crc(bone.name) in scene.animation.bone_frames: - #print("Inserting anim data for bone: {}".format(bone.name)) + + bone_bind_poses = {} + bone_stack_mats = {} + + ''' + for bone in arma.data.bones: + local_mat = bone.matrix_local + if bone.parent: + local_mat = bone.parent.matrix_local.inverted() @ local_mat + bone_bind_poses[bone.name] = local_mat + ''' + + for bone in arma.data.bones: + bone_obj = bpy.data.objects[bone.name] + bone_obj_parent = bone_obj.parent + + bind_mat = bone_obj.matrix_local + stack_mat = Matrix.Identity(4) - bone_local_mat = bone_bind_poses[bone.name] + while(True): + if bone_obj_parent is None or bone_obj_parent.name in arma.data.bones: + break + bind_mat = bone_obj_parent.matrix_local @ bind_mat + stack_mat = bone_obj_parent.matrix_local @ stack_mat + bone_obj_parent = bone_obj_parent.parent - translation_frames, rotation_frames = scene.animation.bone_frames[crc(bone.name)] - - loc_data_path = "pose.bones[\"{}\"].location".format(bone.name) - rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name) - - fcurve_rot_w = action.fcurves.new(rot_data_path, index=0) - fcurve_rot_x = action.fcurves.new(rot_data_path, index=1) - fcurve_rot_y = action.fcurves.new(rot_data_path, index=2) - fcurve_rot_z = action.fcurves.new(rot_data_path, index=3) - - print("\nBone name: " + bone.name) - print("\tRot: {}".format(quat_to_str(rotation_frames[0].rotation))) - - for frame in rotation_frames: - i = frame.index - q = (bone_local_mat.inverted() @ convert_rotation_space(frame.rotation).to_matrix().to_4x4()).to_quaternion() - - fcurve_rot_w.keyframe_points.insert(i,q.w) - fcurve_rot_x.keyframe_points.insert(i,q.x) - fcurve_rot_y.keyframe_points.insert(i,q.y) - fcurve_rot_z.keyframe_points.insert(i,q.z) - - print("\tLoc: {}".format(vec_to_str(translation_frames[0].translation))) + bone_bind_poses[bone.name] = bind_mat.inverted() @ stack_mat - fcurve_loc_x = action.fcurves.new(loc_data_path, index=0) - fcurve_loc_y = action.fcurves.new(loc_data_path, index=1) - fcurve_loc_z = action.fcurves.new(loc_data_path, index=2) - for frame in translation_frames: - i = frame.index - t = convert_vector_space(frame.translation) - bone_local_mat.translation + for bone in arma.pose.bones: + if crc(bone.name) in scene.animation.bone_frames: + #print("Inserting anim data for bone: {}".format(bone.name)) - fcurve_loc_x.keyframe_points.insert(i,t.x) - fcurve_loc_y.keyframe_points.insert(i,t.y) - fcurve_loc_z.keyframe_points.insert(i,t.z) + bind_mat = bone_bind_poses[bone.name] - arma.animation_data.action = action + translation_frames, rotation_frames = scene.animation.bone_frames[crc(bone.name)] + + loc_data_path = "pose.bones[\"{}\"].location".format(bone.name) + rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name) + + + fcurve_rot_w = action.fcurves.new(rot_data_path, index=0) + fcurve_rot_x = action.fcurves.new(rot_data_path, index=1) + fcurve_rot_y = action.fcurves.new(rot_data_path, index=2) + fcurve_rot_z = action.fcurves.new(rot_data_path, index=3) + + for frame in rotation_frames: + i = frame.index + q = (bind_mat @ convert_rotation_space(frame.rotation).to_matrix().to_4x4()).to_quaternion() + + fcurve_rot_w.keyframe_points.insert(i,q.w) + fcurve_rot_x.keyframe_points.insert(i,q.x) + fcurve_rot_y.keyframe_points.insert(i,q.y) + fcurve_rot_z.keyframe_points.insert(i,q.z) + + + fcurve_loc_x = action.fcurves.new(loc_data_path, index=0) + fcurve_loc_y = action.fcurves.new(loc_data_path, index=1) + fcurve_loc_z = action.fcurves.new(loc_data_path, index=2) + + for frame in translation_frames: + i = frame.index + t = (bind_mat @ Matrix.Translation(convert_vector_space(frame.translation))).translation + + fcurve_loc_x.keyframe_points.insert(i,t.x) + fcurve_loc_y.keyframe_points.insert(i,t.y) + fcurve_loc_z.keyframe_points.insert(i,t.z) + + arma.animation_data.action = action @@ -187,6 +206,14 @@ def extract_refined_skeleton(scene: Scene): refined_skeleton_models = [] + ''' + for bone in skeleton_models: + + if bone.parent: + if + ''' + + for bone in skeleton_models: if bone.parent: @@ -213,7 +240,7 @@ def extract_refined_skeleton(scene: Scene): else: curr_ancestor = model_dict[curr_ancestor.parent] stacked_transform = model_transform_to_matrix(curr_ancestor.transform) @ stacked_transform - + return sort_by_parent(refined_skeleton_models) @@ -245,30 +272,30 @@ def extract_models(scene: Scene, materials_map): weights_offsets = {} if model.geometry: - for i,seg in enumerate(model.geometry): + for i,seg in enumerate(model.geometry): - if i == 0: - mat_name = seg.material_name + 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 seg.weights: - weights_offsets[offset] = seg.weights + if seg.weights: + weights_offsets[offset] = seg.weights - if seg.texcoords is not None: - full_texcoords += seg.texcoords - else: - full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))] + if seg.texcoords is not None: + full_texcoords += seg.texcoords + else: + full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))] - if seg.triangles: - faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles] - else: - for strip in seg.triangle_strips: - for i in range(len(strip) - 2): - face = tuple([offset + strip[j] for j in range(i,i+3)]) - faces.append(face) + if seg.triangles: + faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles] + else: + for strip in seg.triangle_strips: + for i in range(len(strip) - 2): + face = tuple([offset + strip[j] for j in range(i,i+3)]) + faces.append(face) - offset += len(seg.positions) + offset += len(seg.positions) new_mesh.from_pydata(verts, [], faces) new_mesh.update() @@ -379,6 +406,36 @@ def extract_scene(filepath: str, scene: Scene): skel = extract_refined_skeleton(scene) armature = refined_skeleton_to_armature(skel, model_map) + + for bone in armature.data.bones: + bone_local = bone.matrix_local + if bone.parent: + bone_local = bone.parent.matrix_local.inverted() @ bone_local + + bone_obj_local = bpy.data.objects[bone.name].matrix_local + obj_loc, obj_rot, _ = bone_obj_local.decompose() + + loc, rot, _ = bone_local.decompose() + + locdiff = obj_loc - loc + quatdiff = obj_rot - rot + + if quatdiff.magnitude > .01: + print("Big quat diff here") + print("\t{}: obj quat: {} bone quat: {}".format(bone.name, quat_to_str(obj_rot), quat_to_str(rot))) + + + #if locdiff.magnitude > .01: + # print("Big loc diff here") + # print("\t{}: obj loc: {} bone loc: {}".format(bone.name, vec_to_str(obj_loc), vec_to_str(loc))) + + + + + + + + reparent_obj = None for model in scene.models: if model.model_type == ModelType.SKIN: @@ -398,15 +455,12 @@ def extract_scene(filepath: str, scene: Scene): armature.select_set(False) bpy.context.view_layer.objects.active = None - print("About to parent to bones....") - if armature is not None: for bone in armature.data.bones: for model in scene.models: - if model.parent in armature.data.bones and model.name not in armature.data.bones: - parent_object_to_bone(model_map[model.name], armature, model.parent) + if model.parent in armature.data.bones and model.model_type != ModelType.NULL: + pass#parent_object_to_bone(model_map[model.name], armature, model.parent) - print("Done parenting to bones") ''' if reparent_obj is not None and armature.name != reparent_obj.name: @@ -418,14 +472,14 @@ def extract_scene(filepath: str, scene: Scene): armature.select_set(False) reparent_obj.select_set(False) bpy.context.view_layer.objects.active = None - + ''' for model in scene.models: if model.name in bpy.data.objects: obj = bpy.data.objects[model.name] - if get_is_model_hidden(obj) and len(obj.children) == 0: + if get_is_model_hidden(obj) and len(obj.children) == 0 and model.model_type != ModelType.NULL: obj.hide_set(True) - ''' + From 8a20c3813225adb545cb536dfd3cfce9bdd711b4 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Fri, 11 Dec 2020 11:50:03 -0500 Subject: [PATCH 28/39] Animation batch import --- addons/io_scene_swbf_msh/__init__.py | 24 ++++++--- addons/io_scene_swbf_msh/msh_to_blend.py | 69 ++++++------------------ 2 files changed, 31 insertions(+), 62 deletions(-) diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index 387c0f2..5e0d95f 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -54,7 +54,7 @@ if "bpy" in locals(): import bpy from bpy_extras.io_utils import ExportHelper, ImportHelper -from bpy.props import BoolProperty, EnumProperty +from bpy.props import BoolProperty, EnumProperty, CollectionProperty from bpy.types import Operator from .msh_scene import create_scene from .msh_scene_save import save_scene @@ -145,6 +145,11 @@ class ImportMSH(Operator, ImportHelper): bl_label = "Import SWBF .msh File" filename_ext = ".msh" + files: CollectionProperty( + name="File Path", + type=bpy.types.OperatorFileListElement, + ) + filter_glob: StringProperty( default="*.msh", options={'HIDDEN'}, @@ -159,13 +164,16 @@ class ImportMSH(Operator, ImportHelper): def execute(self, context): - with open(self.filepath, 'rb') as input_file: - scene = read_scene(input_file, self.animation_only) - - if not self.animation_only: - extract_scene(self.filepath, scene) - else: - extract_and_apply_anim(self.filepath, scene) + dirname = os.path.dirname(self.filepath) + for file in self.files: + filepath = os.path.join(dirname, file.name) + with open(filepath, 'rb') as input_file: + scene = read_scene(input_file, self.animation_only) + + if not self.animation_only: + extract_scene(filepath, scene) + else: + extract_and_apply_anim(filepath, scene) return {'FINISHED'} diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index cf65bb2..93fd4c7 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -32,22 +32,13 @@ def extract_and_apply_anim(filename, scene): head, tail = os.path.split(filename) anim_name = tail.split(".")[0] action = bpy.data.actions.new(anim_name) + action.use_fake_user = True if not arma.animation_data: arma.animation_data_create() - bone_bind_poses = {} - bone_stack_mats = {} - - ''' - for bone in arma.data.bones: - local_mat = bone.matrix_local - if bone.parent: - local_mat = bone.parent.matrix_local.inverted() @ local_mat - bone_bind_poses[bone.name] = local_mat - ''' for bone in arma.data.bones: bone_obj = bpy.data.objects[bone.name] @@ -67,7 +58,6 @@ def extract_and_apply_anim(filename, scene): bone_bind_poses[bone.name] = bind_mat.inverted() @ stack_mat - for bone in arma.pose.bones: if crc(bone.name) in scene.animation.bone_frames: #print("Inserting anim data for bone: {}".format(bone.name)) @@ -80,10 +70,10 @@ def extract_and_apply_anim(filename, scene): rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name) - fcurve_rot_w = action.fcurves.new(rot_data_path, index=0) - fcurve_rot_x = action.fcurves.new(rot_data_path, index=1) - fcurve_rot_y = action.fcurves.new(rot_data_path, index=2) - fcurve_rot_z = action.fcurves.new(rot_data_path, index=3) + fcurve_rot_w = action.fcurves.new(rot_data_path, index=0, action_group=bone.name) + fcurve_rot_x = action.fcurves.new(rot_data_path, index=1, action_group=bone.name) + fcurve_rot_y = action.fcurves.new(rot_data_path, index=2, action_group=bone.name) + fcurve_rot_z = action.fcurves.new(rot_data_path, index=3, action_group=bone.name) for frame in rotation_frames: i = frame.index @@ -95,9 +85,9 @@ def extract_and_apply_anim(filename, scene): fcurve_rot_z.keyframe_points.insert(i,q.z) - fcurve_loc_x = action.fcurves.new(loc_data_path, index=0) - fcurve_loc_y = action.fcurves.new(loc_data_path, index=1) - fcurve_loc_z = action.fcurves.new(loc_data_path, index=2) + fcurve_loc_x = action.fcurves.new(loc_data_path, index=0, action_group=bone.name) + fcurve_loc_y = action.fcurves.new(loc_data_path, index=1, action_group=bone.name) + fcurve_loc_z = action.fcurves.new(loc_data_path, index=2, action_group=bone.name) for frame in translation_frames: i = frame.index @@ -148,25 +138,22 @@ def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): bone_obj = model_map[bone.name] - #edit_bone.head = bone_obj.matrix_world.translation - edit_bone.matrix = bone_obj.matrix_world edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,1.0,0.0)) - #edit_bone.length = 1 - ''' bone_children = [b for b in get_model_children(bone, refined_skeleton)] - + tail_pos = Vector() if bone_children: - edit_bone.tail = Vector((-0.00001,0.0,0.0)) for bone_child in bone_children: - edit_bone.tail += model_map[bone_child.name].matrix_world.translation - edit_bone.tail = edit_bone.tail / len(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: - edit_bone.tail = model_map[bone.name].matrix_world @ Vector((-0.2,0.0,0.0)) - ''' + 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') @@ -205,14 +192,6 @@ def extract_refined_skeleton(scene: Scene): refined_skeleton_models = [] - - ''' - for bone in skeleton_models: - - if bone.parent: - if - ''' - for bone in skeleton_models: @@ -226,7 +205,6 @@ def extract_refined_skeleton(scene: Scene): if crc(curr_ancestor.name) in scene.skeleton or curr_ancestor.name == scene.models[0].name: new_model = Model() new_model.name = bone.name - print("Adding {} to refined skeleton...".format(bone.name)) new_model.parent = curr_ancestor.name if curr_ancestor.name != scene.models[0].name else "" loc, rot, _ = stacked_transform.decompose() @@ -417,23 +395,6 @@ def extract_scene(filepath: str, scene: Scene): loc, rot, _ = bone_local.decompose() - locdiff = obj_loc - loc - quatdiff = obj_rot - rot - - if quatdiff.magnitude > .01: - print("Big quat diff here") - print("\t{}: obj quat: {} bone quat: {}".format(bone.name, quat_to_str(obj_rot), quat_to_str(rot))) - - - #if locdiff.magnitude > .01: - # print("Big loc diff here") - # print("\t{}: obj loc: {} bone loc: {}".format(bone.name, vec_to_str(obj_loc), vec_to_str(loc))) - - - - - - reparent_obj = None From 79543a1cd7edb5917a28cc67dc81e395d51fb4e4 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Fri, 11 Dec 2020 16:49:40 -0500 Subject: [PATCH 29/39] Skeleton properties panel start --- addons/io_scene_swbf_msh/__init__.py | 12 ++++ .../msh_skeleton_properties.py | 59 +++++++++++++++++++ addons/io_scene_swbf_msh/msh_to_blend.py | 11 ++++ 3 files changed, 82 insertions(+) create mode 100644 addons/io_scene_swbf_msh/msh_skeleton_properties.py diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index 5e0d95f..a9f2313 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -60,6 +60,7 @@ from .msh_scene import create_scene from .msh_scene_save import save_scene from .msh_scene_read import read_scene from .msh_material_properties import * +from .msh_skeleton_properties import * from .msh_to_blend import * @@ -186,19 +187,30 @@ def menu_func_import(self, context): def register(): bpy.utils.register_class(MaterialProperties) bpy.utils.register_class(MaterialPropertiesPanel) + + bpy.utils.register_class(SkeletonProperties) + bpy.utils.register_class(SkeletonPropertiesPanel) + bpy.utils.register_class(ExportMSH) bpy.utils.register_class(ImportMSH) bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + bpy.types.Material.swbf_msh = bpy.props.PointerProperty(type=MaterialProperties) + bpy.types.Armature.swbf_msh_skel = bpy.props.CollectionProperty(type=SkeletonProperties) def unregister(): bpy.utils.unregister_class(MaterialProperties) bpy.utils.unregister_class(MaterialPropertiesPanel) + + bpy.utils.unregister_class(SkeletonProperties) + bpy.utils.unregister_class(SkeletonPropertiesPanel) + bpy.utils.unregister_class(ExportMSH) bpy.utils.unregister_class(ImportMSH) + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/addons/io_scene_swbf_msh/msh_skeleton_properties.py b/addons/io_scene_swbf_msh/msh_skeleton_properties.py new file mode 100644 index 0000000..4835975 --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_skeleton_properties.py @@ -0,0 +1,59 @@ +""" Contains Blender properties and UI for .msh materials. """ + +import bpy +from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatVectorProperty, IntProperty +from bpy.types import PropertyGroup +from .msh_material_ui_strings import * +from .msh_model import * + + +class SkeletonProperties(PropertyGroup): + name: StringProperty(name="Name", default="Bone Name") + parent: StringProperty(name="Parent", default="Bone Parent") + loc: FloatVectorProperty(name="Local Position", default=(0.0, 0.0, 0.0), subtype="XYZ", size=3) + rot: FloatVectorProperty(name="Local Rotation", default=(0.0, 0.0, 0.0, 0.0), subtype="QUATERNION", size=4) + + + + + +class SkeletonPropertiesPanel(bpy.types.Panel): + """ Creates a Panel in the Object properties window """ + bl_label = "SWBF Skeleton Properties" + bl_idname = "SKELETON_PT_swbf_msh" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "data" + bl_options = {'DEFAULT_CLOSED'} + + + @classmethod + def poll(cls, context): + return context.object.type == 'ARMATURE' + + + def draw(self, context): + if context.object is None: + return + + layout = self.layout + + skel_props = context.object.data.swbf_msh_skel + + for prop in skel_props: + layout.prop(prop, "name") + layout.prop(prop, "parent") + layout.prop(prop, "loc") + layout.prop(prop, "rot") + + + ''' + layout.prop(skel_props, "name") + layout.prop(skel_props, "parent") + layout.prop(skel_props, "loc") + layout.prop(skel_props, "rot") + ''' + + + #self.layout.label(text=context.object.swbf_msh_skel.yolo[1]) + \ No newline at end of file diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index 93fd4c7..f6132d7 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -12,6 +12,7 @@ from .msh_model import * from .msh_model_utilities import * from .msh_utilities import * from .msh_model_gather import * +from .msh_skeleton_properties import * from .crc import * import os @@ -126,6 +127,16 @@ def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): bpy.context.view_layer.active_layer_collection.collection.objects.link(armature_obj) armature_obj.select_set(True) + preserved = armature_obj.data.swbf_msh_skel + for model in refined_skeleton: + loc,rot,_ = model_map[model.name].matrix_world.decompose() + print(str(loc)) + entry = preserved.add() + entry.name = model.name + entry.loc = loc + entry.rot = rot + entry.parent = model.parent + bpy.context.view_layer.objects.active = armature_obj bpy.ops.object.mode_set(mode='EDIT') From e67e675ee7e36f6b098e8ac2c5a2757b54e174a3 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Thu, 7 Jan 2021 00:26:19 -0500 Subject: [PATCH 30/39] zaabin import --- addons/io_scene_swbf_msh/__init__.py | 20 +- addons/io_scene_swbf_msh/zaa_reader.py | 137 +++++++++ addons/io_scene_swbf_msh/zaa_to_blend.py | 357 +++++++++++++++++++++++ 3 files changed, 506 insertions(+), 8 deletions(-) create mode 100644 addons/io_scene_swbf_msh/zaa_reader.py create mode 100644 addons/io_scene_swbf_msh/zaa_to_blend.py diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index a9f2313..bc29a21 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -62,6 +62,7 @@ from .msh_scene_read import read_scene from .msh_material_properties import * from .msh_skeleton_properties import * from .msh_to_blend import * +from .zaa_to_blend import * class ExportMSH(Operator, ExportHelper): @@ -152,7 +153,7 @@ class ImportMSH(Operator, ImportHelper): ) filter_glob: StringProperty( - default="*.msh", + default="*.msh;*.zaa;*.zaabin", options={'HIDDEN'}, maxlen=255, # Max internal buffer length, longer would be clamped. ) @@ -168,13 +169,16 @@ class ImportMSH(Operator, ImportHelper): dirname = os.path.dirname(self.filepath) for file in self.files: filepath = os.path.join(dirname, file.name) - with open(filepath, 'rb') as input_file: - scene = read_scene(input_file, self.animation_only) - - if not self.animation_only: - extract_scene(filepath, scene) - else: - extract_and_apply_anim(filepath, scene) + if filepath.endswith(".zaabin") or filepath.endswith(".zaa"): + extract_and_apply_munged_anim(filepath) + else: + with open(filepath, 'rb') as input_file: + scene = read_scene(input_file, self.animation_only) + + if not self.animation_only: + extract_scene(filepath, scene) + else: + extract_and_apply_anim(filepath, scene) return {'FINISHED'} diff --git a/addons/io_scene_swbf_msh/zaa_reader.py b/addons/io_scene_swbf_msh/zaa_reader.py new file mode 100644 index 0000000..f80eef3 --- /dev/null +++ b/addons/io_scene_swbf_msh/zaa_reader.py @@ -0,0 +1,137 @@ + +import io +import struct +import os + +class ZAAReader: + def __init__(self, file, parent=None, indent=0): + self.file = file + self.size: int = 0 + self.size_pos = None + self.parent = parent + self.indent = " " * indent #for print debugging + + + def __enter__(self): + self.size_pos = self.file.tell() + + if self.parent is not None: + self.header = self.read_bytes(4).decode("utf-8") + else: + self.header = "HEAD" + + if self.parent is not None: + self.size = self.read_u32() + else: + self.size = os.path.getsize(self.file.name) - 8 + + padding_length = 4 - (self.size % 4) if self.size % 4 > 0 else 0 + self.end_pos = self.size_pos + padding_length + self.size + 8 + + if self.parent is not None: + print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) + else: + print(self.indent + "Begin head, Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) + + + return self + + + def __exit__(self, exc_type, exc_value, traceback): + if self.size > self.MAX_SIZE: + raise OverflowError(f".msh file overflowed max size. size = {self.size} MAX_SIZE = {self.MAX_SIZE}") + + print(self.indent + "End " + self.header) + self.file.seek(self.end_pos) + + + + def read_bytes(self,num_bytes): + return self.file.read(num_bytes) + + + def read_string(self): + last_byte = self.read_bytes(1) + result = b'' + while last_byte[0] != 0x0: + result += last_byte + last_byte = self.read_bytes(1) + + return result.decode("utf-8") + + def read_i8(self, num=1): + buf = self.read_bytes(num) + result = struct.unpack(f"<{num}b", buf) + return result[0] if num == 1 else result + + def read_u8(self, num=1): + buf = self.read_bytes(num) + result = struct.unpack(f"<{num}B", buf) + return result[0] if num == 1 else result + + def read_i16(self, num=1): + buf = self.read_bytes(num * 2) + result = struct.unpack(f"<{num}h", buf) + return result[0] if num == 1 else result + + def read_u16(self, num=1): + buf = self.read_bytes(num * 2) + result = struct.unpack(f"<{num}H", buf) + return result[0] if num == 1 else result + + def read_i32(self, num=1): + buf = self.read_bytes(num * 4) + result = struct.unpack(f"<{num}i", buf) + return result[0] if num == 1 else result + + def read_u32(self, num=1): + buf = self.read_bytes(num * 4) + result = struct.unpack(f"<{num}I", buf) + return result[0] if num == 1 else result + + def read_f32(self, num=1): + buf = self.read_bytes(num * 4) + result = struct.unpack(f"<{num}f", buf) + return result[0] if num == 1 else result + + + + def read_child(self): + child = ZAAReader(self.file, parent=self, indent=int(len(self.indent) / 2) + 1) + return child + + + def skip_bytes(self,num): + self.file.seek(num,1) + + + def peak_next_header(self): + + buf = self.read_bytes(4); + self.file.seek(-4,1) + + try: + result = buf.decode("utf-8") + return result + except: + return "" + + def get_current_pos(self): + return self.file.tell() + + def reset_pos(self): + self.file.seek(self.size_pos - self.file.tell() + 8, 1) + + def how_much_left(self, pos): + return self.end_pos - pos + + def skip_until(self, header): + while (self.could_have_child() and header not in self.peak_next_header()): + self.skip_bytes(1) + + + def could_have_child(self): + return self.end_pos - self.file.tell() >= 8 + + + MAX_SIZE: int = 2147483647 - 8 diff --git a/addons/io_scene_swbf_msh/zaa_to_blend.py b/addons/io_scene_swbf_msh/zaa_to_blend.py new file mode 100644 index 0000000..c8d7715 --- /dev/null +++ b/addons/io_scene_swbf_msh/zaa_to_blend.py @@ -0,0 +1,357 @@ +import os +import bpy +import re + +from .zaa_reader import * +from .crc import * + +from .msh_model import * +from .msh_model_utilities import * +from .msh_utilities import * + +from typing import List, Set, Dict, Tuple + + + + #anims #bones #comps #keyframes: index,value +def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]]]: + + decompressed_anims: Dict[int, Dict[int, List[ Dict[int,float]]]] = {} + + with ZAAReader(input_file) as head: + head.skip_until("SMNA") + head.skip_bytes(20) + num_anims = head.read_u16() + + print("\nFile contains {} animations\n".format(num_anims)) + + head.skip_bytes(2) + + anim_crcs = [] + anim_metadata = {} + + with head.read_child() as mina: + + for i in range(num_anims): + mina.skip_bytes(8) + + anim_hash = mina.read_u32() + anim_crcs += [anim_hash] + + anim_data = {} + anim_data["num_frames"] = mina.read_u16() + anim_data["num_bones"] = mina.read_u16() + + anim_metadata[anim_hash] = anim_data + + + with head.read_child() as tnja: + + for i,anim_crc in enumerate(anim_crcs): + + bone_params = {} + + for _ in range(anim_metadata[anim_crc]["num_bones"]): + + bone_hash = tnja.read_u32() + + rot_offsets = [tnja.read_u32() for _ in range(4)] + loc_offsets = [tnja.read_u32() for _ in range(3)] + + qparams = [tnja.read_f32() for _ in range(4)] + + params = {"rot_offsets" : rot_offsets, "loc_offsets" : loc_offsets, "qparams" : qparams} + + bone_params[bone_hash] = params + + anim_metadata[anim_crc]["bone_params"] = bone_params + + + with head.read_child() as tada: + + for anim_crc in anim_crcs: + + decompressed_anims[anim_crc] = {} + + num_frames = anim_metadata[anim_crc]["num_frames"] + num_bones = anim_metadata[anim_crc]["num_bones"] + + #print("\n\tAnim hash: {} Num frames: {} Num joints: {}".format(hex(anim_crc), num_frames, num_bones)) + + #num_frames = 5 + + for bone_num, bone_hash in enumerate(anim_metadata[anim_crc]["bone_params"]): + + + + keyframes = [] + + params_bone = anim_metadata[anim_crc]["bone_params"][bone_hash] + + offsets_list = params_bone["rot_offsets"] + params_bone["loc_offsets"] + qparams = params_bone["qparams"] + + #print("\n\t\tBone #{} hash: {}".format(bone_num,hex(bone_hash))) + #print("\n\t\tQParams: {}, {}, {}, {}".format(*qparams)) + + for o,start_offset in enumerate(offsets_list): + tada.skip_bytes(start_offset) + + curve = {} + val = 0.0 + + if o < 4: + mult = 1 / 2047 + offset = 0.0 + else: + mult = qparams[-1] + offset = qparams[o - 4] + + #print("\n\t\t\tBias = {}, multiplier = {}".format(offset, mult)) + + #print("\n\t\t\tOffset {}: {} ({}, {} remaining)".format(o,start_offset, tada.get_current_pos(), tada.how_much_left(tada.get_current_pos()))) + + j = 0 + exit_loop = False + while (j < num_frames and not exit_loop): + val = offset + mult * tada.read_i16() + curve[j if j < num_frames else num_frames] = val + + #print("\t\t\t\t{}: {}".format(j, val)) + j+=1 + + if (j >= num_frames): + break + + while (True): + + if (j >= num_frames): + exit_loop = True + break + + control = tada.read_i8() + + if control == 0x00: + #curve[j if j < num_frames else num_frames] = val + #print("\t\t\t\tControl: HOLDING FOR A FRAME") + #print("\t\t\t\t{}: {}".format(j, val)) + j+=1 + + if (j >= num_frames): + break + + elif control == -0x7f: + #print("\t\t\t\tControl: READING NEXT FRAME") + break #get ready for new frame + + elif control == -0x80: + num_skips = tada.read_u8() + #print("\t\t\t\tControl: HOLDING FOR {} FRAMES".format(num_skips)) + + for _ in range(num_skips): + j+=1 + + if (j >= num_frames): + break + + else: + val += mult * float(control) + curve[j if j < num_frames else num_frames] = val + + #print("\t\t\t\t{}: {}".format(j, val)) + j+=1 + + curve[num_frames - 1] = val + + tada.reset_pos() + + keyframes.append(curve) + + decompressed_anims[anim_crc][bone_hash] = keyframes + + return decompressed_anims + + + +def read_anims_file(anims_file_path): + + if not os.path.exists(anims_file_path): + return None + + anims_text = "" + with open(anims_file_path, 'r') as file: + anims_text = file.read() + + splits = anims_text.split('"') + + if len(splits) > 1: + return splits[1:-1:2] + + return None + + + + + + + + + + +def extract_and_apply_munged_anim(input_file_path): + + with open(input_file_path,"rb") as input_file: + discrete_curves = decompress_curves(input_file) + + anim_names = None + if input_file_path.endswith(".zaabin"): + anim_names = read_anims_file(input_file_path.replace(".zaabin", ".anims")) + + + arma = bpy.context.view_layer.objects.active + if arma.type != 'ARMATURE': + raise Exception("Select an armature to attach the imported animation to!") + + if arma.animation_data is not None: + arma.animation_data_clear() + arma.animation_data_create() + + + bone_bind_poses = {} + + for bone in arma.data.bones: + bone_obj = bpy.data.objects[bone.name] + bone_obj_parent = bone_obj.parent + + bind_mat = bone_obj.matrix_local + stack_mat = Matrix.Identity(4) + + + while(True): + if bone_obj_parent is None or bone_obj_parent.name in arma.data.bones: + break + bind_mat = bone_obj_parent.matrix_local @ bind_mat + stack_mat = bone_obj_parent.matrix_local @ stack_mat + bone_obj_parent = bone_obj_parent.parent + + bone_bind_poses[bone.name] = bind_mat.inverted() @ stack_mat + + + + for anim_crc in discrete_curves: + + anim_str = str(hex(anim_crc)) + if anim_names is not None: + for anim_name in anim_names: + if anim_crc == crc(anim_name): + anim_str = anim_name + + #if crc(anim_name) not in discrete_curves: + # continue + + #print("\nExtracting anim: " + anim_crc_str) + + if anim_str in bpy.data.actions: + bpy.data.actions[anim_str].use_fake_user = False + bpy.data.actions.remove(bpy.data.actions[anim_str]) + + action = bpy.data.actions.new(anim_str) + action.use_fake_user = True + + anim_curves = discrete_curves[anim_crc] + + for bone in arma.pose.bones: + bone_crc = crc(bone.name) + + #print("\tGetting curves for bone: " + bone.name) + + if bone_crc not in anim_curves: + continue; + + bind_mat = bone_bind_poses[bone.name] + loc_data_path = "pose.bones[\"{}\"].location".format(bone.name) + rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name) + + bone_curves = anim_curves[bone_crc] + num_frames = max(bone_curves[0]) + + #print("\t\tNum frames: " + str(num_frames)) + + last_values = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + + def get_quat(index): + nonlocal bone_curves, last_values + + q = Quaternion() + valmap = [1,2,3,0] + #valmap = [0,1,2,3] + + has_key = False + + for i in range(4): + curve = bone_curves[i] + if index in curve: + has_key = True + last_values[i] = curve[index] + q[valmap[i]] = last_values[i] + + return q if has_key else None + + def get_vec(index): + nonlocal bone_curves, last_values + + v = Vector() + has_key = False + + for i in range(4,7): + curve = bone_curves[i] + if index in curve: + has_key = True + last_values[i] = curve[index] + v[i - 4] = last_values[i] + + return v if has_key else None + + + fcurve_rot_w = action.fcurves.new(rot_data_path, index=0, action_group=bone.name) + fcurve_rot_x = action.fcurves.new(rot_data_path, index=1, action_group=bone.name) + fcurve_rot_y = action.fcurves.new(rot_data_path, index=2, action_group=bone.name) + fcurve_rot_z = action.fcurves.new(rot_data_path, index=3, action_group=bone.name) + + fcurve_loc_x = action.fcurves.new(loc_data_path, index=0, action_group=bone.name) + fcurve_loc_y = action.fcurves.new(loc_data_path, index=1, action_group=bone.name) + fcurve_loc_z = action.fcurves.new(loc_data_path, index=2, action_group=bone.name) + + for frame in range(num_frames): + + q = get_quat(frame) + if q is not None: + q = (bind_mat @ convert_rotation_space(q).to_matrix().to_4x4()).to_quaternion() + fcurve_rot_w.keyframe_points.insert(frame,q.w) + fcurve_rot_x.keyframe_points.insert(frame,q.x) + fcurve_rot_y.keyframe_points.insert(frame,q.y) + fcurve_rot_z.keyframe_points.insert(frame,q.z) + + t = get_vec(frame) + if t is not None: + t = (bind_mat @ Matrix.Translation(convert_vector_space(t))).translation + fcurve_loc_x.keyframe_points.insert(frame,t.x) + fcurve_loc_y.keyframe_points.insert(frame,t.y) + fcurve_loc_z.keyframe_points.insert(frame,t.z) + + arma.animation_data.action = action + + + + + + + + + + + + + + + From e0a71bc89907c46265eed5ed1e9bbd7daa26f9c9 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Thu, 7 Jan 2021 01:05:09 -0500 Subject: [PATCH 31/39] Cleanup, reader and extraction code commented --- addons/io_scene_swbf_msh/zaa_reader.py | 8 + addons/io_scene_swbf_msh/zaa_to_blend.py | 194 ++++++++++++----------- 2 files changed, 108 insertions(+), 94 deletions(-) diff --git a/addons/io_scene_swbf_msh/zaa_reader.py b/addons/io_scene_swbf_msh/zaa_reader.py index f80eef3..89855ef 100644 --- a/addons/io_scene_swbf_msh/zaa_reader.py +++ b/addons/io_scene_swbf_msh/zaa_reader.py @@ -1,3 +1,11 @@ +""" +Basically the same as msh reader but with a couple additional +methods for making TADA easier to navigate and treats the whole +file as an initial dummy chunk to avoid the oddities of SMNA and +to handle both zaa and zaabin. +""" + + import io import struct diff --git a/addons/io_scene_swbf_msh/zaa_to_blend.py b/addons/io_scene_swbf_msh/zaa_to_blend.py index c8d7715..02da5f9 100644 --- a/addons/io_scene_swbf_msh/zaa_to_blend.py +++ b/addons/io_scene_swbf_msh/zaa_to_blend.py @@ -1,3 +1,11 @@ +""" +Script for reading zaabin/zaa files and applying the unmunged animation +to the currently selected armature. + +As regards decompress_curves, I should really make a separate AnimationSet +dataclass instead of returning a convoluted nested dict. +""" + import os import bpy import re @@ -19,54 +27,50 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] decompressed_anims: Dict[int, Dict[int, List[ Dict[int,float]]]] = {} with ZAAReader(input_file) as head: + + # Dont read SMNA as child, since it has a length field always set to 0... head.skip_until("SMNA") head.skip_bytes(20) num_anims = head.read_u16() - print("\nFile contains {} animations\n".format(num_anims)) + #print("\nFile contains {} animations\n".format(num_anims)) head.skip_bytes(2) anim_crcs = [] anim_metadata = {} + # Read metadata (crc, num frames, num bones) for each anim with head.read_child() as mina: for i in range(num_anims): mina.skip_bytes(8) - anim_hash = mina.read_u32() - anim_crcs += [anim_hash] - - anim_data = {} - anim_data["num_frames"] = mina.read_u16() - anim_data["num_bones"] = mina.read_u16() - - anim_metadata[anim_hash] = anim_data + anim_crc = mina.read_u32() + anim_crcs.append(anim_crc) + anim_metadata[anim_crc] = {"num_frames" : mina.read_u16(), "num_bones" : mina.read_u16()} + # Read TADA offsets and quantization parameters for each rot + loc component, for each bone, for each anim with head.read_child() as tnja: - for i,anim_crc in enumerate(anim_crcs): + for i, anim_crc in enumerate(anim_crcs): bone_params = {} for _ in range(anim_metadata[anim_crc]["num_bones"]): - bone_hash = tnja.read_u32() + bone_crc = tnja.read_u32() - rot_offsets = [tnja.read_u32() for _ in range(4)] - loc_offsets = [tnja.read_u32() for _ in range(3)] - - qparams = [tnja.read_f32() for _ in range(4)] - - params = {"rot_offsets" : rot_offsets, "loc_offsets" : loc_offsets, "qparams" : qparams} - - bone_params[bone_hash] = params + bone_params[bone_crc] = { + "rot_offsets" : [tnja.read_u32() for _ in range(4)], # Offsets into TADA for rotation + "loc_offsets" : [tnja.read_u32() for _ in range(3)], # and translation curves + "qparams" : [tnja.read_f32() for _ in range(4)], # Translation quantization parameters, 3 biases, 1 multiplier + } anim_metadata[anim_crc]["bone_params"] = bone_params - + # Decompress/dequantize frame data into discrete per-component curves with head.read_child() as tada: for anim_crc in anim_crcs: @@ -78,107 +82,100 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] #print("\n\tAnim hash: {} Num frames: {} Num joints: {}".format(hex(anim_crc), num_frames, num_bones)) - #num_frames = 5 + for bone_num, bone_crc in enumerate(anim_metadata[anim_crc]["bone_params"]): - for bone_num, bone_hash in enumerate(anim_metadata[anim_crc]["bone_params"]): + bone_curves = [] - - - keyframes = [] - - params_bone = anim_metadata[anim_crc]["bone_params"][bone_hash] + params_bone = anim_metadata[anim_crc]["bone_params"][bone_crc] offsets_list = params_bone["rot_offsets"] + params_bone["loc_offsets"] qparams = params_bone["qparams"] - #print("\n\t\tBone #{} hash: {}".format(bone_num,hex(bone_hash))) + #print("\n\t\tBone #{} hash: {}".format(bone_num,hex(bone_crc))) #print("\n\t\tQParams: {}, {}, {}, {}".format(*qparams)) - for o,start_offset in enumerate(offsets_list): + for o, start_offset in enumerate(offsets_list): + + # Skip to start of compressed data for component, as specified in TNJA tada.skip_bytes(start_offset) - curve = {} - val = 0.0 + # Init curve dict + curve : Dict[int,float] = {} + # Init accumulator + accumulator = 0.0 + + + # 2047 = max val of signed 12 bit int, the (overwhelmingly) common compression amount. + # This is used for all rotation components in the file, with no offset if o < 4: mult = 1 / 2047 - offset = 0.0 + bias = 0.0 + + # Translations have specific quantization parameters; biases for each component and + # a single multiplier for all three else: mult = qparams[-1] - offset = qparams[o - 4] + bias = qparams[o - 4] - #print("\n\t\t\tBias = {}, multiplier = {}".format(offset, mult)) + #print("\n\t\t\tBias = {}, multiplier = {}".format(bias, mult)) #print("\n\t\t\tOffset {}: {} ({}, {} remaining)".format(o,start_offset, tada.get_current_pos(), tada.how_much_left(tada.get_current_pos()))) j = 0 - exit_loop = False - while (j < num_frames and not exit_loop): - val = offset + mult * tada.read_i16() - curve[j if j < num_frames else num_frames] = val + while (j < num_frames): + accumulator = bias + mult * tada.read_i16() + curve[j if j < num_frames else num_frames] = accumulator - #print("\t\t\t\t{}: {}".format(j, val)) + #print("\t\t\t\t{}: {}".format(j, accumulator)) j+=1 - if (j >= num_frames): - break - - while (True): - - if (j >= num_frames): - exit_loop = True - break + while (j < num_frames): control = tada.read_i8() - if control == 0x00: - #curve[j if j < num_frames else num_frames] = val - #print("\t\t\t\tControl: HOLDING FOR A FRAME") - #print("\t\t\t\t{}: {}".format(j, val)) - j+=1 - - if (j >= num_frames): - break - - elif control == -0x7f: + # Reset the accumulator to next dequantized i16 + if control == -0x7f: #print("\t\t\t\tControl: READING NEXT FRAME") - break #get ready for new frame + break + # RLE: hold current accumulator for the next u8 frames elif control == -0x80: num_skips = tada.read_u8() #print("\t\t\t\tControl: HOLDING FOR {} FRAMES".format(num_skips)) + j += num_skips - for _ in range(num_skips): - j+=1 - - if (j >= num_frames): - break - + # If not a special value, increment accumulator by the dequantized i8 + # The bias is NOT applied here, only for accumulator resets else: - val += mult * float(control) - curve[j if j < num_frames else num_frames] = val + accumulator += mult * float(control) + curve[j if j < num_frames else num_frames] = accumulator - #print("\t\t\t\t{}: {}".format(j, val)) + #print("\t\t\t\t{}: {}".format(j, accumulator)) j+=1 - curve[num_frames - 1] = val + curve[num_frames - 1] = accumulator tada.reset_pos() - keyframes.append(curve) + bone_curves.append(curve) - decompressed_anims[anim_crc][bone_hash] = keyframes + decompressed_anims[anim_crc][bone_crc] = bone_curves return decompressed_anims +''' +Gets the animation names from the supplied +.anims file. Handy since .zaabin files often +share a dir with a .anims file. +''' def read_anims_file(anims_file_path): if not os.path.exists(anims_file_path): - return None + return [] - anims_text = "" with open(anims_file_path, 'r') as file: anims_text = file.read() @@ -187,27 +184,27 @@ def read_anims_file(anims_file_path): if len(splits) > 1: return splits[1:-1:2] - return None - - - - - + return [] +''' +Unmunge the .zaa(bin) file and apply the resulting animation +to the currently selected armature object. +Contains some bloated code for calculating the world transforms of each bone, +for now this will work ONLY if the model was directly imported from a .msh file. +''' def extract_and_apply_munged_anim(input_file_path): with open(input_file_path,"rb") as input_file: - discrete_curves = decompress_curves(input_file) + animation_set = decompress_curves(input_file) - anim_names = None + anim_names = [] if input_file_path.endswith(".zaabin"): anim_names = read_anims_file(input_file_path.replace(".zaabin", ".anims")) - arma = bpy.context.view_layer.objects.active if arma.type != 'ARMATURE': raise Exception("Select an armature to attach the imported animation to!") @@ -217,6 +214,17 @@ def extract_and_apply_munged_anim(input_file_path): arma.animation_data_create() + + """ + When directly imported from .msh files, + all skeleton models are saved as emptys, since + some are excluded from the actual armature (effectors, roots, eg...). + + bond_bind_poses contains matrices for converting the transform of + bones found in .msh/.zaabin files to ones that'll fit the extracted armature. + This will be replaced with the eventual importer release. + """ + bone_bind_poses = {} for bone in arma.data.bones: @@ -238,16 +246,13 @@ def extract_and_apply_munged_anim(input_file_path): - for anim_crc in discrete_curves: + for anim_crc in animation_set: - anim_str = str(hex(anim_crc)) - if anim_names is not None: - for anim_name in anim_names: - if anim_crc == crc(anim_name): - anim_str = anim_name - - #if crc(anim_name) not in discrete_curves: - # continue + found_anim = [anim_name for anim_name in anim_names if crc(anim_name) == anim_crc] + if found_anim: + anim_str = found_anim[0] + else: + anim_str = str(hex(anim_crc)) #print("\nExtracting anim: " + anim_crc_str) @@ -258,21 +263,21 @@ def extract_and_apply_munged_anim(input_file_path): action = bpy.data.actions.new(anim_str) action.use_fake_user = True - anim_curves = discrete_curves[anim_crc] + animation = animation_set[anim_crc] for bone in arma.pose.bones: bone_crc = crc(bone.name) #print("\tGetting curves for bone: " + bone.name) - if bone_crc not in anim_curves: + if bone_crc not in animation: continue; bind_mat = bone_bind_poses[bone.name] loc_data_path = "pose.bones[\"{}\"].location".format(bone.name) rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name) - bone_curves = anim_curves[bone_crc] + bone_curves = animation[bone_crc] num_frames = max(bone_curves[0]) #print("\t\tNum frames: " + str(num_frames)) @@ -284,7 +289,6 @@ def extract_and_apply_munged_anim(input_file_path): q = Quaternion() valmap = [1,2,3,0] - #valmap = [0,1,2,3] has_key = False @@ -326,6 +330,7 @@ def extract_and_apply_munged_anim(input_file_path): q = get_quat(frame) if q is not None: + # Very bloated, but works for now q = (bind_mat @ convert_rotation_space(q).to_matrix().to_4x4()).to_quaternion() fcurve_rot_w.keyframe_points.insert(frame,q.w) fcurve_rot_x.keyframe_points.insert(frame,q.x) @@ -334,6 +339,7 @@ def extract_and_apply_munged_anim(input_file_path): t = get_vec(frame) if t is not None: + # '' t = (bind_mat @ Matrix.Translation(convert_vector_space(t))).translation fcurve_loc_x.keyframe_points.insert(frame,t.x) fcurve_loc_y.keyframe_points.insert(frame,t.y) From 6f2c1cf16822cb03548338be7208b91be0b8c8cf Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Thu, 7 Jan 2021 01:25:09 -0500 Subject: [PATCH 32/39] CRC rewrite --- addons/io_scene_swbf_msh/crc.py | 85 ++++----------------- addons/io_scene_swbf_msh/msh_anim_gather.py | 10 +-- addons/io_scene_swbf_msh/msh_scene_read.py | 2 +- addons/io_scene_swbf_msh/msh_to_blend.py | 12 +-- addons/io_scene_swbf_msh/zaa_to_blend.py | 4 +- 5 files changed, 30 insertions(+), 83 deletions(-) diff --git a/addons/io_scene_swbf_msh/crc.py b/addons/io_scene_swbf_msh/crc.py index 8b59617..daa69a8 100644 --- a/addons/io_scene_swbf_msh/crc.py +++ b/addons/io_scene_swbf_msh/crc.py @@ -1,15 +1,10 @@ -from struct import pack +''' +Based on code by Benedikt Schatz from https://github.com/Schlechtwetterfront/xsizetools/blob/master/Application/Modules/msh2_crc.py +''' -class CRCError(Exception): - def __init__(self, val): - self.val = val - - def __str__(self): - return '{0}'.format(self.val) - # CRC lookup table. -TABLE_32 = ( +table32_lookup = ( 0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9, 0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005, 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61, @@ -76,67 +71,19 @@ TABLE_32 = ( 0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4 ) -# Used to calculate the lowercase CRC. -TOLOWER = ( - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, - 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, - 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, - 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, - 0x40, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, - 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, - 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, - 0x78, 0x79, 0x7a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, - 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, - 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, - 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, - 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, - 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, - 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, - 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, - 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, - 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, - 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, - 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, - 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, - 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, - 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, - 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, - 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, - 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, - 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, - 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff -) +def to_lower(charcode): + if charcode <= 64 or charcode > 90: + return charcode + else: + return charcode + 32 -def return_lowest_bits(n): - '''Simulate unsigned behavior.''' - return n & 0xFFFFFFFF - - -def crc(string): - '''Calculate the Zero CRC from string and return it as number.''' - crc_ = 0 - crc_ = return_lowest_bits(~crc_) +# Not sure what Schlechtwetterfront means by "Simulate unsigned behavior.", +# kept it anyways just without the extra functions +def to_crc(string): + crc_ = ~0 & 0xFFFFFFFF if string: for char in string: - ind = (crc_ >> 24) - ind = ind ^ TOLOWER[ord(char)] - crc_ = return_lowest_bits(crc_ << 8) ^ TABLE_32[ind] - return return_lowest_bits(~crc_) - - -def strcrc(string): - '''Calculate the Zero CRC and return it in a structure - usable in .msh files.''' - return pack('> 24) ^ to_lower(ord(char)) + crc_ = ((crc_ << 8) & 0xFFFFFFFF) ^ table32_lookup[ind] + return ~crc_ & 0xFFFFFFFF \ No newline at end of file diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 100285d..52fc6c4 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -11,7 +11,7 @@ from .msh_model_utilities import * from .msh_utilities import * from .msh_model_gather import * -from .crc import crc +from .crc import to_crc def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: @@ -19,7 +19,7 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: action = armature.animation_data.action anim = Animation(); - root_crc = crc(root_name) + root_crc = to_crc(root_name) if not action: framerange = Vector((0.0,1.0)) @@ -33,7 +33,7 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: anim.bone_frames[root_crc] = ([], []) for bone in armature.data.bones: - anim.bone_frames[crc(bone.name)] = ([], []) + anim.bone_frames[to_crc(bone.name)] = ([], []) for frame in range(num_frames): @@ -60,8 +60,8 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: rframe = RotationFrame(frame, convert_rotation_space(rot)) tframe = TranslationFrame(frame, convert_vector_space(loc)) - anim.bone_frames[crc(bone.name)][0].append(tframe) - anim.bone_frames[crc(bone.name)][1].append(rframe) + anim.bone_frames[to_crc(bone.name)][0].append(tframe) + anim.bone_frames[to_crc(bone.name)][1].append(rframe) return anim diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index ba68a10..622b050 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -78,7 +78,7 @@ def read_scene(input_file, anim_only=False) -> Scene: print("Skeleton models: ") for model in scene.models: for i in range(len(scene.skeleton)): - if crc(model.name) == scene.skeleton[i]: + if to_crc(model.name) == scene.skeleton[i]: print("\t" + model.name) if model.model_type == ModelType.SKIN: scene.skeleton.pop(i) diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index f6132d7..d5b6e50 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -60,12 +60,12 @@ def extract_and_apply_anim(filename, scene): for bone in arma.pose.bones: - if crc(bone.name) in scene.animation.bone_frames: + if to_crc(bone.name) in scene.animation.bone_frames: #print("Inserting anim data for bone: {}".format(bone.name)) bind_mat = bone_bind_poses[bone.name] - translation_frames, rotation_frames = scene.animation.bone_frames[crc(bone.name)] + translation_frames, rotation_frames = scene.animation.bone_frames[to_crc(bone.name)] loc_data_path = "pose.bones[\"{}\"].location".format(bone.name) rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name) @@ -194,11 +194,11 @@ def extract_refined_skeleton(scene: Scene): for weight in weight_set: model_weighted_to = scene.models[weight.bone] - if crc(model_weighted_to.name) not in scene.skeleton: - scene.skeleton.append(crc(model_weighted_to.name)) + if to_crc(model_weighted_to.name) not in scene.skeleton: + scene.skeleton.append(to_crc(model_weighted_to.name)) for model in scene.models: - if crc(model.name) in scene.skeleton: + if to_crc(model.name) in scene.skeleton: skeleton_models.append(model) @@ -213,7 +213,7 @@ def extract_refined_skeleton(scene: Scene): while True: - if crc(curr_ancestor.name) in scene.skeleton or curr_ancestor.name == scene.models[0].name: + if to_crc(curr_ancestor.name) in scene.skeleton or curr_ancestor.name == scene.models[0].name: new_model = Model() new_model.name = bone.name new_model.parent = curr_ancestor.name if curr_ancestor.name != scene.models[0].name else "" diff --git a/addons/io_scene_swbf_msh/zaa_to_blend.py b/addons/io_scene_swbf_msh/zaa_to_blend.py index 02da5f9..e59859a 100644 --- a/addons/io_scene_swbf_msh/zaa_to_blend.py +++ b/addons/io_scene_swbf_msh/zaa_to_blend.py @@ -248,7 +248,7 @@ def extract_and_apply_munged_anim(input_file_path): for anim_crc in animation_set: - found_anim = [anim_name for anim_name in anim_names if crc(anim_name) == anim_crc] + found_anim = [anim_name for anim_name in anim_names if to_crc(anim_name) == anim_crc] if found_anim: anim_str = found_anim[0] else: @@ -266,7 +266,7 @@ def extract_and_apply_munged_anim(input_file_path): animation = animation_set[anim_crc] for bone in arma.pose.bones: - bone_crc = crc(bone.name) + bone_crc = to_crc(bone.name) #print("\tGetting curves for bone: " + bone.name) From c320310084a8ef454108829f4d2dbf5c3b4e3874 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Sun, 10 Jan 2021 12:25:03 -0500 Subject: [PATCH 33/39] zaa transforms properly readjusted wrt omitted effectors/roots --- addons/io_scene_swbf_msh/zaa_to_blend.py | 69 +++++++++++++++--------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/addons/io_scene_swbf_msh/zaa_to_blend.py b/addons/io_scene_swbf_msh/zaa_to_blend.py index e59859a..4ae2e52 100644 --- a/addons/io_scene_swbf_msh/zaa_to_blend.py +++ b/addons/io_scene_swbf_msh/zaa_to_blend.py @@ -44,12 +44,18 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] with head.read_child() as mina: for i in range(num_anims): - mina.skip_bytes(8) + + transBitFlags = mina.read_u32() + mina.skip_bytes(4) anim_crc = mina.read_u32() anim_crcs.append(anim_crc) - anim_metadata[anim_crc] = {"num_frames" : mina.read_u16(), "num_bones" : mina.read_u16()} + anim_metadata[anim_crc] = { + "num_frames" : mina.read_u16(), + "num_bones" : mina.read_u16(), + "transBitFlags" : transBitFlags, + } # Read TADA offsets and quantization parameters for each rot + loc component, for each bone, for each anim with head.read_child() as tnja: @@ -57,10 +63,13 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] for i, anim_crc in enumerate(anim_crcs): bone_params = {} + bone_list = [] for _ in range(anim_metadata[anim_crc]["num_bones"]): - bone_crc = tnja.read_u32() + bone_crc = tnja.read_u32() + + bone_list.append(bone_crc) bone_params[bone_crc] = { "rot_offsets" : [tnja.read_u32() for _ in range(4)], # Offsets into TADA for rotation @@ -69,6 +78,7 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] } anim_metadata[anim_crc]["bone_params"] = bone_params + anim_metadata[anim_crc]["bone_list"] = bone_list # Decompress/dequantize frame data into discrete per-component curves with head.read_child() as tada: @@ -80,9 +90,11 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] num_frames = anim_metadata[anim_crc]["num_frames"] num_bones = anim_metadata[anim_crc]["num_bones"] + transBitFlags = anim_metadata[anim_crc]["transBitFlags"] + #print("\n\tAnim hash: {} Num frames: {} Num joints: {}".format(hex(anim_crc), num_frames, num_bones)) - for bone_num, bone_crc in enumerate(anim_metadata[anim_crc]["bone_params"]): + for bone_num, bone_crc in enumerate(anim_metadata[anim_crc]["bone_list"]): bone_curves = [] @@ -96,9 +108,6 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] for o, start_offset in enumerate(offsets_list): - # Skip to start of compressed data for component, as specified in TNJA - tada.skip_bytes(start_offset) - # Init curve dict curve : Dict[int,float] = {} @@ -115,6 +124,12 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] # Translations have specific quantization parameters; biases for each component and # a single multiplier for all three else: + + if (0x00000001 << bone_num) & transBitFlags == 0: + bone_curves.append(None) + continue + + mult = qparams[-1] bias = qparams[o - 4] @@ -122,6 +137,10 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] #print("\n\t\t\tOffset {}: {} ({}, {} remaining)".format(o,start_offset, tada.get_current_pos(), tada.how_much_left(tada.get_current_pos()))) + # Skip to start of compressed data for component, as specified in TNJA + tada.skip_bytes(start_offset) + + j = 0 while (j < num_frames): accumulator = bias + mult * tada.read_i16() @@ -232,17 +251,14 @@ def extract_and_apply_munged_anim(input_file_path): bone_obj_parent = bone_obj.parent bind_mat = bone_obj.matrix_local - stack_mat = Matrix.Identity(4) - while(True): if bone_obj_parent is None or bone_obj_parent.name in arma.data.bones: break bind_mat = bone_obj_parent.matrix_local @ bind_mat - stack_mat = bone_obj_parent.matrix_local @ stack_mat bone_obj_parent = bone_obj_parent.parent - bone_bind_poses[bone.name] = bind_mat.inverted() @ stack_mat + bone_bind_poses[bone.name] = bind_mat.inverted() @@ -254,8 +270,6 @@ def extract_and_apply_munged_anim(input_file_path): else: anim_str = str(hex(anim_crc)) - #print("\nExtracting anim: " + anim_crc_str) - if anim_str in bpy.data.actions: bpy.data.actions[anim_str].use_fake_user = False bpy.data.actions.remove(bpy.data.actions[anim_str]) @@ -268,8 +282,6 @@ def extract_and_apply_munged_anim(input_file_path): for bone in arma.pose.bones: bone_crc = to_crc(bone.name) - #print("\tGetting curves for bone: " + bone.name) - if bone_crc not in animation: continue; @@ -280,6 +292,8 @@ def extract_and_apply_munged_anim(input_file_path): bone_curves = animation[bone_crc] num_frames = max(bone_curves[0]) + has_translation = bone_curves[4] is not None + #print("\t\tNum frames: " + str(num_frames)) last_values = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] @@ -322,14 +336,16 @@ def extract_and_apply_munged_anim(input_file_path): fcurve_rot_y = action.fcurves.new(rot_data_path, index=2, action_group=bone.name) fcurve_rot_z = action.fcurves.new(rot_data_path, index=3, action_group=bone.name) - fcurve_loc_x = action.fcurves.new(loc_data_path, index=0, action_group=bone.name) - fcurve_loc_y = action.fcurves.new(loc_data_path, index=1, action_group=bone.name) - fcurve_loc_z = action.fcurves.new(loc_data_path, index=2, action_group=bone.name) + if has_translation: + fcurve_loc_x = action.fcurves.new(loc_data_path, index=0, action_group=bone.name) + fcurve_loc_y = action.fcurves.new(loc_data_path, index=1, action_group=bone.name) + fcurve_loc_z = action.fcurves.new(loc_data_path, index=2, action_group=bone.name) for frame in range(num_frames): q = get_quat(frame) if q is not None: + # Very bloated, but works for now q = (bind_mat @ convert_rotation_space(q).to_matrix().to_4x4()).to_quaternion() fcurve_rot_w.keyframe_points.insert(frame,q.w) @@ -337,13 +353,16 @@ def extract_and_apply_munged_anim(input_file_path): fcurve_rot_y.keyframe_points.insert(frame,q.y) fcurve_rot_z.keyframe_points.insert(frame,q.z) - t = get_vec(frame) - if t is not None: - # '' - t = (bind_mat @ Matrix.Translation(convert_vector_space(t))).translation - fcurve_loc_x.keyframe_points.insert(frame,t.x) - fcurve_loc_y.keyframe_points.insert(frame,t.y) - fcurve_loc_z.keyframe_points.insert(frame,t.z) + if has_translation: + + t = get_vec(frame) + if t is not None: + + t = (bind_mat @ Matrix.Translation(convert_vector_space(t))).translation + + fcurve_loc_x.keyframe_points.insert(frame,t.x) + fcurve_loc_y.keyframe_points.insert(frame,t.y) + fcurve_loc_z.keyframe_points.insert(frame,t.z) arma.animation_data.action = action From 5eea77adf305b46180d63530b90d11785c78d4e2 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Fri, 7 Jan 2022 13:45:50 -0500 Subject: [PATCH 34/39] Skeleton impurities (roots,effectors) now included by default, zaa_reader and msh_reader combined in chunked_file_reader, skin and skeleton parentage issues worked out. TODO: Fill material properties on import, decide what to do with SkeletonProperties. --- .../{zaa_reader.py => chunked_file_reader.py} | 44 +- addons/io_scene_swbf_msh/msh_model_gather.py | 2 + addons/io_scene_swbf_msh/msh_reader.py | 122 ------ addons/io_scene_swbf_msh/msh_scene_read.py | 150 ++++--- addons/io_scene_swbf_msh/msh_scene_save.py | 3 - .../msh_skeleton_properties.py | 12 +- addons/io_scene_swbf_msh/msh_to_blend.py | 377 +++++++++++------- addons/io_scene_swbf_msh/zaa_to_blend.py | 114 ++++-- 8 files changed, 436 insertions(+), 388 deletions(-) rename addons/io_scene_swbf_msh/{zaa_reader.py => chunked_file_reader.py} (73%) delete mode 100644 addons/io_scene_swbf_msh/msh_reader.py diff --git a/addons/io_scene_swbf_msh/zaa_reader.py b/addons/io_scene_swbf_msh/chunked_file_reader.py similarity index 73% rename from addons/io_scene_swbf_msh/zaa_reader.py rename to addons/io_scene_swbf_msh/chunked_file_reader.py index 89855ef..7094613 100644 --- a/addons/io_scene_swbf_msh/zaa_reader.py +++ b/addons/io_scene_swbf_msh/chunked_file_reader.py @@ -1,23 +1,22 @@ """ -Basically the same as msh reader but with a couple additional -methods for making TADA easier to navigate and treats the whole -file as an initial dummy chunk to avoid the oddities of SMNA and -to handle both zaa and zaabin. +Reader class for both zaabin, zaa, and msh files. """ - - import io import struct import os -class ZAAReader: - def __init__(self, file, parent=None, indent=0): +from mathutils import Vector, Quaternion + + +class Reader: + def __init__(self, file, parent=None, indent=0, debug=False): self.file = file self.size: int = 0 self.size_pos = None self.parent = parent - self.indent = " " * indent #for print debugging + self.indent = " " * indent #for print debugging, should be stored as str so msh_scene_read can access it + self.debug = debug def __enter__(self): @@ -26,7 +25,7 @@ class ZAAReader: if self.parent is not None: self.header = self.read_bytes(4).decode("utf-8") else: - self.header = "HEAD" + self.header = "FILE" if self.parent is not None: self.size = self.read_u32() @@ -36,20 +35,22 @@ class ZAAReader: padding_length = 4 - (self.size % 4) if self.size % 4 > 0 else 0 self.end_pos = self.size_pos + padding_length + self.size + 8 - if self.parent is not None: - print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) - else: - print(self.indent + "Begin head, Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) - + if self.debug: + if self.parent is not None: + print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", At pos: " + str(self.size_pos)) + else: + print(self.indent + "Begin file, Size: " + str(self.size) + ", At pos: " + str(self.size_pos)) return self def __exit__(self, exc_type, exc_value, traceback): if self.size > self.MAX_SIZE: - raise OverflowError(f".msh file overflowed max size. size = {self.size} MAX_SIZE = {self.MAX_SIZE}") + raise OverflowError(f"File overflowed max size. size = {self.size} MAX_SIZE = {self.MAX_SIZE}") + + if self.debug: + print(self.indent + "End " + self.header) - print(self.indent + "End " + self.header) self.file.seek(self.end_pos) @@ -103,9 +104,16 @@ class ZAAReader: return result[0] if num == 1 else result + def read_quat(self): + rot = self.read_f32(4) + return Quaternion((rot[3], rot[0], rot[1], rot[2])) + + def read_vec(self): + return Vector(self.read_f32(3)) + def read_child(self): - child = ZAAReader(self.file, parent=self, indent=int(len(self.indent) / 2) + 1) + child = Reader(self.file, parent=self, indent=int(len(self.indent) / 2) + 1, debug=self.debug) return child diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 2c5fe99..1eade29 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -240,6 +240,8 @@ def get_is_model_hidden(obj: bpy.types.Object) -> bool: name = obj.name.lower() + if name.startswith("c_"): + return True if name.startswith("sv_"): return True if name.startswith("p_"): diff --git a/addons/io_scene_swbf_msh/msh_reader.py b/addons/io_scene_swbf_msh/msh_reader.py deleted file mode 100644 index c629b47..0000000 --- a/addons/io_scene_swbf_msh/msh_reader.py +++ /dev/null @@ -1,122 +0,0 @@ - -import io -import struct -from mathutils import Vector, Quaternion - -class Reader: - def __init__(self, file, parent=None, indent=0, debug=False): - self.file = file - self.size: int = 0 - self.size_pos = None - self.parent = parent - self.indent = " " * indent #for print debugging - self.debug = debug - - - def __enter__(self): - self.size_pos = self.file.tell() - self.header = self.read_bytes(4).decode("utf-8") - self.size = self.read_u32() - self.end_pos = self.size_pos + self.size + 8 - - if self.debug: - print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", Pos: " + str(self.size_pos)) - - return self - - - def __exit__(self, exc_type, exc_value, traceback): - if self.size > self.MAX_SIZE: - raise OverflowError(f".msh file overflowed max size. size = {self.size} MAX_SIZE = {self.MAX_SIZE}") - - if self.debug: - print(self.indent + "End " + self.header) - - self.file.seek(self.end_pos) - - - - def read_bytes(self,num_bytes): - return self.file.read(num_bytes) - - - def read_string(self): - last_byte = self.read_bytes(1) - result = b'' - while last_byte[0] != 0x0: - result += last_byte - last_byte = self.read_bytes(1) - - return result.decode("utf-8") - - def read_i8(self, num=1): - buf = self.read_bytes(num) - result = struct.unpack(f"<{num}b", buf) - return result[0] if num == 1 else result - - def read_u8(self, num=1): - buf = self.read_bytes(num) - result = struct.unpack(f"<{num}B", buf) - return result[0] if num == 1 else result - - def read_i16(self, num=1): - buf = self.read_bytes(num * 2) - result = struct.unpack(f"<{num}h", buf) - return result[0] if num == 1 else result - - def read_u16(self, num=1): - buf = self.read_bytes(num * 2) - result = struct.unpack(f"<{num}H", buf) - return result[0] if num == 1 else result - - def read_i32(self, num=1): - buf = self.read_bytes(num * 4) - result = struct.unpack(f"<{num}i", buf) - return result[0] if num == 1 else result - - def read_u32(self, num=1): - buf = self.read_bytes(num * 4) - result = struct.unpack(f"<{num}I", buf) - return result[0] if num == 1 else result - - def read_f32(self, num=1): - buf = self.read_bytes(num * 4) - result = struct.unpack(f"<{num}f", buf) - return result[0] if num == 1 else result - - - def read_quat(self): - rot = self.read_f32(4) - return Quaternion((rot[3], rot[0], rot[1], rot[2])) - - def read_vec(self): - return Vector(self.read_f32(3)) - - - - def read_child(self): - child = Reader(self.file, parent=self, indent=int(len(self.indent) / 2) + 1, debug=self.debug) - return child - - - def skip_bytes(self,num): - self.file.seek(num,1) - - - def peak_next_header(self): - buf = self.read_bytes(4); - self.file.seek(-4,1) - try: - result = buf.decode("utf-8") - except: - result = "" - - return result - - - - def could_have_child(self): - return self.end_pos - self.file.tell() >= 8 - - - MAX_SIZE: int = 2147483647 - 8 diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index 622b050..64ae66d 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -5,17 +5,34 @@ from typing import Dict from .msh_scene import Scene from .msh_model import * from .msh_material import * -from .msh_reader import Reader from .msh_utilities import * from .crc import * +from .chunked_file_reader import Reader + + + +# Current model position model_counter = 0 -mndx_remap = {} +# Used to remap MNDX to the MODL's actual position +mndx_remap : Dict[int, int] = {} + +# How much to print +debug_level = 0 -def read_scene(input_file, anim_only=False) -> Scene: +''' +Debug levels just indicate how much info should be printed. +0 = nothing +1 = just blurbs about valuable info in the chunks +2 = #1 + full chunk structure +''' +def read_scene(input_file, anim_only=False, debug=0) -> Scene: + + global debug_level + debug_level = debug scene = Scene() scene.models = [] @@ -27,54 +44,58 @@ def read_scene(input_file, anim_only=False) -> Scene: global model_counter model_counter = 0 - with Reader(file=input_file, debug=True) as hedr: + with Reader(file=input_file, debug=debug_level>0) as head: - while hedr.could_have_child(): + head.skip_until("HEDR") - next_header = hedr.peak_next_header() + with head.read_child() as hedr: - if next_header == "MSH2": + while hedr.could_have_child(): - with hedr.read_child() as msh2: + next_header = hedr.peak_next_header() + + if next_header == "MSH2": + + with hedr.read_child() as msh2: + + if not anim_only: + materials_list = [] + + while (msh2.could_have_child()): + + next_header = msh2.peak_next_header() + + if next_header == "SINF": + with msh2.read_child() as sinf: + pass + + elif next_header == "MATL": + with msh2.read_child() as matl: + materials_list += _read_matl_and_get_materials_list(matl) + for i,mat in enumerate(materials_list): + scene.materials[mat.name] = mat + + elif next_header == "MODL": + with msh2.read_child() as modl: + scene.models.append(_read_modl(modl, materials_list)) + + else: + msh2.skip_bytes(1) + + elif next_header == "SKL2": + with hedr.read_child() as skl2: + num_bones = skl2.read_u32() + scene.skeleton = [skl2.read_u32(5)[0] for i in range(num_bones)] - if not anim_only: - materials_list = [] + elif next_header == "ANM2": + with hedr.read_child() as anm2: + scene.animation = _read_anm2(anm2) - while (msh2.could_have_child()): + else: + hedr.skip_bytes(1) - next_header = msh2.peak_next_header() - - if next_header == "SINF": - with msh2.read_child() as sinf: - pass - - elif next_header == "MATL": - with msh2.read_child() as matl: - materials_list += _read_matl_and_get_materials_list(matl) - for i,mat in enumerate(materials_list): - scene.materials[mat.name] = mat - - elif next_header == "MODL": - with msh2.read_child() as modl: - scene.models.append(_read_modl(modl, materials_list)) - - else: - msh2.skip_bytes(1) - - elif next_header == "SKL2": - with hedr.read_child() as skl2: - num_bones = skl2.read_u32() - scene.skeleton = [skl2.read_u32(5)[0] for i in range(num_bones)] - - elif next_header == "ANM2": - with hedr.read_child() as anm2: - scene.animation = _read_anm2(anm2) - - else: - hedr.skip_bytes(1) - - - if scene.skeleton: + # Print models in skeleton + if scene.skeleton and debug_level > 0: print("Skeleton models: ") for model in scene.models: for i in range(len(scene.skeleton)): @@ -84,7 +105,11 @@ def read_scene(input_file, anim_only=False) -> Scene: scene.skeleton.pop(i) break - + ''' + Iterate through every vertex weight in the scene and + change its index to directly reference its bone's index. + It will reference the MNDX of its bone's MODL by default. + ''' for model in scene.models: if model.geometry: for seg in model.geometry: @@ -173,7 +198,7 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: index = mndx.read_u32() global model_counter - print(mndx.indent + "MNDX doesn't match counter, expected: {} found: {}".format(model_counter, index)) + #print(mndx.indent + "MNDX doesn't match counter, expected: {} found: {}".format(model_counter, index)) global mndx_remap mndx_remap[index] = model_counter @@ -203,6 +228,7 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: with modl.read_child() as geom: while geom.could_have_child(): + #print("Searching for next seg or envl child..") next_header_geom = geom.peak_next_header() if next_header_geom == "SEGM": @@ -239,7 +265,9 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: else: modl.skip_bytes(1) - print(modl.indent + "Read model " + model.name + " of type: " + str(model.model_type)[10:]) + global debug_level + if debug_level > 0: + print(modl.indent + "Read model " + model.name + " of type: " + str(model.model_type)[10:]) return model @@ -253,7 +281,9 @@ def _read_tran(tran: Reader) -> ModelTransform: xform.rotation = tran.read_quat() xform.translation = tran.read_vec() - print(tran.indent + "Rot: {} Loc: {}".format(str(xform.rotation), str(xform.translation))) + global debug_level + if debug_level > 0: + print(tran.indent + "Rot: {} Loc: {}".format(str(xform.rotation), str(xform.translation))) return xform @@ -301,12 +331,16 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: geometry_seg.texcoords.append(Vector(uv0l.read_f32(2))) elif next_header == "NDXL": + with segm.read_child() as ndxl: + pass + ''' num_polygons = ndxl.read_u32() for _ in range(num_polygons): polygon = ndxl.read_u16(ndxl.read_u16()) geometry_seg.polygons.append(polygon) + ''' elif next_header == "NDXT": with segm.read_child() as ndxt: @@ -374,6 +408,7 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: geometry_seg.weights.append(weight_set) else: + #print("Skipping...") segm.skip_bytes(1) return geometry_seg @@ -390,7 +425,8 @@ def _read_anm2(anm2: Reader) -> Animation: if next_header == "CYCL": with anm2.read_child() as cycl: - pass + # Dont even know what CYCL's data does. Tried playing + # with the values but didn't change anything in zenasset or ingame... ''' num_anims = cycl.read_u32() @@ -399,15 +435,19 @@ def _read_anm2(anm2: Reader) -> Animation: cycl.skip_bytes(64) print("CYCL play style {}".format(cycl.read_u32(4)[1])) ''' + pass elif next_header == "KFR3": with anm2.read_child() as kfr3: num_bones = kfr3.read_u32() + bone_crcs = [] + for _ in range(num_bones): bone_crc = kfr3.read_u32() + bone_crcs.append(bone_crc) frames = ([],[]) @@ -423,6 +463,18 @@ def _read_anm2(anm2: Reader) -> Animation: frames[1].append(RotationFrame(kfr3.read_u32(), kfr3.read_quat())) anim.bone_frames[bone_crc] = frames + + + for bone_crc in sorted(bone_crcs): + + global debug_level + if debug_level > 0: + print("\t{}: ".format(hex(bone_crc))) + + bone_frames = anim.bone_frames[bone_crc] + + loc_frames = bone_frames[0] + rot_frames = bone_frames[1] else: anm2.skip_bytes(1) diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 7ca557c..ccc7587 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -27,9 +27,6 @@ def save_scene(output_file, scene: Scene): material_index = _write_matl_and_get_material_index(matl, scene) for index, model in enumerate(scene.models): - - #print("Name: {:.10}, Pos: {:15}, Rot: {:15}, Parent: {}".format(model.name, vec_to_str(model.transform.translation), quat_to_str(model.transform.rotation), model.parent)) - with msh2.create_child("MODL") as modl: _write_modl(modl, model, index, material_index, model_index) diff --git a/addons/io_scene_swbf_msh/msh_skeleton_properties.py b/addons/io_scene_swbf_msh/msh_skeleton_properties.py index 4835975..ce682ba 100644 --- a/addons/io_scene_swbf_msh/msh_skeleton_properties.py +++ b/addons/io_scene_swbf_msh/msh_skeleton_properties.py @@ -9,9 +9,9 @@ from .msh_model import * class SkeletonProperties(PropertyGroup): name: StringProperty(name="Name", default="Bone Name") - parent: StringProperty(name="Parent", default="Bone Parent") - loc: FloatVectorProperty(name="Local Position", default=(0.0, 0.0, 0.0), subtype="XYZ", size=3) - rot: FloatVectorProperty(name="Local Rotation", default=(0.0, 0.0, 0.0, 0.0), subtype="QUATERNION", size=4) + #parent: StringProperty(name="Parent", default="Bone Parent") + #loc: FloatVectorProperty(name="Local Position", default=(0.0, 0.0, 0.0), subtype="XYZ", size=3) + #rot: FloatVectorProperty(name="Local Rotation", default=(0.0, 0.0, 0.0, 0.0), subtype="QUATERNION", size=4) @@ -40,12 +40,10 @@ class SkeletonPropertiesPanel(bpy.types.Panel): skel_props = context.object.data.swbf_msh_skel + layout.label(text = "Bones In MSH Skeleton: ") + for prop in skel_props: layout.prop(prop, "name") - layout.prop(prop, "parent") - layout.prop(prop, "loc") - layout.prop(prop, "rot") - ''' layout.prop(skel_props, "name") diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index d5b6e50..7892f4d 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -17,9 +17,8 @@ from .crc import * import os - - -def extract_and_apply_anim(filename, scene): +# Extracts and applies anims in the scene to the currently selected armature +def extract_and_apply_anim(filename : str, scene : Scene): arma = bpy.context.view_layer.objects.active @@ -39,29 +38,26 @@ def extract_and_apply_anim(filename, scene): arma.animation_data_create() + # Record the starting transforms of each bone. Pose space is relative + # to bones starting transforms. Starting = in edit mode bone_bind_poses = {} - for bone in arma.data.bones: - bone_obj = bpy.data.objects[bone.name] - bone_obj_parent = bone_obj.parent + bpy.context.view_layer.objects.active = arma + bpy.ops.object.mode_set(mode='EDIT') - bind_mat = bone_obj.matrix_local - stack_mat = Matrix.Identity(4) + for edit_bone in arma.data.edit_bones: + if edit_bone.parent: + bone_local = edit_bone.parent.matrix.inverted() @ edit_bone.matrix + else: + bone_local = arma.matrix_local @ edit_bone.matrix + bone_bind_poses[edit_bone.name] = bone_local.inverted() - while(True): - if bone_obj_parent is None or bone_obj_parent.name in arma.data.bones: - break - bind_mat = bone_obj_parent.matrix_local @ bind_mat - stack_mat = bone_obj_parent.matrix_local @ stack_mat - bone_obj_parent = bone_obj_parent.parent - - bone_bind_poses[bone.name] = bind_mat.inverted() @ stack_mat + bpy.ops.object.mode_set(mode='OBJECT') for bone in arma.pose.bones: if to_crc(bone.name) in scene.animation.bone_frames: - #print("Inserting anim data for bone: {}".format(bone.name)) bind_mat = bone_bind_poses[bone.name] @@ -85,7 +81,6 @@ def extract_and_apply_anim(filename, scene): fcurve_rot_y.keyframe_points.insert(i,q.y) fcurve_rot_z.keyframe_points.insert(i,q.z) - fcurve_loc_x = action.fcurves.new(loc_data_path, index=0, action_group=bone.name) fcurve_loc_y = action.fcurves.new(loc_data_path, index=1, action_group=bone.name) fcurve_loc_z = action.fcurves.new(loc_data_path, index=2, action_group=bone.name) @@ -103,58 +98,62 @@ def extract_and_apply_anim(filename, scene): -def parent_object_to_bone(obj, armature, bone_name): - - worldmat = obj.matrix_world - - obj.parent = None - obj.parent = armature - obj.parent_type = 'BONE' - obj.parent_bone = bone_name - - obj.matrix_basis = Matrix() - obj.matrix_parent_inverse = Matrix() - - obj.matrix_world = worldmat +''' +Creates armature from the required nodes. +Assumes the required_skeleton is already sorted by parent. -def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): +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) - armature_obj.select_set(True) + preserved = armature_obj.data.swbf_msh_skel - for model in refined_skeleton: - loc,rot,_ = model_map[model.name].matrix_world.decompose() - print(str(loc)) - entry = preserved.add() - entry.name = model.name - entry.loc = loc - entry.rot = rot - entry.parent = model.parent + for model in required_skeleton: + if to_crc(model.name) in msh_scene.skeleton: + entry = preserved.add() + entry.name = model.name + #loc,rot,_ = model_map[model.name].matrix_world.decompose() + #entry.loc = loc + #entry.rot = rot + #entry.parent = model.parent + + 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 refined_skeleton: + for bone in required_skeleton: edit_bone = armature.edit_bones.new(bone.name) - if bone.parent: + 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, refined_skeleton)] + 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: @@ -164,8 +163,6 @@ def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): 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) @@ -176,14 +173,26 @@ def refined_skeleton_to_armature(refined_skeleton : List[Model], model_map): +''' +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] = {} -def extract_refined_skeleton(scene: Scene): - - model_dict = {} - skeleton_models = [] - + # 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. for model in scene.models: model_dict[model.name] = model @@ -194,57 +203,73 @@ def extract_refined_skeleton(scene: Scene): for weight in weight_set: model_weighted_to = scene.models[weight.bone] - if to_crc(model_weighted_to.name) not in scene.skeleton: - scene.skeleton.append(to_crc(model_weighted_to.name)) - - for model in scene.models: - if to_crc(model.name) in scene.skeleton: - skeleton_models.append(model) + 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 = [] - refined_skeleton_models = [] - - for bone in skeleton_models: + # Set of nodes to be included in required skeleton/were visited + visited_nodes = set() - if bone.parent: + ''' + 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): - curr_ancestor = model_dict[bone.parent] - stacked_transform = model_transform_to_matrix(bone.transform) + # 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 - while True: + potential_bones = [bone] + visited_nodes.add(bone.name) - if to_crc(curr_ancestor.name) in scene.skeleton or curr_ancestor.name == scene.models[0].name: - new_model = Model() - new_model.name = bone.name - new_model.parent = curr_ancestor.name if curr_ancestor.name != scene.models[0].name else "" + # 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] - loc, rot, _ = stacked_transform.decompose() + while True: - new_model.transform.rotation = rot - new_model.transform.translation = loc - - refined_skeleton_models.append(new_model) - break + # 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 - else: - curr_ancestor = model_dict[curr_ancestor.parent] - stacked_transform = model_transform_to_matrix(curr_ancestor.transform) @ stacked_transform - - return sort_by_parent(refined_skeleton_models) + # 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]: + # This will be filled with model names -> Blender objects and returned + model_map : Dict[str, bpy.types.Object] = {} + sorted_models : List[Model] = sort_by_parent(scene.models) - -def extract_models(scene: Scene, materials_map): - - model_map = {} - - for model in sort_by_parent(scene.models): + for model in sorted_models: new_obj = None if model.model_type == ModelType.STATIC or model.model_type == ModelType.SKIN: @@ -358,19 +383,19 @@ def extract_models(scene: Scene, materials_map): return model_map +# TODO: Add to custom material info struct, maybe some material conversion/import? +def extract_materials(folder_path: str, scene: Scene) -> Dict[str, bpy.types.Material]: -def extract_materials(folder_path: str, scene: Scene) -> Dict[str,bpy.types.Material]: + extracted_materials : Dict[str, bpy.types.Material] = {} - extracted_materials = {} - - for material_name in scene.materials.keys(): + for material_name, material in scene.materials.items(): new_mat = bpy.data.materials.new(name=material_name) new_mat.use_nodes = True bsdf = new_mat.node_tree.nodes["Principled BSDF"] - tex_path_def = os.path.join(folder_path, scene.materials[material_name].texture0) - tex_path_alt = os.path.join(folder_path, "PC", scene.materials[material_name].texture0) + tex_path_def = os.path.join(folder_path, material.texture0) + tex_path_alt = os.path.join(folder_path, "PC", material.texture0) tex_path = tex_path_def if os.path.exists(tex_path_def) else tex_path_alt @@ -379,6 +404,20 @@ def extract_materials(folder_path: str, scene: Scene) -> Dict[str,bpy.types.Mate texImage.image = bpy.data.images.load(tex_path) new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) + # Fill MaterialProperties datablock + ''' + material_properties = new_mat.swbf_msh + material_properties.specular_color = material.specular_color.copy() + material_properties.diffuse_map = material.texture0 + + result.rendertype = _read_material_props_rendertype(props) + result.flags = _read_material_props_flags(props) + result.data = _read_material_props_data(props) + result.texture1 = _read_normal_map_or_distortion_map_texture(props) + result.texture2 = _read_detail_texture(props) + result.texture3 = _read_envmap_texture(props) + ''' + extracted_materials[material_name] = new_mat return extracted_materials @@ -388,82 +427,112 @@ def extract_materials(folder_path: str, scene: Scene) -> Dict[str,bpy.types.Mate def extract_scene(filepath: str, scene: Scene): folder = os.path.join(os.path.dirname(filepath),"") - matmap = extract_materials(folder, scene) - model_map = extract_models(scene, matmap) + # material_map mapes Material names to Blender materials + material_map = extract_materials(folder, scene) - skel = extract_refined_skeleton(scene) - armature = refined_skeleton_to_armature(skel, model_map) + # model_map maps Model names to Blender objects. + model_map = extract_models(scene, material_map) + + # skel contains all models needed in an armature + skel = extract_required_skeleton(scene) + + # Create the armature if skel is non-empty + armature = None if not skel else required_skeleton_to_armature(skel, model_map, scene) - for bone in armature.data.bones: - bone_local = bone.matrix_local - if bone.parent: - bone_local = bone.parent.matrix_local.inverted() @ bone_local + ''' + If an armature was created, we need to do a few extra + things to ensure the import makes sense in Blender. It can + get a bit messy, as XSI + SWBF have very loose requirements + when it comes to skin-skeleton parentage. - bone_obj_local = bpy.data.objects[bone.name].matrix_local - obj_loc, obj_rot, _ = bone_obj_local.decompose() + If not, we're good. + ''' + if armature is not None: - loc, rot, _ = bone_local.decompose() + has_skin = False + + # Handle armature related parenting + for curr_model in scene.models: + + curr_obj = model_map[curr_model.name] + + # Parent all skins to armature + if curr_model.model_type == ModelType.SKIN: + + has_skin = True + + curr_obj.select_set(True) + armature.select_set(True) + bpy.context.view_layer.objects.active = armature + + bpy.ops.object.parent_clear(type='CLEAR') + bpy.ops.object.parent_set(type='ARMATURE') + + curr_obj.select_set(False) + armature.select_set(False) + bpy.context.view_layer.objects.active = None + + # Parent the object to a bone if necessary + else: + if curr_model.parent in armature.data.bones and curr_model.name not in armature.data.bones: + # Some of this is redundant, but necessary... + worldmat = curr_obj.matrix_world + # '' + curr_obj.parent = None + curr_obj.parent = armature + curr_obj.parent_type = 'BONE' + curr_obj.parent_bone = curr_model.parent + # '' + curr_obj.matrix_basis = Matrix() + curr_obj.matrix_parent_inverse = Matrix() + curr_obj.matrix_world = worldmat + + ''' + Sometimes skins are parented to other skins. We need to find the skin highest in the hierarchy and + parent all skins to its parent (armature_reparent_obj). + + If not skin exists, we just reparent the armature to the parent of the highest node in the skeleton + ''' + armature_reparent_obj = None + if has_skin: + for model in sort_by_parent(scene.models): + if model.model_type == ModelType.SKIN: + armature_reparent_obj = None if not model.parent else model_map[model.parent] + else: + skeleton_parent_name = skel[0].parent + for model in scene.models: + if model.name == skeleton_parent_name: + armature_reparent_obj = None if not skeleton_parent_name else model_map[skeleton_parent_name] + # Now we reparent the armature to the node (armature_reparent_obj) we just found + if armature_reparent_obj is not None and armature.name != armature_reparent_obj.name: - reparent_obj = None - for model in scene.models: - if model.model_type == ModelType.SKIN: - - if model.parent: - reparent_obj = model_map[model.parent] - - skin_obj = model_map[model.name] - skin_obj.select_set(True) armature.select_set(True) - bpy.context.view_layer.objects.active = armature + armature_reparent_obj.select_set(True) - bpy.ops.object.parent_clear(type='CLEAR') - bpy.ops.object.parent_set(type='ARMATURE') + bpy.context.view_layer.objects.active = armature_reparent_obj + bpy.ops.object.parent_set(type='OBJECT') - skin_obj.select_set(False) armature.select_set(False) + armature_reparent_obj.select_set(False) bpy.context.view_layer.objects.active = None - if armature is not None: - for bone in armature.data.bones: - for model in scene.models: - if model.parent in armature.data.bones and model.model_type != ModelType.NULL: - pass#parent_object_to_bone(model_map[model.name], armature, model.parent) - ''' - if reparent_obj is not None and armature.name != reparent_obj.name: - - armature.select_set(True) - reparent_obj.select_set(True) - bpy.context.view_layer.objects.active = reparent_obj - bpy.ops.object.parent_set(type='OBJECT') - - armature.select_set(False) - reparent_obj.select_set(False) - bpy.context.view_layer.objects.active = None - ''' - - for model in scene.models: - if model.name in bpy.data.objects: - obj = bpy.data.objects[model.name] - if get_is_model_hidden(obj) and len(obj.children) == 0 and model.model_type != ModelType.NULL: - obj.hide_set(True) + # If an bone exists in the armature, delete its + # object counterpart (as created in extract_models) + for bone in skel: + model_to_remove = model_map[bone.name] + if model_to_remove: + bpy.data.objects.remove(model_to_remove, do_unlink=True) + model_map.pop(bone.name) - - - - - - - - - - - - - - + # Lastly, hide all that is hidden in the msh scene + for model in scene.models: + if model.name in model_map: + obj = model_map[model.name] + if get_is_model_hidden(obj) and len(obj.children) == 0: + obj.hide_set(True) diff --git a/addons/io_scene_swbf_msh/zaa_to_blend.py b/addons/io_scene_swbf_msh/zaa_to_blend.py index 4ae2e52..93b8466 100644 --- a/addons/io_scene_swbf_msh/zaa_to_blend.py +++ b/addons/io_scene_swbf_msh/zaa_to_blend.py @@ -10,7 +10,7 @@ import os import bpy import re -from .zaa_reader import * +from .chunked_file_reader import Reader from .crc import * from .msh_model import * @@ -20,20 +20,25 @@ from .msh_utilities import * from typing import List, Set, Dict, Tuple +debug = False - #anims #bones #comps #keyframes: index,value + + #anims #bones #components #keyframes: index,value def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]]]: + global debug + decompressed_anims: Dict[int, Dict[int, List[ Dict[int,float]]]] = {} - with ZAAReader(input_file) as head: + with Reader(input_file, debug=debug) as head: # Dont read SMNA as child, since it has a length field always set to 0... head.skip_until("SMNA") head.skip_bytes(20) num_anims = head.read_u16() - #print("\nFile contains {} animations\n".format(num_anims)) + if debug: + print("\nFile contains {} animations\n".format(num_anims)) head.skip_bytes(2) @@ -92,7 +97,8 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] transBitFlags = anim_metadata[anim_crc]["transBitFlags"] - #print("\n\tAnim hash: {} Num frames: {} Num joints: {}".format(hex(anim_crc), num_frames, num_bones)) + if debug: + print("\n\tAnim hash: {} Num frames: {} Num joints: {}".format(hex(anim_crc), num_frames, num_bones)) for bone_num, bone_crc in enumerate(anim_metadata[anim_crc]["bone_list"]): @@ -103,8 +109,9 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] offsets_list = params_bone["rot_offsets"] + params_bone["loc_offsets"] qparams = params_bone["qparams"] - #print("\n\t\tBone #{} hash: {}".format(bone_num,hex(bone_crc))) - #print("\n\t\tQParams: {}, {}, {}, {}".format(*qparams)) + if debug: + print("\n\t\tBone #{} hash: {}".format(bone_num,hex(bone_crc))) + print("\n\t\tQParams: {}, {}, {}, {}".format(*qparams)) for o, start_offset in enumerate(offsets_list): @@ -125,17 +132,14 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] # a single multiplier for all three else: - if (0x00000001 << bone_num) & transBitFlags == 0: - bone_curves.append(None) - continue - - mult = qparams[-1] bias = qparams[o - 4] - #print("\n\t\t\tBias = {}, multiplier = {}".format(bias, mult)) + if debug: + print("\n\t\t\tBias = {}, multiplier = {}".format(bias, mult)) - #print("\n\t\t\tOffset {}: {} ({}, {} remaining)".format(o,start_offset, tada.get_current_pos(), tada.how_much_left(tada.get_current_pos()))) + if debug: + print("\n\t\t\tOffset {}: {} ({}, {} remaining)".format(o,start_offset, tada.get_current_pos(), tada.how_much_left(tada.get_current_pos()))) # Skip to start of compressed data for component, as specified in TNJA tada.skip_bytes(start_offset) @@ -146,7 +150,9 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] accumulator = bias + mult * tada.read_i16() curve[j if j < num_frames else num_frames] = accumulator - #print("\t\t\t\t{}: {}".format(j, accumulator)) + if debug: + print("\t\t\t\t{}: {}".format(j, accumulator)) + j+=1 while (j < num_frames): @@ -155,13 +161,15 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] # Reset the accumulator to next dequantized i16 if control == -0x7f: - #print("\t\t\t\tControl: READING NEXT FRAME") + if debug: + print("\t\t\t\tControl: READING NEXT FRAME") break # RLE: hold current accumulator for the next u8 frames elif control == -0x80: num_skips = tada.read_u8() - #print("\t\t\t\tControl: HOLDING FOR {} FRAMES".format(num_skips)) + if debug: + print("\t\t\t\tControl: HOLDING FOR {} FRAMES".format(num_skips)) j += num_skips # If not a special value, increment accumulator by the dequantized i8 @@ -169,8 +177,8 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] else: accumulator += mult * float(control) curve[j if j < num_frames else num_frames] = accumulator - - #print("\t\t\t\t{}: {}".format(j, accumulator)) + if debug: + print("\t\t\t\t{}: {}".format(j, accumulator)) j+=1 curve[num_frames - 1] = accumulator @@ -217,6 +225,8 @@ for now this will work ONLY if the model was directly imported from a .msh file. def extract_and_apply_munged_anim(input_file_path): + global debug + with open(input_file_path,"rb") as input_file: animation_set = decompress_curves(input_file) @@ -244,23 +254,37 @@ def extract_and_apply_munged_anim(input_file_path): This will be replaced with the eventual importer release. """ + animated_bones = set() + for anim_crc in animation_set: + for bone_crc in animation_set[anim_crc]: + animated_bones.add(bone_crc) + + + bpy.context.view_layer.objects.active = arma + bpy.ops.object.mode_set(mode='EDIT') + bone_bind_poses = {} - for bone in arma.data.bones: - bone_obj = bpy.data.objects[bone.name] - bone_obj_parent = bone_obj.parent + for edit_bone in arma.data.edit_bones: + if to_crc(edit_bone.name) not in animated_bones: + continue - bind_mat = bone_obj.matrix_local + curr_ancestor = edit_bone.parent + while curr_ancestor is not None and to_crc(curr_ancestor.name) not in animated_bones: + curr_ancestor = curr_ancestor.parent - while(True): - if bone_obj_parent is None or bone_obj_parent.name in arma.data.bones: - break - bind_mat = bone_obj_parent.matrix_local @ bind_mat - bone_obj_parent = bone_obj_parent.parent + if curr_ancestor: + bind_mat = curr_ancestor.matrix.inverted() @ edit_bone.matrix + else: + bind_mat = arma.matrix_local @ edit_bone.matrix - bone_bind_poses[bone.name] = bind_mat.inverted() + bone_bind_poses[edit_bone.name] = bind_mat.inverted() + + bpy.ops.object.mode_set(mode='OBJECT') + if debug: + print("Extracting {} animations from {}:".format(len(animation_set), input_file_path)) for anim_crc in animation_set: @@ -270,16 +294,30 @@ def extract_and_apply_munged_anim(input_file_path): else: anim_str = str(hex(anim_crc)) - if anim_str in bpy.data.actions: - bpy.data.actions[anim_str].use_fake_user = False - bpy.data.actions.remove(bpy.data.actions[anim_str]) + if debug: + print("\tExtracting anim {}:".format(anim_str)) + + + #if anim_str in bpy.data.actions: + # bpy.data.actions[anim_str].use_fake_user = False + # bpy.data.actions.remove(bpy.data.actions[anim_str]) action = bpy.data.actions.new(anim_str) action.use_fake_user = True animation = animation_set[anim_crc] - for bone in arma.pose.bones: + bone_crcs_list = [bone_crc_ for bone_crc_ in animation] + + for bone_crc in sorted(bone_crcs_list): + + bone_name = next((bone.name for bone in arma.pose.bones if to_crc(bone.name) == bone_crc), None) + + if bone_name is None: + continue + + bone = arma.pose.bones[bone_name] + bone_crc = to_crc(bone.name) if bone_crc not in animation: @@ -294,7 +332,8 @@ def extract_and_apply_munged_anim(input_file_path): has_translation = bone_curves[4] is not None - #print("\t\tNum frames: " + str(num_frames)) + if debug: + print("\t\tBone {} has {} frames: ".format(bone_name, num_frames)) last_values = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] @@ -330,7 +369,6 @@ def extract_and_apply_munged_anim(input_file_path): return v if has_key else None - fcurve_rot_w = action.fcurves.new(rot_data_path, index=0, action_group=bone.name) fcurve_rot_x = action.fcurves.new(rot_data_path, index=1, action_group=bone.name) fcurve_rot_y = action.fcurves.new(rot_data_path, index=2, action_group=bone.name) @@ -346,6 +384,9 @@ def extract_and_apply_munged_anim(input_file_path): q = get_quat(frame) if q is not None: + if debug: + print("\t\t\tRot key: ({}, {})".format(frame, quat_to_str(q))) + # Very bloated, but works for now q = (bind_mat @ convert_rotation_space(q).to_matrix().to_4x4()).to_quaternion() fcurve_rot_w.keyframe_points.insert(frame,q.w) @@ -358,6 +399,9 @@ def extract_and_apply_munged_anim(input_file_path): t = get_vec(frame) if t is not None: + if debug: + print("\t\t\tPos key: ({}, {})".format(frame, vec_to_str(t))) + t = (bind_mat @ Matrix.Translation(convert_vector_space(t))).translation fcurve_loc_x.keyframe_points.insert(frame,t.x) From 7244446dd9209d6eabc9e986d846b2710879c56e Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Tue, 11 Jan 2022 14:02:17 -0500 Subject: [PATCH 35/39] Fill material properties upon import + if SKL2 missing, assume all BONE models are in it + SkeletonProperties only relevant for imported meshes + chunked_file_reader no longer assumes 4-byte alignment --- .../io_scene_swbf_msh/chunked_file_reader.py | 15 +- .../msh_material_to_blend.py | 133 ++++++++++++++++++ addons/io_scene_swbf_msh/msh_scene_read.py | 38 ++--- .../msh_skeleton_properties.py | 16 +-- addons/io_scene_swbf_msh/msh_to_blend.py | 86 +++++------ addons/io_scene_swbf_msh/zaa_to_blend.py | 7 + 6 files changed, 201 insertions(+), 94 deletions(-) create mode 100644 addons/io_scene_swbf_msh/msh_material_to_blend.py diff --git a/addons/io_scene_swbf_msh/chunked_file_reader.py b/addons/io_scene_swbf_msh/chunked_file_reader.py index 7094613..b83ef9b 100644 --- a/addons/io_scene_swbf_msh/chunked_file_reader.py +++ b/addons/io_scene_swbf_msh/chunked_file_reader.py @@ -25,21 +25,18 @@ class Reader: if self.parent is not None: self.header = self.read_bytes(4).decode("utf-8") else: - self.header = "FILE" + self.header = "File" if self.parent is not None: self.size = self.read_u32() else: self.size = os.path.getsize(self.file.name) - 8 - - padding_length = 4 - (self.size % 4) if self.size % 4 > 0 else 0 - self.end_pos = self.size_pos + padding_length + self.size + 8 + + # No padding to multiples of 4. Files exported from XSI via zetools do not align by 4! + self.end_pos = self.size_pos + self.size + 8 if self.debug: - if self.parent is not None: - print(self.indent + "Begin " + self.header + ", Size: " + str(self.size) + ", At pos: " + str(self.size_pos)) - else: - print(self.indent + "Begin file, Size: " + str(self.size) + ", At pos: " + str(self.size_pos)) + print("{}Begin {} of Size {} at pos {}:".format(self.indent, self.header, self.size, self.size_pos)) return self @@ -49,7 +46,7 @@ class Reader: raise OverflowError(f"File overflowed max size. size = {self.size} MAX_SIZE = {self.MAX_SIZE}") if self.debug: - print(self.indent + "End " + self.header) + print("{}End {} at pos: {}".format(self.indent, self.header, self.end_pos)) self.file.seek(self.end_pos) diff --git a/addons/io_scene_swbf_msh/msh_material_to_blend.py b/addons/io_scene_swbf_msh/msh_material_to_blend.py new file mode 100644 index 0000000..a0ae612 --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_material_to_blend.py @@ -0,0 +1,133 @@ +""" For finding textures and assigning material properties from entries in a Material """ + +import bpy +from typing import Dict +from .msh_material import * +from .msh_material_gather import * +from .msh_material_properties import * + +import os + + + +def find_texture_path(folder_path : str, name : str) -> str: + + if not folder_path or not name: + return "" + + possible_paths = [ + os.path.join(folder_path, name), + os.path.join(folder_path, "PC", name), + os.path.join(folder_path, "pc", name), + os.path.join(folder_path, ".." , name), + ] + + for possible_path in possible_paths: + if os.path.exists(possible_path): + return possible_path + + return "" + + + +def fill_material_props(material : Material, material_properties): + """ Fills MaterialProperties from Material instance """ + + if material_properties is None or material is None: + return + + material_properties.specular_color = (material.specular_color[0], material.specular_color[1], material.specular_color[2]) + material_properties.diffuse_map = material.texture0 + + _fill_material_props_rendertype(material, material_properties) + _fill_material_props_flags(material, material_properties) + _fill_material_props_data(material, material_properties) + + _fill_normal_map_or_distortion_map_texture(material, material_properties) + _fill_detail_texture(material, material_properties) + _fill_envmap_texture(material, material_properties) + + + +def _fill_material_props_rendertype(material, material_properties): + + _REVERSE_RENDERTYPES_MAPPING = { + Rendertype.NORMAL : "NORMAL_BF2", + Rendertype.SCROLLING : "SCROLLING_BF2", + Rendertype.ENVMAPPED : "ENVMAPPED_BF2", + Rendertype.ANIMATED : "ANIMATED_BF2", + Rendertype.REFRACTION : "REFRACTION_BF2", + Rendertype.BLINK : "BLINK_BF2", + Rendertype.NORMALMAPPED_TILED : "NORMALMAPPED_TILED_BF2", + Rendertype.NORMALMAPPED_ENVMAPPED : "NORMALMAPPED_ENVMAPPED_BF2", + Rendertype.NORMALMAPPED : "NORMALMAPPED_BF2", + Rendertype.NORMALMAPPED_TILED_ENVMAP : "NORMALMAPPED_TILED_ENVMAPPED_BF2"} + + material_properties.rendertype = _REVERSE_RENDERTYPES_MAPPING[material.rendertype] + + + +def _fill_material_props_flags(material, material_properties): + if material.rendertype == Rendertype.REFRACTION: + material_properties.blended_transparency = True + return + + flags = material.flags + + material_properties.blended_transparency = bool(flags & MaterialFlags.BLENDED_TRANSPARENCY) + material_properties.additive_transparency = bool(flags & MaterialFlags.ADDITIVE_TRANSPARENCY) + material_properties.hardedged_transparency = bool(flags & MaterialFlags.HARDEDGED_TRANSPARENCY) + material_properties.unlit = bool(flags & MaterialFlags.UNLIT) + material_properties.glow = bool(flags & MaterialFlags.GLOW) + material_properties.perpixel = bool(flags & MaterialFlags.PERPIXEL) + material_properties.specular = bool(flags & MaterialFlags.SPECULAR) + material_properties.doublesided = bool(flags & MaterialFlags.DOUBLESIDED) + + +def _fill_material_props_data(material, material_properties): + if material.rendertype == Rendertype.SCROLLING: + material_properties.scroll_speed_u = material.data[0] + material_properties.scroll_speed_v = material.data[1] + + elif material.rendertype == Rendertype.BLINK: + material_properties.blink_min_brightness = material.data[0] + material_properties.blink_speed = material.data[1] + + elif material.rendertype == Rendertype.NORMALMAPPED_TILED_ENVMAP or material.rendertype == Rendertype.NORMALMAPPED_TILED: + material_properties.normal_map_tiling_u = material.data[0] + material_properties.normal_map_tiling_v = material.data[1] + + elif material.rendertype == Rendertype.REFRACTION: + pass + + elif material.rendertype == Rendertype.ANIMATED: + + anim_length_index = int(sqrt(length)) - 1 + if animation_length_index < 0: + animation_length_index = 0 + elif animation_length_index > len(UI_MATERIAL_ANIMATION_LENGTHS): + animation_length_index = len(UI_MATERIAL_ANIMATION_LENGTHS) - 1 + + material_properties.animation_length = UI_MATERIAL_ANIMATION_LENGTHS[animation_length_index] + material_properties.animation_speed = material.data[1] + + else: + material_properties.detail_map_tiling_u = material.data[0] + material_properties.detail_map_tiling_v = material.data[1] + + +def _fill_normal_map_or_distortion_map_texture(material, material_properties): + if material.rendertype == Rendertype.REFRACTION: + material_properties.distortion_map = material.texture1 + elif material.rendertype.value > 24: + material_properties.normal_map = material.texture1 + + +def _fill_detail_texture(material, material_properties): + if material.rendertype != Rendertype.REFRACTION: + material_properties.detail_map = material.texture2 + + +def _fill_envmap_texture(material, material_properties): + if material.rendertype != Rendertype.ENVMAPPED: + material_properties.environment_map = material.texture3 diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index 64ae66d..e537dcc 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -110,13 +110,27 @@ def read_scene(input_file, anim_only=False, debug=0) -> Scene: change its index to directly reference its bone's index. It will reference the MNDX of its bone's MODL by default. ''' + for model in scene.models: if model.geometry: for seg in model.geometry: if seg.weights: for weight_set in seg.weights: for vweight in weight_set: - vweight.bone = mndx_remap[vweight.bone] + + if vweight.bone in mndx_remap: + vweight.bone = mndx_remap[vweight.bone] + else: + 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 @@ -155,8 +169,8 @@ def _read_matd(matd: Reader) -> Material: elif next_header == "ATRB": with matd.read_child() as atrb: - mat.flags = atrb.read_u8() - mat.rendertype = atrb.read_u8() + mat.flags = MaterialFlags(atrb.read_u8()) + mat.rendertype = Rendertype(atrb.read_u8()) mat.data = atrb.read_u8(2) elif next_header == "TX0D": @@ -197,8 +211,10 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: with modl.read_child() as mndx: index = mndx.read_u32() + global model_counter #print(mndx.indent + "MNDX doesn't match counter, expected: {} found: {}".format(model_counter, index)) + #print("Model counter: {} MNDX: {}".format(model_counter, index)) global mndx_remap mndx_remap[index] = model_counter @@ -228,7 +244,6 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: with modl.read_child() as geom: while geom.could_have_child(): - #print("Searching for next seg or envl child..") next_header_geom = geom.peak_next_header() if next_header_geom == "SEGM": @@ -242,16 +257,12 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: else: geom.skip_bytes(1) - #with geom.read_child() as null: - #pass for seg in model.geometry: if seg.weights and envelope: for weight_set in seg.weights: - for i in range(len(weight_set)): - vertex_weight = weight_set[i] - index = vertex_weight.bone - weight_set[i] = VertexWeight(vertex_weight.weight, envelope[vertex_weight.bone]) + for vertex_weight in weight_set: + vertex_weight.bone = envelope[vertex_weight.bone] elif next_header == "SWCI": prim = CollisionPrimitive() @@ -348,7 +359,7 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: for _ in range(num_tris): geometry_seg.triangles.append(ndxt.read_u16(3)) - + # elif next_header == "STRP": strips : List[List[int]] = [] @@ -408,7 +419,6 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: geometry_seg.weights.append(weight_set) else: - #print("Skipping...") segm.skip_bytes(1) return geometry_seg @@ -467,10 +477,6 @@ def _read_anm2(anm2: Reader) -> Animation: for bone_crc in sorted(bone_crcs): - global debug_level - if debug_level > 0: - print("\t{}: ".format(hex(bone_crc))) - bone_frames = anim.bone_frames[bone_crc] loc_frames = bone_frames[0] diff --git a/addons/io_scene_swbf_msh/msh_skeleton_properties.py b/addons/io_scene_swbf_msh/msh_skeleton_properties.py index ce682ba..139e5b0 100644 --- a/addons/io_scene_swbf_msh/msh_skeleton_properties.py +++ b/addons/io_scene_swbf_msh/msh_skeleton_properties.py @@ -9,11 +9,6 @@ from .msh_model import * class SkeletonProperties(PropertyGroup): name: StringProperty(name="Name", default="Bone Name") - #parent: StringProperty(name="Parent", default="Bone Parent") - #loc: FloatVectorProperty(name="Local Position", default=(0.0, 0.0, 0.0), subtype="XYZ", size=3) - #rot: FloatVectorProperty(name="Local Rotation", default=(0.0, 0.0, 0.0, 0.0), subtype="QUATERNION", size=4) - - @@ -29,7 +24,7 @@ class SkeletonPropertiesPanel(bpy.types.Panel): @classmethod def poll(cls, context): - return context.object.type == 'ARMATURE' + return context.object.type == 'ARMATURE' and context.object.data.swbf_msh_skel and len(context.object.data.swbf_msh_skel) > 0 def draw(self, context): @@ -45,13 +40,4 @@ class SkeletonPropertiesPanel(bpy.types.Panel): for prop in skel_props: layout.prop(prop, "name") - ''' - layout.prop(skel_props, "name") - layout.prop(skel_props, "parent") - layout.prop(skel_props, "loc") - layout.prop(skel_props, "rot") - ''' - - - #self.layout.label(text=context.object.swbf_msh_skel.yolo[1]) \ No newline at end of file diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index 7892f4d..7701138 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -8,6 +8,7 @@ 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 * @@ -118,10 +119,6 @@ def required_skeleton_to_armature(required_skeleton : List[Model], model_map : D if to_crc(model.name) in msh_scene.skeleton: entry = preserved.add() entry.name = model.name - #loc,rot,_ = model_map[model.name].matrix_world.decompose() - #entry.loc = loc - #entry.rot = rot - #entry.parent = model.parent bones_set = set([model.name for model in required_skeleton]) @@ -186,17 +183,29 @@ 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. + ''' + 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. + ''' + 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.geometry: + #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: @@ -346,6 +355,7 @@ 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') @@ -394,29 +404,14 @@ def extract_materials(folder_path: str, scene: Scene) -> Dict[str, bpy.types.Mat new_mat.use_nodes = True bsdf = new_mat.node_tree.nodes["Principled BSDF"] - tex_path_def = os.path.join(folder_path, material.texture0) - tex_path_alt = os.path.join(folder_path, "PC", material.texture0) + diffuse_texture_path = find_texture_path(folder_path, material.texture0) - tex_path = tex_path_def if os.path.exists(tex_path_def) else tex_path_alt - - if os.path.exists(tex_path): + if diffuse_texture_path: texImage = new_mat.node_tree.nodes.new('ShaderNodeTexImage') - texImage.image = bpy.data.images.load(tex_path) - new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) + texImage.image = bpy.data.images.load(diffuse_texture_path) + new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) - # Fill MaterialProperties datablock - ''' - material_properties = new_mat.swbf_msh - material_properties.specular_color = material.specular_color.copy() - material_properties.diffuse_map = material.texture0 - - result.rendertype = _read_material_props_rendertype(props) - result.flags = _read_material_props_flags(props) - result.data = _read_material_props_data(props) - result.texture1 = _read_normal_map_or_distortion_map_texture(props) - result.texture2 = _read_detail_texture(props) - result.texture3 = _read_envmap_texture(props) - ''' + fill_material_props(material, new_mat.swbf_msh) extracted_materials[material_name] = new_mat @@ -434,6 +429,7 @@ def extract_scene(filepath: str, scene: Scene): # model_map maps Model names to Blender objects. model_map = extract_models(scene, material_map) + # skel contains all models needed in an armature skel = extract_required_skeleton(scene) @@ -463,24 +459,16 @@ def extract_scene(filepath: str, scene: Scene): has_skin = True - curr_obj.select_set(True) - armature.select_set(True) - bpy.context.view_layer.objects.active = armature - - bpy.ops.object.parent_clear(type='CLEAR') - bpy.ops.object.parent_set(type='ARMATURE') - - curr_obj.select_set(False) - armature.select_set(False) - bpy.context.view_layer.objects.active = None + curr_obj.parent = armature + curr_obj.parent_type = 'ARMATURE' # Parent the object to a bone if necessary else: if curr_model.parent in armature.data.bones and curr_model.name not in armature.data.bones: - # Some of this is redundant, but necessary... + # Not sure what the different mats do, but saving the worldmat and + # applying it after clearing the other mats yields correct results... worldmat = curr_obj.matrix_world - # '' - curr_obj.parent = None + curr_obj.parent = armature curr_obj.parent_type = 'BONE' curr_obj.parent_bone = curr_model.parent @@ -506,19 +494,9 @@ def extract_scene(filepath: str, scene: Scene): if model.name == skeleton_parent_name: armature_reparent_obj = None if not skeleton_parent_name else model_map[skeleton_parent_name] - # Now we reparent the armature to the node (armature_reparent_obj) we just found if armature_reparent_obj is not None and armature.name != armature_reparent_obj.name: - - armature.select_set(True) - armature_reparent_obj.select_set(True) - - bpy.context.view_layer.objects.active = armature_reparent_obj - bpy.ops.object.parent_set(type='OBJECT') - - armature.select_set(False) - armature_reparent_obj.select_set(False) - bpy.context.view_layer.objects.active = None + armature.parent = armature_reparent_obj # If an bone exists in the armature, delete its diff --git a/addons/io_scene_swbf_msh/zaa_to_blend.py b/addons/io_scene_swbf_msh/zaa_to_blend.py index 93b8466..eeaad2e 100644 --- a/addons/io_scene_swbf_msh/zaa_to_blend.py +++ b/addons/io_scene_swbf_msh/zaa_to_blend.py @@ -45,6 +45,8 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] anim_crcs = [] anim_metadata = {} + head.skip_until("MINA") + # Read metadata (crc, num frames, num bones) for each anim with head.read_child() as mina: @@ -62,6 +64,9 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] "transBitFlags" : transBitFlags, } + + head.skip_until("TNJA") + # Read TADA offsets and quantization parameters for each rot + loc component, for each bone, for each anim with head.read_child() as tnja: @@ -85,6 +90,8 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]] anim_metadata[anim_crc]["bone_params"] = bone_params anim_metadata[anim_crc]["bone_list"] = bone_list + head.skip_until("TADA") + # Decompress/dequantize frame data into discrete per-component curves with head.read_child() as tada: From c314592d489dd8ba1c512d25cb3f4f9eb3ccbe65 Mon Sep 17 00:00:00 2001 From: Will Snyder Date: Thu, 13 Jan 2022 12:52:30 -0800 Subject: [PATCH 36/39] Dont include bone in exported anim if its not keyed or in preserved skel... --- addons/io_scene_swbf_msh/msh_anim_gather.py | 26 ++++++++++++++++++- addons/io_scene_swbf_msh/msh_model_gather.py | 19 +++++++++----- .../io_scene_swbf_msh/msh_model_utilities.py | 2 +- addons/io_scene_swbf_msh/msh_scene_save.py | 8 +++++- addons/io_scene_swbf_msh/msh_to_blend.py | 18 ++++++------- 5 files changed, 55 insertions(+), 18 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 52fc6c4..8a4c613 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -14,9 +14,29 @@ from .msh_model_gather import * from .crc import to_crc + + def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: action = armature.animation_data.action + + # Set of bones to include in SKL2/animation stuff + keyable_bones : Set[str] = set() + + msh_skel = armature.data.swbf_msh_skel + + has_preserved_skel = len(msh_skel) > 0 + + if has_preserved_skel: + for bone in msh_skel: + #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(); root_crc = to_crc(root_name) @@ -33,7 +53,8 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: anim.bone_frames[root_crc] = ([], []) for bone in armature.data.bones: - anim.bone_frames[to_crc(bone.name)] = ([], []) + if bone.name in keyable_bones: + anim.bone_frames[to_crc(bone.name)] = ([], []) for frame in range(num_frames): @@ -50,6 +71,9 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: for bone in armature.pose.bones: + if bone.name not in keyable_bones: + continue + transform = bone.matrix if bone.parent: diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 1eade29..90361d6 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -311,7 +311,8 @@ def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveSh if "box" in name or "cube" in name or "cuboid" in name: return CollisionPrimitiveShape.BOX - raise RuntimeError(f"Object '{obj.name}' has no primitive type specified in it's name!") + return CollisionPrimitiveShape.BOX + #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): """ Checks if the object has an LOD suffix that is known to be ignored by """ @@ -371,10 +372,11 @@ def select_objects(export_target: str) -> List[bpy.types.Object]: -def expand_armature(obj: bpy.types.Object) -> List[Model]: +def expand_armature(armature: bpy.types.Object) -> List[Model]: + bones: List[Model] = [] - for bone in obj.data.bones: + for bone in armature.data.bones: model = Model() transform = bone.matrix_local @@ -382,11 +384,16 @@ def expand_armature(obj: bpy.types.Object) -> List[Model]: if bone.parent: transform = bone.parent.matrix_local.inverted() @ transform model.parent = bone.parent.name + # If the bone has no parent_bone: + # set model parent to SKIN object if there is one + # set model parent to armature parent if there is one else: - model.parent = obj.parent.name - for child_obj in obj.children: - if child_obj.vertex_groups and not get_is_model_hidden(obj) and not obj.parent_bone: + for child_obj in armature.children: + if child_obj.vertex_groups and not get_is_model_hidden(child_obj) and not child_obj.parent_bone: model.parent = child_obj.name + break + if not model.parent and armature.parent: + model.parent = armature.parent.name local_translation, local_rotation, _ = transform.decompose() diff --git a/addons/io_scene_swbf_msh/msh_model_utilities.py b/addons/io_scene_swbf_msh/msh_model_utilities.py index afc692a..1b8d314 100644 --- a/addons/io_scene_swbf_msh/msh_model_utilities.py +++ b/addons/io_scene_swbf_msh/msh_model_utilities.py @@ -9,7 +9,7 @@ from mathutils import Vector, Matrix def inject_dummy_data(model : Model): - """ Adds a triangle and material to the scene root when exporting skeletons to satisfy ZenAsset. """ + """ Adds a triangle and material to the model (scene root). Needed to export zenasst-compatible skeletons. """ model.hidden = True dummy_seg = GeometrySegment() diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index ccc7587..098aa89 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -30,14 +30,20 @@ def save_scene(output_file, scene: Scene): with msh2.create_child("MODL") as modl: _write_modl(modl, model, index, material_index, model_index) + # Contrary to earlier belief, anim/skel info does not need to be exported for animated models + # BUT, unless a model is a BONE, it wont animate! + # This is not necessary when exporting animations. When exporting animations, the following + # chunks are necessary and the animated models can be marked as NULLs if scene.animation is not None: + # Seems as though SKL2 is wholly unneccessary from SWBF's perspective (for models and anims), + # but it is there in all stock models/anims with hedr.create_child("SKL2") as skl2: _write_skl2(skl2, scene.animation) with hedr.create_child("BLN2") as bln2: _write_bln2(bln2, scene.animation) - with hedr.create_child("ANM2") as anm2: #simple for now + with hedr.create_child("ANM2") as anm2: _write_anm2(anm2, scene.animation) with hedr.create_child("CL1L"): diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index 7701138..bde5682 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -113,14 +113,7 @@ def required_skeleton_to_armature(required_skeleton : List[Model], model_map : D armature_obj = bpy.data.objects.new("skeleton", armature) bpy.context.view_layer.active_layer_collection.collection.objects.link(armature_obj) - - preserved = armature_obj.data.swbf_msh_skel - for model in required_skeleton: - if to_crc(model.name) in msh_scene.skeleton: - entry = preserved.add() - entry.name = model.name - - + bones_set = set([model.name for model in required_skeleton]) armature_obj.select_set(True) @@ -200,7 +193,7 @@ def extract_required_skeleton(scene: Scene) -> List[Model]: 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)) + # 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)) @@ -436,6 +429,13 @@ def extract_scene(filepath: str, scene: Scene): # Create the armature if skel is non-empty 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 + for model in scene.models: + if to_crc(model.name) in scene.skeleton: + entry = preserved.add() + entry.name = model.name + ''' If an armature was created, we need to do a few extra From bae32bdfe426f7566b206076d6c47a92edd7e688 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Sat, 15 Jan 2022 11:45:38 -0500 Subject: [PATCH 37/39] Other/unsupported material option with stub rendertypes and UI elements + fill MaterialProperties on import --- addons/io_scene_swbf_msh/msh_material.py | 23 +++++ .../io_scene_swbf_msh/msh_material_gather.py | 36 ++++--- .../msh_material_properties.py | 53 ++++++++-- .../msh_material_to_blend.py | 99 ++++++++----------- .../msh_material_ui_strings.py | 5 + .../msh_material_utilities.py | 29 ++++++ addons/io_scene_swbf_msh/msh_scene_read.py | 20 ++-- 7 files changed, 168 insertions(+), 97 deletions(-) diff --git a/addons/io_scene_swbf_msh/msh_material.py b/addons/io_scene_swbf_msh/msh_material.py index 560e3bb..6b66700 100644 --- a/addons/io_scene_swbf_msh/msh_material.py +++ b/addons/io_scene_swbf_msh/msh_material.py @@ -18,6 +18,29 @@ class Rendertype(Enum): NORMALMAPPED_ENVMAPPED = 26 NORMALMAPPED = 27 NORMALMAPPED_TILED_ENVMAP = 29 + OTHER_1 = 1 + OTHER_2 = 2 + OTHER_4 = 4 + OTHER_5 = 5 + OTHER_8 = 8 + OTHER_9 = 9 + OTHER_10 = 10 + OTHER_11 = 11 + OTHER_12 = 12 + OTHER_13 = 13 + OTHER_14 = 14 + OTHER_15 = 15 + OTHER_16 = 16 + OTHER_17 = 17 + OTHER_18 = 18 + OTHER_19 = 19 + OTHER_20 = 20 + OTHER_21 = 21 + OTHER_23 = 23 + OTHER_28 = 28 + OTHER_30 = 30 + OTHER_31 = 31 + class MaterialFlags(Flag): NONE = 0 diff --git a/addons/io_scene_swbf_msh/msh_material_gather.py b/addons/io_scene_swbf_msh/msh_material_gather.py index 4fa3f3d..7f058f6 100644 --- a/addons/io_scene_swbf_msh/msh_material_gather.py +++ b/addons/io_scene_swbf_msh/msh_material_gather.py @@ -5,6 +5,8 @@ import bpy from typing import Dict from .msh_material import * +from .msh_material_utilities import _RENDERTYPES_MAPPING + def gather_materials() -> Dict[str, Material]: """ Gathers the Blender materials and returns them as a dictionary of strings and Material objects. """ @@ -31,27 +33,27 @@ def read_material(blender_material: bpy.types.Material) -> Material: result.rendertype = _read_material_props_rendertype(props) result.flags = _read_material_props_flags(props) result.data = _read_material_props_data(props) - result.texture0 = props.diffuse_map - result.texture1 = _read_normal_map_or_distortion_map_texture(props) - result.texture2 = _read_detail_texture(props) - result.texture3 = _read_envmap_texture(props) + + if "UNSUPPORTED" not in props.rendertype: + result.texture0 = props.diffuse_map + result.texture1 = _read_normal_map_or_distortion_map_texture(props) + result.texture2 = _read_detail_texture(props) + result.texture3 = _read_envmap_texture(props) + + else: + result.texture0 = props.texture_0 + result.texture1 = props.texture_1 + result.texture2 = props.texture_2 + result.texture3 = props.texture_3 return result -_RENDERTYPES_MAPPING = { - "NORMAL_BF2": Rendertype.NORMAL, - "SCROLLING_BF2": Rendertype.SCROLLING, - "ENVMAPPED_BF2": Rendertype.ENVMAPPED, - "ANIMATED_BF2": Rendertype.ANIMATED, - "REFRACTION_BF2": Rendertype.REFRACTION, - "BLINK_BF2": Rendertype.BLINK, - "NORMALMAPPED_TILED_BF2": Rendertype.NORMALMAPPED_TILED, - "NORMALMAPPED_ENVMAPPED_BF2": Rendertype.NORMALMAPPED_ENVMAPPED, - "NORMALMAPPED_BF2": Rendertype.NORMALMAPPED, - "NORMALMAPPED_TILED_ENVMAPPED_BF2": Rendertype.NORMALMAPPED_TILED_ENVMAP} def _read_material_props_rendertype(props) -> Rendertype: - return _RENDERTYPES_MAPPING[props.rendertype] + if "UNSUPPORTED" in props.rendertype: + return Rendertype(props.rendertype_value) + else: + return _RENDERTYPES_MAPPING[props.rendertype] def _read_material_props_flags(props) -> MaterialFlags: if "REFRACTION" in props.rendertype: @@ -79,6 +81,8 @@ def _read_material_props_flags(props) -> MaterialFlags: return flags def _read_material_props_data(props) -> Tuple[int, int]: + if "UNSUPPORTED" in props.rendertype: + return (props.data_value_0, props.data_value_1) if "SCROLLING" in props.rendertype: return (props.scroll_speed_u, props.scroll_speed_v) if "BLINK" in props.rendertype: diff --git a/addons/io_scene_swbf_msh/msh_material_properties.py b/addons/io_scene_swbf_msh/msh_material_properties.py index 97c7313..cdca6c8 100644 --- a/addons/io_scene_swbf_msh/msh_material_properties.py +++ b/addons/io_scene_swbf_msh/msh_material_properties.py @@ -3,7 +3,11 @@ import bpy from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatVectorProperty, IntProperty from bpy.types import PropertyGroup + +from .msh_material import * from .msh_material_ui_strings import * +from .msh_material_utilities import _REVERSE_RENDERTYPES_MAPPING + UI_MATERIAL_RENDERTYPES = ( ('NORMAL_BF2', "00 Normal (SWBF2)", UI_RENDERTYPE_NORMAL_BF2_DESC), @@ -15,7 +19,9 @@ UI_MATERIAL_RENDERTYPES = ( ('NORMALMAPPED_TILED_BF2', "24 Normalmapped Tiled (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_TILED_BF2_DESC), ('NORMALMAPPED_ENVMAPPED_BF2', "26 Normalmapped Envmapped (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_ENVMAPPED_BF2_DESC), ('NORMALMAPPED_BF2', "27 Normalmapped (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_BF2_DESC), - ('NORMALMAPPED_TILED_ENVMAPPED_BF2', "29 Normalmapped Tiled Envmapped (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_TILED_ENVMAPPED_BF2_DESC)) + ('NORMALMAPPED_TILED_ENVMAPPED_BF2', "29 Normalmapped Tiled Envmapped (SWBF2)", UI_RENDERTYPE_NORMALMAPPED_TILED_ENVMAPPED_BF2_DESC), + ('UNSUPPORTED', "Other (SWBF1/2)", UI_RENDERTYPE_UNSUPPORTED_BF2_DESC)) + def _make_anim_length_entry(length): from math import sqrt @@ -182,6 +188,17 @@ class MaterialProperties(PropertyGroup): "distort the scene behind them. Should be a normal map " "with '-forceformat v8u8' in it's '.tga.option' file.") + data_value_0: IntProperty(name="", description="First data value") + data_value_1: IntProperty(name="", description="Second data value") + + rendertype_value: IntProperty(name="Rendertype Value", description="Raw number value of rendertype.", min=0, max=31) + + texture_0: StringProperty(name="1", description="First texture slot") + texture_1: StringProperty(name="2", description="Second texture slot") + texture_2: StringProperty(name="3", description="Third texture slot") + texture_3: StringProperty(name="4", description="Fourth texture slot") + + class MaterialPropertiesPanel(bpy.types.Panel): """ Creates a Panel in the Object properties window """ bl_label = "SWBF .msh Properties" @@ -190,6 +207,8 @@ class MaterialPropertiesPanel(bpy.types.Panel): bl_region_type = 'WINDOW' bl_context = "material" + + def draw(self, context): if context.material is None: return @@ -199,6 +218,10 @@ class MaterialPropertiesPanel(bpy.types.Panel): material_props = context.material.swbf_msh layout.prop(material_props, "rendertype") + + if "UNSUPPORTED" in material_props.rendertype: + layout.prop(material_props, "rendertype_value") + layout.prop(material_props, "specular_color") if "REFRACTION" not in material_props.rendertype: @@ -233,21 +256,31 @@ class MaterialPropertiesPanel(bpy.types.Panel): elif "NORMALMAPPED_TILED" in material_props.rendertype: row.prop(material_props, "normal_map_tiling_u") row.prop(material_props, "normal_map_tiling_v") + elif "UNSUPPORTED" in material_props.rendertype: + row.prop(material_props, "data_value_0") + row.prop(material_props, "data_value_1") else: row.prop(material_props, "detail_map_tiling_u") row.prop(material_props, "detail_map_tiling_v") layout.label(text="Texture Maps: ") - layout.prop(material_props, "diffuse_map") + if "UNSUPPORTED" not in material_props.rendertype: + layout.prop(material_props, "diffuse_map") - if "REFRACTION" not in material_props.rendertype: - layout.prop(material_props, "detail_map") + if "REFRACTION" not in material_props.rendertype: + layout.prop(material_props, "detail_map") - if "NORMALMAPPED" in material_props.rendertype: - layout.prop(material_props, "normal_map") + if "NORMALMAPPED" in material_props.rendertype: + layout.prop(material_props, "normal_map") - if "ENVMAPPED" in material_props.rendertype: - layout.prop(material_props, "environment_map") + if "ENVMAPPED" in material_props.rendertype: + layout.prop(material_props, "environment_map") + + if "REFRACTION" in material_props.rendertype: + layout.prop(material_props, "distortion_map") + else: + layout.prop(material_props, "texture_0") + layout.prop(material_props, "texture_1") + layout.prop(material_props, "texture_2") + layout.prop(material_props, "texture_3") - if "REFRACTION" in material_props.rendertype: - layout.prop(material_props, "distortion_map") diff --git a/addons/io_scene_swbf_msh/msh_material_to_blend.py b/addons/io_scene_swbf_msh/msh_material_to_blend.py index a0ae612..3ec0367 100644 --- a/addons/io_scene_swbf_msh/msh_material_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_material_to_blend.py @@ -6,6 +6,10 @@ from .msh_material import * from .msh_material_gather import * from .msh_material_properties import * +from .msh_material_utilities import _REVERSE_RENDERTYPES_MAPPING + +from math import sqrt + import os @@ -36,35 +40,22 @@ def fill_material_props(material : Material, material_properties): if material_properties is None or material is None: return + material_properties.rendertype_value = material.rendertype.value + material_properties.specular_color = (material.specular_color[0], material.specular_color[1], material.specular_color[2]) - material_properties.diffuse_map = material.texture0 _fill_material_props_rendertype(material, material_properties) _fill_material_props_flags(material, material_properties) _fill_material_props_data(material, material_properties) - - _fill_normal_map_or_distortion_map_texture(material, material_properties) - _fill_detail_texture(material, material_properties) - _fill_envmap_texture(material, material_properties) + _fill_material_props_texture_maps(material, material_properties) def _fill_material_props_rendertype(material, material_properties): - - _REVERSE_RENDERTYPES_MAPPING = { - Rendertype.NORMAL : "NORMAL_BF2", - Rendertype.SCROLLING : "SCROLLING_BF2", - Rendertype.ENVMAPPED : "ENVMAPPED_BF2", - Rendertype.ANIMATED : "ANIMATED_BF2", - Rendertype.REFRACTION : "REFRACTION_BF2", - Rendertype.BLINK : "BLINK_BF2", - Rendertype.NORMALMAPPED_TILED : "NORMALMAPPED_TILED_BF2", - Rendertype.NORMALMAPPED_ENVMAPPED : "NORMALMAPPED_ENVMAPPED_BF2", - Rendertype.NORMALMAPPED : "NORMALMAPPED_BF2", - Rendertype.NORMALMAPPED_TILED_ENVMAP : "NORMALMAPPED_TILED_ENVMAPPED_BF2"} - - material_properties.rendertype = _REVERSE_RENDERTYPES_MAPPING[material.rendertype] - + if material.rendertype in _REVERSE_RENDERTYPES_MAPPING: + material_properties.rendertype = _REVERSE_RENDERTYPES_MAPPING[material.rendertype] + else: + material_properties.rendertype = "UNSUPPORTED" def _fill_material_props_flags(material, material_properties): @@ -85,49 +76,41 @@ def _fill_material_props_flags(material, material_properties): def _fill_material_props_data(material, material_properties): - if material.rendertype == Rendertype.SCROLLING: - material_properties.scroll_speed_u = material.data[0] - material_properties.scroll_speed_v = material.data[1] - - elif material.rendertype == Rendertype.BLINK: - material_properties.blink_min_brightness = material.data[0] - material_properties.blink_speed = material.data[1] - - elif material.rendertype == Rendertype.NORMALMAPPED_TILED_ENVMAP or material.rendertype == Rendertype.NORMALMAPPED_TILED: - material_properties.normal_map_tiling_u = material.data[0] - material_properties.normal_map_tiling_v = material.data[1] - - elif material.rendertype == Rendertype.REFRACTION: - pass - - elif material.rendertype == Rendertype.ANIMATED: - anim_length_index = int(sqrt(length)) - 1 - if animation_length_index < 0: - animation_length_index = 0 - elif animation_length_index > len(UI_MATERIAL_ANIMATION_LENGTHS): - animation_length_index = len(UI_MATERIAL_ANIMATION_LENGTHS) - 1 + material_properties.data_value_0 = material.data[0] + material_properties.data_value_1 = material.data[1] - material_properties.animation_length = UI_MATERIAL_ANIMATION_LENGTHS[animation_length_index] - material_properties.animation_speed = material.data[1] - - else: - material_properties.detail_map_tiling_u = material.data[0] - material_properties.detail_map_tiling_v = material.data[1] + material_properties.scroll_speed_u = material.data[0] + material_properties.scroll_speed_v = material.data[1] + + material_properties.blink_min_brightness = material.data[0] + material_properties.blink_speed = material.data[1] + + material_properties.normal_map_tiling_u = material.data[0] + material_properties.normal_map_tiling_v = material.data[1] + + anim_length_index = int(sqrt(material.data[0])) + if anim_length_index < 0: + anim_length_index = 0 + elif anim_length_index > len(UI_MATERIAL_ANIMATION_LENGTHS): + anim_length_index = len(UI_MATERIAL_ANIMATION_LENGTHS) - 1 + + material_properties.animation_length = UI_MATERIAL_ANIMATION_LENGTHS[anim_length_index][0] + material_properties.animation_speed = material.data[1] + + material_properties.detail_map_tiling_u = material.data[0] + material_properties.detail_map_tiling_v = material.data[1] -def _fill_normal_map_or_distortion_map_texture(material, material_properties): - if material.rendertype == Rendertype.REFRACTION: +def _fill_material_props_texture_maps(material, material_properties): + + material_properties.texture_0 = material.texture0 + material_properties.texture_1 = material.texture1 + material_properties.texture_2 = material.texture2 + material_properties.texture_3 = material.texture3 + + material_properties.diffuse_map = material.texture0 material_properties.distortion_map = material.texture1 - elif material.rendertype.value > 24: material_properties.normal_map = material.texture1 - - -def _fill_detail_texture(material, material_properties): - if material.rendertype != Rendertype.REFRACTION: material_properties.detail_map = material.texture2 - - -def _fill_envmap_texture(material, material_properties): - if material.rendertype != Rendertype.ENVMAPPED: material_properties.environment_map = material.texture3 diff --git a/addons/io_scene_swbf_msh/msh_material_ui_strings.py b/addons/io_scene_swbf_msh/msh_material_ui_strings.py index 2dd115d..d4fba86 100644 --- a/addons/io_scene_swbf_msh/msh_material_ui_strings.py +++ b/addons/io_scene_swbf_msh/msh_material_ui_strings.py @@ -1,5 +1,10 @@ """ UI strings that are too long to have in msh_materials_properties.py """ + +UI_RENDERTYPE_UNSUPPORTED_BF2_DESC = \ + "Unsupported rendertype. The raw values of the material "\ + "are fully accessible, but their purpose is unknown. " + UI_RENDERTYPE_DETAIL_MAP_DESC = \ "Can optionally have a Detail Map." diff --git a/addons/io_scene_swbf_msh/msh_material_utilities.py b/addons/io_scene_swbf_msh/msh_material_utilities.py index f390324..1765032 100644 --- a/addons/io_scene_swbf_msh/msh_material_utilities.py +++ b/addons/io_scene_swbf_msh/msh_material_utilities.py @@ -4,6 +4,35 @@ from typing import Dict, List from .msh_material import * from .msh_model import * + +_RENDERTYPES_MAPPING = { + "NORMAL_BF2": Rendertype.NORMAL, + "SCROLLING_BF2": Rendertype.SCROLLING, + "ENVMAPPED_BF2": Rendertype.ENVMAPPED, + "ANIMATED_BF2": Rendertype.ANIMATED, + "REFRACTION_BF2": Rendertype.REFRACTION, + "BLINK_BF2": Rendertype.BLINK, + "NORMALMAPPED_TILED_BF2": Rendertype.NORMALMAPPED_TILED, + "NORMALMAPPED_ENVMAPPED_BF2": Rendertype.NORMALMAPPED_ENVMAPPED, + "NORMALMAPPED_BF2": Rendertype.NORMALMAPPED, + "NORMALMAPPED_TILED_ENVMAPPED_BF2": Rendertype.NORMALMAPPED_TILED_ENVMAP} + + +_REVERSE_RENDERTYPES_MAPPING = { + Rendertype.NORMAL : "NORMAL_BF2", + Rendertype.SCROLLING : "SCROLLING_BF2", + Rendertype.ENVMAPPED : "ENVMAPPED_BF2", + Rendertype.ANIMATED : "ANIMATED_BF2", + Rendertype.REFRACTION : "REFRACTION_BF2", + Rendertype.BLINK : "BLINK_BF2", + Rendertype.NORMALMAPPED_TILED : "NORMALMAPPED_TILED_BF2", + Rendertype.NORMALMAPPED_ENVMAPPED : "NORMALMAPPED_ENVMAPPED_BF2", + Rendertype.NORMALMAPPED : "NORMALMAPPED_BF2", + Rendertype.NORMALMAPPED_TILED_ENVMAP : "NORMALMAPPED_TILED_ENVMAPPED_BF2"} + + + + def remove_unused_materials(materials: Dict[str, Material], models: List[Model]) -> Dict[str, Material]: """ Given a dictionary of materials and a list of models diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index e537dcc..f05267d 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -169,7 +169,7 @@ def _read_matd(matd: Reader) -> Material: elif next_header == "ATRB": with matd.read_child() as atrb: - mat.flags = MaterialFlags(atrb.read_u8()) + mat.flags = MaterialFlags(atrb.read_u8()) mat.rendertype = Rendertype(atrb.read_u8()) mat.data = atrb.read_u8(2) @@ -211,11 +211,7 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: with modl.read_child() as mndx: index = mndx.read_u32() - global model_counter - #print(mndx.indent + "MNDX doesn't match counter, expected: {} found: {}".format(model_counter, index)) - #print("Model counter: {} MNDX: {}".format(model_counter, index)) - global mndx_remap mndx_remap[index] = model_counter @@ -255,6 +251,10 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: num_indicies = envl.read_u32() envelope += [envl.read_u32() for _ in range(num_indicies)] + elif next_header_geom == "CLTH": + with geom.read_child() as clth: + pass + else: geom.skip_bytes(1) @@ -359,7 +359,8 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: for _ in range(num_tris): geometry_seg.triangles.append(ndxt.read_u16(3)) - # + + # There could be major issues with this, so far it hasn't failed but its inelegance irks me elif next_header == "STRP": strips : List[List[int]] = [] @@ -474,13 +475,6 @@ def _read_anm2(anm2: Reader) -> Animation: anim.bone_frames[bone_crc] = frames - - for bone_crc in sorted(bone_crcs): - - bone_frames = anim.bone_frames[bone_crc] - - loc_frames = bone_frames[0] - rot_frames = bone_frames[1] else: anm2.skip_bytes(1) From dce3f4e498791400570f13b0c800449f1ef562f4 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Tue, 18 Jan 2022 15:16:49 -0500 Subject: [PATCH 38/39] 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 --- addons/io_scene_swbf_msh/__init__.py | 11 +- addons/io_scene_swbf_msh/msh_anim_gather.py | 77 +++--- .../msh_collision_prim_properties.py | 16 ++ addons/io_scene_swbf_msh/msh_material.py | 2 + .../io_scene_swbf_msh/msh_material_gather.py | 4 +- .../msh_material_properties.py | 3 +- .../msh_material_to_blend.py | 2 +- addons/io_scene_swbf_msh/msh_model.py | 6 +- addons/io_scene_swbf_msh/msh_model_gather.py | 21 +- .../io_scene_swbf_msh/msh_model_utilities.py | 19 +- addons/io_scene_swbf_msh/msh_scene.py | 76 +----- addons/io_scene_swbf_msh/msh_scene_read.py | 24 +- addons/io_scene_swbf_msh/msh_scene_save.py | 4 +- .../io_scene_swbf_msh/msh_scene_utilities.py | 85 ++++++ .../msh_skeleton_properties.py | 13 +- .../msh_skeleton_utilities.py | 211 +++++++++++++++ addons/io_scene_swbf_msh/msh_to_blend.py | 242 ++++-------------- 17 files changed, 469 insertions(+), 347 deletions(-) create mode 100644 addons/io_scene_swbf_msh/msh_collision_prim_properties.py create mode 100644 addons/io_scene_swbf_msh/msh_scene_utilities.py create mode 100644 addons/io_scene_swbf_msh/msh_skeleton_utilities.py diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index bc29a21..7602390 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -56,11 +56,12 @@ import bpy from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy.props import BoolProperty, EnumProperty, CollectionProperty 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_read import read_scene from .msh_material_properties import * from .msh_skeleton_properties import * +from .msh_collision_prim_properties import * from .msh_to_blend import * from .zaa_to_blend import * @@ -189,6 +190,8 @@ def menu_func_import(self, context): def register(): + bpy.utils.register_class(CollisionPrimitiveProperties) + bpy.utils.register_class(MaterialProperties) 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_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) + def unregister(): + bpy.utils.unregister_class(CollisionPrimitiveProperties) + bpy.utils.unregister_class(MaterialProperties) bpy.utils.unregister_class(MaterialPropertiesPanel) diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 8a4c613..909340c 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -1,5 +1,4 @@ -""" Gathers the Blender objects from the current scene and returns them as a list of - Model objects. """ +""" Converts currently active Action to an msh Animation """ import bpy import math @@ -11,30 +10,40 @@ from .msh_model_utilities import * from .msh_utilities import * from .msh_model_gather import * +from .msh_skeleton_utilities import * + 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: + 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 + # Set of bones to include in SKL2/animation stuff - keyable_bones : Set[str] = set() - - msh_skel = armature.data.swbf_msh_skel + keyable_bones = get_real_BONES(armature) - 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: - for bone in msh_skel: - #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) + # Subset of above bones to key with dummy frames (all bones not in armature) + dummy_bones = set([keyable_bone for keyable_bone in keyable_bones if keyable_bone not in armature.data.bones]) anim = Animation(); @@ -51,41 +60,41 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: anim.end_index = num_frames - 1 - anim.bone_frames[root_crc] = ([], []) - for bone in armature.data.bones: - if bone.name in keyable_bones: - anim.bone_frames[to_crc(bone.name)] = ([], []) + + for keyable_bone in keyable_bones: + anim.bone_frames[to_crc(keyable_bone)] = ([], []) + for frame in range(num_frames): frame_time = framerange.x + frame * increment bpy.context.scene.frame_set(frame_time) + for keyable_bone in keyable_bones: - rframe_dummy = RotationFrame(frame, convert_rotation_space(Quaternion())) - tframe_dummy = TranslationFrame(frame, Vector((0.0,0.0,0.0))) + bone_crc = to_crc(keyable_bone) - anim.bone_frames[root_crc][0].append(tframe_dummy) - anim.bone_frames[root_crc][1].append(rframe_dummy) + if keyable_bone in dummy_bones: + 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: - continue + bone = armature.pose.bones[keyable_bone] - transform = bone.matrix + transform = bone.matrix - if bone.parent: - transform = bone.parent.matrix.inverted() @ transform - - loc, rot, _ = transform.decompose() + if bone.parent: + transform = bone.parent.matrix.inverted() @ transform + + loc, rot, _ = transform.decompose() - rframe = RotationFrame(frame, convert_rotation_space(rot)) - tframe = TranslationFrame(frame, convert_vector_space(loc)) + rframe = RotationFrame(frame, convert_rotation_space(rot)) + tframe = TranslationFrame(frame, convert_vector_space(loc)) - anim.bone_frames[to_crc(bone.name)][0].append(tframe) - anim.bone_frames[to_crc(bone.name)][1].append(rframe) + anim.bone_frames[bone_crc][0].append(tframe) + anim.bone_frames[bone_crc][1].append(rframe) return anim diff --git a/addons/io_scene_swbf_msh/msh_collision_prim_properties.py b/addons/io_scene_swbf_msh/msh_collision_prim_properties.py new file mode 100644 index 0000000..f5074cb --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_collision_prim_properties.py @@ -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) + + + \ No newline at end of file diff --git a/addons/io_scene_swbf_msh/msh_material.py b/addons/io_scene_swbf_msh/msh_material.py index 6b66700..da55a42 100644 --- a/addons/io_scene_swbf_msh/msh_material.py +++ b/addons/io_scene_swbf_msh/msh_material.py @@ -18,6 +18,8 @@ class Rendertype(Enum): NORMALMAPPED_ENVMAPPED = 26 NORMALMAPPED = 27 NORMALMAPPED_TILED_ENVMAP = 29 + + # Placeholders to avoid crashes/import-export inconsistencies OTHER_1 = 1 OTHER_2 = 2 OTHER_4 = 4 diff --git a/addons/io_scene_swbf_msh/msh_material_gather.py b/addons/io_scene_swbf_msh/msh_material_gather.py index 7f058f6..39ea6d4 100644 --- a/addons/io_scene_swbf_msh/msh_material_gather.py +++ b/addons/io_scene_swbf_msh/msh_material_gather.py @@ -24,10 +24,10 @@ def read_material(blender_material: bpy.types.Material) -> Material: result = Material() - if blender_material.swbf_msh is None: + if blender_material.swbf_msh_mat is None: return result - props = blender_material.swbf_msh + props = blender_material.swbf_msh_mat result.specular_color = props.specular_color.copy() result.rendertype = _read_material_props_rendertype(props) diff --git a/addons/io_scene_swbf_msh/msh_material_properties.py b/addons/io_scene_swbf_msh/msh_material_properties.py index cdca6c8..9a35768 100644 --- a/addons/io_scene_swbf_msh/msh_material_properties.py +++ b/addons/io_scene_swbf_msh/msh_material_properties.py @@ -188,6 +188,7 @@ class MaterialProperties(PropertyGroup): "distort the scene behind them. Should be a normal map " "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_1: IntProperty(name="", description="Second data value") @@ -215,7 +216,7 @@ class MaterialPropertiesPanel(bpy.types.Panel): layout = self.layout - material_props = context.material.swbf_msh + material_props = context.material.swbf_msh_mat layout.prop(material_props, "rendertype") diff --git a/addons/io_scene_swbf_msh/msh_material_to_blend.py b/addons/io_scene_swbf_msh/msh_material_to_blend.py index 3ec0367..398abc5 100644 --- a/addons/io_scene_swbf_msh/msh_material_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_material_to_blend.py @@ -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 from typing import Dict diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index e2d624b..ecf7c47 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -12,7 +12,11 @@ class ModelType(Enum): CLOTH = 2 BONE = 3 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): SPHERE = 0 diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 90361d6..86764cc 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -9,6 +9,7 @@ from itertools import zip_longest from .msh_model import * from .msh_model_utilities import * from .msh_utilities import * +from .msh_skeleton_utilities import * SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"} 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: if obj.parent is not None: 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 else: model.parent = obj.parent.name @@ -298,6 +299,9 @@ def get_collision_primitive(obj: bpy.types.Object) -> CollisionPrimitive: return primitive + + + def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveShape: """ Gets the CollisionPrimitiveShape of an object or raises an error if 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: return CollisionPrimitiveShape.BOX - return CollisionPrimitiveShape.BOX - #raise RuntimeError(f"Object '{obj.name}' has no primitive type specified in it's name!") + # arc170 fighter has examples of box colliders without proper naming + 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): """ 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]: - + + proper_BONES = get_real_BONES(armature) + bones: List[Model] = [] 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() - model.model_type = ModelType.BONE + model.model_type = ModelType.BONE if bone.name in proper_BONES else ModelType.NULL model.name = bone.name model.transform.rotation = convert_rotation_space(local_rotation) model.transform.translation = convert_vector_space(local_translation) diff --git a/addons/io_scene_swbf_msh/msh_model_utilities.py b/addons/io_scene_swbf_msh/msh_model_utilities.py index 1b8d314..fb73a40 100644 --- a/addons/io_scene_swbf_msh/msh_model_utilities.py +++ b/addons/io_scene_swbf_msh/msh_model_utilities.py @@ -26,14 +26,17 @@ def inject_dummy_data(model : Model): model.geometry = [dummy_seg] 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)) -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)) 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]): """ Scales are positions in the GeometrySegment list. """ @@ -145,13 +148,3 @@ def is_model_name_unused(name: str, models: List[Model]) -> bool: 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)) - diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index 755a365..c803dda 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -4,17 +4,14 @@ 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_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 + @dataclass class SceneAABB: @@ -47,69 +44,4 @@ class Scene: animation: Animation = None - 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 + skeleton: List[int] = field(default_factory=list) \ No newline at end of file diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index f05267d..c946263 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -117,20 +117,10 @@ def read_scene(input_file, anim_only=False, debug=0) -> Scene: if seg.weights: for weight_set in seg.weights: for vweight in weight_set: - if vweight.bone in mndx_remap: vweight.bone = mndx_remap[vweight.bone] else: 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 @@ -276,10 +266,6 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: else: 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 @@ -292,10 +278,6 @@ def _read_tran(tran: Reader) -> ModelTransform: xform.rotation = tran.read_quat() 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 @@ -341,6 +323,7 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: for _ in range(num_texcoords): geometry_seg.texcoords.append(Vector(uv0l.read_f32(2))) + # TODO: Can't remember exact issue here... elif next_header == "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 - #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) elif next_header == "WGHT": @@ -439,6 +423,8 @@ def _read_anm2(anm2: Reader) -> Animation: # Dont even know what CYCL's data does. Tried playing # 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() diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 098aa89..a2930df 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -2,7 +2,8 @@ from itertools import islice 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_material import * from .msh_writer import Writer @@ -40,6 +41,7 @@ def save_scene(output_file, scene: Scene): with hedr.create_child("SKL2") as skl2: _write_skl2(skl2, scene.animation) + # Def not necessary, including anyways with hedr.create_child("BLN2") as bln2: _write_bln2(bln2, scene.animation) diff --git a/addons/io_scene_swbf_msh/msh_scene_utilities.py b/addons/io_scene_swbf_msh/msh_scene_utilities.py new file mode 100644 index 0000000..3927824 --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_scene_utilities.py @@ -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 diff --git a/addons/io_scene_swbf_msh/msh_skeleton_properties.py b/addons/io_scene_swbf_msh/msh_skeleton_properties.py index 139e5b0..1bf5530 100644 --- a/addons/io_scene_swbf_msh/msh_skeleton_properties.py +++ b/addons/io_scene_swbf_msh/msh_skeleton_properties.py @@ -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 -from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatVectorProperty, IntProperty +from bpy.props import StringProperty from bpy.types import PropertyGroup -from .msh_material_ui_strings import * -from .msh_model import * class SkeletonProperties(PropertyGroup): diff --git a/addons/io_scene_swbf_msh/msh_skeleton_utilities.py b/addons/io_scene_swbf_msh/msh_skeleton_utilities.py new file mode 100644 index 0000000..7e260ca --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_skeleton_utilities.py @@ -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 diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index bde5682..6814fb7 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -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 From 58e229f6ad3c922d33f5e8a28b269ea4c8c50df5 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 18 Jan 2022 15:46:30 -0500 Subject: [PATCH 39/39] Check if nothing selected before applying anim --- addons/io_scene_swbf_msh/msh_to_blend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index 6814fb7..f814332 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -26,7 +26,7 @@ def extract_and_apply_anim(filename : str, scene : Scene): arma = bpy.context.view_layer.objects.active - if arma.type != 'ARMATURE': + if not arma or arma.type != 'ARMATURE': raise Exception("Select an armature to attach the imported animation to!") if scene.animation is None: