Skip to content

Commit 67e0f92

Browse files
authored
Merge pull request #506 from HENDRIX-ZT2/develop
Speedup anim import & various other fixes
2 parents 273b8f8 + 6aaae94 commit 67e0f92

26 files changed

Lines changed: 736 additions & 651 deletions

File tree

docs/user/features/geometry/geometry.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,15 @@ Vertex Color & Alpha
9494
* `This image should clarify per-face vertex colouring
9595
<http://i211.photobucket.com/albums/bb189/NifTools/Blender/documentation/per_face_vertex_color.jpg>`_
9696
* On export, the scripts will create extra vertices for different vertex colors per face.
97+
98+
99+
.. _geometry-shapekeys:
100+
101+
Shape Key Animations
102+
--------------------
103+
104+
**Example:**
105+
106+
#. :ref:`Create a mesh-object <geometry-mesh>`.
107+
#. Add relative shape keys to your mesh.
108+
#. Keyframe each shape key's value so that the key influences the shape of the mesh at the desired time.

io_scene_niftools/modules/nif_export/animation/morph.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ def __init__(self):
5555
EGMData.data = None
5656

5757
def export_morph(self, b_mesh, n_trishape, vertmap):
58-
# shape b_key morphing
58+
NifLog.debug(f"Checking {b_mesh.name} for shape keys")
59+
# shape keys are only present on non-evaluated meshes!
5960
b_key = b_mesh.shape_keys
6061
if b_key and len(b_key.key_blocks) > 1:
61-
62+
NifLog.debug(f"{b_mesh.name} has shape keys")
6263
# yes, there is a b_key object attached
6364
# export as egm, or as morph_data?
6465
if b_key.key_blocks[1].name.startswith("EGM"):

io_scene_niftools/modules/nif_export/animation/transform.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from io_scene_niftools.modules.nif_export.block_registry import block_store
4747
from io_scene_niftools.utils import math, consts
4848
from io_scene_niftools.utils.logging import NifError, NifLog
49+
from io_scene_niftools.utils.consts import QUAT, EULER, LOC, SCALE
4950

5051

5152
class TransformAnimation(Animation):
@@ -166,7 +167,7 @@ def export_transforms(self, parent_block, b_obj, b_action, bone=None):
166167
# matrix_local = matrix_parent_inverse * matrix_basis
167168
bind_matrix = b_obj.matrix_parent_inverse
168169
exp_fcurves = [fcu for fcu in b_action.fcurves if
169-
fcu.data_path in ("rotation_quaternion", "rotation_euler", "location", "scale")]
170+
fcu.data_path in (QUAT, EULER, LOC, SCALE)]
170171

171172
else:
172173
# bone isn't keyframed in this action, nothing to do here

io_scene_niftools/modules/nif_export/geometry/mesh/__init__.py

Lines changed: 171 additions & 178 deletions
Large diffs are not rendered by default.

io_scene_niftools/modules/nif_export/object/__init__.py

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -97,35 +97,27 @@ def get_export_objects(self, only_selected=True):
9797
def export_root_node(self, root_objects, filebase):
9898
""" Exports a nif's root node; use blender root if there is only one, else create a meta root """
9999
# TODO [collsion] detect root collision -> root collision node (can be mesh or empty)
100-
# self.nif_export.collisionhelper.export_collision(b_obj, n_parent)
100+
# self.export_collision(b_obj, n_parent)
101101
# return None # done; stop here
102102
self.n_root = None
103+
node_type = bpy.context.scene.niftools_scene.rootnode
103104
# there is only one root object so that will be our final root
104105
if len(root_objects) == 1:
105106
b_obj = root_objects[0]
106-
self.export_node(b_obj, None)
107+
self.export_node(b_obj, None, n_node_type=node_type)
107108

108109
# there is more than one root object so we create a meta root
109110
else:
110-
NifLog.info("Created meta root because blender scene had multiple root objects")
111-
self.n_root = types.create_ninode()
111+
NifLog.info(f"Created meta root because blender scene had {len(root_objects)} root objects")
112+
self.n_root = types.create_ninode(n_node_type=node_type)
112113
self.n_root.name = "Scene Root"
113114
for b_obj in root_objects:
114115
self.export_node(b_obj, self.n_root)
115116

