Basic reading/writing

This commit is contained in:
Will Snyder 2020-11-29 20:27:46 -05:00
commit 500c3f2bd1
11 changed files with 832 additions and 18 deletions

View File

@ -53,12 +53,15 @@ if "bpy" in locals():
# End of stuff taken from glTF # End of stuff taken from glTF
import bpy 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.props import BoolProperty, EnumProperty
from bpy.types import Operator from bpy.types import Operator
from .msh_scene import create_scene from .msh_scene import create_scene
from .msh_scene_save import save_scene from .msh_scene_save import save_scene
from .msh_scene_read import read_scene
from .msh_material_properties import * from .msh_material_properties import *
from .msh_to_blend import *
class ExportMSH(Operator, ExportHelper): class ExportMSH(Operator, ExportHelper):
""" Export the current scene as a SWBF .msh file. """ """ Export the current scene as a SWBF .msh file. """
@ -126,16 +129,47 @@ class ExportMSH(Operator, ExportHelper):
return {'FINISHED'} return {'FINISHED'}
# Only needed if you want to add into a dynamic menu # Only needed if you want to add into a dynamic menu
def menu_func_export(self, context): def menu_func_export(self, context):
self.layout.operator(ExportMSH.bl_idname, text="SWBF msh (.msh)") 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:
extract_scene(self.filepath, read_scene(input_file))
return {'FINISHED'}
def menu_func_import(self, context):
self.layout.operator(ImportMSH.bl_idname, text="SWBF msh (.msh)")
def register(): def register():
bpy.utils.register_class(MaterialProperties) bpy.utils.register_class(MaterialProperties)
bpy.utils.register_class(MaterialPropertiesPanel) bpy.utils.register_class(MaterialPropertiesPanel)
bpy.utils.register_class(ExportMSH) 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_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.Material.swbf_msh = bpy.props.PointerProperty(type=MaterialProperties)
@ -143,7 +177,10 @@ def unregister():
bpy.utils.unregister_class(MaterialProperties) bpy.utils.unregister_class(MaterialProperties)
bpy.utils.unregister_class(MaterialPropertiesPanel) bpy.utils.unregister_class(MaterialPropertiesPanel)
bpy.utils.unregister_class(ExportMSH) 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_export.remove(menu_func_export)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
if __name__ == "__main__": if __name__ == "__main__":
register() register()

View File

@ -32,8 +32,9 @@ class MaterialFlags(Flag):
@dataclass @dataclass
class Material: class Material:
""" Data class representing a .msh material. """ Data class representing a .msh material."""
Intended to be stored in a dictionary so name is missing. """
name: str = ""
specular_color: Color = Color((1.0, 1.0, 1.0)) specular_color: Color = Color((1.0, 1.0, 1.0))
rendertype: Rendertype = Rendertype.NORMAL rendertype: Rendertype = Rendertype.NORMAL

View File

@ -2,7 +2,7 @@
saved to a .msh file. """ saved to a .msh file. """
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Dict, Tuple from typing import List, Tuple
from enum import Enum from enum import Enum
from mathutils import Vector, Quaternion from mathutils import Vector, Quaternion
@ -12,12 +12,13 @@ class ModelType(Enum):
CLOTH = 2 CLOTH = 2
BONE = 3 BONE = 3
STATIC = 4 STATIC = 4
SHADOWVOLUME = 6
class CollisionPrimitiveShape(Enum): class CollisionPrimitiveShape(Enum):
SPHERE = 0 SPHERE = 0
# ELLIPSOID = 1 ELLIPSOID = 1
CYLINDER = 2 CYLINDER = 2
# MESH = 3 MESH = 3
BOX = 4 BOX = 4
@dataclass @dataclass
@ -51,6 +52,8 @@ class GeometrySegment:
triangles: List[List[int]] = field(default_factory=list) triangles: List[List[int]] = field(default_factory=list)
triangle_strips: List[List[int]] = None triangle_strips: List[List[int]] = None
weights: List[List[Tuple[int, float]]] = None
@dataclass @dataclass
class CollisionPrimitive: class CollisionPrimitive:
""" Class representing a 'SWCI' section in a .msh file. """ """ Class representing a 'SWCI' section in a .msh file. """
@ -76,6 +79,7 @@ class Model:
geometry: List[GeometrySegment] = None geometry: List[GeometrySegment] = None
collisionprimitive: CollisionPrimitive = None collisionprimitive: CollisionPrimitive = None
@dataclass @dataclass
class Animation: class Animation:
""" Class representing 'CYCL' + 'KFR3' sections in a .msh file """ """ Class representing 'CYCL' + 'KFR3' sections in a .msh file """

