zaabin import
This commit is contained in:
parent
79543a1cd7
commit
e67e675ee7
|
@ -62,6 +62,7 @@ from .msh_scene_read import read_scene
|
||||||
from .msh_material_properties import *
|
from .msh_material_properties import *
|
||||||
from .msh_skeleton_properties import *
|
from .msh_skeleton_properties import *
|
||||||
from .msh_to_blend import *
|
from .msh_to_blend import *
|
||||||
|
from .zaa_to_blend import *
|
||||||
|
|
||||||
|
|
||||||
class ExportMSH(Operator, ExportHelper):
|
class ExportMSH(Operator, ExportHelper):
|
||||||
|
@ -152,7 +153,7 @@ class ImportMSH(Operator, ImportHelper):
|
||||||
)
|
)
|
||||||
|
|
||||||
filter_glob: StringProperty(
|
filter_glob: StringProperty(
|
||||||
default="*.msh",
|
default="*.msh;*.zaa;*.zaabin",
|
||||||
options={'HIDDEN'},
|
options={'HIDDEN'},
|
||||||
maxlen=255, # Max internal buffer length, longer would be clamped.
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
||||||
)
|
)
|
||||||
|
@ -168,13 +169,16 @@ class ImportMSH(Operator, ImportHelper):
|
||||||
dirname = os.path.dirname(self.filepath)
|
dirname = os.path.dirname(self.filepath)
|
||||||
for file in self.files:
|
for file in self.files:
|
||||||
filepath = os.path.join(dirname, file.name)
|
filepath = os.path.join(dirname, file.name)
|
||||||
with open(filepath, 'rb') as input_file:
|
if filepath.endswith(".zaabin") or filepath.endswith(".zaa"):
|
||||||
scene = read_scene(input_file, self.animation_only)
|
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:
|
if not self.animation_only:
|
||||||
extract_scene(filepath, scene)
|
extract_scene(filepath, scene)
|
||||||
else:
|
else:
|
||||||
extract_and_apply_anim(filepath, scene)
|
extract_and_apply_anim(filepath, scene)
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue