Skip to content

Commit acd55e3

Browse files
authored
Merge pull request #485 from niftools/release/v0.0.12
Release/v0.0.12
2 parents 6b0bf04 + 0c2b744 commit acd55e3

16 files changed

Lines changed: 301 additions & 256 deletions

File tree

CHANGELOG.rst

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
1+
Version v0.0.12
2+
==============
3+
4+
- #481 Anim system improvements
5+
- allows anim export for Morrowind, Megami Tensei: Imagine
6+
- refactors the anim system
7+
- force import of nifs as armatures if mode is 'import skeleton only', even if the nif has no skinned geometries
8+
- anim import class now keeps track of imported actions and provides them to the import keyframes function
9+
- import of animated transforms now works on objects too, not just bones
10+
- kf import no longer requires a skeleton
11+
- Closes #479 - Add animation support + Fix Merging of Materials to Megami Tensei: Imagine
12+
- Closes #478 - Rewrite anim import to use bind pose
13+
- #484 Pull out nifformat_to_mathutils_matrix, improve comment
14+
- #176 - Number of bones in a partition
15+
116
Version v0.0.11
217
==============
18+
319
- #469 Shader flags and BSLightingShaderProperty updates, and minor fixes.
4-
- Normals are not exported when using a face tint.
5-
- Added hair tint import/export.
6-
- Fixed export warning for negative scales.
7-
- Performance improvement long texture path search.
8-
- Fixed BSLightingShaderProperty slot 6 export to agree with import.
9-
- 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.
10-
- Changed imported object name generation to agree with expected name from the skeleton root field.
11-
- Fixed issue where export would error if the root object was a mesh object.
12-
- Changed shader flag UI/transference to be dynamic, rather than using hardcoded keys.
13-
- Changed use of is in comparison with string literals.
14-
- Remove default=0 from collision_layer EnumProperty definition.
15-
- Changed armature export: no longer sets pose to bind pose, and pose gets exported.
16-
- Tangent space converter is not added when model_space_normal shader flag is present in nif.
17-
18-
- Closes #470
20+
21+
- Normals are not exported when using a face tint.
22+
- Added hair tint import/export.
23+
- Fixed export warning for negative scales.
24+
- Performance improvement long texture path search.
25+
- Fixed BSLightingShaderProperty slot 6 export to agree with import.
26+
- 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.
27+
- Changed imported object name generation to agree with expected name from the skeleton root field.
28+
- Fixed issue where export would error if the root object was a mesh object.
29+
- Changed shader flag UI/transference to be dynamic, rather than using hardcoded keys.
30+
- Changed use of is in comparison with string literals.
31+
- Changed armature export: no longer sets pose to bind pose, and pose gets exported.
32+
- Tangent space converter is not added when model_space_normal shader flag is present in nif.
33+
- Remove default=0 from collision_layer EnumProperty definition.
34+
- Fixes #470 - Unable to import Skyrim SE NIFF on Blender 2.93.5 with version 0.0.10
1935

2036
Version v0.0.10
2137
==============

io_scene_niftools/VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.0.11
1+
v0.0.12

io_scene_niftools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"description": "Import and export files in the NetImmerse/Gamebryo formats (.nif, .kf, .egm)",
5050
"author": "Niftools team",
5151
"blender": (2, 82, 0),
52-
"version": (0, 0, 11), # can't read from VERSION, blender wants it hardcoded
52+
"version": (0, 0, 12), # can't read from VERSION, blender wants it hardcoded
5353
"api": 39257,
5454
"location": "File > Import-Export",
5555
"warning": "Partially functional port from 2.49 series still in progress",

