From ca686f3e35cbfed2bdef72230883ba08ec38fdc0 Mon Sep 17 00:00:00 2001 From: opparco Date: Mon, 28 Aug 2017 22:38:26 +0900 Subject: [PATCH] Import --- __init__.py | 26 ++++ bin/.gitkeep | 0 hka_export.py | 102 +++++++++++++ hka_export_op.py | 38 +++++ hka_import.py | 255 +++++++++++++++++++++++++++++++ hka_import_op.py | 48 ++++++ io/__init__.py | 0 io/hka.py | 334 +++++++++++++++++++++++++++++++++++++++++ naming.py | 58 +++++++ resources/skeleton.bin | Bin 0 -> 7071 bytes tmp/.gitkeep | 0 11 files changed, 861 insertions(+) create mode 100644 __init__.py create mode 100644 bin/.gitkeep create mode 100644 hka_export.py create mode 100644 hka_export_op.py create mode 100644 hka_import.py create mode 100644 hka_import_op.py create mode 100644 io/__init__.py create mode 100644 io/hka.py create mode 100644 naming.py create mode 100644 resources/skeleton.bin create mode 100644 tmp/.gitkeep diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..107e85b --- /dev/null +++ b/__init__.py @@ -0,0 +1,26 @@ +bl_info = { + "name": "Skyrim hkx format", + "category": "Import-Export"} + +import bpy +from io_anim_hkx import hka_import_op, hka_export_op + +def menu_func_import(self, context): + self.layout.operator(hka_import_op.hkaImportOperator.bl_idname, text="Skyrim hkx (.hkx)") + +def menu_func_export(self, context): + self.layout.operator(hka_export_op.hkaExportOperator.bl_idname, text="Skyrim hkx (.hkx)") + +def register(): + bpy.utils.register_module(__name__) + bpy.types.INFO_MT_file_import.append(menu_func_import) + bpy.types.INFO_MT_file_export.append(menu_func_export) + +def unregister(): + bpy.types.INFO_MT_file_import.remove(menu_func_import) + bpy.types.INFO_MT_file_export.remove(menu_func_export) + bpy.utils.unregister_module(__name__) + +if __name__ == "__main__": + register() + diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/hka_export.py b/hka_export.py new file mode 100644 index 0000000..2fb30d3 --- /dev/null +++ b/hka_export.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +"""anim.bin exporter for blender 2.78 +""" + +import os +import bpy +from math import radians +from mathutils import Euler, Matrix, Quaternion, Vector +# import numpy as np +from io_anim_hkx.io.hka import hkaSkeleton, hkaAnimation, hkaPose, Transform +from io_anim_hkx.naming import get_bone_name_for_blender + + +def export_hkaAnimation(anim, skeleton): + + # + # create bone map + # + # map pose_bone name to bone_idx + + bone_indices = {} + + nbones = len(skeleton.bones) + + for i in range(nbones): + bone = skeleton.bones[i] + # blender naming convention + # io_scene_nifに合わせる + p_bone_name = get_bone_name_for_blender(bone.name) + bone_indices[p_bone_name] = i + + def detect_armature(): + found = None + for ob in bpy.context.selected_objects: + if ob.type == 'ARMATURE': + found = ob + break + return found + + def export_pose(): + arm_ob = detect_armature() + arm_ob.select = True + + anim.numOriginalFrames = 1 + anim.duration = 0.033333 + + del anim.pose[:] + pose = hkaPose() + anim.pose.append(pose) + + pose.time = 0.0 + + for bone in skeleton.bones: + t = bone.local.copy() + pose.transforms.append(t) + + for p_bone in arm_ob.pose.bones: + # bone mapに含まれないnameは無視する + if p_bone.name not in bone_indices: + continue + bone_i = bone_indices[p_bone.name] + + pose_local = p_bone.bone.matrix_local * p_bone.matrix_basis + + if p_bone.bone.parent: + m = p_bone.bone.parent.matrix_local.inverted() * pose_local + else: + m = pose_local + + location, rotation, scale = m.decompose() + + t = pose.transforms[bone_i] + t.translation = location + t.rotation = rotation + t.scale = scale.z + + export_pose() + # export_motion() + + +def export_hkafile(skeleton_file, anim_file): + + skeleton = hkaSkeleton() + skeleton.load(skeleton_file) + + anim = hkaAnimation() + export_hkaAnimation(anim, skeleton) + + anim.save(anim_file) + +if __name__ == "__main__": + from time import time + + start_time = time() + + skeleton_file = os.path.join(os.environ['HOME'], "resources/skeleton.bin") + anim_file = "anim.bin" + export_hkafile(skeleton_file, anim_file) + + end_time = time() + print('bin export time:', end_time - start_time) diff --git a/hka_export_op.py b/hka_export_op.py new file mode 100644 index 0000000..515f7ee --- /dev/null +++ b/hka_export_op.py @@ -0,0 +1,38 @@ + +"""hka export operator +""" + +import os +import subprocess + +import bpy +from bpy_extras.io_utils import ExportHelper +from io_anim_hkx.hka_export import export_hkafile + + +class hkaExportOperator(bpy.types.Operator, ExportHelper): + """Export a hkaAnimationContainer file + """ + bl_idname = "export_anim.hkx" + + bl_label = "Export hkx" + + filename_ext = ".hkx" + filter_glob = bpy.props.StringProperty(default="*.hkx", options={'HIDDEN'}) + + def execute(self, context): + dirname = os.path.dirname(os.path.abspath(__file__)) + + skeleton_file = dirname + "/resources/skeleton.bin" + anim_hkx_file = self.properties.filepath + + basename = os.path.basename(anim_hkx_file) + basename, extension = os.path.splitext(basename) + anim_bin_file = dirname + '/tmp/' + basename + '.bin' + + export_hkafile(skeleton_file, anim_bin_file) + + command = dirname + '/bin/hkconv.exe' + process = subprocess.run([command, '-o', anim_hkx_file, anim_bin_file]) + + return {'FINISHED'} diff --git a/hka_import.py b/hka_import.py new file mode 100644 index 0000000..858277a --- /dev/null +++ b/hka_import.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python + +"""skeleton.bin anim.bin importer for blender 2.78 +""" + +import os +import bpy +from math import radians +from mathutils import Euler, Matrix, Quaternion, Vector +import numpy as np +from io_anim_hkx.io.hka import hkaSkeleton, hkaAnimation +from io_anim_hkx.naming import get_bone_name_for_blender + + +def import_hkaSkeleton(skeleton): + + def detect_armature(): + found = None + for ob in bpy.context.selected_objects: + if ob.type == 'ARMATURE': + found = ob + break + return found + + def import_armature(): + # objectがないと失敗するのでpoll + if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode="OBJECT") + + armature = bpy.data.armatures.new('Armature') + armature.show_axes = True # 座標軸 + arm_ob = bpy.data.objects.new(armature.name, armature) + arm_ob.select = True + arm_ob.show_x_ray = True # 透視 + + scene = bpy.context.scene + scene.objects.link(arm_ob) + scene.objects.active = arm_ob + + world_scale = 0.1 + + bpy.ops.object.mode_set(mode="EDIT") + for bone in skeleton.bones: + b_bone_name = get_bone_name_for_blender(bone.name) + b_bone = armature.edit_bones.new(b_bone_name) + armature.edit_bones.active = b_bone + + b_bone.parent = bone.parent.b_bone if bone.parent else None + + b_bone.tail.y = world_scale + b_bone.transform(bone.world_coordinate().to_matrix()) + + bone.b_bone = b_bone + + return arm_ob + + # 既存のarmatureを使う場合 + armature_object = detect_armature() + + if armature_object is None: + # skeleton.bonesをimportしてarmatureを作成する場合 + armature_object = import_armature() + + +def import_hkaAnimation(anim, skeleton, use_anim=False): + + # + # create bone map + # + # map pose_bone name to bone_idx + + bone_indices = {} + + nbones = len(skeleton.bones) + + # len(p.transforms) for p in anim.pose は全て同値 + # 仮にidx=0をみる + ntransforms = len(anim.pose[0].transforms) + + # ntransformsを超える位置のnameは無視したいのでminで評価 + for i in range(min(ntransforms, nbones)): + bone = skeleton.bones[i] + # blender naming convention + # io_scene_nifに合わせる + p_bone_name = get_bone_name_for_blender(bone.name) + bone_indices[p_bone_name] = i + + def detect_armature(): + found = None + for ob in bpy.context.selected_objects: + if ob.type == 'ARMATURE': + found = ob + break + return found + + def import_pose(): + arm_ob = detect_armature() + arm_ob.select = True + + scene = bpy.context.scene + # scene.objects.link(arm_ob) + scene.objects.active = arm_ob + + pose = anim.pose[0] + + bpy.ops.object.mode_set(mode="POSE") + + for p_bone in arm_ob.pose.bones: + # bone mapに含まれないnameは無視する + if p_bone.name not in bone_indices: + continue + bone_i = bone_indices[p_bone.name] + + t = pose.transforms[bone_i] + + # NOTE: bone.matrix_local + # 4x4 bone matrix relative to armature + + if p_bone.bone.parent: + pose_local = p_bone.bone.parent.matrix_local * t.to_matrix() + else: + pose_local = t.to_matrix() + + p_bone.matrix_basis = p_bone.bone.matrix_local.inverted() * pose_local + + def import_motion(): + + # + # constants + # + + # euler order + order = 'XYZ' + + # action_group name + location_group = 'Location' + angle_group = 'Rotation' + + # + # + # + + arm_ob = detect_armature() + arm_ob.select = True + + scene = bpy.context.scene + # scene.objects.link(arm_ob) + scene.objects.active = arm_ob + + arm_ob.animation_data_create() + action = bpy.data.actions.new(name='Action') + arm_ob.animation_data.action = action + + npose = len(anim.pose) + print("#pose: {0}".format(npose)) + + time = np.zeros(npose, dtype=np.float32) + locations = np.zeros((npose, 3), dtype=np.float32) + angles = np.zeros((npose, 3), dtype=np.float32) + + for i in range(npose): + pose = anim.pose[i] + time[i] = 1.0 + pose.time * 30.0 + + bpy.ops.object.mode_set(mode="POSE") + + for p_bone in arm_ob.pose.bones: + # bone mapに含まれないnameは無視する + if p_bone.name not in bone_indices: + continue + bone_i = bone_indices[p_bone.name] + + p_bone.rotation_mode = order + + angle = Euler((0, 0, 0)) + + for i in range(npose): + pose = anim.pose[i] + + t = pose.transforms[bone_i] + + # NOTE: bone.matrix_local + # 4x4 bone matrix relative to armature + + if p_bone.bone.parent: + pose_local = p_bone.bone.parent.matrix_local * t.to_matrix() + else: + pose_local = t.to_matrix() + + matrix_basis = p_bone.bone.matrix_local.inverted() * pose_local + + location, rotation, scale = matrix_basis.decompose() + + angle = rotation.to_euler(order, angle) + + locations[i] = location + angles[i] = angle + + location_path = 'pose.bones["{0}"].location'.format(p_bone.name) + + for axis_i in range(3): # xyz + curve = action.fcurves.new( + data_path=location_path, index=axis_i, action_group=location_group) + keyframe_points = curve.keyframe_points + curve.keyframe_points.add(npose) + + for i in range(npose): + bez = keyframe_points[i] + bez.co = time[i], locations[i, axis_i] + + angle_path = 'pose.bones["{0}"].rotation_euler'.format(p_bone.name) + + for axis_i in range(3): # xyz + curve = action.fcurves.new( + data_path=angle_path, index=axis_i, action_group=angle_group) + keyframe_points = curve.keyframe_points + curve.keyframe_points.add(npose) + + for i in range(npose): + bez = keyframe_points[i] + bez.co = time[i], angles[i, axis_i] + + for cu in action.fcurves: + for bez in cu.keyframe_points: + bez.interpolation = 'LINEAR' + + if use_anim: + import_motion() + else: + import_pose() + + +def import_hkafile(skeleton_file, anim_file, use_anim=False): + + skeleton = hkaSkeleton() + skeleton.load(skeleton_file) + + import_hkaSkeleton(skeleton) + + anim = hkaAnimation() + anim.load(anim_file) + + import_hkaAnimation(anim, skeleton, use_anim) + +if __name__ == "__main__": + from time import time + + start_time = time() + + skeleton_file = os.path.join(os.environ['HOME'], "resources/skeleton.bin") + anim_file = os.path.join(os.environ['HOME'], "resources/idle.bin") + import_hkafile(skeleton_file, anim_file) + + end_time = time() + print('bin import time:', end_time - start_time) diff --git a/hka_import_op.py b/hka_import_op.py new file mode 100644 index 0000000..9a59e18 --- /dev/null +++ b/hka_import_op.py @@ -0,0 +1,48 @@ + +"""hka import operator +""" + +import os +import subprocess + +import bpy +from bpy.props import BoolProperty +from bpy_extras.io_utils import ImportHelper +from io_anim_hkx.hka_import import import_hkafile + + +class hkaImportOperator(bpy.types.Operator, ImportHelper): + """Import a hkaAnimationContainer file + """ + bl_idname = "import_anim.hkx" + + bl_label = "Import hkx" + + filename_ext = ".hkx" + filter_glob = bpy.props.StringProperty(default="*.hkx", options={'HIDDEN'}) + + use_anim = BoolProperty( + name="Import to Animation", + description="if uncheck then import to Pose", + default=False, + ) + + def execute(self, context): + dirname = os.path.dirname(os.path.abspath(__file__)) + + skeleton_file = dirname + "/resources/skeleton.bin" + anim_hkx_file = self.properties.filepath + + basename = os.path.basename(anim_hkx_file) + basename, extension = os.path.splitext(basename) + anim_bin_file = dirname + '/tmp/' + basename + '.bin' + + command = dirname + '/bin/hkdump-bin.exe' + process = subprocess.run([command, '-o', anim_bin_file, anim_hkx_file]) + + use_anim = self.properties.use_anim + + if process.returncode == 0: + import_hkafile(skeleton_file, anim_bin_file, use_anim) + + return {'FINISHED'} diff --git a/io/__init__.py b/io/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/io/hka.py b/io/hka.py new file mode 100644 index 0000000..014e62b --- /dev/null +++ b/io/hka.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python + +"""skeleton.bin anim.bin format +""" + +from struct import pack, unpack +from mathutils import Matrix, Quaternion, Vector + + +def read_headerstring(file): + bytes = b'' + while True: + c = file.read(1) + if c == b'\x0a': # '\n' + break + bytes += c + str = bytes.decode('utf-8') + return str + + +def read_cstring(file): + bytes = b'' + while True: + c = file.read(1) + if c == b'\x00': + break + bytes += c + str = bytes.decode('utf-8') + return str + + +def read_float(file): + return unpack('Wq>yvpU0KEjLwe%QQ{MVBZ5~6}8J11Rj zv{>xeNl$2dlRnV)CHka%iVP%SWH9*;GK35z zPec1F@(k@egp4Gk$S|TNqsbV$4TWoi>Cq?>L3+WpVbCKU5Nha?41IgRhAsXZP14DD zXfw!5&`uOb_7KDF=~JhuXJR9r=9tSCl~h3|*h`Tt zbzX7F{9>TbE1s`}(oJRaAd+4-Pl;sEv}eszc#TRoqpzD$Ep;>cx*4IOv|6-VZfS|N z+L&gkFvrA_fD=vNiB>wv3WP}~GE6#=VQGOd=|qM}Co(J}5GI|-FzG}~QG&-DYb784^CbrVuGtH(V^-OH( z`Gh5A%nF5OsWVL#a4{>hLb|9n$`K=t_^Go3Zet|5#U zsZs4aono3_QaEAWJkVN&ewN`hEc+XdC8U7#INT6bdO~ z^hL6iPrg&Me}vb_jh~b8urUq`uR$wbTj(y_UI+( z1()Qz$z>0?o6&c-pljHV-fh~#l0{~#Tm-JhEFYzr3QLtk%VPOxylMV?A9jkVPyy#I zwpfegie!{j044^rnGqw;1W1q+#k6I__|qmuapV~>zKluOSjrF^#1KmvVuKiBDMM@! zgN8C_f*3TEK@-HFp$wWJ1}$aK1~F(UgEojkOBu953_8l73u4ew23-(?jxy+E1}L*H zlSW@AeNkY5sLbGt0s};Y7%-E@3?-8a7BdOaAO_4NM1vSGlg11slL`Z75~4v2m`R8R zF<>T*8A>J<2FxTxgBUQA5Dj9$Od2zkOezeRNr(n9U?w374C$tFv(==v!s-NvS|y-P zw^USHEoJl-KzjW1c0$CRgz(Hr&24Q6;hTSaC12a!y_~_AeGrq6POD5qDqL;vRm9gqWS9G`Cw-WM$O-SMpmC4`cUj2-?2yQ}HL} zOcEXgKEbthn=7($IWzHBYh7(?C%b=;$9qramvEwejp-L@n~$#XI0mfHlM5M1iT&@V zG*m5TRB}G?J`xt&2xq@dm$~Yz&k9YkPYBm`H>O;vkMNY*rZW#IuTlf5hYi$I{K2-= z;h<+h|FtfD*<^j!V|w@lgs2aop3Z%143{HU=tI6uX`fqj|KJADBk7RFEa~Cn2#Ik< zf}S^yKg-MpJzIBeI#^I2VQ37{qv#in3%I%$w+p8xU1u>r@8;>4&9I%TsMWt-UaQCa z;C73SgWdu3kc6gP+1!6`ED^uR)#}4YrA|dwXrJ>gXTl&ap|JJv<%b845wZvIyF%Gc zqS4!{6CF)^zp@WJwA_~;Uww@Wyg_<|_dw4H(6g|-*8LOcP&TTDzvsVJ%1$gAfP4P71>p_q68w01dI$6|nFnM5#r(ZM?yL!{<;TAPp|}XbI*3?4^ec!D4L`^NgXgpR6ogyj}SxRR0a2J z&wc2}BR}dw^!<*(34htMLoC>3WeF*Do^5DxnZ6m{I^&br0~I&@{`B#S$@^j5aftVO zapY!h(%#oL>!UqiYE&1~S{o)Py#D#Zi_hqYdW|db(_xUGw8T-%@46d1MElZJ&mU+Q zxy+Xv#m^VJ&I{4=Tx{94ce%<<^F6)(m)DOxJw-S*Xsw}kUJsgg)bGnbcn5}&5MnRj zau#hqWdC}bq3b}KVM#}`{`i79g5(=s_9>2-hc(LXAbT8~>6hVYs;=1{SQA=9$@!Mm z7nV@(qZVnMAml>Vc?R_qfSxy*V#BNxy#CqKQi!GW{+<@tdBTzElC{m zW?S}|Wq^@#snmDhf@XK}>JU!cSk5+${heXz`5%%jPYg-Ub?r^*OF4NuW*BZ?r9@jPG zR_Y7-oa&Ei58RYL3b;qZhPC}Ue&mGbsnFfsZNOv2KT1BuSC0$qh`%u}-ALi` zK3{)m#H2R&oIc~2W#4Z1oPBqUJWjtJe#UWzJo7h(1^k-N*QX`E)aIVsXPhwY+wF$y zfJa_sgZq)F-dPFDx4h35y!S2(Zy!mG4K1lR`%Ki9*N%$A^#SXP&kW^xp$3hOajV2P z@Acw3H`RMpq@YDb#Mls02hV+J9!lD!d=^07Rsy~j@E=~P=R!#oybq{b!bq79kNy1f zoo{L0n#l1Rr-^*4(V!wZF(4D)IbbiXJ6K=;e0;i%?^xjqU-vwB_v@2PYUoDK8;yVM zICL+(!}?w|)sI*upM(rZ_W*Q+RauJF#LO+C_Xg~bVBq^9*Q>_f?ZEqX`s5UR!$Xg_{&7tw%(#Epb>+H_372!K)R&M~ z+6l2GtgKwe&i{0Wx4rqM>+VIJ(DA`d*Nux39=ZK^0`u1fV;~7<8+#mD6u$c4FwlMt z_M^k%C_ida@GR9&=(;vDb1fIbKLyWVu{DJBqt9RTzfx!5B!9YkRcu=26^mE=##{SM zjnK?1P$+zx<<2uB+DxF*EK8H+$z8^I*dI!;kM_=T9m*##6A}r*BXq&_y>1yz*zFe$#UOeVm z|6~Nya*pwN&A)!)^NWwKe0nOYneOynzY%2^TJyfX#gXSq-~VfhM=f{{NDus)&BP|o5&07h3tB7+^%v%;-F{qHx#T-`zP8?5bhO;_k4tuUde;mCdtrf~5_0zV@j>!qwGb(co4=x00l`SP^&+zRmX$g<(7mAi`c z=x5ZU%wfBKUdF(r_->=jRQrgOdF7OVJ~Cwh654=Ygvlxa+r8 z8;ZcsJHXEu`&xaqQtFFZz@rZI^MB2(=k$`F;TuXvYk%;ww5~m(hx+*x_&MLJ;!cB~ z;oSl9c~y&kR>rE->!UDMIAv=ZTV^ef7mUOXH>U%TWM+9`#)j z_cr($I5jF#+433q8F5MtB)qhKis0v_0b$%&>Sv;bzsXxuWj|vd$)kj{C7Qr*H}(+n zJ$!G0=d4J-7uiDQ`o9;U_JH{!-^RZ){ml2G2tVxqJwi-zX@;rDob3O)&OIJVdpwl& oc*qE$=_OUk@&|e8g#PbQ;Ft6)`X$|e@&EYDE?tyA=%e_50bWc%qyPW_ literal 0 HcmV?d00001 diff --git a/tmp/.gitkeep b/tmp/.gitkeep new file mode 100644 index 0000000..e69de29