1 """Spells for optimizing nif files.
2
3 .. autoclass:: SpellCleanRefLists
4 :show-inheritance:
5 :members:
6
7 .. autoclass:: SpellMergeDuplicates
8 :show-inheritance:
9 :members:
10
11 .. autoclass:: SpellOptimizeGeometry
12 :show-inheritance:
13 :members:
14
15 .. autoclass:: SpellOptimize
16 :show-inheritance:
17 :members:
18
19 .. autoclass:: SpellDelUnusedBones
20 :show-inheritance:
21 :members:
22
23 """
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 from itertools import izip
65 import os.path
66
67 from pyffi.formats.nif import NifFormat
68 from pyffi.utils import unique_map
69 import pyffi.utils.tristrip
70 import pyffi.utils.vertex_cache
71 import pyffi.spells
72 import pyffi.spells.nif
73 import pyffi.spells.nif.fix
74 import pyffi.spells.nif.modify
75
76
77
78
79 _ = lambda msg: msg
80
81
82 __readonly__ = False
83
84
85 __examples__ = """* Standard usage:
86
87 python niftoaster.py optimize /path/to/copy/of/my/nifs
88
89 * Optimize, but do not merge NiMaterialProperty blocks:
90
91 python niftoaster.py optimize --exclude=NiMaterialProperty /path/to/copy/of/my/nifs
92 """
158
212
214 """Optimize all geometries:
215 - remove duplicate vertices
216 - triangulate
217 - recalculate skin partition
218 - recalculate tangent space
219 """
220
221 SPELLNAME = "opt_geometry"
222 READONLY = False
223
224
225 VERTEXPRECISION = 3
226 NORMALPRECISION = 3
227 UVPRECISION = 5
228 VCOLPRECISION = 3
229
235
245
249
260
261 - def branchentry(self, branch):
262 """Optimize a NiTriStrips or NiTriShape block:
263 - remove duplicate vertices
264 - retriangulate for vertex cache
265 - recalculate skin partition
266 - recalculate tangent space
267
268 @todo: Limit the length of strips (see operation optimization mod for
269 Oblivion!)
270 """
271 if not isinstance(branch, NifFormat.NiTriBasedGeom):
272
273 return True
274
275 if branch in self.optimized:
276
277 return False
278
279 if branch.data.additional_data:
280
281
282
283 self.toaster.msg(
284 "mesh has additional geometry data"
285 " which is not well understood: not optimizing")
286 return False
287
288
289
290
291 self.changed = True
292
293
294 if branch.data.num_vertices < 3 or branch.data.num_triangles == 0:
295 self.toaster.msg(
296 "less than 3 vertices or no triangles: removing branch")
297 self.data.replace_global_node(branch, None)
298 return False
299
300
301 data = branch.data
302
303 v_map, v_map_inverse = self.optimize_vertices(data)
304
305 new_numvertices = len(v_map_inverse)
306 self.toaster.msg("(num vertices was %i and is now %i)"
307 % (len(v_map), new_numvertices))
308
309
310
311 triangles = list(pyffi.utils.vertex_cache.get_unique_triangles(
312 (v_map[v0], v_map[v1], v_map[v2])
313 for v0, v1, v2 in data.get_triangles()))
314 old_atvr = pyffi.utils.vertex_cache.average_transform_to_vertex_ratio(
315 triangles)
316 self.toaster.msg("optimizing triangle ordering")
317 new_triangles = pyffi.utils.vertex_cache.get_cache_optimized_triangles(
318 triangles)
319 new_atvr = pyffi.utils.vertex_cache.average_transform_to_vertex_ratio(
320 new_triangles)
321 if new_atvr < old_atvr:
322 triangles = new_triangles
323 self.toaster.msg(
324 "(ATVR reduced from %.3f to %.3f)" % (old_atvr, new_atvr))
325 else:
326 self.toaster.msg(
327 "(ATVR stable at %.3f)" % old_atvr)
328
329 self.toaster.msg("optimizing vertex ordering")
330 v_map_opt = pyffi.utils.vertex_cache.get_cache_optimized_vertex_map(
331 triangles)
332 triangles = [(v_map_opt[v0], v_map_opt[v1], v_map_opt[v2])
333 for v0, v1, v2 in triangles]
334
335 for i in xrange(data.num_vertices):
336 try:
337 v_map[i] = v_map_opt[v_map[i]]
338 except IndexError:
339
340 v_map[i] = None
341 if v_map[i] is not None:
342 v_map_inverse[v_map[i]] = i
343 else:
344 self.toaster.logger.warn("unused vertex")
345 try:
346 new_numvertices = max(v for v in v_map if v is not None) + 1
347 except ValueError:
348
349
350 self.toaster.msg(
351 "less than 3 vertices or no triangles: removing branch")
352 self.data.replace_global_node(branch, None)
353 return False
354 del v_map_inverse[new_numvertices:]
355
356
357 if not isinstance(branch, NifFormat.NiTriShape):
358 self.toaster.msg("replacing branch by NiTriShape")
359 newbranch = branch.get_interchangeable_tri_shape(
360 triangles=triangles)
361 self.data.replace_global_node(branch, newbranch)
362 branch = newbranch
363 data = newbranch.data
364 else:
365 data.set_triangles(triangles)
366
367
368 oldverts = [[v.x, v.y, v.z] for v in data.vertices]
369 oldnorms = [[n.x, n.y, n.z] for n in data.normals]
370 olduvs = [[[uv.u, uv.v] for uv in uvset] for uvset in data.uv_sets]
371 oldvcols = [[c.r, c.g, c.b, c.a] for c in data.vertex_colors]
372 if branch.skin_instance:
373 oldweights = branch.get_vertex_weights()
374
375 data.num_vertices = new_numvertices
376 if data.has_vertices:
377 data.vertices.update_size()
378 for i, v in enumerate(data.vertices):
379 old_i = v_map_inverse[i]
380 v.x = oldverts[old_i][0]
381 v.y = oldverts[old_i][1]
382 v.z = oldverts[old_i][2]
383 if data.has_normals:
384 data.normals.update_size()
385 for i, n in enumerate(data.normals):
386 old_i = v_map_inverse[i]
387 n.x = oldnorms[old_i][0]
388 n.y = oldnorms[old_i][1]
389 n.z = oldnorms[old_i][2]
390
391 data.uv_sets.update_size()
392 for j, uvset in enumerate(data.uv_sets):
393 for i, uv in enumerate(uvset):
394 old_i = v_map_inverse[i]
395 uv.u = olduvs[j][old_i][0]
396 uv.v = olduvs[j][old_i][1]
397 if data.has_vertex_colors:
398 data.vertex_colors.update_size()
399 for i, c in enumerate(data.vertex_colors):
400 old_i = v_map_inverse[i]
401 c.r = oldvcols[old_i][0]
402 c.g = oldvcols[old_i][1]
403 c.b = oldvcols[old_i][2]
404 c.a = oldvcols[old_i][3]
405 del oldverts
406 del oldnorms
407 del olduvs
408 del oldvcols
409
410
411 if branch.skin_instance:
412 self.toaster.msg("update skin data vertex mapping")
413 skindata = branch.skin_instance.data
414 newweights = []
415 for i in xrange(new_numvertices):
416 newweights.append(oldweights[v_map_inverse[i]])
417 for bonenum, bonedata in enumerate(skindata.bone_list):
418 w = []
419 for i, weightlist in enumerate(newweights):
420 for bonenum_i, weight_i in weightlist:
421 if bonenum == bonenum_i:
422 w.append((i, weight_i))
423 bonedata.num_vertices = len(w)
424 bonedata.vertex_weights.update_size()
425 for j, (i, weight_i) in enumerate(w):
426 bonedata.vertex_weights[j].index = i
427 bonedata.vertex_weights[j].weight = weight_i
428
429
430 if branch.get_skin_partition():
431 self.toaster.msg("updating skin partition")
432 if isinstance(branch.skin_instance,
433 NifFormat.BSDismemberSkinInstance):
434
435 triangles, trianglepartmap = (
436 branch.skin_instance.get_dismember_partitions())
437 maximize_bone_sharing = True
438
439 new_triangles = []
440 new_trianglepartmap = []
441 for triangle, trianglepart in izip(triangles, trianglepartmap):
442 new_triangle = tuple(v_map[i] for i in triangle)
443
444
445
446
447
448 if None not in new_triangle:
449 new_triangles.append(new_triangle)
450 new_trianglepartmap.append(trianglepart)
451 triangles = new_triangles
452 trianglepartmap = new_trianglepartmap
453 else:
454
455 triangles = None
456 trianglepartmap = None
457 maximize_bone_sharing = False
458
459 branch.update_skin_partition(
460 maxbonesperpartition=18, maxbonespervertex=4,
461 stripify=False, verbose=0,
462 triangles=triangles, trianglepartmap=trianglepartmap,
463 maximize_bone_sharing=maximize_bone_sharing)
464
465
466 for morphctrl in branch.get_controllers():
467 if isinstance(morphctrl, NifFormat.NiGeomMorpherController):
468 morphdata = morphctrl.data
469
470 if not morphdata:
471 continue
472
473 self.toaster.msg("updating morphs")
474
475
476
477
478 if morphdata.num_vertices != len(v_map):
479 self.toaster.logger.warn(
480 "number of vertices in morph ({0}) does not match"
481 " number of vertices in shape ({1}):"
482 " resizing morph, graphical glitches might result"
483 .format(morphdata.num_vertices, len(v_map)))
484 morphdata.num_vertices = len(v_map)
485 for morph in morphdata.morphs:
486 morph.arg = morphdata.num_vertices
487 morph.vectors.update_size()
488
489 for morph in morphdata.morphs:
490
491 oldmorphvectors = [(vec.x, vec.y, vec.z)
492 for vec in morph.vectors]
493 for old_i, vec in izip(v_map_inverse, morph.vectors):
494 vec.x = oldmorphvectors[old_i][0]
495 vec.y = oldmorphvectors[old_i][1]
496 vec.z = oldmorphvectors[old_i][2]
497 del oldmorphvectors
498
499 morphdata.num_vertices = new_numvertices
500 for morph in morphdata.morphs:
501 morph.arg = morphdata.num_vertices
502 morph.vectors.update_size()
503
504
505 if (branch.find(block_name='Tangent space (binormal & tangent vectors)',
506 block_type=NifFormat.NiBinaryExtraData)
507 or (data.num_uv_sets & 61440)
508 or (data.bs_num_uv_sets & 61440)):
509 self.toaster.msg("recalculating tangent space")
510 branch.update_tangent_space()
511
512
513 return False
514
517 """Optimize geometry by splitting large models into pieces.
518 (This spell is not yet fully implemented!)
519 """
520 SPELLNAME = "opt_split"
521 READONLY = False
522 THRESHOLD_RADIUS = 100
523
524
525 @staticmethod
526 - def addVertex(sourceindex, v_map, sourcedata, destdata):
560
561
562 @staticmethod
563 - def addTriangle(sourcetriangle, v_map, sourcedata, destdata):
573
574
575 @staticmethod
577 """Calculate size of geometry data + given triangle."""
578 def helper(oper, coord):
579 return oper((getattr(vert, coord) for vert in triangle),
580 oper(getattr(vert, coord) for vert in vertices))
581 minx = helper(min, "x")
582 miny = helper(min, "y")
583 minz = helper(min, "z")
584 maxx = helper(max, "x")
585 maxy = helper(max, "y")
586 maxz = helper(max, "z")
587 return max((maxx - minx, maxy - miny, maxz - minz))
588
589
590 @staticmethod
636
642
645
648
649 - def branchentry(self, branch):
650 if not isinstance(branch, NifFormat.NiTriBasedGeom):
651
652 return True
653
654 if branch in self.optimized:
655
656 return False
657
658
659
660
661 geomdata = block.data
662 if not geomdata:
663 self.optimized.append(block)
664 return False
665
666 if geomdata.radius < self.THRESHOLD_RADIUS:
667 optimized_geometries.append(block)
668 return False
669
670 newblock = split(block, threshold_radius = THRESHOLD_RADIUS)
671
672 data.replace_global_node(block, newblock)
673
674 self.optimized.append(block)
675
676
677 return False
678
713
737
739 """Reduce vertices of all geometries."""
740
741 SPELLNAME = "opt_reducegeometry"
742 READONLY = False
743
744 @classmethod
745 - def toastentry(cls, toaster):
746 if not toaster.options["arg"]:
747 toaster.logger.warn(
748 "must specify degree of reduction as argument "
749 "(e.g. 2 to reduce a little, 1 to reduce more, "
750 "0 to reduce even more, -0.1 is usually the highest "
751 "level of optimization possible before significant "
752 " graphical oddities occur) to to apply spell")
753 return False
754 else:
755 precision = float(toaster.options["arg"])
756 cls.VERTEXPRECISION = precision
757 cls.NORMALPRECISION = max(precision, 0)
758 cls.UVPRECISION = max(precision, 0)
759 cls.VCOLPRECISION = max(precision, 0)
760 return True
761
763 """Optimize collision geometries by converting shapes to primitive
764 boxes where appropriate.
765 """
766 SPELLNAME = "opt_collisionbox"
767 READONLY = False
768 VERTEXPRECISION = 3
769
775
781
788
790 """Check if the given shape is has a box shape. If so, return an
791 equivalent (bhkConvexTransformShape +) bhkBoxShape.
792
793 Shape should be a bhkPackedNiTriStripsShape or a bhkNiTriStripsShape.
794 """
795 PRECISION = 100
796
797
798 if isinstance(shape, NifFormat.bhkPackedNiTriStripsShape):
799
800 if len(shape.get_sub_shapes()) != 1:
801 return None
802 vertices = shape.data.vertices
803 triangles = [
804 (hk_triangle.triangle.v_1,
805 hk_triangle.triangle.v_2,
806 hk_triangle.triangle.v_3)
807 for hk_triangle in shape.data.triangles]
808 material = shape.get_sub_shapes()[0].material
809 factor = 1.0
810 elif isinstance(shape, NifFormat.bhkNiTriStripsShape):
811 if shape.num_strips_data != 1:
812 return None
813 vertices = shape.strips_data[0].vertices
814 triangles = shape.strips_data[0].get_triangles()
815 material = shape.material
816 factor = 7.0
817
818 if len(triangles) != 12:
819 return None
820
821 unit_box = [(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1),
822 (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)]
823
824 verts = sorted(list(vert.as_tuple() for vert in vertices))
825 min_ = [min(vert[i] for vert in verts) for i in range(3)]
826 size = [max(vert[i] for vert in verts) - min_[i] for i in range(3)]
827 if any((s < 1e-10) for s in size):
828
829 return None
830 scaled_verts = sorted(
831 set(tuple(int(0.5 + PRECISION * ((vert[i] - min_[i]) / size[i]))
832 for i in range(3))
833 for vert in verts))
834
835
836 if len(scaled_verts) != 8:
837
838 return None
839 non_boxiness = sum(sum(abs(PRECISION * vert[i] - othervert[i])
840 for i in range(3))
841 for vert, othervert in zip(unit_box, scaled_verts))
842 if non_boxiness > 0:
843
844 return None
845
846 boxshape = NifFormat.bhkBoxShape()
847 boxshape.dimensions.x = size[0] / (2 * factor)
848 boxshape.dimensions.y = size[1] / (2 * factor)
849 boxshape.dimensions.z = size[2] / (2 * factor)
850 boxshape.minimum_size = min(size) / factor
851 try:
852 boxshape.material = material
853 except ValueError:
854
855 pass
856 boxshape.radius = 0.1
857 boxshape.unknown_8_bytes[0] = 0x6b
858 boxshape.unknown_8_bytes[1] = 0xee
859 boxshape.unknown_8_bytes[2] = 0x43
860 boxshape.unknown_8_bytes[3] = 0x40
861 boxshape.unknown_8_bytes[4] = 0x3a
862 boxshape.unknown_8_bytes[5] = 0xef
863 boxshape.unknown_8_bytes[6] = 0x8e
864 boxshape.unknown_8_bytes[7] = 0x3e
865
866 mid = [min_[i] + 0.5 * size[i] for i in range(3)]
867 if sum(abs(mid[i]) for i in range(3)) < 1e-6:
868
869 return boxshape
870 else:
871
872 tfshape = NifFormat.bhkConvexTransformShape()
873 tfshape.shape = boxshape
874 tfshape.material = boxshape.material
875 tfshape.transform.m_14 = mid[0] / factor
876 tfshape.transform.m_24 = mid[1] / factor
877 tfshape.transform.m_34 = mid[2] / factor
878 return tfshape
879
880 - def branchentry(self, branch):
881 """Optimize a vertex based collision block:
882 - remove duplicate vertices
883 - rebuild triangle indice and welding info
884 - update MOPP data if applicable.
885 """
886 if branch in self.optimized:
887
888 return False
889
890 if (isinstance(branch, NifFormat.bhkMoppBvTreeShape)
891 and isinstance(branch.shape, NifFormat.bhkPackedNiTriStripsShape)
892 and isinstance(branch.shape.data,
893 NifFormat.hkPackedNiTriStripsData)):
894
895 box_shape = self.get_box_shape(branch.shape)
896 if box_shape:
897
898 self.data.replace_global_node(branch, box_shape)
899 self.toaster.msg(_("optimized box collision"))
900 self.changed = True
901 self.optimized.append(branch)
902 return False
903 elif (isinstance(branch, NifFormat.bhkRigidBody)
904 and isinstance(branch.shape, NifFormat.bhkNiTriStripsShape)):
905
906 box_shape = self.get_box_shape(branch.shape)
907 if box_shape:
908
909 self.data.replace_global_node(branch.shape, box_shape)
910 self.toaster.msg(_("optimized box collision"))
911 self.changed = True
912 self.optimized.append(branch)
913
914 return False
915 elif (isinstance(branch, NifFormat.bhkRigidBody)
916 and isinstance(branch.shape,
917 NifFormat.bhkPackedNiTriStripsShape)):
918
919 box_shape = self.get_box_shape(branch.shape)
920 if box_shape:
921
922 self.data.replace_global_node(branch.shape, box_shape)
923 self.toaster.msg(_("optimized box collision"))
924 self.changed = True
925 self.optimized.append(branch)
926 return False
927
928 return True
929
931 """Optimize collision geometries by removing duplicate vertices."""
932
933 SPELLNAME = "opt_collisiongeometry"
934 READONLY = False
935 VERTEXPRECISION = 3
936
942
948
955
957 """Optimize a bhkMoppBvTreeShape."""
958 shape = mopp.shape
959 data = shape.data
960
961
962 self.toaster.msg(_("removing duplicate vertices"))
963
964
965 full_v_map = []
966 full_v_map_inverse = []
967 subshape_counts = []
968 for subshape_index in range(len(shape.get_sub_shapes())):
969 self.toaster.msg(_("(processing subshape %i)")
970 % subshape_index)
971 v_map, v_map_inverse = unique_map(
972 shape.get_vertex_hash_generator(
973 vertexprecision=self.VERTEXPRECISION,
974 subshape_index=subshape_index))
975 self.toaster.msg(
976 _("(num vertices in collision shape was %i and is now %i)")
977 % (len(v_map), len(v_map_inverse)))
978
979 subshape_counts.append(len(v_map_inverse))
980
981 num_vertices = len(full_v_map_inverse)
982 old_num_vertices = len(full_v_map)
983 full_v_map += [num_vertices + i
984 for i in v_map]
985 full_v_map_inverse += [old_num_vertices + old_i
986 for old_i in v_map_inverse]
987
988 oldverts = [[v.x, v.y, v.z] for v in data.vertices]
989
990 for subshape_index, subshape_count in enumerate(subshape_counts):
991 if shape.sub_shapes:
992
993 shape.sub_shapes[subshape_index].num_vertices = subshape_count
994 if shape.data.sub_shapes:
995
996 shape.data.sub_shapes[subshape_index].num_vertices = subshape_count
997
998 data.num_vertices = len(full_v_map_inverse)
999 data.vertices.update_size()
1000 for old_i, v in izip(full_v_map_inverse, data.vertices):
1001 v.x = oldverts[old_i][0]
1002 v.y = oldverts[old_i][1]
1003 v.z = oldverts[old_i][2]
1004 del oldverts
1005
1006 for tri in data.triangles:
1007 tri.triangle.v_1 = full_v_map[tri.triangle.v_1]
1008 tri.triangle.v_2 = full_v_map[tri.triangle.v_2]
1009 tri.triangle.v_3 = full_v_map[tri.triangle.v_3]
1010
1011
1012
1013
1014
1015 if len(shape.get_sub_shapes()) != 1:
1016 return
1017
1018
1019 self.toaster.msg(_("removing duplicate triangles"))
1020 t_map, t_map_inverse = unique_map(shape.get_triangle_hash_generator())
1021 new_numtriangles = len(t_map_inverse)
1022 self.toaster.msg(_("(num triangles in collision shape was %i and is now %i)")
1023 % (len(t_map), new_numtriangles))
1024
1025 oldtris = [[tri.triangle.v_1, tri.triangle.v_2, tri.triangle.v_3,
1026 tri.normal.x, tri.normal.y, tri.normal.z]
1027 for tri in data.triangles]
1028
1029 data.num_triangles = new_numtriangles
1030 data.triangles.update_size()
1031 for old_i, tri in izip(t_map_inverse, data.triangles):
1032 if old_i is None:
1033 continue
1034 tri.triangle.v_1 = oldtris[old_i][0]
1035 tri.triangle.v_2 = oldtris[old_i][1]
1036 tri.triangle.v_3 = oldtris[old_i][2]
1037 tri.normal.x = oldtris[old_i][3]
1038 tri.normal.y = oldtris[old_i][4]
1039 tri.normal.z = oldtris[old_i][5]
1040
1041 del oldtris
1042
1043 mopp.update_mopp_welding()
1044
1045 - def branchentry(self, branch):
1046 """Optimize a vertex based collision block:
1047 - remove duplicate vertices
1048 - rebuild triangle indices and welding info
1049 - update mopp data if applicable.
1050
1051 (note: latter two are skipped at the moment due to
1052 multimaterial bug in mopper)
1053 """
1054 if branch in self.optimized:
1055
1056 return False
1057
1058 if (isinstance(branch, NifFormat.bhkMoppBvTreeShape)
1059 and isinstance(branch.shape, NifFormat.bhkPackedNiTriStripsShape)
1060 and isinstance(branch.shape.data,
1061 NifFormat.hkPackedNiTriStripsData)):
1062
1063 self.toaster.msg(_("optimizing mopp"))
1064 self.optimize_mopp(branch)
1065 if branch.shape.data.num_vertices < 3:
1066 self.toaster.msg(_("less than 3 vertices: removing branch"))
1067 self.data.replace_global_node(branch, None)
1068 self.changed = True
1069 return False
1070 self.optimized.append(branch)
1071 self.changed = True
1072 return False
1073 elif (isinstance(branch, NifFormat.bhkRigidBody)
1074 and isinstance(branch.shape, NifFormat.bhkNiTriStripsShape)):
1075 if branch.layer == NifFormat.OblivionLayer.OL_CLUTTER:
1076
1077
1078
1079 return False
1080
1081 self.toaster.msg(_("packing collision"))
1082 new_shape = branch.shape.get_interchangeable_packed_shape()
1083 self.data.replace_global_node(branch.shape, new_shape)
1084
1085
1086 self.branchentry(branch)
1087 self.changed = True
1088
1089 return False
1090 elif (isinstance(branch, NifFormat.bhkRigidBody)
1091 and isinstance(branch.shape,
1092 NifFormat.bhkPackedNiTriStripsShape)):
1093
1094
1095 if any(sub_shape.layer != 1
1096 for sub_shape in branch.shape.get_sub_shapes()):
1097
1098 return False
1099 self.toaster.msg(_("adding mopp"))
1100 mopp = NifFormat.bhkMoppBvTreeShape()
1101 shape = branch.shape
1102 self.data.replace_global_node(branch.shape, mopp)
1103 mopp.shape = shape
1104 mopp.material = shape.get_sub_shapes()[0].material
1105 mopp.unknown_8_bytes[0] = 160
1106 mopp.unknown_8_bytes[1] = 13
1107 mopp.unknown_8_bytes[2] = 75
1108 mopp.unknown_8_bytes[3] = 1
1109 mopp.unknown_8_bytes[4] = 192
1110 mopp.unknown_8_bytes[5] = 207
1111 mopp.unknown_8_bytes[6] = 144
1112 mopp.unknown_8_bytes[7] = 11
1113 mopp.unknown_float = 1.0
1114 mopp.update_mopp_welding()
1115
1116
1117 self.branchentry(mopp)
1118 self.changed = True
1119 return False
1120
1121 return True
1122
1124 """Optimizes animations by removing duplicate keys"""
1125
1126 SPELLNAME = "opt_optimizeanimation"
1127 READONLY = False
1128
1129 @classmethod
1130 - def toastentry(cls, toaster):
1131 if not toaster.options["arg"]:
1132 cls.significance_check = 4
1133 else:
1134 cls.significance_check = float(toaster.options["arg"])
1135 return True
1136
1137
1139
1140
1141 return True
1142
1154
1156 """Helper function to optimize the keys."""
1157 new_keys = []
1158
1159
1160
1161
1162
1163 if len(keys) < 3: return keys
1164 precision = 10**self.significance_check
1165 if isinstance(keys[0].value,(float,int)):
1166 for i, key in enumerate(keys):
1167 if i == 0:
1168 new_keys.append(key)
1169 continue
1170 try:
1171 if int(precision*keys[i-1].value) != int(precision*key.value):
1172 new_keys.append(key)
1173 continue
1174 if int(precision*keys[i+1].value) != int(precision*key.value):
1175 new_keys.append(key)
1176 except IndexError:
1177 new_keys.append(key)
1178 return new_keys
1179 elif isinstance(keys[0].value,(str)):
1180 for i, key in enumerate(keys):
1181 if i == 0:
1182 new_keys.append(key)
1183 continue
1184 try:
1185 if keys[i-1].value != key.value:
1186 new_keys.append(key)
1187 continue
1188 if keys[i+1].value != key.value:
1189 new_keys.append(key)
1190 except IndexError:
1191 new_keys.append(key)
1192 return new_keys
1193 elif isinstance(keys[0].value,(NifFormat.Vector4,NifFormat.Quaternion,NifFormat.QuaternionXYZW)):
1194 tempkey = [[int(keys[0].value.w*precision),int(keys[0].value.x*precision),int(keys[0].value.y*precision),int(keys[0].value.z*precision)],[int(keys[1].value.w*precision),int(keys[1].value.x*precision),int(keys[1].value.y*precision),int(keys[1].value.z*precision)],[int(keys[2].value.w*precision),int(keys[2].value.x*precision),int(keys[2].value.y*precision),int(keys[2].value.z*precision)]]
1195 for i, key in enumerate(keys):
1196 if i == 0:
1197 new_keys.append(key)
1198 continue
1199 tempkey[0] = tempkey[1]
1200 tempkey[1] = tempkey[2]
1201 tempkey[2] = []
1202 try:
1203 tempkey[2].append(int(keys[i+1].value.w*precision))
1204 tempkey[2].append(int(keys[i+1].value.x*precision))
1205 tempkey[2].append(int(keys[i+1].value.y*precision))
1206 tempkey[2].append(int(keys[i+1].value.z*precision))
1207 except IndexError:
1208 new_keys.append(key)
1209 continue
1210 if tempkey[1] != tempkey[0]:
1211 new_keys.append(key)
1212 continue
1213 if tempkey[1] != tempkey[2]:
1214 new_keys.append(key)
1215 return new_keys
1216 elif isinstance(keys[0].value,(NifFormat.Vector3)):
1217 tempkey = [[int(keys[0].value.x*precision),int(keys[0].value.y*precision),int(keys[0].value.z*precision)],[int(keys[1].value.x*precision),int(keys[1].value.y*precision),int(keys[1].value.z*precision)],[int(keys[2].value.x*precision),int(keys[2].value.y*precision),int(keys[2].value.z*precision)]]
1218 for i, key in enumerate(keys):
1219 if i == 0:
1220 new_keys.append(key)
1221 continue
1222 tempkey[0] = tempkey[1]
1223 tempkey[1] = tempkey[2]
1224 tempkey[2] = []
1225 try:
1226 tempkey[2].append(int(keys[i+1].value.x*precision))
1227 tempkey[2].append(int(keys[i+1].value.y*precision))
1228 tempkey[2].append(int(keys[i+1].value.z*precision))
1229 except IndexError:
1230 new_keys.append(key)
1231 continue
1232 if tempkey[1] != tempkey[0]:
1233 new_keys.append(key)
1234 continue
1235 if tempkey[1] != tempkey[2]:
1236 new_keys.append(key)
1237 return new_keys
1238 else:
1239
1240 return keys
1241
1250
1252 self.toaster.msg(_("Num keys was %i and is now %i") % (len(old_keygroup),len(new_keys)))
1253 old_keygroup.update_size()
1254 for old_key, new_key in izip(old_keygroup,new_keys):
1255 old_key.time = new_key.time
1256 old_key.value = new_key.value
1257 self.changed = True
1258
1259 - def branchentry(self, branch):
1260
1261 if isinstance(branch, NifFormat.NiKeyframeData):
1262
1263 if branch.num_rotation_keys != 0:
1264 if branch.rotation_type == 4:
1265 for rotation in branch.xyz_rotations:
1266 new_keys = self.optimize_keys(rotation.keys)
1267 if len(new_keys) != rotation.num_keys:
1268 self.update_animation(rotation,new_keys)
1269 else:
1270 new_keys = self.optimize_keys(branch.quaternion_keys)
1271 if len(new_keys) != branch.num_rotation_keys:
1272 branch.num_rotation_keys = len(new_keys)
1273 self.update_animation_quaternion(branch.quaternion_keys,new_keys)
1274 if branch.translations.num_keys != 0:
1275 new_keys = self.optimize_keys(branch.translations.keys)
1276 if len(new_keys) != branch.translations.num_keys:
1277 self.update_animation(branch.translations,new_keys)
1278 if branch.scales.num_keys != 0:
1279 new_keys = self.optimize_keys(branch.scales.keys)
1280 if len(new_keys) != branch.scales.num_keys:
1281 self.update_animation(branch.scales,new_keys)
1282
1283 return False
1284 elif isinstance(branch, NifFormat.NiTextKeyExtraData):
1285 self.optimize_keys(branch.text_keys)
1286
1287 return False
1288 elif isinstance(branch, NifFormat.NiFloatData):
1289
1290
1291 return False
1292 else:
1293
1294 return True
1295
1296 -class SpellOptimize(
1297 pyffi.spells.SpellGroupSeries(
1298 pyffi.spells.nif.modify.SpellCleanFarNif,
1299 pyffi.spells.SpellGroupParallel(
1300 pyffi.spells.nif.fix.SpellDelUnusedRoots,
1301 SpellCleanRefLists,
1302 pyffi.spells.nif.fix.SpellDetachHavokTriStripsData,
1303 pyffi.spells.nif.fix.SpellFixTexturePath,
1304 pyffi.spells.nif.fix.SpellClampMaterialAlpha,
1305 pyffi.spells.nif.fix.SpellFixBhkSubShapes,
1306 pyffi.spells.nif.fix.SpellFixEmptySkeletonRoots),
1307 SpellMergeDuplicates,
1308 SpellOptimizeGeometry,
1309 SpellOptimizeCollisionBox,
1310 SpellOptimizeCollisionGeometry,
1311 )):
1312 """Global fixer and optimizer spell."""
1313 SPELLNAME = "optimize"
1314