Home | Trees | Indices | Help |
|
---|
|
1 """ 2 :mod:`pyffi.spells.nif.fix` --- spells to fix errors 3 ===================================================== 4 5 Module which contains all spells that fix something in a nif. 6 7 Implementation 8 -------------- 9 10 .. autoclass:: SpellDelTangentSpace 11 :show-inheritance: 12 :members: 13 14 .. autoclass:: SpellAddTangentSpace 15 :show-inheritance: 16 :members: 17 18 .. autoclass:: SpellFFVT3RSkinPartition 19 :show-inheritance: 20 :members: 21 22 .. autoclass:: SpellFixTexturePath 23 :show-inheritance: 24 :members: 25 26 .. autoclass:: SpellDetachHavokTriStripsData 27 :show-inheritance: 28 :members: 29 30 .. autoclass:: SpellClampMaterialAlpha 31 :show-inheritance: 32 :members: 33 34 .. autoclass:: SpellSendGeometriesToBindPosition 35 :show-inheritance: 36 :members: 37 38 .. autoclass:: SpellSendDetachedGeometriesToNodePosition 39 :show-inheritance: 40 :members: 41 42 .. autoclass:: SpellSendBonesToBindPosition 43 :show-inheritance: 44 :members: 45 46 .. autoclass:: SpellMergeSkeletonRoots 47 :show-inheritance: 48 :members: 49 50 .. autoclass:: SpellApplySkinDeformation 51 :show-inheritance: 52 :members: 53 54 .. autoclass:: SpellScale 55 :show-inheritance: 56 :members: 57 58 .. autoclass:: SpellFixCenterRadius 59 :show-inheritance: 60 :members: 61 62 .. autoclass:: SpellFixSkinCenterRadius 63 :show-inheritance: 64 :members: 65 66 .. autoclass:: SpellFixMopp 67 :show-inheritance: 68 :members: 69 70 .. autoclass:: SpellCleanStringPalette 71 :show-inheritance: 72 :members: 73 74 .. autoclass:: SpellDelUnusedRoots 75 :show-inheritance: 76 :members: 77 78 .. autoclass:: SpellFixEmptySkeletonRoots 79 :show-inheritance: 80 :members: 81 82 Regression tests 83 ---------------- 84 """ 85 86 # -------------------------------------------------------------------------- 87 # ***** BEGIN LICENSE BLOCK ***** 88 # 89 # Copyright (c) 2007-2011, NIF File Format Library and Tools. 90 # All rights reserved. 91 # 92 # Redistribution and use in source and binary forms, with or without 93 # modification, are permitted provided that the following conditions 94 # are met: 95 # 96 # * Redistributions of source code must retain the above copyright 97 # notice, this list of conditions and the following disclaimer. 98 # 99 # * Redistributions in binary form must reproduce the above 100 # copyright notice, this list of conditions and the following 101 # disclaimer in the documentation and/or other materials provided 102 # with the distribution. 103 # 104 # * Neither the name of the NIF File Format Library and Tools 105 # project nor the names of its contributors may be used to endorse 106 # or promote products derived from this software without specific 107 # prior written permission. 108 # 109 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 110 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 111 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 112 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 113 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 114 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 115 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 116 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 117 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 118 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 119 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 120 # POSSIBILITY OF SUCH DAMAGE. 121 # 122 # ***** END LICENSE BLOCK ***** 123 # -------------------------------------------------------------------------- 124 125 from pyffi.formats.nif import NifFormat 126 from pyffi.spells.nif import NifSpell 127 import pyffi.spells.nif 128 import pyffi.spells.nif.check # recycle checking spells for update spells131 """Delete tangentspace if it is present.""" 132 133 SPELLNAME = "fix_deltangentspace" 134 READONLY = False 135 138 142157144 if isinstance(branch, NifFormat.NiTriBasedGeom): 145 # does this block have tangent space data? 146 for extra in branch.get_extra_datas(): 147 if isinstance(extra, NifFormat.NiBinaryExtraData): 148 if (extra.name == 149 'Tangent space (binormal & tangent vectors)'): 150 self.toaster.msg("removing tangent space block") 151 branch.remove_extra_data(extra) 152 self.changed = True 153 # all extra blocks here done; no need to recurse further 154 return False 155 # recurse further 156 return True159 """Add tangentspace if none is present.""" 160 161 SPELLNAME = "fix_addtangentspace" 162 READONLY = False 163 166 170189172 if isinstance(branch, NifFormat.NiTriBasedGeom): 173 # does this block have tangent space data? 174 for extra in branch.get_extra_datas(): 175 if isinstance(extra, NifFormat.NiBinaryExtraData): 176 if (extra.name == 177 'Tangent space (binormal & tangent vectors)'): 178 # tangent space found, done! 179 return False 180 # no tangent space found 181 self.toaster.msg("adding tangent space") 182 branch.update_tangent_space() 183 self.changed = True 184 # all extra blocks here done; no need to recurse further 185 return False 186 else: 187 # recurse further 188 return True191 """Create or update skin partition, with settings that work for Freedom 192 Force vs. The 3rd Reich.""" 193 194 SPELLNAME = "fix_ffvt3rskinpartition" 195 READONLY = False 196 199 203219205 if isinstance(branch, NifFormat.NiTriBasedGeom): 206 # if the branch has skinning info 207 if branch.skin_instance: 208 # then update the skin partition 209 self.toaster.msg("updating skin partition") 210 branch.update_skin_partition( 211 maxbonesperpartition=4, maxbonespervertex=4, 212 stripify=False, verbose=0, padbones=True) 213 self.changed = True 214 return False 215 # done; no need to recurse further in this branch 216 else: 217 # recurse further 218 return True221 """Base class for spells which must parse all texture paths, with 222 hook for texture path substitution. 223 """ 224 225 # abstract spell, so no spell name 226 READONLY = False 227252229 """Helper function to allow subclasses of this spell to 230 change part of the path with minimum of code. 231 This implementation returns path unmodified. 232 """ 233 return old_path234236 # only run the spell if there are NiSourceTexture blocks 237 return self.inspectblocktype(NifFormat.NiSourceTexture)238240 # only inspect the NiAVObject branch, texturing properties and source 241 # textures 242 return isinstance(branch, (NifFormat.NiAVObject, 243 NifFormat.NiTexturingProperty, 244 NifFormat.NiSourceTexture))245247 if isinstance(branch, NifFormat.NiSourceTexture): 248 branch.file_name = self.substitute(branch.file_name) 249 return False 250 else: 251 return True254 r"""Fix the texture path. Transforms 0x0a into \n and 0x0d into 255 \r. This fixes a bug in nifs saved with older versions of 256 nifskope. Also transforms / into \. This fixes problems when 257 packing files into a bsa archive. Also if the version is 20.0.0.4 258 or higher it will check for bad texture path form of e.g. 259 c:\program files\bethsoft\ob\textures\file\path.dds and replace it 260 with e.g. textures\file\path.dds. 261 """ 262 263 SPELLNAME = "fix_texturepath" 264291266 new_path = old_path 267 new_path = new_path.replace( 268 '\n'.encode("ascii"), 269 '\\n'.encode("ascii")) 270 new_path = new_path.replace( 271 '\r'.encode("ascii"), 272 '\\r'.encode("ascii")) 273 new_path = new_path.replace( 274 '/'.encode("ascii"), 275 '\\'.encode("ascii")) 276 # baphometal found some nifs that use double slashes 277 # this causes textures not to show, so here we convert them 278 # back to single slashes 279 new_path = new_path.replace( 280 '\\\\'.encode("ascii"), 281 '\\'.encode("ascii")) 282 textures_index = new_path.lower().find("textures\\") 283 if textures_index > 0: 284 # path contains textures\ at position other than starting 285 # position 286 new_path = new_path[textures_index:] 287 if new_path != old_path: 288 self.toaster.msg("fixed file name '%s'" % new_path) 289 self.changed = True 290 return new_path292 # the next spell solves issue #2065018, MiddleWolfRug01.NIF 293 -class SpellDetachHavokTriStripsData(NifSpell):294 """For NiTriStrips if their NiTriStripsData also occurs in a 295 bhkNiTriStripsShape, make deep copy of data in havok. This is 296 mainly useful as a preperation for other spells that act on 297 NiTriStripsData, to ensure that the havok data remains untouched.""" 298 299 SPELLNAME = "fix_detachhavoktristripsdata" 300 READONLY = False 301338303 NifSpell.__init__(self, *args, **kwargs) 304 # provides the bhknitristripsshapes within the current NiTriStrips 305 self.bhknitristripsshapes = None306308 # only run the spell if there are bhkNiTriStripsShape blocks 309 return self.inspectblocktype(NifFormat.bhkNiTriStripsShape)310312 # build list of all NiTriStrips blocks 313 self.nitristrips = [branch for branch in self.data.get_global_iterator() 314 if isinstance(branch, NifFormat.NiTriStrips)] 315 if self.nitristrips: 316 return True 317 else: 318 return False319321 # only inspect the NiAVObject branch and collision branch 322 return isinstance(branch, (NifFormat.NiAVObject, 323 NifFormat.bhkCollisionObject, 324 NifFormat.bhkRefObject))325327 if isinstance(branch, NifFormat.bhkNiTriStripsShape): 328 for i, data in enumerate(branch.strips_data): 329 if data in [otherbranch.data 330 for otherbranch in self.nitristrips]: 331 # detach! 332 self.toaster.msg("detaching havok data") 333 branch.strips_data[i] = NifFormat.NiTriStripsData().deepcopy(data) 334 self.changed = True 335 return False 336 else: 337 return True340 """Clamp corrupted material alpha values.""" 341 342 SPELLNAME = "fix_clampmaterialalpha" 343 READONLY = False 344374346 # only run the spell if there are material property blocks 347 return self.inspectblocktype(NifFormat.NiMaterialProperty)348350 # only inspect the NiAVObject branch, and material properties 351 return isinstance(branch, (NifFormat.NiAVObject, 352 NifFormat.NiMaterialProperty))353355 if isinstance(branch, NifFormat.NiMaterialProperty): 356 # check if alpha exceeds usual values 357 if branch.alpha > 1: 358 # too large 359 self.toaster.msg( 360 "clamping alpha value (%f -> 1.0)" % branch.alpha) 361 branch.alpha = 1.0 362 self.changed = True 363 elif branch.alpha < 0: 364 # too small 365 self.toaster.msg( 366 "clamping alpha value (%f -> 0.0)" % branch.alpha) 367 branch.alpha = 0.0 368 self.changed = True 369 # stop recursion 370 return False 371 else: 372 # keep recursing into children 373 return True376 """Transform skinned geometries so similar bones have the same bone data, 377 and hence, the same bind position, over all geometries. 378 """ 379 SPELLNAME = "fix_sendgeometriestobindposition" 380 READONLY = False 381386383 self.toaster.msg("sending geometries to bind position") 384 branch.send_geometries_to_bind_position() 385 self.changed = True388 """Transform geometries so each set of geometries that shares bones 389 is aligned with the transform of the root bone of that set. 390 """ 391 SPELLNAME = "fix_senddetachedgeometriestonodeposition" 392 READONLY = False 393398395 self.toaster.msg("sending detached geometries to node position") 396 branch.send_detached_geometries_to_node_position() 397 self.changed = True400 """Transform bones so bone data agrees with bone transforms, 401 and hence, all bones are in bind position. 402 """ 403 SPELLNAME = "fix_sendbonestobindposition" 404 READONLY = False 405410407 self.toaster.msg("sending bones to bind position") 408 branch.send_bones_to_bind_position() 409 self.changed = True412 """Merges skeleton roots in the nif file so that no skeleton root has 413 another skeleton root as child. Warns if merge is impossible (this happens 414 if the global skin data of the geometry is not the unit transform). 415 """ 416 SPELLNAME = "fix_mergeskeletonroots" 417 READONLY = False 418468 473420 # only run the spell if there are skinned geometries 421 return self.inspectblocktype(NifFormat.NiSkinInstance)422424 # make list of skeleton roots 425 skelroots = [] 426 for branch in self.data.get_global_iterator(): 427 if isinstance(branch, NifFormat.NiGeometry): 428 if branch.skin_instance: 429 skelroot = branch.skin_instance.skeleton_root 430 if skelroot and not skelroot in skelroots: 431 skelroots.append(skelroot) 432 # find the 'root' skeleton roots (those that have no other skeleton 433 # roots as child) 434 self.skelrootlist = set() 435 for skelroot in skelroots: 436 for skelroot_other in skelroots: 437 if skelroot_other is skelroot: 438 continue 439 if skelroot_other.find_chain(skelroot): 440 # skelroot_other has skelroot as child 441 # so skelroot is no longer an option 442 break 443 else: 444 # no skeleton root children! 445 self.skelrootlist.add(skelroot) 446 # only apply spell if there are skeleton roots 447 if self.skelrootlist: 448 return True 449 else: 450 return False451 455457 if branch in self.skelrootlist: 458 result, failed = branch.merge_skeleton_roots() 459 self.changed = True 460 for geom in result: 461 self.toaster.msg("reassigned skeleton root of %s" % geom.name) 462 self.skelrootlist.remove(branch) 463 # continue recursion only if there is still more to come 464 if self.skelrootlist: 465 return True 466 else: 467 return False475 """Scale a model.""" 476 477 SPELLNAME = "fix_scale" 478 READONLY = False 479 480 @classmethod507482 if not toaster.options["arg"]: 483 toaster.logger.warn( 484 "must specify scale as argument (e.g. -a 10) " 485 "to apply spell") 486 return False 487 else: 488 toaster.scale = float(toaster.options["arg"]) 489 return True490492 # initialize list of blocks that have been scaled 493 self.toaster.msg("scaling by factor %f" % self.toaster.scale) 494 self.scaled_branches = [] 495 return True496 500502 branch.apply_scale(self.toaster.scale) 503 self.changed = True 504 self.scaled_branches.append(branch) 505 # continue recursion 506 return True509 """Recalculate geometry centers and radii.""" 510 SPELLNAME = "fix_centerradius" 511 READONLY = False512514 """Recalculate skin centers and radii.""" 515 SPELLNAME = "fix_skincenterradius" 516 READONLY = False517519 """Recalculate mopp data from collision geometry.""" 520 SPELLNAME = "fix_mopp" 521 READONLY = False 522534524 # we don't recycle the check mopp code here 525 # that spell does not actually recalculate the mopp at all 526 # it only parses the existing mopp... 527 if not isinstance(branch, NifFormat.bhkMoppBvTreeShape): 528 # keep recursing 529 return True 530 else: 531 self.toaster.msg("updating mopp") 532 branch.update_mopp() 533 self.changed = True536 """Remove unused strings from string palette.""" 537 538 SPELLNAME = "fix_cleanstringpalette" 539 READONLY = False 540636542 """Helper function to substitute strings in the string palette, 543 to allow subclasses of this spell can modify the strings. 544 This implementation returns string unmodified. 545 """ 546 return old_string547549 # only run the spell if there is a string palette block 550 return self.inspectblocktype(NifFormat.NiStringPalette)551553 # only inspect branches where NiControllerSequence can occur 554 return isinstance(branch, (NifFormat.NiAVObject, 555 NifFormat.NiControllerManager, 556 NifFormat.NiControllerSequence))557559 """Parses string palette of either a single controller sequence, 560 or of all controller sequences in a controller manager. 561 562 >>> seq = NifFormat.NiControllerSequence() 563 >>> seq.string_palette = NifFormat.NiStringPalette() 564 >>> block = seq.add_controlled_block() 565 >>> block.string_palette = seq.string_palette 566 >>> block.set_variable_1("there") 567 >>> block.set_node_name("hello") 568 >>> block.string_palette.palette.add_string("test") 569 12 570 >>> seq.string_palette.palette.get_all_strings() 571 ['there', 'hello', 'test'] 572 >>> SpellCleanStringPalette().branchentry(seq) 573 pyffi.toaster:INFO:parsing string palette 574 False 575 >>> seq.string_palette.palette.get_all_strings() 576 ['hello', 'there'] 577 >>> block.get_variable_1() 578 'there' 579 >>> block.get_node_name() 580 'hello' 581 """ 582 if isinstance(branch, (NifFormat.NiControllerManager, 583 NifFormat.NiControllerSequence)): 584 # get list of controller sequences 585 if isinstance(branch, NifFormat.NiControllerManager): 586 # multiple controller sequences sharing a single 587 # string palette 588 if not branch.controller_sequences: 589 # no controller sequences: nothing to do 590 return False 591 controller_sequences = branch.controller_sequences 592 else: 593 # unmanaged controller sequence 594 controller_sequences = [branch] 595 # and clean their string palettes 596 self.toaster.msg("parsing string palette") 597 # use the first string palette as reference 598 string_palette = controller_sequences[0].string_palette 599 palette = string_palette.palette 600 # 1) calculate number of strings, for reporting 601 # (this assumes that all blocks already use the same 602 # string palette!) 603 num_strings = len(palette.get_all_strings()) 604 # 2) substitute strings 605 # first convert the controlled block strings to the old style 606 # (storing the actual string, and not just an offset into the 607 # string palette) 608 for controller_sequence in controller_sequences: 609 for block in controller_sequence.controlled_blocks: 610 # set old style strings from string palette strings 611 block.node_name = self.substitute(block.get_node_name()) 612 block.property_type = self.substitute(block.get_property_type()) 613 block.controller_type = self.substitute(block.get_controller_type()) 614 block.variable_1 = self.substitute(block.get_variable_1()) 615 block.variable_2 = self.substitute(block.get_variable_2()) 616 # ensure single string palette for all controlled blocks 617 block.string_palette = string_palette 618 # ensure single string palette for all controller sequences 619 controller_sequence.string_palette = string_palette 620 # clear the palette 621 palette.clear() 622 # and then convert old style back to new style 623 for controller_sequence in controller_sequences: 624 for block in controller_sequence.controlled_blocks: 625 block.set_node_name(block.node_name) 626 block.set_property_type(block.property_type) 627 block.set_controller_type(block.controller_type) 628 block.set_variable_1(block.variable_1) 629 block.set_variable_2(block.variable_2) 630 self.changed = True 631 # do not recurse further 632 return False 633 else: 634 # keep looking for managers or sequences 635 return True638 """Remove root branches that shouldn't be root branches and are 639 unused in the file such as NiProperty branches that are not 640 properly parented. 641 """ 642 643 SPELLNAME = "fix_delunusedroots" 644 READONLY = False 645680647 if self.inspectblocktype(NifFormat.NiAVObject): 648 # check last 8 bytes 649 pos = self.stream.tell() 650 try: 651 self.stream.seek(-8, 2) 652 if self.stream.read(8) == '\x01\x00\x00\x00\x00\x00\x00\x00': 653 # standard nif with single root: do not remove anything 654 # and quit early without reading the full file 655 return False 656 else: 657 return True 658 finally: 659 self.stream.seek(pos) 660 else: 661 return False662664 # make list of good roots 665 good_roots = [ 666 root for root in self.data.roots 667 if isinstance(root, (NifFormat.NiAVObject, 668 NifFormat.NiSequence, 669 NifFormat.NiPixelData, 670 NifFormat.NiPhysXProp, 671 NifFormat.NiSequenceStreamHelper))] 672 # if actual roots differ from good roots set roots to good 673 # roots and report 674 if self.data.roots != good_roots: 675 self.toaster.msg("removing %i bad roots" 676 % (len(self.data.roots) - len(good_roots))) 677 self.data.roots = good_roots 678 self.changed = True 679 return False682 """Fix bad subshape vertex counts in bhkPackedNiTriStripsShape blocks.""" 683 684 SPELLNAME = "fix_bhksubshapes" 685 READONLY = False 686 689734691 # only inspect the NiAVObject branch and collision branch 692 return isinstance(branch, ( 693 NifFormat.NiAVObject, 694 NifFormat.bhkCollisionObject, 695 NifFormat.bhkRefObject))696698 if isinstance(branch, NifFormat.bhkPackedNiTriStripsShape): 699 if not branch.data: 700 # no data... this is weird, but let's just ignore it 701 return False 702 # calculate number of vertices in subshapes 703 num_verts_in_sub_shapes = sum( 704 (sub_shape.num_vertices 705 for sub_shape in branch.get_sub_shapes()), 0) 706 if num_verts_in_sub_shapes != branch.data.num_vertices: 707 self.toaster.logger.warn( 708 "bad subshape vertex count (expected %i, got %i)" 709 % (branch.data.num_vertices, num_verts_in_sub_shapes)) 710 # remove or add vertices from subshapes (start with the last) 711 for sub_shape in reversed(branch.get_sub_shapes()): 712 self.toaster.msg("fixing count in subshape") 713 # calculate new number of vertices 714 # if everything were to be fixed with this shape 715 sub_shape_num_vertices = ( 716 sub_shape.num_vertices 717 + branch.data.num_vertices 718 - num_verts_in_sub_shapes) 719 if sub_shape_num_vertices > 0: 720 # we can do everything in the last shape 721 # so do it 722 sub_shape.num_vertices = sub_shape_num_vertices 723 break 724 else: 725 # too many to remove... 726 # first remove everything from this shape 727 # the remainder will come from the following shapes 728 num_verts_in_sub_shapes -= sub_shape.num_vertices 729 sub_shape.num_vertices = 0 730 # no need to recurse further 731 return False 732 # recurse further 733 return True736 """Fix empty skeleton roots in an as sane as possible way.""" 737 738 SPELLNAME = "fix_emptyskeletonroots" 739 READONLY = False 740781742 # only run the spell if there is a skin instance block 743 return self.inspectblocktype(NifFormat.NiSkinInstance)744746 # set skeleton root: first block of data 747 # this is what the engine usually seems to assume if it is not present 748 if not self.data.roots: 749 return False 750 self.skeleton_root = self.data.roots[0] 751 # sanity check 752 if not isinstance(self.skeleton_root, NifFormat.NiAVObject): 753 # we'll fail in this case... 754 self.skeleton_root = None 755 self.toaster.logger.info("no skeleton root candidate") 756 return False 757 return True758760 # only inspect branches where NiSkinInstance can occur 761 return isinstance(branch, (NifFormat.NiAVObject, 762 NifFormat.NiSkinInstance))763765 if isinstance(branch, NifFormat.NiSkinInstance): 766 if not branch.skeleton_root: 767 if self.skeleton_root: 768 self.toaster.logger.warn( 769 "fixed missing skeleton root") 770 branch.skeleton_root = self.skeleton_root 771 self.changed = True 772 else: 773 self.toaster.logger.error( 774 "missing skeleton root, " 775 "but no skeleton root candidate!") 776 # do not recurse further 777 return False 778 else: 779 # keep looking for managers or sequences 780 return True
Home | Trees | Indices | Help |
|
---|
Generated by Epydoc 3.0.1 on Mon Oct 10 19:04:05 2011 | http://epydoc.sourceforge.net |