View File

@ -4,7 +4,7 @@
import bpy import bpy
import math import math
from enum import Enum 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 itertools import zip_longest
from .msh_model import * from .msh_model import *
from .msh_model_utilities import * from .msh_model_utilities import *
@ -80,6 +80,7 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
mesh = obj.to_mesh() mesh = obj.to_mesh()
model.geometry = create_mesh_geometry(mesh, obj.vertex_groups) model.geometry = create_mesh_geometry(mesh, obj.vertex_groups)
obj.to_mesh_clear() obj.to_mesh_clear()
_, _, world_scale = obj.matrix_world.decompose() _, _, world_scale = obj.matrix_world.decompose()
@ -103,7 +104,6 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool
models_list.append(model) models_list.append(model)
return models_list return models_list
@ -119,7 +119,6 @@ def create_parents_set() -> Set[str]:
return parents return parents
def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> 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. """ Creates a list of GeometrySegment objects from a Blender mesh.
Does NOT create triangle strips in the GeometrySegment however. """ Does NOT create triangle strips in the GeometrySegment however. """
@ -218,7 +217,6 @@ def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[Geomet
return new_index return new_index
for tri in mesh.loop_triangles: for tri in mesh.loop_triangles:
polygons[tri.material_index].add(tri.polygon_index) polygons[tri.material_index].add(tri.polygon_index)
segments[tri.material_index].triangles.append([ segments[tri.material_index].triangles.append([

View File

@ -26,6 +26,14 @@ def inject_dummy_data(model : Model):
model.geometry = [dummy_seg] model.geometry = [dummy_seg]
model.model_type = ModelType.STATIC model.model_type = ModelType.STATIC
def convert_vector_space_(vec: Vector) -> Vector:
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]): def scale_segments(scale: Vector, segments: List[GeometrySegment]):
""" Scales are positions in the GeometrySegment list. """ """ Scales are positions in the GeometrySegment list. """

View File

@ -0,0 +1,105 @@
import io
import struct
class Reader:
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()
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
#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 = Reader(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)
return buf.decode("utf-8")
def could_have_child(self):
return self.end_pos - self.file.tell() >= 8
MAX_SIZE: int = 2147483647 - 8

View File

@ -6,7 +6,7 @@ from typing import List, Dict
from copy import copy from copy import copy
import bpy import bpy
from mathutils import Vector from mathutils import Vector
from .msh_model import Model, Animation from .msh_model import Model
from .msh_model_gather import gather_models 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
from .msh_model_triangle_strips import create_models_triangle_strips 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_gather import gather_materials
from .msh_material_utilities import remove_unused_materials from .msh_material_utilities import remove_unused_materials
from .msh_utilities import * from .msh_utilities import *
from .msh_anim_gather import *
@dataclass @dataclass
class SceneAABB: class SceneAABB:
@ -44,8 +43,8 @@ class Scene:
name: str = "Scene" name: str = "Scene"
materials: Dict[str, Material] = field(default_factory=dict) materials: Dict[str, Material] = field(default_factory=dict)
models: List[Model] = field(default_factory=list) models: List[Model] = field(default_factory=list)
anims: List[Animation] = field(default_factory=list)
skeleton: List[int] = field(default_factory=list)
def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool) -> 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. """ """ Create a msh Scene from the active Blender scene. """

View File