116-
# TODO [object] How dow we know we are selecting the right node in the case of multi-root?
117-
# making root block a fade node
118-
root_type = b_obj.niftools.rootnode
119-
if bpy.context.scene.niftools_scene.game in ('FALLOUT_3', 'SKYRIM') and root_type == 'BSFadeNode':
120-
NifLog.info("Making root block a BSFadeNode")
121-
fade_root_block = NifFormat.BSFadeNode().deepcopy(self.n_root)
122-
fade_root_block.replace_global_node(self.n_root, fade_root_block)
123-
self.n_root = fade_root_block
124-
125117
# various extra datas
126118
object_property = ObjectDataProperty()
127119
object_property.export_bsxflags_upb(self.n_root, root_objects)
128-
object_property.export_inventory_marker(self.n_root, root_objects)
120+
object_property.export_inventory_marker(self.n_root, b_obj)
129121
object_property.export_weapon_location(self.n_root, b_obj)
130122
types.export_furniture_marker(self.n_root, filebase)
131123
return self.n_root
@@ -147,7 +139,7 @@ def set_node_flags(self, b_obj, n_node):
147139
else:
148140
n_node.flags = 0x000C # morrowind
149141

150-
def export_node(self, b_obj, n_parent):
142+
def export_node(self, b_obj, n_parent, n_node_type=None):
151143
"""Export a mesh/armature/empty object b_obj as child of n_parent.
152144
Export also all children of b_obj.
153145
@@ -190,7 +182,7 @@ def export_node(self, b_obj, n_parent):
190182
b_action = False
191183

192184
# -> everything else (empty/armature) is a (more or less regular) node
193-
node = types.create_ninode(b_obj)
185+
node = types.create_ninode(b_obj, n_node_type=n_node_type)
194186
# set parenting here so that it can be accessed
195187
if not self.n_root:
196188
self.n_root = node
@@ -207,32 +199,31 @@ def export_node(self, b_obj, n_parent):
207199
# export object animation
208200
self.transform_anim.export_transforms(node, b_obj, b_action)
209201
self.object_anim.export_visibility(node, b_action)
210-
# if it is a mesh, export the mesh as trishape children of this ninode
202+
# if it is a mesh, export the mesh as n_geom children of this ninode
211203
if b_obj.type == 'MESH':
212204
return self.mesh_helper.export_tri_shapes(b_obj, node, self.n_root)
213205
# if it is an armature, export the bones as ninode children of this ninode
214206
elif b_obj.type == 'ARMATURE':
215207
self.armaturehelper.export_bones(b_obj, node)
216-
217-
# export all children of this b_obj as children of this NiNode
218-
self.export_children(b_obj, node)
208+
# special case: objects parented to armature bones
209+
for b_child in b_obj.children:
210+
# find and attach to the right node
211+
if b_child.parent_bone:
212+
b_obj_bone = b_obj.data.bones[b_child.parent_bone]
213+
# find the correct n_node
214+
# todo [object] this is essentially the same as Mesh.get_bone_block()
215+
n_node = [k for k, v in block_store.block_to_obj.items() if v == b_obj_bone][0]
216+
self.export_node(b_child, n_node)
217+
# just child of the armature itself, so attach to armature root
218+
else:
219+
self.export_node(b_child, node)
220+
else:
221+
# export all children of this empty object as children of this node
222+
for b_child in b_obj.children:
223+
self.export_node(b_child, node)
219224

220225
return node
221226

222-
def export_children(self, b_parent, n_parent):
223-
"""Export all children of blender object b_parent as children of n_parent."""
224-
# loop over all obj's children
225-
for b_child in b_parent.children:
226-
temp_parent = n_parent
227-
# special case: objects parented to armature bones - find the nif parent bone
228-
if b_parent.type == 'ARMATURE' and b_child.parent_bone != "":
229-
parent_bone = b_parent.data.bones[b_child.parent_bone]
230-
assert (parent_bone in block_store.block_to_obj.values())
231-
for temp_parent, obj in block_store.block_to_obj.items():
232-
if obj == parent_bone:
233-
break
234-
self.export_node(b_child, temp_parent)
235-
236227
def export_collision(self, b_obj, n_parent):
237228
"""Main function for adding collision object b_obj to a node.
238229
Returns True if this object is exported as a collision"""
@@ -259,7 +250,7 @@ def export_collision(self, b_obj, n_parent):
259250
else:
260251
# all nodes failed so add new one
261252
node = types.create_ninode(b_obj)
262-
node.name = 'collisiondummy{:d}'.format(n_parent.num_children)
253+
node.name = f'collisiondummy{n_parent.num_children}'
263254
if b_obj.niftools.flags != 0:
264255
node_flag_hex = hex(b_obj.niftools.flags)
265256
else:
@@ -273,4 +264,4 @@ def export_collision(self, b_obj, n_parent):
273264
else:
274265
NifLog.warn(f"Collisions not supported for game '{bpy.context.scene.niftools_scene.game}', skipped collision object '{b_obj.name}'")
275266

276-
return True
267+
return True

io_scene_niftools/modules/nif_export/property/object/__init__.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -220,21 +220,19 @@ def has_collision():
220220

221221
# TODO [object][property] Move to object property
222222
@staticmethod
223-
def export_inventory_marker(n_root, root_objects):
224-
if bpy.context.scene.niftools_scene.game in ('SKYRIM',):
225-
for root_object in root_objects:
226-
if root_object.niftools_bs_invmarker:
227-
for extra_item in n_root.extra_data_list:
228-
if isinstance(extra_item, NifFormat.BSInvMarker):
229-
raise NifError("Multiple Items have Inventory marker data only one item may contain this data")
230-
else:
231-
n_extra_list = NifFormat.BSInvMarker()
232-
n_extra_list.name = root_object.niftools_bs_invmarker[0].name.encode()
233-
n_extra_list.rotation_x = (-root_object.niftools_bs_invmarker[0].bs_inv_x % (2 * pi)) * 1000
234-
n_extra_list.rotation_y = (-root_object.niftools_bs_invmarker[0].bs_inv_y % (2 * pi)) * 1000
235-
n_extra_list.rotation_z = (-root_object.niftools_bs_invmarker[0].bs_inv_z % (2 * pi)) * 1000
236-
n_extra_list.zoom = root_object.niftools_bs_invmarker[0].bs_inv_zoom
237-
n_root.add_extra_data(n_extra_list)
223+
def export_inventory_marker(n_root, b_obj):
224+
"""Attaches a BSInvMarker to n_root if desired and fill in its values"""
225+
niftools_scene = bpy.context.scene.niftools_scene
226+
bs_inv_store = b_obj.niftools.bs_inv
227+
if niftools_scene.game in ('SKYRIM',) and bs_inv_store:
228+
bs_inv = bs_inv_store[0]
229+
n_bs_inv_marker = NifFormat.BSInvMarker()
230+
n_bs_inv_marker.name = bs_inv.name.encode()
231+
n_bs_inv_marker.rotation_x = (-bs_inv.x % (2 * pi)) * 1000
232+
n_bs_inv_marker.rotation_y = (-bs_inv.y % (2 * pi)) * 1000
233+
n_bs_inv_marker.rotation_z = (-bs_inv.z % (2 * pi)) * 1000
234+
n_bs_inv_marker.zoom = bs_inv.zoom
235+
n_root.add_extra_data(n_bs_inv_marker)
238236

239237
# TODO [object][property] Move to new object type
240238
def export_weapon_location(self, n_root, root_obj):

io_scene_niftools/modules/nif_export/types.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,22 @@
4545
from io_scene_niftools.utils.singleton import NifOp
4646

4747

48-
def create_ninode(b_obj=None):
48+
def create_ninode(b_obj=None, n_node_type=None):
4949
"""Essentially a wrapper around create_block() that creates nodes of the right type"""
50-
# when no b_obj is passed, it means we create a root node
50+
# when no b_obj is passed, use the passed n_node_type
5151
if not b_obj:
52-
return block_store.create_block("NiNode")
53-
52+
if n_node_type is None:
53+
n_node_type = "NiNode"
5454
# get node type - some are stored as custom property of the b_obj
55-
try:
56-
n_node_type = b_obj["type"]
57-
except KeyError:
58-
n_node_type = "NiNode"
59-
60-
# ...others by presence of constraints
61-
if has_track(b_obj):
62-
n_node_type = "NiBillboardNode"
55+
else:
56+
try:
57+
n_node_type = b_obj["type"]
58+
except KeyError:
59+
n_node_type = "NiNode"
60+
61+
# ...others by presence of constraints
62+
if has_track(b_obj):
63+
n_node_type = "NiBillboardNode"
6364

6465
# now create the node
6566
n_node = block_store.create_block(n_node_type, b_obj)

io_scene_niftools/modules/nif_import/animation/__init__.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from pyffi.formats.nif import NifFormat
4242

4343
from io_scene_niftools.utils.logging import NifLog
44+
from io_scene_niftools.utils.consts import QUAT, EULER, LOC, SCALE
4445

4546

4647
class Animation:
@@ -54,6 +55,23 @@ def __init__(self):
5455
# and still be able to access existing actions from this run
5556
self.actions = {}
5657

58+
@staticmethod
59+
def get_controller_data(ctrl):
60+
"""Return data for ctrl, look in interpolator (for newer games) or directly on ctrl"""
61+
if ctrl.interpolator:
62+
data = ctrl.interpolator.data
63+
else:
64+
data = ctrl.data
65+
# these have their data set as a KeyGroup on data
66+
if isinstance(data, (NifFormat.NiBoolData, NifFormat.NiFloatData, NifFormat.NiPosData)):
67+
return data.data
68+
return data
69+
70+
@staticmethod
71+
def get_keys_values(items):
72+
"""Returns list of times and keys for an array 'items' with key elements having 'time' and 'value' attributes"""
73+
return [key.time for key in items], [key.value for key in items]
74+
5775
@staticmethod
5876
def show_pose_markers():
5977
"""Helper function to ensure that pose markers are shown"""
@@ -89,21 +107,21 @@ def create_action(self, b_obj, action_name):
89107
b_obj.animation_data.action = b_action
90108
return b_action
91109

92-
def create_fcurves(self, action, dtype, drange, flags=None, bonename=None, keyname=None):
110+
def create_fcurves(self, action, dtype, drange, flags, bone_name, key_name):
93111
""" Create fcurves in action for desired conditions. """
94112
# armature pose bone animation
95-
if bonename:
113+
if bone_name:
96114
fcurves = [
97-
action.fcurves.new(data_path=f'pose.bones["{bonename}"].{dtype}', index=i, action_group=bonename)
115+
action.fcurves.new(data_path=f'pose.bones["{bone_name}"].{dtype}', index=i, action_group=bone_name)
98116
for i in drange]
99117
# shapekey pose bone animation
100-
elif keyname:
118+
elif key_name:
101119
fcurves = [
102-
action.fcurves.new(data_path=f'key_blocks["{keyname}"].{dtype}', index=0,)
120+
action.fcurves.new(data_path=f'key_blocks["{key_name}"].{dtype}', index=0,)
103121
]
104122
else:
105123
# Object animation (non-skeletal) is lumped into the "LocRotScale" action_group
106-
if dtype in ("rotation_euler", "rotation_quaternion", "location", "scale"):
124+
if dtype in (QUAT, EULER, LOC, SCALE):
107125
action_group = "LocRotScale"
108126
# Non-transformaing animations (eg. visibility or material anims) use no action groups
109127
else:
@@ -141,13 +159,34 @@ def set_extrapolation(extend_type, fcurves):
141159
for fcurve in fcurves:
142160
fcurve.extrapolation = 'CONSTANT'
143161

144-
def add_key(self, fcurves, t, key, interp):
162+
def add_keys(self, b_action, key_type, key_range, flags, times, keys, interp, bone_name=None, key_name=None):
145163
"""
146-
Add a key (len=n) to a set of fcurves (len=n) at the given frame. Set the key's interpolation to interp.
164+
Create needed fcurves and add a list of keys to an action.
147165
"""
148-
frame = round(t * self.fps)
149-
for fcurve, k in zip(fcurves, key):
150-
fcurve.keyframe_points.insert(frame, k).interpolation = interp
166+
samples = [round(t * self.fps) for t in times]
167+
assert len(samples) == len(keys)
168+
# get interpolation enum representation
169+
ipo = bpy.types.Keyframe.bl_rna.properties['interpolation'].enum_items[interp].value
170+
interpolations = [ipo for _ in range(len(samples))]
171+
# import the keys
172+
try:
173+
fcurves = self.create_fcurves(b_action, key_type, key_range, flags, bone_name, key_name)
174+
if len(key_range) == 1:
175+
# flat key - make it zippable
176+
key_per_fcurve = [keys]
177+
else:
178+
key_per_fcurve = zip(*keys)
179+
for fcurve, fcu_keys in zip(fcurves, key_per_fcurve):
180+
# add new points
181+
fcurve.keyframe_points.add(count=len(fcu_keys))
182+
# populate points with keys for this curve
183+
fcurve.keyframe_points.foreach_set("co", [x for co in zip(samples, fcu_keys) for x in co])
184+
fcurve.keyframe_points.foreach_set("interpolation", interpolations)
185+
# update
186+
fcurve.update()
187+
except RuntimeError:
188+
# blender throws F-Curve ... already exists in action ...
189+
NifLog.warn(f"Could not add fcurve '{key_type}' to '{b_action.name}', already added before?")
151190

152191
# import animation groups
153192
def import_text_keys(self, n_block, b_action):
@@ -212,3 +251,4 @@ def set_frames_per_second(self, roots):
212251
self.fps = fps
213252
bpy.context.scene.render.fps = fps
214253
bpy.context.scene.frame_set(0)
254+

0 commit comments

Comments
 (0)