Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for KFA files import/export (DAOC) #558

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions io_scene_niftools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@
# Blender addon info.
bl_info = {
"name": "NetImmerse/Gamebryo format support",
"description": "Import and export files in the NetImmerse/Gamebryo formats (.nif, .kf, .egm)",
"description": "Import and export files in the NetImmerse/Gamebryo formats (.nif, .kf, .kfa, .egm)",
"author": "Niftools team",
"blender": (2, 82, 0),
"version": (0, 0, 14), # can't read from VERSION, blender wants it hardcoded
"version": (0, 0, 15), # can't read from VERSION, blender wants it hardcoded
"api": 39257,
"location": "File > Import-Export",
"warning": "Generally stable port of the Niftool's Blender NifScripts, many improvements, still work in progress",
Expand Down
118 changes: 118 additions & 0 deletions io_scene_niftools/kfa_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""This script imports Netimmerse/Gamebryo nif files to Blender."""

# ***** BEGIN LICENSE BLOCK *****
#
# Copyright © 2019, NIF File Format Library and Tools contributors.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# * Neither the name of the NIF File Format Library and Tools
# project nor the names of its contributors may be used to endorse
# or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# ***** END LICENSE BLOCK *****

import os
import bpy

import pyffi.spells.nif.fix

from io_scene_niftools.file_io.kf import KFFile
from io_scene_niftools.modules.nif_export import armature
from io_scene_niftools.modules.nif_export.animation.transform import TransformAnimation
from io_scene_niftools.nif_common import NifCommon
from io_scene_niftools.utils import math
from io_scene_niftools.utils.singleton import NifOp, NifData
from io_scene_niftools.utils.logging import NifLog, NifError
from io_scene_niftools.modules.nif_export import scene
from io_scene_niftools.modules.nif_export.block_registry import block_store


class KfaExport(NifCommon):

def __init__(self, operator, context):
NifCommon.__init__(self, operator, context)

# Helper systems
self.transform_anim = TransformAnimation()

def execute(self):
"""Main export function."""

NifLog.info(f"Exporting {NifOp.props.filepath}")

# extract directory, base name, extension
directory = os.path.dirname(NifOp.props.filepath)
filebase, fileext = os.path.splitext(os.path.basename(NifOp.props.filepath))

if bpy.context.scene.niftools_scene.game == 'NONE':
raise NifError("You have not selected a game. Please select a game in the scene tab.")

prefix = ""
self.version, data = scene.get_version_data()
NifData.init(data)

b_armature = math.get_armature()
# some scenes may not have an armature, so nothing to do here
if b_armature:
math.set_bone_orientation(b_armature.data.niftools.axis_forward, b_armature.data.niftools.axis_up)

NifLog.info("Creating keyframe tree")
kfa_root = self.transform_anim.export_kfa_root(b_armature)

# write kfa
ext = ".kfa"
NifLog.info(f"Writing {prefix}{ext} file")

data.roots = []
# first NiNode
data.roots.append(kfa_root)

# remaining NiNodes : corresponding to first bone position computed via NiKeyframeController
kfc = kfa_root.controller

while kfc != None:
node_root = block_store.create_block("NiNode")
# TODO : rotation
# node_root.rotation = compute from kfc.data.quaternion_keys[0].value
node_root.translation = kfc.data.translations.keys[0].value*NifOp.props.scale_correction
# scale to improve
node_root.scale = 1.0
data.roots.append(node_root)
kfc = kfc.next_controller

# scale correction for the skeleton
self.apply_scale(data, round(1 / NifOp.props.scale_correction))

kfafile = os.path.join(directory, prefix + filebase + ext)
with open(kfafile, "wb") as stream:
data.write(stream)

NifLog.info("Finished successfully")
return {'FINISHED'}

91 changes: 91 additions & 0 deletions io_scene_niftools/kfa_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""This script imports Netimmerse/Gamebryo nif files to Blender."""

# ***** BEGIN LICENSE BLOCK *****
#
# Copyright © 2019, NIF File Format Library and Tools contributors.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# * Neither the name of the NIF File Format Library and Tools
# project nor the names of its contributors may be used to endorse
# or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# ***** END LICENSE BLOCK *****

import os

import pyffi.spells.nif.fix

from io_scene_niftools.file_io.kf import KFFile
from io_scene_niftools.modules.nif_export import armature
from io_scene_niftools.modules.nif_import.animation.transform import TransformAnimation
from io_scene_niftools.nif_common import NifCommon
from io_scene_niftools.utils import math
from io_scene_niftools.utils.singleton import NifOp
from io_scene_niftools.utils.logging import NifLog, NifError


class KfaImport(NifCommon):

def __init__(self, operator, context):
NifCommon.__init__(self, operator, context)

# Helper systems
self.transform_anim = TransformAnimation()

def execute(self):
"""Main import function."""

try:
dirname = os.path.dirname(NifOp.props.filepath)
kfa_files = [os.path.join(dirname, file.name) for file in NifOp.props.files if file.name.lower().endswith(".kfa")]
# if an armature is present, prepare the bones for all actions
b_armature = math.get_armature()
if b_armature:
# the axes used for bone correction depend on the armature in our scene
math.set_bone_orientation(b_armature.data.niftools.axis_forward, b_armature.data.niftools.axis_up)
# get nif space bind pose of armature here for all anims
self.transform_anim.get_bind_data(b_armature)
for kfa_file in kfa_files:
kfadata = KFFile.load_kf(kfa_file)

self.apply_scale(kfadata, NifOp.props.scale_correction)

# calculate and set frames per second
self.transform_anim.set_frames_per_second(kfadata.roots)
# verify if NiNodes are present
if len(kfadata.roots)>0 :
kfa_root=kfadata.roots[0]
# no usage identified for others NiNode, so taking care only of the first one
self.transform_anim.import_kfa_root(kfa_root, b_armature)

except NifError:
return {'CANCELLED'}

NifLog.info("Finished successfully")
return {'FINISHED'}
75 changes: 74 additions & 1 deletion io_scene_niftools/modules/nif_export/animation/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@
from io_scene_niftools.modules.nif_export.block_registry import block_store
from io_scene_niftools.utils import math, consts
from io_scene_niftools.utils.logging import NifError, NifLog
from io_scene_niftools.utils.bones_mapper import BonesMapper

from io_scene_niftools.modules.nif_export import block_registry

class TransformAnimation(Animation):

Expand Down Expand Up @@ -119,6 +121,75 @@ def export_kf_root(self, b_armature=None):
kf_root.target_name = targetname
return kf_root


def export_kfa_root(self, b_armature=None):
"""Creates and returns a KFA root block and exports controllers for objects and bones"""
scene = bpy.context.scene
game = scene.niftools_scene.game
kfa_root = block_store.create_block("NiNode")

anim_textextra = self.create_text_keys(kfa_root)
targetname = "Scene Root"

# mapping between bones name and id
bonename_dict = BonesMapper.bonename_dict

# per-node animation
if b_armature:
b_action = self.get_active_action(b_armature)
# generate array of NiStringExtraData to indice the bones list used
extraDataList = []
data_nb=0
for b_bone in b_armature.data.bones:
# get bone name
bone_name = block_registry.ExportBlockRegistry.get_bone_name_for_nif(b_bone.name)
# retrieve the bone id from the bone name
bone_id = bonename_dict.get(bone_name)

if bone_id != None:
if (b_bone and b_bone.name in b_action.groups) or (not b_bone) :
# NiStringExtraData corresponding to bone
extraData = block_store.create_block("NiStringExtraData")
extraData.name="NiStringED"+'%03d'%data_nb
extraData.string_data = str(bone_id)
extraDataList.append(extraData)
# Create and add the NiKeyframeController corresponding to the bone in the chained list
self.export_transforms(kfa_root, b_armature, b_action, b_bone)
data_nb = data_nb +1

kfa_root.set_extra_datas(extraDataList)

# quick hack to set correct target name
if "Bip01" in b_armature.data.bones:
targetname = "Bip01"
elif "Bip02" in b_armature.data.bones:
targetname = "Bip02"

# per-object animation
else:
for b_obj in bpy.data.objects:
b_action = self.get_active_action(b_obj)
self.export_transforms(kfa_root, b_obj, b_action)

#self.export_text_keys(b_action, anim_textextra)

kfa_root.name = b_action.name
kfa_root.unknown_int_1 = 1
kfa_root.weight = 1.0
kfa_root.cycle_type = NifFormat.CycleType.CYCLE_CLAMP
kfa_root.frequency = 1.0

if anim_textextra.num_text_keys > 0:
kfa_root.start_time = anim_textextra.text_keys[0].time
kfa_root.stop_time = anim_textextra.text_keys[anim_textextra.num_text_keys - 1].time
else:
kfa_root.start_time = scene.frame_start / self.fps
kfa_root.stop_time = scene.frame_end / self.fps

kfa_root.target_name = targetname
return kfa_root


def export_transforms(self, parent_block, b_obj, b_action, bone=None):
"""
If bone == None, object level animation is exported.
Expand Down Expand Up @@ -174,6 +245,7 @@ def export_transforms(self, parent_block, b_obj, b_action, bone=None):

# decompose the bind matrix
bind_scale, bind_rot, bind_trans = math.decompose_srt(bind_matrix)

n_kfc, n_kfi = self.create_controller(parent_block, target_name, priority)

# fill in the non-trivial values
Expand Down Expand Up @@ -273,7 +345,8 @@ def export_transforms(self, parent_block, b_obj, b_action, bone=None):
for key, (frame, scale) in zip(n_kfd.scales.keys, scale_curve):
key.time = frame / self.fps
key.value = scale



def create_text_keys(self, kf_root):
"""Create the text keys before filling in the data so that the extra data hierarchy is correct"""
# add a NiTextKeyExtraData block
Expand Down
50 changes: 49 additions & 1 deletion io_scene_niftools/modules/nif_import/animation/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from io_scene_niftools.utils import math
from io_scene_niftools.utils.blocks import safe_decode
from io_scene_niftools.utils.logging import NifLog

from io_scene_niftools.utils.bones_mapper import BonesMapper

def interpolate(x_out, x_in, y_in):
"""
Expand Down Expand Up @@ -174,6 +174,54 @@ def import_controller_sequence(self, kf_root, b_armature_obj):
extend = self.get_extend_from_cycle_type(kf_root.cycle_type)
self.set_extrapolation(extend, b_action.fcurves)

def import_kfa_root(self, kf_root, b_armature_obj):

# extract from figures/animnode.dat
bone_names = BonesMapper.bone_names

b_action_name = self.import_generic_kf_root(kf_root)
actions = set()

curr_controller = kf_root.controller

k = 1
while curr_controller != None :

# retrieve the corresponding bone
j = 1
b_name = ""

# first format : bone references are in an array
if len(kf_root.extra_data_list) > 0 :
bone_ref = kf_root.extra_data_list[k-1]
b_name = bone_names[int(bone_ref.string_data)]

# second format : bone references are in chained list
else :
bone_ref = kf_root.extra_data
match_k = 1
while ( bone_ref != None and match_k != k ) :
bone_ref = bone_ref.next_extra_data
match_k = match_k + 1
b_name = bone_names[int(bone_ref.string_data)]

data=curr_controller.data

# retrieve bone from blender armature
b_target = self.get_target(b_armature_obj, b_name)

# retrieve action root name
b_action_name = self.import_generic_kf_root(kf_root)
actions.add(self.import_keyframe_controller(curr_controller, b_armature_obj, b_target, b_action_name))

curr_controller = curr_controller.next_controller
k = k +1

for b_action in actions:
if b_action:
self.import_text_keys(kf_root, b_action)


def import_keyframe_controller(self, n_kfc, b_armature, b_target, b_action_name):
"""
Imports a keyframe controller as fcurves in an action, which is created if necessary.
Expand Down
Loading