1 """Module which contains all spells that check something in a nif file."""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 from __future__ import with_statement
43 from contextlib import closing
44 from itertools import izip, repeat
45 import tempfile
46
47 from pyffi.formats.nif import NifFormat
48 import pyffi.spells.nif
49 import pyffi.utils.tristrip
52 """Like the original read-write spell, but with additional file size
53 check."""
54
55 SPELLNAME = "check_readwrite"
56
58 """Only process nifs if they have all admissible block types.
59 Note that the default rule is to process a nif if it has at
60 least one admissible block type, but for read write spells it
61 makes more sense to impose all.
62 """
63 return all(self.toaster.is_admissible_branch_class(header_type)
64 for header_type in self.header_types)
65
66 - def dataentry(self):
67 self.toaster.msgblockbegin("writing to temporary file")
68
69 f_tmp = tempfile.TemporaryFile()
70 try:
71 self.data.write(f_tmp)
72
73
74
75 self.toaster.msg("comparing file sizes")
76 self.stream.seek(0, 2)
77 f_tmp.seek(0, 2)
78 if self.stream.tell() != f_tmp.tell():
79 self.toaster.msg("original size: %i" % self.stream.tell())
80 self.toaster.msg("written size: %i" % f_tmp.tell())
81 f_tmp.seek(0)
82 f_debug = open("debug.nif", "wb")
83 f_debug.write(f_tmp.read(-1))
84 f_debug.close()
85 raise Exception('write check failed: file sizes differ (written file saved as debug.nif for inspection)')
86 finally:
87 f_tmp.close()
88
89 self.toaster.msgblockend()
90
91
92 return False
93
95 """This spell goes over all nif files, and at the end, it gives a summary
96 of which node names where used with particular flags."""
97
98 SPELLNAME = "check_nodenamesbyflag"
99
100 @classmethod
101 - def toastentry(cls, toaster):
102 toaster.flagdict = {}
103 return True
104
105 @classmethod
109
112
116
117 - def branchentry(self, branch):
118 if isinstance(branch, NifFormat.NiAVObject):
119 if not branch.flags in self.toaster.flagdict:
120 self.toaster.flagdict[branch.flags] = []
121 if not branch.name in self.toaster.flagdict[branch.flags]:
122 self.toaster.flagdict[branch.flags].append(branch.name)
123 return True
124 else:
125 return False
126
128 """This spell compares skinning data with a reference nif."""
129
130 SPELLNAME = "check_compareskindata"
131
132
133
134 @staticmethod
139
140 @staticmethod
142 return (max([max([abs(x-y)
143 for (x,y) in izip(oldrow, newrow)])
144 for (oldrow, newrow) in izip(oldmat.as_list(),
145 newmat.as_list())])
146 < tolerance)
147
148 @staticmethod
150 return abs(oldfloat - newfloat) < tolerance
151
152 @classmethod
153 - def toastentry(cls, toaster):
154 """Read reference nif file given as argument."""
155
156 if not toaster.options.get("arg"):
157 return False
158
159 toaster.refdata = NifFormat.Data()
160 with closing(open(toaster.options["arg"], "rb")) as reffile:
161 toaster.refdata.read(reffile)
162
163 toaster.refbonedata = []
164 for refgeom in toaster.refdata.get_global_iterator():
165 if (isinstance(refgeom, NifFormat.NiGeometry)
166 and refgeom.skin_instance and refgeom.skin_instance.data):
167 toaster.refbonedata += zip(
168 repeat(refgeom.skin_instance.skeleton_root),
169 repeat(refgeom.skin_instance.data),
170 refgeom.skin_instance.bones,
171 refgeom.skin_instance.data.bone_list)
172
173 return bool(toaster.refbonedata)
174
177
181
182 - def branchentry(self, branch):
183 if (isinstance(branch, NifFormat.NiGeometry)
184 and branch.skin_instance and branch.skin_instance.data):
185 for skelroot, skeldata, bonenode, bonedata in izip(
186 repeat(branch.skin_instance.skeleton_root),
187 repeat(branch.skin_instance.data),
188 branch.skin_instance.bones,
189 branch.skin_instance.data.bone_list):
190 for refskelroot, refskeldata, refbonenode, refbonedata \
191 in self.toaster.refbonedata:
192 if bonenode.name == refbonenode.name:
193 self.toaster.msgblockbegin("checking bone %s"
194 % bonenode.name)
195
196
197 if skelroot.name == refskelroot.name:
198
199 branchtransform_extra = NifFormat.Matrix44()
200 branchtransform_extra.set_identity()
201 else:
202 self.toaster.msg(
203 "skipping: skeleton roots are not identical")
204 self.toaster.msgblockend()
205 continue
206
207
208
209
210
211
212
213 for refskelroot_branch \
214 in self.toaster.refdata.get_global_iterator():
215 if not isinstance(refskelroot_branch,
216 NifFormat.NiAVObject):
217 continue
218 if skelroot.name == refskelroot_branch.name:
219
220
221
222 branchtransform_extra = \
223 refskelroot_branch.get_transform(refskelroot).get_inverse()
224 break
225 else:
226 for skelroot_ref \
227 in self.data.get_global_iterator():
228 if not isinstance(skelroot_ref,
229 NifFormat.NiAVObject):
230 continue
231 if refskelroot.name == skelroot_ref.name:
232
233
234
235 branchtransform_extra = \
236 skelroot_ref.get_transform(skelroot)
237 break
238 else:
239 self.toaster.msgblockbegin("""\
240 skipping: skeleton roots are not identical
241 and no alternative found""")
242 self.toaster.msgblockend()
243 continue
244
245
246
247
248 reftransform = (
249 refbonedata.get_transform()
250 * refbonenode.get_transform(refskelroot)
251 * refskeldata.get_transform())
252
253
254
255 branchtransform = (
256 bonedata.get_transform()
257 * refbonenode.get_transform(refskelroot)
258 * skeldata.get_transform()
259 * branchtransform_extra)
260
261 if not self.are_matrices_equal(reftransform,
262 branchtransform):
263
264 self.toaster.msg(
265 "transform mismatch\n%s\n!=\n%s\n"
266 % (reftransform, branchtransform))
267
268 self.toaster.msgblockend()
269
270 return False
271 else:
272
273 return True
274
275 -class SpellCheckBhkBodyCenter(pyffi.spells.nif.NifSpell):
276 """Recalculate the center of mass and inertia matrix,
277 compare them to the originals, and report accordingly.
278 """
279
280 SPELLNAME = "check_bhkbodycenter"
281
282 - def datainspect(self):
284
285 - def branchinspect(self, branch):
289
290 - def branchentry(self, branch):
291 if not isinstance(branch, NifFormat.bhkRigidBody):
292
293 return True
294 else:
295 self.toaster.msg("getting rigid body mass, center, and inertia")
296 mass = branch.mass
297 center = branch.center.get_copy()
298 inertia = branch.inertia.get_copy()
299
300 self.toaster.msg("recalculating...")
301 branch.update_mass_center_inertia(mass=branch.mass)
302
303
304
305
306
307
308
309
310
311
312
313
314
315 self.toaster.msg("checking center...")
316 if center != branch.center:
317
318 self.toaster.logger.warn(
319 "center does not match; original %s, calculated %s"
320 % (center, branch.center))
321
322 self.toaster.msg("checking inertia...")
323
324 scale = max(max(abs(x) for x in row) for row in inertia.as_list() + branch.inertia.as_list())
325 if (max(max(abs(x - y)
326 for x, y in zip(row1, row2))
327 for row1, row2 in zip(inertia.as_list(), branch.inertia.as_list()))
328 > 0.1 * scale):
329
330 self.toaster.logger.warn(
331 "inertia does not match:\n\noriginal\n%s\n\ncalculated\n%s\n"
332 % (inertia, branch.inertia))
333
334 return False
335
337 """Recalculate the center and radius, compare them to the originals,
338 and report mismatches.
339 """
340
341
342
343
344
345
346
347
348 SPELLNAME = "check_centerradius"
349
352
357
358 - def branchentry(self, branch):
359 if not isinstance(branch, NifFormat.NiGeometryData):
360
361 return True
362 else:
363 self.toaster.msg("getting bounding sphere")
364 center = NifFormat.Vector3()
365 center.x = branch.center.x
366 center.y = branch.center.y
367 center.z = branch.center.z
368 radius = branch.radius
369
370 self.toaster.msg("checking that all vertices are inside")
371 maxr = 0.0
372 maxv = None
373 for vert in branch.vertices:
374 dist = vert - center
375 if dist * dist > maxr:
376 maxr = dist * dist
377 maxv = vert
378 maxr = maxr ** 0.5
379
380 if maxr > 1.01 * radius + 0.01:
381
382 self.toaster.logger.warn(
383 "not all vertices inside bounding sphere (vertex %s, error %s)"
384 % (maxv, abs(maxr - radius)))
385
386 self.toaster.msg("recalculating bounding sphere")
387 branch.update_center_radius()
388
389 self.toaster.msg("comparing old and new spheres")
390 if center != branch.center:
391 self.toaster.logger.warn(
392 "center does not match; original %s, calculated %s"
393 % (center, branch.center))
394 if abs(radius - branch.radius) > NifFormat.EPSILON:
395 self.toaster.logger.warn(
396 "radius does not match; original %s, calculated %s"
397 % (radius, branch.radius))
398
399 return False
400
402 """Recalculate the skindata center and radius for each bone, compare them
403 to the originals, and report mismatches.
404 """
405
406 SPELLNAME = "check_skincenterradius"
407
410
414
415 - def branchentry(self, branch):
416 if not(isinstance(branch, NifFormat.NiGeometry) and branch.is_skin()):
417
418 return True
419 else:
420 self.toaster.msg("getting skindata block bounding spheres")
421 center = []
422 radius = []
423 for skindatablock in branch.skin_instance.data.bone_list:
424 center.append(skindatablock.bounding_sphere_offset.get_copy())
425 radius.append(skindatablock.bounding_sphere_radius)
426
427 self.toaster.msg("recalculating bounding spheres")
428 branch.update_skin_center_radius()
429
430 self.toaster.msg("comparing old and new spheres")
431 for i, skindatablock in enumerate(branch.skin_instance.data.bone_list):
432 if center[i] != skindatablock.bounding_sphere_offset:
433 self.toaster.logger.error(
434 "%s center does not match; original %s, calculated %s"
435 % (branch.skin_instance.bones[i].name,
436 center[i], skindatablock.bounding_sphere_offset))
437 if abs(radius[i] - skindatablock.bounding_sphere_radius) \
438 > NifFormat.EPSILON:
439 self.toaster.logger.error(
440 "%s radius does not match; original %s, calculated %s"
441 % (branch.skin_instance.bones[i].name,
442 radius[i], skindatablock.bounding_sphere_radius))
443
444 return False
445
447 """This test checks whether each vertex is the intersection of at least
448 three planes.
449 """
450 SPELLNAME = "check_convexverticesshape"
451
454
459
460 - def branchentry(self, branch):
461 if not isinstance(branch, NifFormat.bhkConvexVerticesShape):
462
463 return True
464 else:
465 self.toaster.msg("checking vertices and planes")
466 for v4 in branch.vertices:
467 v = NifFormat.Vector3()
468 v.x = v4.x
469 v.y = v4.y
470 v.z = v4.z
471 num_intersect = 0
472 for n4 in branch.normals:
473 n = NifFormat.Vector3()
474 n.x = n4.x
475 n.y = n4.y
476 n.z = n4.z
477 d = n4.w
478 if abs(v * n + d) < 0.01:
479 num_intersect += 1
480 if num_intersect == 0:
481 self.toaster.logger.error(
482 "vertex %s does not intersect with any plane" % v)
483 elif num_intersect == 1:
484 self.toaster.logger.warn(
485 "vertex %s only intersects with one plane" % v)
486 elif num_intersect == 2:
487 self.toaster.logger.warn(
488 "vertex %s only intersects with two planes" % v)
489
490 return False
491
493 """Parse and dump mopp trees, and check their validity:
494
495 * do they have correct origin and scale?
496 * do they refer to every triangle exactly once?
497 * does the parser visit every byte exactly once?
498
499 Mainly useful to check the heuristic parser and for debugging mopp codes.
500 """
501 SPELLNAME = "check_mopp"
502
505
510
511 - def branchentry(self, branch):
512 if not isinstance(branch, NifFormat.bhkMoppBvTreeShape):
513
514 return True
515 else:
516 mopp = [b for b in branch.mopp_data]
517 o = NifFormat.Vector3()
518 o.x = branch.origin.x
519 o.y = branch.origin.y
520 o.z = branch.origin.z
521 scale = branch.scale
522
523 self.toaster.msg("recalculating mopp origin and scale")
524 branch.update_origin_scale()
525
526 if branch.origin != o:
527 self.toaster.logger.warn("origin mismatch")
528 self.toaster.logger.warn("(was %s and is now %s)"
529 % (o, branch.origin))
530 if abs(branch.scale - scale) > 0.5:
531 self.toaster.logger.warn("scale mismatch")
532 self.toaster.logger.warn("(was %s and is now %s)"
533 % (scale, branch.scale))
534
535 self.toaster.msg("parsing mopp")
536
537 ids, tris = branch.parse_mopp(verbose=True)
538
539 error = False
540
541
542 counts = [tris.count(i) for i in xrange(branch.shape.data.num_triangles)]
543 missing = [i for i in xrange(branch.shape.data.num_triangles)
544 if counts[i] != 1]
545 if missing:
546 self.toaster.logger.error(
547 "some triangles never visited, or visited more than once")
548 self.toaster.logger.debug(
549 "triangles index, times visited")
550 for i in missing:
551 self.toaster.logger.debug(i, counts[i])
552 error = True
553
554 wrong = [i for i in tris if i > branch.shape.data.num_triangles]
555 if wrong:
556 self.toaster.logger.error("invalid triangle indices")
557 self.toaster.logger.debug(wrong)
558 error = True
559
560
561 counts = [ids.count(i) for i in xrange(branch.mopp_data_size)]
562 missing = [i for i in xrange(branch.mopp_data_size) if counts[i] != 1]
563 if missing:
564 self.toaster.logger.error(
565 "some bytes never visited, or visited more than once")
566 self.toaster.logger.debug(
567 "byte index, times visited, value")
568 for i in missing:
569 self.toaster.logger.debug(i, counts[i], "0x%02X" % mopp[i])
570 self.toaster.logger.debug([mopp[k] for k in xrange(i, min(branch.mopp_data_size, i + 10))])
571 error = True
572
573
574
575
576
577 return False
578
580 """Check and recalculate the tangent space, compare them to the originals,
581 and report accordingly.
582 """
583 SPELLNAME = 'check_tangentspace'
584 PRECISION = 0.3
585
588
591
592 - def branchentry(self, branch):
593 if not isinstance(branch, NifFormat.NiTriBasedGeom):
594
595 return True
596 else:
597
598 tangentspace = branch.get_tangent_space()
599 if not tangentspace:
600
601 return False
602 self.toaster.msg("checking tangent space")
603 oldspace = []
604 for i, (n, t, b) in enumerate(tangentspace):
605 oldspace.append(n.as_list() + t.as_list() + b.as_list())
606 if abs(n * n - 1) > NifFormat.EPSILON:
607 self.toaster.logger.warn(
608 'non-unit normal %s (norm %f) at vertex %i'
609 % (n, (n * n) ** 0.5, i))
610 if abs(t * t - 1) > NifFormat.EPSILON:
611 self.toaster.logger.warn(
612 'non-unit tangent %s (norm %f) at vertex %i'
613 % (t, (t * t) ** 0.5, i))
614 if abs(b * b - 1) > NifFormat.EPSILON:
615 self.toaster.logger.warn(
616 'non-unit binormal %s (norm %f) at vertex %i'
617 % (b, (b * b) ** 0.5, i))
618 if abs(n * t) + abs(n * b) > NifFormat.EPSILON:
619 volume = n * t.crossproduct(b)
620 self.toaster.logger.warn(
621 'non-ortogonal tangent space at vertex %i' % i)
622 self.toaster.logger.warn(
623 'n * t = %s * %s = %f'%(n, t, n * t))
624 self.toaster.logger.warn(
625 'n * b = %s * %s = %f'%(n, b, n * b))
626 self.toaster.logger.warn(
627 't * b = %s * %s = %f'%(t, b, t * b))
628 self.toaster.logger.warn(
629 'volume = %f' % volume)
630
631 branch.update_tangent_space()
632 newspace = []
633 for i, (n, t, b) in enumerate(branch.get_tangent_space()):
634 newspace.append(n.as_list() + t.as_list() + b.as_list())
635
636 for i, (old, new) in enumerate(izip(oldspace, newspace)):
637 for oldvalue, newvalue in izip(old, new):
638
639 if abs(oldvalue - newvalue) > self.PRECISION:
640 self.toaster.logger.warn(
641 'calculated tangent space differs from original '
642 'at vertex %i' % i)
643 self.toaster.logger.warn('old: %s' % old[0:3])
644 self.toaster.logger.warn('old: %s' % old[3:6])
645 self.toaster.logger.warn('old: %s' % old[6:9])
646 self.toaster.logger.warn('new: %s' % new[0:3])
647 self.toaster.logger.warn('new: %s' % new[3:6])
648 self.toaster.logger.warn('new: %s' % new[6:9])
649 break
650
651
652 return False
653
655 """Run the stripifier on all triangles from nif files. This spell is also
656 useful for checking and profiling the stripifier and the
657 stitcher/unstitcher (for instance it checks that it does not
658 change the geometry).
659
660 Reports at the end with average strip length (this is useful to compare
661 various stripification algorithms over a large collection of geometries).
662 """
663 SPELLNAME = 'check_tristrip'
664
665 @classmethod
666 - def toastentry(cls, toaster):
667 toaster.striplengths = []
668 return True
669
670 @classmethod
675
678
682
683 - def branchentry(self, branch):
684
685 def report_strip_statistics(triangles, strips):
686 """Print some statistics."""
687
688 if not strips:
689 return
690
691
692 self.toaster.msg('checking strip triangles')
693 pyffi.utils.tristrip._check_strips(triangles, strips)
694
695 if len(strips) == 1:
696
697 stitchedstrip = strips[0]
698 self.toaster.msg("stitched strip length = %i"
699 % len(stitchedstrip))
700 unstitchedstrips = pyffi.utils.tristrip.unstitch_strip(
701 stitchedstrip)
702 self.toaster.msg("num stitches = %i"
703 % (len(stitchedstrip)
704 - sum(len(strip)
705 for strip in unstitchedstrips)))
706
707
708 self.toaster.msg('checking unstitched strip triangles')
709 pyffi.utils.tristrip._check_strips(triangles, unstitchedstrips)
710
711
712 self.toaster.msg("restitching")
713 restitchedstrip = pyffi.utils.tristrip.stitch_strips(
714 unstitchedstrips)
715 self.toaster.msg("stitched strip length = %i"
716 % len(restitchedstrip))
717 self.toaster.msg("num stitches = %i"
718 % (len(restitchedstrip)
719 - sum(len(strip)
720 for strip in unstitchedstrips)))
721
722
723 self.toaster.msg('checking restitched strip triangles')
724 pyffi.utils.tristrip._check_strips(triangles, [restitchedstrip])
725
726 else:
727 unstitchedstrips = strips
728
729 self.toaster.msg("num strips = %i"
730 % len(unstitchedstrips))
731 self.toaster.msg("average strip length = %.3f"
732 % (sum((len(strip) for strip in unstitchedstrips), 0.0)
733 / len(unstitchedstrips)))
734
735 if not isinstance(branch, NifFormat.NiTriBasedGeomData):
736
737 return True
738 else:
739
740 self.toaster.msg('getting triangles')
741 triangles = branch.get_triangles()
742
743 if isinstance(branch, NifFormat.NiTriStripsData):
744 report_strip_statistics(triangles, branch.get_strips())
745
746 self.toaster.msg('recalculating strips')
747 try:
748 strips = pyffi.utils.tristrip.stripify(
749 triangles, stitchstrips=False)
750 report_strip_statistics(triangles, strips)
751 except Exception:
752 self.toaster.logger.error('failed to strip triangles')
753 self.toaster.logger.error('%s' % triangles)
754 raise
755
756
757 self.toaster.striplengths += [len(strip) for strip in strips]
758
759 self.toaster.msg('checking stitched strip triangles')
760 stitchedstrip = pyffi.utils.tristrip.stitch_strips(strips)
761 pyffi.utils.tristrip._check_strips(triangles, [stitchedstrip])
762
763 self.toaster.msg('checking unstitched strip triangles')
764 unstitchedstrips = pyffi.utils.tristrip.unstitch_strip(stitchedstrip)
765 pyffi.utils.tristrip._check_strips(triangles, unstitchedstrips)
766
768 """Checks all versions used by the files (without reading the full files).
769 """
770 SPELLNAME = 'check_version'
771
772 @classmethod
773 - def toastentry(cls, toaster):
774 toaster.versions = {}
775 toaster.user_versions = {}
776 toaster.user_version2s = {}
777 return True
778
779 @classmethod
787
808
810 """Check (and warn) about potentially bad material emissive values."""
811
812 SPELLNAME = "check_materialemissivevalue"
813
817
818 - def dataentry(self):
819 self.check_emissive_done = False
820 return True
821
829
830 - def branchentry(self, branch):
831 if isinstance(branch, NifFormat.NiMaterialProperty):
832
833 emissive = branch.emissive_color
834 if emissive.r > 0.5 or emissive.g > 0.5 or emissive.b > 0.5:
835
836
837
838
839 self.toaster.logger.warn(
840 "emissive value may be too high (highest value: %f)"
841 % (max(emissive.r, emissive.g, emissive.b)))
842
843 self.check_emissive_done = True
844
845 return False
846 else:
847
848 return True
849
881
882 try:
883 import numpy
884 import scipy.optimize
885 except ImportError:
886 numpy = None
887 scipy = None
890 """Find optimal parameters for vertex cache algorithm by simulated
891 annealing.
892 """
893
894 SPELLNAME = "check_triangles_atvr"
895 INITIAL = [1.5, 0.75, 2.0, 0.5]
896 LOWER = [0.01, -10.0, 0.1, 0.01]
897 UPPER = [5.0, 1.0, 10.0, 5.0]
898
899 @classmethod
900 - def toastentry(cls, toaster):
901
902 if not SpellCheckTriangles.toastentry(toaster):
903 return False
904
905 if (numpy is None) or (scipy is None):
906 toaster.logger.error(
907 self.SPELLNAME
908 + " requires numpy and scipy (http://www.scipy.org/)")
909 return False
910 return True
911
912 @classmethod
914
915 if any(value < lower or value > upper
916 for (lower, value, upper) in zip(
917 cls.LOWER, args, cls.UPPER)):
918 return 1e30
919 cache_decay_power, last_tri_score, valence_boost_scale, valence_boost_power = args
920 vertex_score = pyffi.utils.vertex_cache.VertexScore()
921 vertex_score.CACHE_DECAY_POWER = cache_decay_power
922 vertex_score.LAST_TRI_SCORE = last_tri_score
923 vertex_score.VALENCE_BOOST_SCALE = valence_boost_scale
924 vertex_score.VALENCE_BOOST_POWER = valence_boost_power
925 vertex_score.precalculate()
926 print("{0:.3f} {1:.3f} {2:.3f} {3:.3f}".format(
927 cache_decay_power, last_tri_score,
928 valence_boost_scale, valence_boost_power))
929 atvr = []
930 for triangles in toaster.geometries:
931 mesh = pyffi.utils.vertex_cache.Mesh(triangles, vertex_score)
932 new_triangles = mesh.get_cache_optimized_triangles()
933 atvr.append(
934 pyffi.utils.vertex_cache.average_transform_to_vertex_ratio(
935 new_triangles, 32))
936 print(sum(atvr) / len(atvr))
937 return sum(atvr) / len(atvr)
938
939 @classmethod
955