Package pyffi :: Package spells :: Package nif :: Module check
[hide private]
[frames] | no frames]

Source Code for Module pyffi.spells.nif.check

  1  """Module which contains all spells that check something in a nif file.""" 
  2   
  3  # -------------------------------------------------------------------------- 
  4  # ***** BEGIN LICENSE BLOCK ***** 
  5  # 
  6  # Copyright (c) 2007-2011, NIF File Format Library and Tools. 
  7  # All rights reserved. 
  8  # 
  9  # Redistribution and use in source and binary forms, with or without 
 10  # modification, are permitted provided that the following conditions 
 11  # are met: 
 12  # 
 13  #    * Redistributions of source code must retain the above copyright 
 14  #      notice, this list of conditions and the following disclaimer. 
 15  # 
 16  #    * Redistributions in binary form must reproduce the above 
 17  #      copyright notice, this list of conditions and the following 
 18  #      disclaimer in the documentation and/or other materials provided 
 19  #      with the distribution. 
 20  # 
 21  #    * Neither the name of the NIF File Format Library and Tools 
 22  #      project nor the names of its contributors may be used to endorse 
 23  #      or promote products derived from this software without specific 
 24  #      prior written permission. 
 25  # 
 26  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 27  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 28  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 29  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 30  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 31  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 32  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 33  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 34  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 35  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 36  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 37  # POSSIBILITY OF SUCH DAMAGE. 
 38  # 
 39  # ***** END LICENSE BLOCK ***** 
 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 # for check_tristrip 