@ -0,0 +1,369 @@
""" Contains functions for extracting a scene from 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 *
envls = []
model_counter = 0
def read_scene(input_file) -> Scene:
scene = Scene()
scene.models = []
scene.materials = {}
with Reader(file=input_file) as hedr:
while hedr.could_have_child():
next_header = hedr.peak_next_header()
if "MSH2" in next_header:
with hedr.read_child() as msh2:
materials_list = []
while (msh2.could_have_child()):
next_header = msh2.peak_next_header()
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 "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:
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):
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)
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
def _read_matl_and_get_materials_list(matl: Reader) -> List[Material]:
materials_list: List[Material] = []
num_mats = matl.read_u32()
for _ in range(num_mats):
with matl.read_child() 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() as name:
mat.name = name.read_string()
elif "DATA" in next_header:
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() 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() as tx0d:
mat.texture0 = tx0d.read_string()
elif "TX1D" in next_header:
with matd.read_child() as tx1d:
mat.texture1 = tx1d.read_string()
elif "TX2D" in next_header:
with matd.read_child() as tx2d:
mat.texture2 = tx2d.read_string()
elif "TX3D" in next_header:
with matd.read_child() as tx3d:
mat.texture3 = tx3d.read_string()
else:
matd.skip_bytes(4)
return mat
def _read_modl(modl: Reader, materials_list: List[Material]) -> Model:
model = Model()
while modl.could_have_child():
next_header = modl.peak_next_header()
if "MTYP" in next_header:
with modl.read_child() as mtyp:
model.model_type = ModelType(mtyp.read_u32())
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))
model_counter += 1
elif "NAME" in next_header:
with modl.read_child() as name:
model.name = name.read_string()
elif "PRNT" in next_header:
with modl.read_child() as prnt:
model.parent = prnt.read_string()
elif "FLGS" in next_header:
with modl.read_child() as flgs:
model.hidden = flgs.read_u32()
elif "TRAN" in next_header:
with modl.read_child() as tran:
model.transform = _read_tran(tran)
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()
if "SEGM" in next_header_geom:
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:
num_indicies = envl.read_u32()
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()
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() as unknown:
pass
return model
def _read_tran(tran: Reader) -> ModelTransform:
xform = 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))
return xform
def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
geometry_seg = GeometrySegment()
while segm.could_have_child():
next_header = segm.peak_next_header()
if "MATI" in next_header:
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() 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() 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:
geometry_seg.colors = []
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() 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() 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() 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() as strp:
pass
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:
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:
pass
return geometry_seg
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)

View File

@ -8,9 +8,13 @@ from .msh_material import *
from .msh_writer import Writer from .msh_writer import Writer
from .msh_utilities import * from .msh_utilities import *
<<<<<<< HEAD
from .crc import * from .crc import *
def save_scene(output_file, scene: Scene, is_animated: bool): def save_scene(output_file, scene: Scene, is_animated: bool):
=======
def save_scene(output_file, scene: Scene):
>>>>>>> mshread
""" Saves scene to the supplied file. """ """ Saves scene to the supplied file. """
with Writer(file=output_file, chunk_id="HEDR") as hedr: with Writer(file=output_file, chunk_id="HEDR") as hedr:
@ -52,7 +56,7 @@ def _write_sinf(sinf: Writer, scene: Scene):
with sinf.create_child("FRAM") as fram: with sinf.create_child("FRAM") as fram:
fram.write_i32(0, 1) fram.write_i32(0, 1)
fram.write_f32(29.97) fram.write_f32(29.97003)
with sinf.create_child("BBOX") as bbox: with sinf.create_child("BBOX") as bbox:
aabb = create_scene_aabb(scene) aabb = create_scene_aabb(scene)
@ -298,6 +302,3 @@ def _write_anm2(anm2: Writer, anim: Animation):
for i, xform in enumerate(anim.bone_transforms[boneName]): for i, xform in enumerate(anim.bone_transforms[boneName]):
kfr3.write_u32(i) kfr3.write_u32(i)
kfr3.write_f32(xform.rotation.x, xform.rotation.y, xform.rotation.z, xform.rotation.w) kfr3.write_f32(xform.rotation.x, xform.rotation.y, xform.rotation.z, xform.rotation.w)

View File

@ -0,0 +1,280 @@
""" 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 *
from .crc import *
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 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
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 = {}
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)
verts = []
faces = []
offset = 0
mat_name = ""
full_texcoords = []
weights_offsets = {}
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.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))]
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()
if full_texcoords:
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 = 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)
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...
'''
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)
else:
new_obj = bpy.data.objects.new(model.name, None)
new_obj.empty_display_size = 1
new_obj.empty_display_type = 'PLAIN_AXES'
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)
return model_map
def extract_materials(folder_path: str, 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"]
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
return extracted_materials
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)
model_map = extract_models(scene, matmap)
skel = extract_refined_skeleton(scene)
refined_skeleton_to_armature(skel, model_map)

View File

@ -1,6 +1,7 @@
""" Misc utilities. """ """ Misc utilities. """
from mathutils import Vector from mathutils import Vector
from typing import List
def vec_to_str(vec): def vec_to_str(vec):
@ -36,3 +37,14 @@ def pack_color(color) -> int:
packed |= (int(color[3] * 255.0 + 0.5) << 24) packed |= (int(color[3] * 255.0 + 0.5) << 24)
return packed 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]