diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c23952d --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +.idea/ +cmake-*/ +build/ +__pycache__/ +release/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e9c42e5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.10) +project(animassist) + +message(STATUS "havok sdk root local=${HAVOK_SDK_ROOT}") +message(STATUS "havok sdk root env=$ENV{HAVOK_SDK_ROOT}") + +set( + PROJ_LIBRARIES + hkCompat.lib + hkBase.lib + hkSerialize.lib + hkSceneData.lib + hkVisualize.lib + hkInternal.lib + hkImageUtilities.lib + hkaAnimation.lib + hkaInternal.lib + hkaPhysics2012Bridge.lib + hkcdCollide.lib + hkcdInternal.lib + hkGeometryUtilities.lib +) + +include_directories( + "$ENV{HAVOK_SDK_ROOT}/Source/" +) + +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + link_directories("$ENV{HAVOK_SDK_ROOT}/Lib/win32_vs2012_win7_noSimd/debug/") +else() + link_directories("$ENV{HAVOK_SDK_ROOT}/Lib/win32_vs2012_win7_noSimd/release/") +endif() + +add_definitions(-DUNICODE -D_UNICODE) +add_executable(animassist main.cpp) +target_link_libraries(animassist ${PROJ_LIBRARIES}) + +set(CMAKE_CXX_STANDARD 14) \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8cffccc --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. \ No newline at end of file diff --git a/MultiAssist.py b/MultiAssist.py new file mode 100644 index 0000000..5fb4dfb --- /dev/null +++ b/MultiAssist.py @@ -0,0 +1,883 @@ +from sqlite3 import DateFromTicks +from unicodedata import name +from bs4 import BeautifulSoup +import argparse +import struct +import tempfile +import subprocess +import os +import sys +import dearpygui.dearpygui as dpg +import warnings +import re +warnings.filterwarnings("ignore", category=UserWarning, module='bs4') + +gui = 0 + +debug = False +def dbgprint(text: str) -> None: + if debug: + print(f"[dbg] {text}") + + +def define_parser(): + parser = argparse.ArgumentParser(description="Utilities for the manual animation modding workflow in XIV.", + formatter_class=argparse.RawDescriptionHelpFormatter) + + subparsers = parser.add_subparsers(title="Requires (one of)", dest="command") + + extract_parser = subparsers.add_parser("extract", help="Extract a editable animation file, using a pap and sklb file from XIV.") + extract_parser.add_argument("-s", "--skeleton-file", type=str, help="The input skeleton sklb file.", required=True) + extract_parser.add_argument("-p", "--pap-file", type=str, help="The input animation pap file.", required=True) + extract_parser.add_argument("-i", "--anim-index", type=str, help="The index of the animation you are extracting.", required=True) + extract_parser.add_argument("-o", "--out-file", type=str, help="The output file.", required=True) + extract_parser.add_argument("-t", "--file-type", type=str, help="File type to export.", required=True) + + pack_parser = subparsers.add_parser("pack", help="Repack an existing pap file with a new animation. Requires the input skeleton.") + pack_parser.add_argument("-s", "--skeleton-file", type=str, help="The input skeleton sklb file.", required=True) + pack_parser.add_argument("-p", "--pap-file", type=str, help="The input pap file.", required=True) + pack_parser.add_argument("-a", "--anim-file", type=str, help="The modified animation Havok XML packfile.", required=True) + pack_parser.add_argument("-i", "--anim-index", type=str, help="The index of the animation you are replacing.", required=True) + pack_parser.add_argument("-o", "--out-file", type=str, help="The output pap file.", required=True) + + aextract_parser = subparsers.add_parser("aextract", help="Extract a Havok binary packfile for editing in 3DS Max, using a pap and sklb file from XIV.") + aextract_parser.add_argument("-s", "--skeleton-file", type=argparse.FileType("rb"), help="The input skeleton sklb file.", required=True) + aextract_parser.add_argument("-p", "--pap-file", type=argparse.FileType("rb"), help="The input animation pap file.", required=True) + aextract_parser.add_argument("-o", "--out-file", type=str, help="The output Havok binary packfile.", required=True) + + apack_parser = subparsers.add_parser("apack", help="Repack an existing pap file with a new animation. Requires the input skeleton.") + apack_parser.add_argument("-s", "--skeleton-file", type=argparse.FileType("rb"), help="The input skeleton sklb file.", required=True) + apack_parser.add_argument("-p", "--pap-file", type=argparse.FileType("rb"), help="The input pap file.", required=True) + apack_parser.add_argument("-a", "--anim-file", type=argparse.FileType("rb"), help="The modified animation Havok XML packfile.", required=True) + apack_parser.add_argument("-o", "--out-file", type=str, help="The output pap file.", required=True) + + import textwrap + parser.epilog = textwrap.dedent( + f"""\ + commands usage: + {extract_parser.format_usage()} + {pack_parser.format_usage()} + {aextract_parser.format_usage()} + {apack_parser.format_usage()} + Remember that each command also has its own, more descriptive, -h/--help arg, describing what file types are expected. + """ + ) + return parser + +def animassist_check(): + if not os.path.exists("animassist.exe"): + print("Animassist.exe is missing :(") + if(gui!=0): + gui.show_info("Error!","Animassist.exe is missing") + sys.exit(1) + +def to_havok_check(): + if not os.path.exists("fbx2havok.exe"): + print("fbx2havok.exe is missing :(") + if(gui!=0): + gui.show_info("Error!","fbx2havok.exe is missing") + sys.exit(1) + +def to_fbx_check(): + if not os.path.exists("tofbx.exe"): + print("tofbx.exe is missing :(") + if(gui!=0): + gui.show_info("Error!","tofbx.exe is missing") + sys.exit(1) + +def assist_skl_tag(skeleton_path, out_path) -> None: + animassist_check() + + complete = subprocess.run(["animassist.exe", "1", skeleton_path, out_path], capture_output=True, encoding="utf8") + dbgprint(f"{complete.returncode}") + dbgprint(f"{complete.stdout}") + if not os.path.exists(out_path): + print("skeleton binary tag file operation failed") + else: + dbgprint(f"Saved skeleton xml to {out_path}") + +def assist_skl_anim_pack(skeleton_path, out_path) -> None: + animassist_check() + + complete = subprocess.run(["animassist.exe", "2", skeleton_path, out_path], capture_output=True, encoding="utf8",text=True) + dbgprint(f"{complete.returncode}") + dbgprint(f"{complete.stdout}") + if not os.path.exists(out_path): + print("skeleton xml packfile assist failed") + else: + dbgprint(f"Saved packfile to {out_path}") + +def assist_combine(skeleton_path, animation_path, animation_index, out_path) -> None: + animassist_check() + + complete = subprocess.run(["animassist.exe", "3", skeleton_path, animation_path, animation_index, out_path], capture_output=True, encoding="utf8") + dbgprint(f"{complete.returncode}") + print(f"{complete.stdout}") + + if not os.path.exists(out_path): + print("binary packfile assist failed") + else: + dbgprint(f"Saved importable file to {out_path}") + +def assist_xml(skl_path, animation_path, out_path) -> None: + animassist_check() + + complete = subprocess.run(["animassist.exe", "4", skl_path, animation_path, out_path], capture_output=True, encoding="utf8") + dbgprint(f"{complete.returncode}") + dbgprint(f"{complete.stdout}") + + if not os.path.exists(out_path): + print("xml packfile assist failed") + else: + dbgprint(f"Saved importable file to {out_path}") + +def assist_tag(xml_path, out_path) -> None: + animassist_check() + + complete = subprocess.run(["animassist.exe", "5", xml_path, out_path], capture_output=True, encoding="utf8") + dbgprint(f"{complete.returncode}") + print(f"{complete.stdout}") + + if not os.path.exists(out_path): + print("xml tagfile assist failed") + else: + dbgprint(f"Saved importable file to {out_path}") + return + +def assist_combine_tag(skeleton_path, animation_path, animation_index, out_path): + animassist_check() + + complete = subprocess.run(["animassist.exe", "6", skeleton_path, animation_path, animation_index, out_path], capture_output=True, encoding="utf8") + print(f"{complete.returncode}") + print(f"{complete.stdout}") + + if not os.path.exists(out_path): + print("binary tagfile assist failed") + else: + dbgprint(f"Saved importable file to {out_path}") + +def to_hkx(skeleton_path, hkx_path, fbx_path, out_path) -> None: + to_havok_check() + complete = subprocess.run("fbx2havok.exe -hk_skeleton " + skeleton_path + " -hk_anim " + hkx_path + " -fbx " + fbx_path + " -hkout " + out_path, capture_output=True, encoding="utf8") + dbgprint(f"{complete.stdout}") + if not os.path.exists(out_path): + print("fbx2havok operation failed.") + else: + dbgprint(f"Saved importable file to {out_path}") + +def to_fbx(skeleton_path, hkx_path, out_path): + #to_fbx_check() + complete = subprocess.run("tofbx.exe -hk_skeleton " + skeleton_path + " -hk_anim " + hkx_path + " -fbx " + out_path, capture_output=True, encoding="utf8") + print(f"{complete.stdout}") + print(f"{complete.returncode}") + if not os.path.exists(out_path): + print("havok2fbx operation failed.") + else: + print(f"Saved importable file to {out_path}") + +def extract(skeleton_file, anim_file, out_path): + sklb_data = skeleton_file.read() + pap_data = anim_file.read() + sklb_hdr = read_sklb_header(sklb_data) + pap_hdr = read_pap_header(pap_data) + + print(f"The input skeleton is for ID {sklb_hdr['skele_id']}.") + print(f"The input animation is for ID {pap_hdr['skele_id']}.") + print(f"If these mismatch, things will go very badly.") + + + num_anims = len(pap_hdr["anim_infos"]) + if num_anims > 1: + print("Please choose which one number to use and press enter:") + for i in range(num_anims): + print(f"{i + 1}: {pap_hdr['anim_infos'][i]['name']}") + print(f"{num_anims + 1}: Quit") + while True: + try: + choice = int(input()) + except ValueError: + continue + if choice and choice > -1 and choice <= num_anims + 1: + break + if choice >= num_anims + 1: + sys.exit(1) + havok_anim_index = pap_hdr["anim_infos"][choice]["havok_index"] + else: + havok_anim_index = 0 + + with tempfile.TemporaryDirectory() as tmp_folder: + tmp_skel_path = os.path.join(tmp_folder, "tmp_skel") + tmp_anim_path = os.path.join(tmp_folder, "tmp_anim") + dbgprint(f"we have {tmp_skel_path} as tmp_skel") + dbgprint(f"we have {tmp_anim_path} as tmp_anim") + havok_skel = get_havok_from_sklb(sklb_data) + havok_anim = get_havok_from_pap(pap_data) + with open(tmp_skel_path, "wb") as tmp_skel: + tmp_skel.write(havok_skel) + with open(tmp_anim_path, "wb") as tmp_anim: + tmp_anim.write(havok_anim) + assist_combine(tmp_skel_path, tmp_anim_path, str(havok_anim_index), out_path) + +def export(skeleton_path, pap_path, anim_index, output_path, file_type): + + with open(pap_path, "rb") as p: + pap_data = p.read() + with open(skeleton_path, "rb") as s: + sklb_data = s.read() + + sklb_hdr = read_sklb_header(sklb_data) + pap_hdr = read_pap_header(pap_data) + + print(f"The input skeleton is for ID {sklb_hdr['skele_id']}.") + print(f"The input animation is for ID {pap_hdr['skele_id']}.") + print(f"If these mismatch, things will go very badly.") + + num_anims = len(pap_hdr["anim_infos"]) + if num_anims > 1: + anim_index = pap_hdr["anim_infos"][int(anim_index)]["havok_index"] # Just to be safe, unsure if this index will ever mismatch + else: + anim_index = 0 + + with tempfile.TemporaryDirectory() as tmp_folder: + tmp_skel_path = os.path.join(tmp_folder, "tmp_skel") + tmp_anim_path = os.path.join(tmp_folder, "tmp_anim") + tmp_skel_xml_path = os.path.join(tmp_folder, "tmp_skel_xml") + tmp_anim_bin_path = os.path.join(tmp_folder, "tmp_anim_bin") + dbgprint(f"we have {tmp_skel_path} as tmp_skel") + dbgprint(f"we have {tmp_anim_path} as tmp_anim") + havok_skel = get_havok_from_sklb(sklb_data) + havok_anim = get_havok_from_pap(pap_data) + with open(tmp_skel_path, "wb") as tmp_skel: + tmp_skel.write(havok_skel) + with open(tmp_anim_path, "wb") as tmp_anim: + tmp_anim.write(havok_anim) + assist_skl_tag(tmp_skel_path, tmp_skel_xml_path) + #assist_combine(tmp_skel_path, tmp_anim_path, str(anim_index), tmp_anim_bin_path) + if (file_type=="fbx"): + assist_combine_tag(tmp_skel_path, tmp_anim_path, str(anim_index), tmp_anim_bin_path) + to_fbx_check() + complete = subprocess.call("tofbx.exe -hk_skeleton " + tmp_skel_path + " -hk_anim " + tmp_anim_bin_path + " -fbx " + output_path) + elif (file_type=="hkxp"): + assist_combine(tmp_skel_path, tmp_anim_path, str(anim_index), output_path) + elif (file_type=="hkxt"): + assist_combine_tag(tmp_skel_path, tmp_anim_path, str(anim_index), output_path) + elif (file_type=="xml"): + assist_xml(tmp_skel_path, tmp_anim_path, output_path) + if not os.path.exists(output_path): + dbgprint("no file was written") + if(gui!=0): + gui.show_info("Error!","No file was written, something went wrong.") + else: + dbgprint(f"Saved to {output_path}") + if(gui!=0): + gui.show_info("Success!","You can find your exported file in " + output_path) + +def xml_snipper(): + # Standalone way to snip XML + return + +def repack(skeleton_file, anim_file, mod_file, out_path): + orig_sklb_data = skeleton_file.read() + orig_pap_data = anim_file.read() + + sklb_hdr = read_sklb_header(orig_sklb_data) + pap_hdr = read_pap_header(orig_pap_data) + + print(f"The input skeleton is for ID {sklb_hdr['skele_id']}.") + print(f"The input animation is for ID {pap_hdr['skele_id']}.") + print(f"If these mismatch, things will go very badly.") + with tempfile.TemporaryDirectory() as tmp_folder: + skel_bin_path = os.path.join(tmp_folder, "tmp_skel_bin") + skel_xml_path = os.path.join(tmp_folder, "tmp_skel_xml") + tmp_mod_xml_path = os.path.join(tmp_folder, "tmp_mod_xml") + tmp_mod_bin_path = os.path.join(tmp_folder, "tmp_mod_bin") + + with open(skel_bin_path, "wb") as sklbin: + sklbin.write(get_havok_from_sklb(orig_sklb_data)) + assist_skl_tag(skel_bin_path, skel_xml_path) + + with open(skel_xml_path, "r", encoding="utf8") as sklxml: + skl_xml_str = sklxml.read() + mod_xml_str = mod_file.read() + + new_xml = get_remapped_xml(skl_xml_str, mod_xml_str) + with open(tmp_mod_xml_path, "w") as tmpxml: + tmpxml.write(new_xml) + + assist_skl_anim_pack(tmp_mod_xml_path, tmp_mod_bin_path) + + with open(tmp_mod_bin_path, "rb") as modbin: + new_havok = modbin.read() + + pre_havok = bytearray(orig_pap_data[:pap_hdr["havok_offset"]]) + new_timeline_offset = len(pre_havok) + len(new_havok) + offs_bytes = new_timeline_offset.to_bytes(4, "little") + for i in range(4): + pre_havok[22 + i] = offs_bytes[i] + + post_havok = orig_pap_data[pap_hdr["timeline_offset"]:] + + with open(out_path, "wb") as out: + out.write(pre_havok) + out.write(new_havok) + out.write(post_havok) + print(f"Wrote new pap to {out_path}!") + +def multi_repack(skeleton_file : str, anim_file : str, mod_file : str, anim_index, out_path : str): + with open(skeleton_file, "rb") as s: + orig_sklb_data = s.read() + with open(anim_file, "rb") as a: + orig_pap_data = a.read() + with open(mod_file, "rb") as m: + mod_data = m.read() + + + sklb_hdr = read_sklb_header(orig_sklb_data) + pap_hdr = read_pap_header(orig_pap_data) + print(f"The input skeleton is for ID {sklb_hdr['skele_id']}.") + print(f"The input animation is for ID {pap_hdr['skele_id']}.") + print(f"If these mismatch, things will go very badly.") + with tempfile.TemporaryDirectory() as tmp_folder: + tmp_fbx_path = os.path.join(tmp_folder, "tmp_fbx_bin") #.fbx + tmp_skel_path = os.path.join(tmp_folder, "tmp_skel_bin") #.hkx + tmp_pap_path = os.path.join(tmp_folder, "tmp_pap_bin") # .hkx + tmp_mod_path = os.path.join(tmp_folder, "tmp_mod_bin") # .hkx + tmp_skl_xml_path = os.path.join(tmp_folder, "tmp_skl_xml") + tmp_pap_xml_path = os.path.join(tmp_folder, "tmp_pap_xml") # .xml + tmp_mod_xml_path = os.path.join(tmp_folder, "tmp_mod_xml") # .xml + tmp_out_xml_path = os.path.join(tmp_folder, "tmp_out_xml") # .xml + tmp_out_bin_path = os.path.join(tmp_folder, "tmp_out_bin") # .xml + + havok_skel = get_havok_from_sklb(orig_sklb_data) + havok_pap = get_havok_from_pap(orig_pap_data) + + with open(tmp_fbx_path, "wb") as tmp_fbx: + tmp_fbx.write(mod_data) + with open(tmp_skel_path, "wb") as tmp_skel: + tmp_skel.write(havok_skel) + with open(tmp_pap_path, "wb") as tmp_pap: + tmp_pap.write(havok_pap) + + if(mod_file.endswith(".fbx")): + to_hkx(tmp_skel_path, tmp_pap_path, tmp_fbx_path, tmp_mod_path) + assist_xml(tmp_skel_path, tmp_mod_path, tmp_mod_xml_path) + else: + with open (mod_file, "rb") as mf: + mod_xml_str = mf.read() + assist_skl_tag(tmp_skel_path, tmp_skl_xml_path) + with open(tmp_skl_xml_path, "r", encoding="utf8") as sklxml: + skl_xml_str = sklxml.read() + new_xml = get_remapped_xml(skl_xml_str, mod_xml_str) + with open(tmp_mod_xml_path, "w") as tmpxml: + tmpxml.write(new_xml) + + assist_xml(tmp_skel_path, tmp_pap_path, tmp_pap_xml_path) + with open(tmp_pap_xml_path, "r", encoding="utf8") as pxml: + pap_xml_str = pxml.read() + + if(mod_file.endswith(".fbx")): + with open(tmp_mod_xml_path, "r", encoding="utf8") as mxml: + mod_xml_str = mxml.read() + else: + mod_xml_str = new_xml + merged_xml = merge_xml(pap_xml_str, mod_xml_str, int(anim_index)) + + with open(out_path+"M.xml", 'w') as om: + om.write(merged_xml) + + with open(out_path+"MX.xml", 'w') as ox: + ox.write(mod_xml_str) + + with open(out_path+"PX.xml", 'w') as op: + op.write(pap_xml_str) + + with open(tmp_out_xml_path, "w") as fd: + fd.write(merged_xml) + + assist_tag(tmp_out_xml_path, tmp_out_bin_path) + + with open(tmp_out_bin_path, "rb") as modbin: + new_havok = modbin.read() + + pre_havok = bytearray(orig_pap_data[:pap_hdr["havok_offset"]]) + print(len(pre_havok) + len(new_havok)) + new_timeline_offset = len(pre_havok) + len(new_havok) + + offs_bytes = new_timeline_offset.to_bytes(4, "little") + + for i in range(4): + pre_havok[22 + i] = offs_bytes[i] + + post_havok = orig_pap_data[pap_hdr["timeline_offset"]:] + + if os.path.getsize(tmp_out_bin_path) != 0: + with open(out_path, "wb") as out: + out.write(pre_havok) + out.write(new_havok) + out.write(post_havok) + print(f"Wrote new pap to {out_path}!") + if not os.path.exists(out_path): + dbgprint("No file was written") + if(gui!=0): + gui.show_info("Error!","No file was written, something went wrong.") + else: + dbgprint(f"Saved to {out_path}") + if(gui!=0): + gui.show_info("Success!","You can find your exported file in " + out_path) + +def get_remapped_xml(skl_xml: str, skl_anim_xml: str) -> list: + sk_soup = BeautifulSoup(skl_xml, features="html.parser") + anim_soup = BeautifulSoup(skl_anim_xml, features="html.parser") + + sk_bones = sk_soup.find("hkparam", {"name": "bones"}).find_all("hkparam", {"name": "name"}) + base_bonemap = list(map(lambda x: x.text, sk_bones)) + + anim_bones = anim_soup.find("hkparam", {"name": "annotationTracks"}).find_all("hkparam", {"name": "trackName"}) + anim_bonemap = list(map(lambda x: x.text, anim_bones)) + + new_bonemap = [] + for i in range(len(anim_bonemap)): + search_bone = anim_bonemap[i] + for i, bone in enumerate(base_bonemap): + if bone == search_bone: + new_bonemap.append(i) + break + bonemap_str = ' '.join(str(x) for x in new_bonemap) + dbgprint(f"New bonemap is: {bonemap_str}") + + tt_element = anim_soup.find("hkparam", {"name": "transformTrackToBoneIndices"}) + return str(skl_anim_xml, encoding="utf8").replace(tt_element.text, bonemap_str) + +def merge_xml(pap_xml: str, mod_xml: str, anim_index: int) -> str: + # I'll probably move this to animassist.exe in the future + # For the moment, handled correctly, this absolutely localize change within the havok data to the animation/binding you've swapped. + # Other than the Havok version data, this should be functionally identical to the original .pap, except for the modded area, of course. + # It's also just the first method I thought of when trying to multi-pack. + orig_soup = BeautifulSoup(pap_xml, features="html.parser") + mod_soup = BeautifulSoup(mod_xml, features="html.parser") + anims = [] + bindings = [] + m_anims = [] + m_bindings = [] + + # Will probably clean this up later + # Not handling the whitespace characters results in the joining of two elements in very long .pap files, making them untargetable and offsetting the higher elements. + for a in re.sub(r"\s+", " ", orig_soup.find("hkobject", {"class": "hkaAnimationContainer"}).find("hkparam", {"name": "animations"}).text).strip().split(" "): + anims.append(a) + for a in re.sub(r"\s+", " ", orig_soup.find("hkobject", {"class": "hkaAnimationContainer"}).find("hkparam", {"name": "bindings"}).text).strip().split(" "): + bindings.append(a) + print(anims) + print(anims[anim_index]) + + # The modded animation should really only have one anim/binding at the moment. I'll probably keep this as is to facilitate in-app animation swapping in the future. + # The hkobject renaming will have to be somewhat different when such is implemented, however. + for a in re.sub(r"\s+", " ", mod_soup.find("hkobject", {"class": "hkaAnimationContainer"}).find("hkparam", {"name": "animations"}).text).strip().split(" "): + m_anims.append(a) + for a in re.sub(r"\s+", " ", mod_soup.find("hkobject", {"class": "hkaAnimationContainer"}).find("hkparam", {"name": "bindings"}).text).strip().split(" "): + m_bindings.append(a) + + # This operation must be done before merging the modded data, otherwise it will create duplicate animation/bindings ids that will corrupt the output. + m_ra = mod_soup.find("hkobject", {"name": m_anims[0]}) + m_rb = mod_soup.find("hkobject", {"name": m_bindings[0]}) + + m_ra["name"] = anims[anim_index] + m_rb["name"] = bindings[anim_index] + m_rb.find("hkparam", {"name":"animation"}).string.replaceWith(anims[anim_index]) + + # Replace the animations[index] and bindings[index] objects with the fromatted animations from the modded XML. + orig_soup.find("hkobject", {"name": anims[anim_index]}).replace_with(m_ra) + orig_soup.find("hkobject", {"name": bindings[anim_index]}).replace_with(m_rb) + return str(orig_soup) + +SKLB_HDR_1 = ['magic', 'version', 'offset1', 'havok_offset', 'skele_id', 'other_ids'] +def read_sklb_header_1(sklb_data) -> dict: + # 4 byte magic + # 4 byte version + # 2 byte offset to ? + # 2 byte offset to havok + # 4 byte skeleton id + # 4 x 4 byte other skeleton ids + + hdr = { k: v for k, v in zip(SKLB_HDR_1, struct.unpack('<4sIHHI4I', sklb_data[0:32])) } + for key in hdr.keys(): + dbgprint(f"{key} : {hdr[key]}") + return hdr + +SKLB_HDR_2 = ['magic', 'version', 'offset1', 'havok_offset', 'unknown1', 'skele_id', 'other_ids'] +def read_sklb_header_2(sklb_data) -> dict: + # 4 byte magic + # 4 byte version + # 4 byte offset to ? + # 4 byte offset to havok + # 4 byte ? + # 4 byte skeleton id + # 4 x 4 byte other skeleton ids + + hdr = { k: v for k, v in zip(SKLB_HDR_2, struct.unpack('<4sIIIII4I', sklb_data[0:40])) } + for key in hdr.keys(): + dbgprint(f"{key} : {hdr[key]}") + return hdr + +def read_sklb_header(sklb_data) -> dict: + magic, ver = struct.unpack(" dict: + # 4 byte magic 4 + # uint version 8 + # ushort info num 10 + # uint skele id 14 + # uint offset to info 18 + # uint offset to havok container 22 + # uint offset to timeline 26 + + hdr = { k: v for k, v in zip(PAP_HDR, struct.unpack('<4sIHIIII', pap_data[0:26])) } + + hdr["anim_infos"] = [] + for i in range(hdr["anim_count"]): + start = 26 + 40 * i + end = 26 + 40 * (i + 1) + anim_info = { k: v for k, v in zip(ANIM_INFO, struct.unpack('<32sHHI', pap_data[start:end])) } + anim_info["name"] = str(anim_info["name"], encoding="utf8").split("\0")[0] + hdr["anim_infos"].append(anim_info) + + for key in hdr.keys(): + dbgprint(f"{key} : {hdr[key]}") + + return hdr + +def get_havok_from_pap(pap_data): + hdr = read_pap_header(pap_data) + dbgprint(f"This pap file is for the skeleton c{hdr['skele_id']}. It has {hdr['anim_count']} animation(s).") + + havok_start = hdr['havok_offset'] + havok_end = hdr['timeline_offset'] + new_data = pap_data[havok_start:havok_end] + + return new_data + + +class GUI: + def __init__(self) -> None: + anims=[] + reanims=[] + + def start_loading(self): + self._working_window() + return + + def stop_loading(self): + return + + def show_info(self, title, message): + + # guarantee these commands happen in the same frame + with dpg.mutex(): + + viewport_width = dpg.get_viewport_client_width() + viewport_height = dpg.get_viewport_client_height() + + with dpg.window(label=title, modal=True, no_close=True) as modal_id: + dpg.add_text(message) + dpg.add_button(label="Ok", width=75, user_data=(modal_id, True), callback=lambda:dpg.delete_item(modal_id)) + + # guarantee these commands happen in another frame + dpg.split_frame() + width = dpg.get_item_width(modal_id) + height = dpg.get_item_height(modal_id) + dpg.set_item_pos(modal_id, [viewport_width // 2 - width // 2, viewport_height // 2 - height // 2]) + + def _file_handler(self, sender, app_data, user_data): + + #dpg.set_value(user_data+"_status", "Selected: ") + + dpg.set_value(user_data, app_data['file_path_name']) + return + + def _populate_anims(self, sender, app_data, user_data): + if dpg.get_value(user_data['pap_input']) == "" or dpg.get_value(user_data['sklb_input']) == "": + self.show_info("Error", "Please make sure you have selected both a .pap and .sklb file.") + return + with open(dpg.get_value(user_data['pap_input']), "rb") as p: + pap_data = p.read() + with open(dpg.get_value(user_data['sklb_input']), "rb") as s: + sklb_data = s.read() + + pap_hdr = read_pap_header(pap_data) + sklb_hdr = read_sklb_header(sklb_data) + num_anims = len(pap_hdr["anim_infos"]) + + if(sklb_hdr['skele_id']!=pap_hdr['skele_id']): + gui.show_info("Warning!", "The Skeleton ID ("+str(sklb_hdr['skele_id'])+") does not match the Skeleton ID in the .pap ("+ str(pap_hdr['skele_id']) +")\n\nAre you sure you are importing the right files?") + + a = [] + for i in range(num_anims): + a.append(pap_hdr['anim_infos'][i]['name']) + dpg.configure_item(user_data['output'], default_value=a[0]) + dpg.configure_item(user_data['output'], items=a) + if(user_data['output'] == "anim_list"): + self.anims = a + if(user_data['output'] == "reanim_list"): + self.reanims = a + + return + + #WIP plan responsiveness ig + def _working_window(self): + # guarantee these commands happen in the same frame + with dpg.mutex(): + + viewport_width = dpg.get_viewport_client_width() + viewport_height = dpg.get_viewport_client_height() + + with dpg.window(label="Processing...", modal=True, no_close=True, tag="loading") as modal_id: + dpg.add_text("The operation is ongoing") + dpg.add_button(label="Ok", width=75, user_data=(modal_id, True), callback=lambda:dpg.delete_item(modal_id)) + + # guarantee these commands happen in another frame + dpg.split_frame() + width = viewport_width + height = dpg.get_item_height(modal_id) + dpg.set_item_pos(modal_id, [viewport_width // 2 - width // 2, viewport_height // 2 - height // 2]) + + def _get_ft(self): + # kinda dislike this + x = dpg.get_value("extension_selector") + if (x == ".fbx (Requires Noesis conversion)"): + return "fbx" + if (x == ".hkx packfile (HavokMax compatible)"): + return "hkxp" + if (x == ".hkx tagfile"): + return "hkxt" + if (x == ".xml"): + return "xml" + + def _copy_tab(self, tab): + if tab == "repack": + if dpg.get_value("selected_repap") != "": dpg.set_value("selected_pap", dpg.get_value("selected_repap")) + if dpg.get_value("selected_resklb") != "": dpg.set_value("selected_sklb", dpg.get_value("selected_resklb")) + elif tab == "extract": + if dpg.get_value("selected_pap") != "": dpg.set_value("selected_repap", dpg.get_value("selected_pap")) + if dpg.get_value("selected_sklb") != "": dpg.set_value("selected_resklb", dpg.get_value("selected_sklb")) + + def _extract_window(self): + with dpg.child_window(autosize_x=True, height=200): + with dpg.group(): + dpg.add_text("Select .pap:") + with dpg.group(horizontal=True): + #dpg.add_text("None selected", tag="selected_pap_status") + dpg.add_input_text(label="", tag="selected_pap") + dpg.add_button(label="...", callback=lambda: dpg.show_item("anim_dialog")) + dpg.add_text("Select .sklb:") + with dpg.group(horizontal=True): + #dpg.add_text("None selected", tag="selected_sklb_status") + dpg.add_input_text(label="", tag="selected_sklb") + dpg.add_button(label="...", callback=lambda: dpg.show_item("sklb_dialog")) + with dpg.group(horizontal=True): + dpg.add_button(label="Submit", callback=self._populate_anims, user_data={"output" : "anim_list", "pap_input":"selected_pap", "sklb_input":"selected_sklb"}) + dpg.add_button(label="Copy Repack Tab", callback=lambda: self._copy_tab("repack")) + with dpg.group(horizontal=True): + with dpg.child_window(autosize_y=True, width=200): + with dpg.group(): + dpg.add_text("Select Animation") + dpg.add_radio_button(tag="anim_list", label="radio") + with dpg.child_window(autosize_y=True, autosize_x=True, tag="export_window"): + dpg.add_text("Export Options") + dpg.add_text("Export as: ") + with dpg.group(horizontal=True): + dpg.add_combo(items=[".fbx (Requires Noesis conversion)", ".hkx packfile (HavokMax compatible)", ".hkx tagfile", ".xml"], default_value=".fbx (Requires Noesis conversion)", tag="extension_selector", callback=lambda: dpg.set_value("extension", dpg.get_value("extension_selector")[0:4])) + dpg.add_text("Name: ") + with dpg.group(horizontal=True): + dpg.add_input_text(tag="name") + dpg.add_text(".fbx", tag="extension") + dpg.add_text("Export directory: ") + with dpg.group(horizontal=True): + dpg.add_input_text(label="", tag="export_directory") + dpg.add_button(label="...", callback=lambda: dpg.show_item("dir_dialog")) + dpg.add_button(label="Export", callback=self._export_callback) + + def _export_callback(self, sender, app_data): + if not os.path.exists(dpg.get_value("selected_pap")) or not os.path.exists(dpg.get_value("selected_sklb")): + self.show_info("Error","One or more of the files selected does not exist.") + return + export(dpg.get_value("selected_sklb"), dpg.get_value("selected_pap"), self.anims.index(dpg.get_value("anim_list")), dpg.get_value("export_directory")+"\\"+dpg.get_value("name")+dpg.get_value("extension"),self._get_ft() ) + + + def _repack_callback(self, sender, app_data): + if not os.path.exists(dpg.get_value("selected_repap")) or not os.path.exists(dpg.get_value("selected_resklb")) or not os.path.exists(dpg.get_value("selected_fbx")): + self.show_info("Error","One or more of the files selected does not exist.") + return + multi_repack( dpg.get_value("selected_resklb"),dpg.get_value("selected_repap"),dpg.get_value("selected_fbx"), self.reanims.index(dpg.get_value("reanim_list")), dpg.get_value("reexport_directory")+"\\"+dpg.get_value("rename")+".pap") + + def _repack_window(self): + with dpg.child_window(autosize_x=True, height=200): + #with dpg.group(horizontal=True): + # dpg.add_text("Method: ") + # dpg.add_combo(["Multi Pap", "Original AnimAssist (No multi-animation repacking, .hkx only)"]) + dpg.add_text("Select .pap:") + with dpg.group(horizontal=True): + #dpg.add_text("None selected", tag="selected_repap_status") + dpg.add_input_text(label="", tag="selected_repap") + dpg.add_button(label="...", callback=lambda: dpg.show_item("reanim_dialog")) + dpg.add_text("Select .sklb:") + with dpg.group(horizontal=True): + #dpg.add_text("None selected", tag="selected_resklb_status") + dpg.add_input_text(label="", tag="selected_resklb") + dpg.add_button(label="...", callback=lambda: dpg.show_item("resklb_dialog")) + dpg.add_text("Select .fbx or HavokMax .hka/.hkx/.hkt:") + with dpg.group(horizontal=True): + #dpg.add_text("None selected", tag="selected_fbx_status") + dpg.add_input_text(label="", tag="selected_fbx") + dpg.add_button(label="...", callback=lambda: dpg.show_item("fbx_dialog")) + with dpg.group(horizontal=True): + dpg.add_button(label="Submit", callback=self._populate_anims, user_data={"output" : "reanim_list", "pap_input":"selected_repap", "sklb_input":"selected_resklb"}) + dpg.add_button(label="Copy Extract Tab", callback=lambda: self._copy_tab("extract")) + with dpg.group(horizontal=True): + with dpg.child_window(autosize_y=True, width=200): + dpg.add_text("Select Animation") + dpg.add_radio_button(tag="reanim_list", label="radio", default_value=0) + with dpg.child_window(autosize_y=True, autosize_x=True, tag="reexport_window"): + dpg.add_text("Export Options") + dpg.add_text("Name:") + with dpg.group(horizontal=True): + dpg.add_input_text(tag="rename") + dpg.add_text(".pap") + dpg.add_text("Export directory: ") + with dpg.group(horizontal=True): + dpg.add_input_text(label="", tag="reexport_directory") + dpg.add_button(label="...", callback=lambda: dpg.show_item("redir_dialog")) + dpg.add_button(label="Repack", callback=self._repack_callback) + + def _set_theme(self): + accent_light = (239, 179, 221) + accent = (206, 154, 190) + accent_dark = (169, 127, 156) + with dpg.theme() as global_theme: + + with dpg.theme_component(dpg.mvAll): + dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (59, 44, 55), category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvThemeCol_FrameBgHovered, accent_light, category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_FrameBgActive, accent, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvThemeCol_TitleBgActive, accent_dark, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvThemeCol_CheckMark, accent, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvThemeCol_SliderGrab, accent_light, category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_SliderGrabActive, accent, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, accent_light, category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, accent, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvThemeCol_HeaderHovered, accent_light, category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderActive, accent, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvThemeCol_TabHovered, accent_light, category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_TabActive, accent, category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_TabUnfocusedActive, accent, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvThemeCol_DockingPreview, accent, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvThemeCol_TextSelectedBg, accent_dark, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvNodeCol_TitleBarHovered, accent_dark, category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvNodeCol_TitleBarSelected, accent, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvNodeCol_LinkHovered, accent, category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvNodeCol_LinkSelected, accent, category=dpg.mvThemeCat_Core) + + dpg.add_theme_color(dpg.mvNodeCol_BoxSelector, accent, category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvNodeCol_BoxSelectorOutline, accent, category=dpg.mvThemeCat_Core) + + dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 5, category=dpg.mvThemeCat_Core) + + dpg.bind_theme(global_theme) + + dpg.show_style_editor() + + def run(self): + dpg.create_context() + + # surely there's a better way to make these dialogs lol + # also these suck i'll just replace them with tkinter dialogs eventually + with dpg.file_dialog(directory_selector=False, show=False, tag="anim_dialog", modal=True, height=300, callback=self._file_handler, user_data="selected_pap"): + dpg.add_file_extension(".pap", color=(0, 255, 0, 255)) + dpg.add_file_extension(".hkx", color=(0, 255, 0, 255)) + + with dpg.file_dialog(directory_selector=False, show=False, tag="sklb_dialog", modal=True, height=300, callback=self._file_handler, user_data="selected_sklb"): + dpg.add_file_extension(".sklb", color=(0, 255, 0, 255)) + + with dpg.file_dialog(directory_selector=False, show=False, tag="reanim_dialog", modal=True, height=300, callback=self._file_handler, user_data="selected_repap"): + dpg.add_file_extension(".pap", color=(0, 255, 0, 255)) + + with dpg.file_dialog(directory_selector=False, show=False, tag="resklb_dialog", modal=True, height=300, callback=self._file_handler, user_data="selected_resklb"): + dpg.add_file_extension(".sklb", color=(0, 255, 0, 255)) + + with dpg.file_dialog(directory_selector=False, show=False, tag="fbx_dialog", modal=True, height=300, callback=self._file_handler, user_data="selected_fbx"): + dpg.add_file_extension("Anim files (*.fbx *.hkx *.hka *.hkt){.fbx,.hkx,.hka,.hkt}", color=(0, 255, 0, 255)) # Colour doesn't function + + dpg.add_file_dialog(directory_selector=True, show=False, tag="dir_dialog", modal=True, height=300, callback=lambda a, b:dpg.set_value("export_directory", b['file_path_name'])) + dpg.add_file_dialog(directory_selector=True, show=False, tag="redir_dialog", modal=True, height=300, callback=lambda a, b:dpg.set_value("reexport_directory", b['file_path_name'])) + + + with dpg.window(tag="Primary Window"): + dpg.add_text("MultiAssist") + with dpg.tab_bar(label='tabbar'): + with dpg.tab(label='Extract'): + self._extract_window() + with dpg.tab(label='Repack'): + self._repack_window() + + self._set_theme() + + dpg.create_viewport(title='MultiAssist', width=650, height=510) + dpg.set_viewport_large_icon("icon/icon_large.ico") + dpg.set_viewport_small_icon("icon/icon_large.ico") + + dpg.setup_dearpygui() + dpg.show_viewport() + dpg.set_primary_window("Primary Window", True) + dpg.start_dearpygui() + dpg.destroy_context() + +def main(): + parser = define_parser() + + try: + args = parser.parse_args() + except FileNotFoundError: + print("All input files must exist!") + + if args.command == "extract": + export(args.skeleton_file, args.pap_file, args.anim_index, args.out_file, args.file_type) + elif args.command == "pack": + multi_repack(args.skeleton_file, args.pap_file, args.anim_file, args.anim_index, args.out_file) + elif args.command == "aextract": + extract(args.skeleton_file, args.pap_file, args.out_file) + elif args.command == "apack": + repack(args.skeleton_file, args.pap_file, args.anim_file, args.out_file) + else: + global gui + gui = GUI() + gui.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcd2f46 --- /dev/null +++ b/README.md @@ -0,0 +1,214 @@ +![mabanner](gh/banner.png) +# MultiAssist +[AnimAssist](https://github.com/lmcintyre/AnimAssist) GUI with support for repacking .pap files with multiple animations, minor bug fixes, and more extensive export options, including FBX. It can also be used without the GUI through command line arguments. + +This project assists in the editing of FFXIV animation files. + +I started this fork so I had a quick way to extract some animation data from XIV that AnimAssist itself did not provide by default, I expanded upon it until it became MultiAssist. The multi-animation repacking process seems to be an enigma to many due to some unfortunate things, hopefully this makes it more accessible. + +Essentially everything good in this project comes from Perchbird's work, everything bad in this project is probably by me. + +# Pre-requisites +### Required software +* The latest release in [Releases](#) (or your own build of the project). +* [VC++2012 32-bit Redist](https://www.microsoft.com/en-us/download/details.aspx?id=30679#) + * Select "VSU_4\vcredist_x86.exe" when choosing which file to download. + * This is required for Havok stuff, please install it. +* [animassist.exe/tofbx.exe/fbx2havok.exe and associated libraries](#) + * **These are already included in the release download.** + * If you wish to view their source or to build them yourself, refer to: + * animassist.exe was built from the C++ project in this repository, you **must** use this version of animassist.exe and cannot use the executable from the original project. Refer to [Building](#building) for build requirements. + * [tofbx](https://github.com/lmcintyre/fbx2havok) (or a renamed [havok2fbx](https://github.com/razar51/havok2fbx)) + * [fbx2havok](https://github.com/lmcintyre/fbx2havok) (The build in this project is slightly modified to output uncompressed animations) + +### Recommended software +* [FFXIV Explorer (goat fork)](https://github.com/goaaats/ffxiv-explorer-fork) + * Convenient way to browse and extract Skeleton and Animation files. Please use raw extraction (Ctrl+Shift+E) for MultiAssist purposes. Other methods of extractions, such as TexTools, will also work. +* [Noesis](https://richwhitehouse.com/index.php?content=inc_projects.php&showproject=91) + * Use this to convert FBX file exported from MultiAssist to binary FBX files. Please use this converted FBX file when importing into 3D software, rather than the export directly from MultiAssist. + * Unconverted FBX files can still be imported into some 3D editors, however they will likely not import as expected. + * Consider FBX repacking to be in an unpolished state, I would appreciate oddities being reported, as testing this stuff is really monotonous. +* (**Recommended if you have 3DS Max**) [HavokMax - 3DS Max plugin](https://github.com/PredatorCZ/HavokMax/) + * HavokMax adds the functionality to import and export .hkx packfiles through 3DS max. + * You do not need this if you plan to edit .FBX files. If you wish to edit .FBX files, please refer to Noesis in the [Recommended Software](#recommended-software) section. +* A 3D editor + * To edit the extracted animations. + * I only tested with 3DS max. +### Optional Software +* Python 3 (If running from MultiAsisst.py rather than the pre-built release) + * I was using python 3.10.4 + * BeautifulSoup: `pip install bs4` in terminal/command line. + * Dear PyGui: `pip install dearpygui` in terminal/command line. + * If you are using the .py, make sure to build or otherwise acquire the companion executables. +* [Godbert](https://github.com/xivapi/SaintCoinach#godbert) + * May be useful in assisting yourself in familiarizing yourself with the location of FFXIV animations, entirely optional. + +# Installation and Usage +*(NOTE: UX, input validation and error reporting within the GUI is quite poor within the current release! Please follow these instructions carefully and with this in mind. For now, a command window should open alongside the MultiAssist executable which should provide insight into any errors you run into, as well as a way to track the progress of any operations you perform.)* +## Installation +To install MultiAssist, head to (Releases)[https://github.com/ilmheg/MultiAssist/releases] and download the latest MultiAssist.zip. Extract the files to an accessible location and run MultiAssist.exe to use the GUI. Do not remove any of the extracted files in this folder. + +The MultiAssist GUI might open under the terminal window. + +## Extracting an animation + + +### Extracting raw animation and skeleton files from XIV +For demonstration purposes, we will use FFXIV Explorer to export our animations and skeleton files. If it's your first time using FFXIV Explorer, make sure your FFXIV Path is set in Options > Settings > FFXIV Path. + +Most animations you will want to edit are located in 040000.win32.index, to open this in FFXIV Explorer navigate to File > Open and select 040000.win32.index. + +The contents of this index file are divided into folders such as chara/human and chara/monster. Skeletons will generally be located at `chara/{category}/{type_char}{id}/skeleton/base/b0001/skl_{type_char}{id}b0001.sklb`. + + +For a quick reference on the different human folders, take a loot at this table. + +| Race/Gender | Folder | Notes | +| ------------------|----------------------| -------| +| Hyur Midlander M | chara/human/c0101/ | A lot of emotes use the Hyur male skeleton +| Hyur Midlander F | chara/human/c0201/ | +| Hyur Highlander M | chara/human/c0301/ | +| Hyur Highlander F | chara/human/c0401/ | +| Elezen M | chara/human/c0501/ | +| Elezen F | chara/human/c0601/ | +| Miqote M | chara/human/c0701/ | +| Miqote F | chara/human/c0801/ | +| Roegadyn M | chara/human/c0901/ | +| Roegadyn F | chara/human/c1001/ | +| Lalafell M | chara/human/c1101/ | +| Lalafell F | chara/human/c1201/ | +| Au Ra M | chara/human/c1301/ | +| Au Ra F | chara/human/c1401/ | +| Hrothgar M | chara/human/c1501/ | +| Hrothgar F | | c1601 (probably) +| Viera M | chara/human/c1701/ | +| Viera F | chara/human/c1801/ | + +Animation files always have the `.pap` extension. It can sometimes be difficult to determine which skeleton an animation uses. MultiAssist will warn you if there is a mismatch between the skeleton and the skeleton the animation file was built with. .pap files will usually be nested within an `/animation/` folder. Emotes will generally further be within a `bt_common` folder, for example `chara/human/c0101/animation/bt_common/emote` for most Male Hyur emotes. + +Finding non-human files is broader topic, however, searching for a mount/minion/etc. in TextTools will provide you with an id within its material/model paths that should provide sufficient information. + +For this demonstration, I will be exporting the Fatter Cat mount skeleton and animation file. The basic animations for a mount are stored within a `mount.pap`. To do grab this file, navigate to and select `chara/monster/m0512/animation/a0001/bt_common/resident/mount.pap` in FFXIV explorer. With it selected, select File > Extract Raw (Or press Ctrl+Shift+E) and select your save directory. It is essential that you extract this file as a raw file. + +The skeleton for Fatter Cat is listed under `chara/monster/m0512/skeleton/base/b0001/skl_m0512b0001.sklb`. We just need the .sklb file, so select it and extract the raw file again. + +With both the skeleton file and .pap animation file extracted we can move to MultiAssist. You may wish to move the files to a more convenient location. Note that the .pap and .sklb files will be nested within the same folder layout as described in FFXIV Explorer after extracting. + + +### Exporting editable animation files with MultiAssist +After extracting our animation and skeleton file, we can finally open `MultiAssist.exe` to extract an editable animation file. + +With the Extract tab selected, we first should select our .pap and .sklb files. You can select these with the in-app file explorer by pressing [...] or by typing in a path manually. + +With the paths to the respective files set, press [Submit] to populate the animations list in the lower left corner. If your .pap only has one animation in it, only a single animation with be displayed here, otherwise, select the animation you wish to extract. If you change the .pap file at any point, make sure to press submit once again. + +The export options in the lower right corner allow you to select the file type and path of your exported animation file. I'll select .fbx and name my animation file export. Select your export directory and and press [Export] to begin exporting the file. This should be a very quick process. + +Please make sure you have everything correctly inputted before pressing [Export]. If successful, there will be a success dialog displaying the export directory. + +![j](gh/etab.png) + +## Editing your exported animation +### FBX +#### ***NOTE ON FBX SUPPORT:*** +*fbx2havok is an older proof-of-concept project by Perchbird, provided mostly as is. This executable in this release is modified to output uncompressed animations, rather than the compressed animation of the original, it is otherwise the original project. **As far as I can tell, this is enough to prevent fbx2havok from being a glitchy mess.** I'm not confident things such as no. of frames is properly preserved, however. If something goes wrong, keep the unpolished nature of this in mind. I would like to see this method fleshed out, and thus am interested in any errors you encounter using this method.* + +To get started editing this FBX, we must first convert it into a binary .FBX file through [Noesis](https://richwhitehouse.com/index.php?content=inc_projects.php&showproject=91). While you can import the unconverted FBX file into some 3D editors, it'll likely display and export as a broken animation. + +Within Noesis, navigate to your export folder in the file tree on the left pane. Your exported .fbx will appear in the middle pane. Selecting it will allow you to preview the animation on the right-most pane. Right-selecting it will allow you to export the .fbx to .fbx. Use the settings in the screenshot below: + +![noesisexport](gh/noeexp.png) + +When exported, the file should be called `.rda.fbx`. + +The exported file can be imported into your 3D editor of choice. Make sure you import the right one, if you are using 3DS max you should see `Animation Take: Noesis Frames` when importing. The default import settings should be correct. + +After making edits to your animation, it should again be exported as an .fbx file before beginning the repacking process. + +### HKX (Packfile) [HavokMax] + +From the [AnimAssist README.md](https://github.com/lmcintyre/AnimAssist/blob/main/README.md) +> Open 3DS Max and check the Plug-in Manager to ensure HavokMax is properly installed. + +> From the Max menu, click Import and select your output_file.hkx from the previous step to open it in 3DS Max. Select disable scale, invert top, and set Back: Top. Click import, and you'll get a wonderful fistbumping skeleton, read directly from what was once game files. + +> You have some sweet changes to some animation, but now we need to put it back into the game. + +> From the Max menu, click Export and type any filename you want, and save it in a cool, dry place. In the "Save as type" drop-down, select "Havok Export (*.HKA, *.HKX, *.,HKT)". We will be saving an hkx file once more. Export your file, again, selecting disable scale, invert top, and set Back: Top. You must select those, as well as Export animation and include skeleton. Set toolset to 2014. In my testing, I found that it was optional, however, it seems safest to disable optimize tracks according to user reports. + + +### Other export formats (havok binary tagfile and XML) +Not properly implemented yet. The XML file will contain all animations if more than one is present. +These probably aren't super useful. Both files (and the .hkx packfile) can be opened in Havok Standalone Tools, and probably not much else. I consider the xml file useful for debugging. At the current time, these two formats cannot be repacked into a .pap with MultiAssist. + +## Repacking your edited animation +With an edited and exported .fbx or .hkx, repackaging the animation is simple. + +As with the extraction process, select your .pap and .sklb file. These should be the same, unedited files your used to extract the animation from. You may re-extract these files from FFXIV if you have lost them. + +Select your edited .fbx or HavokMax export, and press submit to populate the animation list. You should select the animation you want to replace. This can technically be any animation, but generally you will want to replace the same animation you extracted. + +Enter a name and export directory into the Export options and press [Repack]. As always, make sure you fill out every field before pressing [Repack]. + +![noesisexport](gh/rtab.png) + +The resulting .pap file can be imported into FFXIV through TexTools by using the raw file operations (leaving Decompressed Type 2 Data checked), or by placing it in the right place in penumbra. + +# (Optional) Command Line Usage +This project maintains the command line functionality of AnimAssist, both within animassist.exe and MultiAssist.exe. Using command line arguments will not initialize the GUI and at this point probably has better error reporting than the GUI. Here's a basic rundown of the commands, refer to the above Usage for further information. + +### Extracting +`multiasisst.exe extract -s original_skeleton_file.sklb -p original_pap_file.pap -a modified_animation.hkx -i -o new_ffxiv_animation.pap -f ` + +where `` is one of: +* fbx +* hkxp +* hkxt +* xml +### Repacking +`multiasisst.exe pack -s original_skeleton_file.sklb -p original_pap_file.pap -a modified_animation -i -o new_ffxiv_animation.pap` + +Futhermore, you can use the original AnimAssist commands with `aextract` and `apack` and the respective original arguments. Please note the original commands do not support multi-pap repacking, but do retain some minor fixes in the extraction process. + +# Technical rundown of the process +### Extraction +#### MultiAssist.py +1. Strips headers of pap and sklb, leaving us with a havok binary tagfile of the animation and skeleton. +#### animassist.exe +2. Creates XML packfile of Skeleton from the havok tagfile. +3. Selects the animation and binding of the specified index and repackages them with the Skeleton packfile into a new havok packfile. +#### MultiAssist.py +4. Output differs depending on file type input. + * FBX - Runs havok data through havok2fbx.exe and returns an FBX. + * Packfile - Returns the packfile from step 3. + * Tagfile - Returns the havok anim data from step 1. + * XML - Uses animassist.exe and the havok data from step 1 to return an XML file with the same format as Havok Preview Tool exports. + +### Repackaging +In short, the files end up as the XML export format discussed in the final step of extraction. From this, the animation and binding of the modded animation replace the animation and binding of the user inputted animation index. The merged XML is passed back to animassist.exe where it is converted into a binary tagfile. The header is regenerated with new offsets and the timeline data is copied from the original .pap into the new export. + + +# Future development plans for MultiAssist +I hope to continue supporting this project. In the near future I will improve input validation and error reporting within the GUI. Additionally, I am looking for ways to further streamline the process in the more distant future, such exporting directly to penumbra. Less generally, I believe direct animation swapping and features of a similar vein are also within the scope of this project. + +# Building, contributions, notes +## Notes +Please note I nor this project are affiliated with AnimAssist, fbx2havok, havok2fbx, or any other projects, nor their contributors. + +In the spirit of the original AnimAssist, this project's license if WTFPL. Refer to the LICENSE within the release folder for the license of the included redistributed executables. + +I hope this program further streamlines animation editing in FFXIV and makes the process more accessible. + +Lastly, please note I *really* don't know what I'm doing!! + +## Building +Building animassist.exe requires the Havok 2014 SDK and an env var of HAVOK_SDK_ROOT set to the directory, as well as the Visual C++ Platform Toolset v110. This is included in any install of VS2012, including the Community edition. + +You can find the Havok SDK to compile with [in the description of this video](https://www.youtube.com/watch?v=U88C9K-mSHs). Please note that is NOT a download controlled by any contributor to AnimAssist, use at your own risk. + +Building the associated fbx2havok/havok2fbx projects will require additionally require FBX SDK 2014.2.1, refer to the respective repositories for further guidance. + +For the python pre-requisites, see [Optional Software](#optional-software). + +## Contributing +Contributions are welcome, please help make bad things good etc diff --git a/fbx2havok/.gitignore b/fbx2havok/.gitignore new file mode 100644 index 0000000..d43ccc1 --- /dev/null +++ b/fbx2havok/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +cmake*/ diff --git a/fbx2havok/CMakeLists.txt b/fbx2havok/CMakeLists.txt new file mode 100644 index 0000000..c7882c0 --- /dev/null +++ b/fbx2havok/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.10) +project(fbx2havok) + +set( + PROJ_LIBRARIES + hkBase.lib + hkSerialize.lib + hkSceneData.lib + hkVisualize.lib + hkInternal.lib + hkImageUtilities.lib + hkaAnimation.lib + hkaInternal.lib + hkaPhysics2012Bridge.lib + hkcdCollide.lib + hkcdInternal.lib + hkGeometryUtilities.lib + hkCompat.lib + libfbxsdk-md.lib +) + +include_directories( + "C:/Program Files/Autodesk/FBX/FBX SDK/2014.2.1/include/" + "C:/Program Files/HAVOK/SDK/hk2014_1_0_r1/Source/" +) + +link_directories( + "C:/Program Files/Autodesk/FBX/FBX SDK/2014.2.1/lib/vs2012/x86/release/" + "C:/Program Files/HAVOK/SDK/hk2014_1_0_r1/Lib/win32_vs2012_win7_noSimd/release/" +) + +add_library(commonLib + Core/EulerAngles.h + Core/FBXCommon.cxx + Core/FBXCommon.h + Core/hkAssetManagementUtil.cpp + Core/hkAssetManagementUtil.h + Core/MathHelper.h + Core/stdafx.cpp + Core/stdafx.h + Core/targetver.h +) + +add_executable(fbx2havok Core/main.cpp) +target_link_libraries(fbx2havok ${PROJ_LIBRARIES} commonLib) + +add_executable(tofbx Core/tofbx.cpp) +target_link_libraries(tofbx ${PROJ_LIBRARIES} commonLib) + +set(CMAKE_CXX_STANDARD 14) + +include_directories(Core converttest) + + + diff --git a/fbx2havok/Core/EulerAngles.h b/fbx2havok/Core/EulerAngles.h new file mode 100644 index 0000000..d7bd8c4 --- /dev/null +++ b/fbx2havok/Core/EulerAngles.h @@ -0,0 +1,187 @@ +/**** EulerAngles.h - Support for 24 angle schemes ****/ +/* Ken Shoemake, 1993 */ +#ifndef _H_EulerAngles +#define _H_EulerAngles + +#include +#include + +/*** Definitions ***/ +typedef struct {double x, y, z, w;} Quat; /* Quaternion */ +enum QuatPart {X, Y, Z, W}; +typedef double HMatrix[4][4]; /* Right-handed, for column vectors */ +typedef Quat EulerAngles; /* (x,y,z)=ang 1,2,3, w=order code */ + +/*** Order type constants, constructors, extractors ***/ + /* There are 24 possible conventions, designated by: */ + /* o EulAxI = axis used initially */ + /* o EulPar = parity of axis permutation */ + /* o EulRep = repetition of initial axis as last */ + /* o EulFrm = frame from which axes are taken */ + /* Axes I,J,K will be a permutation of X,Y,Z. */ + /* Axis H will be either I or K, depending on EulRep. */ + /* Frame S takes axes from initial static frame. */ + /* If ord = (AxI=X, Par=Even, Rep=No, Frm=S), then */ + /* {a,b,c,ord} means Rz(c)Ry(b)Rx(a), where Rz(c)v */ + /* rotates v around Z by c radians. */ +#define EulFrmS 0 +#define EulFrmR 1 +#define EulFrm(ord) ((unsigned)(ord)&1) +#define EulRepNo 0 +#define EulRepYes 1 +#define EulRep(ord) (((unsigned)(ord)>>1)&1) +#define EulParEven 0 +#define EulParOdd 1 +#define EulPar(ord) (((unsigned)(ord)>>2)&1) +/* this code is merely a quick (and legal!) way to set arrays, EulSafe being 0,1,2,0 */ +#define EulSafe "\000\001\002\000" +#define EulNext "\001\002\000\001" +#define EulAxI(ord) ((int)(EulSafe[(((unsigned)(ord)>>3)&3)])) +#define EulAxJ(ord) ((int)(EulNext[EulAxI(ord)+(EulPar(ord)==EulParOdd)])) +#define EulAxK(ord) ((int)(EulNext[EulAxI(ord)+(EulPar(ord)!=EulParOdd)])) +#define EulAxH(ord) ((EulRep(ord)==EulRepNo)?EulAxK(ord):EulAxI(ord)) + /* EulGetOrd unpacks all useful information about order simultaneously. */ +#define EulGetOrd(ord,i,j,k,h,n,s,f) {unsigned o=ord;f=o&1;o>>=1;s=o&1;o>>=1;\ + n=o&1;o>>=1;i=EulSafe[o&3];j=EulNext[i+n];k=EulNext[i+1-n];h=s?k:i;} + /* EulOrd creates an order value between 0 and 23 from 4-tuple choices. */ +#define EulOrd(i,p,r,f) (((((((i)<<1)+(p))<<1)+(r))<<1)+(f)) + /* Static axes */ +#define EulOrdXYZs EulOrd(X,EulParEven,EulRepNo,EulFrmS) +#define EulOrdXYXs EulOrd(X,EulParEven,EulRepYes,EulFrmS) +#define EulOrdXZYs EulOrd(X,EulParOdd,EulRepNo,EulFrmS) +#define EulOrdXZXs EulOrd(X,EulParOdd,EulRepYes,EulFrmS) +#define EulOrdYZXs EulOrd(Y,EulParEven,EulRepNo,EulFrmS) +#define EulOrdYZYs EulOrd(Y,EulParEven,EulRepYes,EulFrmS) +#define EulOrdYXZs EulOrd(Y,EulParOdd,EulRepNo,EulFrmS) +#define EulOrdYXYs EulOrd(Y,EulParOdd,EulRepYes,EulFrmS) +#define EulOrdZXYs EulOrd(Z,EulParEven,EulRepNo,EulFrmS) +#define EulOrdZXZs EulOrd(Z,EulParEven,EulRepYes,EulFrmS) +#define EulOrdZYXs EulOrd(Z,EulParOdd,EulRepNo,EulFrmS) +#define EulOrdZYZs EulOrd(Z,EulParOdd,EulRepYes,EulFrmS) + /* Rotating axes */ +#define EulOrdZYXr EulOrd(X,EulParEven,EulRepNo,EulFrmR) +#define EulOrdXYXr EulOrd(X,EulParEven,EulRepYes,EulFrmR) +#define EulOrdYZXr EulOrd(X,EulParOdd,EulRepNo,EulFrmR) +#define EulOrdXZXr EulOrd(X,EulParOdd,EulRepYes,EulFrmR) +#define EulOrdXZYr EulOrd(Y,EulParEven,EulRepNo,EulFrmR) +#define EulOrdYZYr EulOrd(Y,EulParEven,EulRepYes,EulFrmR) +#define EulOrdZXYr EulOrd(Y,EulParOdd,EulRepNo,EulFrmR) +#define EulOrdYXYr EulOrd(Y,EulParOdd,EulRepYes,EulFrmR) +#define EulOrdYXZr EulOrd(Z,EulParEven,EulRepNo,EulFrmR) +#define EulOrdZXZr EulOrd(Z,EulParEven,EulRepYes,EulFrmR) +#define EulOrdXYZr EulOrd(Z,EulParOdd,EulRepNo,EulFrmR) +#define EulOrdZYZr EulOrd(Z,EulParOdd,EulRepYes,EulFrmR) + +EulerAngles Eul_(double ai, double aj, double ah, int order) +{ + EulerAngles ea; + ea.x = ai; ea.y = aj; ea.z = ah; + ea.w = order; + return (ea); +} +/* Construct quaternion from Euler angles (in radians). */ +Quat Eul_ToQuat(EulerAngles ea) +{ + Quat qu; + double a[3], ti, tj, th, ci, cj, ch, si, sj, sh, cc, cs, sc, ss; + int i,j,k,h,n,s,f; + EulGetOrd(ea.w,i,j,k,h,n,s,f); + if (f==EulFrmR) {double t = ea.x; ea.x = ea.z; ea.z = t;} + if (n==EulParOdd) ea.y = -ea.y; + ti = ea.x*0.5; tj = ea.y*0.5; th = ea.z*0.5; + ci = cos(ti); cj = cos(tj); ch = cos(th); + si = sin(ti); sj = sin(tj); sh = sin(th); + cc = ci*ch; cs = ci*sh; sc = si*ch; ss = si*sh; + if (s==EulRepYes) { + a[i] = cj*(cs + sc); /* Could speed up with */ + a[j] = sj*(cc + ss); /* trig identities. */ + a[k] = sj*(cs - sc); + qu.w = cj*(cc - ss); + } else { + a[i] = cj*sc - sj*cs; + a[j] = cj*ss + sj*cc; + a[k] = cj*cs - sj*sc; + qu.w = cj*cc + sj*ss; + } + if (n==EulParOdd) a[j] = -a[j]; + qu.x = a[X]; qu.y = a[Y]; qu.z = a[Z]; + return (qu); +} + +/* Construct matrix from Euler angles (in radians). */ +void Eul_ToHMatrix(EulerAngles ea, HMatrix M) +{ + double ti, tj, th, ci, cj, ch, si, sj, sh, cc, cs, sc, ss; + int i,j,k,h,n,s,f; + EulGetOrd(ea.w,i,j,k,h,n,s,f); + if (f==EulFrmR) {double t = ea.x; ea.x = ea.z; ea.z = t;} + if (n==EulParOdd) {ea.x = -ea.x; ea.y = -ea.y; ea.z = -ea.z;} + ti = ea.x; tj = ea.y; th = ea.z; + ci = cos(ti); cj = cos(tj); ch = cos(th); + si = sin(ti); sj = sin(tj); sh = sin(th); + cc = ci*ch; cs = ci*sh; sc = si*ch; ss = si*sh; + if (s==EulRepYes) { + M[i][i] = cj; M[i][j] = sj*si; M[i][k] = sj*ci; + M[j][i] = sj*sh; M[j][j] = -cj*ss+cc; M[j][k] = -cj*cs-sc; + M[k][i] = -sj*ch; M[k][j] = cj*sc+cs; M[k][k] = cj*cc-ss; + } else { + M[i][i] = cj*ch; M[i][j] = sj*sc-cs; M[i][k] = sj*cc+ss; + M[j][i] = cj*sh; M[j][j] = sj*ss+cc; M[j][k] = sj*cs-sc; + M[k][i] = -sj; M[k][j] = cj*si; M[k][k] = cj*ci; + } + M[W][X]=M[W][Y]=M[W][Z]=M[X][W]=M[Y][W]=M[Z][W]=0.0; M[W][W]=1.0; +} + +/* Convert matrix to Euler angles (in radians). */ +EulerAngles Eul_FromHMatrix(HMatrix M, int order) +{ + EulerAngles ea; + int i,j,k,h,n,s,f; + EulGetOrd(order,i,j,k,h,n,s,f); + if (s==EulRepYes) { + double sy = sqrt(M[i][j]*M[i][j] + M[i][k]*M[i][k]); + if (sy > 16*FLT_EPSILON) { + ea.x = atan2(M[i][j], M[i][k]); + ea.y = atan2(sy, M[i][i]); + ea.z = atan2(M[j][i], -M[k][i]); + } else { + ea.x = atan2(-M[j][k], M[j][j]); + ea.y = atan2(sy, M[i][i]); + ea.z = 0; + } + } else { + double cy = sqrt(M[i][i]*M[i][i] + M[j][i]*M[j][i]); + if (cy > 16*FLT_EPSILON) { + ea.x = atan2(M[k][j], M[k][k]); + ea.y = atan2(-M[k][i], cy); + ea.z = atan2(M[j][i], M[i][i]); + } else { + ea.x = atan2(-M[j][k], M[j][j]); + ea.y = atan2(-M[k][i], cy); + ea.z = 0; + } + } + if (n==EulParOdd) {ea.x = -ea.x; ea.y = - ea.y; ea.z = -ea.z;} + if (f==EulFrmR) {double t = ea.x; ea.x = ea.z; ea.z = t;} + ea.w = order; + return (ea); +} + +/* Convert quaternion to Euler angles (in radians). */ +EulerAngles Eul_FromQuat(Quat q, int order) +{ + HMatrix M; + double Nq = q.x*q.x+q.y*q.y+q.z*q.z+q.w*q.w; + double s = (Nq > 0.0) ? (2.0 / Nq) : 0.0; + double xs = q.x*s, ys = q.y*s, zs = q.z*s; + double wx = q.w*xs, wy = q.w*ys, wz = q.w*zs; + double xx = q.x*xs, xy = q.x*ys, xz = q.x*zs; + double yy = q.y*ys, yz = q.y*zs, zz = q.z*zs; + M[X][X] = 1.0 - (yy + zz); M[X][Y] = xy - wz; M[X][Z] = xz + wy; + M[Y][X] = xy + wz; M[Y][Y] = 1.0 - (xx + zz); M[Y][Z] = yz - wx; + M[Z][X] = xz - wy; M[Z][Y] = yz + wx; M[Z][Z] = 1.0 - (xx + yy); + M[W][X]=M[W][Y]=M[W][Z]=M[X][W]=M[Y][W]=M[Z][W]=0.0; M[W][W]=1.0; + return (Eul_FromHMatrix(M, order)); +} + +#endif diff --git a/fbx2havok/Core/FBXCommon.cxx b/fbx2havok/Core/FBXCommon.cxx new file mode 100644 index 0000000..3b87d0d --- /dev/null +++ b/fbx2havok/Core/FBXCommon.cxx @@ -0,0 +1,225 @@ +/**************************************************************************************** + + Copyright (C) 2013 Autodesk, Inc. + All rights reserved. + + Use of this software is subject to the terms of the Autodesk license agreement + provided at the time of installation or download, or which otherwise accompanies + this software in either electronic or hard copy form. + +****************************************************************************************/ + +#include "FBXCommon.h" + +#ifdef IOS_REF + #undef IOS_REF + #define IOS_REF (*(pManager->GetIOSettings())) +#endif + +void InitializeSdkObjects(FbxManager*& pManager, FbxScene*& pScene) +{ + //The first thing to do is to create the FBX Manager which is the object allocator for almost all the classes in the SDK + pManager = FbxManager::Create(); + if( !pManager ) + { + FBXSDK_printf("Error: Unable to create FBX Manager!\n"); + exit(1); + } + else FBXSDK_printf("Autodesk FBX SDK version %s\n", pManager->GetVersion()); + + //Create an IOSettings object. This object holds all import/export settings. + FbxIOSettings* ios = FbxIOSettings::Create(pManager, IOSROOT); + pManager->SetIOSettings(ios); + + //Load plugins from the executable directory (optional) + FbxString lPath = FbxGetApplicationDirectory(); + pManager->LoadPluginsDirectory(lPath.Buffer()); + + //Create an FBX scene. This object holds most objects imported/exported from/to files. + pScene = FbxScene::Create(pManager, "My Scene"); + if( !pScene ) + { + FBXSDK_printf("Error: Unable to create FBX scene!\n"); + exit(1); + } +} + +void DestroySdkObjects(FbxManager* pManager, bool pExitStatus) +{ + //Delete the FBX Manager. All the objects that have been allocated using the FBX Manager and that haven't been explicitly destroyed are also automatically destroyed. + if( pManager ) pManager->Destroy(); + if( pExitStatus ) FBXSDK_printf("Program Success!\n"); +} + +bool SaveScene(FbxManager* pManager, FbxDocument* pScene, const char* pFilename, int pFileFormat, bool pEmbedMedia) +{ + int lMajor, lMinor, lRevision; + bool lStatus = true; + + // Create an exporter. + FbxExporter* lExporter = FbxExporter::Create(pManager, ""); + + if( pFileFormat < 0 || pFileFormat >= pManager->GetIOPluginRegistry()->GetWriterFormatCount() ) + { + // Write in fall back format in less no ASCII format found + pFileFormat = pManager->GetIOPluginRegistry()->GetNativeWriterFormat(); + + //Try to export in ASCII if possible + int lFormatIndex, lFormatCount = pManager->GetIOPluginRegistry()->GetWriterFormatCount(); + + for (lFormatIndex=0; lFormatIndexGetIOPluginRegistry()->WriterIsFBX(lFormatIndex)) + { + FbxString lDesc =pManager->GetIOPluginRegistry()->GetWriterFormatDescription(lFormatIndex); + const char *lASCII = "ascii"; + if (lDesc.Find(lASCII)>=0) + { + pFileFormat = lFormatIndex; + break; + } + } + } + } + + // Set the export states. By default, the export states are always set to + // true except for the option eEXPORT_TEXTURE_AS_EMBEDDED. The code below + // shows how to change these states. + IOS_REF.SetBoolProp(EXP_FBX_MATERIAL, true); + IOS_REF.SetBoolProp(EXP_FBX_TEXTURE, true); + IOS_REF.SetBoolProp(EXP_FBX_EMBEDDED, pEmbedMedia); + IOS_REF.SetBoolProp(EXP_FBX_SHAPE, true); + IOS_REF.SetBoolProp(EXP_FBX_GOBO, true); + IOS_REF.SetBoolProp(EXP_FBX_ANIMATION, true); + IOS_REF.SetBoolProp(EXP_FBX_GLOBAL_SETTINGS, true); + + // Initialize the exporter by providing a filename. + if(lExporter->Initialize(pFilename, pFileFormat, pManager->GetIOSettings()) == false) + { + FBXSDK_printf("Call to FbxExporter::Initialize() failed.\n"); + FBXSDK_printf("Error returned: %s\n\n", lExporter->GetStatus().GetErrorString()); + return false; + } + + FbxManager::GetFileFormatVersion(lMajor, lMinor, lRevision); + FBXSDK_printf("FBX file format version %d.%d.%d\n\n", lMajor, lMinor, lRevision); + + // Export the scene. + lStatus = lExporter->Export(pScene); + + // Destroy the exporter. + lExporter->Destroy(); + return lStatus; +} + +bool LoadScene(FbxManager* pManager, FbxDocument* pScene, const char* pFilename) +{ + int lFileMajor, lFileMinor, lFileRevision; + int lSDKMajor, lSDKMinor, lSDKRevision; + //int lFileFormat = -1; + int i, lAnimStackCount; + bool lStatus; + char lPassword[1024]; + + // Get the file version number generate by the FBX SDK. + FbxManager::GetFileFormatVersion(lSDKMajor, lSDKMinor, lSDKRevision); + + // Create an importer. + FbxImporter* lImporter = FbxImporter::Create(pManager,""); + + // Initialize the importer by providing a filename. + const bool lImportStatus = lImporter->Initialize(pFilename, -1, pManager->GetIOSettings()); + lImporter->GetFileVersion(lFileMajor, lFileMinor, lFileRevision); + + if( !lImportStatus ) + { + FbxString error = lImporter->GetStatus().GetErrorString(); + FBXSDK_printf("Call to FbxImporter::Initialize() failed.\n"); + FBXSDK_printf("Error returned: %s\n\n", error.Buffer()); + + if (lImporter->GetStatus().GetCode() == FbxStatus::eInvalidFileVersion) + { + FBXSDK_printf("FBX file format version for this FBX SDK is %d.%d.%d\n", lSDKMajor, lSDKMinor, lSDKRevision); + FBXSDK_printf("FBX file format version for file '%s' is %d.%d.%d\n\n", pFilename, lFileMajor, lFileMinor, lFileRevision); + } + + return false; + } + + FBXSDK_printf("FBX file format version for this FBX SDK is %d.%d.%d\n", lSDKMajor, lSDKMinor, lSDKRevision); + + if (lImporter->IsFBX()) + { + FBXSDK_printf("FBX file format version for file '%s' is %d.%d.%d\n\n", pFilename, lFileMajor, lFileMinor, lFileRevision); + + // From this point, it is possible to access animation stack information without + // the expense of loading the entire file. + + FBXSDK_printf("Animation Stack Information\n"); + + lAnimStackCount = lImporter->GetAnimStackCount(); + + FBXSDK_printf(" Number of Animation Stacks: %d\n", lAnimStackCount); + FBXSDK_printf(" Current Animation Stack: \"%s\"\n", lImporter->GetActiveAnimStackName().Buffer()); + FBXSDK_printf("\n"); + + for(i = 0; i < lAnimStackCount; i++) + { + FbxTakeInfo* lTakeInfo = lImporter->GetTakeInfo(i); + + FBXSDK_printf(" Animation Stack %d\n", i); + FBXSDK_printf(" Name: \"%s\"\n", lTakeInfo->mName.Buffer()); + FBXSDK_printf(" Description: \"%s\"\n", lTakeInfo->mDescription.Buffer()); + + // Change the value of the import name if the animation stack should be imported + // under a different name. + FBXSDK_printf(" Import Name: \"%s\"\n", lTakeInfo->mImportName.Buffer()); + + // Set the value of the import state to false if the animation stack should be not + // be imported. + FBXSDK_printf(" Import State: %s\n", lTakeInfo->mSelect ? "true" : "false"); + FBXSDK_printf("\n"); + } + + // Set the import states. By default, the import states are always set to + // true. The code below shows how to change these states. + IOS_REF.SetBoolProp(IMP_FBX_MATERIAL, true); + IOS_REF.SetBoolProp(IMP_FBX_TEXTURE, true); + IOS_REF.SetBoolProp(IMP_FBX_LINK, true); + IOS_REF.SetBoolProp(IMP_FBX_SHAPE, true); + IOS_REF.SetBoolProp(IMP_FBX_GOBO, true); + IOS_REF.SetBoolProp(IMP_FBX_ANIMATION, true); + IOS_REF.SetBoolProp(IMP_FBX_GLOBAL_SETTINGS, true); + } + + // Import the scene. + lStatus = lImporter->Import(pScene); + + if(lStatus == false && lImporter->GetStatus().GetCode() == FbxStatus::ePasswordError) + { + FBXSDK_printf("Please enter password: "); + + lPassword[0] = '\0'; + + FBXSDK_CRT_SECURE_NO_WARNING_BEGIN + scanf("%s", lPassword); + FBXSDK_CRT_SECURE_NO_WARNING_END + + FbxString lString(lPassword); + + IOS_REF.SetStringProp(IMP_FBX_PASSWORD, lString); + IOS_REF.SetBoolProp(IMP_FBX_PASSWORD_ENABLE, true); + + lStatus = lImporter->Import(pScene); + + if(lStatus == false && lImporter->GetStatus().GetCode() == FbxStatus::ePasswordError) + { + FBXSDK_printf("\nPassword is wrong, import aborted.\n"); + } + } + + // Destroy the importer. + lImporter->Destroy(); + + return lStatus; +} diff --git a/fbx2havok/Core/FBXCommon.h b/fbx2havok/Core/FBXCommon.h new file mode 100644 index 0000000..6876755 --- /dev/null +++ b/fbx2havok/Core/FBXCommon.h @@ -0,0 +1,25 @@ +/**************************************************************************************** + + Copyright (C) 2013 Autodesk, Inc. + All rights reserved. + + Use of this software is subject to the terms of the Autodesk license agreement + provided at the time of installation or download, or which otherwise accompanies + this software in either electronic or hard copy form. + +****************************************************************************************/ +#ifndef _COMMON_H +#define _COMMON_H + +#include + +void InitializeSdkObjects(FbxManager*& pManager, FbxScene*& pScene); +void DestroySdkObjects(FbxManager* pManager, bool pExitStatus); +void CreateAndFillIOSettings(FbxManager* pManager); + +bool SaveScene(FbxManager* pManager, FbxDocument* pScene, const char* pFilename, int pFileFormat=-1, bool pEmbedMedia=false); +bool LoadScene(FbxManager* pManager, FbxDocument* pScene, const char* pFilename); + +#endif // #ifndef _COMMON_H + + diff --git a/fbx2havok/Core/MathHelper.h b/fbx2havok/Core/MathHelper.h new file mode 100644 index 0000000..afd6949 --- /dev/null +++ b/fbx2havok/Core/MathHelper.h @@ -0,0 +1,12 @@ +#include +#include + +#define _USE_MATH_DEFINES +#include + +using namespace std; + +double rad2deg(double rad) +{ + return rad*180.0/M_PI; +} \ No newline at end of file diff --git a/fbx2havok/Core/hkAssetManagementUtil.cpp b/fbx2havok/Core/hkAssetManagementUtil.cpp new file mode 100644 index 0000000..47202e7 --- /dev/null +++ b/fbx2havok/Core/hkAssetManagementUtil.cpp @@ -0,0 +1,100 @@ +/* + * + * Confidential Information of Telekinesys Research Limited (t/a Havok). Not for disclosure or distribution without Havok's + * prior written consent. This software contains code, techniques and know-how which is confidential and proprietary to Havok. + * Product and Trade Secret source code contains trade secrets of Havok. Havok Software (C) Copyright 1999-2014 Telekinesys Research Limited t/a Havok. All Rights Reserved. Use of this software is subject to the terms of an end user license agreement. + * + */ +#include "hkAssetManagementUtil.h" +#include + +#define NEED_PLATFORM_SPECIFIC_EXTENSION + +const char* hkAssetManagementUtil::getFileEnding(hkStringBuf& e, hkStructureLayout::LayoutRules rules) +{ + hkStructureLayout l; + e.printf("_L%d%d%d%d", + rules.m_bytesInPointer, + rules.m_littleEndian? 1 : 0, + rules.m_reusePaddingOptimization? 1 : 0, + rules.m_emptyBaseClassOptimization? 1 : 0); + return e; +} + +static bool _fileExists( const char* filename ) +{ + // Open + hkIfstream file( filename ); + + // Check + if (file.isOk()) + { + // Dummy read + char ch; + file.read( &ch , 1); + return file.isOk(); + } + + return false; + +} + +const char* HK_CALL hkAssetManagementUtil::getFilePath( hkStringBuf& filename, hkStructureLayout::LayoutRules rules ) +{ +#ifdef NEED_PLATFORM_SPECIFIC_EXTENSION + if (! _fileExists( filename ) ) + { + // Try platform extension + int extn = filename.lastIndexOf('.'); + if (extn != -1) + { + hkStringBuf fe; getFileEnding(fe, rules); + filename.insert(extn, fe); + } + } +#endif + +#ifdef HK_DEBUG + { + int a0 = filename.lastIndexOf('\\'); + int a1 = filename.lastIndexOf('/'); + int aLen = filename.getLength() - 1; // last index + int mD0 = a0 >= 0? a0 : 0; + int mD1 = a1 >= 0? a1 : 0; + int maxSlash = mD0 > mD1? mD0 : mD1; + if ( (aLen - maxSlash) > 42 ) + { + hkStringBuf w; + w.printf("Your file name [%s] is longer than 42 characters. May have issues on some consoles (like Xbox360).", filename.cString() ); + HK_WARN(0x04324, w.cString() ); + } + } +#endif + return filename; +} + +const char* hkAssetManagementUtil::getFilePath( hkStringBuf& filename ) +{ + return getFilePath( filename, hkStructureLayout::HostLayoutRules ); +} + +const char* HK_CALL hkAssetManagementUtil::getFilePath( const char* pathIn, hkStringBuf& pathOut) +{ + pathOut = pathIn; + return getFilePath( pathOut, hkStructureLayout::HostLayoutRules ); +} + +/* + * Havok SDK - NO SOURCE PC DOWNLOAD, BUILD(#20140907) + * + * Confidential Information of Havok. (C) Copyright 1999-2014 + * Telekinesys Research Limited t/a Havok. All Rights Reserved. The Havok + * Logo, and the Havok buzzsaw logo are trademarks of Havok. Title, ownership + * rights, and intellectual property rights in the Havok software remain in + * Havok and/or its suppliers. + * + * Use of this software for evaluation purposes is subject to and indicates + * acceptance of the End User licence Agreement for this product. A copy of + * the license is included with this software and is also available at www.havok.com/tryhavok. + * + */ diff --git a/fbx2havok/Core/hkAssetManagementUtil.h b/fbx2havok/Core/hkAssetManagementUtil.h new file mode 100644 index 0000000..ab351f3 --- /dev/null +++ b/fbx2havok/Core/hkAssetManagementUtil.h @@ -0,0 +1,45 @@ +/* + * + * Confidential Information of Telekinesys Research Limited (t/a Havok). Not for disclosure or distribution without Havok's + * prior written consent. This software contains code, techniques and know-how which is confidential and proprietary to Havok. + * Product and Trade Secret source code contains trade secrets of Havok. Havok Software (C) Copyright 1999-2014 Telekinesys Research Limited t/a Havok. All Rights Reserved. Use of this software is subject to the terms of an end user license agreement. + * + */ + +#ifndef ASSET_MGT_UTIL_H +#define ASSET_MGT_UTIL_H + +#include +#include + +// This class helps with asset management for our demos +class hkAssetManagementUtil +{ + public: + /// Fill in the string e with the appropriate name identifier stuff + static const char* HK_CALL getFileEnding(hkStringBuf& e, hkStructureLayout::LayoutRules rules); + + /// This class takes a general filename and converts it to a platform specific one + static const char* HK_CALL getFilePath( hkStringBuf& inout ); + static const char* HK_CALL getFilePath( const char* pathIn, hkStringBuf& pathOut); + + /// This class takes a general filename and converts it to a platform specific one + static const char* HK_CALL getFilePath( hkStringBuf& inout, hkStructureLayout::LayoutRules rules ); +}; + +#endif // ASSET_MGT_UTIL_H + +/* + * Havok SDK - NO SOURCE PC DOWNLOAD, BUILD(#20140907) + * + * Confidential Information of Havok. (C) Copyright 1999-2014 + * Telekinesys Research Limited t/a Havok. All Rights Reserved. The Havok + * Logo, and the Havok buzzsaw logo are trademarks of Havok. Title, ownership + * rights, and intellectual property rights in the Havok software remain in + * Havok and/or its suppliers. + * + * Use of this software for evaluation purposes is subject to and indicates + * acceptance of the End User licence Agreement for this product. A copy of + * the license is included with this software and is also available at www.havok.com/tryhavok. + * + */ diff --git a/fbx2havok/Core/main.cpp b/fbx2havok/Core/main.cpp new file mode 100644 index 0000000..4f11812 --- /dev/null +++ b/fbx2havok/Core/main.cpp @@ -0,0 +1,529 @@ +// Based on havok2fbx by Highflex + +#include "stdafx.h" +#include +#include +#include +#include +#include +#include // copy +#include // back_inserter +#include // regex, sregex_token_iterator + +// HAVOK stuff now +#include +#include +#include +#include + +#include +#include + +#include + +// Compatibility +#include + +// Scene +#include + +#include + +// Geometry +#include + +// Serialize +#include +#include +#include +#include +#include + +// Animation +#include +#include +#include +#include +#include +#include +#include +#include + +// Reflection +#include +#include +#include +#include + +// Utils +#include "hkAssetManagementUtil.h" +#include "MathHelper.h" +#include "EulerAngles.h" + +// FBX +#include +#include "FBXCommon.h" // samples common path, todo better way + +// FBX Function prototypes. +bool CreateScene(FbxManager* pSdkManager, FbxScene* pScene); // create FBX scene +FbxNode* CreateSkeleton(FbxScene* pScene, const char* pName); // create the actual skeleton +void AnimateSkeleton(FbxScene* pScene); // add animation to it +hkRootLevelContainer* ConvertHavok(FbxScene *pScene); + +int GetNodeIDByName(FbxScene *pScene, std::string NodeName); +FbxNode* GetNodeIndexByName(FbxScene* pScene, const std::string& NodeName); + +hkVector4 GetV4(FbxVector4 vec4); +hkQuaternion GetQuat(FbxQuaternion fQuat); +hkQuaternion GetQuat(FbxVector4 fQuat); +hkQuaternion GetQuat2(FbxQuaternion fQuat); +hkQuaternion GetQuat2(FbxVector4 fQuat); +hkQsTransform* ConvertTransform(FbxAMatrix* matrix); + +void PlatformInit(); +void PlatformFileSystemInit(); + +static void HK_CALL errorReport(const char* msg, void* userContext) +{ + using namespace std; + printf("%s", msg); +} + +bool file_exists(const char *fileName) +{ + std::ifstream infile(fileName); + return infile.good(); +} + +// http://stackoverflow.com/questions/6417817/easy-way-to-remove-extension-from-a-filename +std::string remove_extension(const std::string& filename) +{ + size_t lastdot = filename.find_last_of('.'); + if (lastdot == std::string::npos) return filename; + return filename.substr(0, lastdot); +} + +static void show_usage() +{ + // TODO: better versioning + std::cerr << "fbx2havok for FFXIV by perchbird\n\n" + << "Options:\n" + << "\t-h,--help\n\tShow this help message\n\n" + << "\t-hk_skeleton,--havokskeleton PATH\n\tSpecify the original Havok skeleton file\n\n" + << "\t-hk_anim,--havokanim PATH\n\tSpecify the original Havok animation file\n\n" + << "\t-fbx,--fbxfile PATH\n\tSpecify the FBX input file to convert\n\n" + << "\t-hkout,--havokout PATH\n\tSpecify the Havok output file to save\n\n" + << std::endl; +} + +// global so we can access this later +class hkLoader* loader; +class hkaSkeleton* skeleton; +class hkaAnimation** animations; +class hkaAnimationBinding** bindings; + +int numBindings; +int numAnims; + +bool bAnimationGiven = false; + +#define HK_GET_DEMOS_ASSET_FILENAME(fname) fname + +// From hkxcmd/hkfutils.cpp +//hkResult LoadDefaultRegistry() +//{ +// hkVersionPatchManager patchManager; +// { +// extern void HK_CALL CustomRegisterPatches(hkVersionPatchManager& patchManager); +// CustomRegisterPatches(patchManager); +// } +// hkDefaultClassNameRegistry &defaultRegistry = hkDefaultClassNameRegistry::getInstance(); +// { +// extern void HK_CALL CustomRegisterDefaultClasses(); +// extern void HK_CALL ValidateClassSignatures(); +// CustomRegisterDefaultClasses(); +// ValidateClassSignatures(); +// } +// return HK_SUCCESS; +//} + +int HK_CALL main(int argc, const char** argv) +{ + // user needs to specify only the input file + // if no output argument was given just assume same path as input and write file there + if (argc < 2) + { + show_usage(); + return 1; + } + + hkStringBuf havokskeleton; + hkStringBuf havokanim; + const char* havokout = nullptr; + const char* fbxfile = nullptr; + std::string havok_path_backup; + + bool bSkeletonIsValid = false; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if ((arg == "-h") || (arg == "--help")) { + show_usage(); + return 0; + } else { + // skeleton is required + if ((arg == "-hk_skeleton") || (arg == "--havokskeleton") || (arg == "-skl")) { + if (i + 1 < argc) { + // check if file is valid before going on + if (file_exists(argv[i+1])) { + bSkeletonIsValid = true; + havokskeleton = argv[i+1]; + havok_path_backup = argv[i+1]; + std::cout << "HAVOK SKELETON FILEPATH IS: " << havokskeleton << "\n"; + } else { + std::cerr << "ERROR: specified havok skeleton file doesn't exist!" << std::endl; + return 1; + } + } else { + std::cerr << "--havokskeleton option requires path argument." << std::endl; + return 1; + } + } + + if (((arg == "-hk_anim") || (arg == "--havokanim") || (arg == "-anim")) && bSkeletonIsValid) { + if (i + 1 < argc) { + // check if file is valid before going on + if(file_exists(argv[i + 1])) { + havokanim = argv[i + 1]; + std::cout << "HAVOK ANIMATION FILEPATH IS: " << havokanim << "\n"; + bAnimationGiven = true; + } else { + std::cerr << "ERROR: specified havok animation file doesn't exist!" << std::endl; + return 1; + } + } + } + + if ((arg == "-fbx") || (arg == "--fbxfile")) { + if (i + 1 < argc) { + if (file_exists(argv[i + 1])) { + fbxfile = argv[i+1]; + std::cout << "FBX FILEPATH IS: " << fbxfile << std::endl; + } else { + std::cerr << "ERROR: Must specify FBX file to read." << std::endl; + } + } else { + std::cerr << "--fbxfile option requires path argument." << std::endl; + return 1; + } + } + + if (((arg == "-hkout") || (arg == "--havokout") || (arg == "-o")) && bSkeletonIsValid) { + if (i + 1 < argc) { + havokout = argv[i + 1]; + std::cout << "HAVOK ANIMATION OUTPUT IS: " << havokout << "\n"; + } + } + } + } + + PlatformInit(); + + hkMemoryRouter* memoryRouter = hkMemoryInitUtil::initDefault( hkMallocAllocator::m_defaultMallocAllocator, hkMemorySystem::FrameInfo(1024 * 1024) ); + hkBaseSystem::init( memoryRouter, errorReport ); + + PlatformFileSystemInit(); +// LoadDefaultRegistry(); + + { + loader = new hkLoader(); + { + hkStringBuf assetFile(havokskeleton); hkAssetManagementUtil::getFilePath(assetFile); + hkRootLevelContainer* container = loader->load( HK_GET_DEMOS_ASSET_FILENAME(assetFile.cString())); +// container-> + HK_ASSERT2(0x27343437, container != HK_NULL , "Could not load asset"); + auto* ac = reinterpret_cast(container->findObjectByType( hkaAnimationContainerClass.getName())); + + HK_ASSERT2(0x27343435, ac && (ac->m_skeletons.getSize() > 0), "No skeleton loaded"); + skeleton = ac->m_skeletons[0]; + } + + { + hkStringBuf assetFile(havokanim); hkAssetManagementUtil::getFilePath(assetFile); + + hkRootLevelContainer* container = loader->load( HK_GET_DEMOS_ASSET_FILENAME(assetFile.cString())); + HK_ASSERT2(0x27343437, container != HK_NULL , "Could not load asset"); + auto* ac = reinterpret_cast(container->findObjectByType(hkaAnimationContainerClass.getName())); + + + + // We don't need these +// HK_ASSERT2(0x27343435, ac && (ac->m_animations.getSize() > 0), "No animation loaded"); +// numAnims = ac->m_animations.getSize(); +// animations = new hkaAnimation*[numAnims]; +// for (int i = 0; i < numAnims; i++) +// animations[i] = ac->m_animations[i]; + + HK_ASSERT2(0x27343435, ac && (ac->m_bindings.getSize() > 0), "No binding loaded"); + numBindings = ac->m_bindings.getSize(); + bindings = new hkaAnimationBinding*[numBindings]; + + for (int i = 0; i < numBindings; i++) + bindings[i] = ac->m_bindings[i]; + } + } + + FbxManager* lSdkManager = nullptr; + FbxScene* lScene = nullptr; + + InitializeSdkObjects(lSdkManager, lScene); + bool lResult = LoadScene(lSdkManager, lScene, fbxfile); + + if(!lResult) + { + FBXSDK_printf("\n\nAn error occurred while loading the scene...\n"); + DestroySdkObjects(lSdkManager, lResult); + return 0; + } + + const char* outfilename = nullptr; + std::string fbx_extension = ".hkx"; + std::string combined_path; + + if(fbxfile != nullptr) + { + outfilename = havokout; + } + else + { + // get havok skel path and trim the extension from it + combined_path = remove_extension(havok_path_backup) + fbx_extension; + outfilename = combined_path.c_str(); + + std::cout << "\n" << "Saving HKX to: " << outfilename << "\n"; + } + + auto root = ConvertHavok(lScene); + hkVariant vRoot = { &root, &hkRootLevelContainer::staticClass() }; + hkOstream stream(outfilename); + int flags = hkSerializeUtil::SAVE_DEFAULT; + flags |= hkSerializeUtil::SAVE_CONCISE; + + hkPackfileWriter::Options packOptions; packOptions.m_layout = hkStructureLayout::MsvcAmd64LayoutRules; +// hkTagfileWriter::Options tagOptions; tagOptions.m_layout = hkStructureLayout::MsvcAmd64LayoutRules; + +// hkResult res = hkSerializeUtil::savePackfile(root, hkRootLevelContainer::staticClass(), stream.getStreamWriter(), packOptions, HK_NULL, (hkSerializeUtil::SaveOptionBits) flags); + hkResult res = hkSerializeUtil::saveTagfile(root, hkRootLevelContainer::staticClass(), stream.getStreamWriter(), HK_NULL, (hkSerializeUtil::SaveOptionBits) flags); + + if (res.m_enum == hkResultEnum::HK_SUCCESS) { + + DestroySdkObjects(lSdkManager, lResult); + hkBaseSystem::quit(); + hkMemoryInitUtil::quit(); + printf("Done!\n"); + + return 0; + } else { + FBXSDK_printf("\n\nAn error occurred while saving the scene...\n"); + DestroySdkObjects(lSdkManager, lResult); + return 0; + } +} + +// This wouldn't be possible without figment's hkxcmd KF conversion code. +// Thanks figment! +void CreateAnimFromStack(FbxScene* pScene, FbxAnimStack* stack, int stackNum, hkaAnimationContainer* animCont) { + + hkRefPtr newBinding = new hkaAnimationBinding(); + + animCont->m_bindings.append(&newBinding, 1); + +// int numTracks = pScene->GetSrcObjectCount(); +// int numTracks = skeleton->m_bones.getSize(); + int numTracks = bindings[stackNum]->m_transformTrackToBoneIndices.getSize(); + float duration = (float) stack->GetReferenceTimeSpan().GetDuration().GetSecondDouble(); + float frametime = (1.0 / 30); //always 30fps + int numFrames = (int) (duration / frametime); + + hkRefPtr anim = new hkaInterleavedUncompressedAnimation(); + anim->m_duration = duration; + anim->m_numberOfTransformTracks = numTracks; + anim->m_numberOfFloatTracks = 0; + anim->m_transforms.setSize(numTracks * numFrames, hkQsTransform::getIdentity()); + anim->m_floats.setSize(0); +// anim->m_annotationTracks.setSize(numTracks); + + hkArray& transforms = anim->m_transforms; + + auto BoneIDContainer = new FbxNode*[numTracks]; + for (int y = 0; y < numTracks; y++) { + short currentBoneIndex = bindings[stackNum]->m_transformTrackToBoneIndices[y]; + const char *CurrentBoneName = skeleton->m_bones[currentBoneIndex].m_name; + std::string CurBoneNameString = CurrentBoneName; + BoneIDContainer[y] = GetNodeIndexByName(pScene, CurrentBoneName); + } + + auto evaluator = pScene->GetAnimationEvaluator(); + + FbxTime currentFbxTime(0); + float time = 0; + for (int frame = 0; frame < numFrames; frame++, time += frametime) { + + if (frame % 10 == 0) + cout << "."; + // per-frame init stuff + currentFbxTime.SetSecondDouble(time); + + for (int track = 0; track < numTracks; track++) { + FbxNode* node = BoneIDContainer[track]; + FbxAMatrix local = evaluator->GetNodeLocalTransform(node, currentFbxTime); + + hkQsTransform* hkLocal = ConvertTransform(&local); + const hkVector4& t = hkLocal->getTranslation(); + const hkQuaternion& r = hkLocal->getRotation(); + const hkVector4& s = hkLocal->getScale(); + + anim->m_transforms[frame * numTracks + track].set(t, r, s); + } + } + cout << "\nFrames done.\n"; + + hkaSkeletonUtils::normalizeRotations(anim->m_transforms.begin(), anim->m_transforms.getSize()); + + { +// auto tParams = new hkaSplineCompressedAnimation::TrackCompressionParams(); +// auto aParams = new hkaSplineCompressedAnimation::AnimationCompressionParams(); + + auto outAnim = anim; + //new hkaSplineCompressedAnimation(*anim); + newBinding->m_animation = outAnim; + + // copy transform track to bone indices + for (int t = 0; t < numTracks; t++) + newBinding->m_transformTrackToBoneIndices.pushBack(bindings[stackNum]->m_transformTrackToBoneIndices[t]); + newBinding->m_originalSkeletonName = bindings[stackNum]->m_originalSkeletonName; + } + + animCont->m_animations.pushBack(newBinding->m_animation); +} + +hkRootLevelContainer* ConvertHavok(FbxScene *pScene) { + + int numStacks = pScene->GetSrcObjectCount(); + + auto* rootContainer = new hkRootLevelContainer(); + auto* animContainer = new hkaAnimationContainer(); + + hkRefPtr animCont = new hkaAnimationContainer(); + + for (int i = 0; i < numStacks; i++) + CreateAnimFromStack(pScene, pScene->GetSrcObject(i), i, animContainer); + + rootContainer->m_namedVariants.pushBack(hkRootLevelContainer::NamedVariant("Merged Animation Container", animContainer, &hkaAnimationContainer::staticClass())); + + return rootContainer; +} + +// Utility to make sure we always return the right index for the given node +// Very handy for skeleton hierachy work in the FBX SDK +FbxNode* GetNodeIndexByName(FbxScene* pScene, const std::string& NodeName) +{ + // temp hacky + FbxNode* NodeToReturn = FbxNode::Create(pScene,"empty"); + + for (int i=0; i < pScene->GetNodeCount(); i++) + { + std::string CurrentNodeName = pScene->GetNode(i)->GetName(); + + if (CurrentNodeName == NodeName) + NodeToReturn = pScene->GetNode(i); + } + + return NodeToReturn; +} + +int GetNodeIDByName(FbxScene *pScene, std::string NodeName) { + int NodeNumber = 0; + + for (int i = 0; i < pScene->GetNodeCount(); i++) { + std::string CurrentNodeName = pScene->GetNode(i)->GetName(); + + if (CurrentNodeName == NodeName) { + NodeNumber = i; + } + } + + return NodeNumber; +} + +hkVector4 GetV4(FbxVector4 vec4) { + hkVector4 ret; + ret.set(vec4.mData[0], vec4.mData[1], vec4.mData[2], vec4.mData[3]); + return ret; +} + +hkQuaternion GetQuat(FbxQuaternion fQuat) { + hkQuaternion ret; + ret.m_vec.set(fQuat.mData[0], fQuat.mData[1], fQuat.mData[2], fQuat.mData[3]); + return ret; +} + +hkQuaternion GetQuat(FbxVector4 fQuat) { + hkQuaternion ret; + ret.m_vec.set(fQuat.mData[0], fQuat.mData[1], fQuat.mData[2], fQuat.mData[3]); + return ret; +} + +hkQuaternion GetQuat2(FbxQuaternion fQuat) { + hkVector4 v(fQuat.mData[0], fQuat.mData[1], fQuat.mData[2], fQuat.mData[3]); + v.normalize4(); + hkQuaternion ret(v.getSimdAt(0), v.getSimdAt(1), v.getSimdAt(2), v.getSimdAt(3)); + return ret; +} + +hkQuaternion GetQuat2(FbxVector4 fQuat) { + hkVector4 v(fQuat.mData[0], fQuat.mData[1], fQuat.mData[2], fQuat.mData[3]); + v.normalize4(); + hkQuaternion ret(v.getSimdAt(0), v.getSimdAt(1), v.getSimdAt(2), v.getSimdAt(3)); + return ret; +} + +hkQsTransform* ConvertTransform(FbxAMatrix* matrix) { + + auto ret = new hkQsTransform(hkQsTransform::IdentityInitializer::IDENTITY); + + hkVector4 t = GetV4(matrix->GetT()); + hkQuaternion r = GetQuat(matrix->GetQ()); + hkVector4 s = GetV4(matrix->GetS()); + + ret->set(t, r, s); + return ret; +} + +// [id=keycode] +#include + +// [id=productfeatures] +// We're not using anything product specific yet. We undef these so we don't get the usual +// product initialization for the products. +#undef HK_FEATURE_PRODUCT_AI +//#undef HK_FEATURE_PRODUCT_ANIMATION +#undef HK_FEATURE_PRODUCT_CLOTH +#undef HK_FEATURE_PRODUCT_DESTRUCTION_2012 +#undef HK_FEATURE_PRODUCT_DESTRUCTION +#undef HK_FEATURE_PRODUCT_BEHAVIOR +#undef HK_FEATURE_PRODUCT_PHYSICS_2012 +#undef HK_FEATURE_PRODUCT_SIMULATION +#undef HK_FEATURE_PRODUCT_PHYSICS + +// We can also restrict the compatibility to files created with the current version only using HK_SERIALIZE_MIN_COMPATIBLE_VERSION. +// If we wanted to have compatibility with at most version 650b1 we could have used something like: +// #define HK_SERIALIZE_MIN_COMPATIBLE_VERSION 650b1. +#define HK_SERIALIZE_MIN_COMPATIBLE_VERSION 201130r1 // 201130r1 can read FFXIV Havok files +//#define HK_SERIALIZE_MIN_COMPATIBLE_VERSION 201130r1 //FFXIV is compatible with 201130r1 + +#include + +// Platform specific initialization +#include diff --git a/fbx2havok/Core/stdafx.cpp b/fbx2havok/Core/stdafx.cpp new file mode 100644 index 0000000..1206d93 --- /dev/null +++ b/fbx2havok/Core/stdafx.cpp @@ -0,0 +1,8 @@ +// stdafx.cpp : source file that includes just the standard includes +// havok2fbx.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/fbx2havok/Core/stdafx.h b/fbx2havok/Core/stdafx.h new file mode 100644 index 0000000..b005a83 --- /dev/null +++ b/fbx2havok/Core/stdafx.h @@ -0,0 +1,15 @@ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#include "targetver.h" + +#include +#include + + + +// TODO: reference additional headers your program requires here diff --git a/fbx2havok/Core/targetver.h b/fbx2havok/Core/targetver.h new file mode 100644 index 0000000..87c0086 --- /dev/null +++ b/fbx2havok/Core/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/fbx2havok/Core/tofbx.cpp b/fbx2havok/Core/tofbx.cpp new file mode 100644 index 0000000..d36331e --- /dev/null +++ b/fbx2havok/Core/tofbx.cpp @@ -0,0 +1,622 @@ +// Based on havok2fbx by Highflex + +#include "stdafx.h" +#include +#include +#include +#include +#include +#include // copy +#include // back_inserter +#include // regex, sregex_token_iterator + +// HAVOK stuff now +#include +#include +#include +#include + +#include +#include + +#include + +// Compatibility +#include + +// Scene +#include + +#include + +// Geometry +#include + +// Serialize +#include +#include +#include +#include +#include + +// Animation +#include +#include +#include +#include +#include +#include +#include +#include + +// Reflection +#include +#include +#include +#include + +// Utils +#include "hkAssetManagementUtil.h" +#include "MathHelper.h" +#include "EulerAngles.h" + +// FBX +#include +#include "FBXCommon.h" // samples common path, todo better way + +// FBX Function prototypes. +bool CreateScene(FbxManager* pSdkManager, FbxScene* pScene); // create FBX scene +FbxNode* CreateSkeleton(FbxScene* pScene, const char* pName); // create the actual skeleton +void AnimateSkeleton(FbxScene* pScene); // add animation to it + +void PlatformInit(); +void PlatformFileSystemInit(); + +static void HK_CALL errorReport(const char* msg, void* userContext) +{ + using namespace std; + printf("%s", msg); +} + +bool file_exists(const char *fileName) +{ + std::ifstream infile(fileName); + return infile.good(); +} + +// http://stackoverflow.com/questions/6417817/easy-way-to-remove-extension-from-a-filename +std::string remove_extension(const std::string& filename) +{ + size_t lastdot = filename.find_last_of('.'); + if (lastdot == std::string::npos) return filename; + return filename.substr(0, lastdot); +} + +static void show_usage() +{ + // TODO: better versioning + std::cerr << "havok2fbx Version 0.1a public by Highflex 2015 (slightly modified for XIV\n\n" + << "Options:\n" + << "\t-h,--help\n\tShow this help message\n\n" + << "\t-hk_skeleton,--havokskeleton PATH\n\tSpecify the Havok skeleton file\n\n" + << "\t-hk_anim,--havokanim PATH\n\tSpecify the Havok animation file\n\n" + << "\t-fbx,--fbxfile PATH\n\tSpecify the FBX output file to save\n\n" + << std::endl; +} + +// global so we can access this later +class hkLoader* loader; +class hkaSkeleton* skeleton; +class hkaAnimation** animations; +class hkaAnimationBinding** bindings; + +int numBindings; +int numAnims; + +bool bAnimationGiven = false; + +#define HK_GET_DEMOS_ASSET_FILENAME(fname) fname + +int HK_CALL main(int argc, const char** argv) +{ + // user needs to specify only the input file + // if no output argument was given just assume same path as input and write file there + if (argc < 2) + { + show_usage(); + return 1; + } + + hkStringBuf havokskeleton; + hkStringBuf havokanim; + const char* fbxfile = nullptr; + std::string havok_path_backup; + + bool bSkeletonIsValid = false; + + for (int i = 1; i < argc; ++i) + { + std::string arg = argv[i]; + + if ((arg == "-h") || (arg == "--help")) + { + show_usage(); + return 0; + } + else + { + // skeleton is required + if ((arg == "-hk_skeleton") || (arg == "--havokskeleton") || (arg == "-skl")) + { + if (i + 1 < argc) + { + // check if file is valid before going on + if(file_exists(argv[i+1])) + { + bSkeletonIsValid = true; + havokskeleton = argv[i+1]; + havok_path_backup = argv[i+1]; + std::cout << "HAVOK SKELETON FILEPATH IS: " << havokskeleton << "\n"; + } + else + { + std::cerr << "ERROR: specified havok skeleton file doesn't exist!" << std::endl; + return 1; + } + } + else + { + std::cerr << "--havokskeleton option requires path argument." << std::endl; + return 1; + } + } + + if (((arg == "-hk_anim") || (arg == "--havokanim") || (arg == "-anim")) && bSkeletonIsValid) + { + if (i + 1 < argc) + { + // check if file is valid before going on + if(file_exists(argv[i + 1])) + { + havokanim = argv[i + 1]; + std::cout << "HAVOK ANIMATION FILEPATH IS: " << havokanim << "\n"; + bAnimationGiven = true; + } + else + { + std::cerr << "ERROR: specified havok animation file doesn't exist!" << std::endl; + return 1; + } + } + } + + if ((arg == "-fbx") || (arg == "--fbxfile")) + { + if (i + 1 < argc) + { + fbxfile = argv[i+1]; + std::cout << "FBX FILEPATH IS: " << fbxfile << "\n"; + } + else + { + std::cerr << "--fbxfile option requires path argument." << std::endl; + return 1; + } + } + } + } + + PlatformInit(); + + hkMemoryRouter* memoryRouter = hkMemoryInitUtil::initDefault( hkMallocAllocator::m_defaultMallocAllocator, hkMemorySystem::FrameInfo(1024 * 1024) ); + hkBaseSystem::init( memoryRouter, errorReport ); + + PlatformFileSystemInit(); + + { + loader = new hkLoader(); + { + hkStringBuf assetFile(havokskeleton); hkAssetManagementUtil::getFilePath(assetFile); + hkRootLevelContainer* container = loader->load( HK_GET_DEMOS_ASSET_FILENAME(assetFile.cString())); + HK_ASSERT2(0x27343437, container != HK_NULL , "Could not load asset"); + auto* ac = reinterpret_cast(container->findObjectByType( hkaAnimationContainerClass.getName())); + + HK_ASSERT2(0x27343435, ac && (ac->m_skeletons.getSize() > 0), "No skeleton loaded"); + skeleton = ac->m_skeletons[0]; + } + + // if we do not have any animation specified proceed to exporting the skeleton data otherwise use animation + // Get the animation and the binding + if (bAnimationGiven) + { + hkStringBuf assetFile(havokanim); hkAssetManagementUtil::getFilePath(assetFile); + + hkRootLevelContainer* container = loader->load( HK_GET_DEMOS_ASSET_FILENAME(assetFile.cString())); + HK_ASSERT2(0x27343437, container != HK_NULL , "Could not load asset"); + auto* ac = reinterpret_cast(container->findObjectByType(hkaAnimationContainerClass.getName())); + + HK_ASSERT2(0x27343435, ac && (ac->m_animations.getSize() > 0), "No animation loaded"); + numAnims = ac->m_animations.getSize(); + animations = new hkaAnimation*[numAnims]; + for (int i = 0; i < numAnims; i++) + animations[i] = ac->m_animations[i]; + + HK_ASSERT2(0x27343435, ac && (ac->m_bindings.getSize() > 0), "No binding loaded"); + numBindings = ac->m_bindings.getSize(); + bindings = new hkaAnimationBinding*[numBindings]; + + for (int i = 0; i < numBindings; i++) + bindings[i] = ac->m_bindings[i]; + } + // todo delete stuff after usage + } + + // after gathering havok data, write the stuff now into a FBX + // INIT FBX + FbxManager* lSdkManager = nullptr; + FbxScene* lScene = nullptr; + + InitializeSdkObjects(lSdkManager, lScene); + bool lResult = CreateScene(lSdkManager, lScene); + + if(!lResult) + { + FBXSDK_printf("\n\nAn error occurred while creating the scene...\n"); + DestroySdkObjects(lSdkManager, lResult); + return 0; + } + + // Save the scene to FBX. + // check if the user has given a FBX filename/path if not use the same location as the havok input + // The example can take an output file name as an argument. + const char* lSampleFileName = nullptr; + std::string fbx_extension = ".fbx"; + std::string combined_path; + + if(fbxfile != nullptr) + { + lSampleFileName = fbxfile; + } + else + { + // get havok skel path and trim the extension from it + combined_path = remove_extension(havok_path_backup) + fbx_extension; + lSampleFileName = combined_path.c_str(); + + std::cout << "\n" << "Saving FBX to: " << lSampleFileName << "\n"; + } + + lResult = SaveScene(lSdkManager, lScene, lSampleFileName); + + if (lResult) { + + // Destroy all objects created by the FBX SDK. + DestroySdkObjects(lSdkManager, lResult); + + // destroy objects not required + hkBaseSystem::quit(); + hkMemoryInitUtil::quit(); + + return 0; + } else { + FBXSDK_printf("\n\nAn error occurred while saving the scene...\n"); + DestroySdkObjects(lSdkManager, lResult); + return 0; + } +} + +bool CreateScene(FbxManager *pSdkManager, FbxScene* pScene) +{ + // create scene info + FbxDocumentInfo* sceneInfo = FbxDocumentInfo::Create(pSdkManager,"SceneInfo"); + sceneInfo->mTitle = "Converted Havok File"; + sceneInfo->mSubject = "A file converted from Havok formats to FBX using havok2fbx."; + sceneInfo->mAuthor = "havok2fbx"; + sceneInfo->mRevision = "rev. 1.0"; + sceneInfo->mKeywords = "havok animation"; + sceneInfo->mComment = "no particular comments required."; + + FbxAxisSystem directXAxisSys(FbxAxisSystem::EUpVector::eYAxis, FbxAxisSystem::EFrontVector::eParityEven, FbxAxisSystem::eRightHanded); + directXAxisSys.ConvertScene(pScene); + + pScene->SetSceneInfo(sceneInfo); + FbxNode* lSkeletonRoot = CreateSkeleton(pScene, "Skeleton"); + + // Build the node tree. + FbxNode* lRootNode = pScene->GetRootNode(); + lRootNode->AddChild(lSkeletonRoot); + + // Animation only if specified + if(bAnimationGiven) + AnimateSkeleton(pScene); + + return true; +} + +// Utility to make sure we always return the right index for the given node +// Very handy for skeleton hierachy work in the FBX SDK +FbxNode* GetNodeIndexByName(FbxScene* pScene, const std::string& NodeName) +{ + // temp hacky + FbxNode* NodeToReturn = FbxNode::Create(pScene,"empty"); + + for (int i=0; i < pScene->GetNodeCount(); i++) + { + std::string CurrentNodeName = pScene->GetNode(i)->GetName(); + + if (CurrentNodeName == NodeName) + NodeToReturn = pScene->GetNode(i); + } + + return NodeToReturn; +} + +int GetNodeIDByName(FbxScene* pScene, std::string NodeName); + +// Create the skeleton first +FbxNode* CreateSkeleton(FbxScene* pScene, const char* pName) +{ + // get number of bones and apply reference pose + const int numBones = skeleton->m_bones.getSize(); + + std::cout << "\nSkeleton file has been loaded!" << " Number of Bones: " << numBones << "\n"; + + // create base limb objects first + for (hkInt16 b=0; b < numBones; b++) + { + const hkaBone& bone = skeleton->m_bones[b]; + + hkQsTransform localTransform = skeleton->m_referencePose[b]; + const hkVector4& pos = localTransform.getTranslation(); + const hkQuaternion& rot = localTransform.getRotation(); + + FbxSkeleton* lSkeletonLimbNodeAttribute1 = FbxSkeleton::Create(pScene,pName); + + if(b == 0) + lSkeletonLimbNodeAttribute1->SetSkeletonType(FbxSkeleton::eRoot); + else + lSkeletonLimbNodeAttribute1->SetSkeletonType(FbxSkeleton::eLimbNode); + + lSkeletonLimbNodeAttribute1->Size.Set(1.0); + FbxNode* BaseJoint = FbxNode::Create(pScene,bone.m_name); + BaseJoint->SetNodeAttribute(lSkeletonLimbNodeAttribute1); + + // Set Translation + BaseJoint->LclTranslation.Set(FbxVector4(pos.getSimdAt(0), pos.getSimdAt(1), pos.getSimdAt(2))); + + // convert quat to euler + Quat QuatTest = {rot.m_vec.getSimdAt(0), rot.m_vec.getSimdAt(1), rot.m_vec.getSimdAt(2), rot.m_vec.getSimdAt(3)}; + EulerAngles inAngs = Eul_FromQuat(QuatTest, EulOrdXYZs); + BaseJoint->LclRotation.Set(FbxVector4(rad2deg(inAngs.x), rad2deg(inAngs.y), rad2deg(inAngs.z))); + + pScene->GetRootNode()->AddChild(BaseJoint); + } + + // process parenting and transform now + for (int c = 0; c < numBones; c++) + { + const hkInt32& parent = skeleton->m_parentIndices[c]; + + if(parent != -1) + { + const char* ParentBoneName = skeleton->m_bones[parent].m_name; + const char* CurrentBoneName = skeleton->m_bones[c].m_name; + std::string CurBoneNameString = CurrentBoneName; + std::string ParentBoneNameString = ParentBoneName; + + FbxNode* ParentJointNode = GetNodeIndexByName(pScene, ParentBoneName); + FbxNode* CurrentJointNode = GetNodeIndexByName(pScene, CurrentBoneName); + ParentJointNode->AddChild(CurrentJointNode); + } + } + + return pScene->GetRootNode(); +} + +// Create animation stack. +void AnimateSkeleton(FbxScene* pScene) { + for (int a = 0; a < numAnims; a++) { + FbxString lAnimStackName; + FbxTime lTime; + int lKeyIndex = 0; + + // First animation stack + // TODO: add support for reading in multipile havok anims into a single FBX container + lAnimStackName = "HavokAnimation"; + FbxAnimStack *lAnimStack = FbxAnimStack::Create(pScene, lAnimStackName); + + FbxAnimLayer *lAnimLayer = FbxAnimLayer::Create(pScene, "Base Layer"); + lAnimStack->AddMember(lAnimLayer); + + // havok related animation stuff now + const int numBones = skeleton->m_bones.getSize(); + + int FrameNumber = animations[a]->getNumOriginalFrames(); + int TrackNumber = animations[a]->m_numberOfTransformTracks; + int FloatNumber = animations[a]->m_numberOfFloatTracks; + + hkReal incrFrame = animations[a]->m_duration / (hkReal) (FrameNumber - 1); + + if (TrackNumber > numBones) { + FBXSDK_printf("\nERROR: Number of tracks is not equal to bones\n"); + return; + } + +// hkLocalArray floatsOut(FloatNumber); + hkLocalArray transformOut(TrackNumber); +// floatsOut.setSize(FloatNumber); + transformOut.setSize(TrackNumber); + hkReal startTime = 0.0; + + hkArray tracks; + tracks.setSize(TrackNumber); + for (int i = 0; i < TrackNumber; ++i) tracks[i] = i; + + hkReal time = startTime; + + // used to store the bone id used inside the FBX scene file + int *BoneIDContainer; + BoneIDContainer = new int[numBones]; + + // store IDs once to cut down process time + // TODO utilize for skeleton code aswell + for (int y = 0; y < numBones; y++) { + const char *CurrentBoneName = skeleton->m_bones[y].m_name; + std::string CurBoneNameString = CurrentBoneName; + BoneIDContainer[y] = GetNodeIDByName(pScene, CurrentBoneName); + +// std::cout << "\n Bone:" << CurBoneNameString << " ID: " << BoneIDContainer[y] << "\n"; + } + + auto *animatedSkeleton = new hkaAnimatedSkeleton(skeleton); + auto *control = new hkaDefaultAnimationControl(bindings[a]); + animatedSkeleton->addAnimationControl(control); + + hkaPose pose(animatedSkeleton->getSkeleton()); + + // loop through keyframes + for (int iFrame = 0; iFrame < FrameNumber; ++iFrame, time += incrFrame) { + control->setLocalTime(time); + animatedSkeleton->sampleAndCombineAnimations(pose.accessUnsyncedPoseModelSpace().begin(), pose.getFloatSlotValues().begin()); + const hkQsTransform* transforms = pose.getSyncedPoseModelSpace().begin(); + + // assume 1-to-1 transforms + // loop through animated bones + for (int i = 0; i < TrackNumber; ++i) { + FbxNode *CurrentJointNode = pScene->GetNode(BoneIDContainer[i]); + + bool pCreate = iFrame == 0; + + // Translation + FbxAnimCurve* lCurve_Trans_X = CurrentJointNode->LclTranslation.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_X, pCreate); + FbxAnimCurve* lCurve_Trans_Y = CurrentJointNode->LclTranslation.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, pCreate); + FbxAnimCurve* lCurve_Trans_Z = CurrentJointNode->LclTranslation.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, pCreate); + + // Rotation + FbxAnimCurve* lCurve_Rot_X = CurrentJointNode->LclRotation.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_X, pCreate); + FbxAnimCurve* lCurve_Rot_Y = CurrentJointNode->LclRotation.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, pCreate); + FbxAnimCurve* lCurve_Rot_Z = CurrentJointNode->LclRotation.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, pCreate); + + //Scale + FbxAnimCurve* lCurve_Scal_X = CurrentJointNode->LclScaling.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_X, pCreate); + FbxAnimCurve* lCurve_Scal_Y = CurrentJointNode->LclScaling.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, pCreate); + FbxAnimCurve* lCurve_Scal_Z = CurrentJointNode->LclScaling.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, pCreate); + + hkQsTransform transform = transforms[i]; + const hkVector4 anim_pos = transform.getTranslation(); + const hkQuaternion anim_rot = transform.getRotation(); + const hkVector4f anim_scal = transform.getScale(); + + // todo support for anything else beside 30 fps? + lTime.SetTime(0, 0, 0, iFrame, 0, 0, lTime.eFrames30); + + // Translation first + lCurve_Trans_X->KeyModifyBegin(); + lKeyIndex = lCurve_Trans_X->KeyAdd(lTime); + lCurve_Trans_X->KeySetValue(lKeyIndex, anim_pos.getSimdAt(0)); + lCurve_Trans_X->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationConstant); + lCurve_Trans_X->KeyModifyEnd(); + + lCurve_Trans_Y->KeyModifyBegin(); + lKeyIndex = lCurve_Trans_Y->KeyAdd(lTime); + lCurve_Trans_Y->KeySetValue(lKeyIndex, anim_pos.getSimdAt(1)); + lCurve_Trans_Y->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationConstant); + lCurve_Trans_Y->KeyModifyEnd(); + + lCurve_Trans_Z->KeyModifyBegin(); + lKeyIndex = lCurve_Trans_Z->KeyAdd(lTime); + lCurve_Trans_Z->KeySetValue(lKeyIndex, anim_pos.getSimdAt(2)); + lCurve_Trans_Z->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationConstant); + lCurve_Trans_Z->KeyModifyEnd(); + + // Rotation + Quat QuatRotNew = { + anim_rot.m_vec.getSimdAt(0), + anim_rot.m_vec.getSimdAt(1), + anim_rot.m_vec.getSimdAt(2), + anim_rot.m_vec.getSimdAt(3) + }; + EulerAngles inAngs_Animation = Eul_FromQuat(QuatRotNew, EulOrdXYZs); + + lCurve_Rot_X->KeyModifyBegin(); + lKeyIndex = lCurve_Rot_X->KeyAdd(lTime); + lCurve_Rot_X->KeySetValue(lKeyIndex, float(rad2deg(inAngs_Animation.x))); + lCurve_Rot_X->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationConstant); + lCurve_Rot_X->KeyModifyEnd(); + + lCurve_Rot_Y->KeyModifyBegin(); + lKeyIndex = lCurve_Rot_Y->KeyAdd(lTime); + lCurve_Rot_Y->KeySetValue(lKeyIndex, float(rad2deg(inAngs_Animation.y))); + lCurve_Rot_Y->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationConstant); + lCurve_Rot_Y->KeyModifyEnd(); + + lCurve_Rot_Z->KeyModifyBegin(); + lKeyIndex = lCurve_Rot_Z->KeyAdd(lTime); + lCurve_Rot_Z->KeySetValue(lKeyIndex, float(rad2deg(inAngs_Animation.z))); + lCurve_Rot_Z->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationConstant); + lCurve_Rot_Z->KeyModifyEnd(); + + // Scale + lCurve_Scal_X->KeyModifyBegin(); + lKeyIndex = lCurve_Scal_X->KeyAdd(lTime); + lCurve_Scal_X->KeySetValue(lKeyIndex, anim_scal.getSimdAt(0)); + lCurve_Scal_X->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationConstant); + lCurve_Scal_X->KeyModifyEnd(); + + lCurve_Scal_Y->KeyModifyBegin(); + lKeyIndex = lCurve_Scal_Y->KeyAdd(lTime); + lCurve_Scal_Y->KeySetValue(lKeyIndex, anim_scal.getSimdAt(1)); + lCurve_Scal_Y->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationConstant); + lCurve_Scal_Y->KeyModifyEnd(); + + lCurve_Scal_Z->KeyModifyBegin(); + lKeyIndex = lCurve_Scal_Z->KeyAdd(lTime); + lCurve_Scal_Z->KeySetValue(lKeyIndex, anim_scal.getSimdAt(2)); + lCurve_Scal_Z->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationConstant); + lCurve_Scal_Z->KeyModifyEnd(); + } + } + } +} + +int GetNodeIDByName(FbxScene *pScene, std::string NodeName) { + int NodeNumber = 0; + + for (int i=0; i < pScene->GetNodeCount(); i++) + { + std::string CurrentNodeName = pScene->GetNode(i)->GetName(); + + if(CurrentNodeName == NodeName) + { + NodeNumber = i; + } + } + + return NodeNumber; +} + +// [id=keycode] +#include + +// [id=productfeatures] +// We're not using anything product specific yet. We undef these so we don't get the usual +// product initialization for the products. +#undef HK_FEATURE_PRODUCT_AI +//#undef HK_FEATURE_PRODUCT_ANIMATION +#undef HK_FEATURE_PRODUCT_CLOTH +#undef HK_FEATURE_PRODUCT_DESTRUCTION_2012 +#undef HK_FEATURE_PRODUCT_DESTRUCTION +#undef HK_FEATURE_PRODUCT_BEHAVIOR +#undef HK_FEATURE_PRODUCT_PHYSICS_2012 +#undef HK_FEATURE_PRODUCT_SIMULATION +#undef HK_FEATURE_PRODUCT_PHYSICS + +// We can also restrict the compatibility to files created with the current version only using HK_SERIALIZE_MIN_COMPATIBLE_VERSION. +// If we wanted to have compatibility with at most version 650b1 we could have used something like: +// #define HK_SERIALIZE_MIN_COMPATIBLE_VERSION 650b1. +#define HK_SERIALIZE_MIN_COMPATIBLE_VERSION 201130r1 //FFXIV is compatible with 201130r1 + +#include + +// Platform specific initialization +#include diff --git a/fbx2havok/README.md b/fbx2havok/README.md new file mode 100644 index 0000000..fa348f6 --- /dev/null +++ b/fbx2havok/README.md @@ -0,0 +1,31 @@ +FBX to Havok for FFXIV +============================= + +Proof of concept allowing for conversion of Havok animations to FBX and back to Havok, facilitating custom animations in XIV. + +## To use +Grab the skeleton and animation you want from XIV and strip the XIV skeleton header, and XIV animation header and the timeline at the end if it exists. In other words, get the Havok data out of the files. +Build and use + +```tofbx.exe -hk_skeleton -hk_anim -fbx ``` + + to convert the animation and skeleton to FBX. Open the FBX file and fiddle with it however you want. + +Use + +```fbx2havok.exe -hk_skeleton -hk_anim -fbx -hkout ``` + +to convert the FBX file you edited back into a Havok animation. Import the file into FFXIV and use it, or use the Havok Preview Tool to check the animation. + +# Development +Required Libraries/Applications: +- Havok SDK 2014-1-0 +- FBX SDK 2014.2.1 +- Visual Studio 2012 for Platform Toolset V110 + +Required Environment Variables for installed libraries: +- HAVOK_SDK_ROOT +- FBX_SDK_ROOT + +# Support +No support is provided. FBX back to Havok conversion is still glitchy and proof-of-concept level, but works. Animation paths in XIV are finicky, there are certain animations that use a different path depending on if you're looking at a character taller/shorter than you. Make sure you're using the correct path when importing. On that note, you must find your own way to import files into XIV. \ No newline at end of file diff --git a/gh/banner.png b/gh/banner.png new file mode 100644 index 0000000..b33d95b Binary files /dev/null and b/gh/banner.png differ diff --git a/gh/etab.png b/gh/etab.png new file mode 100644 index 0000000..805b4a7 Binary files /dev/null and b/gh/etab.png differ diff --git a/gh/noeexp.png b/gh/noeexp.png new file mode 100644 index 0000000..a0d8b52 Binary files /dev/null and b/gh/noeexp.png differ diff --git a/gh/rtab.png b/gh/rtab.png new file mode 100644 index 0000000..25d1110 Binary files /dev/null and b/gh/rtab.png differ diff --git a/icon/icon_large.ico b/icon/icon_large.ico new file mode 100644 index 0000000..3439c9b Binary files /dev/null and b/icon/icon_large.ico differ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..b9a36ce --- /dev/null +++ b/main.cpp @@ -0,0 +1,184 @@ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "Common/Base/System/Init/PlatformInit.cxx" + +static void HK_CALL errorReport(const char* msg, void* userContext) +{ + using namespace std; + printf("%s", msg); +} + +void init() { + PlatformInit(); + hkMemoryRouter* memoryRouter = hkMemoryInitUtil::initDefault(hkMallocAllocator::m_defaultMallocAllocator, hkMemorySystem::FrameInfo(1024 * 1024)); + hkBaseSystem::init(memoryRouter, errorReport); + PlatformFileSystemInit(); + hkSerializeDeprecatedInit::initDeprecated(); +} + +inline std::string convert_from_wstring(const std::wstring& wstr) +{ + std::wstring_convert, wchar_t> conv; + return conv.to_bytes(wstr); +} + +// animassist.exe (1) in_skl_hkx out_skl_xml +// animassist.exe (2) in_edited_hk_xml out_anim_hkx +// animassist.exe (3) in_skl_hkx in_anim_hkx anim_index out_merged_hkx +// animassist.exe (4) in_anim_hkx out_anim_xml +// animassist.exe (6) in_anim_packfile out_anmim_hkx +int main(int argc, const char** argv) { + + int nargc = 0; + wchar_t** nargv; + + auto command_line = GetCommandLineW(); + if (command_line == nullptr) + { + printf("Fatal error."); + return 1; + } + nargv = CommandLineToArgvW(command_line, &nargc); + if (nargv == nullptr) + { + printf("Fatal error."); + return 1; + } + + hkStringBuf skl_hkt; + hkStringBuf anim_hkt; + int anim_index; + std::string outw; + hkStringBuf out; + hkRootLevelContainer* skl_root_container; + hkRootLevelContainer* anim_root_container; + + // 1 = skl -> xml packfile + // 2 = xml packfile of skl and anim -> binary tagfile + // 3 = skl + anim -> out hk* + // 4 = skl + anim -> xml packfile + // 5 = xml packfile of anim -> binary tagfile + int mode = _wtoi(nargv[1]); + + if (mode == 1 || mode == 2) { + skl_hkt = convert_from_wstring(nargv[2]).c_str(); + out = convert_from_wstring(nargv[3]).c_str(); + } + if (mode == 3 || mode == 6) { + skl_hkt = convert_from_wstring(nargv[2]).c_str(); + anim_hkt = convert_from_wstring(nargv[3]).c_str(); + anim_index = _wtoi(nargv[4]); + out = convert_from_wstring(nargv[5]).c_str(); + } + if (mode == 4) { + skl_hkt = convert_from_wstring(nargv[2]).c_str(); + anim_hkt = convert_from_wstring(nargv[3]).c_str(); + out = convert_from_wstring(nargv[4]).c_str(); + } + if (mode == 5) { + anim_hkt = convert_from_wstring(nargv[2]).c_str(); + out = convert_from_wstring(nargv[3]).c_str(); + } + + printf("Mode is %d\n", mode); + init(); + auto loader = new hkLoader(); + + if (mode == 1 || mode == 2 || mode == 3 || mode == 6) { + skl_root_container = loader->load(skl_hkt); + } + if (mode == 3 || mode == 4 || mode == 6) { + anim_root_container = loader->load(anim_hkt); + } + if (mode == 5) { + anim_root_container = hkSerializeUtil::loadObject(anim_hkt); + } + + hkOstream stream(out); + hkPackfileWriter::Options packOptions; + hkSerializeUtil::ErrorDetails errOut; + + auto layoutRules = hkStructureLayout::HostLayoutRules; + layoutRules.m_bytesInPointer = 8; + if (mode != 4) { + packOptions.m_layout = layoutRules; + } + + hkResult res; + if (mode == 1) { + auto* skl_container = reinterpret_cast(skl_root_container->findObjectByType(hkaAnimationContainerClass.getName())); + res = hkSerializeDeprecated::getInstance().saveXmlPackfile(skl_root_container, hkRootLevelContainer::staticClass(), stream.getStreamWriter(), packOptions, nullptr, &errOut); + } else if (mode == 2) { + auto* skl_container = reinterpret_cast(skl_root_container->findObjectByType(hkaAnimationContainerClass.getName())); + skl_container->m_skeletons.clear(); + res = hkSerializeUtil::saveTagfile(skl_root_container, hkRootLevelContainer::staticClass(), stream.getStreamWriter(), nullptr, hkSerializeUtil::SAVE_DEFAULT); + } else if (mode == 3) { + auto* skl_container = reinterpret_cast(skl_root_container->findObjectByType(hkaAnimationContainerClass.getName())); + auto anim_container = reinterpret_cast(anim_root_container->findObjectByType(hkaAnimationContainerClass.getName())); + auto anim_ptr = anim_container->m_animations[anim_index]; + auto binding_ptr = anim_container->m_bindings[anim_index]; + auto anim_ref = hkRefPtr(anim_ptr); + auto binding_ref = hkRefPtr(binding_ptr); + skl_container->m_animations.append(&anim_ref, 1); + skl_container->m_bindings.append(&binding_ref, 1); + res = hkSerializeUtil::savePackfile(skl_root_container, hkRootLevelContainer::staticClass(), stream.getStreamWriter(), packOptions, nullptr, hkSerializeUtil::SAVE_DEFAULT); + } else if (mode == 4) { + packOptions.m_writeMetaInfo = true; + packOptions.m_writeSerializedFalse = false; + res = hkSerializeDeprecated::getInstance().saveXmlPackfile(anim_root_container, hkRootLevelContainer::staticClass(), stream.getStreamWriter(), packOptions, nullptr, &errOut); + } else if (mode == 5) { + res = hkSerializeUtil::saveTagfile(anim_root_container, hkRootLevelContainer::staticClass(), stream.getStreamWriter(), nullptr, hkSerializeUtil::SAVE_DEFAULT); + } else if (mode == 6) { + auto* skl_container = reinterpret_cast(skl_root_container->findObjectByType(hkaAnimationContainerClass.getName())); + auto anim_container = reinterpret_cast(anim_root_container->findObjectByType(hkaAnimationContainerClass.getName())); + auto anim_ptr = anim_container->m_animations[anim_index]; + auto binding_ptr = anim_container->m_bindings[anim_index]; + auto anim_ref = hkRefPtr(anim_ptr); + auto binding_ref = hkRefPtr(binding_ptr); + skl_container->m_animations.append(&anim_ref, 1); + skl_container->m_bindings.append(&binding_ref, 1); + res = hkSerializeUtil::saveTagfile(skl_root_container, hkRootLevelContainer::staticClass(), stream.getStreamWriter(), nullptr, hkSerializeUtil::SAVE_DEFAULT); + } + + if (res.isSuccess()) { + // I had some cleanup here. And then Havok decided to access violate every time. + return 0; + } else { + std::cout << "\n\nAn error occurred within animassist...\n"; + return 1; + } +} + +#include + +#undef HK_FEATURE_PRODUCT_AI +//#undef HK_FEATURE_PRODUCT_ANIMATION +#undef HK_FEATURE_PRODUCT_CLOTH +#undef HK_FEATURE_PRODUCT_DESTRUCTION_2012 +#undef HK_FEATURE_PRODUCT_DESTRUCTION +#undef HK_FEATURE_PRODUCT_BEHAVIOR +#undef HK_FEATURE_PRODUCT_PHYSICS_2012 +#undef HK_FEATURE_PRODUCT_SIMULATION +#undef HK_FEATURE_PRODUCT_PHYSICS + +#define HK_SERIALIZE_MIN_COMPATIBLE_VERSION 201130r1 + +#include \ No newline at end of file diff --git a/main.h b/main.h new file mode 100644 index 0000000..331bab4 --- /dev/null +++ b/main.h @@ -0,0 +1,2 @@ + +int main(int nargc, const char** nargv); \ No newline at end of file