Skip to content

Commit

Permalink
Merge pull request #485 from niftools/release/v0.0.12
Browse files Browse the repository at this point in the history
Release/v0.0.12
  • Loading branch information
neomonkeus authored Nov 20, 2021
2 parents 6b0bf04 + 0c2b744 commit acd55e3
Show file tree
Hide file tree
Showing 16 changed files with 301 additions and 256 deletions.
46 changes: 31 additions & 15 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
Version v0.0.12
==============

- #481 Anim system improvements
- allows anim export for Morrowind, Megami Tensei: Imagine
- refactors the anim system
- force import of nifs as armatures if mode is 'import skeleton only', even if the nif has no skinned geometries
- anim import class now keeps track of imported actions and provides them to the import keyframes function
- import of animated transforms now works on objects too, not just bones
- kf import no longer requires a skeleton
- Closes #479 - Add animation support + Fix Merging of Materials to Megami Tensei: Imagine
- Closes #478 - Rewrite anim import to use bind pose
- #484 Pull out nifformat_to_mathutils_matrix, improve comment
- #176 - Number of bones in a partition

Version v0.0.11
==============

- #469 Shader flags and BSLightingShaderProperty updates, and minor fixes.
- Normals are not exported when using a face tint.
- Added hair tint import/export.
- Fixed export warning for negative scales.
- Performance improvement long texture path search.
- Fixed BSLightingShaderProperty slot 6 export to agree with import.
- Changed texture export to not strip file path when not in textures folder when the file is not found, and work with relative file paths.
- Changed imported object name generation to agree with expected name from the skeleton root field.
- Fixed issue where export would error if the root object was a mesh object.
- Changed shader flag UI/transference to be dynamic, rather than using hardcoded keys.
- Changed use of is in comparison with string literals.
- Remove default=0 from collision_layer EnumProperty definition.
- Changed armature export: no longer sets pose to bind pose, and pose gets exported.
- Tangent space converter is not added when model_space_normal shader flag is present in nif.

- Closes #470

- Normals are not exported when using a face tint.
- Added hair tint import/export.
- Fixed export warning for negative scales.
- Performance improvement long texture path search.
- Fixed BSLightingShaderProperty slot 6 export to agree with import.
- Changed texture export to not strip file path when not in textures folder when the file is not found, and work with relative file paths.
- Changed imported object name generation to agree with expected name from the skeleton root field.
- Fixed issue where export would error if the root object was a mesh object.
- Changed shader flag UI/transference to be dynamic, rather than using hardcoded keys.
- Changed use of is in comparison with string literals.
- Changed armature export: no longer sets pose to bind pose, and pose gets exported.
- Tangent space converter is not added when model_space_normal shader flag is present in nif.
- Remove default=0 from collision_layer EnumProperty definition.
- Fixes #470 - Unable to import Skyrim SE NIFF on Blender 2.93.5 with version 0.0.10

Version v0.0.10
==============
Expand Down
2 changes: 1 addition & 1 deletion io_scene_niftools/VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.0.11
v0.0.12
2 changes: 1 addition & 1 deletion io_scene_niftools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"description": "Import and export files in the NetImmerse/Gamebryo formats (.nif, .kf, .egm)",
"author": "Niftools team",
"blender": (2, 82, 0),
"version": (0, 0, 11), # can't read from VERSION, blender wants it hardcoded
"version": (0, 0, 12), # can't read from VERSION, blender wants it hardcoded
"api": 39257,
"location": "File > Import-Export",
"warning": "Partially functional port from 2.49 series still in progress",
Expand Down
16 changes: 7 additions & 9 deletions io_scene_niftools/kf_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,13 @@ def execute(self):
try:
dirname = os.path.dirname(NifOp.props.filepath)
kf_files = [os.path.join(dirname, file.name) for file in NifOp.props.files if file.name.lower().endswith(".kf")]
# if an armature is present, prepare the bones for all actions
b_armature = math.get_armature()
if not b_armature:
raise NifError("No armature was found in scene, can not import KF animation!")