50 51 -class SpellReadWrite(pyffi.spells.nif.NifSpell):
52 """Like the original read-write spell, but with additional file size 53 check.""" 54 55 SPELLNAME = "check_readwrite" 56
57 - def datainspect(self):
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 # comparing the files will usually be different because 73 # blocks may have been written back in a different order, 74 # so cheaply just compare file sizes 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 # spell is finished: prevent recursing into the tree 92 return False
93
94 -class SpellNodeNamesByFlag(pyffi.spells.nif.NifSpell):
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
106 - def toastexit(cls, toaster):
107 for flag, names in toaster.flagdict.iteritems(): 108 toaster.msg("%s %s" % (flag, names))
109
110 - def datainspect(self):
112
113 - def branchinspect(self, branch):
114 # stick to main tree 115 return isinstance(branch, NifFormat.NiAVObject)
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
127 -class SpellCompareSkinData(pyffi.spells.nif.NifSpell):
128 """This spell compares skinning data with a reference nif.""" 129 130 SPELLNAME = "check_compareskindata" 131 132 # helper functions (to compare with custom tolerance) 133 134 @staticmethod
135 - def are_vectors_equal(oldvec, newvec, tolerance=0.01):
136 return (max([abs(x-y) 137 for (x,y) in izip(oldvec.as_list(), newvec.as_list())]) 138 < tolerance)
139 140 @staticmethod
141 - def are_matrices_equal(oldmat, newmat, tolerance=0.01):
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
149 - def are_floats_equal(oldfloat, newfloat, tolerance=0.01):
150 return abs(oldfloat - newfloat) < tolerance
151 152 @classmethod
153 - def toastentry(cls, toaster):
154 """Read reference nif file given as argument.""" 155 # if no argument given, do not apply spell 156 if not toaster.options.get("arg"): 157 return False 158 # read reference nif 159 toaster.refdata = NifFormat.Data() 160 with closing(open(toaster.options["arg"], "rb")) as reffile: 161 toaster.refdata.read(reffile) 162 # find bone data in reference nif 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 # only apply spell if the reference nif has bone data 173 return bool(toaster.refbonedata)
174
175 - def datainspect(self):
177
178 - def branchinspect(self, branch):
179 # stick to main tree 180 return isinstance(branch, NifFormat.NiAVObject)
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 # check that skeleton roots are identical 197 if skelroot.name == refskelroot.name: 198 # no extra transform 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 # the following is an experimental way of 208 # compensating for different skeleton roots 209 # (disabled by default) 210 211 # can we find skeleton root of data in reference 212 # data? 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 # yes! found! 220 #self.toaster.msg( 221 # "found alternative in reference nif") 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 # yes! found! 233 #self.toaster.msg( 234 # "found alternative in nif") 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 # calculate total transform matrix that would be applied 246 # to a vertex in the reference geometry in the position 247 # of the reference bone 248 reftransform = ( 249 refbonedata.get_transform() 250 * refbonenode.get_transform(refskelroot) 251 * refskeldata.get_transform()) 252 # calculate total transform matrix that would be applied 253 # to a vertex in this branch in the position of the 254 # reference bone 255 branchtransform = ( 256 bonedata.get_transform() 257 * refbonenode.get_transform(refskelroot) # NOT a typo 258 * skeldata.get_transform() 259 * branchtransform_extra) # skelroot differences 260 # compare 261 if not self.are_matrices_equal(reftransform, 262 branchtransform): 263 #raise ValueError( 264 self.toaster.msg( 265 "transform mismatch\n%s\n!=\n%s\n" 266 % (reftransform, branchtransform)) 267 268 self.toaster.msgblockend() 269 # stop in this branch 270 return False 271 else: 272 # keep iterating 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):
286 return isinstance(branch, (NifFormat.NiAVObject, 287 NifFormat.bhkNiCollisionObject, 288 NifFormat.bhkRigidBody))
289
290 - def branchentry(self, branch):
291 if not isinstance(branch, NifFormat.bhkRigidBody): 292 # keep recursing 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 #self.toaster.msg("checking mass...") 304 #if mass != branch.mass: 305 # #raise ValueError("center does not match; original %s, calculated %s"%(center, branch.center)) 306 # self.toaster.logger.warn("warning: mass does not match; original %s, calculated %s"%(mass, branch.mass)) 307 # # adapt calculated inertia matrix with observed mass 308 # if mass > 0.001: 309 # correction = mass / branch.mass 310 # for i in xrange(12): 311 # branch.inertia[i] *= correction 312 #else: 313 # self.toaster.msg("perfect match!") 314 315 self.toaster.msg("checking center...") 316 if center != branch.center: 317 #raise ValueError("center does not match; original %s, calculated %s"%(center, branch.center)) 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 #raise ValueError("center does not match; original %s, calculated %s"%(center, branch.center)) 330 self.toaster.logger.warn( 331 "inertia does not match:\n\noriginal\n%s\n\ncalculated\n%s\n" 332 % (inertia, branch.inertia)) 333 # stop recursing 334 return False
335
336 -class SpellCheckCenterRadius(pyffi.spells.nif.NifSpell):
337 """Recalculate the center and radius, compare them to the originals, 338 and report mismatches. 339 """ 340 # tentative results 341 # ----------------- 342 # oblivion: ok 343 # civ4: mostly ok (with very few exceptions: effects/magpie/flock.nif, units/!errorunit/bear.nif, maybe some more) 344 # daoc: ok 345 # morrowind: usually ok (quite some exceptions here) 346 # zoo tycoon 2: mostly ok (except *_Adult_*.nif files) 347 348 SPELLNAME = "check_centerradius" 349
350 - def datainspect(self):
352
353 - def branchinspect(self, branch):
354 return isinstance(branch, (NifFormat.NiAVObject, 355 NifFormat.NiGeometry, 356 NifFormat.NiGeometryData))
357
358 - def branchentry(self, branch):
359 if not isinstance(branch, NifFormat.NiGeometryData): 360 # keep recursing 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 #raise ValueError( 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 # stop recursing 399 return False
400
401 -class SpellCheckSkinCenterRadius(pyffi.spells.nif.NifSpell):
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
408 - def datainspect(self):
410
411 - def branchinspect(self, branch):
412 return isinstance(branch, (NifFormat.NiAVObject, 413 NifFormat.NiGeometry))
414
415 - def branchentry(self, branch):
416 if not(isinstance(branch, NifFormat.NiGeometry) and branch.is_skin()): 417 # keep recursing 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 # stop recursing 444 return False
445
446 -class SpellCheckConvexVerticesShape(pyffi.spells.nif.NifSpell):
447 """This test checks whether each vertex is the intersection of at least 448 three planes. 449 """ 450 SPELLNAME = "check_convexverticesshape" 451
452 - def datainspect(self):
454
455 - def branchinspect(self, branch):
456 return isinstance(branch, (NifFormat.NiAVObject, 457 NifFormat.bhkNiCollisionObject, 458 NifFormat.bhkRefObject))
459
460 - def branchentry(self, branch):
461 if not isinstance(branch, NifFormat.bhkConvexVerticesShape): 462 # keep recursing 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 # stop recursing 490 return False
491
492 -class SpellCheckMopp(pyffi.spells.nif.NifSpell):
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
503 - def datainspect(self):
505
506 - def branchinspect(self, branch):
507 return isinstance(branch, (NifFormat.NiAVObject, 508 NifFormat.bhkNiCollisionObject, 509 NifFormat.bhkRefObject))
510
511 - def branchentry(self, branch):
512 if not isinstance(branch, NifFormat.bhkMoppBvTreeShape): 513 # keep recursing 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 # ids = indices of bytes processed, tris = triangle indices 537 ids, tris = branch.parse_mopp(verbose=True) 538 539 error = False 540 541 # check triangles 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 # check bytes 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 #if error: 574 # raise ValueError("mopp parsing failed") 575 576 # stop recursing 577 return False
578
579 -class SpellCheckTangentSpace(pyffi.spells.nif.NifSpell):
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 #: Difference between values worth warning about. 585
586 - def datainspect(self):
588
589 - def branchinspect(self, branch):
590 return isinstance(branch, NifFormat.NiAVObject)
591
592 - def branchentry(self, branch):
593 if not isinstance(branch, NifFormat.NiTriBasedGeom): 594 # keep recursing 595 return True 596 else: 597 # get tangent space 598 tangentspace = branch.get_tangent_space() 599 if not tangentspace: 600 # no tangent space present 601 return False 602 self.toaster.msg("checking tangent space") 603 oldspace = [] # we will store the old tangent space here 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 # recalculate the tangent space 631 branch.update_tangent_space() 632 newspace = [] # we will store the old tangent space here 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 # check if old matches new 636 for i, (old, new) in enumerate(izip(oldspace, newspace)): 637 for oldvalue, newvalue in izip(old, new): 638 # allow fairly big error 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 # don't recurse further 652 return False
653
654 -class SpellCheckTriStrip(pyffi.spells.nif.NifSpell):
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
671 - def toastexit(cls, toaster):
672 toaster.msg("average strip length = %.6f" 673 % (sum(toaster.striplengths) 674 / float(len(toaster.striplengths))))
675
676 - def datainspect(self):
678
679 - def branchinspect(self, branch):
680 return isinstance(branch, (NifFormat.NiAVObject, 681 NifFormat.NiTriBasedGeomData))
682
683 - def branchentry(self, branch):
684 685 def report_strip_statistics(triangles, strips): 686 """Print some statistics.""" 687 # handle this just in case 688 if not strips: 689 return 690 691 # run check 692 self.toaster.msg('checking strip triangles') 693 pyffi.utils.tristrip._check_strips(triangles, strips) 694 695 if len(strips) == 1: 696 # stitched strip 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 # run check 708 self.toaster.msg('checking unstitched strip triangles') 709 pyffi.utils.tristrip._check_strips(triangles, unstitchedstrips) 710 711 # test stitching algorithm 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 # run check 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 # keep recursing 737 return True 738 else: 739 # get triangles 740 self.toaster.msg('getting triangles') 741 triangles = branch.get_triangles() 742 # report original strip statistics 743 if isinstance(branch, NifFormat.NiTriStripsData): 744 report_strip_statistics(triangles, branch.get_strips()) 745 # recalculate strips 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 # keep track of strip length 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
767 -class SpellCheckVersion(pyffi.spells.nif.NifSpell):
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 = {} # counts number of nifs with version 775 toaster.user_versions = {} # tracks used user version's per version 776 toaster.user_version2s = {} # tracks used user version2's per version 777 return True
778 779 @classmethod
780 - def toastexit(cls, toaster):
781 for version in toaster.versions: 782 toaster.msgblockbegin("version 0x%08X" % version) 783 toaster.msg("number of nifs: %s" % toaster.versions[version]) 784 toaster.msg("user version: %s" % toaster.user_versions[version]) 785 toaster.msg("user version2: %s" % toaster.user_version2s[version]) 786 toaster.msgblockend()
787
788 - def datainspect(self):
789 # some shortcuts 790 version = self.data.version 791 user_version = self.data.user_version 792 user_version2 = self.data.user_version2 793 # report 794 self.toaster.msg("version 0x%08X" % version) 795 self.toaster.msg("user version %i" % user_version) 796 self.toaster.msg("user version %i" % user_version2) 797 # update stats 798 if version not in self.toaster.versions: 799 self.toaster.versions[version] = 0 800 self.toaster.user_versions[version] = [] 801 self.toaster.user_version2s[version] = [] 802 self.toaster.versions[version] += 1 803 if user_version not in self.toaster.user_versions[version]: 804 self.toaster.user_versions[version].append(user_version) 805 if user_version2 not in self.toaster.user_version2s[version]: 806 self.toaster.user_version2s[version].append(user_version2) 807 return False
808
809 -class SpellCheckMaterialEmissiveValue(pyffi.spells.nif.NifSpell):
810 """Check (and warn) about potentially bad material emissive values.""" 811 812 SPELLNAME = "check_materialemissivevalue" 813
814 - def datainspect(self):
815 # only run the spell if there are material property blocks 816 return self.inspectblocktype(NifFormat.NiMaterialProperty)
817
818 - def dataentry(self):
819 self.check_emissive_done = False 820 return True
821
822 - def branchinspect(self, branch):
823 # if we are done, don't recurse further 824 if self.check_emissive_done: 825 return False 826 # only inspect the NiAVObject branch, and material properties 827 return isinstance(branch, (NifFormat.NiAVObject, 828 NifFormat.NiMaterialProperty))
829
830 - def branchentry(self, branch):
831 if isinstance(branch, NifFormat.NiMaterialProperty): 832 # check if any emissive values exceeds usual values 833 emissive = branch.emissive_color 834 if emissive.r > 0.5 or emissive.g > 0.5 or emissive.b > 0.5: 835 # potentially too high (there are some instances (i.e. 836 # most glass, flame, gems, willothewisps etc.) that 837 # that is not too high but most other instances (i.e. 838 # ogres!) that this is the case it is incorrect) 839 self.toaster.logger.warn( 840 "emissive value may be too high (highest value: %f)" 841 % (max(emissive.r, emissive.g, emissive.b))) 842 # we're done... 843 self.check_emissive_done = True 844 # stop recursion 845 return False 846 else: 847 # keep recursing into children 848 return True
849
850 -class SpellCheckTriangles(pyffi.spells.nif.NifSpell):
851 """Base class for spells which need to check all triangles.""" 852 853 SPELLNAME = "check_triangles" 854
855 - def datainspect(self):
856 # only run the spell if there are geometries 857 return self.inspectblocktype(NifFormat.NiTriBasedGeom)
858 859 @classmethod
860 - def toastentry(cls, toaster):
861 toaster.geometries = [] 862 return True
863
864 - def branchinspect(self, branch):
865 # only inspect the NiAVObject branch 866 return isinstance(branch, NifFormat.NiAVObject)
867
868 - def branchentry(self, branch):
869 if isinstance(branch, NifFormat.NiTriBasedGeom): 870 # get triangles 871 self.toaster.geometries.append(branch.data.get_triangles()) 872 # stop recursion 873 return False 874 else: 875 # keep recursing into children 876 return True
877 878 @classmethod
879 - def toastexit(cls, toaster):
880 toaster.msg("found {0} geometries".format(len(toaster.geometries)))
881 882 try: 883 import numpy 884 import scipy.optimize 885 except ImportError: 886 numpy = None 887 scipy = None
888 889 -class SpellCheckTrianglesATVR(SpellCheckTriangles):
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 # call base class method 902 if not SpellCheckTriangles.toastentry(toaster): 903 return False 904 # check that we have numpy and scipy 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
913 - def get_atvr(cls, toaster, *args):
914 # check bounds 915 if any(value < lower or value > upper 916 for (lower, value, upper) in zip( 917 cls.LOWER, args, cls.UPPER)): 918 return 1e30 # infinity 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
940 - def toastexit(cls, toaster):
941 toaster.msg("found {0} geometries".format(len(toaster.geometries))) 942 result = scipy.optimize.anneal( 943 lambda x: cls.get_atvr(toaster, *x), 944 numpy.array(cls.INITIAL), 945 full_output=True, 946 lower=numpy.array(cls.LOWER), 947 upper=numpy.array(cls.UPPER), 948 #maxeval=10, 949 #maxaccept=10, 950 #maxiter=10, 951 #dwell=10, 952 #feps=0.1, 953 ) 954 toaster.msg(str(result))
955