Module import_nif
[hide private]
[frames] | no frames]

Source Code for Module import_nif

   1  #!BPY 
   2   
   3  """  
   4  Name: 'NetImmerse/Gamebryo (.nif & .kf & .egm)' 
   5  Blender: 245 
   6  Group: 'Import' 
   7  Tip: 'Import NIF File Format (.nif & .kf & .egm)' 
   8  """ 
   9   
  10   
  11  __author__ = "The NifTools team, http://niftools.sourceforge.net/" 
  12  __url__ = ("blender", "elysiun", "http://niftools.sourceforge.net/") 
  13  __bpydoc__ = """\ 
  14  This script imports Netimmerse and Gamebryo .NIF files to Blender. 
  15  """ 
  16   
  17  import Blender 
  18  from Blender.Mathutils import * 
  19   
  20  from nif_common import NifImportExport 
  21  from nif_common import NifConfig 
  22  from nif_common import NifFormat 
  23  from nif_common import EgmFormat 
  24  from nif_common import __version__ 
  25   
  26  from itertools import izip 
  27  import logging 
  28  import math 
  29  import operator 
  30   
  31  import pyffi.utils.quickhull 
  32  import pyffi.spells.nif 
  33  import pyffi.spells.nif.fix 
  34   
  35  # -------------------------------------------------------------------------- 
  36  # ***** BEGIN LICENSE BLOCK ***** 
  37  #  
  38  # BSD License 
  39  #  
  40  # Copyright (c) 2005-2009, NIF File Format Library and Tools 
  41  # All rights reserved. 
  42  #  
  43  # Redistribution and use in source and binary forms, with or without 
  44  # modification, are permitted provided that the following conditions 
  45  # are met: 
  46  # 1. Redistributions of source code must retain the above copyright 
  47  #    notice, this list of conditions and the following disclaimer. 
  48  # 2. Redistributions in binary form must reproduce the above copyright 
  49  #    notice, this list of conditions and the following disclaimer in the 
  50  #    documentation and/or other materials provided with the distribution. 
  51  # 3. The name of the NIF File Format Library and Tools project may not be 
  52  #    used to endorse or promote products derived from this software 
  53  #    without specific prior written permission. 
  54  #  
  55  # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 
  56  # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
  57  # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
  58  # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 
  59  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
  60  # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
  61  # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
  62  # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
  63  # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
  64  # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
  65  # 
  66  # ***** END LICENSE BLOCK ***** 
  67  # -------------------------------------------------------------------------- 
  68   