io_scene_niftools/kf_import.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,13 @@ def execute(self):
6464
try:
6565
dirname = os.path.dirname(NifOp.props.filepath)
6666
kf_files = [os.path.join(dirname, file.name) for file in NifOp.props.files if file.name.lower().endswith(".kf")]
67+
# if an armature is present, prepare the bones for all actions
6768
b_armature = math.get_armature()
68-
if not b_armature:
69-
raise NifError("No armature was found in scene, can not import KF animation!")
70-
71-
# the axes used for bone correction depend on the armature in our scene
72-
math.set_bone_orientation(b_armature.data.niftools.axis_forward, b_armature.data.niftools.axis_up)
73-
74-
# get nif space bind pose of armature here for all anims
75-
bind_data = armature.get_bind_data(b_armature)
69+
if b_armature:
70+
# the axes used for bone correction depend on the armature in our scene
71+
math.set_bone_orientation(b_armature.data.niftools.axis_forward, b_armature.data.niftools.axis_up)
72+
# get nif space bind pose of armature here for all anims
73+
self.transform_anim.get_bind_data(b_armature)
7674
for kf_file in kf_files:
7775
kfdata = KFFile.load_kf(kf_file)
7876

@@ -81,7 +79,7 @@ def execute(self):
8179
# calculate and set frames per second
8280
self.transform_anim.set_frames_per_second(kfdata.roots)
8381
for kf_root in kfdata.roots:
84-
self.transform_anim.import_kf_root(kf_root, b_armature, bind_data)
82+
self.transform_anim.import_kf_root(kf_root, b_armature)
8583

8684
except NifError:
8785
return {'CANCELLED'}

io_scene_niftools/modules/nif_export/animation/__init__.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,9 @@
4141
import bpy
4242
from pyffi.formats.nif import NifFormat
4343

44-
import io_scene_niftools.utils.logging
4544
from io_scene_niftools.modules.nif_export.block_registry import block_store
4645
from io_scene_niftools.utils.singleton import NifOp, NifData
47-
from io_scene_niftools.utils.logging import NifLog
48-
49-
# FPS = 30
46+
from io_scene_niftools.utils.logging import NifLog, NifError
5047

5148

5249
class Animation(ABC):
@@ -110,8 +107,7 @@ def get_controllers(nodes):
110107
node_kfctrls[node].append(ctrl)
111108
return node_kfctrls
112109

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

176181
return n_kfc, n_kfi
177182

@@ -193,14 +198,6 @@ def add_dummy_markers(self, b_action):
193198
# define a default animation group
194199
NifLog.info("Checking action pose markers.")
195200
if not b_action.pose_markers:
196-
# has_controllers = False
197-
# for block in block_store.block_to_obj:
198-
# # has it a controller field?
199-
# if isinstance(block, NifFormat.NiObjectNET):
200-
# if block.controller:
201-
# has_controllers = True
202-
# break
203-
# if has_controllers:
204201
NifLog.info("Defining default action pose markers.")
205202
for frame, text in zip(b_action.frame_range, ("Idle: Start/Idle: Loop Start", "Idle: Loop Stop/Idle: Stop")):
206203
marker = b_action.pose_markers.new(text)

io_scene_niftools/modules/nif_export/animation/transform.py

Lines changed: 72 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@
4444

4545
from io_scene_niftools.modules.nif_export.animation import Animation
4646
from io_scene_niftools.modules.nif_export.block_registry import block_store
47-
from io_scene_niftools.utils import math
48-
from io_scene_niftools.utils.singleton import NifOp
47+
from io_scene_niftools.utils import math, consts
4948
from io_scene_niftools.utils.logging import NifError, NifLog
5049

5150

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