# 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
bind_data = armature.get_bind_data(b_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 kf_file in kf_files:
kfdata = KFFile.load_kf(kf_file)

Expand All @@ -81,7 +79,7 @@ def execute(self):
# calculate and set frames per second
self.transform_anim.set_frames_per_second(kfdata.roots)
for kf_root in kfdata.roots:
self.transform_anim.import_kf_root(kf_root, b_armature, bind_data)
self.transform_anim.import_kf_root(kf_root, b_armature)

except NifError:
return {'CANCELLED'}
Expand Down
27 changes: 12 additions & 15 deletions io_scene_niftools/modules/nif_export/animation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,9 @@
import bpy
from pyffi.formats.nif import NifFormat

import io_scene_niftools.utils.logging
from io_scene_niftools.modules.nif_export.block_registry import block_store
from io_scene_niftools.utils.singleton import NifOp, NifData
from io_scene_niftools.utils.logging import NifLog

# FPS = 30
from io_scene_niftools.utils.logging import NifLog, NifError


class Animation(ABC):
Expand Down Expand Up @@ -110,8 +107,7 @@ def get_controllers(nodes):
node_kfctrls[node].append(ctrl)
return node_kfctrls

@staticmethod
def create_controller(parent_block, target_name, priority=0):
def create_controller(self, parent_block, target_name, priority=0):
# todo[anim] - make independent of global NifData.data.version, and move check for NifOp.props.animation outside
n_kfi = None
n_kfc = None
Expand Down Expand Up @@ -170,8 +166,17 @@ def create_controller(parent_block, target_name, priority=0):
palette = controlled_block.string_palette.palette
controlled_block.node_name_offset = palette.add_string(controlled_block.node_name)
controlled_block.controller_type_offset = palette.add_string(controlled_block.controller_type)
# morrowind style
elif isinstance(parent_block, NifFormat.NiSequenceStreamHelper):
# create node reference by name
nodename_extra = block_store.create_block("NiStringExtraData")
nodename_extra.bytes_remaining = len(target_name) + 4
nodename_extra.string_data = target_name
# the controllers and extra datas form a chain down from the kf root
parent_block.add_extra_data(nodename_extra)
parent_block.add_controller(n_kfc)
else:
raise io_scene_niftools.utils.logging.NifError("Unsupported KeyframeController parent!")
raise NifError("Unsupported KeyframeController parent!")

return n_kfc, n_kfi

Expand All @@ -193,14 +198,6 @@ def add_dummy_markers(self, b_action):
# define a default animation group
NifLog.info("Checking action pose markers.")
if not b_action.pose_markers:
# has_controllers = False
# for block in block_store.block_to_obj:
# # has it a controller field?
# if isinstance(block, NifFormat.NiObjectNET):
# if block.controller:
# has_controllers = True
# break
# if has_controllers:
NifLog.info("Defining default action pose markers.")
for frame, text in zip(b_action.frame_range, ("Idle: Start/Idle: Loop Start", "Idle: Loop Stop/Idle: Stop")):
marker = b_action.pose_markers.new(text)
Expand Down
157 changes: 72 additions & 85 deletions io_scene_niftools/modules/nif_export/animation/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@

from io_scene_niftools.modules.nif_export.animation import Animation
from io_scene_niftools.modules.nif_export.block_registry import block_store
from io_scene_niftools.utils import math
from io_scene_niftools.utils.singleton import NifOp
from io_scene_niftools.utils import math, consts
from io_scene_niftools.utils.logging import NifError, NifLog


Expand All @@ -67,77 +66,57 @@ def iter_frame_key(fcurves, mathutilclass):
yield frame, mathutilclass(key)

def export_kf_root(self, b_armature=None):

"""Creates and returns a KF root block and exports controllers for objects and bones"""
scene = bpy.context.scene
# morrowind
if scene.niftools_scene.game in ('MORROWIND', 'FREEDOM_FORCE'):
# create kf root header
game = scene.niftools_scene.game
if game in ('MORROWIND', 'FREEDOM_FORCE'):
kf_root = block_store.create_block("NiSequenceStreamHelper")
# kf_root.add_extra_data(anim_textextra)
# # reparent controller tree
# for node, ctrls in node_kfctrls.items():
# for ctrl in ctrls:
# # create node reference by name
# nodename_extra = block_store.create_block("NiStringExtraData")
# nodename_extra.bytes_remaining = len(node.name) + 4
# nodename_extra.string_data = node.name

# # break the controller chain
# ctrl.next_controller = None

# # add node reference and controller
# kf_root.add_extra_data(nodename_extra)
# kf_root.add_controller(ctrl)
# # wipe controller target
# ctrl.target = None

elif scene.niftools_scene.game in (
'SKYRIM', 'OBLIVION', 'FALLOUT_3', 'CIVILIZATION_IV', 'ZOO_TYCOON_2', 'FREEDOM_FORCE_VS_THE_3RD_REICH'):

# create kf root header
elif game in (
'SKYRIM', 'OBLIVION', 'FALLOUT_3', 'CIVILIZATION_IV', 'ZOO_TYCOON_2', 'FREEDOM_FORCE_VS_THE_3RD_REICH',
'MEGAMI_TENSEI_IMAGINE'):
kf_root = block_store.create_block("NiControllerSequence")
targetname = "Scene Root"

# per-node animation
if b_armature:
b_action = self.get_active_action(b_armature)
for b_bone in b_armature.data.bones:
self.export_transforms(kf_root, b_armature, b_action, b_bone)
if scene.niftools_scene.game in ('SKYRIM', ):
targetname = "NPC Root [Root]"
else:
# 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(kf_root, b_obj, b_action)

anim_textextra = self.export_text_keys(b_action)

kf_root.name = b_action.name
kf_root.unknown_int_1 = 1
kf_root.weight = 1.0
kf_root.text_keys = anim_textextra
kf_root.cycle_type = NifFormat.CycleType.CYCLE_CLAMP
kf_root.frequency = 1.0

if anim_textextra.num_text_keys > 0:
kf_root.start_time = anim_textextra.text_keys[0].time
kf_root.stop_time = anim_textextra.text_keys[anim_textextra.num_text_keys - 1].time
else:
raise NifError(f"Keyframe export for '{game}' is not supported.")

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

# per-node animation
if b_armature:
b_action = self.get_active_action(b_armature)
for b_bone in b_armature.data.bones:
self.export_transforms(kf_root, b_armature, b_action, b_bone)
if game in ('SKYRIM',):
targetname = "NPC Root [Root]"
else:
kf_root.start_time = scene.frame_start / self.fps
kf_root.stop_time = scene.frame_end / self.fps
# 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"

kf_root.target_name = targetname
# per-object animation
else:
raise NifError(
f"Keyframe export for '{bpy.context.scene.niftools_scene.game}' is not supported.")
for b_obj in bpy.data.objects:
b_action = self.get_active_action(b_obj)
self.export_transforms(kf_root, b_obj, b_action)

self.export_text_keys(b_action, anim_textextra)

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

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

kf_root.target_name = targetname
return kf_root

def export_transforms(self, parent_block, b_obj, b_action, bone=None):
Expand All @@ -161,11 +140,11 @@ def export_transforms(self, parent_block, b_obj, b_action, bone=None):

# skeletal animation - with bone correction & coordinate corrections
if bone and bone.name in b_action.groups:
# get bind matrix for bone or object
# get bind matrix for bone
bind_matrix = math.get_object_bind(bone)
exp_fcurves = b_action.groups[bone.name].channels
# just for more detailed error reporting later on
bonestr = " in bone " + bone.name
bonestr = f" in bone {bone.name}"
target_name = block_store.get_full_name(bone)
priority = bone.niftools.priority

Expand Down Expand Up @@ -295,23 +274,20 @@ def export_transforms(self, parent_block, b_obj, b_action, bone=None):
key.time = frame / self.fps
key.value = scale

def export_text_keys(self, b_action):
"""Process b_action's pose markers and return an extra string data block."""
try:
if NifOp.props.animation == 'GEOM_NIF':
# animation group extra data is not present in geometry only files
return
except AttributeError:
# kf export has no animation mode
pass
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
n_text_extra = block_store.create_block("NiTextKeyExtraData", None)
if isinstance(kf_root, NifFormat.NiControllerSequence):
kf_root.text_keys = n_text_extra
elif isinstance(kf_root, NifFormat.NiSequenceStreamHelper):
kf_root.add_extra_data(n_text_extra)
return n_text_extra

def export_text_keys(self, b_action, n_text_extra):
"""Process b_action's pose markers and populate the extra string data block."""
NifLog.info("Exporting animation groups")

self.add_dummy_markers(b_action)

# add a NiTextKeyExtraData block
n_text_extra = block_store.create_block("NiTextKeyExtraData", b_action.pose_markers)

# create a text key for each frame descriptor
n_text_extra.num_text_keys = len(b_action.pose_markers)
n_text_extra.text_keys.update_size()
Expand All @@ -320,8 +296,19 @@ def export_text_keys(self, b_action):
f = marker.frame
if (f < f0) or (f > f1):
NifLog.warn(f"Marker out of animated range ({f} not between [{f0}, {f1}])")

key.time = f / self.fps
key.value = marker.name.replace('/', '\r\n')

return n_text_extra
def add_dummy_controllers(self):
NifLog.info("Adding controllers and interpolators for skeleton")
# note: block_store.block_to_obj changes during iteration, so need list copy
for n_block in list(block_store.block_to_obj.keys()):
if isinstance(n_block, NifFormat.NiNode) and n_block.name.decode() == "Bip01":
for n_bone in n_block.tree(block_type=NifFormat.NiNode):
n_kfc, n_kfi = self.transform_anim.create_controller(n_bone, n_bone.name.decode())
# todo [anim] use self.nif_export.animationhelper.set_flags_and_timing
n_kfc.flags = 12
n_kfc.frequency = 1.0
n_kfc.phase = 0.0
n_kfc.start_time = consts.FLOAT_MAX
n_kfc.stop_time = consts.FLOAT_MIN
Loading

0 comments on commit acd55e3

Please sign in to comment.