def determine_texture_types(self, b_mat): """Checks all texture nodes of a material and checks their labels for relevant texture cues. Stores all slots as class properties.""" self.b_mat = b_mat self._reset_fields() for b_texture_node in self.get_used_textslots(b_mat): shown_label = b_texture_node.label if shown_label == '': shown_label = b_texture_node.image.name NifLog.debug( f"Found node {b_texture_node.name} of type {shown_label}") # go over all slots for slot_name in self.slots.keys(): if slot_name in shown_label: # slot has already been populated if self.slots[slot_name]: raise NifError( f"Multiple {slot_name} textures in material '{b_mat.name}''.\n" f"Make sure there is only one texture node labeled as '{slot_name}'" ) # it's a new slot so store it self.slots[slot_name] = b_texture_node break # unsupported texture type else: raise NifError( f"Do not know how to export texture node '{b_texture_node.name}' in material '{b_mat.name}' with label '{shown_label}'." f"Delete it or change its label.")
def register(): # addon updater code and configurations in case of broken version, try to register the updater first # so that users can revert back to a working version NifLog.debug("Starting registration") configure_autoupdater() register_modules(MODS, __name__)
def import_bhkcapsule_shape(self, bhk_shape): """Import a BhkCapsule block as a simple cylinder collision object""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") radius = bhk_shape.radius * self.HAVOK_SCALE p_1 = bhk_shape.first_point p_2 = bhk_shape.second_point length = (p_1 - p_2).norm() * self.HAVOK_SCALE first_point = p_1 * self.HAVOK_SCALE second_point = p_2 * self.HAVOK_SCALE minx = miny = -radius maxx = maxy = +radius minz = -radius - length / 2 maxz = length / 2 + radius # create blender object b_obj = Object.box_from_extents("capsule", minx, maxx, miny, maxy, minz, maxz) # here, these are not encoded as a direction so we must first calculate the direction b_obj.matrix_local = self.center_origin_to_matrix( (first_point + second_point) / 2, first_point - second_point) self.set_b_collider(b_obj, bounds_type="CAPSULE", display_type="CAPSULE", radius=radius, n_obj=bhk_shape) return [b_obj]
def import_bhk_ridgidbody_t(self, bhk_shape): """Imports a BhkRigidBody block and applies the transform to the collision objects""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # import shapes collision_objs = self.import_bhk_shape(bhk_shape.shape) # find transformation matrix in case of the T version # set rotation b_rot = bhk_shape.rotation transform = mathutils.Quaternion([b_rot.w, b_rot.x, b_rot.y, b_rot.z]).to_matrix().to_4x4() # set translation b_trans = bhk_shape.translation transform.translation = mathutils.Vector( (b_trans.x, b_trans.y, b_trans.z)) * self.HAVOK_SCALE # apply transform for b_col_obj in collision_objs: b_col_obj.matrix_local = b_col_obj.matrix_local @ transform self._import_bhk_rigid_body(bhk_shape, collision_objs) # and return a list of transformed collision shapes return collision_objs
def process_nispecular_property(self, prop): """SpecularProperty based specular""" # TODO [material][property] if NifData.data.version == 0x14000004: self.b_mat.specular_intensity = 0.0 # no specular prop NifLog.debug("NiSpecularProperty property processed")
def fix_pose(self, n_armature, n_node, armature_space_bind_store, armature_space_pose_store): """reposition non-skeletal bones to maintain their local orientation to their skeletal parents""" for n_child_node in n_node.children: # only process nodes if not isinstance(n_child_node, NifFormat.NiNode): continue if n_child_node not in armature_space_bind_store and n_child_node in armature_space_pose_store: NifLog.debug( f"Calculating bind pose for non-skeletal bone {n_child_node.name}" ) # get matrices for n_node (the parent) - fallback to getter if it is not in the store n_armature_pose = armature_space_pose_store.get( n_node, n_node.get_transform(n_armature)) # get bind of parent node or pose if it has no bind pose n_armature_bind = armature_space_bind_store.get( n_node, n_armature_pose) # the child must have a pose, no need for a fallback n_child_armature_pose = armature_space_pose_store[n_child_node] # get the relative transform of n_child_node from pose * inverted parent pose n_child_local_pose = n_child_armature_pose * n_armature_pose.get_inverse( fast=False) # get object space transform by multiplying with bind of parent bone armature_space_bind_store[ n_child_node] = n_child_local_pose * n_armature_bind self.fix_pose(n_armature, n_child_node, armature_space_bind_store, armature_space_pose_store)
def import_sequence_stream_helper(self, kf_root, b_armature_obj, bind_data): NifLog.debug('Importing NiSequenceStreamHelper...') b_action = self.create_action(b_armature_obj, kf_root.name.decode(), retrieve=False) # import parallel trees of extra datas and keyframe controllers extra = kf_root.extra_data controller = kf_root.controller while extra and controller: # textkeys in the stack do not specify node names, import as markers while isinstance(extra, NifFormat.NiTextKeyExtraData): self.import_text_key_extra_data(extra, b_action) extra = extra.next_extra_data # grabe the node name from string data bone_name = None if isinstance(extra, NifFormat.NiStringExtraData): node_name = extra.string_data.decode() bone_name = block_registry.get_bone_name_for_blender(node_name) # import keyframe controller if bone_name in bind_data: niBone_bind_scale, niBone_bind_rot_inv, niBone_bind_trans = bind_data[ bone_name] self.import_keyframe_controller(controller, b_armature_obj, bone_name, niBone_bind_scale, niBone_bind_rot_inv, niBone_bind_trans) # grab next pair of extra and controller extra = extra.next_extra_data controller = controller.next_controller
def process_nimaterial_property(self, prop): """Import a NiMaterialProperty based material""" NiMaterial().import_material(self.n_block, self.b_mat, prop) # TODO [animation][material] merge this call into import_material MaterialAnimation().import_material_controllers( self.n_block, self.b_mat) NifLog.debug("NiMaterialProperty property processed")
def import_controller_manager(self, n_block, b_obj, b_armature): ctrlm = n_block.controller if ctrlm and isinstance(ctrlm, NifFormat.NiControllerManager): NifLog.debug(f'Importing NiControllerManager') if b_armature: self.get_bind_data(b_armature) for ctrl in ctrlm.controller_sequences: self.import_kf_root(ctrl, b_armature)
def import_bhksphere_shape(self, bhk_shape): """Import a BhkSphere block as a simple sphere collision object""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") r = bhk_shape.radius * self.HAVOK_SCALE b_obj = Object.box_from_extents("sphere", -r, r, -r, r, -r, r) self.set_b_collider(b_obj, display_type="SPHERE", bounds_type='SPHERE', radius=r, n_obj=bhk_shape) return [b_obj]
def import_bhk_ridgid_body(self, bhk_shape): """Imports a BhkRigidBody block and applies the transform to the collision objects""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # import shapes collision_objs = self.import_bhk_shape(bhk_shape.shape) self._import_bhk_rigid_body(bhk_shape, collision_objs) # and return a list of transformed collision shapes return collision_objs
def create_and_link(self, slot_name, n_tex_info): """""" slot_lower = slot_name.lower().replace(' ', '_') import_func_name = f"link_{slot_lower}_node" import_func = getattr(self, import_func_name, None) if not import_func: NifLog.debug(f"Could not find texture linking function {import_func_name}") return b_texture = self.create_texture_slot(n_tex_info) import_func(b_texture)
def import_morph_controller(self, n_node, b_obj): """Import NiGeomMorpherController as shape keys for blender object.""" n_morphCtrl = math.find_controller(n_node, NifFormat.NiGeomMorpherController) if n_morphCtrl: NifLog.debug("NiGeomMorpherController processed") b_mesh = b_obj.data morphData = n_morphCtrl.data if morphData.num_morphs: # get name for base key keyname = morphData.morphs[0].frame_name.decode() if not keyname: keyname = 'Base' # insert base key at frame 1, using relative keys sk_basis = b_obj.shape_key_add(name=keyname) # get base vectors and import all morphs baseverts = morphData.morphs[0].vectors shape_action = self.create_action(b_obj.data.shape_keys, b_obj.name + "-Morphs") for idxMorph in range(1, morphData.num_morphs): # get name for key keyname = morphData.morphs[idxMorph].frame_name.decode() if not keyname: keyname = f'Key {idxMorph}' NifLog.info(f"Inserting key '{keyname}'") # get vectors morph_verts = morphData.morphs[idxMorph].vectors self.morph_mesh(b_mesh, baseverts, morph_verts) shape_key = b_obj.shape_key_add(name=keyname, from_mix=False) # first find the keys # older versions store keys in the morphData morph_data = morphData.morphs[idxMorph] # newer versions store keys in the controller if not morph_data.keys: try: if n_morphCtrl.interpolators: morph_data = n_morphCtrl.interpolators[idxMorph].data.data elif n_morphCtrl.interpolator_weights: morph_data = n_morphCtrl.interpolator_weights[idxMorph].interpolator.data.data except KeyError: NifLog.info(f"Unsupported interpolator \"{type(n_morphCtrl.interpolator_weights['idxMorph'].interpolator)}\"") continue # get the interpolation mode interp = self.get_b_interp_from_n_interp( morph_data.interpolation) fcu = self.create_fcurves(shape_action, "value", (0,), flags=n_morphCtrl.flags, keyname=shape_key.name) # set keyframes for key in morph_data.keys: self.add_key(fcu, key.time, (key.value,), interp)
def store_pose_matrices(self, n_node, n_root): """Stores the nif armature space matrix of a node tree""" # check that n_block is indeed a bone if not self.is_bone(n_node): return None NifLog.debug(f"Storing pose matrix for {n_node.name}") # calculate the transform relative to root, ie. turn nif local into nif armature space self.pose_store[n_node] = n_node.get_transform(n_root) # move down the hierarchy for n_child in n_node.children: self.store_pose_matrices(n_child, n_root)
def configure_autoupdater(): NifLog.debug("Configuring auto-updater") addon_updater_ops.register(bl_info) addon_updater_ops.updater.select_link = select_zip_file addon_updater_ops.updater.use_releases = True addon_updater_ops.updater.remove_pre_update_patterns = [ "*.py", "*.pyc", "*.xml", "*.exe", "*.rst", "VERSION", "*.xsd" ] addon_updater_ops.updater.user = "******" addon_updater_ops.updater.repo = "blender_niftools_addon" addon_updater_ops.updater.website = "https://github.com/niftools/blender_niftools_addon/" addon_updater_ops.updater.version_min_update = (0, 0, 4)
def import_nitextureprop_textures(self, n_texture_desc, nodes_wrapper): # NifLog.debug(f"Importing {n_texture_desc}") # go over all valid texture slots for slot_name, _ in self.slots.items(): # get the field name used by nif xml for this texture slot_lower = slot_name.lower().replace(' ', '_') field_name = f"{slot_lower}_texture" # get the tex desc link has_tex = getattr(n_texture_desc, "has_" + field_name, None) if has_tex: NifLog.debug(f"Texdesc has active {slot_name}") n_tex = getattr(n_texture_desc, field_name) nodes_wrapper.create_and_link(slot_name, n_tex)
def populate_bone_tree(self, skelroot): """Add all of skelroot's bones to its dict_armatures list.""" for bone in skelroot.tree(): if bone is skelroot: continue if not isinstance(bone, NifFormat.NiNode): continue if isinstance(bone, NifFormat.NiLODNode): # LOD nodes are never bones continue if bone not in self.dict_armatures[skelroot]: self.dict_armatures[skelroot].append(bone) NifLog.debug(f"'{bone.name}' marked as extra bone of armature '{skelroot.name}'")
def import_bhkconvex_vertices_shape(self, bhk_shape): """Import a BhkConvexVertex block as a convex hull collision object""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # find vertices (and fix scale) scaled_verts = [(self.HAVOK_SCALE * n_vert.x, self.HAVOK_SCALE * n_vert.y, self.HAVOK_SCALE * n_vert.z) for n_vert in bhk_shape.vertices] verts, faces = qhull3d(scaled_verts) b_obj = Object.mesh_from_data("convexpoly", verts, faces) radius = bhk_shape.radius * self.HAVOK_SCALE self.set_b_collider(b_obj, bounds_type="CONVEX_HULL", radius=radius, n_obj=bhk_shape) return [b_obj]
def check_for_skin(self, n_root): """Checks all tri geometry for skinning, sets self.skinned accordingly""" # set these here once per run self.n_armature = None self.skinned = False for n_block in self.get_skinned_geometries(n_root): self.skinned = True NifLog.debug(f"{n_block.name} has skinning.") # one is enough to require an armature, so stop return # force import of nodes as bones, even if no geometries are present if NifOp.props.process == "SKELETON_ONLY": self.skinned = True NifLog.debug(f"Found no skinned geometries.")
def export_nitextureprop_tex_descs(self, texprop): # go over all valid texture slots for slot_name, b_texture_node in self.slots.items(): if b_texture_node: # get the field name used by nif xml for this texture field_name = f"{slot_name.lower().replace(' ', '_')}_texture" NifLog.debug(f"Activating {field_name} for {b_texture_node.name}") setattr(texprop, "has_"+field_name, True) # get the tex desc link texdesc = getattr(texprop, field_name) uv_index = self.get_uv_node(b_texture_node) # set uv index and source texture to the texdesc texdesc.uv_set = uv_index texdesc.source = TextureWriter.export_source_texture(b_texture_node)
def get_matching_block(self, block_type, **kwargs): """Try to find a block matching block_type. Keyword arguments are a dict of parameters and required attributes of the block""" # go over all blocks of block_type NifLog.debug(f"Looking for {block_type} block. Kwargs: {kwargs}") for block in block_store.block_to_obj: # if isinstance(block, block_type): if block_type in str(type(block)): # skip blocks that don't match additional conditions for param, attribute in kwargs.items(): # now skip this block if any of the conditions does not match if attribute is not None: ret_attr = getattr(block, param, None) if ret_attr != attribute: NifLog.debug( f"break, {param} != {attribute}, returns {ret_attr}" ) break else: # we did not break out of the loop, so all checks went through, so we can use this block NifLog.debug( f"Found existing {block_type} block matching all criteria!" ) return block # we are still here, so we must create a block of this type and set all attributes accordingly NifLog.debug( f"Created new {block_type} block because none matched the required criteria!" ) block = block_store.create_block(block_type) for param, attribute in kwargs.items(): if attribute is not None: setattr(block, param, attribute) return block
def export_uv_controller(self, b_material, n_geom): """Export the material UV controller data.""" # get fcurves - a bit more elaborate here so we can zip with the NiUVData later # nb. these are actually specific to the texture slot in blender # here we don't care and just take the first fcurve that matches fcurves = [] for dp, ind in (("offset", 0), ("offset", 1), ("scale", 0), ("scale", 1)): for fcu in b_material.animation_data.action.fcurves: if dp in fcu.data_path and fcu.array_index == ind: fcurves.append(fcu) break else: fcurves.append(None) # continue if at least one fcurve exists if not any(fcurves): return # get the uv curves and translate them into nif data n_uv_data = NifFormat.NiUVData() for fcu, n_uv_group in zip(fcurves, n_uv_data.uv_groups): if fcu: NifLog.debug(f"Exporting {fcu} as NiUVData") n_uv_group.num_keys = len(fcu.keyframe_points) n_uv_group.interpolation = NifFormat.KeyType.LINEAR_KEY n_uv_group.keys.update_size() for b_point, n_key in zip(fcu.keyframe_points, n_uv_group.keys): # add each point of the curve b_frame, b_value = b_point.co if "offset" in fcu.data_path: # offsets are negated in blender b_value = -b_value n_key.arg = n_uv_group.interpolation n_key.time = b_frame / self.fps n_key.value = b_value # if uv data is present then add the controller so it is exported if fcurves[0].keyframe_points: n_uv_ctrl = NifFormat.NiUVController() self.set_flags_and_timing(n_uv_ctrl, fcurves) n_uv_ctrl.data = n_uv_data # attach block to geometry n_geom.add_controller(n_uv_ctrl)
def import_bhkpackednitristrips_shape(self, bhk_shape): """Import a BhkPackedNiTriStrips block as a Triangle-Mesh collision object""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # create mesh for each sub shape hk_objects = [] vertex_offset = 0 subshapes = bhk_shape.sub_shapes if not subshapes: # fallout 3 stores them in the data subshapes = bhk_shape.data.sub_shapes for subshape_num, subshape in enumerate(subshapes): verts = [] faces = [] for vert_index in range(vertex_offset, vertex_offset + subshape.num_vertices): n_vert = bhk_shape.data.vertices[vert_index] verts.append( (n_vert.x * self.HAVOK_SCALE, n_vert.y * self.HAVOK_SCALE, n_vert.z * self.HAVOK_SCALE)) for bhk_triangle in bhk_shape.data.triangles: bhk_tri = bhk_triangle.triangle if (vertex_offset <= bhk_tri.v_1) and ( bhk_tri.v_1 < vertex_offset + subshape.num_vertices): faces.append((bhk_tri.v_1 - vertex_offset, bhk_tri.v_2 - vertex_offset, bhk_tri.v_3 - vertex_offset)) else: continue b_obj = Object.mesh_from_data(f'poly{subshape_num:d}', verts, faces) radius = min(vert.co.length for vert in b_obj.data.vertices) self.set_b_collider(b_obj, bounds_type="MESH", radius=radius, n_obj=subshape) vertex_offset += subshape.num_vertices hk_objects.append(b_obj) return hk_objects
def complete_bone_tree(self, bone, skelroot): """Make sure that the complete hierarchy from bone up to skelroot is marked in dict_armatures.""" # we must already have marked both as a bone assert skelroot in self.dict_armatures # debug assert bone in self.dict_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 boneparent not in self.dict_armatures[skelroot]: # neither is it marked as a bone: so mark the parent as a bone self.dict_armatures[skelroot].append(boneparent) # store the coordinates for realignement autodetection NifLog.debug(f"'{boneparent.name}' is a bone of armature '{skelroot.name}'") # now the parent is marked as a bone # recursion: complete the bone tree, # this time starting from the parent bone self.complete_bone_tree(boneparent, skelroot)
def import_bhkbox_shape(self, bhk_shape): """Import a BhkBox block as a simple Box collision object""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # create box r = bhk_shape.radius * self.HAVOK_SCALE dims = bhk_shape.dimensions minx = -dims.x * self.HAVOK_SCALE maxx = +dims.x * self.HAVOK_SCALE miny = -dims.y * self.HAVOK_SCALE maxy = +dims.y * self.HAVOK_SCALE minz = -dims.z * self.HAVOK_SCALE maxz = +dims.z * self.HAVOK_SCALE # create blender object b_obj = Object.box_from_extents("box", minx, maxx, miny, maxy, minz, maxz) self.set_b_collider(b_obj, radius=r, n_obj=bhk_shape) return [b_obj]
def process_property_list(self, n_block, b_obj): b_mesh = b_obj.data # get all valid properties that are attached to n_block props = list(prop for prop in itertools.chain( n_block.properties, n_block.bs_properties) if prop is not None) # we need no material if we have no properties if not props: return # just to avoid duped materials, a first pass, make sure a named material is created or retrieved for prop in props: if prop.name: name = prop.name.decode() if name and name in bpy.data.materials: b_mat = bpy.data.materials[name] NifLog.debug( f"Retrieved already imported material {b_mat.name} from name {name}" ) else: b_mat = bpy.data.materials.new(name) NifLog.debug( f"Created material {name} to store properties in {b_mat.name}" ) break else: # bs shaders often have no name, so generate one from mesh name name = n_block.name.decode() + "_nt_mat" b_mat = bpy.data.materials.new(name) NifLog.debug( f"Created material {name} to store properties in {b_mat.name}") # do initial settings for the material here self.nodes_wrapper.b_mat = b_mat self.nodes_wrapper.clear_default_nodes() # link the material to the mesh b_mesh.materials.append(b_mat) # set the vars on every processor for processor in self.processors: processor.n_block = n_block processor.b_obj = b_obj processor.b_mesh = b_mesh processor.b_mat = b_mat processor.nodes_wrapper = self.nodes_wrapper # run all processors for prop in props: NifLog.debug(f"{type(prop)} property found") self.process_property(prop) self.nodes_wrapper.connect_to_output(b_mesh.vertex_colors)
def get_body_part_groups(self, b_obj, b_mesh): """Returns a set of vertices (no dupes) for each body part""" bodypartgroups = [] for bodypartgroupname in NifFormat.BSDismemberBodyPartType( ).get_editor_keys(): vertex_group = b_obj.vertex_groups.get(bodypartgroupname) vertices_list = set() if vertex_group: for b_vert in b_mesh.vertices: for b_groupname in b_vert.groups: if b_groupname.group == vertex_group.index: vertices_list.add(b_vert.index) NifLog.debug(f"Found body part {bodypartgroupname}") bodypartgroups.append([ bodypartgroupname, getattr(NifFormat.BSDismemberBodyPartType, bodypartgroupname), vertices_list ]) return bodypartgroups
def import_name(n_block): """Get name of n_block, ready for blender but not necessarily unique. :param n_block: A named nif block. :type n_block: :class:`~pyffi.formats.nif.NifFormat.NiObjectNET` """ if n_block is None: return "" NifLog.debug(f"Importing name for {n_block.__class__.__name__} block from {n_block.name}") n_name = n_block.name.decode() # if name is empty, create something non-empty if not n_name: n_name = "noname" n_name = get_bone_name_for_blender(n_name) return n_name
def import_controller_sequence(self, kf_root, b_armature_obj, bind_data): NifLog.debug('Importing NiControllerSequence...') b_action = self.create_action(b_armature_obj, kf_root.name.decode()) # import text keys self.import_text_keys(kf_root, b_action) # go over all controlled blocks (NiKeyframeController) for controlledblock in kf_root.controlled_blocks: # get bone name # todo [pyffi] fixed get_node_name() is up, make release and clean up here # ZT2 - old way is not supported by pyffi's get_node_name() n_name = controlledblock.target_name # fallout (node_name) & Loki (StringPalette) if not n_name: n_name = controlledblock.get_node_name() bone_name = block_registry.get_bone_name_for_blender(n_name) if bone_name not in b_armature_obj.data.bones: continue b_bone = b_armature_obj.data.bones[bone_name] # import bone priority b_bone.niftools.priority = controlledblock.priority # import animation if bone_name in bind_data: niBone_bind_scale, niBone_bind_rot_inv, niBone_bind_trans = bind_data[ bone_name] # ZT2 kfc = controlledblock.controller # fallout, Loki if not kfc: kfc = controlledblock.interpolator if kfc: self.import_keyframe_controller(kfc, b_armature_obj, bone_name, niBone_bind_scale, niBone_bind_rot_inv, niBone_bind_trans) # fallout: set global extrapolation mode here (older versions have extrapolation per controller) if kf_root.cycle_type: extend = self.get_extend_from_cycle_type(kf_root.cycle_type) self.set_extrapolation(extend, b_action.fcurves)
def set_alpha(b_mat, n_alpha_prop): NifLog.debug("Alpha prop detected") # flags is a bitfield blend_enable = 1 & n_alpha_prop.flags test_enable = (1 << 9) & n_alpha_prop.flags if blend_enable and test_enable: b_mat.blend_method = "HASHED" b_mat.shadow_method = "HASHED" elif blend_enable: b_mat.blend_method = "BLEND" b_mat.shadow_method = "HASHED" elif test_enable: b_mat.blend_method = "CLIP" b_mat.shadow_method = "CLIP" else: b_mat.blend_method = "OPAQUE" b_mat.shadow_method = "OPAQUE" b_mat.alpha_threshold = n_alpha_prop.threshold / 255 # transparency threshold b_mat.niftools_alpha.alphaflag = n_alpha_prop.flags return b_mat