6968
def export_kf_root(self, b_armature=None):
70-
69+
"""Creates and returns a KF root block and exports controllers for objects and bones"""
7170
scene = bpy.context.scene
72-
# morrowind
73-
if scene.niftools_scene.game in ('MORROWIND', 'FREEDOM_FORCE'):
74-
# create kf root header
71+
game = scene.niftools_scene.game
72+
if game in ('MORROWIND', 'FREEDOM_FORCE'):
7573
kf_root = block_store.create_block("NiSequenceStreamHelper")
76-
# kf_root.add_extra_data(anim_textextra)
77-
# # reparent controller tree
78-
# for node, ctrls in node_kfctrls.items():
79-
# for ctrl in ctrls:
80-
# # create node reference by name
81-
# nodename_extra = block_store.create_block("NiStringExtraData")
82-
# nodename_extra.bytes_remaining = len(node.name) + 4
83-
# nodename_extra.string_data = node.name
84-
85-
# # break the controller chain
86-
# ctrl.next_controller = None
87-
88-
# # add node reference and controller
89-
# kf_root.add_extra_data(nodename_extra)
90-
# kf_root.add_controller(ctrl)
91-
# # wipe controller target
92-
# ctrl.target = None
93-
94-
elif scene.niftools_scene.game in (
95-
'SKYRIM', 'OBLIVION', 'FALLOUT_3', 'CIVILIZATION_IV', 'ZOO_TYCOON_2', 'FREEDOM_FORCE_VS_THE_3RD_REICH'):
96-
97-
# create kf root header
74+
elif game in (
75+
'SKYRIM', 'OBLIVION', 'FALLOUT_3', 'CIVILIZATION_IV', 'ZOO_TYCOON_2', 'FREEDOM_FORCE_VS_THE_3RD_REICH',
76+
'MEGAMI_TENSEI_IMAGINE'):
9877
kf_root = block_store.create_block("NiControllerSequence")
99-
targetname = "Scene Root"
100-
101-
# per-node animation
102-
if b_armature:
103-
b_action = self.get_active_action(b_armature)
104-
for b_bone in b_armature.data.bones:
105-
self.export_transforms(kf_root, b_armature, b_action, b_bone)
106-
if scene.niftools_scene.game in ('SKYRIM', ):
107-
targetname = "NPC Root [Root]"
108-
else:
109-
# quick hack to set correct target name
110-
if "Bip01" in b_armature.data.bones:
111-
targetname = "Bip01"
112-
elif "Bip02" in b_armature.data.bones:
113-
targetname = "Bip02"
114-
115-
# per-object animation
116-
else:
117-
for b_obj in bpy.data.objects:
118-
b_action = self.get_active_action(b_obj)
119-
self.export_transforms(kf_root, b_obj, b_action)
120-
121-
anim_textextra = self.export_text_keys(b_action)
122-
123-
kf_root.name = b_action.name
124-
kf_root.unknown_int_1 = 1
125-
kf_root.weight = 1.0
126-
kf_root.text_keys = anim_textextra
127-
kf_root.cycle_type = NifFormat.CycleType.CYCLE_CLAMP
128-
kf_root.frequency = 1.0
129-
130-
if anim_textextra.num_text_keys > 0:
131-
kf_root.start_time = anim_textextra.text_keys[0].time
132-
kf_root.stop_time = anim_textextra.text_keys[anim_textextra.num_text_keys - 1].time
78+
else:
79+
raise NifError(f"Keyframe export for '{game}' is not supported.")
80+
81+
anim_textextra = self.create_text_keys(kf_root)
82+
targetname = "Scene Root"
83+
84+
# per-node animation
85+
if b_armature:
86+
b_action = self.get_active_action(b_armature)
87+
for b_bone in b_armature.data.bones:
88+
self.export_transforms(kf_root, b_armature, b_action, b_bone)
89+
if game in ('SKYRIM',):
90+
targetname = "NPC Root [Root]"
13391
else:
134-
kf_root.start_time = scene.frame_start / self.fps
135-
kf_root.stop_time = scene.frame_end / self.fps
92+
# quick hack to set correct target name
93+
if "Bip01" in b_armature.data.bones:
94+
targetname = "Bip01"
95+
elif "Bip02" in b_armature.data.bones:
96+
targetname = "Bip02"
13697