69 -class NifImportError(StandardError):
70 """A simple custom exception class for import errors.""" 71 pass
72
73 -class NifImport(NifImportExport):
74 """A class which bundles the main import function along with all helper 75 functions and data shared between these functions.""" 76 # class constants: 77 # correction matrices list, the order is +X, +Y, +Z, -X, -Y, -Z 78 BONE_CORRECTION_MATRICES = ( 79 Matrix([ 0.0,-1.0, 0.0],[ 1.0, 0.0, 0.0],[ 0.0, 0.0, 1.0]), 80 Matrix([ 1.0, 0.0, 0.0],[ 0.0, 1.0, 0.0],[ 0.0, 0.0, 1.0]), 81 Matrix([ 1.0, 0.0, 0.0],[ 0.0, 0.0, 1.0],[ 0.0,-1.0, 0.0]), 82 Matrix([ 0.0, 1.0, 0.0],[-1.0, 0.0, 0.0],[ 0.0, 0.0, 1.0]), 83 Matrix([-1.0, 0.0, 0.0],[ 0.0,-1.0, 0.0],[ 0.0, 0.0, 1.0]), 84 Matrix([ 1.0, 0.0, 0.0],[ 0.0, 0.0,-1.0],[ 0.0, 1.0, 0.0]) ) 85 # identity matrix, for comparisons 86 IDENTITY44 = Matrix( [ 1.0, 0.0, 0.0, 0.0], 87 [ 0.0, 1.0, 0.0, 0.0], 88 [ 0.0, 0.0, 1.0, 0.0], 89 [ 0.0, 0.0, 0.0, 1.0] ) 90 # radians to degrees conversion constant 91 R2D = 3.14159265358979/180.0 92
93 - def __init__(self, **config):
94 """Main import function: open file and import all trees.""" 95 96 # initialize progress bar 97 self.msg_progress("Initializing", progbar = 0) 98 99 # store config settings 100 for name, value in config.iteritems(): 101 setattr(self, name, value) 102 103 # shortcut to import logger 104 self.logger = logging.getLogger("niftools.blender.import") 105 106 # save file name 107 self.filename = self.IMPORT_FILE[:] 108 self.filepath = Blender.sys.dirname(self.filename) 109 self.filebase, self.fileext = Blender.sys.splitext( 110 Blender.sys.basename(self.filename)) 111 112 # dictionary of texture files, to reuse textures 113 self.textures = {} 114 115 # dictionary of materials, to reuse materials 116 self.materials = {} 117 118 # dictionary of names, to map NIF blocks to correct Blender names 119 self.names = {} 120 121 # dictionary of bones, maps Blender name to NIF block 122 self.blocks = {} 123 124 # dictionary of bones, maps Blender bone name to matrix that maps the 125 # NIF bone matrix on the Blender bone matrix 126 # B' = X * B, where B' is the Blender bone matrix, and B is the NIF bone matrix 127 self.bonesExtraMatrix = {} 128 129 # dictionary of bones that belong to a certain armature 130 # maps NIF armature name to list of NIF bone name 131 self.armatures = {} 132 133 # bone animation priorities (maps bone NiNode to priority number); 134 # priorities are set in importKfRoot and are stored into the name 135 # of a NULL constraint (for lack of something better) in 136 # importArmature 137 self.bonePriorities = {} 138 139 # dictionary mapping bhkRigidBody objects to list of objects imported 140 # in Blender; after we've imported the tree, we use this dictionary 141 # to set the physics constraints (ragdoll etc) 142 self.havokObjects = {} 143 144 # Blender scene 145 self.scene = Blender.Scene.GetCurrent() 146 147 # selected objects 148 # find and store this list now, as creating new objects adds them 149 # to the selection list 150 self.selectedObjects = [ob for ob in self.scene.objects.selected] 151 152 # catch NifImportError 153 try: 154 # check that one armature is selected in 'import geometry + parent 155 # to armature' mode 156 if self.IMPORT_SKELETON == 2: 157 if len(self.selectedObjects) != 1 or self.selectedObjects[0].getType() != 'Armature': 158 raise NifImportError("You must select exactly one armature in 'Import Geometry Only + Parent To Selected Armature' mode.") 159 160 # open file for binary reading 161 self.logger.info("Importing %s" % self.filename) 162 niffile = open(self.filename, "rb") 163 data = NifFormat.Data() 164 try: 165 # check if nif file is valid 166 data.inspect(niffile) 167 self.version = data.version 168 if self.version >= 0: 169 # it is valid, so read the file 170 self.logger.info("NIF file version: 0x%08X" % self.version) 171 self.msg_progress("Reading file") 172 data.read(niffile) 173 root_blocks = data.roots 174 elif self.version == -1: 175 raise NifImportError("Unsupported NIF version.") 176 else: 177 raise NifImportError("Not a NIF file.") 178 finally: 179 # the file has been read or an error occurred: close file 180 niffile.close() 181 182 if self.IMPORT_KEYFRAMEFILE: 183 # open keyframe file for binary reading 184 self.logger.info("Importing %s" % self.IMPORT_KEYFRAMEFILE) 185 kffile = open(self.IMPORT_KEYFRAMEFILE, "rb") 186 kfdata = NifFormat.Data() 187 try: 188 # check if kf file is valid 189 kfdata.inspect(kffile) 190 self.kfversion = kfdata.version 191 if self.kfversion >= 0: 192 # it is valid, so read the file 193 self.logger.info("KF file version: 0x%08X" % self.kfversion) 194 self.msg_progress("Reading keyframe file") 195 kfdata.read(kffile) 196 kf_root_blocks = kfdata.roots 197 elif self.kfversion == -1: 198 raise NifImportError("Unsupported KF version.") 199 else: 200 raise NifImportError("Not a KF file.") 201 finally: 202 # the file has been read or an error occurred: close file 203 kffile.close() 204 else: 205 kf_root_blocks = [] 206 207 if self.IMPORT_EGMFILE: 208 # open facegen egm file for binary reading 209 self.logger.info("Importing %s" % self.IMPORT_EGMFILE) 210 egmfile = open(self.IMPORT_EGMFILE, "rb") 211 self.egmdata = EgmFormat.Data() 212 try: 213 # check if kf file is valid 214 self.egmdata.inspect(egmfile) 215 if self.egmdata.version >= 0: 216 # it is valid, so read the file 217 self.logger.info("EGM file version: %03i" 218 % self.egmdata.version) 219 self.msg_progress("Reading FaceGen egm file") 220 self.egmdata.read(egmfile) 221 # scale the data 222 self.egmdata.apply_scale(self.IMPORT_SCALE_CORRECTION) 223 elif self.egmdata.version == -1: 224 raise NifImportError("Unsupported EGM version.") 225 else: 226 raise NifImportError("Not an EGM file.") 227 finally: 228 # the file has been read or an error occurred: close file 229 egmfile.close() 230 else: 231 self.egmdata = None 232 233 self.msg_progress("Importing data") 234 # calculate and set frames per second 235 if self.IMPORT_ANIMATION: 236 self.fps = self.getFramesPerSecond(root_blocks + kf_root_blocks) 237 self.scene.getRenderingContext().fps = self.fps 238 239 # import all root blocks 240 for block in root_blocks: 241 root = block 242 # root hack for corrupt better bodies meshes 243 # and remove geometry from better bodies on skeleton import 244 for b in (b for b in block.tree() 245 if isinstance(b, NifFormat.NiGeometry) 246 and b.isSkin()): 247 # check if root belongs to the children list of the 248 # skeleton root (can only happen for better bodies meshes) 249 if root in [c for c in b.skinInstance.skeletonRoot.children]: 250 # fix parenting and update transform accordingly 251 b.skinInstance.data.setTransform( 252 root.getTransform() 253 * b.skinInstance.data.getTransform()) 254 b.skinInstance.skeletonRoot = root 255 # delete non-skeleton nodes if we're importing 256 # skeleton only 257 if self.IMPORT_SKELETON == 1: 258 nonbip_children = (child for child in root.children 259 if child.name[:6] != 'Bip01 ') 260 for child in nonbip_children: 261 root.removeChild(child) 262 # import this root block 263 self.logger.debug("Root block: %s" % root.getGlobalDisplay()) 264 # merge animation from kf tree into nif tree 265 if self.IMPORT_ANIMATION: 266 for kf_root in kf_root_blocks: 267 self.importKfRoot(kf_root, root) 268 # import the nif tree 269 self.importRoot(root) 270 except NifImportError, e: # in that case, we raise a menu too 271 self.logger.exception('NifImportError: %s' % e) 272 Blender.Draw.PupMenu('ERROR%t|' + str(e)) 273 raise 274 finally: 275 # clear progress bar 276 self.msg_progress("Finished", progbar = 1) 277 # do a full scene update to ensure that transformations are applied 278 self.scene.update(1) 279 280 # save nif root blocks (used by test suites) 281 self.root_blocks = root_blocks
282 283
284 - def importRoot(self, root_block):
285 """Main import function.""" 286 # preprocessing: 287 288 # check that this is not a kf file 289 if isinstance(root_block, 290 (NifFormat.NiSequence, 291 NifFormat.NiSequenceStreamHelper)): 292 raise NifImportError("direct .kf import not supported") 293 294 # merge skeleton roots 295 # and transform geometry into the rest pose 296 # fake a data element with given root, for spells 297 # TODO: use data element directly, i.e. upgrade this importRoot 298 # function to an importData function 299 data = NifFormat.Data() 300 data.roots = [root_block] 301 if self.IMPORT_MERGESKELETONROOTS: 302 pyffi.spells.nif.fix.SpellMergeSkeletonRoots(data=data).recurse() 303 if self.IMPORT_SENDGEOMETRIESTOBINDPOS: 304 pyffi.spells.nif.fix.SpellSendGeometriesToBindPosition(data=data).recurse() 305 if self.IMPORT_SENDDETACHEDGEOMETRIESTONODEPOS: 306 pyffi.spells.nif.fix.SpellSendDetachedGeometriesToNodePosition(data=data).recurse() 307 if self.IMPORT_SENDBONESTOBINDPOS: 308 pyffi.spells.nif.fix.SpellSendBonesToBindPosition(data=data).recurse() 309 if self.IMPORT_APPLYSKINDEFORM: 310 for niBlock in root_block.tree(unique=True): 311 if not isinstance(niBlock, NifFormat.NiGeometry): 312 continue 313 if not niBlock.isSkin(): 314 continue 315 self.logger.info('Applying skin deformation on geometry %s' 316 % niBlock.name) 317 vertices, normals = niBlock.getSkinDeformation() 318 for vold, vnew in izip(niBlock.data.vertices, vertices): 319 vold.x = vnew.x 320 vold.y = vnew.y 321 vold.z = vnew.z 322 323 # sets the root block parent to None, so that when crawling back the 324 # script won't barf 325 root_block._parent = None 326 327 # set the block parent through the tree, to ensure I can always move 328 # backward 329 self.set_parents(root_block) 330 331 # scale tree 332 toaster = pyffi.spells.nif.NifToaster() 333 toaster.scale = self.IMPORT_SCALE_CORRECTION 334 pyffi.spells.nif.fix.SpellScale(data=data, toaster=toaster).recurse() 335 336 # mark armature nodes and bones 337 self.markArmaturesBones(root_block) 338 339 # import the keyframe notes 340 if self.IMPORT_ANIMATION: 341 self.importTextkey(root_block) 342 343 # read the NIF tree 344 if self.is_armature_root(root_block): 345 # special case 1: root node is skeleton root 346 self.logger.debug("%s is an armature root" % root_block.name) 347 b_obj = self.importBranch(root_block) 348 elif self.is_grouping_node(root_block): 349 # special case 2: root node is grouping node 350 self.logger.debug("%s is a grouping node" % root_block.name) 351 b_obj = self.importBranch(root_block) 352 elif isinstance(root_block, NifFormat.NiTriBasedGeom): 353 # trishape/tristrips root 354 b_obj = self.importBranch(root_block) 355 elif isinstance(root_block, NifFormat.NiNode): 356 # root node is dummy scene node 357 # process collision 358 if root_block.collisionObject: 359 bhk_body = root_block.collisionObject.body 360 if not isinstance(bhk_body, NifFormat.bhkRigidBody): 361 self.logger.warning("Unsupported collision structure under node %s" % root_block.name) 362 self.importBhkShape(bhk_body) 363 # process all its children 364 for child in root_block.children: 365 b_obj = self.importBranch(child) 366 elif isinstance(root_block, NifFormat.NiCamera): 367 self.logger.warning('Skipped NiCamera root') 368 elif isinstance(root_block, NifFormat.NiPhysXProp): 369 self.logger.warning('Skipped NiPhysXProp root') 370 else: 371 self.logger.warning( 372 "Skipped unsupported root block type '%s' (corrupted nif?)." 373 % root_block.__class__) 374 375 # store bone matrix offsets for re-export 376 if self.bonesExtraMatrix: 377 self.storeBonesExtraMatrix() 378 379 # store original names for re-export 380 if self.names: 381 self.storeNames() 382 383 # now all havok objects are imported, so we are 384 # ready to import the havok constraints 385 for hkbody in self.havokObjects: 386 self.importHavokConstraints(hkbody) 387 388 # parent selected meshes to imported skeleton 389 if self.IMPORT_SKELETON == 1: 390 # rename vertex groups to reflect bone names 391 # (for blends imported with older versions of the scripts!) 392 for b_child_obj in self.selectedObjects: 393 if b_child_obj.getType() == "Mesh": 394 for oldgroupname in b_child_obj.data.getVertGroupNames(): 395 newgroupname = self.get_bone_name_for_blender(oldgroupname) 396 if oldgroupname != newgroupname: 397 self.logger.info( 398 "%s: renaming vertex group %s to %s" 399 % (b_child_obj, oldgroupname, newgroupname)) 400 b_child_obj.data.renameVertGroup( 401 oldgroupname, newgroupname) 402 # set parenting 403 b_obj.makeParentDeform(self.selectedObjects)
404
405 - def importBranch(self, niBlock):
406 """Read the content of the current NIF tree branch to Blender 407 recursively.""" 408 self.msg_progress("Importing data") 409 if niBlock: 410 if isinstance(niBlock, NifFormat.NiTriBasedGeom) \ 411 and self.IMPORT_SKELETON == 0: 412 # it's a shape node and we're not importing skeleton only 413 # (IMPORT_SKELETON == 1) and not importing skinned geometries 414 # only (IMPORT_SKELETON == 2) 415 self.logger.debug("Building mesh in importBranch") 416 return self.importMesh(niBlock) 417 elif isinstance(niBlock, NifFormat.NiNode): 418 children = niBlock.children 419 bbox = self.find_extra(niBlock, NifFormat.BSBound) 420 if children or niBlock.collisionObject or bbox or self.IMPORT_EXTRANODES: 421 # it's a parent node 422 # import object + children 423 if self.is_armature_root(niBlock): 424 # all bones in the tree are also imported by 425 # importArmature 426 if self.IMPORT_SKELETON != 2: 427 b_obj = self.importArmature(niBlock) 428 else: 429 b_obj = self.selectedObjects[0] 430 self.logger.info("Merging nif tree '%s' with armature '%s'" 431 % (niBlock.name, b_obj.name)) 432 if niBlock.name != b_obj.name: 433 self.logger.warning( 434 "Taking nif block '%s' as armature '%s' but names do not match" 435 % (niBlock.name, b_obj.name)) 436 # now also do the meshes 437 self.importArmatureBranch(b_obj, niBlock, niBlock) 438 else: 439 # is it a grouping node? 440 geom_group = self.is_grouping_node(niBlock) 441 # if importing animation, remove children that have 442 # morph controllers from geometry group 443 if self.IMPORT_ANIMATION: 444 for child in geom_group: 445 if self.find_controller( 446 child, NifFormat.NiGeomMorpherController): 447 geom_group.remove(child) 448 # import geometry/empty + remaining children 449 if (not geom_group 450 or not self.IMPORT_COMBINESHAPES 451 or len(geom_group) > 16): 452 # no grouping node, or too many materials to 453 # group the geometry into a single mesh 454 # so import it as an empty 455 b_obj = self.importEmpty(niBlock) 456 geom_group = [] 457 else: 458 # node groups geometries, so import it as a mesh 459 self.logger.info( 460 "joining geometries %s to single object '%s'" 461 %([child.name for child in geom_group], 462 niBlock.name)) 463 b_obj = None 464 for child in geom_group: 465 b_obj = self.importMesh(child, 466 group_mesh = b_obj, 467 applytransform = True) 468 b_obj.name = self.importName(niBlock, 22) 469 # settings for collision node 470 if isinstance(niBlock, NifFormat.RootCollisionNode): 471 b_obj.setDrawType( 472 Blender.Object.DrawTypes['BOUNDBOX']) 473 b_obj.setDrawMode(32) # wire 474 b_obj.rbShapeBoundType = \ 475 Blender.Object.RBShapes['POLYHEDERON'] 476 # also remove duplicate vertices 477 b_mesh = b_obj.getData(mesh=True) 478 numverts = len(b_mesh.verts) 479 # 0.005 = 1/200 480 numdel = b_mesh.remDoubles(0.005) 481 if numdel: 482 self.logger.info('Removed %i duplicate vertices (out of %i) from collision mesh' % (numdel, numverts)) 483 # import children that aren't part of the geometry group 484 b_children_list = [] 485 children = [ child for child in niBlock.children 486 if child not in geom_group ] 487 for child in children: 488 b_child_obj = self.importBranch(child) 489 if b_child_obj: 490 b_children_list.append(b_child_obj) 491 b_obj.makeParent(b_children_list) 492 493 # if not importing skeleton only 494 if self.IMPORT_SKELETON != 1: 495 # import collision objects 496 if niBlock.collisionObject: 497 bhk_body = niBlock.collisionObject.body 498 if not isinstance(bhk_body, NifFormat.bhkRigidBody): 499 self.logger.warning("Unsupported collision structure under node %s" % niBlock.name) 500 collision_objs = self.importBhkShape(bhk_body) 501 # make parent 502 b_obj.makeParent(collision_objs) 503 504 # import bounding box 505 if bbox: 506 b_obj.makeParent([self.importBSBound(bbox)]) 507 508 # track camera for billboard nodes 509 if isinstance(niBlock, NifFormat.NiBillboardNode): 510 # find camera object 511 for obj in self.scene.objects: 512 if obj.getType() == "Camera": 513 break 514 else: 515 raise NifImportError("""Scene needs camera for billboard node (add a camera and try again)""") 516 # make b_obj track camera object 517 #b_obj.setEuler(0,0,0) 518 b_obj.constraints.append( 519 Blender.Constraint.Type.TRACKTO) 520 self.logger.warning("""Constraint for billboard node on %s added but target not set due to transform bug in Blender. Set target to Camera manually.""" % b_obj) 521 constr = b_obj.constraints[-1] 522 constr[Blender.Constraint.Settings.TRACK] = Blender.Constraint.Settings.TRACKZ 523 constr[Blender.Constraint.Settings.UP] = Blender.Constraint.Settings.UPY 524 # yields transform bug! 525 #constr[Blender.Constraint.Settings.TARGET] = obj 526 527 # set object transform 528 # this must be done after all children objects have been 529 # parented to b_obj 530 b_obj.setMatrix(self.importMatrix(niBlock)) 531 532 # import the animations 533 if self.IMPORT_ANIMATION: 534 self.set_animation(niBlock, b_obj) 535 # import the extras 536 self.importTextkey(niBlock) 537 538 return b_obj 539 # all else is currently discarded 540 return None
541
542 - def importArmatureBranch( 543 self, b_armature, niArmature, niBlock, group_mesh = None):
544 """Reads the content of the current NIF tree branch to Blender 545 recursively, as meshes parented to a given armature or parented 546 to the closest bone in the armature. Note that 547 niArmature must have been imported previously as an armature, along 548 with all its bones. This function only imports meshes and armature 549 ninodes. 550 551 If this imports a mesh, then it returns the a nif block and a mesh 552 object. The nif block is the one relative to which the mesh transform 553 is calculated (so the mesh should be parented to the blender object 554 corresponding to this block by the caller). It corresponds either to 555 a Blender bone or to a Blender armature.""" 556 # check if the block is non-null 557 if not niBlock: 558 return None, None 559 # get the block that is considered as parent of this block within this 560 # branch; this is a block corresponding to a bone in Blender 561 # if niBlock is a bone, then this is the branch parent 562 if self.is_bone(niBlock): 563 branch_parent = niBlock 564 # otherwise, find closest bone in this armature 565 else: 566 branch_parent = self.get_closest_bone(niBlock, skelroot = niArmature) 567 # no bone parent, so then simply take nif block of armature object as 568 # parent 569 if not branch_parent: 570 branch_parent = niArmature 571 # is it a mesh? 572 if isinstance(niBlock, NifFormat.NiTriBasedGeom) \ 573 and self.IMPORT_SKELETON != 1: 574 575 self.logger.debug("Building mesh %s in importArmatureBranch" % 576 niBlock.name) 577 # apply transform relative to the branch parent 578 return branch_parent, self.importMesh(niBlock, 579 group_mesh = group_mesh, 580 applytransform = True, 581 relative_to = branch_parent) 582 # is it another armature? 583 elif self.is_armature_root(niBlock) and niBlock != niArmature: 584 # an armature parented to this armature 585 fb_arm = self.importArmature(niBlock) 586 # import the armature branch 587 self.importArmatureBranch(fb_arm, niBlock, niBlock) 588 return branch_parent, fb_arm # the matrix will be set by the caller 589 # is it a NiNode in the niArmature tree (possibly niArmature itself, 590 # on first call)? 591 elif isinstance(niBlock, NifFormat.NiNode): 592 # import children 593 if niBlock.children: 594 # check if geometries should be merged on import 595 node_name = niBlock.name 596 geom_group = self.is_grouping_node(niBlock) 597 geom_other = [ child for child in niBlock.children 598 if not child in geom_group ] 599 b_objects = [] # list of (nif block, blender object) pairs 600 # import grouped geometries 601 if (geom_group 602 and self.IMPORT_COMBINESHAPES 603 and self.IMPORT_SKELETON != 1): 604 self.logger.info( 605 "joining geometries %s to single object '%s'" 606 %([child.name for child in geom_group], node_name)) 607 b_mesh = None 608 for child in geom_group: 609 b_mesh_branch_parent, b_mesh = self.importArmatureBranch( 610 b_armature, niArmature, child, group_mesh = b_mesh) 611 assert(b_mesh_branch_parent == branch_parent) # DEBUG 612 if b_mesh: 613 b_mesh.name = self.importName(niBlock) 614 b_objects.append((niBlock, branch_parent, b_mesh)) 615 # import other objects 616 for child in geom_other: 617 b_obj_branch_parent, b_obj = self.importArmatureBranch( 618 b_armature, niArmature, child, group_mesh = None) 619 if b_obj: 620 b_objects.append((child, b_obj_branch_parent, b_obj)) 621 # fix transform and parentship 622 for child, b_obj_branch_parent, b_obj in b_objects: 623 # note, b_obj is either a mesh or an armature 624 # check if it is parented to a bone or not 625 if b_obj_branch_parent is not niArmature: 626 # object was parented to a bone 627 628 # get parent bone and its name 629 b_par_bone_name = self.names[branch_parent] 630 b_par_bone = b_armature.data.bones[b_par_bone_name] 631 632 # get transform matrix of collision object; 633 # note that this matrix is already relative to 634 # branch_parent because the call to importMesh has 635 # relative_to = branch_parent 636 b_obj_matrix = Blender.Mathutils.Matrix( 637 b_obj.getMatrix()) 638 # fix transform 639 # the bone has in the nif file an armature space transform 640 # given by niBlock.getTransform(relative_to = niArmature) 641 # 642 # in detail: 643 # a vertex in the collision object has global 644 # coordinates 645 # v * Z * B 646 # with v the vertex, Z the object transform 647 # (currently b_obj_matrix) 648 # and B the nif bone matrix 649 650 # in Blender however a vertex has coordinates 651 # v * O * T * B' 652 # with B' the Blender bone matrix 653 # so we need that 654 # Z * B = O * T * B' or equivalently 655 # O = Z * B * B'^{-1} * T^{-1} 656 # = Z * X^{-1} * T^{-1} 657 # since 658 # B' = X * B 659 # with X = self.bonesExtraMatrix[B] 660 661 # post multiply Z with X^{-1} 662 extra = Blender.Mathutils.Matrix( 663 self.bonesExtraMatrix[branch_parent]) 664 extra.invert() 665 b_obj_matrix = b_obj_matrix * extra 666 # cancel out the tail translation T 667 # (the tail causes a translation along 668 # the local Y axis) 669 b_obj_matrix[3][1] -= b_par_bone.length 670 # set the matrix 671 b_obj.setMatrix(b_obj_matrix) 672 673 # make it parent of the bone 674 b_armature.makeParentBone( 675 [b_obj], self.names[b_obj_branch_parent]) 676 else: 677 # mesh is parented to the armature 678 # the transform has already been applied 679 # still need to make it parent of the armature 680 # (if b_obj is an armature then this falls back to the 681 # usual parenting) 682 b_armature.makeParentDeform([b_obj]) 683 684 # if not importing skeleton only 685 if self.IMPORT_SKELETON != 1: 686 # import collisions (only bhkNiCollisionObjects for now) 687 if isinstance(niBlock.collisionObject, 688 NifFormat.bhkNiCollisionObject): 689 # collision object parented to a bone 690 # first import collision object 691 bhk_body = niBlock.collisionObject.body 692 if not isinstance(bhk_body, NifFormat.bhkRigidBody): 693 self.logger.warning( 694 "Unsupported collision structure under node %s" 695 % niBlock.name) 696 collision_objs = self.importBhkShape(bhk_body) 697 # get blender bone and its name 698 # (TODO also cover case where niBlock != branch_parent) 699 if niBlock != branch_parent: 700 self.logger.warning("Collision object has non-bone parent, this is not supported and transform errors may result") 701 b_par_bone_name = self.names[branch_parent] 702 b_par_bone = b_armature.data.bones[b_par_bone_name] 703 for b_obj in collision_objs: 704 # get transform matrix of collision object 705 # this is relative to niBlock 706 # (TODO fix so it's relative to branch_parent!) 707 b_obj_matrix = b_obj.getMatrix() 708 # fix transform, see explanation above 709 # O = Z * X^{-1} * T^{-1} 710 extra = Blender.Mathutils.Matrix( 711 self.bonesExtraMatrix[branch_parent]) 712 extra.invert() 713 b_obj_matrix = b_obj_matrix * extra 714 b_obj_matrix[3][1] -= b_par_bone.length 715 # set the matrix 716 b_obj.setMatrix(b_obj_matrix) 717 # make it parent of the bone 718 b_armature.makeParentBone( 719 [b_obj], self.names[niBlock]) 720 721 ### import bounding box? 722 ### not supported within armature branch (no need so far) 723 724 # anything else: throw away 725 return None, None
726
727 - def importName(self, niBlock, max_length=22, postfix=""):
728 """Get unique name for an object, preserving existing names. 729 The maximum name length defaults to 22, since this is the 730 maximum for Blender objects. Bone names can reach length 32. 731 732 @param niBlock: A named nif block. 733 @type niBlock: C{NiObjectNET} 734 @param max_length: The maximum length of the name. 735 @type max_length: C{int} 736 @param postfix: Extra string to append to the name. 737 @type postfix: C{str} 738 """ 739 if niBlock in self.names: 740 if not postfix: 741 return self.names[niBlock] 742 else: 743 # force postfix! 744 # the same block may be imported with different postfixes 745 if self.names[niBlock].endswith(postfix): 746 return self.names[niBlock] 747 748 self.logger.debug("Importing name for %s block from %s%s" % (niBlock.__class__.__name__, niBlock.name, postfix)) 749 750 # find unique name for Blender to use 751 uniqueInt = 0 752 # strip null terminator from name 753 niBlock.name = niBlock.name.strip("\x00") 754 niName = niBlock.name 755 # if name is empty, create something non-empty 756 if not niName: 757 if isinstance(niBlock, NifFormat.RootCollisionNode): 758 niName = "collision" 759 else: 760 niName = "noname" 761 for uniqueInt in xrange(-1, 100): 762 # limit name length 763 if uniqueInt == -1: 764 if max_length - len(postfix) - 1 <= 0: 765 raise ValueError("Name postfix '%s' too long." % postfix) 766 shortName = niName[:max_length-len(postfix)-1] + postfix 767 else: 768 if max_length - len(postfix) - 4 <= 0: 769 raise ValueError("Name postfix '%s' too long." % postfix) 770 shortName = ('%s.%02d%s' 771 % (niName[:max_length-len(postfix)-4], 772 uniqueInt, 773 postfix)) 774 # bone naming convention for blender 775 shortName = self.get_bone_name_for_blender(shortName) 776 # make sure it is unique 777 try: 778 Blender.Object.Get(shortName) 779 except ValueError: 780 # short name not found in object list 781 pass 782 else: 783 # try another integer 784 continue 785 try: 786 Blender.Material.Get(shortName) 787 except NameError: 788 # short name not found in material list 789 break 790 else: 791 raise RuntimeError("Ran out of names.") 792 # save mapping 793 # block niBlock has Blender name shortName 794 self.names[niBlock] = shortName 795 # Blender name shortName corresponds to niBlock 796 self.blocks[shortName] = niBlock 797 self.logger.debug("Selected unique name %s" % shortName) 798 return shortName
799
800 - def importMatrix(self, niBlock, relative_to = None):
801 """Retrieves a niBlock's transform matrix as a Mathutil.Matrix.""" 802 return Matrix(*niBlock.getTransform(relative_to).asList())
803
804 - def decompose_srt(self, m):
805 """Decompose Blender transform matrix as a scale, rotation matrix, and 806 translation vector.""" 807 # get scale components 808 b_scale_rot = m.rotationPart() 809 b_scale_rot_T = Matrix(b_scale_rot) 810 b_scale_rot_T.transpose() 811 b_scale_rot_2 = b_scale_rot * b_scale_rot_T 812 b_scale = Vector(b_scale_rot_2[0][0] ** 0.5,\ 813 b_scale_rot_2[1][1] ** 0.5,\ 814 b_scale_rot_2[2][2] ** 0.5) 815 # and fix their sign 816 if (b_scale_rot.determinant() < 0): b_scale.negate() 817 # only uniform scaling 818 if (abs(b_scale[0]-b_scale[1]) >= self.EPSILON 819 or abs(b_scale[1]-b_scale[2]) >= self.EPSILON): 820 self.logger.warn( 821 "Corrupt rotation matrix in nif: geometry errors may result.") 822 b_scale = b_scale[0] 823 # get rotation matrix 824 b_rot = b_scale_rot * (1.0/b_scale) 825 # get translation 826 b_trans = m.translationPart() 827 # done! 828 return b_scale, b_rot, b_trans
829
830 - def importEmpty(self, niBlock):
831 """Creates and returns a grouping empty.""" 832 shortName = self.importName(niBlock, 22) 833 b_empty = self.scene.objects.new("Empty", shortName) 834 b_empty.properties['longName'] = niBlock.name 835 return b_empty
836
837 - def importArmature(self, niArmature):
838 """Scans an armature hierarchy, and returns a whole armature. 839 This is done outside the normal node tree scan to allow for positioning 840 of the bones before skins are attached.""" 841 armature_name = self.importName(niArmature,22) 842 843 b_armatureData = Blender.Armature.Armature() 844 b_armatureData.name = armature_name 845 b_armatureData.drawAxes = True 846 b_armatureData.envelopes = False 847 b_armatureData.vertexGroups = True 848 b_armatureData.drawType = Blender.Armature.STICK 849 b_armature = self.scene.objects.new(b_armatureData, armature_name) 850 851 # make armature editable and create bones 852 b_armatureData.makeEditable() 853 niChildBones = [child for child in niArmature.children 854 if self.is_bone(child)] 855 for niBone in niChildBones: 856 self.importBone( 857 niBone, b_armature, b_armatureData, niArmature) 858 b_armatureData.update() 859 860 # The armature has been created in editmode, 861 # now we are ready to set the bone keyframes. 862 if self.IMPORT_ANIMATION: 863 # create an action 864 action = Blender.Armature.NLA.NewAction() 865 action.setActive(b_armature) 866 # go through all armature pose bones 867 # see http://www.elysiun.com/forum/viewtopic.php?t=58693 868 self.msg_progress('Importing Animations') 869 for bone_name, b_posebone in b_armature.getPose().bones.items(): 870 # denote progress 871 self.msg_progress('Animation: %s' % bone_name) 872 self.logger.debug('Importing animation for bone %s' % bone_name) 873 niBone = self.blocks[bone_name] 874 875 # get bind matrix (NIF format stores full transformations in keyframes, 876 # but Blender wants relative transformations, hence we need to know 877 # the bind position for conversion). Since 878 # [ SRchannel 0 ] [ SRbind 0 ] [ SRchannel * SRbind 0 ] [ SRtotal 0 ] 879 # [ Tchannel 1 ] * [ Tbind 1 ] = [ Tchannel * SRbind + Tbind 1 ] = [ Ttotal 1 ] 880 # with 881 # 'total' the transformations as stored in the NIF keyframes, 882 # 'bind' the Blender bind pose, and 883 # 'channel' the Blender IPO channel, 884 # it follows that 885 # Schannel = Stotal / Sbind 886 # Rchannel = Rtotal * inverse(Rbind) 887 # Tchannel = (Ttotal - Tbind) * inverse(Rbind) / Sbind 888 bone_bm = self.importMatrix(niBone) # base pose 889 niBone_bind_scale, niBone_bind_rot, niBone_bind_trans = self.decompose_srt(bone_bm) 890 niBone_bind_rot_inv = Matrix(niBone_bind_rot) 891 niBone_bind_rot_inv.invert() 892 niBone_bind_quat_inv = niBone_bind_rot_inv.toQuat() 893 # we also need the conversion of the original matrix to the 894 # new bone matrix, say X, 895 # B' = X * B 896 # (with B' the Blender matrix and B the NIF matrix) because we 897 # need that 898 # C' * B' = X * C * B 899 # and therefore 900 # C' = X * C * B * inverse(B') = X * C * inverse(X), 901 # where X = B' * inverse(B) 902 # 903 # In detail: 904 # [ SRX 0 ] [ SRC 0 ] [ SRX 0 ] 905 # [ TX 1 ] * [ TC 1 ] * inverse( [ TX 1 ] ) = 906 # [ SRX * SRC 0 ] [ inverse(SRX) 0 ] 907 # [ TX * SRC + TC 1 ] * [ -TX * inverse(SRX) 1 ] = 908 # [ SRX * SRC * inverse(SRX) 0 ] 909 # [ (TX * SRC + TC - TX) * inverse(SRX) 1 ] 910 # Hence 911 # SC' = SX * SC / SX = SC 912 # RC' = RX * RC * inverse(RX) 913 # TC' = (TX * SC * RC + TC - TX) * inverse(RX) / SX 914 extra_matrix_scale, extra_matrix_rot, extra_matrix_trans = self.decompose_srt(self.bonesExtraMatrix[niBone]) 915 extra_matrix_quat = extra_matrix_rot.toQuat() 916 extra_matrix_rot_inv = Matrix(extra_matrix_rot) 917 extra_matrix_rot_inv.invert() 918 extra_matrix_quat_inv = extra_matrix_rot_inv.toQuat() 919 # now import everything 920 # ############################## 921 922 # get controller, interpolator, and data 923 # note: the NiKeyframeController check also includes 924 # NiTransformController (see hierarchy!) 925 kfc = self.find_controller(niBone, 926 NifFormat.NiKeyframeController) 927 # old style: data directly on controller 928 kfd = kfc.data if kfc else None 929 # new style: data via interpolator 930 kfi = kfc.interpolator if kfc else None 931 # next is a quick hack to make the new transform 932 # interpolator work as if it is an old style keyframe data 933 # block parented directly on the controller 934 if isinstance(kfi, NifFormat.NiTransformInterpolator): 935 kfd = kfi.data 936 # for now, in this case, ignore interpolator 937 kfi = None 938 939 # B-spline curve import 940 if isinstance(kfi, NifFormat.NiBSplineInterpolator): 941 times = list(kfi.getTimes()) 942 translations = list(kfi.getTranslations()) 943 scales = list(kfi.getScales()) 944 rotations = list(kfi.getRotations()) 945 946 # if we have translation keys, we make a dictionary of 947 # rot_keys and scale_keys, this makes the script work MUCH 948 # faster in most cases 949 if translations: 950 scale_keys_dict = {} 951 rot_keys_dict = {} 952 953 # scales: ignore for now, implement later 954 # should come here 955 scales = None 956 957 # rotations 958 if rotations: 959 self.logger.debug('Rotation keys...(bspline quaternions)') 960 for time, quat in izip(times, rotations): 961 frame = 1 + int(time * self.fps + 0.5) 962 quat = Blender.Mathutils.Quaternion( 963 [quat[0], quat[1], quat[2], quat[3]]) 964 # beware, CrossQuats takes arguments in a 965 # counter-intuitive order: 966 # q1.toMatrix() * q2.toMatrix() == CrossQuats(q2, q1).toMatrix() 967 quatVal = CrossQuats(niBone_bind_quat_inv, quat) # Rchannel = Rtotal * inverse(Rbind) 968 rot = CrossQuats(CrossQuats(extra_matrix_quat_inv, quatVal), extra_matrix_quat) # C' = X * C * inverse(X) 969 b_posebone.quat = rot 970 b_posebone.insertKey(b_armature, frame, 971 [Blender.Object.Pose.ROT]) 972 # fill optimizer dictionary 973 if translations: 974 rot_keys_dict[frame] = Blender.Mathutils.Quaternion(rot) 975 976 # translations 977 if translations: 978 self.logger.debug('Translation keys...(bspline)') 979 for time, translation in izip(times, translations): 980 # time 0.0 is frame 1 981 frame = 1 + int(time * self.fps + 0.5) 982 trans = Blender.Mathutils.Vector(*translation) 983 locVal = (trans - niBone_bind_trans) * niBone_bind_rot_inv * (1.0/niBone_bind_scale)# Tchannel = (Ttotal - Tbind) * inverse(Rbind) / Sbind 984 # the rotation matrix is needed at this frame (that's 985 # why the other keys are inserted first) 986 if rot_keys_dict: 987 try: 988 rot = rot_keys_dict[frame].toMatrix() 989 except KeyError: 990 # fall back on slow method 991 ipo = action.getChannelIpo(bone_name) 992 quat = Blender.Mathutils.Quaternion() 993 quat.x = ipo.getCurve('QuatX').evaluate(frame) 994 quat.y = ipo.getCurve('QuatY').evaluate(frame) 995 quat.z = ipo.getCurve('QuatZ').evaluate(frame) 996 quat.w = ipo.getCurve('QuatW').evaluate(frame) 997 rot = quat.toMatrix() 998 else: 999 rot = Blender.Mathutils.Matrix([1.0,0.0,0.0], 1000 [0.0,1.0,0.0], 1001 [0.0,0.0,1.0]) 1002 # we also need the scale at this frame 1003 if scale_keys_dict: 1004 try: 1005 sizeVal = scale_keys_dict[frame] 1006 except KeyError: 1007 ipo = action.getChannelIpo(bone_name) 1008 if ipo.getCurve('SizeX'): 1009 sizeVal = ipo.getCurve('SizeX').evaluate(frame) # assume uniform scale 1010 else: 1011 sizeVal = 1.0 1012 else: 1013 sizeVal = 1.0 1014 size = Blender.Mathutils.Matrix([sizeVal, 0.0, 0.0], 1015 [0.0, sizeVal, 0.0], 1016 [0.0, 0.0, sizeVal]) 1017 # now we can do the final calculation 1018 loc = (extra_matrix_trans * size * rot + locVal - extra_matrix_trans) * extra_matrix_rot_inv * (1.0/extra_matrix_scale) # C' = X * C * inverse(X) 1019 b_posebone.loc = loc 1020 b_posebone.insertKey(b_armature, frame, [Blender.Object.Pose.LOC]) 1021 1022 # delete temporary dictionaries 1023 if translations: 1024 del scale_keys_dict 1025 del rot_keys_dict 1026 1027 # NiKeyframeData and NiTransformData import 1028 elif isinstance(kfd, NifFormat.NiKeyframeData): 1029 1030 translations = kfd.translations 1031 scales = kfd.scales 1032 # if we have translation keys, we make a dictionary of 1033 # rot_keys and scale_keys, this makes the script work MUCH 1034 # faster in most cases 1035 if translations: 1036 scale_keys_dict = {} 1037 rot_keys_dict = {} 1038 # add the keys 1039 1040 # Scaling 1041 if scales.keys: 1042 self.logger.debug('Scale keys...') 1043 for scaleKey in scales.keys: 1044 # time 0.0 is frame 1 1045 frame = 1 + int(scaleKey.time * self.fps + 0.5) 1046 sizeVal = scaleKey.value 1047 size = sizeVal / niBone_bind_scale # Schannel = Stotal / Sbind 1048 b_posebone.size = Blender.Mathutils.Vector(size, size, size) 1049 b_posebone.insertKey(b_armature, frame, [Blender.Object.Pose.SIZE]) # this is very slow... :( 1050 # fill optimizer dictionary 1051 if translations: 1052 scale_keys_dict[frame] = size 1053 1054 # detect the type of rotation keys 1055 rotationType = kfd.rotationType 1056 1057 # Euler Rotations 1058 if rotationType == 4: 1059 # uses xyz rotation 1060 if kfd.xyzRotations[0].keys: 1061 self.logger.debug('Rotation keys...(euler)') 1062 for xkey, ykey, zkey in izip(kfd.xyzRotations[0].keys, 1063 kfd.xyzRotations[1].keys, 1064 kfd.xyzRotations[2].keys): 1065 # time 0.0 is frame 1 1066 # XXX it is assumed that all the keys have the 1067 # XXX same times!!! 1068 if (abs(xkey.time - ykey.time) > self.EPSILON 1069 or abs(xkey.time - zkey.time) > self.EPSILON): 1070 self.logger.warn( 1071 "xyz key times do not correspond, " 1072 "animation may not be correctly imported") 1073 frame = 1 + int(xkey.time * self.fps + 0.5) 1074 euler = Blender.Mathutils.Euler( 1075 [xkey.value*180.0/math.pi, 1076 ykey.value*180.0/math.pi, 1077 zkey.value*180.0/math.pi]) 1078 quat = euler.toQuat() 1079 1080 # beware, CrossQuats takes arguments in a counter-intuitive order: 1081 # q1.toMatrix() * q2.toMatrix() == CrossQuats(q2, q1).toMatrix() 1082 1083 quatVal = CrossQuats(niBone_bind_quat_inv, quat) # Rchannel = Rtotal * inverse(Rbind) 1084 rot = CrossQuats(CrossQuats(extra_matrix_quat_inv, quatVal), extra_matrix_quat) # C' = X * C * inverse(X) 1085 b_posebone.quat = rot 1086 b_posebone.insertKey(b_armature, frame, [Blender.Object.Pose.ROT]) # this is very slow... :( 1087 # fill optimizer dictionary 1088 if translations: 1089 rot_keys_dict[frame] = Blender.Mathutils.Quaternion(rot) 1090 1091 # Quaternion Rotations 1092 else: 1093 # TODO take rotation type into account for interpolation 1094 if kfd.quaternionKeys: 1095 self.logger.debug('Rotation keys...(quaternions)') 1096 quaternionKeys = kfd.quaternionKeys 1097 for key in quaternionKeys: 1098 frame = 1 + int(key.time * self.fps + 0.5) 1099 keyVal = key.value 1100 quat = Blender.Mathutils.Quaternion([keyVal.w, keyVal.x, keyVal.y, keyVal.z]) 1101 # beware, CrossQuats takes arguments in a 1102 # counter-intuitive order: 1103 # q1.toMatrix() * q2.toMatrix() == CrossQuats(q2, q1).toMatrix() 1104 quatVal = CrossQuats(niBone_bind_quat_inv, quat) # Rchannel = Rtotal * inverse(Rbind) 1105 rot = CrossQuats(CrossQuats(extra_matrix_quat_inv, quatVal), extra_matrix_quat) # C' = X * C * inverse(X) 1106 b_posebone.quat = rot 1107 b_posebone.insertKey(b_armature, frame, 1108 [Blender.Object.Pose.ROT]) 1109 # fill optimizer dictionary 1110 if translations: 1111 rot_keys_dict[frame] = Blender.Mathutils.Quaternion(rot) 1112 # else: 1113 # print("""Rotation keys...(unknown) 1114 #WARNING: rotation animation data of type %i found, but this type is not yet 1115 # supported; data has been skipped""" % rotationType) 1116 1117 # Translations 1118 if translations.keys: 1119 self.logger.debug('Translation keys...') 1120 for key in translations.keys: 1121 # time 0.0 is frame 1 1122 frame = 1 + int(key.time * self.fps + 0.5) 1123 keyVal = key.value 1124 trans = Blender.Mathutils.Vector(keyVal.x, keyVal.y, keyVal.z) 1125 locVal = (trans - niBone_bind_trans) * niBone_bind_rot_inv * (1.0/niBone_bind_scale)# Tchannel = (Ttotal - Tbind) * inverse(Rbind) / Sbind 1126 # the rotation matrix is needed at this frame (that's 1127 # why the other keys are inserted first) 1128 if rot_keys_dict: 1129 try: 1130 rot = rot_keys_dict[frame].toMatrix() 1131 except KeyError: 1132 # fall back on slow method 1133 ipo = action.getChannelIpo(bone_name) 1134 quat = Blender.Mathutils.Quaternion() 1135 quat.x = ipo.getCurve('QuatX').evaluate(frame) 1136 quat.y = ipo.getCurve('QuatY').evaluate(frame) 1137 quat.z = ipo.getCurve('QuatZ').evaluate(frame) 1138 quat.w = ipo.getCurve('QuatW').evaluate(frame) 1139 rot = quat.toMatrix() 1140 else: 1141 rot = Blender.Mathutils.Matrix([1.0,0.0,0.0], 1142 [0.0,1.0,0.0], 1143 [0.0,0.0,1.0]) 1144 # we also need the scale at this frame 1145 if scale_keys_dict: 1146 try: 1147 sizeVal = scale_keys_dict[frame] 1148 except KeyError: 1149 ipo = action.getChannelIpo(bone_name) 1150 if ipo.getCurve('SizeX'): 1151 sizeVal = ipo.getCurve('SizeX').evaluate(frame) # assume uniform scale 1152 else: 1153 sizeVal = 1.0 1154 else: 1155 sizeVal = 1.0 1156 size = Blender.Mathutils.Matrix([sizeVal, 0.0, 0.0], 1157 [0.0, sizeVal, 0.0], 1158 [0.0, 0.0, sizeVal]) 1159 # now we can do the final calculation 1160 loc = (extra_matrix_trans * size * rot + locVal - extra_matrix_trans) * extra_matrix_rot_inv * (1.0/extra_matrix_scale) # C' = X * C * inverse(X) 1161 b_posebone.loc = loc 1162 b_posebone.insertKey(b_armature, frame, [Blender.Object.Pose.LOC]) 1163 if translations: 1164 del scale_keys_dict 1165 del rot_keys_dict 1166 # set extend mode for all ipo curves 1167 if kfc: 1168 try: 1169 ipo = action.getChannelIpo(bone_name) 1170 except ValueError: 1171 # no channel for bone_name 1172 pass 1173 else: 1174 for b_curve in ipo: 1175 b_curve.extend = self.get_extend_from_flags(kfc.flags) 1176 1177 # constraints (priority) 1178 # must be done oudside edit mode hence after calling 1179 for bone_name, b_posebone in b_armature.getPose().bones.items(): 1180 # find bone nif block 1181 niBone = self.blocks[bone_name] 1182 # store bone priority, if applicable 1183 if niBone in self.bonePriorities: 1184 constr = b_posebone.constraints.append( 1185 Blender.Constraint.Type.NULL) 1186 constr.name = "priority:%i" % self.bonePriorities[niBone] 1187 1188 return b_armature
1189
1190 - def importBone(self, niBlock, b_armature, b_armatureData, niArmature):
1191 """Adds a bone to the armature in edit mode.""" 1192 # check that niBlock is indeed a bone 1193 if not self.is_bone(niBlock): 1194 return None 1195 1196 # bone length for nubs and zero length bones 1197 nub_length = 5.0 1198 scale = self.IMPORT_SCALE_CORRECTION 1199 # bone name 1200 bone_name = self.importName(niBlock, 32) 1201 niChildBones = [ child for child in niBlock.children 1202 if self.is_bone(child) ] 1203 # create a new bone 1204 b_bone = Blender.Armature.Editbone() 1205 # head: get position from niBlock 1206 armature_space_matrix = self.importMatrix(niBlock, 1207 relative_to = niArmature) 1208 1209 b_bone_head_x = armature_space_matrix[3][0] 1210 b_bone_head_y = armature_space_matrix[3][1] 1211 b_bone_head_z = armature_space_matrix[3][2] 1212 # temporarily sets the tail as for a 0 length bone 1213 b_bone_tail_x = b_bone_head_x 1214 b_bone_tail_y = b_bone_head_y 1215 b_bone_tail_z = b_bone_head_z 1216 is_zero_length = True 1217 # tail: average of children location 1218 if len(niChildBones) > 0: 1219 m_correction = self.find_correction_matrix(niBlock, niArmature) 1220 child_matrices = [ self.importMatrix(child, 1221 relative_to = niArmature) 1222 for child in niChildBones ] 1223 b_bone_tail_x = sum(child_matrix[3][0] 1224 for child_matrix 1225 in child_matrices) / len(child_matrices) 1226 b_bone_tail_y = sum(child_matrix[3][1] 1227 for child_matrix 1228 in child_matrices) / len(child_matrices) 1229 b_bone_tail_z = sum(child_matrix[3][2] 1230 for child_matrix 1231 in child_matrices) / len(child_matrices) 1232 # checking bone length 1233 dx = b_bone_head_x - b_bone_tail_x 1234 dy = b_bone_head_y - b_bone_tail_y 1235 dz = b_bone_head_z - b_bone_tail_z 1236 is_zero_length = abs(dx + dy + dz) * 200 < self.EPSILON 1237 elif self.IMPORT_REALIGN_BONES == 2: 1238 # The correction matrix value is based on the childrens' head 1239 # positions. 1240 # If there are no children then set it as the same as the 1241 # parent's correction matrix. 1242 m_correction = self.find_correction_matrix(niBlock._parent, 1243 niArmature) 1244 1245 if is_zero_length: 1246 # this is a 0 length bone, to avoid it being removed 1247 # set a default minimum length 1248 if (self.IMPORT_REALIGN_BONES == 2) \ 1249 or not self.is_bone(niBlock._parent): 1250 # no parent bone, or bone is realigned with correction 1251 # set one random direction 1252 b_bone_tail_x = b_bone_head_x + (nub_length * scale) 1253 else: 1254 # to keep things neat if bones aren't realigned on import 1255 # orient it as the vector between this 1256 # bone's head and the parent's tail 1257 parent_tail = b_armatureData.bones[ 1258 self.names[niBlock._parent]].tail 1259 dx = b_bone_head_x - parent_tail[0] 1260 dy = b_bone_head_y - parent_tail[1] 1261 dz = b_bone_head_z - parent_tail[2] 1262 if abs(dx + dy + dz) * 200 < self.EPSILON: 1263 # no offset from the parent: follow the parent's 1264 # orientation 1265 parent_head = b_armatureData.bones[ 1266 self.names[niBlock._parent]].head 1267 dx = parent_tail[0] - parent_head[0] 1268 dy = parent_tail[1] - parent_head[1] 1269 dz = parent_tail[2] - parent_head[2] 1270 direction = Vector(dx, dy, dz) 1271 direction.normalize() 1272 b_bone_tail_x = b_bone_head_x + (direction[0] * nub_length * scale) 1273 b_bone_tail_y = b_bone_head_y + (direction[1] * nub_length * scale) 1274 b_bone_tail_z = b_bone_head_z + (direction[2] * nub_length * scale) 1275 1276 # sets the bone heads & tails 1277 b_bone.head = Vector(b_bone_head_x, b_bone_head_y, b_bone_head_z) 1278 b_bone.tail = Vector(b_bone_tail_x, b_bone_tail_y, b_bone_tail_z) 1279 1280 if self.IMPORT_REALIGN_BONES == 2: 1281 # applies the corrected matrix explicitly 1282 b_bone.matrix = m_correction.resize4x4() * armature_space_matrix 1283 elif self.IMPORT_REALIGN_BONES == 1: 1284 # do not do anything, keep unit matrix 1285 pass 1286 else: 1287 # no realign, so use original matrix 1288 b_bone.matrix = armature_space_matrix 1289 1290 # set bone name and store the niBlock for future reference 1291 b_armatureData.bones[bone_name] = b_bone 1292 # calculate bone difference matrix; we will need this when 1293 # importing animation 1294 old_bone_matrix_inv = Blender.Mathutils.Matrix(armature_space_matrix) 1295 old_bone_matrix_inv.invert() 1296 new_bone_matrix = Blender.Mathutils.Matrix(b_bone.matrix) 1297 new_bone_matrix.resize4x4() 1298 new_bone_matrix[3][0] = b_bone_head_x 1299 new_bone_matrix[3][1] = b_bone_head_y 1300 new_bone_matrix[3][2] = b_bone_head_z 1301 # stores any correction or alteration applied to the bone matrix 1302 # new * inverse(old) 1303 self.bonesExtraMatrix[niBlock] = new_bone_matrix * old_bone_matrix_inv 1304 # set bone children 1305 for niBone in niChildBones: 1306 b_child_bone = self.importBone( 1307 niBone, b_armature, b_armatureData, niArmature) 1308 b_child_bone.parent = b_bone 1309 1310 return b_bone
1311
1312 - def find_correction_matrix(self, niBlock, niArmature):
1313 """Returns the correction matrix for a bone.""" 1314 m_correction = self.IDENTITY44.rotationPart() 1315 if (self.IMPORT_REALIGN_BONES == 2) and self.is_bone(niBlock): 1316 armature_space_matrix = self.importMatrix(niBlock, 1317 relative_to = niArmature) 1318 1319 niChildBones = [ child for child in niBlock.children 1320 if self.is_bone(child) ] 1321 (sum_x, sum_y, sum_z, dummy) = armature_space_matrix[3] 1322 if len(niChildBones) > 0: 1323 child_local_matrices = [ self.importMatrix(child) 1324 for child in niChildBones ] 1325 sum_x = sum(cm[3][0] for cm in child_local_matrices) 1326 sum_y = sum(cm[3][1] for cm in child_local_matrices) 1327 sum_z = sum(cm[3][2] for cm in child_local_matrices) 1328 list_xyz = [ int(c * 200) 1329 for c in (sum_x, sum_y, sum_z, 1330 -sum_x, -sum_y, -sum_z) ] 1331 idx_correction = list_xyz.index(max(list_xyz)) 1332 alignment_offset = 0.0 1333 if (idx_correction == 0 or idx_correction == 3) and abs(sum_x) > 0: 1334 alignment_offset = float(abs(sum_y) + abs(sum_z)) / abs(sum_x) 1335 elif (idx_correction == 1 or idx_correction == 4) and abs(sum_y) > 0: 1336 alignment_offset = float(abs(sum_z) + abs(sum_x)) / abs(sum_y) 1337 elif abs(sum_z) > 0: 1338 alignment_offset = float(abs(sum_x) + abs(sum_y)) / abs(sum_z) 1339 # if alignment is good enough, use the (corrected) NIF matrix 1340 # this gives nice ortogonal matrices 1341 if alignment_offset < 0.25: 1342 m_correction = self.BONE_CORRECTION_MATRICES[idx_correction] 1343 return m_correction
1344 1345
1346 - def getTextureHash(self, source):
1347 """Helper function for importTexture. Returns a key that uniquely 1348 identifies a texture from its source (which is either a 1349 NiSourceTexture block, or simply a path string). 1350 """ 1351 if not source: 1352 return None 1353 elif isinstance(source, NifFormat.NiSourceTexture): 1354 return source.getHash() 1355 elif isinstance(source, basestring): 1356 return source.lower() 1357 else: 1358 raise TypeError("source must be NiSourceTexture block or string")
1359
1360 - def importTexture(self, source):
1361 """Convert a NiSourceTexture block, or simply a path string, 1362 to a Blender Texture object, return the Texture object and 1363 stores it in the self.textures dictionary to avoid future 1364 duplicate imports. 1365 """ 1366 1367 # if the source block is not linked then return None 1368 if not source: 1369 return None 1370 1371 # calculate the texture hash key 1372 texture_hash = self.getTextureHash(source) 1373 1374 try: 1375 # look up the texture in the dictionary of imported textures 1376 # and return it if found 1377 return self.textures[texture_hash] 1378 except KeyError: 1379 pass 1380 1381 b_image = None 1382 1383 if (isinstance(source, NifFormat.NiSourceTexture) 1384 and not source.useExternal): 1385 # find a file name (but avoid overwriting) 1386 n = 0 1387 while True: 1388 fn = "image%03i.dds" % n 1389 tex = Blender.sys.join(Blender.sys.dirname(self.IMPORT_FILE), 1390 fn) 1391 if not Blender.sys.exists(tex): 1392 break 1393 n += 1 1394 if self.IMPORT_EXPORTEMBEDDEDTEXTURES: 1395 # save embedded texture as dds file 1396 stream = open(tex, "wb") 1397 try: 1398 self.logger.info("Saving embedded texture as %s" % tex) 1399 source.pixelData.saveAsDDS(stream) 1400 except ValueError: 1401 # value error means that the pixel format is not supported 1402 b_image = None 1403 else: 1404 # saving dds succeeded so load the file 1405 b_image = Blender.Image.Load(tex) 1406 # Blender will return an image object even if the 1407 # file format is not supported, 1408 # so to check if the image is actually loaded an error 1409 # is forced via "b_image.size" 1410 try: 1411 b_image.size 1412 except: # RuntimeError: couldn't load image data in Blender 1413 b_image = None # not supported, delete image object 1414 finally: 1415 stream.close() 1416 else: 1417 b_image = None 1418 else: 1419 # the texture uses an external image file 1420 if isinstance(source, NifFormat.NiSourceTexture): 1421 fn = source.fileName 1422 elif isinstance(source, basestring): 1423 fn = source 1424 else: 1425 raise TypeError( 1426 "source must be NiSourceTexture block or string") 1427 fn = fn.replace( '\\', Blender.sys.sep ) 1428 fn = fn.replace( '/', Blender.sys.sep ) 1429 # go searching for it 1430 importpath = Blender.sys.dirname(self.IMPORT_FILE) 1431 searchPathList = ( 1432 [importpath] 1433 + self.IMPORT_TEXTURE_PATH) 1434 if Blender.Get("texturesdir"): 1435 searchPathList += [ 1436 Blender.sys.dirname(Blender.Get("texturesdir"))] 1437 # if it looks like a Morrowind style path, use common sense to 1438 # guess texture path 1439 meshes_index = importpath.lower().find("meshes") 1440 if meshes_index != -1: 1441 searchPathList.append(importpath[:meshes_index] + 'textures') 1442 # if it looks like a Civilization IV style path, use common sense 1443 # to guess texture path 1444 art_index = importpath.lower().find("art") 1445 if art_index != -1: 1446 searchPathList.append(importpath[:art_index] + 'shared') 1447 # go through all texture search paths 1448 for texdir in searchPathList: 1449 texdir = texdir.replace( '\\', Blender.sys.sep ) 1450 texdir = texdir.replace( '/', Blender.sys.sep ) 1451 # go through all possible file names, try alternate extensions 1452 # too; for linux, also try lower case versions of filenames 1453 texfns = reduce(operator.add, 1454 [ [ fn[:-4]+ext, fn[:-4].lower()+ext ] 1455 for ext in ('.DDS','.dds','.PNG','.png', 1456 '.TGA','.tga','.BMP','.bmp', 1457 '.JPG','.jpg') ] ) 1458 texfns = [fn, fn.lower()] + list(set(texfns)) 1459 for texfn in texfns: 1460 # now a little trick, to satisfy many Morrowind mods 1461 if (texfn[:9].lower() == 'textures' + Blender.sys.sep) \ 1462 and (texdir[-9:].lower() == Blender.sys.sep + 'textures'): 1463 # strip one of the two 'textures' from the path 1464 tex = Blender.sys.join( texdir[:-9], texfn ) 1465 else: 1466 tex = Blender.sys.join( texdir, texfn ) 1467 self.logger.debug("Searching %s" % tex) 1468 if Blender.sys.exists(tex) == 1: 1469 # tries to load the file 1470 b_image = Blender.Image.Load(tex) 1471 # Blender will return an image object even if the 1472 # file format is not supported, 1473 # so to check if the image is actually loaded an error 1474 # is forced via "b_image.size" 1475 try: 1476 b_image.size 1477 except: # RuntimeError: couldn't load image data in Blender 1478 b_image = None # not supported, delete image object 1479 else: 1480 # file format is supported 1481 self.logger.debug("Found '%s' at %s" % (fn, tex)) 1482 break 1483 if b_image: 1484 break 1485 else: 1486 tex = Blender.sys.join(searchPathList[0], fn) 1487 1488 # create a stub image if the image could not be loaded 1489 if not b_image: 1490 self.logger.warning("""\ 1491 Texture '%s' not found or not supported and no alternate available""" 1492 % fn) 1493 b_image = Blender.Image.New(fn, 1, 1, 24) # create a stub 1494 b_image.filename = tex 1495 1496 # create a texture 1497 b_texture = Blender.Texture.New() 1498 b_texture.setType('Image') 1499 b_texture.setImage(b_image) 1500 b_texture.imageFlags |= Blender.Texture.ImageFlags.INTERPOL 1501 b_texture.imageFlags |= Blender.Texture.ImageFlags.MIPMAP 1502 1503 # save texture to avoid duplicate imports, and return it 1504 self.textures[texture_hash] = b_texture 1505 return b_texture
1506
1507 - def getMaterialHash(self, matProperty, textProperty, 1508 alphaProperty, specProperty, 1509 textureEffect, wireProperty, 1510 bsShaderProperty, extraDatas):
1511 """Helper function for importMaterial. Returns a key that 1512 uniquely identifies a material from its properties. The key 1513 ignores the material name as that does not affect the 1514 rendering. 1515 """ 1516 return (matProperty.getHash(ignore_strings=True) 1517 if matProperty else None, 1518 textProperty.getHash() if textProperty else None, 1519 alphaProperty.getHash() if alphaProperty else None, 1520 specProperty.getHash() if specProperty else None, 1521 textureEffect.getHash() if textureEffect else None, 1522 wireProperty.getHash() if wireProperty else None, 1523 bsShaderProperty.getHash() if bsShaderProperty else None, 1524 tuple(extra.getHash() for extra in extraDatas))
1525
1526 - def importMaterial(self, matProperty, textProperty, 1527 alphaProperty, specProperty, 1528 textureEffect, wireProperty, 1529 bsShaderProperty, extraDatas):
1530 """Creates and returns a material.""" 1531 # First check if material has been created before. 1532 material_hash = self.getMaterialHash(matProperty, textProperty, 1533 alphaProperty, specProperty, 1534 textureEffect, wireProperty, 1535 bsShaderProperty, 1536 extraDatas) 1537 try: 1538 return self.materials[material_hash] 1539 except KeyError: 1540 pass 1541 # use the material property for the name, other properties usually have 1542 # no name 1543 name = self.importName(matProperty) 1544 material = Blender.Material.New(name) 1545 # get apply mode, and convert to blender "blending mode" 1546 blendmode = Blender.Texture.BlendModes["MIX"] # default 1547 if textProperty: 1548 if textProperty.applyMode == NifFormat.ApplyMode.APPLY_MODULATE: 1549 blendmode = Blender.Texture.BlendModes["MIX"] 1550 elif textProperty.applyMode == NifFormat.ApplyMode.APPLY_REPLACE: 1551 blendmode = Blender.Texture.BlendModes["MIX"] 1552 elif textProperty.applyMode == NifFormat.ApplyMode.APPLY_DECAL: 1553 blendmode = Blender.Texture.BlendModes["MIX"] 1554 elif textProperty.applyMode == NifFormat.ApplyMode.APPLY_HILIGHT: 1555 blendmode = Blender.Texture.BlendModes["LIGHTEN"] 1556 elif textProperty.applyMode == NifFormat.ApplyMode.APPLY_HILIGHT2: 1557 blendmode = Blender.Texture.BlendModes["MULTIPLY"] 1558 else: 1559 self.logger.warning("Unknown apply mode (%i) in material '%s', using blending mode 'MIX'"% (textProperty.applyMode, matProperty.name)) 1560 elif bsShaderProperty: 1561 # default blending mode for fallout 3 1562 blendmode = Blender.Texture.BlendModes["MIX"] 1563 # Sets the colors 1564 # Specular color 1565 spec = matProperty.specularColor 1566 material.setSpecCol([spec.r, spec.g, spec.b]) 1567 # Blender multiplies specular color with this value 1568 material.setSpec(1.0) 1569 # Diffuse color 1570 diff = matProperty.diffuseColor 1571 emit = matProperty.emissiveColor 1572 # fallout 3 hack: convert diffuse black to emit if emit is not black 1573 if diff.r < self.EPSILON and diff.g < self.EPSILON and diff.b < self.EPSILON: 1574 if (emit.r + emit.g + emit.b) < self.EPSILON: 1575 # emit is black... set diffuse color to white 1576 diff.r = 1.0 1577 diff.g = 1.0 1578 diff.b = 1.0 1579 else: 1580 diff.r = emit.r 1581 diff.g = emit.g 1582 diff.b = emit.b 1583 material.setRGBCol(diff.r, diff.g, diff.b) 1584 # Ambient & emissive color 1585 # We assume that ambient & emissive are fractions of the diffuse color. 1586 # If it is not an exact fraction, we average out. 1587 amb = matProperty.ambientColor 1588 # fallout 3 hack:convert ambient black to white and set emit 1589 if amb.r < self.EPSILON and amb.g < self.EPSILON and amb.b < self.EPSILON: 1590 amb.r = 1.0 1591 amb.g = 1.0 1592 amb.b = 1.0 1593 b_amb = 1.0 1594 if (emit.r + emit.g + emit.b) < self.EPSILON: 1595 b_emit = 0.0 1596 else: 1597 b_emit = matProperty.emitMulti / 10.0 1598 else: 1599 b_amb = 0.0 1600 b_emit = 0.0 1601 b_n = 0 1602 if diff.r > self.EPSILON: 1603 b_amb += amb.r/diff.r 1604 b_emit += emit.r/diff.r 1605 b_n += 1 1606 if diff.g > self.EPSILON: 1607 b_amb += amb.g/diff.g 1608 b_emit += emit.g/diff.g 1609 b_n += 1 1610 if diff.b > self.EPSILON: 1611 b_amb += amb.b/diff.b 1612 b_emit += emit.b/diff.b 1613 b_n += 1 1614 if b_n > 0: 1615 b_amb /= b_n 1616 b_emit /= b_n 1617 if b_amb > 1.0: 1618 b_amb = 1.0 1619 if b_emit > 1.0: 1620 b_emit = 1.0 1621 material.setAmb(b_amb) 1622 material.setEmit(b_emit) 1623 # glossiness 1624 glossiness = matProperty.glossiness 1625 hardness = int(glossiness * 4) # just guessing really 1626 if hardness < 1: hardness = 1 1627 if hardness > 511: hardness = 511 1628 material.setHardness(hardness) 1629 # Alpha 1630 alpha = matProperty.alpha 1631 material.setAlpha(alpha) 1632 baseTexture = None 1633 glowTexture = None 1634 envmapTexture = None # for NiTextureEffect 1635 bumpTexture = None 1636 darkTexture = None 1637 detailTexture = None 1638 refTexture = None 1639 if textProperty: 1640 # standard texture slots 1641 baseTexDesc = textProperty.baseTexture 1642 glowTexDesc = textProperty.glowTexture 1643 bumpTexDesc = textProperty.bumpMapTexture 1644 glossTexDesc = textProperty.glossTexture 1645 darkTexDesc = textProperty.darkTexture 1646 detailTexDesc = textProperty.detailTexture 1647 refTexDesc = None 1648 # extra texture shader slots 1649 for shaderTexDesc in textProperty.shaderTextures: 1650 if not shaderTexDesc.isUsed: 1651 continue 1652 # it is used, figure out the slot it is used for 1653 for extra in extraDatas: 1654 if extra.integerData == shaderTexDesc.mapIndex: 1655 # found! 1656 shader_name = extra.name 1657 break 1658 else: 1659 # none found 1660 self.logger.warn( 1661 "No slot for shader texture %s." 1662 % shaderTexDesc.textureData.source.fileName) 1663 continue 1664 try: 1665 extra_shader_index = ( 1666 self.EXTRA_SHADER_TEXTURES.index(shader_name)) 1667 except ValueError: 1668 # shader_name not in self.EXTRA_SHADER_TEXTURES 1669 self.logger.warn( 1670 "No slot for shader texture %s." 1671 % shaderTexDesc.textureData.source.fileName) 1672 continue 1673 if extra_shader_index == 0: 1674 # EnvironmentMapIndex 1675 if shaderTexDesc.textureData.source.fileName.lower().startswith("rrt_engine_env_map"): 1676 # sid meier's railroads: env map generated by engine 1677 # we can skip this 1678 continue 1679 # XXX todo, civ4 uses this 1680 self.logger.warn("Skipping environment map texture.") 1681 continue 1682 elif extra_shader_index == 1: 1683 # NormalMapIndex 1684 bumpTexDesc = shaderTexDesc.textureData 1685 elif extra_shader_index == 2: 1686 # SpecularIntensityIndex 1687 glossTexDesc = shaderTexDesc.textureData 1688 elif extra_shader_index == 3: 1689 # EnvironmentIntensityIndex (this is reflection) 1690 refTexDesc = shaderTexDesc.textureData 1691 elif extra_shader_index == 4: 1692 # LightCubeMapIndex 1693 if shaderTexDesc.textureData.source.fileName.lower().startswith("rrt_cube_light_map"): 1694 # sid meier's railroads: light map generated by engine 1695 # we can skip this 1696 continue 1697 self.logger.warn("Skipping light cube texture.") 1698 continue 1699 elif extra_shader_index == 5: 1700 # ShadowTextureIndex 1701 self.logger.warn("Skipping shadow texture.") 1702 continue 1703 1704 if baseTexDesc: 1705 baseTexture = self.importTexture(baseTexDesc.source) 1706 if baseTexture: 1707 # set the texture to use face UV coordinates 1708 texco = Blender.Texture.TexCo.UV 1709 # map the texture to the base color channel 1710 mapto = Blender.Texture.MapTo.COL 1711 # set the texture for the material 1712 material.setTexture(0, baseTexture, texco, mapto) 1713 mbaseTexture = material.getTextures()[0] 1714 mbaseTexture.blendmode = blendmode 1715 mbaseTexture.uvlayer = self.getUVLayerName(baseTexDesc.uvSet) 1716 if glowTexDesc: 1717 glowTexture = self.importTexture(glowTexDesc.source) 1718 if glowTexture: 1719 # glow maps use alpha from rgb intensity 1720 glowTexture.imageFlags |= Blender.Texture.ImageFlags.CALCALPHA 1721 # set the texture to use face UV coordinates 1722 texco = Blender.Texture.TexCo.UV 1723 # map the texture to the base color and emit channel 1724 mapto = Blender.Texture.MapTo.COL | Blender.Texture.MapTo.EMIT 1725 # set the texture for the material 1726 material.setTexture(1, glowTexture, texco, mapto) 1727 mglowTexture = material.getTextures()[1] 1728 mglowTexture.uvlayer = self.getUVLayerName(glowTexDesc.uvSet) 1729 if bumpTexDesc: 1730 bumpTexture = self.importTexture(bumpTexDesc.source) 1731 if bumpTexture: 1732 # set the texture to use face UV coordinates 1733 texco = Blender.Texture.TexCo.UV 1734 # map the texture to the normal channel 1735 mapto = Blender.Texture.MapTo.NOR 1736 # set the texture for the material 1737 material.setTexture(2, bumpTexture, texco, mapto) 1738 mbumpTexture = material.getTextures()[2] 1739 mbumpTexture.uvlayer = self.getUVLayerName(bumpTexDesc.uvSet) 1740 if glossTexDesc: 1741 glossTexture = self.importTexture(glossTexDesc.source) 1742 if glossTexture: 1743 # set the texture to use face UV coordinates 1744 texco = Blender.Texture.TexCo.UV 1745 # map the texture to the specularity channel 1746 mapto = Blender.Texture.MapTo.SPEC 1747 # set the texture for the material 1748 material.setTexture(4, glossTexture, texco, mapto) 1749 mglossTexture = material.getTextures()[4] 1750 mglossTexture.uvlayer = self.getUVLayerName(glossTexDesc.uvSet) 1751 if darkTexDesc: 1752 darkTexture = self.importTexture(darkTexDesc.source) 1753 if darkTexture: 1754 # set the texture to use face UV coordinates 1755 texco = Blender.Texture.TexCo.UV 1756 # map the texture to the COL channel 1757 mapto = Blender.Texture.MapTo.COL 1758 # set the texture for the material 1759 material.setTexture(5, darkTexture, texco, mapto) 1760 mdarkTexture = material.getTextures()[5] 1761 mdarkTexture.uvlayer = self.getUVLayerName(darkTexDesc.uvSet) 1762 # set blend mode to "DARKEN" 1763 mdarkTexture.blendmode = Blender.Texture.BlendModes["DARKEN"] 1764 if detailTexDesc: 1765 detailTexture = self.importTexture(detailTexDesc.source) 1766 if detailTexture: 1767 # import detail texture as extra base texture 1768 # set the texture to use face UV coordinates 1769 texco = Blender.Texture.TexCo.UV 1770 # map the texture to the COL channel 1771 mapto = Blender.Texture.MapTo.COL 1772 # set the texture for the material 1773 material.setTexture(6, detailTexture, texco, mapto) 1774 mdetailTexture = material.getTextures()[6] 1775 mdetailTexture.uvlayer = self.getUVLayerName(detailTexDesc.uvSet) 1776 if refTexDesc: 1777 refTexture = self.importTexture(refTexDesc.source) 1778 if refTexture: 1779 # set the texture to use face UV coordinates 1780 texco = Blender.Texture.TexCo.UV 1781 # map the texture to the base color and emit channel 1782 mapto = Blender.Texture.MapTo.REF 1783 # set the texture for the material 1784 material.setTexture(7, refTexture, texco, mapto) 1785 mrefTexture = material.getTextures()[7] 1786 mrefTexture.uvlayer = self.getUVLayerName(refTexDesc.uvSet) 1787 # if not a texture property, but a bethesda shader property... 1788 elif bsShaderProperty: 1789 # also contains textures, used in fallout 3 1790 baseTexFile = bsShaderProperty.textureSet.textures[0] 1791 if baseTexFile: 1792 baseTexture = self.importTexture(baseTexFile) 1793 if baseTexture: 1794 # set the texture to use face UV coordinates 1795 texco = Blender.Texture.TexCo.UV 1796 # map the texture to the base color channel 1797 mapto = Blender.Texture.MapTo.COL 1798 # set the texture for the material 1799 material.setTexture(0, baseTexture, texco, mapto) 1800 mbaseTexture = material.getTextures()[0] 1801 mbaseTexture.blendmode = blendmode 1802 1803 glowTexFile = bsShaderProperty.textureSet.textures[2] 1804 if glowTexFile: 1805 glowTexture = self.importTexture(glowTexFile) 1806 if glowTexture: 1807 # glow maps use alpha from rgb intensity 1808 glowTexture.imageFlags |= Blender.Texture.ImageFlags.CALCALPHA 1809 # set the texture to use face UV coordinates 1810 texco = Blender.Texture.TexCo.UV 1811 # map the texture to the base color and emit channel 1812 mapto = Blender.Texture.MapTo.COL | Blender.Texture.MapTo.EMIT 1813 # set the texture for the material 1814 material.setTexture(1, glowTexture, texco, mapto) 1815 mglowTexture = material.getTextures()[1] 1816 mglowTexture.blendmode = blendmode 1817 1818 bumpTexFile = bsShaderProperty.textureSet.textures[1] 1819 if bumpTexFile: 1820 bumpTexture = self.importTexture(bumpTexFile) 1821 if bumpTexture: 1822 # set the texture to use face UV coordinates 1823 texco = Blender.Texture.TexCo.UV 1824 # map the texture to the normal channel 1825 mapto = Blender.Texture.MapTo.NOR 1826 # set the texture for the material 1827 material.setTexture(2, bumpTexture, texco, mapto) 1828 mbumpTexture = material.getTextures()[2] 1829 mbumpTexture.blendmode = blendmode 1830 1831 if textureEffect: 1832 envmapTexture = self.importTexture(textureEffect.sourceTexture) 1833 if envmapTexture: 1834 # set the texture to use face reflection coordinates 1835 texco = Blender.Texture.TexCo.REFL 1836 # map the texture to the base color channel 1837 mapto = Blender.Texture.MapTo.COL 1838 # set the texture for the material 1839 material.setTexture(3, envmapTexture, texco, mapto) 1840 menvmapTexture = material.getTextures()[3] 1841 menvmapTexture.blendmode = Blender.Texture.BlendModes["ADD"] 1842 # check transparency 1843 if alphaProperty: 1844 material.mode |= Blender.Material.Modes.ZTRANSP # enable z-buffered transparency 1845 # if the image has an alpha channel => then this overrides the material alpha value 1846 if baseTexture: 1847 # old method: 1848 #if baseTexture.image.depth == 32 or baseTexture.image.size == [1,1]: # check for alpha channel in texture; if it's a stub then assume alpha channel 1849 # new method: let's just assume there is alpha 1850 if True: 1851 baseTexture.imageFlags |= Blender.Texture.ImageFlags.USEALPHA # use the alpha channel 1852 mbaseTexture.mapto |= Blender.Texture.MapTo.ALPHA # and map the alpha channel to transparency 1853 # for proper display in Blender, we must set the alpha value 1854 # to 0 and the "Var" slider in the texture Map To tab to the 1855 # NIF material alpha value 1856 material.setAlpha(0.0) 1857 mbaseTexture.varfac = alpha 1858 # non-transparent glow textures have their alpha calculated from RGB 1859 # not sure what to do with glow textures that have an alpha channel 1860 # for now we ignore those alpha channels 1861 else: 1862 # no alpha property: force alpha 1.0 in Blender 1863 material.setAlpha(1.0) 1864 # check specularity 1865 if (not specProperty) and (self.version != 0x14000004): 1866 # no specular property: specular color is ignored 1867 # (for most games) 1868 # we do this by setting specularity zero 1869 # however, for Sid Meier's Railroads the specular color is NOT 1870 # ignored! hence the exception for version 0x14000004 1871 material.setSpec(0.0) 1872 # check wireframe property 1873 if wireProperty: 1874 # enable wireframe rendering 1875 material.mode |= Blender.Material.Modes.WIRE 1876 1877 self.materials[material_hash] = material 1878 return material
1879
1880 - def importMaterialControllers(self, b_material, n_geom):
1881 """Import material animation data for given geometry.""" 1882 if not self.IMPORT_ANIMATION: 1883 return 1884 1885 self.importMaterialUVController(b_material, n_geom)
1886
1887 - def importMaterialUVController(self, b_material, n_geom):
1888 """Import UV controller data.""" 1889 # search for the block 1890 for n_ctrl in n_geom.getControllers(): 1891 if isinstance(n_ctrl, NifFormat.NiUVController): 1892 self.logger.info("importing UV controller") 1893 b_channels = ("OfsX", "OfsY", "SizeX", "SizeY") 1894 for b_channel, n_uvgroup in zip(b_channels, 1895 n_ctrl.data.uvGroups): 1896 if n_uvgroup.keys: 1897 # create curve in material ipo 1898 b_ipo = self.getMaterialIpo(b_material) 1899 b_curve = b_ipo.addCurve(b_channel) 1900 # XXX todo: get interpolation from nif data 1901 # XXX these are reasonable defaults 1902 b_curve.interpolation = Blender.IpoCurve.InterpTypes.LINEAR 1903 b_curve.extend = self.get_extend_from_flags(n_ctrl.flags) 1904 for n_key in n_uvgroup.keys: 1905 if b_channel.startswith("Ofs"): 1906 # offsets are negated 1907 b_curve[1 + n_key.time * self.fps] = -n_key.value 1908 else: 1909 b_curve[1 + n_key.time * self.fps] = n_key.value
1910
1911 - def getMaterialIpo(self, b_material):
1912 """Return existing material ipo data, or if none exists, create one 1913 and return that. 1914 """ 1915 if not b_material.ipo: 1916 b_material.ipo = Blender.Ipo.New("Material", "MatIpo") 1917 return b_material.ipo
1918
1919 - def importMesh(self, niBlock, 1920 group_mesh=None, 1921 applytransform=False, 1922 relative_to=None):
1923 """Creates and returns a raw mesh, or appends geometry data to 1924 group_mesh. 1925 1926 @param niBlock: The nif block whose mesh data to import. 1927 @type niBlock: C{NiTriBasedGeom} 1928 @param group_mesh: The mesh to which to append the geometry 1929 data. If C{None}, a new mesh is created. 1930 @type group_mesh: A Blender object that has mesh data. 1931 @param applytransform: Whether to apply the niBlock's 1932 transformation to the mesh. If group_mesh is not C{None}, 1933 then applytransform must be C{True}. 1934 @type applytransform: C{bool} 1935 """ 1936 assert(isinstance(niBlock, NifFormat.NiTriBasedGeom)) 1937 1938 logger = logging.getLogger("niftools.blender.import.mesh") 1939 logger.info("Importing mesh data for geometry %s" % niBlock.name) 1940 1941 if group_mesh: 1942 b_mesh = group_mesh 1943 b_meshData = group_mesh.getData(mesh=True) 1944 else: 1945 # Mesh name -> must be unique, so tag it if needed 1946 b_name = self.importName(niBlock, 22) 1947 # create mesh data 1948 b_meshData = Blender.Mesh.New(b_name) 1949 b_meshData.properties['longName'] = niBlock.name 1950 # create mesh object and link to data 1951 b_mesh = self.scene.objects.new(b_meshData, b_name) 1952 1953 # Mesh hidden flag 1954 if niBlock.flags & 1 == 1: 1955 b_mesh.setDrawType(2) # hidden: wire 1956 else: 1957 b_mesh.setDrawType(4) # not hidden: shaded 1958 1959 # set transform matrix for the mesh 1960 if not applytransform: 1961 if group_mesh: raise NifImportError('BUG: cannot set matrix when importing meshes in groups; use applytransform = True') 1962 b_mesh.setMatrix(self.importMatrix(niBlock, 1963 relative_to = relative_to)) 1964 else: 1965 # used later on 1966 transform = self.importMatrix(niBlock, relative_to = relative_to) 1967 1968 # shortcut for mesh geometry data 1969 niData = niBlock.data 1970 if not niData: 1971 raise NifImportError("no ShapeData returned. Node name: %s " % b_name) 1972 1973 # vertices 1974 verts = niData.vertices 1975 1976 # faces 1977 tris = [list(tri) for tri in niData.getTriangles()] 1978 1979 # "sticky" UV coordinates: these are transformed in Blender UV's 1980 uvco = niData.uvSets 1981 1982 # vertex normals 1983 norms = niData.normals 1984 1985 # Stencil (for double sided meshes) 1986 stencilProperty = self.find_property(niBlock, 1987 NifFormat.NiStencilProperty) 1988 # we don't check flags for now, nothing fancy 1989 if stencilProperty: 1990 b_meshData.mode |= Blender.Mesh.Modes.TWOSIDED 1991 else: 1992 b_meshData.mode &= ~Blender.Mesh.Modes.TWOSIDED 1993 1994 # Material 1995 # note that NIF files only support one material for each trishape 1996 # find material property 1997 matProperty = self.find_property(niBlock, NifFormat.NiMaterialProperty) 1998 if matProperty: 1999 # Texture 2000 textProperty = None 2001 if uvco: 2002 textProperty = self.find_property(niBlock, 2003 NifFormat.NiTexturingProperty) 2004 2005 # Alpha 2006 alphaProperty = self.find_property(niBlock, 2007 NifFormat.NiAlphaProperty) 2008 2009 # Specularity 2010 specProperty = self.find_property(niBlock, 2011 NifFormat.NiSpecularProperty) 2012 2013 # Wireframe 2014 wireProperty = self.find_property(niBlock, 2015 NifFormat.NiWireframeProperty) 2016 2017 # bethesda shader 2018 bsShaderProperty = self.find_property( 2019 niBlock, NifFormat.BSShaderPPLightingProperty) 2020 2021 # texturing effect for environment map 2022 # in official files this is activated by a NiTextureEffect child 2023 # preceeding the niBlock 2024 textureEffect = None 2025 if isinstance(niBlock._parent, NifFormat.NiNode): 2026 lastchild = None 2027 for child in niBlock._parent.children: 2028 if child is niBlock: 2029 if isinstance(lastchild, NifFormat.NiTextureEffect): 2030 textureEffect = lastchild 2031 break 2032 lastchild = child 2033 else: 2034 raise RuntimeError("texture effect scanning bug") 2035 # in some mods the NiTextureEffect child follows the niBlock 2036 # but it still works because it is listed in the effect list 2037 # so handle this case separately 2038 if not textureEffect: 2039 for effect in niBlock._parent.effects: 2040 if isinstance(effect, NifFormat.NiTextureEffect): 2041 textureEffect = effect 2042 break 2043 2044 # extra datas (for sid meier's railroads) that have material info 2045 extraDatas = [] 2046 for extra in niBlock.getExtraDatas(): 2047 if isinstance(extra, NifFormat.NiIntegerExtraData): 2048 if extra.name in self.EXTRA_SHADER_TEXTURES: 2049 # yes, it describes the shader slot number 2050 extraDatas.append(extra) 2051 2052 # create material and assign it to the mesh 2053 # XXX todo: delegate search for properties to importMaterial 2054 material = self.importMaterial(matProperty, textProperty, 2055 alphaProperty, specProperty, 2056 textureEffect, wireProperty, 2057 bsShaderProperty, extraDatas) 2058 # XXX todo: merge this call into importMaterial 2059 self.importMaterialControllers(material, niBlock) 2060 b_mesh_materials = b_meshData.materials 2061 try: 2062 materialIndex = b_mesh_materials.index(material) 2063 except ValueError: 2064 materialIndex = len(b_mesh_materials) 2065 b_meshData.materials += [material] 2066 # if mesh has one material with wireproperty, then make the mesh 2067 # wire in 3D view 2068 if wireProperty: 2069 b_mesh.drawType = Blender.Object.DrawTypes["WIRE"] 2070 else: 2071 material = None 2072 materialIndex = 0 2073 2074 # if there are no vertices then enable face index shifts 2075 # (this fixes an issue with indexing) 2076 if len(b_meshData.verts) == 0: 2077 check_shift = True 2078 else: 2079 check_shift = False 2080 2081 # v_map will store the vertex index mapping 2082 # nif vertex i maps to blender vertex v_map[i] 2083 v_map = [0 for i in xrange(len(verts))] # pre-allocate memory, for faster performance 2084 2085 # Following code avoids introducing unwanted cracks in UV seams: 2086 # Construct vertex map to get unique vertex / normal pair list. 2087 # We use a Python dictionary to remove doubles and to keep track of indices. 2088 # While we are at it, we also add vertices while constructing the map. 2089 n_map = {} 2090 b_v_index = len(b_meshData.verts) 2091 for i, v in enumerate(verts): 2092 # The key k identifies unique vertex /normal pairs. 2093 # We use a tuple of ints for key, this works MUCH faster than a 2094 # tuple of floats. 2095 if norms: 2096 n = norms[i] 2097 k = ( int(v.x*200), int(v.y*200), int(v.z*200), 2098 int(n.x*200), int(n.y*200), int(n.z*200) ) 2099 else: 2100 k = ( int(v.x*200), int(v.y*200), int(v.z*200) ) 2101 # check if vertex was already added, and if so, what index 2102 try: 2103 # this is the bottle neck... 2104 # can we speed this up? 2105 n_map_k = n_map[k] 2106 except KeyError: 2107 n_map_k = None 2108 if not n_map_k: 2109 # not added: new vertex / normal pair 2110 n_map[k] = i # unique vertex / normal pair with key k was added, with NIF index i 2111 v_map[i] = b_v_index # NIF vertex i maps to blender vertex b_v_index 2112 # add the vertex 2113 if applytransform: 2114 v = Blender.Mathutils.Vector(v.x, v.y, v.z) 2115 v *= transform 2116 b_meshData.verts.extend(v) 2117 else: 2118 b_meshData.verts.extend(v.x, v.y, v.z) 2119 # adds normal info if present (Blender recalculates these when 2120 # switching between edit mode and object mode, handled further) 2121 #if norms: 2122 # mv = b_meshData.verts[b_v_index] 2123 # n = norms[i] 2124 # mv.no = Blender.Mathutils.Vector(n.x, n.y, n.z) 2125 b_v_index += 1 2126 else: 2127 # already added 2128 # NIF vertex i maps to Blender vertex v_map[n_map_k] 2129 v_map[i] = v_map[n_map_k] 2130 # report 2131 logger.debug("%i unique vertex-normal pairs" % len(n_map)) 2132 # release memory 2133 del n_map 2134 2135 # Adds the faces to the mesh 2136 f_map = [None]*len(tris) 2137 b_f_index = len(b_meshData.faces) 2138 num_new_faces = 0 # counter for debugging 2139 for i, f in enumerate(tris): 2140 # get face index 2141 f_verts = [b_meshData.verts[v_map[vert_index]] for vert_index in f] 2142 # skip degenerate faces 2143 # we get a ValueError on faces.extend otherwise 2144 if (f_verts[0] == f_verts[1]) or (f_verts[1] == f_verts[2]) or (f_verts[2] == f_verts[0]): continue 2145 tmp1 = len(b_meshData.faces) 2146 # extend checks for duplicate faces 2147 # see http://www.blender3d.org/documentation/240PythonDoc/Mesh.MFaceSeq-class.html 2148 b_meshData.faces.extend(*f_verts) 2149 if tmp1 == len(b_meshData.faces): continue # duplicate face! 2150 # faces.extend does not necessarily add vertices in the order 2151 # they were given in the argument list 2152 # so we must fix the face index order 2153 if check_shift: 2154 added_face = b_meshData.faces[-1] 2155 if added_face.verts[0] == f_verts[0]: # most common case, checking it first will speed up the script 2156 pass # f[0] comes first, everything ok 2157 elif added_face.verts[2] == f_verts[0]: # second most common case 2158 f[0], f[1], f[2] = f[1], f[2], f[0] # f[0] comes last 2159 elif added_face.verts[1] == f_verts[0]: # this never seems to occur, leave it just in case 2160 f[0], f[1], f[2] = f[2], f[0], f[1] # f[0] comes second 2161 else: 2162 raise RuntimeError("face extend index bug") 2163 # keep track of added faces, mapping NIF face index to 2164 # Blender face index 2165 f_map[i] = b_f_index 2166 b_f_index += 1 2167 num_new_faces += 1 2168 # at this point, deleted faces (degenerate or duplicate) 2169 # satisfy f_map[i] = None 2170 2171 logger.debug("%i unique faces" % num_new_faces) 2172 2173 # set face smoothing and material 2174 for b_f_index in f_map: 2175 if b_f_index is None: 2176 continue 2177 f = b_meshData.faces[b_f_index] 2178 f.smooth = 1 if norms else 0 2179 f.mat = materialIndex 2180 2181 # vertex colors 2182 vcol = niData.vertexColors 2183 2184 if vcol: 2185 b_meshData.vertexColors = 1 2186 for f, b_f_index in izip(tris, f_map): 2187 if b_f_index is None: 2188 continue 2189 b_face = b_meshData.faces[b_f_index] 2190 # now set the vertex colors 2191 for f_vert_index, vert_index in enumerate(f): 2192 b_face.col[f_vert_index].r = int(vcol[vert_index].r * 255) 2193 b_face.col[f_vert_index].g = int(vcol[vert_index].g * 255) 2194 b_face.col[f_vert_index].b = int(vcol[vert_index].b * 255) 2195 b_face.col[f_vert_index].a = int(vcol[vert_index].a * 255) 2196 # vertex colors influence lighting... 2197 # so now we have to set the VCOL_LIGHT flag on the material 2198 # see below 2199 2200 # UV coordinates 2201 # NIF files only support 'sticky' UV coordinates, and duplicates 2202 # vertices to emulate hard edges and UV seam. So whenever a hard edge 2203 # or a UV seam is present the mesh, vertices are duplicated. Blender 2204 # only must duplicate vertices for hard edges; duplicating for UV seams 2205 # would introduce unnecessary hard edges. 2206 2207 # only import UV if there are faces 2208 # (some corner cases have only one vertex, and no faces, 2209 # and b_meshData.faceUV = 1 on such mesh raises a runtime error) 2210 if b_meshData.faces: 2211 b_meshData.faceUV = 1 2212 b_meshData.vertexUV = 0 2213 for i, uvSet in enumerate(uvco): 2214 # Set the face UV's for the mesh. The NIF format only supports 2215 # vertex UV's, but Blender only allows explicit editing of face 2216 # UV's, so load vertex UV's as face UV's 2217 uvlayer = self.getUVLayerName(i) 2218 if not uvlayer in b_meshData.getUVLayerNames(): 2219 b_meshData.addUVLayer(uvlayer) 2220 b_meshData.activeUVLayer = uvlayer 2221 for f, b_f_index in izip(tris, f_map): 2222 if b_f_index is None: 2223 continue 2224 uvlist = [ Vector(uvSet[vert_index].u, 1.0 - uvSet[vert_index].v) for vert_index in f ] 2225 b_meshData.faces[b_f_index].uv = tuple(uvlist) 2226 b_meshData.activeUVLayer = self.getUVLayerName(0) 2227 2228 if material: 2229 # fix up vertex colors depending on whether we had textures in the 2230 # material 2231 mbasetex = material.getTextures()[0] 2232 mglowtex = material.getTextures()[1] 2233 if b_meshData.vertexColors == 1: 2234 if mbasetex or mglowtex: 2235 # textured material: vertex colors influence lighting 2236 material.mode |= Blender.Material.Modes.VCOL_LIGHT 2237 else: 2238 # non-textured material: vertex colors incluence color 2239 material.mode |= Blender.Material.Modes.VCOL_PAINT 2240 2241 # if there's a base texture assigned to this material sets it 2242 # to be displayed in Blender's 3D view 2243 # but only if there are UV coordinates 2244 if mbasetex and uvco: 2245 # face mode bitfield value 2246 TEX = Blender.Mesh.FaceModes['TEX'] 2247 imgobj = mbasetex.tex.getImage() 2248 if imgobj: 2249 for b_f_index in f_map: 2250 if b_f_index is None: 2251 continue 2252 f = b_meshData.faces[b_f_index] 2253 f.mode = TEX 2254 f.image = imgobj 2255 2256 # import skinning info, for meshes affected by bones 2257 skininst = niBlock.skinInstance 2258 if skininst: 2259 skindata = skininst.data 2260 bones = skininst.bones 2261 boneWeights = skindata.boneList 2262 for idx, bone in enumerate(bones): 2263 vertexWeights = boneWeights[idx].vertexWeights 2264 groupname = self.names[bone] 2265 if not groupname in b_meshData.getVertGroupNames(): 2266 b_meshData.addVertGroup(groupname) 2267 for skinWeight in vertexWeights: 2268 vert = skinWeight.index 2269 weight = skinWeight.weight 2270 b_meshData.assignVertsToGroup( 2271 groupname, [v_map[vert]], weight, 2272 Blender.Mesh.AssignModes.REPLACE) 2273 2274 # import body parts as vertex groups 2275 if isinstance(skininst, NifFormat.BSDismemberSkinInstance): 2276 skinpart = niBlock.getSkinPartition() 2277 for bodypart, skinpartblock in izip( 2278 skininst.partitions, skinpart.skinPartitionBlocks): 2279 bodypart_wrap = NifFormat.BSDismemberBodyPartType() 2280 bodypart_wrap.setValue(bodypart.bodyPart) 2281 groupname = bodypart_wrap.getDetailDisplay() 2282 # create vertex group if it did not exist yet 2283 if not(groupname in b_meshData.getVertGroupNames()): 2284 b_meshData.addVertGroup(groupname) 2285 # find vertex indices of this group 2286 groupverts = [v_map[v_index] 2287 for v_index in skinpartblock.vertexMap] 2288 # create the group 2289 b_meshData.assignVertsToGroup( 2290 groupname, groupverts, 1, 2291 Blender.Mesh.AssignModes.ADD) 2292 2293 # import morph controller 2294 if self.IMPORT_ANIMATION: 2295 morphCtrl = self.find_controller(niBlock, NifFormat.NiGeomMorpherController) 2296 if morphCtrl: 2297 morphData = morphCtrl.data 2298 if morphData.numMorphs: 2299 # insert base key at frame 1, using relative keys 2300 b_meshData.insertKey(1, 'relative') 2301 baseverts = morphData.morphs[0].vectors 2302 b_ipo = Blender.Ipo.New('Key' , 'KeyIpo') 2303 b_meshData.key.ipo = b_ipo 2304 for idxMorph in xrange(1, morphData.numMorphs): 2305 # get name for key 2306 keyname = morphData.morphs[idxMorph].frameName 2307 if not keyname: 2308 keyname = 'Key %i' % idxMorph 2309 # get vectors 2310 morphverts = morphData.morphs[idxMorph].vectors 2311 # for each vertex calculate the key position from base 2312 # pos + delta offset 2313 assert(len(baseverts) == len(morphverts) == len(v_map)) 2314 for bv, mv, b_v_index in izip(baseverts, morphverts, v_map): 2315 base = Blender.Mathutils.Vector(bv.x, bv.y, bv.z) 2316 delta = Blender.Mathutils.Vector(mv.x, mv.y, mv.z) 2317 v = base + delta 2318 if applytransform: 2319 v *= transform 2320 b_meshData.verts[b_v_index].co[0] = v.x 2321 b_meshData.verts[b_v_index].co[1] = v.y 2322 b_meshData.verts[b_v_index].co[2] = v.z 2323 # update the mesh and insert key 2324 b_meshData.insertKey(idxMorph, 'relative') 2325 # set name for key 2326 b_meshData.key.blocks[idxMorph].name = keyname 2327 # set up the ipo key curve 2328 b_curve = b_ipo.addCurve(keyname) 2329 # no idea how to set up the bezier triples -> switching 2330 # to linear instead 2331 b_curve.interpolation = Blender.IpoCurve.InterpTypes.LINEAR 2332 # select extrapolation 2333 b_curve.extend = self.get_extend_from_flags(morphCtrl.flags) 2334 # set up the curve's control points 2335 # first find the keys 2336 # older versions store keys in the morphData 2337 morphkeys = morphData.morphs[idxMorph].keys 2338 # newer versions store keys in the controller 2339 if (not morphkeys) and morphCtrl.interpolators: 2340 morphkeys = morphCtrl.interpolators[idxMorph].data.data.keys 2341 for key in morphkeys: 2342 x = key.value 2343 frame = 1+int(key.time * self.fps + 0.5) 2344 b_curve.addBezier( ( frame, x ) ) 2345 # finally: return to base position 2346 for bv, b_v_index in izip(baseverts, v_map): 2347 base = Blender.Mathutils.Vector(bv.x, bv.y, bv.z) 2348 if applytransform: 2349 base *= transform 2350 b_meshData.verts[b_v_index].co[0] = base.x 2351 b_meshData.verts[b_v_index].co[1] = base.y 2352 b_meshData.verts[b_v_index].co[2] = base.z 2353 2354 # import facegen morphs 2355 if self.egmdata: 2356 # XXX if there is an egm, the assumption is that there is only one 2357 # XXX mesh in the nif 2358 sym_morphs = [list(morph.get_relative_vertices()) 2359 for morph in self.egmdata.sym_morphs] 2360 asym_morphs = [list(morph.get_relative_vertices()) 2361 for morph in self.egmdata.asym_morphs] 2362 2363 # insert base key at frame 1, using relative keys 2364 b_meshData.insertKey(1, 'relative') 2365 2366 if self.IMPORT_EGMANIM: 2367 # if morphs are animated: create key ipo for mesh 2368 b_ipo = Blender.Ipo.New('Key' , 'KeyIpo') 2369 b_meshData.key.ipo = b_ipo 2370 2371 2372 morphs = ([(morph, "EGM SYM %i" % i) 2373 for i, morph in enumerate(sym_morphs)] 2374 + 2375 [(morph, "EGM ASYM %i" % i) 2376 for i, morph in enumerate(asym_morphs)]) 2377 2378 for morphverts, keyname in morphs: 2379 # length check disabled 2380 # as sometimes, oddly, the morph has more vertices... 2381 #assert(len(verts) == len(morphverts) == len(v_map)) 2382 2383 # for each vertex calculate the key position from base 2384 # pos + delta offset 2385 for bv, mv, b_v_index in izip(verts, morphverts, v_map): 2386 base = Blender.Mathutils.Vector(bv.x, bv.y, bv.z) 2387 delta = Blender.Mathutils.Vector(mv[0], mv[1], mv[2]) 2388 v = base + delta 2389 if applytransform: 2390 v *= transform 2391 b_meshData.verts[b_v_index].co[0] = v.x 2392 b_meshData.verts[b_v_index].co[1] = v.y 2393 b_meshData.verts[b_v_index].co[2] = v.z 2394 # update the mesh and insert key 2395 b_meshData.insertKey(1, 'relative') 2396 # set name for key 2397 b_meshData.key.blocks[-1].name = keyname 2398 2399 if self.IMPORT_EGMANIM: 2400 # set up the ipo key curve 2401 b_curve = b_ipo.addCurve(keyname) 2402 # linear interpolation 2403 b_curve.interpolation = Blender.IpoCurve.InterpTypes.LINEAR 2404 # constant extrapolation 2405 b_curve.extend = Blender.IpoCurve.ExtendTypes.CONST 2406 # set up the curve's control points 2407 framestart = 1 + len(b_meshData.key.blocks) * 10 2408 for frame, value in ((framestart, 0), 2409 (framestart + 5, self.IMPORT_EGMANIMSCALE), 2410 (framestart + 10, 0)): 2411 b_curve.addBezier( ( frame, value ) ) 2412 2413 if self.IMPORT_EGMANIM: 2414 # set begin and end frame 2415 self.scene.getRenderingContext().startFrame(1) 2416 self.scene.getRenderingContext().endFrame( 2417 11 + len(b_meshData.key.blocks) * 10) 2418 2419 # finally: return to base position 2420 for bv, b_v_index in izip(verts, v_map): 2421 base = Blender.Mathutils.Vector(bv.x, bv.y, bv.z) 2422 if applytransform: 2423 base *= transform 2424 b_meshData.verts[b_v_index].co[0] = base.x 2425 b_meshData.verts[b_v_index].co[1] = base.y 2426 b_meshData.verts[b_v_index].co[2] = base.z 2427 2428 # recalculate normals 2429 b_meshData.calcNormals() 2430 2431 return b_mesh
2432 2433 2434 2435 # import animation groups
2436 - def importTextkey(self, niBlock):
2437 """Stores the text keys that define animation start and end in a text 2438 buffer, so that they can be re-exported. Since the text buffer is 2439 cleared on each import only the last import will be exported 2440 correctly.""" 2441 if isinstance(niBlock, NifFormat.NiControllerSequence): 2442 txk = niBlock.textKeys 2443 else: 2444 txk = niBlock.find(block_type=NifFormat.NiTextKeyExtraData) 2445 if txk: 2446 # get animation text buffer, and clear it if it already exists 2447 try: 2448 animtxt = [txt for txt in Blender.Text.Get() if txt.getName() == "Anim"][0] 2449 animtxt.clear() 2450 except: 2451 animtxt = Blender.Text.New("Anim") 2452 2453 frame = 1 2454 for key in txk.textKeys: 2455 newkey = str(key.value).replace('\r\n', '/').rstrip('/') 2456 frame = 1 + int(key.time * self.fps + 0.5) # time 0.0 is frame 1 2457 animtxt.write('%i/%s\n'%(frame, newkey)) 2458 2459 # set start and end frames 2460 self.scene.getRenderingContext().startFrame(1) 2461 self.scene.getRenderingContext().endFrame(frame)
2462
2463 - def storeBonesExtraMatrix(self):
2464 """Stores correction matrices in a text buffer so that the original 2465 alignment can be re-exported. In order for this to work it is necessary 2466 to mantain the imported names unaltered. Since the text buffer is 2467 cleared on each import only the last import will be exported 2468 correctly.""" 2469 # clear the text buffer, or create new buffer 2470 try: 2471 bonetxt = Blender.Text.Get("BoneExMat") 2472 except NameError: 2473 bonetxt = Blender.Text.New("BoneExMat") 2474 bonetxt.clear() 2475 # write correction matrices to text buffer 2476 for niBone, correction_matrix in self.bonesExtraMatrix.iteritems(): 2477 # skip identity transforms 2478 if sum(sum(abs(x) for x in row) 2479 for row in (correction_matrix - self.IDENTITY44)) \ 2480 < self.EPSILON: 2481 continue 2482 # 'pickle' the correction matrix 2483 line = '' 2484 for row in correction_matrix: 2485 line = '%s;%s,%s,%s,%s' % (line, row[0], row[1], row[2], row[3]) 2486 # we write the bone names with their blender name! 2487 blender_bone_name = self.names[niBone] # NOT niBone.name !! 2488 # write it to the text buffer 2489 bonetxt.write('%s/%s\n' % (blender_bone_name, line[1:]))
2490 2491
2492 - def storeNames(self):
2493 """Stores the original, long object names so that they can be 2494 re-exported. In order for this to work it is necessary to mantain the 2495 imported names unaltered. Since the text buffer is cleared on each 2496 import only the last import will be exported correctly.""" 2497 # clear the text buffer, or create new buffer 2498 try: 2499 namestxt = Blender.Text.Get("FullNames") 2500 except NameError: 2501 namestxt = Blender.Text.New("FullNames") 2502 namestxt.clear() 2503 # write the names to the text buffer 2504 for block, shortname in self.names.iteritems(): 2505 if block.name and shortname != block.name: 2506 namestxt.write('%s;%s\n'% (shortname, block.name))
2507
2508 - def getFramesPerSecond(self, roots):
2509 """Scan all blocks and return a reasonable number for FPS.""" 2510 # find all key times 2511 key_times = [] 2512 for root in roots: 2513 for kfd in root.tree(block_type = NifFormat.NiKeyframeData): 2514 key_times.extend(key.time for key in kfd.translations.keys) 2515 key_times.extend(key.time for key in kfd.scales.keys) 2516 key_times.extend(key.time for key in kfd.quaternionKeys) 2517 key_times.extend(key.time for key in kfd.xyzRotations[0].keys) 2518 key_times.extend(key.time for key in kfd.xyzRotations[1].keys) 2519 key_times.extend(key.time for key in kfd.xyzRotations[2].keys) 2520 for kfi in root.tree(block_type = NifFormat.NiBSplineInterpolator): 2521 if not kfi.basisData: 2522 # skip bsplines without basis data (eg bowidle.kf in 2523 # Oblivion) 2524 continue 2525 key_times.extend( 2526 point * (kfi.stopTime - kfi.startTime) 2527 / (kfi.basisData.numControlPoints - 2) 2528 for point in xrange(kfi.basisData.numControlPoints - 2)) 2529 for uvdata in root.tree(block_type = NifFormat.NiUVData): 2530 for uvgroup in uvdata.uvGroups: 2531 key_times.extend(key.time for key in uvgroup.keys) 2532 # not animated, return a reasonable default 2533 if not key_times: 2534 return 30 2535 # calculate FPS 2536 fps = 30 2537 lowestDiff = sum(abs(int(time*fps+0.5)-(time*fps)) for time in key_times) 2538 # for fps in xrange(1,120): #disabled, used for testing 2539 for testFps in [20, 25, 35]: 2540 diff = sum(abs(int(time*testFps+0.5)-(time*testFps)) for time in key_times) 2541 if diff < lowestDiff: 2542 lowestDiff = diff 2543 fps = testFps 2544 return fps
2545
2546 - def store_animation_data(self, rootBlock):
2547 return 2548 # very slow, implement later 2549 """ 2550 niBlockList = [block for block in rootBlock.tree() if isinstance(block, NifFormat.NiAVObject)] 2551 for niBlock in niBlockList: 2552 kfc = self.find_controller(niBlock, NifFormat.NiKeyframeController) 2553 if not kfc: continue 2554 kfd = kfc.data 2555 if not kfd: continue 2556 _ANIMATION_DATA.extend([{'data': key, 'block': niBlock, 'frame': None} for key in kfd.translations.keys]) 2557 _ANIMATION_DATA.extend([{'data': key, 'block': niBlock, 'frame': None} for key in kfd.scales.keys]) 2558 if kfd.rotationType == 4: 2559 _ANIMATION_DATA.extend([{'data': key, 'block': niBlock, 'frame': None} for key in kfd.xyzRotations.keys]) 2560 else: 2561 _ANIMATION_DATA.extend([{'data': key, 'block': niBlock, 'frame': None} for key in kfd.quaternionKeys]) 2562 2563 # set the frames in the _ANIMATION_DATA list 2564 for key in _ANIMATION_DATA: 2565 # time 0 is frame 1 2566 key['frame'] = 1 + int(key['data'].time * self.fps + 0.5) 2567 2568 # sort by frame, I need this later 2569 _ANIMATION_DATA.sort(lambda key1, key2: cmp(key1['frame'], key2['frame'])) 2570 """
2571 2572
2573 - def find_controller(self, niBlock, controllerType):
2574 """Find a controller.""" 2575 ctrl = niBlock.controller 2576 while ctrl: 2577 if isinstance(ctrl, controllerType): 2578 break 2579 ctrl = ctrl.nextController 2580 return ctrl
2581
2582 - def find_property(self, niBlock, propertyType):
2583 """Find a property.""" 2584 for prop in niBlock.properties: 2585 if isinstance(prop, propertyType): 2586 return prop 2587 return None
2588 2589
2590 - def find_extra(self, niBlock, extratype):
2591 """Find extra data.""" 2592 # pre-10.x.x.x system: extra data chain 2593 extra = niBlock.extraData 2594 while extra: 2595 if isinstance(extra, extratype): 2596 break 2597 extra = extra.nextExtraData 2598 if extra: 2599 return extra 2600 2601 # post-10.x.x.x system: extra data list 2602 for extra in niBlock.extraDataList: 2603 if isinstance(extra, extratype): 2604 return extra 2605 return None
2606
2607 - def set_parents(self, niBlock):
2608 """Set the parent block recursively through the tree, to allow 2609 crawling back as needed.""" 2610 if isinstance(niBlock, NifFormat.NiNode): 2611 # list of non-null children 2612 children = [ child for child in niBlock.children if child ] 2613 for child in children: 2614 child._parent = niBlock 2615 self.set_parents(child)
2616
2617 - def markArmaturesBones(self, niBlock):
2618 """Mark armatures and bones by peeking into NiSkinInstance blocks.""" 2619 # case where we import skeleton only, 2620 # or importing an Oblivion skeleton: 2621 # do all NiNode's as bones 2622 if self.IMPORT_SKELETON == 1 or ( 2623 self.version == 0x14000005 and 2624 self.filebase.lower() in ('skeleton', 'skeletonbeast')): 2625 2626 if not isinstance(niBlock, NifFormat.NiNode): 2627 raise NifImportError('cannot import skeleton: root is not a NiNode') 2628 # for morrowind, take the Bip01 node to be the skeleton root 2629 if self.version == 0x04000002: 2630 skelroot = niBlock.find(block_name = 'Bip01', 2631 block_type = NifFormat.NiNode) 2632 if not skelroot: 2633 skelroot = niBlock 2634 else: 2635 skelroot = niBlock 2636 if skelroot not in self.armatures: 2637 self.armatures[skelroot] = [] 2638 self.logger.info("Selecting node '%s' as skeleton root" 2639 % skelroot.name) 2640 # add bones 2641 for bone in skelroot.tree(): 2642 if bone is skelroot: 2643 continue 2644 if not isinstance(bone, NifFormat.NiNode): 2645 continue 2646 if self.is_grouping_node(bone): 2647 continue 2648 if bone not in self.armatures[skelroot]: 2649 self.armatures[skelroot].append(bone) 2650 return # done! 2651 2652 # attaching to selected armature -> first identify armature and bones 2653 elif self.IMPORT_SKELETON == 2 and not self.armatures: 2654 skelroot = niBlock.find(block_name = self.selectedObjects[0].name) 2655 if not skelroot: 2656 raise NifImportError("nif has no armature '%s'"%self.selectedObjects[0].name) 2657 self.logger.debug("Identified '%s' as armature" % skelroot.name) 2658 self.armatures[skelroot] = [] 2659 for bone_name in self.selectedObjects[0].data.bones.keys(): 2660 # blender bone naming -> nif bone naming 2661 nif_bone_name = self.get_bone_name_for_nif(bone_name) 2662 # find a block with bone name 2663 bone_block = skelroot.find(block_name = nif_bone_name) 2664 # add it to the name list if there is a bone with that name 2665 if bone_block: 2666 self.logger.info( 2667 "Identified nif block '%s' with bone '%s' " 2668 "in selected armature" % (nif_bone_name, bone_name)) 2669 self.names[bone_block] = bone_name 2670 self.armatures[skelroot].append(bone_block) 2671 self.complete_bone_tree(bone_block, skelroot) 2672 2673 # search for all NiTriShape or NiTriStrips blocks... 2674 if isinstance(niBlock, NifFormat.NiTriBasedGeom): 2675 # yes, we found one, get its skin instance 2676 if niBlock.isSkin(): 2677 self.logger.debug("Skin found on block '%s'" % niBlock.name) 2678 # it has a skin instance, so get the skeleton root 2679 # which is an armature only if it's not a skinning influence 2680 # so mark the node to be imported as an armature 2681 skininst = niBlock.skinInstance 2682 skelroot = skininst.skeletonRoot 2683 if self.IMPORT_SKELETON == 0: 2684 if not self.armatures.has_key(skelroot): 2685 self.armatures[skelroot] = [] 2686 self.logger.debug("'%s' is an armature" % skelroot.name) 2687 elif self.IMPORT_SKELETON == 2: 2688 if not self.armatures.has_key(skelroot): 2689 raise NifImportError("nif structure incompatible with '%s' as armature: \nnode '%s' has '%s' as armature"%(self.selectedObjects[0].name, niBlock.name, skelroot.name)) 2690 2691 for i, boneBlock in enumerate(skininst.bones): 2692 if boneBlock not in self.armatures[skelroot]: 2693 self.armatures[skelroot].append(boneBlock) 2694 self.logger.debug( 2695 "'%s' is a bone of armature '%s'" 2696 % (boneBlock.name, skelroot.name)) 2697 # now we "attach" the bone to the armature: 2698 # we make sure all NiNodes from this bone all the way 2699 # down to the armature NiNode are marked as bones 2700 self.complete_bone_tree(boneBlock, skelroot) 2701 2702 # mark all nodes as bones if asked 2703 if self.IMPORT_EXTRANODES: 2704 # add bones 2705 for bone in skelroot.tree(): 2706 if bone is skelroot: 2707 continue 2708 if not isinstance(bone, NifFormat.NiNode): 2709 continue 2710 if self.is_grouping_node(bone): 2711 continue 2712 if bone not in self.armatures[skelroot]: 2713 self.armatures[skelroot].append(bone) 2714 self.logger.debug( 2715 "'%s' marked as extra bone of armature '%s'" 2716 % (bone.name, skelroot.name)) 2717 # we make sure all NiNodes from this bone 2718 # all the way down to the armature NiNode 2719 # are marked as bones 2720 self.complete_bone_tree(bone, skelroot) 2721 2722 # continue down the tree 2723 for child in niBlock.getRefs(): 2724 if not isinstance(child, NifFormat.NiAVObject): continue # skip blocks that don't have transforms 2725 self.markArmaturesBones(child)
2726
2727 - def complete_bone_tree(self, bone, skelroot):
2728 """Make sure that the bones actually form a tree all the way down to 2729 the armature node. Call this function on all bones of a skin instance.""" 2730 # we must already have marked this one as a bone 2731 assert self.armatures.has_key(skelroot) # debug 2732 assert bone in self.armatures[skelroot] # debug 2733 # get the node parent, this should be marked as an armature or as a bone 2734 boneparent = bone._parent 2735 if boneparent != skelroot: 2736 # parent is not the skeleton root 2737 if boneparent not in self.armatures[skelroot]: 2738 # neither is it marked as a bone: so mark the parent as a bone 2739 self.armatures[skelroot].append(boneparent) 2740 # store the coordinates for realignement autodetection 2741 self.logger.debug("'%s' is a bone of armature '%s'" % (boneparent.name, skelroot.name)) 2742 # now the parent is marked as a bone 2743 # recursion: complete the bone tree, 2744 # this time starting from the parent bone 2745 self.complete_bone_tree(boneparent, skelroot)
2746
2747 - def is_bone(self, niBlock):
2748 """Tests a NiNode to see if it's a bone.""" 2749 if not niBlock : return False 2750 for bones in self.armatures.values(): 2751 if niBlock in bones: 2752 return True 2753 return False
2754
2755 - def is_armature_root(self, niBlock):
2756 """Tests a block to see if it's an armature.""" 2757 if isinstance(niBlock, NifFormat.NiNode): 2758 return self.armatures.has_key(niBlock) 2759 return False
2760
2761 - def get_closest_bone(self, niBlock, skelroot):
2762 """Detect closest bone ancestor.""" 2763 par = niBlock._parent 2764 while par: 2765 if par == skelroot: 2766 return None 2767 if self.is_bone(par): 2768 return par 2769 par = par._parent 2770 return par
2771
2772 - def get_blender_object(self, niBlock):
2773 """Retrieves the Blender object or Blender bone matching the block.""" 2774 if self.is_bone(niBlock): 2775 boneName = self.names[niBlock] 2776 armatureName = None 2777 for armatureBlock, boneBlocks in self.armatures.iteritems(): 2778 if niBlock in boneBlocks: 2779 armatureName = self.names[armatureBlock] 2780 break 2781 else: 2782 raise NifImportError("cannot find bone '%s'"%boneName) 2783 armatureObject = Blender.Object.Get(armatureName) 2784 return armatureObject.data.bones[boneName] 2785 else: 2786 return Blender.Object.Get(self.names[niBlock])
2787
2788 - def is_grouping_node(self, niBlock):
2789 """Determine whether node is grouping node. 2790 Returns the children which are grouped, or empty list if it is not a 2791 grouping node.""" 2792 # check that it is a ninode 2793 if not isinstance(niBlock, NifFormat.NiNode): return [] 2794 # root collision node: join everything 2795 if isinstance(niBlock, NifFormat.RootCollisionNode): 2796 return [ child for child in niBlock.children if 2797 isinstance(child, NifFormat.NiTriBasedGeom) ] 2798 # check that node has name 2799 node_name = niBlock.name 2800 if not node_name: 2801 return [] 2802 # strip "NonAccum" trailer, if present 2803 if node_name[-9:].lower() == " nonaccum": 2804 node_name = node_name[:-9] 2805 # get all geometry children 2806 return [ child for child in niBlock.children 2807 if (isinstance(child, NifFormat.NiTriBasedGeom) 2808 and child.name.find(node_name) != -1) ]
2809
2810 - def set_animation(self, niBlock, b_obj):
2811 """Load basic animation info for this object.""" 2812 kfc = self.find_controller(niBlock, NifFormat.NiKeyframeController) 2813 if not kfc: 2814 # no animation data: do nothing 2815 return 2816 2817 if kfc.interpolator: 2818 kfd = kfc.interpolator.data 2819 else: 2820 kfd = kfc.data 2821 2822 if not kfd: 2823 # no animation data: do nothing 2824 return 2825 2826 # denote progress 2827 self.msg_progress("Animation") 2828 self.logger.info("Importing animation data for %s" % b_obj.name) 2829 assert(isinstance(kfd, NifFormat.NiKeyframeData)) 2830 # create an Ipo for this object 2831 b_ipo = b_obj.getIpo() 2832 if b_ipo is None: 2833 b_ipo = Blender.Ipo.New('Object', b_obj.name) 2834 b_obj.setIpo(b_ipo) 2835 # get the animation keys 2836 translations = kfd.translations 2837 scales = kfd.scales 2838 # add the keys 2839 self.logger.debug('Scale keys...') 2840 for key in scales.keys: 2841 frame = 1+int(key.time * self.fps + 0.5) # time 0.0 is frame 1 2842 Blender.Set('curframe', frame) 2843 b_obj.SizeX = key.value 2844 b_obj.SizeY = key.value 2845 b_obj.SizeZ = key.value 2846 b_obj.insertIpoKey(Blender.Object.SIZE) 2847 2848 # detect the type of rotation keys 2849 rotationType = kfd.rotationType 2850 if rotationType == 4: 2851 # uses xyz rotation 2852 xkeys = kfd.xyzRotations[0].keys 2853 ykeys = kfd.xyzRotations[1].keys 2854 zkeys = kfd.xyzRotations[2].keys 2855 self.logger.debug('Rotation keys...(euler)') 2856 for (xkey, ykey, zkey) in izip(xkeys, ykeys, zkeys): 2857 frame = 1+int(xkey.time * self.fps + 0.5) # time 0.0 is frame 1 2858 # XXX we assume xkey.time == ykey.time == zkey.time 2859 Blender.Set('curframe', frame) 2860 # both in radians, no conversion needed 2861 b_obj.RotX = xkey.value 2862 b_obj.RotY = ykey.value 2863 b_obj.RotZ = zkey.value 2864 b_obj.insertIpoKey(Blender.Object.ROT) 2865 else: 2866 # uses quaternions 2867 if kfd.quaternionKeys: 2868 self.logger.debug('Rotation keys...(quaternions)') 2869 for key in kfd.quaternionKeys: 2870 frame = 1+int(key.time * self.fps + 0.5) # time 0.0 is frame 1 2871 Blender.Set('curframe', frame) 2872 rot = Blender.Mathutils.Quaternion(key.value.w, key.value.x, key.value.y, key.value.z).toEuler() 2873 # Blender euler is in degrees, object RotXYZ is in radians 2874 b_obj.RotX = rot.x / self.R2D 2875 b_obj.RotY = rot.y / self.R2D 2876 b_obj.RotZ = rot.z / self.R2D 2877 b_obj.insertIpoKey(Blender.Object.ROT) 2878 2879 if translations.keys: 2880 self.logger.debug('Translation keys...') 2881 for key in translations.keys: 2882 frame = 1+int(key.time * self.fps + 0.5) # time 0.0 is frame 1 2883 Blender.Set('curframe', frame) 2884 b_obj.LocX = key.value.x 2885 b_obj.LocY = key.value.y 2886 b_obj.LocZ = key.value.z 2887 b_obj.insertIpoKey(Blender.Object.LOC) 2888 2889 Blender.Set('curframe', 1)
2890
2891 - def importBhkShape(self, bhkshape):
2892 """Import an oblivion collision shape as list of blender meshes.""" 2893 if isinstance(bhkshape, NifFormat.bhkConvexVerticesShape): 2894 # find vertices (and fix scale) 2895 vertices, triangles = pyffi.utils.quickhull.qhull3d( 2896 [ (7 * vert.x, 7 * vert.y, 7 * vert.z) 2897 for vert in bhkshape.vertices ]) 2898 2899 # create convex mesh 2900 me = Blender.Mesh.New('convexpoly') 2901 for vert in vertices: 2902 me.verts.extend(*vert) 2903 for triangle in triangles: 2904 me.faces.extend(triangle) 2905 2906 # link mesh to scene and set transform 2907 ob = self.scene.objects.new(me, 'convexpoly') 2908 2909 # set bounds type 2910 ob.drawType = Blender.Object.DrawTypes['BOUNDBOX'] 2911 # convex hull shape not in blender Python API 2912 # Blender.Object.RBShapes['CONVEXHULL'] should be 5 2913 ob.rbShapeBoundType = 5 2914 ob.drawMode = Blender.Object.DrawModes['WIRE'] 2915 # radius: quick estimate 2916 ob.rbRadius = min(vert.co.length for vert in me.verts) 2917 2918 # also remove duplicate vertices 2919 numverts = len(me.verts) 2920 # 0.005 = 1/200 2921 numdel = me.remDoubles(0.005) 2922 if numdel: 2923 self.logger.info('Removed %i duplicate vertices \ 2924 (out of %i) from collision mesh' % (numdel, numverts)) 2925 2926 return [ ob ] 2927 2928 elif isinstance(bhkshape, NifFormat.bhkTransformShape): 2929 # import shapes 2930 collision_objs = self.importBhkShape(bhkshape.shape) 2931 # find transformation matrix 2932 transform = Blender.Mathutils.Matrix(*bhkshape.transform.asList()) 2933 transform.transpose() 2934 # fix scale 2935 transform[3][0] *= 7 2936 transform[3][1] *= 7 2937 transform[3][2] *= 7 2938 # apply transform 2939 for ob in collision_objs: 2940 ob.setMatrix(ob.getMatrix('localspace') * transform) 2941 # and return a list of transformed collision shapes 2942 return collision_objs 2943 2944 elif isinstance(bhkshape, NifFormat.bhkRigidBody): 2945 # import shapes 2946 collision_objs = self.importBhkShape(bhkshape.shape) 2947 # find transformation matrix in case of the T version 2948 if isinstance(bhkshape, NifFormat.bhkRigidBodyT): 2949 # set rotation 2950 transform = Blender.Mathutils.Quaternion( 2951 bhkshape.rotation.w, bhkshape.rotation.x, 2952 bhkshape.rotation.y, bhkshape.rotation.z).toMatrix() 2953 transform.resize4x4() 2954 # set translation 2955 transform[3][0] = bhkshape.translation.x * 7 2956 transform[3][1] = bhkshape.translation.y * 7 2957 transform[3][2] = bhkshape.translation.z * 7 2958 # apply transform 2959 for ob in collision_objs: 2960 ob.setMatrix(ob.getMatrix('localspace') * transform) 2961 # set physics flags and mass 2962 for ob in collision_objs: 2963 ob.rbFlags = \ 2964 Blender.Object.RBFlags["ACTOR"] + \ 2965 Blender.Object.RBFlags["DYNAMIC"] + \ 2966 Blender.Object.RBFlags["RIGIDBODY"] + \ 2967 Blender.Object.RBFlags["BOUNDS"] 2968 if bhkshape.mass > 0.0001: 2969 ob.rbMass = bhkshape.mass 2970 # import constraints 2971 # this is done once all objects are imported 2972 # for now, store all imported havok shapes with object lists 2973 self.havokObjects[bhkshape] = collision_objs 2974 # and return a list of transformed collision shapes 2975 return collision_objs 2976 2977 elif isinstance(bhkshape, NifFormat.bhkBoxShape): 2978 # create box 2979 minx = -bhkshape.dimensions.x * 7 2980 maxx = +bhkshape.dimensions.x * 7 2981 miny = -bhkshape.dimensions.y * 7 2982 maxy = +bhkshape.dimensions.y * 7 2983 minz = -bhkshape.dimensions.z * 7 2984 maxz = +bhkshape.dimensions.z * 7 2985 2986 me = Blender.Mesh.New('box') 2987 for x in [minx, maxx]: 2988 for y in [miny, maxy]: 2989 for z in [minz, maxz]: 2990 me.verts.extend(x,y,z) 2991 me.faces.extend( 2992 [[0,1,3,2],[6,7,5,4],[0,2,6,4],[3,1,5,7],[4,5,1,0],[7,6,2,3]]) 2993 2994 # link box to scene and set transform 2995 ob = self.scene.objects.new(me, 'box') 2996 2997 # set bounds type 2998 ob.setDrawType(Blender.Object.DrawTypes['BOUNDBOX']) 2999 ob.rbShapeBoundType = Blender.Object.RBShapes['BOX'] 3000 ob.rbRadius = min(maxx, maxy, maxz) 3001 return [ ob ] 3002 3003 elif isinstance(bhkshape, NifFormat.bhkSphereShape): 3004 minx = miny = minz = -bhkshape.radius * 7 3005 maxx = maxy = maxz = +bhkshape.radius * 7 3006 me = Blender.Mesh.New('sphere') 3007 for x in [minx, maxx]: 3008 for y in [miny, maxy]: 3009 for z in [minz, maxz]: 3010 me.verts.extend(x,y,z) 3011 me.faces.extend( 3012 [[0,1,3,2],[6,7,5,4],[0,2,6,4],[3,1,5,7],[4,5,1,0],[7,6,2,3]]) 3013 3014 # link box to scene and set transform 3015 ob = self.scene.objects.new(me, 'sphere') 3016 3017 # set bounds type 3018 ob.setDrawType(Blender.Object.DrawTypes['BOUNDBOX']) 3019 ob.rbShapeBoundType = Blender.Object.RBShapes['SPHERE'] 3020 ob.rbRadius = maxx 3021 return [ ob ] 3022 3023 elif isinstance(bhkshape, NifFormat.bhkCapsuleShape): 3024 # create capsule mesh 3025 length = (bhkshape.firstPoint - bhkshape.secondPoint).norm() 3026 minx = miny = -bhkshape.radius * 7 3027 maxx = maxy = +bhkshape.radius * 7 3028 minz = -(length + 2*bhkshape.radius) * 3.5 3029 maxz = +(length + 2*bhkshape.radius) * 3.5 3030 3031 me = Blender.Mesh.New('capsule') 3032 for x in [minx, maxx]: 3033 for y in [miny, maxy]: 3034 for z in [minz, maxz]: 3035 me.verts.extend(x,y,z) 3036 me.faces.extend( 3037 [[0,1,3,2],[6,7,5,4],[0,2,6,4],[3,1,5,7],[4,5,1,0],[7,6,2,3]]) 3038 3039 # link box to scene and set transform 3040 ob = self.scene.objects.new(me, 'capsule') 3041 3042 # set bounds type 3043 ob.setDrawType(Blender.Object.DrawTypes['BOUNDBOX']) 3044 ob.rbShapeBoundType = Blender.Object.RBShapes['CYLINDER'] 3045 ob.rbRadius = maxx 3046 3047 # find transform 3048 normal = (bhkshape.firstPoint - bhkshape.secondPoint) / length 3049 normal = Blender.Mathutils.Vector(normal.x, normal.y, normal.z) 3050 minindex = min((abs(x), i) for i, x in enumerate(normal))[1] 3051 orthvec = Blender.Mathutils.Vector([(1 if i == minindex else 0) 3052 for i in (0,1,2)]) 3053 vec1 = Blender.Mathutils.CrossVecs(normal, orthvec) 3054 vec1.normalize() 3055 vec2 = Blender.Mathutils.CrossVecs(normal, vec1) 3056 # the rotation matrix should be such that 3057 # (0,0,1) maps to normal 3058 transform = Blender.Mathutils.Matrix(vec1, vec2, normal) 3059 transform.resize4x4() 3060 transform[3][0] = 3.5 * (bhkshape.firstPoint.x 3061 + bhkshape.secondPoint.x) 3062 transform[3][1] = 3.5 * (bhkshape.firstPoint.y 3063 + bhkshape.secondPoint.y) 3064 transform[3][2] = 3.5 * (bhkshape.firstPoint.z 3065 + bhkshape.secondPoint.z) 3066 ob.setMatrix(transform) 3067 3068 # return object 3069 return [ ob ] 3070 3071 elif isinstance(bhkshape, NifFormat.bhkPackedNiTriStripsShape): 3072 # create mesh for each sub shape 3073 hk_objects = [] 3074 vertex_offset = 0 3075 subshapes = bhkshape.subShapes 3076 if not subshapes: 3077 # fallout 3 stores them in the data 3078 subshapes = bhkshape.data.subShapes 3079 for subshape_num, subshape in enumerate(subshapes): 3080 me = Blender.Mesh.New('poly%i' % subshape_num) 3081 for vert_index in xrange(vertex_offset, 3082 vertex_offset + subshape.numVertices): 3083 vert = bhkshape.data.vertices[vert_index] 3084 me.verts.extend(vert.x * 7, vert.y * 7, vert.z * 7) 3085 for hktriangle in bhkshape.data.triangles: 3086 if ((vertex_offset <= hktriangle.triangle.v1) 3087 and (hktriangle.triangle.v1 3088 < vertex_offset + subshape.numVertices)): 3089 me.faces.extend(hktriangle.triangle.v1 - vertex_offset, 3090 hktriangle.triangle.v2 - vertex_offset, 3091 hktriangle.triangle.v3 - vertex_offset) 3092 else: 3093 continue 3094 # check face normal 3095 align_plus = sum(abs(x) 3096 for x in ( me.faces[-1].no[0] 3097 + hktriangle.normal.x, 3098 me.faces[-1].no[1] 3099 + hktriangle.normal.y, 3100 me.faces[-1].no[2] 3101 + hktriangle.normal.z )) 3102 align_minus = sum(abs(x) 3103 for x in ( me.faces[-1].no[0] 3104 - hktriangle.normal.x, 3105 me.faces[-1].no[1] 3106 - hktriangle.normal.y, 3107 me.faces[-1].no[2] 3108 - hktriangle.normal.z )) 3109 # fix face orientation 3110 if align_plus < align_minus: 3111 me.faces[-1].verts = ( me.faces[-1].verts[1], 3112 me.faces[-1].verts[0], 3113 me.faces[-1].verts[2] ) 3114 3115 # link mesh to scene and set transform 3116 ob = self.scene.objects.new(me, 'poly%i' % subshape_num) 3117 3118 # set bounds type 3119 ob.drawType = Blender.Object.DrawTypes['BOUNDBOX'] 3120 ob.rbShapeBoundType = Blender.Object.RBShapes['POLYHEDERON'] 3121 ob.drawMode = Blender.Object.DrawModes['WIRE'] 3122 # radius: quick estimate 3123 ob.rbRadius = min(vert.co.length for vert in me.verts) 3124 # set material 3125 ob.addProperty("HavokMaterial", subshape.material, "INT") 3126 3127 # also remove duplicate vertices 3128 numverts = len(me.verts) 3129 # 0.005 = 1/200 3130 numdel = me.remDoubles(0.005) 3131 if numdel: 3132 self.logger.info('Removed %i duplicate vertices \ 3133 (out of %i) from collision mesh' % (numdel, numverts)) 3134 3135 vertex_offset += subshape.numVertices 3136 hk_objects.append(ob) 3137 3138 return hk_objects 3139 3140 elif isinstance(bhkshape, NifFormat.bhkNiTriStripsShape): 3141 return reduce(operator.add, 3142 (self.importBhkShape(strips) 3143 for strips in bhkshape.stripsData)) 3144 3145 elif isinstance(bhkshape, NifFormat.NiTriStripsData): 3146 me = Blender.Mesh.New('poly') 3147 # no factor 7 correction!!! 3148 for vert in bhkshape.vertices: 3149 me.verts.extend(vert.x, vert.y, vert.z) 3150 me.faces.extend(list(bhkshape.getTriangles())) 3151 3152 # link mesh to scene and set transform 3153 ob = self.scene.objects.new(me, 'poly') 3154 3155 # set bounds type 3156 ob.drawType = Blender.Object.DrawTypes['BOUNDBOX'] 3157 ob.rbShapeBoundType = Blender.Object.RBShapes['POLYHEDERON'] 3158 ob.drawMode = Blender.Object.DrawModes['WIRE'] 3159 # radius: quick estimate 3160 ob.rbRadius = min(vert.co.length for vert in me.verts) 3161 3162 # also remove duplicate vertices 3163 numverts = len(me.verts) 3164 # 0.005 = 1/200 3165 numdel = me.remDoubles(0.005) 3166 if numdel: 3167 self.logger.info('Removed %i duplicate vertices \ 3168 (out of %i) from collision mesh' % (numdel, numverts)) 3169 3170 return [ ob ] 3171 3172 elif isinstance(bhkshape, NifFormat.bhkMoppBvTreeShape): 3173 return self.importBhkShape(bhkshape.shape) 3174 3175 elif isinstance(bhkshape, NifFormat.bhkListShape): 3176 return reduce(operator.add, ( self.importBhkShape(subshape) 3177 for subshape in bhkshape.subShapes )) 3178 3179 self.logger.warning("Unsupported bhk shape %s" 3180 % bhkshape.__class__.__name__) 3181 return []
3182 3183 3184
3185 - def importHavokConstraints(self, hkbody):
3186 """Imports a bone havok constraint as Blender object constraint.""" 3187 assert(isinstance(hkbody, NifFormat.bhkRigidBody)) 3188 3189 # check for constraints 3190 if not hkbody.constraints: 3191 return 3192 3193 # find objects 3194 if len(self.havokObjects[hkbody]) != 1: 3195 self.logger.warning("""\ 3196 Rigid body with no or multiple shapes, constraints skipped""") 3197 return 3198 3199 b_hkobj = self.havokObjects[hkbody][0] 3200 3201 self.logger.info("Importing constraints for %s" % b_hkobj.name) 3202 3203 # now import all constraints 3204 for hkconstraint in hkbody.constraints: 3205 3206 # check constraint entities 3207 if not hkconstraint.numEntities == 2: 3208 self.logger.warning("Constraint with more than 2 entities, skipped") 3209 continue 3210 if not hkconstraint.entities[0] is hkbody: 3211 self.logger.warning("First constraint entity not self, skipped") 3212 continue 3213 if not hkconstraint.entities[1] in self.havokObjects: 3214 self.logger.warning("Second constraint entity not imported, skipped") 3215 continue 3216 3217 # get constraint descriptor 3218 if isinstance(hkconstraint, NifFormat.bhkRagdollConstraint): 3219 hkdescriptor = hkconstraint.ragdoll 3220 elif isinstance(hkconstraint, NifFormat.bhkLimitedHingeConstraint): 3221 hkdescriptor = hkconstraint.limitedHinge 3222 elif isinstance(hkconstraint, NifFormat.bhkHingeConstraint): 3223 hkdescriptor = hkconstraint.hinge 3224 elif isinstance(hkconstraint, NifFormat.bhkMalleableConstraint): 3225 if hkconstraint.type == 7: 3226 hkdescriptor = hkconstraint.ragdoll 3227 elif hkconstraint.type == 2: 3228 hkdescriptor = hkconstraint.limitedHinge 3229 else: 3230 self.logger.warning("Unknown malleable type (%i), skipped" 3231 % hkconstraint.type) 3232 # extra malleable constraint settings 3233 ### damping parameters not yet in Blender Python API 3234 ### tau (force between bodies) not supported by Blender 3235 else: 3236 self.logger.warning("Unknown constraint type (%s), skipped" 3237 % hkconstraint.__class__.__name__) 3238 continue 3239 3240 # add the constraint as a rigid body joint 3241 b_constr = b_hkobj.constraints.append(Blender.Constraint.Type.RIGIDBODYJOINT) 3242 3243 # note: rigidbodyjoint parameters (from Constraint.c) 3244 # CONSTR_RB_AXX 0.0 3245 # CONSTR_RB_AXY 0.0 3246 # CONSTR_RB_AXZ 0.0 3247 # CONSTR_RB_EXTRAFZ 0.0 3248 # CONSTR_RB_MAXLIMIT0 0.0 3249 # CONSTR_RB_MAXLIMIT1 0.0 3250 # CONSTR_RB_MAXLIMIT2 0.0 3251 # CONSTR_RB_MAXLIMIT3 0.0 3252 # CONSTR_RB_MAXLIMIT4 0.0 3253 # CONSTR_RB_MAXLIMIT5 0.0 3254 # CONSTR_RB_MINLIMIT0 0.0 3255 # CONSTR_RB_MINLIMIT1 0.0 3256 # CONSTR_RB_MINLIMIT2 0.0 3257 # CONSTR_RB_MINLIMIT3 0.0 3258 # CONSTR_RB_MINLIMIT4 0.0 3259 # CONSTR_RB_MINLIMIT5 0.0 3260 # CONSTR_RB_PIVX 0.0 3261 # CONSTR_RB_PIVY 0.0 3262 # CONSTR_RB_PIVZ 0.0 3263 # CONSTR_RB_TYPE 12 3264 # LIMIT 63 3265 # PARSIZEY 63 3266 # TARGET [Object "capsule.002"] 3267 3268 # limit 3, 4, 5 correspond to angular limits along x, y and z 3269 # and are measured in degrees 3270 3271 # pivx/y/z is the pivot point 3272 3273 # set constraint target 3274 b_constr[Blender.Constraint.Settings.TARGET] = \ 3275 self.havokObjects[hkconstraint.entities[1]][0] 3276 # set rigid body type (generic) 3277 b_constr[Blender.Constraint.Settings.CONSTR_RB_TYPE] = 12 3278 # limiting parameters (limit everything) 3279 b_constr[Blender.Constraint.Settings.LIMIT] = 63 3280 3281 # get pivot point 3282 pivot = Blender.Mathutils.Vector( 3283 hkdescriptor.pivotA.x * 7, 3284 hkdescriptor.pivotA.y * 7, 3285 hkdescriptor.pivotA.z * 7) 3286 3287 # get z- and x-axes of the constraint 3288 # (also see export_nif.py NifImport.exportConstraints) 3289 if isinstance(hkdescriptor, NifFormat.RagdollDescriptor): 3290 # for ragdoll, take z to be the twist axis (central axis of the 3291 # cone, that is) 3292 axis_z = Blender.Mathutils.Vector( 3293 hkdescriptor.twistA.x, 3294 hkdescriptor.twistA.y, 3295 hkdescriptor.twistA.z) 3296 # for ragdoll, let x be the plane vector 3297 axis_x = Blender.Mathutils.Vector( 3298 hkdescriptor.planeA.x, 3299 hkdescriptor.planeA.y, 3300 hkdescriptor.planeA.z) 3301 # set the angle limits 3302 # (see http://niftools.sourceforge.net/wiki/Oblivion/Bhk_Objects/Ragdoll_Constraint 3303 # for a nice picture explaining this) 3304 b_constr[Blender.Constraint.Settings.CONSTR_RB_MINLIMIT5] = \ 3305 hkdescriptor.twistMinAngle 3306 b_constr[Blender.Constraint.Settings.CONSTR_RB_MAXLIMIT5] = \ 3307 hkdescriptor.twistMaxAngle 3308 b_constr[Blender.Constraint.Settings.CONSTR_RB_MINLIMIT3] = \ 3309 -hkdescriptor.coneMaxAngle 3310 b_constr[Blender.Constraint.Settings.CONSTR_RB_MAXLIMIT3] = \ 3311 hkdescriptor.coneMaxAngle 3312 b_constr[Blender.Constraint.Settings.CONSTR_RB_MINLIMIT4] = \ 3313 hkdescriptor.planeMinAngle 3314 b_constr[Blender.Constraint.Settings.CONSTR_RB_MAXLIMIT4] = \ 3315 hkdescriptor.planeMaxAngle 3316 elif isinstance(hkdescriptor, NifFormat.LimitedHingeDescriptor): 3317 # for hinge, y is the vector on the plane of rotation defining 3318 # the zero angle 3319 axis_y = Blender.Mathutils.Vector( 3320 hkdescriptor.perp2AxleInA1.x, 3321 hkdescriptor.perp2AxleInA1.y, 3322 hkdescriptor.perp2AxleInA1.z) 3323 # for hinge, take x to be the the axis of rotation 3324 # (this corresponds with Blender's convention for hinges) 3325 axis_x = Blender.Mathutils.Vector( 3326 hkdescriptor.axleA.x, 3327 hkdescriptor.axleA.y, 3328 hkdescriptor.axleA.z) 3329 # for hinge, z is the vector on the plane of rotation defining 3330 # the positive direction of rotation 3331 axis_z = Blender.Mathutils.Vector( 3332 hkdescriptor.perp2AxleInA2.x, 3333 hkdescriptor.perp2AxleInA2.y, 3334 hkdescriptor.perp2AxleInA2.z) 3335 # they should form a orthogonal basis 3336 if (Blender.Mathutils.CrossVecs(axis_x, axis_y) 3337 - axis_z).length > 0.01: 3338 # either not orthogonal, or negative orientation 3339 if (Blender.Mathutils.CrossVecs(-axis_x, axis_y) 3340 - axis_z).length > 0.01: 3341 self.logger.warning("""\ 3342 Axes are not orthogonal in %s; arbitrary orientation has been chosen""" 3343 % hkdescriptor.__class__.__name__) 3344 axis_z = Blender.Mathutils.CrossVecs(axis_x, axis_y) 3345 else: 3346 # fix orientation 3347 self.logger.warning("""\ 3348 X axis flipped in %s to fix orientation""" % hkdescriptor.__class__.__name__) 3349 axis_x = -axis_x 3350 elif isinstance(hkdescriptor, NifFormat.HingeDescriptor): 3351 # for hinge, y is the vector on the plane of rotation defining 3352 # the zero angle 3353 axis_y = Blender.Mathutils.Vector( 3354 hkdescriptor.perp2AxleInA1.x, 3355 hkdescriptor.perp2AxleInA1.y, 3356 hkdescriptor.perp2AxleInA1.z) 3357 # for hinge, z is the vector on the plane of rotation defining 3358 # the positive direction of rotation 3359 axis_z = Blender.Mathutils.Vector( 3360 hkdescriptor.perp2AxleInA2.x, 3361 hkdescriptor.perp2AxleInA2.y, 3362 hkdescriptor.perp2AxleInA2.z) 3363 # take x to be the the axis of rotation 3364 # (this corresponds with Blender's convention for hinges) 3365 axis_x = Blender.Mathutils.CrossVecs(axis_y, axis_z) 3366 else: 3367 raise ValueError("unknown descriptor %s" 3368 % hkdescriptor.__class__.__name__) 3369 3370 # transform pivot point and constraint matrix into object 3371 # coordinates 3372 # (also see export_nif.py NifImport.exportConstraints) 3373 3374 # the pivot point v is in hkbody coordinates 3375 # however blender expects it in object coordinates, v' 3376 # v * R * B = v' * O * T * B' 3377 # with R = rigid body transform (usually unit tf) 3378 # B = nif bone matrix 3379 # O = blender object transform 3380 # T = bone tail matrix (translation in Y direction) 3381 # B' = blender bone matrix 3382 # so we need to cancel out the object transformation by 3383 # v' = v * R * B * B'^{-1} * T^{-1} * O^{-1} 3384 3385 # the local rotation L at the pivot point must be such that 3386 # (axis_z + v) * R * B = ([0 0 1] * L + v') * O * T * B' 3387 # so (taking the rotation parts of all matrices!!!) 3388 # [0 0 1] * L = axis_z * R * B * B'^{-1} * T^{-1} * O^{-1} 3389 # and similarly 3390 # [1 0 0] * L = axis_x * R * B * B'^{-1} * T^{-1} * O^{-1} 3391 # hence these give us the first and last row of L 3392 # which is exactly enough to provide the euler angles 3393 3394 # multiply with rigid body transform 3395 if isinstance(hkbody, NifFormat.bhkRigidBodyT): 3396 # set rotation 3397 transform = Blender.Mathutils.Quaternion( 3398 hkbody.rotation.w, hkbody.rotation.x, 3399 hkbody.rotation.y, hkbody.rotation.z).toMatrix() 3400 transform.resize4x4() 3401 # set translation 3402 transform[3][0] = hkbody.translation.x * 7 3403 transform[3][1] = hkbody.translation.y * 7 3404 transform[3][2] = hkbody.translation.z * 7 3405 # apply transform 3406 pivot = pivot * transform 3407 transform = transform.rotationPart() 3408 axis_z = axis_z * transform 3409 axis_x = axis_x * transform 3410 3411 # next, cancel out bone matrix correction 3412 # note that B' = X * B with X = self.bonesExtraMatrix[B] 3413 # so multiply with the inverse of X 3414 for niBone in self.bonesExtraMatrix: 3415 if niBone.collisionObject \ 3416 and niBone.collisionObject.body is hkbody: 3417 transform = Blender.Mathutils.Matrix( 3418 self.bonesExtraMatrix[niBone]) 3419 transform.invert() 3420 pivot = pivot * transform 3421 transform = transform.rotationPart() 3422 axis_z = axis_z * transform 3423 axis_x = axis_x * transform 3424 break 3425 3426 # cancel out bone tail translation 3427 if b_hkobj.parentbonename: 3428 pivot[1] -= b_hkobj.getParent().data.bones[ 3429 b_hkobj.parentbonename].length 3430 3431 # cancel out object transform 3432 transform = Blender.Mathutils.Matrix( 3433 b_hkobj.getMatrix('localspace')) 3434 transform.invert() 3435 pivot = pivot * transform 3436 transform = transform.rotationPart() 3437 axis_z = axis_z * transform 3438 axis_x = axis_x * transform 3439 3440 # set pivot point 3441 b_constr[Blender.Constraint.Settings.CONSTR_RB_PIVX] = pivot[0] 3442 b_constr[Blender.Constraint.Settings.CONSTR_RB_PIVY] = pivot[1] 3443 b_constr[Blender.Constraint.Settings.CONSTR_RB_PIVZ] = pivot[2] 3444 3445 # set euler angles 3446 constr_matrix = Blender.Mathutils.Matrix( 3447 axis_x, 3448 Blender.Mathutils.CrossVecs(axis_z, axis_x), 3449 axis_z) 3450 constr_euler = constr_matrix.toEuler() 3451 b_constr[Blender.Constraint.Settings.CONSTR_RB_AXX] = constr_euler.x 3452 b_constr[Blender.Constraint.Settings.CONSTR_RB_AXY] = constr_euler.y 3453 b_constr[Blender.Constraint.Settings.CONSTR_RB_AXZ] = constr_euler.z 3454 # DEBUG 3455 assert((axis_x - Blender.Mathutils.Vector(1,0,0) * constr_matrix).length < 0.0001) 3456 assert((axis_z - Blender.Mathutils.Vector(0,0,1) * constr_matrix).length < 0.0001) 3457 3458 # the generic rigid body type is very buggy... so for simulation 3459 # purposes let's transform it into ball and hinge 3460 if isinstance(hkdescriptor, NifFormat.RagdollDescriptor): 3461 # ball 3462 b_constr[Blender.Constraint.Settings.CONSTR_RB_TYPE] = 1 3463 elif isinstance(hkdescriptor, (NifFormat.LimitedHingeDescriptor, 3464 NifFormat.HingeDescriptor)): 3465 # (limited) hinge 3466 b_constr[Blender.Constraint.Settings.CONSTR_RB_TYPE] = 2 3467 else: 3468 raise ValueError("unknown descriptor %s" 3469 % hkdescriptor.__class__.__name__)
3470
3471 - def importBSBound(self, bbox):
3472 """Import a bounding box.""" 3473 me = Blender.Mesh.New('BSBound') 3474 minx = bbox.center.x - bbox.dimensions.x * 0.5 3475 miny = bbox.center.y - bbox.dimensions.y * 0.5 3476 minz = bbox.center.z - bbox.dimensions.z * 0.5 3477 maxx = bbox.center.x + bbox.dimensions.x * 0.5 3478 maxy = bbox.center.y + bbox.dimensions.y * 0.5 3479 maxz = bbox.center.z + bbox.dimensions.z * 0.5 3480 for x in [minx, maxx]: 3481 for y in [miny, maxy]: 3482 for z in [minz, maxz]: 3483 me.verts.extend(x,y,z) 3484 me.faces.extend( 3485 [[0,1,3,2],[6,7,5,4],[0,2,6,4],[3,1,5,7],[4,5,1,0],[7,6,2,3]]) 3486 3487 # link box to scene and set transform 3488 ob = self.scene.objects.new(me, 'BSBound') 3489 3490 # set bounds type 3491 ob.setDrawType(Blender.Object.DrawTypes['BOUNDBOX']) 3492 ob.rbShapeBoundType = Blender.Object.RBShapes['BOX'] 3493 ob.rbRadius = min(maxx, maxy, maxz) 3494 return ob
3495
3496 - def getUVLayerName(self, uvset):
3497 return "UVTex.%03i" % uvset if uvset != 0 else "UVTex"
3498 3499 3500
3501 - def importKfRoot(self, kf_root, root):
3502 """Merge kf into nif. 3503 3504 *** Note: this function will eventually move to PyFFI. *** 3505 """ 3506 3507 self.logger.info("Merging kf tree into nif tree") 3508 3509 # check that this is an Oblivion style kf file 3510 if not isinstance(kf_root, NifFormat.NiControllerSequence): 3511 raise NifImportError("non-Oblivion .kf import not supported") 3512 3513 # import text keys 3514 self.importTextkey(kf_root) 3515 3516 3517 # go over all controlled blocks 3518 for controlledblock in kf_root.controlledBlocks: 3519 # get the name 3520 nodename = controlledblock.getNodeName() 3521 # match from nif tree? 3522 node = root.find(block_name = nodename) 3523 if not node: 3524 self.logger.info( 3525 "Animation for %s but no such node found in nif tree" 3526 % nodename) 3527 continue 3528 # node found, now find the controller 3529 controllertype = controlledblock.getControllerType() 3530 if not controllertype: 3531 self.logger.info( 3532 "Animation for %s without controller type, so skipping" 3533 % nodename) 3534 continue 3535 controller = self.find_controller(node, getattr(NifFormat, controllertype)) 3536 if not controller: 3537 self.logger.info("Animation for %s with %s controller, but no such controller type found in corresponding node, so creating one" 3538 % (nodename, controllertype)) 3539 controller = getattr(NifFormat, controllertype)() 3540 # TODO set all the fields of this controller 3541 node.addController(controller) 3542 # yes! attach interpolator 3543 controller.interpolator = controlledblock.interpolator 3544 # in case of a NiTransformInterpolator without a data block 3545 # we still must re-export the interpolator for Oblivion to 3546 # accept the file 3547 # so simply add dummy keyframe data for this one with just a single 3548 # key to flag the exporter to export the keyframe as interpolator 3549 # (i.e. length 1 keyframes are simply interpolators) 3550 if isinstance(controller.interpolator, 3551 NifFormat.NiTransformInterpolator) \ 3552 and controller.interpolator.data is None: 3553 # create data block 3554 kfi = controller.interpolator 3555 kfi.data = NifFormat.NiTransformData() 3556 # fill with info from interpolator 3557 kfd = controller.interpolator.data 3558 # copy rotation 3559 kfd.numRotationKeys = 1 3560 kfd.rotationType = NifFormat.KeyType.LINEAR_KEY 3561 kfd.quaternionKeys.updateSize() 3562 kfd.quaternionKeys[0].time = 0.0 3563 kfd.quaternionKeys[0].value.x = kfi.rotation.x 3564 kfd.quaternionKeys[0].value.y = kfi.rotation.y 3565 kfd.quaternionKeys[0].value.z = kfi.rotation.z 3566 kfd.quaternionKeys[0].value.w = kfi.rotation.w 3567 # copy translation 3568 kfd.translations.numKeys = 1 3569 kfd.translations.keys.updateSize() 3570 kfd.translations.keys[0].time = 0.0 3571 kfd.translations.keys[0].value.x = kfi.translation.x 3572 kfd.translations.keys[0].value.y = kfi.translation.y 3573 kfd.translations.keys[0].value.z = kfi.translation.z 3574 # ignore scale, usually contains invalid data in interpolator 3575 3576 # save priority for future reference 3577 # (priorities will be stored into the name of a NULL constraint on 3578 # bones, see importArmature function) 3579 self.bonePriorities[node] = controlledblock.priority
3580 3581 # DEBUG: save the file for manual inspection 3582 #niffile = open("C:\\test.nif", "wb") 3583 #NifFormat.write(niffile, 3584 # version = 0x14000005, user_version = 11, roots = [root]) 3585
3586 -def config_callback(**config):
3587 """Called when config script is done. Starts and times import.""" 3588 # saves editmode state and exit editmode if it is enabled 3589 # (cannot make changes mesh data in editmode) 3590 is_editmode = Blender.Window.EditMode() 3591 Blender.Window.EditMode(0) 3592 Blender.Window.WaitCursor(1) 3593 t = Blender.sys.time() 3594 3595 try: 3596 # run importer 3597 importer = NifImport(**config) 3598 importer.logger.info( 3599 'Finished in %.2f seconds' % (Blender.sys.time()-t)) 3600 finally: 3601 Blender.Window.WaitCursor(0) 3602 if is_editmode: 3603 Blender.Window.EditMode(1)
3604
3605 -def fileselect_callback(filename):
3606 """Called once file is selected. Starts config GUI.""" 3607 global _CONFIG 3608 _CONFIG.run(NifConfig.TARGET_IMPORT, filename, config_callback)
3609 3610 if __name__ == '__main__': 3611 _CONFIG = NifConfig() # use global so gui elements don't go out of skope 3612 Blender.Window.FileSelector(fileselect_callback, "Import NIF", _CONFIG.config["IMPORT_FILE"]) 3613