import Blender, Configfrom Blender import Draw, BGL, sysfrom Blender.Mathutils import *#try:# import psyco# psyco.full()#except:# print 'no psyco for you!'try: from NifFormat.NifFormat import NifFormatexcept: err = """--------------------------ERROR\nThis script requires the NifFormat Python library.Make sure the NifFormat module resides in your Python path or in your Blender scripts folder.If you do not have it: http://niftools.sourceforge.net/--------------------------""" print err Blender.Draw.PupMenu("ERROR%t|NifFormat not found, check console for details") raise## Global variables.#_CONFIG = {}# All UI elements are kept in this dictionary to make sure they never go out of scope_GUI_ELEMENTS = {}# To avoid confusion with event ID handling I register them all in a list_GUI_EVENTS = []_LOGO_PATH = sys.sep.join((Blender.Get('scriptsdir'), "bpymodules", "nifImEx", "niftools_logo.png"))_LOGO_IMAGE = Blender.Image.Load(_LOGO_PATH)_SCRIPT_VERSION = "1.9.0a"_BLOCK_COUNT = 0_READ_PROGRESS = 0.0_BLOCKS_READ = 0.0# dictionary of texture files, to reuse textures_TEXTURES = {}# dictionary of materials, to reuse materials_MATERIALS = {}# dictionary of names, to map NIF names to correct Blender names_NAMES = {}# dictionary of bones, maps Blender bone name to NIF block_BONES = {}# dictionary of bones, maps Blender bone name to matrix that maps the# NIF bone matrix on the Blender bone matrix# B' = X * B, where B' is the Blender bone matrix, and B is the NIF bone matrix_BONES_EXTRA_MATRIX = {}# dictionary of bones that belong to a certain armature# maps NIF armature name to list of NIF bone name_ARMATURES = {}# correction matrices list, the order is +X, +Y, +Z, -X, -Y, -Z_BONE_CORRECTION_MATRICES = (\ Matrix([ 0.0,-1.0, 0.0],[ 1.0, 0.0, 0.0],[ 0.0, 0.0, 1.0]),\ Matrix([ 1.0, 0.0, 0.0],[ 0.0, 1.0, 0.0],[ 0.0, 0.0, 1.0]),\ Matrix([ 1.0, 0.0, 0.0],[ 0.0, 0.0, 1.0],[ 0.0,-1.0, 0.0]),\ Matrix([ 0.0, 1.0, 0.0],[-1.0, 0.0, 0.0],[ 0.0, 0.0, 1.0]),\ Matrix([-1.0, 0.0, 0.0],[ 0.0,-1.0, 0.0],[ 0.0, 0.0, 1.0]),\ Matrix([ 1.0, 0.0, 0.0],[ 0.0, 0.0,-1.0],[ 0.0, 1.0, 0.0]))# identity matrix, for comparisons_IDENTITY44 = Matrix([ 1.0,0.0, 0.0, 0.0],\ [ 0.0, 1.0, 0.0, 0.0],\ [ 0.0, 0.0, 1.0, 0.0],\ [ 0.0, 0.0, 0.0, 1.0])# some variables_EPSILON = 0.005 # used for checking equality with floats, NOT STORED IN CONFIG_MSG_LEVEL = 3 # verbosity level_R2D = 3.14159265358979/180.0 # radians to degrees conversion constant_D2R = 180.0/3.14159265358979 # degrees to radians conversion constant_SCENE = Blender.Scene.GetCurrent() #Blender scene, to avoid redundant code_FPS = _SCENE.getRenderingContext().framesPerSec() #frames per second# check General scripts config key for default behaviors_VERBOSE = Truedef __init__(): global _CONFIG, _VERBOSE, _EPSILON reload(Config) _CONFIG = Config._CONFIG _EPSILON = _CONFIG['EPSILON'] # used for checking equality with floats, NOT STORED IN CONFIG _VERBOSE = _CONFIG['VERBOSE']def addEvent(evName = "NO_NAME"): global _GUI_EVENTS eventId = len(_GUI_EVENTS) if eventId >= 16383: raise "Maximum number of events exceeded" return None _GUI_EVENTS.append(evName) return eventIddef guiText(str = "", xpos = 0, ypos = 0): # To draw text on the screen I have to position its start point first BGL.glRasterPos2i( xpos, ypos) Draw.Text(str)def gui(): global _GUI_ELEMENTS, _GUI_EVENTS, _CONFIG, _LOGO_IMAGE del _GUI_EVENTS[:] # These are to save me some typing H = Blender.Window.GetAreaSize()[1] E = {} # Draw NifTools logo BGL.glEnable(BGL.GL_BLEND ) # enable alpha blending # The odd scale and clip values seem necessary to avoid image artifacts Draw.Image(_LOGO_IMAGE, 50.0, H-100.0, 1.0001, 1.0001) #Draw.Image(logoImg, 50, H-100, 1.0, 1.0, 1.0, 0) # Draw.String(name, event, x, y, width, height, initial, length, tooltip=None) nifFilePath = sys.sep.join((_CONFIG["NIF_IMPORT_PATH"], _CONFIG["NIF_IMPORT_FILE"])) guiText("NIF file path", 500, H-250) E["NIF_FILE_PATH"] = Draw.String("", addEvent("NIF_FILE_PATH"), 50, H-150, 390, 20, nifFilePath, 350, '') E["BROWSE_FILE_PATH"] = Draw.PushButton('...', addEvent("BROWSE_FILE_PATH"), 440, H-150, 30, 20, 'browse') E["ADVANCED"] = Draw.PushButton('advanced', addEvent("ADVANCED"), 410, H-225, 100, 20) E["CANCEL"] = Draw.PushButton('cancel', addEvent("CANCEL"), 160, H-225, 100, 20) E["IMPORT"] = Draw.PushButton('import', addEvent("IMPORT"), 50, H-225, 100, 20) _GUI_ELEMENTS = E Draw.Redraw(1)def buttonEvent(evt): """ Event handler for buttons """ global _CONFIG, _GUI_EVENTS evName = _GUI_EVENTS[evt] if evName == "IMPORT": # import and close print "todo: add checks for file input box" exitGUI() #closes the GUI nifFilePath = sys.sep.join((_CONFIG["NIF_IMPORT_PATH"], _CONFIG["NIF_IMPORT_FILE"])) import_nif(nifFilePath) elif evName == "CANCEL": # cancel exitGUI() elif evName == "ADVANCED": # advanced exitGUI() Config.openGUI("Import") elif evName == "BROWSE_FILE_PATH": # browse file nifFilePath = sys.sep.join((_CONFIG["NIF_IMPORT_PATH"], _CONFIG["NIF_IMPORT_FILE"])) Blender.Window.FileSelector(selectFile, "import .nif", nifFilePath)def event(evt, val): """ Event handler for GUI elements """ #print "event(%i,%i)"%(arg1,arg2) if evt == Draw.ESCKEY: exitGUI()def selectFile(nifFilePath): global _CONFIG if nifFilePath == '' or not sys.exists(nifFilePath): Draw.PupMenu('No file selected or file does not exist%t|Ok') else: _CONFIG["NIF_IMPORT_PATH"] = sys.dirname(nifFilePath) _CONFIG["NIF_IMPORT_FILE"] = sys.basename(nifFilePath) Config._CONFIG = _CONFIG Config.save()def openGUI(): """ Opens the import GUI """ global _CONFIG reload(Config) Config.load() _CONFIG = Config._CONFIG Draw.Register(gui, event, buttonEvent)def exitGUI(): """ Closes the config GUI """ global _CONFIG Config._CONFIG = _CONFIG Config.save() Draw.Exit() Draw.Redraw(1)# Little wrapper for debug messagesdef msg(message='-', level=2): if _VERBOSE: if level <= _MSG_LEVEL: print message## A simple custom exception class.#class NIFImportError(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value)## Emulates the act of pressing the "home" key#def fit_view(): Draw.Redraw(1) winid = Blender.Window.GetScreenInfo(Blender.Window.Types.VIEW3D)[0]['id'] Blender.Window.SetKeyQualifiers(0) Blender.Window.QAdd(winid, Draw.HOMEKEY, 1) Blender.Window.QHandle(winid) Blender.Window.QAdd(winid, Draw.HOMEKEY, 0) Draw.Redraw(1)## Main import function.#def import_nif(filename): global _SCRIPT_VERSION try: # catch NIFImportErrors print "NIFTools NIF import script version %s" % (_SCRIPT_VERSION) Blender.Window.DrawProgressBar(0.0, "Initializing") # read the NIF file f = open(filename, "rb") version, user_version = NifFormat.getVersion(f) if version >= 0: msg("Nif file version: 0x%08X" % (version), 2) Blender.Window.DrawProgressBar(0.33, "Reading file") root_blocks = NifFormat.read(version, user_version, f, verbose = 0) Blender.Window.DrawProgressBar(0.66, "Importing data") for block in root_blocks: msg("root block: %s" % (block.name), 3) import_main(block) elif version == -1: raise NIFImportError("Unsupported NIF version.") else: raise NIFImportError("Not a NIF file.") except NIFImportError, e: # in that case, we raise a menu instead of an exception Blender.Window.DrawProgressBar(1.0, "Import Failed") print 'NIFImportError: ' + e.value Blender.Draw.PupMenu('ERROR%t|' + e.value) return Blender.Window.DrawProgressBar(1.0, "Finished")## Main import function.#def import_main(root_block): # scene info # used to control the progress bar global _SCENE, _CONFIG, _BLOCK_COUNT, _BLOCKS_READ, _READ_PROGRESS #_BLOCK_COUNT = NiObject_NumObjectsInMemory() _READ_PROGRESS = 0.0 _BLOCKS_READ = 0.0 # preprocessing: # scale tree root_block.applyScale(_CONFIG['IMPORT_SCALE_CORRECTION']) # sets the root block parent to None, so that when crawling back the script won't barf root_block._parent = None # set the block parent through the tree, to ensure I can always move backward set_parents(root_block) # mark armature nodes and bones mark_armatures_bones(root_block) # and merge armatures that are bones of others armatures # *** DISABLED *** only single skeleton root supported #merge_armatures() if _VERBOSE and _MSG_LEVEL >= 4: for arm in _ARMATURES.keys(): print "armature '%s':" % (arm.name) for bone in _ARMATURES[arm]: print " bone '%s'" % (bone.name) # read the NIF tree if not is_armature_root(root_block): msg("%s is not an armature root" % (root_block.name), 3) if root_block.children: # yes, we'll process all children of the root node # (this prevents us having to create an empty as a root) blocks = root_block.children # import the extras textkey = find_extra(root_block, NifFormat.NiTextKeyExtraData) if textkey: fb_textkey(textkey) else: # this fixes an issue with nifs where the first block is a NiTriShape blocks = [ root_block ] for niBlock in blocks: b_obj = read_branch(niBlock) else: b_obj = read_branch(root_block) # store bone matrix offsets for re-export if len(_BONES_EXTRA_MATRIX.keys()) > 0: fb_bonemat() # store original names for re-export if len(_NAMES) > 0: fb_fullnames() _SCENE.update(1) # do a full update to make sure all transformations get applied #fit_view() #_SCENE.getCurrentCamera()# Reads the content of the current NIF tree branch to Blender recursivelydef read_branch(niBlock): # used to control the progress bar global _BLOCK_COUNT, _BLOCKS_READ, _READ_PROGRESS _BLOCKS_READ += 1.0 if (_BLOCKS_READ/(_BLOCK_COUNT+1.0)) >= (_READ_PROGRESS + 0.1): _READ_PROGRESS = _BLOCKS_READ/(_BLOCK_COUNT+1.0) Blender.Window.DrawProgressBar(_READ_PROGRESS, "Importing data") if niBlock: if isinstance(niBlock, NifFormat.NiTriBasedGeom): # it's a shape node msg("building mesh in read_branch",3) return fb_mesh(niBlock) else: children = niBlock.children if children: # it's a parent node # import object + children if is_armature_root(niBlock): # the whole bone branch is imported by fb_armature as well b_obj = fb_armature(niBlock) # now also do the meshes read_armature_branch(b_obj, niBlock, niBlock) else: # it's a grouping node b_obj = fb_empty(niBlock) b_children_list = [] children = niBlock.children for child in children: b_child_obj = read_branch(child) if b_child_obj: b_children_list.append(b_child_obj) b_obj.makeParent(b_children_list) b_obj.setMatrix(fb_matrix(niBlock)) # import the animations set_animation(niBlock, b_obj) # import the extras textkey = find_extra(niBlock, NifFormat.NiTextKeyExtraData) if textkey: fb_textkey(textkey) return b_obj return Nonedef read_armature_branch(b_armature, niArmature, niBlock): """ Reads the content of the current NIF tree branch to Blender recursively, as meshes parented to a given armature. Note that niBlock must have been imported previously as an armature, along with all its bones. This function only imports meshes. """ # check if the block is non-null if niBlock: # bone or group node? # is it an AParentNode? # mesh? if isinstance(niBlock, NifFormat.NiTriBasedGeom): msg("building mesh in read_armature_branch",3) return fb_mesh(niBlock) else: children = niBlock.children if children: for child in children: b_mesh = read_armature_branch(b_armature, niArmature, child) if b_mesh: # correct the transform # it's parented to the armature! armature_matrix_inverse = fb_global_matrix(niArmature) armature_matrix_inverse.invert() b_mesh.setMatrix(fb_global_matrix(child) * armature_matrix_inverse) # add a vertex group if it's parented to a bone par_bone = get_closest_bone(child) if par_bone: # set vertex index 1.0 for all vertices that don't yet have a vertex weight # this will mimick the fact that the mesh is parented to the bone b_meshData = b_mesh.getData(mesh=True) verts = [ v.index for v in b_meshData.verts ] # copy vertices, as indices for groupName in b_meshData.getVertGroupNames(): for v in b_meshData.getVertsFromGroup(groupName): try: verts.remove(v) except ValueError: # remove throws value-error if vertex was already removed previously pass if verts: groupName = _NAMES[par_bone] b_meshData.addVertGroup(groupName) b_meshData.assignVertsToGroup(groupName, verts, 1.0, Blender.Mesh.AssignModes.REPLACE) # make it parent of the armature b_armature.makeParentDeform([b_mesh]) # anything else: throw away return None## Get unique name for an object, preserving existing names#def fb_name(niBlock, max_length=22): """ Get unique name for an object, preserving existing names The maximum name length defaults to 22, since this is the maximum for Blender objects, but bone names can reach 32. The task of catching errors is left to the user """ global _NAMES # find unique name for Blender to use uniqueInt = 0 #niName = niBlock["Name"].asString() niName = niBlock.name # remove the "Tri " prefix; this will help when exporting the model again if niName[:4] == "Tri ": niName = niName[4:] name = niName[:max_length-1] # Blender has a rather small name buffer try: while Blender.Object.Get(name): name = '%s.%02d' % (niName[:max_length-4], uniqueInt) uniqueInt +=1 except: pass # save mapping _NAMES[niBlock] = name return name# Retrieves a niBlock's transform matrix as a Mathutil.Matrixdef fb_matrix(niBlock): """Retrieves a niBlock's transform matrix as a Mathutil.Matrix""" return Matrix(*niBlock.getTransform().asList())# Retrieves a block's global transform matrixdef fb_global_matrix(niBlock): """Retrieves a block's global transform matrix""" b_matrix = fb_matrix(niBlock) if niBlock._parent: return b_matrix * fb_global_matrix(niBlock._parent) # yay, recursion return b_matrix# Retrieves a node's bind position matrix# *** DISABLED *** doesn't do what it should do...##def fb_bind_matrix(niBlock):## """Retrieves a node's bind position matrix relative to the armature."""## # I need this hack to trap a few errors## try:## return niBlock._bindMatrix## except AttributeError:## return fb_matrix(niBlock)# Retrieves a block's global bind position matrix# *** DISABLED *** doesn't do what it should do...##def fb_global_bind_matrix(niBlock):## """Retrieves a node's global bind position matrix"""## if niBlock._parent:## return fb_bind_matrix(niBlock) * fb_global_bind_matrix(niBlock._parent) # yay, recursion## return fb_bind_matrix(niBlock)# Decompose Blender transform matrix as a scale, rotation matrix, and translation vectordef decompose_srt(m): # get scale components b_scale_rot = m.rotationPart() b_scale_rot_T = Matrix(b_scale_rot) b_scale_rot_T.transpose() b_scale_rot_2 = b_scale_rot * b_scale_rot_T b_scale = Vector(b_scale_rot_2[0][0] ** 0.5,\ b_scale_rot_2[1][1] ** 0.5,\ b_scale_rot_2[2][2] ** 0.5) # and fix their sign if (b_scale_rot.determinant() < 0): b_scale.negate() # only uniform scaling assert(abs(b_scale[0]-b_scale[1])<_EPSILON) assert(abs(b_scale[1]-b_scale[2])<_EPSILON) b_scale = b_scale[0] # get rotation matrix b_rot = b_scale_rot * (1.0/b_scale) # get translation b_trans = m.translationPart() # done! return b_scale, b_rot, b_trans# Creates and returns a grouping emptydef fb_empty(niBlock): global _SCENE b_empty = Blender.Object.New("Empty", fb_name(niBlock,22)) b_empty.properties['longName'] = niBlock.name _SCENE.objects.link(b_empty) return b_empty# Scans an armature hierarchy, and returns a whole armature.# This is done outside the normal node tree scan to allow for positioning of# the bones.def fb_armature(niBlock): global _SCENE armature_name = fb_name(niBlock,22) armature_matrix_inverse = fb_global_matrix(niBlock) armature_matrix_inverse.invert() b_armature = Blender.Object.New('Armature', armature_name) b_armatureData = Blender.Armature.Armature() b_armatureData.name = armature_name b_armatureData.makeEditable() b_armatureData.drawAxes = True b_armatureData.envelopes = False b_armatureData.vertexGroups = True b_armatureData.drawType = Blender.Armature.STICK #b_armatureData.drawType = Blender.Armature.ENVELOPE #b_armatureData.drawType = Blender.Armature.OCTAHEDRON b_armature.link(b_armatureData) b_armatureData.makeEditable() niChildren = niBlock.children niChildBones = [child for child in niChildren if is_bone(child)] for niBone in niChildBones: # Ok, possibly forwarding the inverse of the armature matrix through all the bone chain is silly, # but I believe it saves some processing time compared to making it global or recalculating it all the time. # And serves the purpose fine fb_bone(niBone, b_armature, b_armatureData, armature_matrix_inverse) b_armatureData.update() _SCENE.objects.link(b_armature) # The armature has been created in editmode, # now we are ready to set the bone keyframes. # *** DISABLED *** until everything else works :-) return b_armature # create an action action = Blender.Armature.NLA.NewAction() action.setActive(b_armature) # go through all armature pose bones (http://www.elysiun.com/forum/viewtopic.php?t=58693) progress = 0.0 bone_count = len(b_armature.getPose().bones) Blender.Window.DrawProgressBar(progress, 'Importing Animations') for bone_idx, (bone_name, b_posebone) in enumerate(b_armature.getPose().bones.items()): # denote progress if (bone_idx*1.0)/bone_count > (progress + 0.1): progress += 0.1 Blender.Window.DrawProgressBar(progress, 'Animation: %s' % bone_name) #if (progress < 0.85): progress += 0.1 #else: progress = 0.1 msg('Importing animation for bone %s' % bone_name, 4) # get bind matrix (NIF format stores full transformations in keyframes, # but Blender wants relative transformations, hence we need to know # the bind position for conversion). Since # [ SRchannel 0 ] [ SRbind 0 ] [ SRchannel * SRbind 0 ] [ SRtotal 0 ] # [ Tchannel 1 ] * [ Tbind 1 ] = [ Tchannel * SRbind + Tbind 1 ] = [ Ttotal 1 ] # with # 'total' the transformations as stored in the NIF keyframes, # 'bind' the Blender bind pose, and # 'channel' the Blender IPO channel, # it follows that # Schannel = Stotal / Sbind # Rchannel = Rtotal * inverse(Rbind) # Tchannel = (Ttotal - Tbind) * inverse(Rbind) / Sbind niBone = _BONES[bone_name] niBone_bind_scale, niBone_bind_rot, niBone_bind_trans = decompose_srt(fb_bind_matrix(niBone)) niBone_bind_rot_inv = Matrix(niBone_bind_rot) niBone_bind_rot_inv.invert() niBone_bind_quat_inv = niBone_bind_rot_inv.toQuat() # we also need the conversion of the original matrix to the new bone matrix, say X, # B' = X * B # (with B' the Blender matrix and B the NIF matrix) because we need that # C' * B' = X * C * B # and therefore # C' = X * C * B * inverse(B') = X * C * inverse(X), where X = B' * inverse(B) # In detail: # [ SRX 0 ] [ SRC 0 ] [ SRX 0 ] # [ TX 1 ] * [ TC 1 ] * inverse( [ TX 1 ] ) = # [ SRX * SRC 0 ] [ inverse(SRX) 0 ] # [ TX * SRC + TC 1 ] * [ -TX * inverse(SRX) 1 ] = # [ SRX * SRC * inverse(SRX) 0 ] # [ (TX * SRC + TC - TX) * inverse(SRX) 1 ] # Hence # SC' = SX * SC / SX = SC # RC' = RX * RC * inverse(RX) # TC' = (TX * SC * RC + TC - TX) * inverse(RX) / SX extra_matrix_scale, extra_matrix_rot, extra_matrix_trans = decompose_srt(_BONES_EXTRA_MATRIX[niBone]) extra_matrix_quat = extra_matrix_rot.toQuat() extra_matrix_rot_inv = Matrix(extra_matrix_rot) extra_matrix_rot_inv.invert() extra_matrix_quat_inv = extra_matrix_rot_inv.toQuat() # now import everything # ############################## #print "disabled armature animation for debug" #kfc = False kfc = find_controller(niBone, NifFormat.NiKeyframeController) if kfc: # get keyframe data kfd = kfc.data assert(isinstance(kfd, NifFormat.NiKeyframeData)) translations = kfd.translations scales = kfd.scales # if we have translation keys, we make a dictionary of # rot_keys and scale_keys, this makes the script work MUCH faster # in most cases if translations: scale_keys_dict = {} rot_keys_dict = {} # add the keys msg('Scale keys...', 4) for scaleKey in scales.keys: frame = 1+int(scaleKey.time * _FPS) # time 0.0 is frame 1 sizeVal = scaleKey.value.getValue() size = sizeVal / niBone_bind_scale # Schannel = Stotal / Sbind b_posebone.size = Blender.Mathutils.Vector(size, size, size) b_posebone.insertKey(b_armature, frame, [Blender.Object.Pose.SIZE]) # this is very slow... :( # fill optimizer dictionary if translations: scale_keys_dict[frame] = size # detect the type of rotation keys rotationType = kfd.rotationType if rotationType == 4: # uses xyz rotation msg('Rotation keys...(euler)', 4) xyzRotations = kfd.xyzRotations for key in xyzRotations: frame = 1+int(quatKey.time * _FPS) # time 0.0 is frame 1 keyVal = key.value euler = Blender.Mathutils.Euler([keyVal.x, keyVal.y, keyVal.z]) quat = euler.toQuat() # beware, CrossQuats takes arguments in a counter-intuitive order: # q1.toMatrix() * q2.toMatrix() == CrossQuats(q2, q1).toMatrix() quatVal = CrossQuats(niBone_bind_quat_inv, quatKey) # Rchannel = Rtotal * inverse(Rbind) rot = CrossQuats(CrossQuats(extra_matrix_quat_inv, quatVal), extra_matrix_quat) # C' = X * C * inverse(X) b_posebone.quat = rot b_posebone.insertKey(b_armature, frame, [Blender.Object.Pose.ROT]) # this is very slow... :( # fill optimizer dictionary if translations: rot_keys_dict[frame] = Blender.Mathutils.Quaternion(rot) else: # uses quaternions msg('Rotation keys...(quaternions)', 4) quaternionKeys = kfd.quaternionKeys for key in quaternionKeys: frame = 1+int(key.time * _FPS) # time 0.0 is frame 1 keyVal = key.value quat = Blender.Mathutils.Quaternion([keyVal.w, keyVal.x, keyVal.y, keyVal.z]) # beware, CrossQuats takes arguments in a counter-intuitive order: # q1.toMatrix() * q2.toMatrix() == CrossQuats(q2, q1).toMatrix() quatVal = CrossQuats(niBone_bind_quat_inv, quat) # Rchannel = Rtotal * inverse(Rbind) rot = CrossQuats(CrossQuats(extra_matrix_quat_inv, quatVal), extra_matrix_quat) # C' = X * C * inverse(X) b_posebone.quat = rot b_posebone.insertKey(b_armature, frame, [Blender.Object.Pose.ROT]) # this is very slow... :( # fill optimizer dictionary if translations: rot_keys_dict[frame] = Blender.Mathutils.Quaternion(rot) msg('Translation keys...', 4) for key in translations.keys: frame = 1+int(key.time * _FPS) # time 0.0 is frame 1 keyVal = key.value trans = Blender.Mathutils.Vector(keyVal.x, keyVal.y, keyVal.z) locVal = (trans - niBone_bind_trans) * niBone_bind_rot_inv * (1.0/niBone_bind_scale)# Tchannel = (Ttotal - Tbind) * inverse(Rbind) / Sbind # we need the rotation matrix at this frame (that's why we inserted the other keys first) if rot_keys_dict: try: rot = rot_keys_dict[frame].toMatrix() except KeyError: # fall back on slow method ipo = action.getChannelIpo(bone_name) quat = Blender.Mathutils.Quaternion() quat.x = ipo.getCurve('QuatX').evaluate(frame) quat.y = ipo.getCurve('QuatY').evaluate(frame) quat.z = ipo.getCurve('QuatZ').evaluate(frame) quat.w = ipo.getCurve('QuatW').evaluate(frame) rot = quat.toMatrix() else: rot = Blender.Mathutils.Matrix([1.0,0.0,0.0],[0.0,1.0,0.0],[0.0,0.0,1.0]) # we also need the scale at this frame if scale_keys_dict: try: sizeVal = scale_keys_dict[frame] except KeyError: ipo = action.getChannelIpo(bone_name) if ipo.getCurve('SizeX'): sizeVal = ipo.getCurve('SizeX').evaluate(frame) # assume uniform scale else: sizeVal = 1.0 else: sizeVal = 1.0 size = Blender.Mathutils.Matrix([sizeVal, 0.0, 0.0], [0.0, sizeVal, 0.0], [0.0, 0.0, sizeVal]) # now we can do the final calculation loc = (extra_matrix_trans * size * rot + locVal - extra_matrix_trans) * extra_matrix_rot_inv * (1.0/extra_matrix_scale) # C' = X * C * inverse(X) b_posebone.loc = loc b_posebone.insertKey(b_armature, frame, [Blender.Object.Pose.LOC]) if translations: del scale_keys_dict del rot_keys_dict return b_armature# Adds a bone to the armature in edit mode.def fb_bone(niBlock, b_armature, b_armatureData, armature_matrix_inverse): # check if it is used at all global _BONES, _BONES_EXTRA_MATRIX, _BONE_CORRECTION_MATRICES, _CONFIG bone_name = fb_name(niBlock, 32) niChildren = niBlock.children niChildNodes = [child for child in niChildren if isinstance(child, NifFormat.NiNode)] niChildBones = [child for child in niChildNodes if is_bone(child)] if is_bone(niBlock): # create bones here... b_bone = Blender.Armature.Editbone() # head: get position from niBlock #armature_space_matrix = fb_global_bind_matrix(niBlock) * armature_matrix_inverse armature_space_matrix = getattr(niBlock, '_bindMatrix', fb_global_matrix(niBlock) * armature_matrix_inverse) b_bone_head_x = armature_space_matrix[3][0] b_bone_head_y = armature_space_matrix[3][1] b_bone_head_z = armature_space_matrix[3][2] # tail: average of children location if len(niChildNodes) > 0: child_matrices = [(fb_global_matrix(child) * armature_matrix_inverse) for child in niChildNodes] b_bone_tail_x = sum([child_matrix[3][0] for child_matrix in child_matrices]) / len(child_matrices) b_bone_tail_y = sum([child_matrix[3][1] for child_matrix in child_matrices]) / len(child_matrices) b_bone_tail_z = sum([child_matrix[3][2] for child_matrix in child_matrices]) / len(child_matrices) else: # no children... continue bone sequence in the same direction as parent, with the same length # this seems to work fine #parent_matrix = fb_global_bind_matrix(niBlock._parent) * armature_matrix_inverse parent_matrix = niBlock._parent._bindMatrix b_parent_head_x = parent_matrix[3][0] b_parent_head_y = parent_matrix[3][1] b_parent_head_z = parent_matrix[3][2] b_bone_tail_x = 2 * b_bone_head_x - b_parent_head_x b_bone_tail_y = 2 * b_bone_head_y - b_parent_head_y b_bone_tail_z = 2 * b_bone_head_z - b_parent_head_z is_zero_length = (int(sum([b_bone_head_x-b_bone_tail_x, b_bone_head_y-b_bone_tail_y, b_bone_head_z-b_bone_tail_z])*200) == 0) if is_zero_length: # this is a 0 length bone, to avoid it being removed I set a default minimum length # Since we later set the matrix explicitly any axis will do here. # TO DO: Compensate for scale b_bone_tail_x = b_bone_head_x + 0.5 # sets the bone heads & tails b_bone.head = Vector(b_bone_head_x, b_bone_head_y, b_bone_head_z) b_bone.tail = Vector(b_bone_tail_x, b_bone_tail_y, b_bone_tail_z) # Can't test zero length bones, we keep the original alignment # with no further correction b_bone.matrix = armature_space_matrix.rotationPart() else: # sets the bone heads & tails b_bone.head = Vector(b_bone_head_x, b_bone_head_y, b_bone_head_z) b_bone.tail = Vector(b_bone_tail_x, b_bone_tail_y, b_bone_tail_z) b_bone.matrix = armature_space_matrix.rotationPart()# *** DISABLED *** no bone correction until binding works## # here we explicitly try to set the matrix from the NIF matrix; this has the following consequences:## # - head is preserved## # - bone length is preserved## # - tail is lost (up to bone length)## (sum_x, sum_y, sum_z, dummy) = fb_matrix(niBlock)[3]## if len(niChildNodes) > 0:## child_local_matrices = [fb_matrix(child) for child in niChildNodes]## sum_x = sum([cm[3][0] for cm in child_local_matrices])## sum_y = sum([cm[3][1] for cm in child_local_matrices])## sum_z = sum([cm[3][2] for cm in child_local_matrices])## listXYZ = [int(c*200) for c in (sum_x, sum_y, sum_z, -sum_x, -sum_y, -sum_z)]## idx_correction = listXYZ.index(max(listXYZ))## alignment_offset = 0.0## if idx_correction == 0 or idx_correction == 3:## alignment_offset = float(abs(sum_y) + abs(sum_z)) / abs(sum_x)## elif idx_correction == 1 or idx_correction == 4:## alignment_offset = float(abs(sum_z) + abs(sum_x)) / abs(sum_y)## else:## alignment_offset = float(abs(sum_x) + abs(sum_y)) / abs(sum_z)## # if alignment is good enough, use the (corrected) NIF matrix## if alignment_offset < 0.25:## m_correction = _BONE_CORRECTION_MATRICES[idx_correction]## b_bone.matrix = m_correction * armature_space_matrix.rotationPart()## # set bone name and store the niBlock for future reference b_armatureData.bones[bone_name] = b_bone _BONES[bone_name] = niBlock # calculate bone difference matrix; we will need this when importing animation old_bone_matrix_inv = Blender.Mathutils.Matrix(armature_space_matrix) old_bone_matrix_inv.invert() new_bone_matrix = Blender.Mathutils.Matrix(b_bone.matrix) new_bone_matrix.resize4x4() new_bone_matrix[3][0] = b_bone_head_x new_bone_matrix[3][1] = b_bone_head_y new_bone_matrix[3][2] = b_bone_head_z _BONES_EXTRA_MATRIX[niBlock] = new_bone_matrix * old_bone_matrix_inv # new * inverse(old) # set bone children for niBone in niChildBones: b_child_bone = fb_bone(niBone, b_armature, b_armatureData, armature_matrix_inverse) b_child_bone.parent = b_bone return b_bone return Nonedef fb_texture(niSourceTexture): """ Returns a Blender Texture object, and stores it in the global TEXTURES dictionary """ global _TEXTURES, _CONFIG if niSourceTexture: try: return _TEXTURES[niSourceTexture] except: pass b_image = None if niSourceTexture.useExternal: # the texture uses an external image file fn = niSourceTexture.fileName # go searching for it textureFile = None searchPathList = [_CONFIG["NIF_IMPORT_PATH"], _CONFIG["BASE_TEXTURE_FOLDER"]] + _CONFIG["TEXTURE_SEARCH_PATH"] # if it looks like a Morrowind style path, use common sense to guess texture path meshes_index = _CONFIG["NIF_IMPORT_PATH"].find("meshes") if meshes_index != -1: searchPathList.append(_CONFIG["NIF_IMPORT_PATH"][:meshes_index] + 'textures') for texdir in searchPathList: texdir.replace( '\\', Blender.sys.sep ) texdir.replace( '/', Blender.sys.sep ) # now a little trick, to satisfy many Morrowind mods if (fn[:9].lower() == 'textures\\') and (texdir[-9:].lower() == '\\textures'): tex = Blender.sys.join( texdir, fn[9:] ) # strip one of the two 'textures' from the path else: tex = Blender.sys.join( texdir, fn ) msg("Searching %s" % tex, 3) if Blender.sys.exists(tex) == 1: # tries to load the file b_image = Blender.Image.Load(tex) # Blender 2.41 will return an image object even if the file format isn't supported, # so to check if the image is actually loaded I need to force an error, hence the # dummy = b_image.size line. try: dummy = b_image.size # file format is supported msg( "Found %s" % tex, 3 ) del dummy break except: b_image = None # not supported, delete image object # file format is not supported or file was not found, therefore # we try to load alternative texture base=tex[:-4] for ext in ('.PNG','.png','.TGA','.tga','.BMP','.bmp','.JPG','.jpg'): alt_tex = base+ext if Blender.sys.exists(alt_tex) == 1: b_image = None try: b_image = Blender.Image.Load(alt_tex) dummy = b_image.size msg( "Found alternate %s" % alt_tex, 3 ) del dummy break except: pass if b_image == None: msg("Texture %s not found and no alternate available" % fn, 2) b_image = Blender.Image.New(tex, 1, 1, 24) # create a stub b_image.filename = tex else: # the texture image is packed inside the nif -> extract it niPixelData = niSourceTexture.pixelData # we only load the first mipmap width = iPixelData.mipmap[0].width height = iPixelData.mipmap[0].height if niPixelData.pixelFormat == 0: bpp = 24 elif niPixelData.pixelFormat == 1: bpp = 32 else: bpp = None if bpp != None: b_image = Blender.Image.New( "TexImg", width, height, bpp ) pixels = iPixelData.GetColors() for x in range( width ): Blender.Window.DrawProgressBar( float( x + 1 ) / float( width ), "Image Extraction") for y in range( height ): pix = pixels[y*height+x] b_image.setPixelF( x, (height-1)-y, ( pix.r, pix.g, pix.b, pix.a ) ) if b_image != None: # create a texture using the loaded image b_texture = Blender.Texture.New() b_texture.setType( 'Image' ) b_texture.setImage( b_image ) b_texture.imageFlags |= Blender.Texture.ImageFlags.INTERPOL b_texture.imageFlags |= Blender.Texture.ImageFlags.MIPMAP _TEXTURES[niSourceTexture] = b_texture return b_texture else: _TEXTURES[ texsource_hash(niSourceTexture) ] = None return None return None# Creates and returns a materialdef fb_material(matProperty, textProperty, alphaProperty, specProperty): global _MATERIALS # First check if material has been created before. try: material = _MATERIALS[(matProperty, textProperty, alphaProperty, specProperty)] return material except KeyError: pass # use the material property for the name, other properties usually have # no name name = fb_name(matProperty) material = Blender.Material.New(name) # Sets the material colors # Specular color spec = matProperty.specularColor material.setSpecCol([spec.r, spec.g, spec.b]) material.setSpec(1.0) # Blender multiplies specular color with this value # Diffuse color diff = matProperty.diffuseColor material.setRGBCol([diff.r, diff.g, diff.b]) # Ambient & emissive color # We assume that ambient & emissive are fractions of the diffuse color. # If it is not an exact fraction, we average out. amb = matProperty.ambientColor emit = matProperty.emissiveColor b_amb = 0.0 b_emit = 0.0 b_n = 0 if (diff.r > _EPSILON): b_amb += amb.r/diff.r b_emit += emit.r/diff.r b_n += 1 if (diff.g > _EPSILON): b_amb += amb.g/diff.g b_emit += emit.g/diff.g b_n += 1 if (diff.b > _EPSILON): b_amb += amb.b/diff.b b_emit += emit.b/diff.b b_n += 1 if (b_n > 0): b_amb /= b_n b_emit /= b_n if (b_amb > 1.0): b_amb = 1.0 if (b_emit > 1.0): b_emit = 1.0 material.setAmb(b_amb) material.setEmit(b_emit) # glossiness glossiness = matProperty.glossiness hardness = int(glossiness * 4) # just guessing really if hardness < 1: hardness = 1 if hardness > 511: hardness = 511 material.setHardness(hardness) # Alpha alpha = matProperty.alpha material.setAlpha(alpha) baseTexture = None glowTexture = None if textProperty: baseTextureDesc = textProperty.baseTexture if baseTextureDesc: baseTexture = fb_texture(baseTextureDesc.source) if baseTexture: # Sets the texture to use face UV coordinates. texco = Blender.Texture.TexCo.UV # Maps the texture to the base color channel. mapto = Blender.Texture.MapTo.COL # Sets the texture for the material material.setTexture(0, baseTexture, texco, mapto) mbaseTexture = material.getTextures()[0] glowTextureDesc = textProperty.glowTexture if glowTextureDesc: glowTexture = fb_texture(glowTextureDesc.source) if glowTexture: # glow maps use alpha from rgb intensity glowTexture.imageFlags |= Blender.Texture.ImageFlags.CALCALPHA # Sets the texture to use face UV coordinates. texco = Blender.Texture.TexCo.UV # Maps the texture to the base color channel. mapto = Blender.Texture.MapTo.COL | Blender.Texture.MapTo.EMIT # Sets the texture for the material material.setTexture(1, glowTexture, texco, mapto) mglowTexture = material.getTextures()[1] # check transparency if alphaProperty: material.mode |= Blender.Material.Modes.ZTRANSP # enable z-buffered transparency # if the image has an alpha channel => then this overrides the material alpha value if baseTexture: if baseTexture.image.depth == 32: # ... crappy way to check for alpha channel in texture baseTexture.imageFlags |= Blender.Texture.ImageFlags.USEALPHA # use the alpha channel mbaseTexture.mapto |= Blender.Texture.MapTo.ALPHA # and map the alpha channel to transparency # for proper display in Blender, we must set the alpha value # to 0 and the "Var" slider in the texture Map To tab to the # NIF material alpha value material.setAlpha(0.0) mbaseTexture.varfac = alpha # non-transparent glow textures have their alpha calculated from RGB # not sure what to do with glow textures that have an alpha channel # for now we ignore those alpha channels else: # no alpha property: force alpha 1.0 in Blender material.setAlpha(1.0) # check specularity if not specProperty: # no specular property: specular color is ignored # we do this by setting specularity zero material.setSpec(0.0) _MATERIALS[(matProperty, textProperty, alphaProperty, specProperty)] = material return material# Creates and returns a raw meshdef fb_mesh(niBlock): global _SCENE assert(isinstance(niBlock, NifFormat.NiTriBasedGeom)) # Mesh name -> must be unique, so tag it if needed b_name = fb_name(niBlock, 22) b_meshData = Blender.Mesh.New(b_name) b_meshData.properties['longName'] = niBlock.name #b_mesh = _SCENE.objects.new(b_meshData, b_name) b_mesh = Blender.Object.New("Mesh", b_name) b_mesh.link(b_meshData) # Sets the mesh as one-sided. This fixes some issue with normals b_meshData.mode = 0 _SCENE.objects.link(b_mesh) # Mesh hidden flag if niBlock.flags & 1 == 1: b_mesh.setDrawType(2) # hidden: wire else: b_mesh.setDrawType(4) # not hidden: shaded # Mesh transform matrix, sets the transform matrix for the object. b_mesh.setMatrix(fb_matrix(niBlock)) # Mesh geometry data. From this I can retrieve all geometry info niData = niBlock.data if not niData: raise NIFImportError("no ShapeData returned. Node name: %s " % b_name) # Vertices verts = niData.vertices # Faces tris = [] if isinstance(niData, NifFormat.NiTriShapeData): tris = niData.triangles elif isinstance(niData, NifFormat.NiTriStripsData): msg("---loading tristrips",3) stripPoints = niData.points for strip in stripPoints: for vert in xrange(2, len(strip)): tri = NifFormat.Triangle() tri.v1 = strip[vert-2] tri.v2 = strip[vert-1] tri.v3 = strip[vert] tris.append(tri) # "Sticky" UV coordinates. these are transformed in Blender UV's # only the first UV set is loaded right now uvco = niData.uvSets # Vertex normals norms = niData.normals v_map = [0]*len(verts) # pre-allocate memory, for faster performance print "todo: add option for seams import" _SEAMS_IMPORT = True if not _SEAMS_IMPORT: # Fast method: don't care about any seams! for i, v in enumerate(verts): v_map[i] = i # NIF vertex i maps to blender vertex i b_meshData.verts.extend(v.x, v.y, v.z) # add the vertex # adds normal info if present. # Blender doesn't calculate these quite properly when importing strips if norms: mv = b_meshData.verts[i] n = norms[i] mv.no = Blender.Mathutils.Vector(n.x, n.y, n.z) else: # Slow method, but doesn't introduce unwanted cracks in UV seams: # Construct vertex map to get unique vertex / normal pair list. # We use a Python dictionary to remove doubles and to keep track of indices. # While we are at it, we also add vertices while constructing the map. # Normals are calculated by Blender. n_map = {} b_v_index = 0 for i, v in enumerate(verts): # The key k identifies unique vertex /normal pairs. # We use a tuple of ints for key, this works MUCH faster than a # tuple of floats. if norms: n = norms[i] k = (int(v.x*200),int(v.y*200),int(v.z*200),\ int(n.x*200),int(n.y*200),int(n.z*200)) else: k = (int(v.x*200),int(v.y*200),int(v.z*200)) # see if we already added this guy, and if so, what index try: n_map_k = n_map[k] # this is the bottle neck... can we speed this up? except KeyError: n_map_k = None if not n_map_k: # not added: new vertex / normal pair n_map[k] = i # unique vertex / normal pair with key k was added, with NIF index i v_map[i] = b_v_index # NIF vertex i maps to blender vertex b_v_index b_meshData.verts.extend(v.x, v.y, v.z) # add the vertex # adds normal info if present. # Blender doesn't calculate these quite properly when importing strips if norms: mv = b_meshData.verts[b_v_index] n = norms[i] mv.no = Blender.Mathutils.Vector(n.x, n.y, n.z) b_v_index += 1 else: # already added v_map[i] = v_map[n_map_k] # NIF vertex i maps to Blender v_map[vertex n_map_nk] # release memory del n_map # Adds the faces to the mesh f_map = [None]*len(tris) b_f_index = 0 for i, f in enumerate(tris): if f.v1 != f.v2 and f.v1 != f.v3 and f.v2 != f.v3: v1=b_meshData.verts[v_map[f.v1]] v2=b_meshData.verts[v_map[f.v2]] v3=b_meshData.verts[v_map[f.v3]] if (v1 == v2) or (v2 == v3) or (v3 == v1): continue # we get a ValueError on faces.extend otherwise tmp1 = len(b_meshData.faces) # extend checks for duplicate faces # see http://www.blender3d.org/documentation/240PythonDoc/Mesh.MFaceSeq-class.html b_meshData.faces.extend(v1, v2, v3) if tmp1 == len(b_meshData.faces): continue # duplicate face! f_map[i] = b_f_index # keep track of added faces, mapping NIF face index to Blender face index b_f_index += 1 # at this point, deleted faces (degenerate or duplicate) # satisfy f_map[i] = None # Sets face smoothing and material if norms: for f in b_meshData.faces: f.smooth = 1 f.mat = 0 else: for f in b_meshData.faces: f.smooth = 0 # no normals, turn off smoothing f.mat = 0 # vertex colors vcol = niData.vertexColors if vcol: # empty list is false b_meshData.vertexColors = 1 for i, f in enumerate(tris): if f_map[i] == None: continue b_face = b_meshData.faces[f_map[i]] vc = vcol[f.v1] b_face.col[0].r = int(vc.r * 255) b_face.col[0].g = int(vc.g * 255) b_face.col[0].b = int(vc.b * 255) b_face.col[0].a = int(vc.a * 255) vc = vcol[f.v2] b_face.col[1].r = int(vc.r * 255) b_face.col[1].g = int(vc.g * 255) b_face.col[1].b = int(vc.b * 255) b_face.col[1].a = int(vc.a * 255) vc = vcol[f.v3] b_face.col[2].r = int(vc.r * 255) b_face.col[2].g = int(vc.g * 255) b_face.col[2].b = int(vc.b * 255) b_face.col[2].a = int(vc.a * 255) # vertex colors influence lighting... # so now we have to set the VCOL_LIGHT flag on the material # see below # UV coordinates # Nif files only support 'sticky' UV coordinates, and duplicates vertices to emulate hard edges and UV seams. # Essentially whenever an hard edge or an UV seam is present the mesh this is converted to an open mesh. # Blender also supports 'per face' UV coordinates, this could be a problem when exporting. # Also, NIF files support a series of texture sets, each one with its set of texture coordinates. For example # on a single "material" I could have a base texture, with a decal texture over it mapped on another set of UV # coordinates. I don't know if Blender can do the same. for uvSet in uvco: # Sets the face UV's for the mesh on. The NIF format only supports vertex UV's, # but Blender only allows explicit editing of face UV's, so I'll load vertex UV's like face UV's b_meshData.faceUV = 1 b_meshData.vertexUV = 0 for i, f in enumerate(tris): if f_map[i] == None: continue uvlist = [] # We have to be careful here... another Blender pitfall: # faces.extend sometimes adds face vertices in different order than # the order of it's arguments, here we detect how it was added, and # hopefully this works in all cases :-) # (note: we assume that faces.extend does not change the orientation) if (v_map[f.v1] == b_meshData.faces[f_map[i]].verts[0].index): # this is how it "should" be for v in (f.v1, f.v2, f.v3): uv=uvSet[v] uvlist.append(Vector(uv.u, 1.0 - uv.v)) b_meshData.faces[f_map[i]].uv = tuple(uvlist) elif (v_map[f.v1] == b_meshData.faces[f_map[i]].verts[1].index): # vertex 3 was added first for v in (f.v3, f.v1, f.v2): uv=uvSet[v] uvlist.append(Vector(uv.u, 1.0 - uv.v)) b_meshData.faces[f_map[i]].uv = tuple(uvlist) elif (v_map[f.v1] == b_meshData.faces[f_map[i]].verts[2].index): # vertex 2 was added first for v in (f.v2, f.v3, f.v1): uv=uvSet[v] uvlist.append(Vector(uv.u, 1.0 - uv.v)) b_meshData.faces[f_map[i]].uv = tuple(uvlist) else: raise NIFImportError("Invalid UV index (BUG?)") # Sets the material for this mesh. NIF files only support one material for each mesh. matProperty = find_property(niBlock, NifFormat.NiMaterialProperty) if matProperty: # Texture textProperty = None if uvco: textProperty = find_property(niBlock, NifFormat.NiTexturingProperty) # Alpha alphaProperty = find_property(niBlock, NifFormat.NiAlphaProperty) # Specularity specProperty = find_property(niBlock, NifFormat.NiSpecularProperty) # create material and assign it to the mesh material = fb_material(matProperty, textProperty, alphaProperty, specProperty) b_meshData.materials = [material] # fix up vertex colors depending on whether we had textures in the material mbasetex = material.getTextures()[0] mglowtex = material.getTextures()[1] if b_meshData.vertexColors == 1: if mbasetex or mglowtex: material.mode |= Blender.Material.Modes.VCOL_LIGHT # textured material: vertex colors influence lighting else: material.mode |= Blender.Material.Modes.VCOL_PAINT # non-textured material: vertex colors incluence color # if there's a base texture assigned to this material sets it to be displayed in Blender's 3D view # but only if we have UV coordinates... if mbasetex and uvco: TEX = Blender.Mesh.FaceModes['TEX'] # face mode bitfield value imgobj = mbasetex.tex.getImage() if imgobj: for f in b_meshData.faces: f.mode = TEX f.image = imgobj # Skinning info, for meshes affected by bones. Adding groups to a mesh can be done only after this is already # linked to an object. skinInstance = niBlock.skinInstance if skinInstance: skinData = skinInstance.data bones = skinInstance.bones boneWeights = skinData.boneList for idx, bone in enumerate(bones): vertexWeights =boneWeights[idx].vertexWeights groupName = _NAMES[bone] b_meshData.addVertGroup(groupName) for skinWeight in vertexWeights: vert = skinWeight.index weight = skinWeight.weight b_meshData.assignVertsToGroup(groupName, [v_map[vert]], weight, Blender.Mesh.AssignModes.REPLACE) # this doesn't quite work as well as it ought to #b_meshData.calcNormals() # let Blender calculate vertex normals # new implementation, uses Mesh instead morphCtrl = find_controller(niBlock, NifFormat.NiGeomMorpherController) if morphCtrl: morphData = morphCtrl.data if morphData.numMorphs: # insert base key at frame 1 b_meshData.insertKey( 1, 'absolute' ) baseverts = morphData.morphs[0].vectors b_ipo = Blender.Ipo.New( 'Key' , 'KeyIpo' ) b_meshData.key.ipo = b_ipo for idxMorph in xrange(1, morphData.numMorphs): morphverts = morphData.morphs[idxMorph].vectors # for each vertex calculate the key position from base pos + delta offset for i in xrange(morphData.numVertices): x = baseverts[i].x y = baseverts[i].y z = baseverts[i].z dx = morphverts[i].x dy = morphverts[i].y dz = morphverts[i].z b_meshData.verts[v_map[i]].co[0] = x + dx b_meshData.verts[v_map[i]].co[1] = y + dy b_meshData.verts[v_map[i]].co[2] = z + dz # update the mesh and insert key b_meshData.insertKey(idxMorph, 'relative') # set up the ipo key curve b_curve = b_ipo.addCurve('Key %i' % idxMorph) # dunno how to set up the bezier triples -> switching to linear instead b_curve.setInterpolation('Linear') # select extrapolation if ( morphCtrl.flags == 0x000c ): b_curve.setExtrapolation( 'Constant' ) elif ( morphCtrl.flags == 0x0008 ): b_curve.setExtrapolation( 'Cyclic' ) else: msg( 'dunno which extrapolation to use: using constant instead', 2 ) b_curve.setExtrapolation( 'Constant' ) # set up the curve's control points morphkeys = morphData.morphs[idxMorph].keys for key in morphkeys: x = key.value.getValue() frame = 1+int(key.time * _FPS) b_curve.addBezier( ( frame, x ) ) # finally: return to base position for i in xrange(morphData.numVertices): x = baseverts[i].x y = baseverts[i].y z = baseverts[i].z b_meshData.verts[v_map[i]].co[0] = x b_meshData.verts[v_map[i]].co[1] = y b_meshData.verts[v_map[i]].co[2] = z # assign ipo to mesh return b_mesh# import animation groupsdef fb_textkey(niBlock): """ Stores the text keys that define animation start and end in a text buffer, so that they can be re-exported. Since the text buffer is cleared on each import only the last import will be exported correctly """ if isinstance(niBlock, NifFormat.NiTextKeyExtraData) and niBlock.textKeys: # get animation text buffer, and clear it if it already exists try: animtxt = [txt for txt in Blender.Text.Get() if txt.getName() == "Anim"][0] animtxt.clear() except: animtxt = Blender.Text.New("Anim") frame = 1 for key in niBlock.textKeys: newkey = str(key.value).replace('\r\n', '/').rstrip('/') frame = 1 + int(key.time * _FPS) # time 0.0 is frame 1 animtxt.write('%i/%s\n'%(frame, newkey)) # set start and end frames _SCENE.getRenderingContext().startFrame(1) _SCENE.getRenderingContext().endFrame(frame)def fb_bonemat(): """ Stores correction matrices in a text buffer so that the original alignment can be re-exported. In order for this to work it is necessary to mantain the imported names unaltered Since the text buffer is cleared on each import only the last import will be exported correctly """ try: bonetxt = [txt for txt in Blender.Text.Get() if txt.getName() == "BoneExMat"][0] bonetxt.clear() except: bonetxt = Blender.Text.New("BoneExMat") for niBone in _BONES_EXTRA_MATRIX.keys(): ln='' for row in _BONES_EXTRA_MATRIX[niBone]: ln='%s;%s,%s,%s,%s' % (ln, row[0],row[1],row[2],row[3]) # print '%s/%s/%s\n' % (a, b, ln[1:]) bonetxt.write('%s/%s\n' % (niBone.name, ln[1:]))def fb_fullnames(): """ Stores the original, long object names so that they can be re-exported. In order for this to work it is necessary to mantain the imported names unaltered. Since the text buffer is cleared on each import only the last import will be exported correctly """ global _NAMES # get the names text buffer try: namestxt = [txt for txt in Blender.Text.Get() if txt.getName() == "FullNames"][0] namestxt.clear() except: namestxt = Blender.Text.New("FullNames") for niBlock in _NAMES.keys(): namestxt.write('%s;%s\n'% (_NAMES[niBlock], niBlock.name))# find a controllerdef find_controller(niBlock, controllerType): """ Finds a controller """ ctrl = niBlock.controller while ctrl: if isinstance(ctrl, controllerType): break ctrl = ctrl.nextController return ctrl# find a propertydef find_property(niBlock, propertyType): """ Finds a controller """ prop = [p for p in niBlock.properties if isinstance(p, propertyType)] if prop: return prop[0] return None# find extra datadef find_extra(niBlock, extratype): # pre-10.x.x.x system: extra data chain extra = niBlock.extraData while extra: if isinstance(extra, extratype): break extra = extra.nextExtraData if extra: return extra # post-10.x.x.x system: extra data list for extra in niBlock.extraDataList: if isinstance(extra, extratype): return extra return None# sets the parent block recursively through the tree, to allow me to crawl back as neededdef set_parents(niBlock): if isinstance(niBlock, NifFormat.NiNode): # list of non-null children children = [child for child in niBlock.children if child] for child in children: child._parent = niBlock set_parents(child)# mark armatures and bones by peeking into NiSkinInstance blocks# also stores the bind position matrix for correct import of skinning infodef mark_armatures_bones(niBlock): global _ARMATURES # search for all NiTriShape or NiTriStrips blocks... if isinstance(niBlock, NifFormat.NiTriBasedGeom): # yes, we found one, get its skin instance if niBlock.isSkin(): msg("skin found on block '%s'" % niBlock.name,3) # it has a skin instance, so get the skeleton root # which is an armature only if it's not a skinning influence # so mark the node to be imported as an armature skelroot = niBlock.skinInstance.skeletonRoot if not _ARMATURES.has_key(skelroot): if _ARMATURES.keys(): raise NIFImportError('models with multiple skeleton roots not yet supported') _ARMATURES[skelroot] = [] msg("'%s' is an armature" % skelroot.name,3) # stores the mesh bind position for later use # note that this matrix is relative to the skeleton root bindMatrix = Matrix(*niBlock.getGeometryRestPosition().asList()) niBlock._bindMatrix = bindMatrix for boneBlock, boneRestPos in niBlock.getBoneRestPositions().items(): # add them, if we haven't already if not boneBlock in _ARMATURES[skelroot]: # sets the rest position for the affected skin boneBlock._bindMatrix = Matrix(*boneRestPos.asList()) _ARMATURES[skelroot].append(boneBlock) msg("'%s' is a bone of armature '%s'" % (boneBlock.name, skelroot.name), 3) else: # ensure that rest position is unique # ... hmmm Blender docs say matrix comparison works # but apparently it doesn't so commented out for now ... #if Matrix(*boneRestPos.asList()) != boneBlock._bindMatrix: tmp = 0.0 for v in Matrix(*boneRestPos.asList())-boneBlock._bindMatrix: tmp += v.length if tmp > _EPSILON: print 'warning for bone', boneBlock.name print Matrix(*boneRestPos.asList()) print boneBlock._bindMatrix print 'multiple geometries influenced by the same bone with different rest poses' # now we "attach" the bone to the armature: # we make sure all NiNodes from this bone all the way # down to the armature NiNode are marked as bones # *** DISABLED *** #complete_bone_tree(bone, skelroot) # nope, it's not a NiTriShape or NiTriStrips # so continue down the tree else: for child in niBlock.getRefs(): mark_armatures_bones(child)# this function helps to make sure that the bones actually form a tree,# all the way down to the armature node# just call it on all bones of a skin instancedef complete_bone_tree(bone, skelroot): # *** DISABLED *** return global _ARMATURES # we must already have marked this one as a bone assert _ARMATURES.has_key(skelroot) # debug assert bone in _ARMATURES[skelroot] # debug # get the node parent, this should be marked as an armature or as a bone boneparent = bone._parent if boneparent != skelroot: # parent is not the skeleton root if not boneparent in _ARMATURES[skelroot]: # neither is it marked as a bone: so mark the parent as a bone _ARMATURES[skelroot].append(boneparent) # store the coordinates for realignement autodetection msg("'%s' is a bone of armature '%s'"%(boneparent.name, skelroot.name),3) # now the parent is marked as a bone # recursion: complete the bone tree, # this time starting from the parent bone complete_bone_tree(boneparent, skelroot)# merge armatures that are bones of other armaturesdef merge_armatures(): # *** DISABLED *** return global _ARMATURES for arm, bones in _ARMATURES.items(): for arm2, bones2 in _ARMATURES.items(): if arm2 in bones and _ARMATURES.has_key(arm): msg("merging armature '%s' into armature '%s'"%(arm2.name, arm.name),3) # arm_name2 is in _ARMATURES[arm_name] # so add every bone2 in _ARMATURES[arm_name2] too for bone2 in bones2: if not bone2 in bones: _ARMATURES[arm].append(bone2) # remove merged armature del _ARMATURES[arm2]# Tests a NiNode to see if it's a bone.def is_bone(niBlock): if niBlock.name[:6] == "Bip01 ": return True # heuristics for bones in _ARMATURES.values(): if niBlock in bones: return True return False# Tests a block to see if it's an armature.def is_armature_root(niBlock): if isinstance(niBlock, NifFormat.NiNode): return _ARMATURES.has_key(niBlock) return False# Detect closest bone ancestor.def get_closest_bone(niBlock): par = niBlock._parent while par: if is_bone(par): return par par = par._parent return par# Main KFM import function. (BROKEN)def import_kfm(filename): Blender.Window.DrawProgressBar(0.0, "Initializing") try: # catch NIFImportErrors # read the KFM file kfm = Kfm() ver = kfm.Read(filename) if ( ver == VER_INVALID ): raise NIFImportError("Not a KFM file.") elif ( ver == VER_UNSUPPORTED ): raise NIFImportError("Unsupported KFM version.") # import the NIF tree import_main(kfm.MergeActions(Blender.sys.dirname(filename))) except NIFImportError, e: # in that case, we raise a menu instead of an exception Blender.Window.DrawProgressBar(1.0, "Import Failed") print 'NIFImportError: ' + e.value Blender.Draw.PupMenu('ERROR%t|' + e.value) return Blender.Window.DrawProgressBar(1.0, "Finished")# Loads basic animation info for this objectdef set_animation(niBlock, b_obj): # *** DISABLED *** return progress = 0.1 kfc = find_controller(niBlock, NifFormat.NiKeyframeController) if kfc: # create an Ipo for this object b_ipo = b_obj.getIpo() if b_ipo == None: b_ipo = Blender.Ipo.New('Object', b_obj.name) b_obj.setIpo(b_ipo) # denote progress Blender.Window.DrawProgressBar(progress, "Animation") if (progress < 0.85): progress += 0.1 else: progress = 0.1 # get keyframe data kfd = kfc.data assert(isinstance(kfd, NifFormat.NiKeyframeData)) #get the animation keys translations = kfd.translations scales = kfd.scales # add the keys msg('Scale keys...', 4) for key in scales.keys: frame = 1+int(key.time * _FPS) # time 0.0 is frame 1 Blender.Set('curframe', frame) size = key.value.getValue() b_obj.SizeX = size b_obj.SizeY = size b_obj.SizeZ = size b_obj.insertIpoKey(Blender.Object.SIZE) # detect the type of rotation keys rotationType = kfd.rotationType if rotationType == 4: # uses xyz rotation xyzRotations = kfd.xyzRotations msg('Rotation keys...(euler)', 4) for key in xyzRotations: frame = 1+int(key.time * _FPS) # time 0.0 is frame 1 Blender.Set('curframe', frame) keyValue = key.value rot = (keyValue.x, keyValue.y, keyValue.z) b_obj.RotX = rot.x * _R2D b_obj.RotY = rot.y * _R2D b_obj.RotZ = rot.z * _R2D b_obj.insertIpoKey(Blender.Object.ROT) else: # uses quaternions quaternionKeys = kfd.quaternionKeys msg('Rotation keys...(quaternions)', 4) for key in quaternionKeys: frame = 1+int(key.time * _FPS) # time 0.0 is frame 1 Blender.Set('curframe', frame) keyValue = key.value rot = Blender.Mathutils.Quaternion(keyValue.w, keyValue.x, keyValue.y, keyValue.z).toEuler() b_obj.RotX = rot.x * _R2D b_obj.RotY = rot.y * _R2D b_obj.RotZ = rot.z * _R2D b_obj.insertIpoKey(Blender.Object.ROT) msg('Translation keys...', 4) for key in translations.keys: frame = 1+int(key.time * _FPS) # time 0.0 is frame 1 Blender.Set('curframe', frame) loc = key.value b_obj.LocX = loc.x b_obj.LocY = loc.y b_obj.LocZ = loc.z b_obj.insertIpoKey(Blender.Object.LOC) Blender.Set('curframe', 1)