137-
kf_root.target_name = targetname
98+
# per-object animation
13899
else:
139-
raise NifError(
140-
f"Keyframe export for '{bpy.context.scene.niftools_scene.game}' is not supported.")
100+
for b_obj in bpy.data.objects:
101+
b_action = self.get_active_action(b_obj)
102+
self.export_transforms(kf_root, b_obj, b_action)
103+
104+
self.export_text_keys(b_action, anim_textextra)
105+
106+
kf_root.name = b_action.name
107+
kf_root.unknown_int_1 = 1
108+
kf_root.weight = 1.0
109+
kf_root.cycle_type = NifFormat.CycleType.CYCLE_CLAMP
110+
kf_root.frequency = 1.0
111+
112+
if anim_textextra.num_text_keys > 0:
113+
kf_root.start_time = anim_textextra.text_keys[0].time
114+
kf_root.stop_time = anim_textextra.text_keys[anim_textextra.num_text_keys - 1].time
115+
else:
116+
kf_root.start_time = scene.frame_start / self.fps
117+
kf_root.stop_time = scene.frame_end / self.fps
118+
119+
kf_root.target_name = targetname
141120
return kf_root
142121

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

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

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

298-
def export_text_keys(self, b_action):
299-
"""Process b_action's pose markers and return an extra string data block."""
300-
try:
301-
if NifOp.props.animation == 'GEOM_NIF':
302-
# animation group extra data is not present in geometry only files
303-
return
304-
except AttributeError:
305-
# kf export has no animation mode
306-
pass
277+
def create_text_keys(self, kf_root):
278+
"""Create the text keys before filling in the data so that the extra data hierarchy is correct"""
279+
# add a NiTextKeyExtraData block
280+
n_text_extra = block_store.create_block("NiTextKeyExtraData", None)
281+
if isinstance(kf_root, NifFormat.NiControllerSequence):
282+
kf_root.text_keys = n_text_extra
283+
elif isinstance(kf_root, NifFormat.NiSequenceStreamHelper):
284+
kf_root.add_extra_data(n_text_extra)
285+
return n_text_extra
307286

287+
def export_text_keys(self, b_action, n_text_extra):
288+
"""Process b_action's pose markers and populate the extra string data block."""
308289
NifLog.info("Exporting animation groups")
309-
310290
self.add_dummy_markers(b_action)
311-
312-
# add a NiTextKeyExtraData block
313-
n_text_extra = block_store.create_block("NiTextKeyExtraData", b_action.pose_markers)
314-
315291
# create a text key for each frame descriptor
316292
n_text_extra.num_text_keys = len(b_action.pose_markers)
317293
n_text_extra.text_keys.update_size()
@@ -320,8 +296,19 @@ def export_text_keys(self, b_action):
320296
f = marker.frame
321297
if (f < f0) or (f > f1):
322298
NifLog.warn(f"Marker out of animated range ({f} not between [{f0}, {f1}])")
323-
324299
key.time = f / self.fps
325300
key.value = marker.name.replace('/', '\r\n')
326301

327-
return n_text_extra
302+
def add_dummy_controllers(self):
303+
NifLog.info("Adding controllers and interpolators for skeleton")
304+
# note: block_store.block_to_obj changes during iteration, so need list copy
305+
for n_block in list(block_store.block_to_obj.keys()):
306+
if isinstance(n_block, NifFormat.NiNode) and n_block.name.decode() == "Bip01":
307+
for n_bone in n_block.tree(block_type=NifFormat.NiNode):
308+
n_kfc, n_kfi = self.transform_anim.create_controller(n_bone, n_bone.name.decode())
309+
# todo [anim] use self.nif_export.animationhelper.set_flags_and_timing
310+
n_kfc.flags = 12
311+
n_kfc.frequency = 1.0
312+
n_kfc.phase = 0.0
313+
n_kfc.start_time = consts.FLOAT_MAX
314+
n_kfc.stop_time = consts.FLOAT_MIN

0 commit comments

Comments
 (0)