Beispiel #1
0
    def create_skin_inst_data(self, b_obj, b_obj_armature, polygon_parts):
        if bpy.context.scene.niftools_scene.game in (
                'FALLOUT_3', 'SKYRIM') and polygon_parts:
            skininst = block_store.create_block("BSDismemberSkinInstance",
                                                b_obj)
        else:
            skininst = block_store.create_block("NiSkinInstance", b_obj)

        # get skeleton root from custom property
        if b_obj.niftools.skeleton_root:
            n_root_name = b_obj.niftools.skeleton_root
        # or use the armature name
        else:
            n_root_name = block_store.get_full_name(b_obj_armature)
        # make sure that such a block exists, find it
        for block in block_store.block_to_obj:
            if isinstance(block, NifFormat.NiNode):
                if block.name.decode() == n_root_name:
                    skininst.skeleton_root = block
                    break
        else:
            raise NifError(f"Skeleton root '{n_root_name}' not found.")

        # create skinning data and link it
        skindata = block_store.create_block("NiSkinData", b_obj)
        skininst.data = skindata

        skindata.has_vertex_weights = True
        # fix geometry rest pose: transform relative to skeleton root
        skindata.set_transform(math.get_object_matrix(b_obj).get_inverse())
        return skininst, skindata
    def export_bsxflags_upb(self, root_block, root_objects):
        # TODO [object][property] Fixme
        NifLog.info("Checking collision")
        # activate oblivion/Fallout 3 collision and physics
        if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3',
                                                     'SKYRIM'):
            b_obj = self.has_collision()
            if b_obj:
                # enable collision
                bsx = block_store.create_block("BSXFlags")
                bsx.name = 'BSX'
                root_block.add_extra_data(bsx)
                found_bsx = False
                for root_object in root_objects:
                    if root_object.niftools.bsxflags:
                        if found_bsx:
                            raise NifError(
                                "Multiple objects have BSXFlags. Only one onject may contain this data"
                            )
                        else:
                            found_bxs = True
                            bsx.integer_data = root_object.niftools.bsxflags

                # many Oblivion nifs have a UPB, but export is disabled as
                # they do not seem to affect anything in the game
                if b_obj.niftools.upb:
                    upb = block_store.create_block("NiStringExtraData")
                    upb.name = 'UPB'
                    if b_obj.niftools.upb == '':
                        upb.string_data = UPB_DEFAULT
                    else:
                        upb.string_data = b_obj.niftools.upb.encode()
                    root_block.add_extra_data(upb)
def export_furniture_marker(n_root, filebase):
    # oblivion and Fallout 3 furniture markers
    if bpy.context.scene.niftools_scene.game in (
            'OBLIVION', 'FALLOUT_3',
            'SKYRIM') and filebase[:15].lower() == 'furnituremarker':
        # exporting a furniture marker for Oblivion/FO3
        try:
            furniturenumber = int(filebase[15:])
        except ValueError:
            raise io_scene_niftools.utils.logging.NifError(
                "Furniture marker has invalid number ({0}).\n"
                "Name your file 'furnituremarkerxx.nif' where xx is a number between 00 and 19."
                .format(filebase[15:]))

        # create furniture marker block
        furnmark = block_store.create_block("BSFurnitureMarker")
        furnmark.name = "FRN"
        furnmark.num_positions = 1
        furnmark.positions.update_size()
        furnmark.positions[0].position_ref_1 = furniturenumber
        furnmark.positions[0].position_ref_2 = furniturenumber

        # create extra string data sgoKeep
        sgokeep = block_store.create_block("NiStringExtraData")
        sgokeep.name = "UPB"  # user property buffer
        sgokeep.string_data = "sgoKeep=1 ExportSel = Yes"  # Unyielding = 0, sgoKeep=1ExportSel = Yes

        # add extra blocks
        n_root.add_extra_data(furnmark)
        n_root.add_extra_data(sgokeep)
Beispiel #4
0
    def export_material_alpha_color_controller(self, b_material, n_mat_prop,
                                               b_dtype, n_dtype):
        """Export the material alpha or color controller data."""

        # get fcurves
        fcurves = [
            fcu for fcu in b_material.animation_data.action.fcurves
            if b_dtype in fcu.data_path
        ]
        if not fcurves:
            return

        # just set the names of the nif data types, main difference between alpha and color
        if b_dtype == "alpha":
            keydata = "NiFloatData"
            interpolator = "NiFloatInterpolator"
            controller = "NiAlphaController"
        else:
            keydata = "NiPosData"
            interpolator = "NiPoint3Interpolator"
            controller = "NiMaterialColorController"

        # create the key data
        n_key_data = block_store.create_block(keydata, fcurves)
        n_key_data.data.num_keys = len(fcurves[0].keyframe_points)
        n_key_data.data.interpolation = NifFormat.KeyType.LINEAR_KEY
        n_key_data.data.keys.update_size()

        # assumption: all curves have same amount of keys and are sampled at the same time
        for i, n_key in enumerate(n_key_data.data.keys):
            frame = fcurves[0].keyframe_points[i].co[0]
            # add each point of the curves
            n_key.arg = n_key_data.data.interpolation
            n_key.time = frame / self.fps
            if b_dtype == "alpha":
                n_key.value = fcurves[0].keyframe_points[i].co[1]
            else:
                n_key.value.x, n_key.value.y, n_key.value.z = [
                    fcu.keyframe_points[i].co[1] for fcu in fcurves
                ]
        # if key data is present
        # then add the controller so it is exported
        if fcurves[0].keyframe_points:
            n_mat_ctrl = block_store.create_block(controller, fcurves)
            n_mat_ipol = block_store.create_block(interpolator, fcurves)
            n_mat_ctrl.interpolator = n_mat_ipol

            self.set_flags_and_timing(n_mat_ctrl, fcurves)
            # set target color only for color controller
            if n_dtype:
                n_mat_ctrl.set_target_color(n_dtype)
            n_mat_ctrl.data = n_key_data
            n_mat_ipol.data = n_key_data
            # attach block to material property
            n_mat_prop.add_controller(n_mat_ctrl)
    def export_kf_root(self, b_armature=None):
        """Creates and returns a KF root block and exports controllers for objects and bones"""
        scene = bpy.context.scene
        game = scene.niftools_scene.game
        if game in ('MORROWIND', 'FREEDOM_FORCE'):
            kf_root = block_store.create_block("NiSequenceStreamHelper")
        elif game in ('SKYRIM', 'OBLIVION', 'FALLOUT_3', 'CIVILIZATION_IV',
                      'ZOO_TYCOON_2', 'FREEDOM_FORCE_VS_THE_3RD_REICH',
                      'MEGAMI_TENSEI_IMAGINE'):
            kf_root = block_store.create_block("NiControllerSequence")
        else:
            raise NifError(f"Keyframe export for '{game}' is not supported.")

        anim_textextra = self.create_text_keys(kf_root)
        targetname = "Scene Root"

        # per-node animation
        if b_armature:
            b_action = self.get_active_action(b_armature)
            for b_bone in b_armature.data.bones:
                self.export_transforms(kf_root, b_armature, b_action, b_bone)
            if game in ('SKYRIM', ):
                targetname = "NPC Root [Root]"
            else:
                # quick hack to set correct target name
                if "Bip01" in b_armature.data.bones:
                    targetname = "Bip01"
                elif "Bip02" in b_armature.data.bones:
                    targetname = "Bip02"

        # per-object animation
        else:
            for b_obj in bpy.data.objects:
                b_action = self.get_active_action(b_obj)
                self.export_transforms(kf_root, b_obj, b_action)

        self.export_text_keys(b_action, anim_textextra)

        kf_root.name = b_action.name
        kf_root.unknown_int_1 = 1
        kf_root.weight = 1.0
        kf_root.cycle_type = NifFormat.CycleType.CYCLE_CLAMP
        kf_root.frequency = 1.0

        if anim_textextra.num_text_keys > 0:
            kf_root.start_time = anim_textextra.text_keys[0].time
            kf_root.stop_time = anim_textextra.text_keys[
                anim_textextra.num_text_keys - 1].time
        else:
            kf_root.start_time = scene.frame_start / self.fps
            kf_root.stop_time = scene.frame_end / self.fps

        kf_root.target_name = targetname
        return kf_root
    def create_controller(parent_block, target_name, priority=0):
        # todo[anim] - make independent of global NifData.data.version, and move check for NifOp.props.animation outside
        n_kfi = None
        n_kfc = None

        try:
            if NifOp.props.animation == 'GEOM_NIF' and NifData.data.version < 0x0A020000:
                # keyframe controllers are not present in geometry only files
                # for more recent versions, the controller and interpolators are
                # present, only the data is not present (see further on)
                return n_kfc, n_kfi
        except AttributeError:
            # kf export has no animation mode
            pass

        # add a KeyframeController block, and refer to this block in the
        # parent's time controller
        if NifData.data.version < 0x0A020000:
            n_kfc = block_store.create_block("NiKeyframeController", None)
        else:
            n_kfc = block_store.create_block("NiTransformController", None)
            n_kfi = block_store.create_block("NiTransformInterpolator", None)
            # link interpolator from the controller
            n_kfc.interpolator = n_kfi
        # if parent is a node, attach controller to that node
        if isinstance(parent_block, NifFormat.NiNode):
            parent_block.add_controller(n_kfc)
            if n_kfi:
                # set interpolator default data
                n_kfi.scale, n_kfi.rotation, n_kfi.translation = parent_block.get_transform(
                ).get_scale_quat_translation()

        # else ControllerSequence, so create a link
        elif isinstance(parent_block, NifFormat.NiControllerSequence):
            controlled_block = parent_block.add_controlled_block()
            controlled_block.priority = priority
            if NifData.data.version < 0x0A020000:
                # older versions need the actual controller blocks
                controlled_block.target_name = target_name
                controlled_block.controller = n_kfc
                # erase reference to target node
                n_kfc.target = None
            else:
                # newer versions need the interpolator blocks
                controlled_block.interpolator = n_kfi
                controlled_block.node_name = target_name
                controlled_block.controller_type = "NiTransformController"
        else:
            raise io_scene_niftools.utils.logging.NifError(
                "Unsupported KeyframeController parent!")

        return n_kfc, n_kfi
Beispiel #7
0
    def export_text_keys(self, b_action):
        """Process b_action's pose markers and return an extra string data block."""
        if NifOp.props.animation == 'GEOM_NIF':
            # animation group extra data is not present in geometry only files
            return
        NifLog.info("Exporting animation groups")

        self.add_dummy_markers(b_action)

        # add a NiTextKeyExtraData block
        n_text_extra = block_store.create_block("NiTextKeyExtraData",
                                                b_action.pose_markers)

        # create a text key for each frame descriptor
        n_text_extra.num_text_keys = len(b_action.pose_markers)
        n_text_extra.text_keys.update_size()
        f0, f1 = b_action.frame_range
        for key, marker in zip(n_text_extra.text_keys, b_action.pose_markers):
            f = marker.frame
            if (f < f0) or (f > f1):
                NifLog.warn(
                    f"Marker out of animated range ({f} not between [{f0}, {f1}])"
                )

            key.time = f / self.fps
            key.value = marker.name.replace('/', '\r\n')

        return n_text_extra
    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_bhk_convex_vertices_shape(self, b_obj, fdistlist, fnormlist,
                                         radius, vertlist):
        colhull = block_store.create_block("bhkConvexVerticesShape", b_obj)
        # colhull.material = n_havok_mat[0]
        colhull.radius = radius

        unk_6 = colhull.unknown_6_floats
        unk_6[2] = -0.0  # enables arrow detection
        unk_6[5] = -0.0  # enables arrow detection
        # note: unknown 6 floats are usually all 0

        # Vertices
        colhull.num_vertices = len(vertlist)
        colhull.vertices.update_size()
        for vhull, vert in zip(colhull.vertices, vertlist):
            vhull.x = vert[0] / self.HAVOK_SCALE
            vhull.y = vert[1] / self.HAVOK_SCALE
            vhull.z = vert[2] / self.HAVOK_SCALE
            # w component is 0

        # Normals
        colhull.num_normals = len(fnormlist)
        colhull.normals.update_size()
        for nhull, norm, dist in zip(colhull.normals, fnormlist, fdistlist):
            nhull.x = norm[0]
            nhull.y = norm[1]
            nhull.z = norm[2]
            nhull.w = dist / self.HAVOK_SCALE

        return colhull
Beispiel #10
0
    def export_visibility(self, n_node, b_action):
        """Export the visibility controller data."""

        if not b_action:
            return

        # get the hide fcurve
        fcurves = [fcu for fcu in b_action.fcurves if "hide" in fcu.data_path]
        if not fcurves:
            return

        # TODO [animation] which sort of controller should be exported?
        #                  should this be driven by version number?
        #                  we probably don't want both at the same time
        # NiVisData = old style, NiBoolData = new style
        n_vis_data = block_store.create_block("NiVisData", fcurves)
        n_vis_data.num_keys = len(fcurves[0].keyframe_points)
        n_vis_data.keys.update_size()

        # we just leave interpolation at constant
        n_bool_data = block_store.create_block("NiBoolData", fcurves)
        n_bool_data.data.interpolation = NifFormat.KeyType.CONST_KEY
        n_bool_data.data.num_keys = len(fcurves[0].keyframe_points)
        n_bool_data.data.keys.update_size()
        for b_point, n_vis_key, n_bool_key in zip(fcurves[0].keyframe_points,
                                                  n_vis_data.keys,
                                                  n_bool_data.data.keys):
            # add each point of the curve
            b_frame, b_value = b_point.co
            n_vis_key.arg = n_bool_data.data.interpolation  # n_vis_data has no interpolation stored
            n_vis_key.time = b_frame / bpy.context.scene.render.fps
            n_vis_key.value = b_value
            n_bool_key.arg = n_bool_data.data.interpolation
            n_bool_key.time = n_vis_key.time
            n_bool_key.value = n_vis_key.value

        # if alpha data is present (check this by checking if times were added) then add the controller so it is exported
        if fcurves[0].keyframe_points:
            n_vis_ctrl = block_store.create_block("NiVisController", fcurves)
            n_vis_ipol = block_store.create_block("NiBoolInterpolator",
                                                  fcurves)
            self.set_flags_and_timing(n_vis_ctrl, fcurves)
            n_vis_ctrl.interpolator = n_vis_ipol
            n_vis_ctrl.data = n_vis_data
            n_vis_ipol.data = n_bool_data
            # attach block to node
            n_node.add_controller(n_vis_ctrl)
 def create_text_keys(self, kf_root):
     """Create the text keys before filling in the data so that the extra data hierarchy is correct"""
     # add a NiTextKeyExtraData block
     n_text_extra = block_store.create_block("NiTextKeyExtraData", None)
     if isinstance(kf_root, NifFormat.NiControllerSequence):
         kf_root.text_keys = n_text_extra
     elif isinstance(kf_root, NifFormat.NiSequenceStreamHelper):
         kf_root.add_extra_data(n_text_extra)
     return n_text_extra
 def export_bhk_blend_controller(self, b_obj, parent_block):
     # also add a controller for it
     n_blend_ctrl = block_store.create_block("bhkBlendController", b_obj)
     n_blend_ctrl.flags = 12
     n_blend_ctrl.frequency = 1.0
     n_blend_ctrl.phase = 0.0
     n_blend_ctrl.start_time = consts.FLOAT_MAX
     n_blend_ctrl.stop_time = consts.FLOAT_MIN
     parent_block.add_controller(n_blend_ctrl)
Beispiel #13
0
    def export_bs_effect_shader(self, b_mat, bsshader, b_slot):

        # TODO [shader][animation] Do some form of check to ensure that we actually have data
        effect_control = block_store.create_block("BSEffectShaderPropertyFloatController", bsshader)
        effect_control.flags = b_mat.niftools_alpha.textureflag
        effect_control.frequency = b_slot.texture.image.fps
        effect_control.start_time = b_slot.texture.image.frame_start
        effect_control.stop_time = b_slot.texture.image.frame_end
        bsshader.add_controller(effect_control)
 def export_weapon_location(self, n_root, root_obj):
     # export weapon location
     if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3',
                                                  'SKYRIM'):
         loc = root_obj.niftools.prn_location
         if loc != "NONE":
             # add string extra data
             prn = block_store.create_block("NiStringExtraData")
             prn.name = 'Prn'
             prn.string_data = PRN_DICT[loc]
             n_root.add_extra_data(prn)
Beispiel #15
0
    def export_bhk_rigid_body(self, b_obj, n_col_obj):

        n_r_body = block_store.create_block("bhkRigidBody", b_obj)
        n_col_obj.body = n_r_body
        n_r_body.layer = getattr(NifFormat.OblivionLayer,
                                 b_obj.nifcollision.oblivion_layer)
        n_r_body.col_filter = b_obj.nifcollision.col_filter
        n_r_body.unknown_short = 0
        n_r_body.unknown_int_1 = 0
        n_r_body.unknown_int_2 = 2084020722
        unk_3 = n_r_body.unknown_3_ints
        unk_3[0] = 0
        unk_3[1] = 0
        unk_3[2] = 0
        n_r_body.collision_response = 1
        n_r_body.unknown_byte = 0
        n_r_body.process_contact_callback_delay = 65535
        unk_2 = n_r_body.unknown_2_shorts
        unk_2[0] = 35899
        unk_2[1] = 16336
        n_r_body.layer_copy = n_r_body.layer
        n_r_body.col_filter_copy = n_r_body.col_filter
        # TODO [format] nif.xml update required
        # ukn_6 = n_r_body.unknown_6_shorts
        # ukn_6[0] = 21280
        # ukn_6[1] = 4581
        # ukn_6[2] = 62977
        # ukn_6[3] = 65535
        # ukn_6[4] = 44
        # ukn_6[5] = 0

        b_r_body = b_obj.rigid_body
        # mass is 1.0 at the moment (unless property was set on import or by the user)
        # will be fixed in update_rigid_bodies()
        n_r_body.mass = b_r_body.mass
        n_r_body.linear_damping = b_r_body.linear_damping
        n_r_body.angular_damping = b_r_body.angular_damping
        # n_r_body.linear_velocity = linear_velocity
        # n_r_body.angular_velocity = angular_velocity
        n_r_body.friction = b_r_body.friction
        n_r_body.restitution = b_r_body.restitution
        n_r_body.max_linear_velocity = b_obj.nifcollision.max_linear_velocity
        n_r_body.max_angular_velocity = b_obj.nifcollision.max_angular_velocity
        n_r_body.penetration_depth = b_obj.collision.permeability
        n_r_body.motion_system = b_obj.nifcollision.motion_system
        n_r_body.deactivator_type = b_obj.nifcollision.deactivator_type
        n_r_body.solver_deactivation = b_obj.nifcollision.solver_deactivation
        # TODO [collision][properties][ui] expose unknowns to UI & make sure to keep defaults
        n_r_body.unknown_byte_1 = 1
        n_r_body.unknown_byte_2 = 1
        n_r_body.quality_type = b_obj.nifcollision.quality_type
        n_r_body.unknown_int_9 = 0
        return n_r_body
    def export_bhk_collison_object(self, b_obj):
        layer = int(b_obj.nifcollision.collision_layer)
        col_filter = b_obj.nifcollision.col_filter

        n_col_obj = block_store.create_block("bhkCollisionObject", b_obj)
        if layer == NifFormat.OblivionLayer.OL_ANIM_STATIC and col_filter != 128:
            # animated collision requires flags = 41
            # unless it is a constrainted but not keyframed object
            n_col_obj.flags = 41
        else:
            # in all other cases this seems to be enough
            n_col_obj.flags = 1
        return n_col_obj
Beispiel #17
0
    def export_nicollisiondata(self, b_obj, n_parent):
        """ Export b_obj as a NiCollisionData """
        n_coll_data = block_store.create_block("NiCollisionData", b_obj)
        n_coll_data.use_abv = 1
        n_coll_data.target = n_parent
        n_parent.collision_object = n_coll_data

        n_bv = n_coll_data.bounding_volume
        if b_obj.display_bounds_type == 'SPHERE':
            self.export_spherebv(b_obj, n_bv)
        elif b_obj.display_bounds_type == 'BOX':
            self.export_boxbv(b_obj, n_bv)
        elif b_obj.display_bounds_type == 'CAPSULE':
            self.export_capsulebv(b_obj, n_bv)
Beispiel #18
0
    def export_bsxflags_upb(self, root_block):
        # TODO [object][property] Fixme
        NifLog.info("Checking collision")
        # activate oblivion/Fallout 3 collision and physics
        if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3',
                                                     'SKYRIM'):
            b_obj = self.has_collision()
            if b_obj:
                # enable collision
                bsx = block_store.create_block("BSXFlags")
                bsx.name = 'BSX'
                bsx.integer_data = b_obj.niftools.bsxflags
                root_block.add_extra_data(bsx)

                # many Oblivion nifs have a UPB, but export is disabled as
                # they do not seem to affect anything in the game
                if b_obj.niftools.upb:
                    upb = block_store.create_block("NiStringExtraData")
                    upb.name = 'UPB'
                    if b_obj.niftools.upb == '':
                        upb.string_data = 'Mass = 0.000000\r\nEllasticity = 0.300000\r\nFriction = 0.300000\r\nUnyielding = 0\r\nSimulation_Geometry = 2\r\nProxy_Geometry = <None>\r\nUse_Display_Proxy = 0\r\nDisplay_Children = 1\r\nDisable_Collisions = 0\r\nInactive = 0\r\nDisplay_Proxy = <None>\r\n'
                    else:
                        upb.string_data = b_obj.niftools.upb.encode()
                    root_block.add_extra_data(upb)
def create_ninode(b_obj=None):
    """Essentially a wrapper around create_block() that creates nodes of the right type"""
    # when no b_obj is passed, it means we create a root node
    if not b_obj:
        return block_store.create_block("NiNode")

    # get node type - some are stored as custom property of the b_obj
    try:
        n_node_type = b_obj["type"]
    except KeyError:
        n_node_type = "NiNode"

    # ...others by presence of constraints
    if has_track(b_obj):
        n_node_type = "NiBillboardNode"

    # now create the node
    n_node = block_store.create_block(n_node_type, b_obj)

    # customize the node data, depending on type
    if n_node_type == "NiLODNode":
        export_range_lod_data(n_node, b_obj)

    return n_node
Beispiel #20
0
    def create_skin_inst_data(self, b_obj, n_root_name, bodypartgroups):
        if bpy.context.scene.niftools_scene.game in (
                'FALLOUT_3', 'SKYRIM') and bodypartgroups:
            skininst = block_store.create_block("BSDismemberSkinInstance",
                                                b_obj)
        else:
            skininst = block_store.create_block("NiSkinInstance", b_obj)
        for block in block_store.block_to_obj:
            if isinstance(block, NifFormat.NiNode):
                if block.name.decode() == n_root_name:
                    skininst.skeleton_root = block
                    break
        else:
            raise io_scene_niftools.utils.logging.NifError(
                f"Skeleton root '{n_root_name}' not found.")

        # create skinning data and link it
        skindata = block_store.create_block("NiSkinData", b_obj)
        skininst.data = skindata

        skindata.has_vertex_weights = True
        # fix geometry rest pose: transform relative to skeleton root
        skindata.set_transform(math.get_object_matrix(b_obj).get_inverse())
        return skininst, skindata
 def export_bhk_mopp_bv_tree_shape(self, b_obj, n_col_body):
     n_col_mopp = block_store.create_block("bhkMoppBvTreeShape", b_obj)
     n_col_body.shape = n_col_mopp
     # n_col_mopp.material = n_havok_mat[0]
     unk_8 = n_col_mopp.unknown_8_bytes
     unk_8[0] = 160
     unk_8[1] = 13
     unk_8[2] = 75
     unk_8[3] = 1
     unk_8[4] = 192
     unk_8[5] = 207
     unk_8[6] = 144
     unk_8[7] = 11
     n_col_mopp.unknown_float = 1.0
     return n_col_mopp
Beispiel #22
0
 def export_texture_effect(self, n_block, b_mat):
     # todo [texture] detect effect
     ref_mtex = False
     if ref_mtex:
         # create a new parent block for this shape
         extra_node = block_store.create_block("NiNode", ref_mtex)
         n_block.add_child(extra_node)
         # set default values for this ninode
         extra_node.rotation.set_identity()
         extra_node.scale = 1.0
         extra_node.flags = 0x000C  # morrowind
         # create texture effect block and parent the texture effect and trishape to it
         texeff = self.texture_helper.export_texture_effect(ref_mtex)
         extra_node.add_child(texeff)
         extra_node.add_effect(texeff)
         return extra_node
     return n_block
 def export_bhk_packed_nitristrip_shape(self, b_obj, n_col_mopp):
     # the mopp origin, scale, and data are written later
     n_col_shape = block_store.create_block("bhkPackedNiTriStripsShape",
                                            b_obj)
     n_col_shape.unknown_int_1 = 0
     n_col_shape.unknown_int_2 = 21929432
     n_col_shape.unknown_float_1 = 0.1
     n_col_shape.unknown_int_3 = 0
     n_col_shape.unknown_float_2 = 0
     n_col_shape.unknown_float_3 = 0.1
     scale = n_col_shape.scale
     scale.x = 0
     scale.y = 0
     scale.z = 0
     scale.unknown_float_4 = 0
     n_col_shape.scale_copy = scale
     n_col_mopp.shape = n_col_shape
     return n_col_shape
    def export_flip_controller(self, fliptxt, texture, target, target_tex):
        # TODO [animation] port code to use native Blender image strip system
        #                  despite its name a NiFlipController does not flip / mirror a texture
        #                  instead it swaps through a list of textures for a sprite animation
        #
        # fliptxt is a blender text object containing the n_flip definitions
        # texture is the texture object in blender ( texture is used to checked for pack and mipmap flags )
        # target is the NiTexturingProperty
        # target_tex is the texture to n_flip ( 0 = base texture, 4 = glow texture )
        #
        # returns exported NiFlipController

        tlist = fliptxt.asLines()

        # create a NiFlipController
        n_flip = block_store.create_block("NiFlipController", fliptxt)
        target.add_controller(n_flip)

        # fill in NiFlipController's values
        n_flip.flags = 8  # active
        n_flip.frequency = 1.0
        start = bpy.context.scene.frame_start

        n_flip.start_time = (start - 1) * self.fps
        n_flip.stop_time = (bpy.context.scene.frame_end - start) * self.fps
        n_flip.texture_slot = target_tex

        count = 0
        for t in tlist:
            if len(t) == 0:
                continue  # skip empty lines
            # create a NiSourceTexture for each n_flip
            tex = TextureWriter.export_source_texture(texture, t)
            n_flip.num_sources += 1
            n_flip.sources.update_size()
            n_flip.sources[n_flip.num_sources - 1] = tex
            count += 1
        if count < 2:
            raise io_scene_niftools.utils.logging.NifError(
                f"Error in Texture Flip buffer '{fliptxt.name}': must define at least two textures"
            )
        n_flip.delta = (n_flip.stop_time - n_flip.start_time) / count
Beispiel #25
0
def export_range_lod_data(n_node, b_obj):
    """Export range lod data for for the children of b_obj, as a
    NiRangeLODData block on n_node.
    """
    # create range lod data object
    n_range_data = block_store.create_block("NiRangeLODData", b_obj)
    n_node.lod_level_data = n_range_data

    # get the children
    b_children = b_obj.children

    # set the data
    n_node.num_lod_levels = len(b_children)
    n_range_data.num_lod_levels = len(b_children)
    n_node.lod_levels.update_size()
    n_range_data.lod_levels.update_size()
    for b_child, n_lod_level, n_rd_lod_level in zip(b_children, n_node.lod_levels, n_range_data.lod_levels):
        n_lod_level.near_extent = b_child["near_extent"]
        n_lod_level.far_extent = b_child["far_extent"]
        n_rd_lod_level.near_extent = n_lod_level.near_extent
        n_rd_lod_level.far_extent = n_lod_level.far_extent
    def export_collision_list(self, b_obj, n_col_body, layer, n_havok_mat):
        """Add collision object obj to the list of collision objects of n_col_body.
        If n_col_body has no collisions yet, a new list is created.
        If the current collision system is not a list of collisions
        (bhkListShape), then a ValueError is raised."""

        # if no collisions have been exported yet to this parent_block
        # then create new collision tree on parent_block
        # bhkCollisionObject -> bhkRigidBody -> bhkListShape
        # (this works in all cases, can be simplified just before the file is written)
        if not n_col_body.shape:
            n_col_shape = block_store.create_block("bhkListShape")
            n_col_body.shape = n_col_shape
            # n_col_shape.material = n_havok_mat[0]
        else:
            n_col_shape = n_col_body.shape
            if not isinstance(n_col_shape, NifFormat.bhkListShape):
                raise ValueError('Not a list of collisions')

        n_col_shape.add_shape(
            self.export_collision_object(b_obj, layer, n_havok_mat))
Beispiel #27
0
    def export_bsbound(self, b_obj, block_parent):
        box_extends = self.calculate_box_extents(b_obj)
        n_bbox = block_store.create_block("BSBound")
        # ... the following incurs double scaling because it will be added in
        # both the extra data list and in the old extra data sequence!!!
        # block_parent.add_extra_data(n_bbox)
        # quick hack (better solution would be to make apply_scale non-recursive)
        block_parent.num_extra_data_list += 1
        block_parent.extra_data_list.update_size()
        block_parent.extra_data_list[-1] = n_bbox
        # set name, center, and dimensions
        n_bbox.name = "BBX"
        center = n_bbox.center
        center.x = b_obj.location[0]
        center.y = b_obj.location[1]
        center.z = b_obj.location[2]

        largest = self.calculate_largest_value(box_extends)
        dims = n_bbox.dimensions
        dims.x = largest[0]
        dims.y = largest[1]
        dims.z = largest[2]
Beispiel #28
0
    def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None):
        """
        Export a blender object ob of the type mesh, child of nif block
        n_parent, as NiTriShape and NiTriShapeData blocks, possibly
        along with some NiTexturingProperty, NiSourceTexture,
        NiMaterialProperty, and NiAlphaProperty blocks. We export one
        trishape block per mesh material. We also export vertex weights.

        The parameter trishape_name passes on the name for meshes that
        should be exported as a single mesh.
        """
        NifLog.info(f"Exporting {b_obj}")

        assert (b_obj.type == 'MESH')

        # get mesh from b_obj
        b_mesh = self.get_triangulated_mesh(b_obj)
        b_mesh.calc_normals_split()

        # getVertsFromGroup fails if the mesh has no vertices
        # (this happens when checking for fallout 3 body parts)
        # so quickly catch this (rare!) case
        if not b_mesh.vertices:
            # do not export anything
            NifLog.warn(f"{b_obj} has no vertices, skipped.")
            return

        # get the mesh's materials, this updates the mesh material list
        if not isinstance(n_parent, NifFormat.RootCollisionNode):
            mesh_materials = b_mesh.materials
        else:
            # ignore materials on collision trishapes
            mesh_materials = []

        # if the mesh has no materials, all face material indices should be 0, so it's ok to fake one material in the material list
        if not mesh_materials:
            mesh_materials = [None]

        # vertex color check
        mesh_hasvcol = b_mesh.vertex_colors
        # list of body part (name, index, vertices) in this mesh
        polygon_parts = self.get_polygon_parts(b_obj, b_mesh)
        game = bpy.context.scene.niftools_scene.game

        # Non-textured materials, vertex colors are used to color the mesh
        # Textured materials, they represent lighting details

        # let's now export one trishape for every mesh material
        # TODO [material] needs refactoring - move material, texture, etc. to separate function
        for materialIndex, b_mat in enumerate(mesh_materials):

            mesh_hasnormals = False
            if b_mat is not None:
                mesh_hasnormals = True  # for proper lighting
                if (game == 'SKYRIM'
                    ) and b_mat.niftools_shader.slsf_1_model_space_normals:
                    mesh_hasnormals = False  # for proper lighting

            # create a trishape block
            if not NifOp.props.stripify:
                trishape = block_store.create_block("NiTriShape", b_obj)
            else:
                trishape = block_store.create_block("NiTriStrips", b_obj)

            # fill in the NiTriShape's non-trivial values
            if isinstance(n_parent, NifFormat.RootCollisionNode):
                trishape.name = ""
            else:
                if not trishape_name:
                    if n_parent.name:
                        trishape.name = "Tri " + n_parent.name.decode()
                    else:
                        trishape.name = "Tri " + b_obj.name.decode()
                else:
                    trishape.name = trishape_name

                # multimaterial meshes: add material index (Morrowind's child naming convention)
                if len(mesh_materials) > 1:
                    trishape.name = f"{trishape.name.decode()}: {materialIndex}"
                else:
                    trishape.name = block_store.get_full_name(trishape)

            self.set_mesh_flags(b_obj, trishape)

            # extra shader for Sid Meier's Railroads
            if game == 'SID_MEIER_S_RAILROADS':
                trishape.has_shader = True
                trishape.shader_name = "RRT_NormalMap_Spec_Env_CubeLight"
                trishape.unknown_integer = -1  # default

            # if we have an animation of a blender mesh
            # an intermediate NiNode has been created which holds this b_obj's transform
            # the trishape itself then needs identity transform (default)
            if trishape_name is not None:
                # only export the bind matrix on trishapes that were not animated
                math.set_object_matrix(b_obj, trishape)

            # check if there is a parent
            if n_parent:
                # add texture effect block (must be added as parent of the trishape)
                n_parent = self.export_texture_effect(n_parent, b_mat)
                # refer to this mesh in the parent's children list
                n_parent.add_child(trishape)

            self.object_property.export_properties(b_obj, b_mat, trishape)

            # -> now comes the real export
            '''
                NIF has one uv vertex and one normal per vertex,
                per vert, vertex coloring.

                NIF uses the normal table for lighting.
                Smooth faces should use Blender's vertex normals,
                solid faces should use Blender's face normals.

                Blender's uv vertices and normals per face.
                Blender supports per face vertex coloring,
            '''

            # We now extract vertices, uv-vertices, normals, and
            # vertex colors from the mesh's face list. Some vertices must be duplicated.

            # The following algorithm extracts all unique quads(vert, uv-vert, normal, vcol),
            # produce lists of vertices, uv-vertices, normals, vertex colors, and face indices.

            mesh_uv_layers = b_mesh.uv_layers
            vertquad_list = [
            ]  # (vertex, uv coordinate, normal, vertex color) list
            vertex_map = [None for _ in range(len(b_mesh.vertices))
                          ]  # blender vertex -> nif vertices
            vertex_positions = []
            normals = []
            vertex_colors = []
            uv_coords = []
            triangles = []
            # for each face in triangles, a body part index
            bodypartfacemap = []
            polygons_without_bodypart = []

            if b_mesh.polygons:
                if mesh_uv_layers:
                    # if we have uv coordinates double check that we have uv data
                    if not b_mesh.uv_layer_stencil:
                        NifLog.warn(
                            f"No UV map for texture associated with selected mesh '{b_mesh.name}'."
                        )

            use_tangents = False
            if mesh_uv_layers and mesh_hasnormals:
                if game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM') or (
                        game
                        in self.texture_helper.USED_EXTRA_SHADER_TEXTURES):
                    use_tangents = True
                    b_mesh.calc_tangents(uvmap=mesh_uv_layers[0].name)
                    tangents = []
                    bitangent_signs = []

            for poly in b_mesh.polygons:

                # does the face belong to this trishape?
                if b_mat is not None and poly.material_index != materialIndex:
                    # we have a material but this face has another material, so skip
                    continue

                f_numverts = len(poly.vertices)
                if f_numverts < 3:
                    continue  # ignore degenerate polygons
                assert ((f_numverts == 3) or (f_numverts == 4))  # debug

                # find (vert, uv-vert, normal, vcol) quad, and if not found, create it
                f_index = [-1] * f_numverts
                for i, loop_index in enumerate(poly.loop_indices):

                    fv_index = b_mesh.loops[loop_index].vertex_index
                    vertex = b_mesh.vertices[fv_index]
                    vertex_index = vertex.index
                    fv = vertex.co

                    # smooth = vertex normal, non-smooth = face normal)
                    if mesh_hasnormals:
                        if poly.use_smooth:
                            fn = b_mesh.loops[loop_index].normal
                        else:
                            fn = poly.normal
                    else:
                        fn = None

                    fuv = [
                        uv_layer.data[loop_index].uv
                        for uv_layer in b_mesh.uv_layers
                    ]

                    # TODO [geomotry][mesh] Need to map b_verts -> n_verts
                    if mesh_hasvcol:
                        f_col = list(
                            b_mesh.vertex_colors[0].data[loop_index].color)
                    else:
                        f_col = None

                    vertquad = (fv, fuv, fn, f_col)

                    # check for duplicate vertquad?
                    f_index[i] = len(vertquad_list)
                    if vertex_map[vertex_index] is not None:
                        # iterate only over vertices with the same vertex index
                        for j in vertex_map[vertex_index]:
                            # check if they have the same uvs, normals and colors
                            if self.is_new_face_corner_data(
                                    vertquad, vertquad_list[j]):
                                continue
                            # all tests passed: so yes, we already have a vert with the same face corner data!
                            f_index[i] = j
                            break

                    if f_index[i] > 65535:
                        raise NifError(
                            "Too many vertices. Decimate your mesh and try again."
                        )

                    if f_index[i] == len(vertquad_list):
                        # first: add it to the vertex map
                        if not vertex_map[vertex_index]:
                            vertex_map[vertex_index] = []
                        vertex_map[vertex_index].append(len(vertquad_list))
                        # new (vert, uv-vert, normal, vcol) quad: add it
                        vertquad_list.append(vertquad)

                        # add the vertex
                        vertex_positions.append(vertquad[0])
                        if mesh_hasnormals:
                            normals.append(vertquad[2])
                        if use_tangents:
                            tangents.append(b_mesh.loops[loop_index].tangent)
                            bitangent_signs.append(
                                [b_mesh.loops[loop_index].bitangent_sign])
                        if mesh_hasvcol:
                            vertex_colors.append(vertquad[3])
                        if mesh_uv_layers:
                            uv_coords.append(vertquad[1])

                # now add the (hopefully, convex) face, in triangles
                for i in range(f_numverts - 2):
                    if (b_obj.scale.x + b_obj.scale.y + b_obj.scale.z) > 0:
                        f_indexed = (f_index[0], f_index[1 + i],
                                     f_index[2 + i])
                    else:
                        f_indexed = (f_index[0], f_index[2 + i],
                                     f_index[1 + i])
                    triangles.append(f_indexed)

                    # add body part number
                    if game not in ('FALLOUT_3',
                                    'SKYRIM') or not polygon_parts:
                        # TODO: or not self.EXPORT_FO3_BODYPARTS):
                        bodypartfacemap.append(0)
                    else:
                        # add the polygon's body part
                        part_index = polygon_parts[poly.index]
                        if part_index >= 0:
                            bodypartfacemap.append(part_index)
                        else:
                            # this signals an error
                            polygons_without_bodypart.append(poly)

            # check that there are no missing body part polygons
            if polygons_without_bodypart:
                self.select_unassigned_polygons(b_mesh, b_obj,
                                                polygons_without_bodypart)

            if len(triangles) > 65535:
                raise NifError(
                    "Too many polygons. Decimate your mesh and try again.")
            if len(vertex_positions) == 0:
                continue  # m_4444x: skip 'empty' material indices

            # add NiTriShape's data
            if isinstance(trishape, NifFormat.NiTriShape):
                tridata = block_store.create_block("NiTriShapeData", b_obj)
            else:
                tridata = block_store.create_block("NiTriStripsData", b_obj)
            trishape.data = tridata

            # data
            tridata.num_vertices = len(vertex_positions)
            tridata.has_vertices = True
            tridata.vertices.update_size()
            for i, v in enumerate(tridata.vertices):
                v.x, v.y, v.z = vertex_positions[i]
            tridata.update_center_radius()

            if mesh_hasnormals:
                tridata.has_normals = True
                tridata.normals.update_size()
                for i, v in enumerate(tridata.normals):
                    v.x, v.y, v.z = normals[i]

            if mesh_hasvcol:
                tridata.has_vertex_colors = True
                tridata.vertex_colors.update_size()
                for i, v in enumerate(tridata.vertex_colors):
                    v.r, v.g, v.b, v.a = vertex_colors[i]

            if mesh_uv_layers:
                if game in ('FALLOUT_3', 'SKYRIM'):
                    if len(mesh_uv_layers) > 1:
                        raise NifError(
                            f"{game} does not support multiple UV layers.")
                tridata.num_uv_sets = len(mesh_uv_layers)
                tridata.bs_num_uv_sets = len(mesh_uv_layers)
                tridata.has_uv = True
                tridata.uv_sets.update_size()
                for j, uv_layer in enumerate(mesh_uv_layers):
                    for i, uv in enumerate(tridata.uv_sets[j]):
                        if len(uv_coords[i]) == 0:
                            continue  # skip non-uv textures
                        uv.u = uv_coords[i][j][0]
                        # NIF flips the texture V-coordinate (OpenGL standard)
                        uv.v = 1.0 - uv_coords[i][j][1]  # opengl standard

            # set triangles stitch strips for civ4
            tridata.set_triangles(triangles,
                                  stitchstrips=NifOp.props.stitch_strips)

            # update tangent space (as binary extra data only for Oblivion)
            # for extra shader texture games, only export it if those textures are actually exported
            # (civ4 seems to be consistent with not using tangent space on non shadered nifs)
            if use_tangents:
                if game == 'SKYRIM':
                    tridata.bs_num_uv_sets = tridata.bs_num_uv_sets + 4096
                # calculate the bitangents using the normals, tangent list and bitangent sign
                bitangents = bitangent_signs * np.cross(normals, tangents)
                # B_tan: +d(B_u), B_bit: +d(B_v) and N_tan: +d(N_v), N_bit: +d(N_u)
                # moreover, N_v = 1 - B_v, so d(B_v) = - d(N_v), therefore N_tan = -B_bit and N_bit = B_tan
                self.add_defined_tangents(trishape,
                                          tangents=-bitangents,
                                          bitangents=tangents,
                                          as_extra_data=(game == 'OBLIVION'))

            # todo [mesh/object] use more sophisticated armature finding, also taking armature modifier into account
            # now export the vertex weights, if there are any
            if b_obj.parent and b_obj.parent.type == 'ARMATURE':
                b_obj_armature = b_obj.parent
                vertgroups = {
                    vertex_group.name
                    for vertex_group in b_obj.vertex_groups
                }
                bone_names = set(b_obj_armature.data.bones.keys())
                # the vertgroups that correspond to bone_names are bones that influence the mesh
                boneinfluences = vertgroups & bone_names
                if boneinfluences:  # yes we have skinning!
                    # create new skinning instance block and link it
                    skininst, skindata = self.create_skin_inst_data(
                        b_obj, b_obj_armature, polygon_parts)
                    trishape.skin_instance = skininst

                    # Vertex weights,  find weights and normalization factors
                    vert_list = {}
                    vert_norm = {}
                    unweighted_vertices = []

                    for bone_group in boneinfluences:
                        b_list_weight = []
                        b_vert_group = b_obj.vertex_groups[bone_group]

                        for b_vert in b_mesh.vertices:
                            if len(b_vert.groups
                                   ) == 0:  # check vert has weight_groups
                                unweighted_vertices.append(b_vert.index)
                                continue

                            for g in b_vert.groups:
                                if b_vert_group.name in boneinfluences:
                                    if g.group == b_vert_group.index:
                                        b_list_weight.append(
                                            (b_vert.index, g.weight))
                                        break

                        vert_list[bone_group] = b_list_weight

                        # create normalisation groupings
                        for v in vert_list[bone_group]:
                            if v[0] in vert_norm:
                                vert_norm[v[0]] += v[1]
                            else:
                                vert_norm[v[0]] = v[1]

                    self.select_unweighted_vertices(b_obj, unweighted_vertices)

                    # for each bone, first we get the bone block then we get the vertex weights and then we add it to the NiSkinData
                    # note: allocate memory for faster performance
                    vert_added = [False for _ in range(len(vertex_positions))]
                    for b_bone_name in boneinfluences:
                        # find bone in exported blocks
                        bone_block = self.get_bone_block(
                            b_obj_armature.data.bones[b_bone_name])

                        # find vertex weights
                        vert_weights = {}
                        for v in vert_list[b_bone_name]:
                            # v[0] is the original vertex index
                            # v[1] is the weight

                            # vertex_map[v[0]] is the set of vertices (indices) to which v[0] was mapped
                            # so we simply export the same weight as the original vertex for each new vertex

                            # write the weights
                            # extra check for multi material meshes
                            if vertex_map[v[0]] and vert_norm[v[0]]:
                                for vert_index in vertex_map[v[0]]:
                                    vert_weights[
                                        vert_index] = v[1] / vert_norm[v[0]]
                                    vert_added[vert_index] = True
                        # add bone as influence, but only if there were actually any vertices influenced by the bone
                        if vert_weights:
                            trishape.add_bone(bone_block, vert_weights)

                    # update bind position skinning data
                    # trishape.update_bind_position()
                    # override pyffi trishape.update_bind_position with custom one that is relative to the nif root
                    self.update_bind_position(trishape, n_root, b_obj_armature)

                    # calculate center and radius for each skin bone data block
                    trishape.update_skin_center_radius()

                    if NifData.data.version >= 0x04020100 and NifOp.props.skin_partition:
                        NifLog.info("Creating skin partition")
                        part_order = [
                            getattr(NifFormat.BSDismemberBodyPartType,
                                    face_map.name, None)
                            for face_map in b_obj.face_maps
                        ]
                        part_order = [
                            body_part for body_part in part_order
                            if body_part is not None
                        ]
                        # override pyffi trishape.update_skin_partition with custom one (that allows ordering)
                        trishape.update_skin_partition = update_skin_partition.__get__(
                            trishape)
                        lostweight = trishape.update_skin_partition(
                            maxbonesperpartition=NifOp.props.
                            max_bones_per_partition,
                            maxbonespervertex=NifOp.props.max_bones_per_vertex,
                            stripify=NifOp.props.stripify,
                            stitchstrips=NifOp.props.stitch_strips,
                            padbones=NifOp.props.pad_bones,
                            triangles=triangles,
                            trianglepartmap=bodypartfacemap,
                            maximize_bone_sharing=(game
                                                   in ('FALLOUT_3', 'SKYRIM')),
                            part_sort_order=part_order)

                        # warn on bad config settings
                        if game == 'OBLIVION':
                            if NifOp.props.pad_bones:
                                NifLog.warn(
                                    "Using padbones on Oblivion export. Disable the pad bones option to get higher quality skin partitions."
                                )
                        if game in ('OBLIVION', 'FALLOUT_3'):
                            if NifOp.props.max_bones_per_partition < 18:
                                NifLog.warn(
                                    "Using less than 18 bones per partition on Oblivion/Fallout 3 export."
                                    "Set it to 18 to get higher quality skin partitions."
                                )
                        if game == 'SKYRIM':
                            if NifOp.props.max_bones_per_partition < 24:
                                NifLog.warn(
                                    "Using less than 24 bones per partition on Skyrim export."
                                    "Set it to 24 to get higher quality skin partitions."
                                )
                        if lostweight > NifOp.props.epsilon:
                            NifLog.warn(
                                f"Lost {lostweight:f} in vertex weights while creating a skin partition for Blender object '{b_obj.name}' (nif block '{trishape.name}')"
                            )

                    # clean up
                    del vert_weights
                    del vert_added

            # fix data consistency type
            tridata.consistency_flags = b_obj.niftools.consistency_flags

            # export EGM or NiGeomMorpherController animation
            self.morph_anim.export_morph(b_mesh, trishape, vertex_map)
        return trishape
Beispiel #29
0
    def execute(self):
        """Main export function."""
        if bpy.context.mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT', toggle=False)

        NifLog.info(f"Exporting {NifOp.props.filepath}")

        # extract directory, base name, extension
        directory = os.path.dirname(NifOp.props.filepath)
        filebase, fileext = os.path.splitext(
            os.path.basename(NifOp.props.filepath))

        block_store.block_to_obj = {}  # clear out previous iteration

        try:  # catch export errors

            # protect against null nif versions
            if bpy.context.scene.niftools_scene.game == 'NONE':
                raise NifError(
                    "You have not selected a game. Please select a game and"
                    " nif version in the scene tab.")

            # find all objects that do not have a parent
            self.exportable_objects, self.root_objects = self.objecthelper.get_export_objects(
            )
            if not self.exportable_objects:
                NifLog.warn("No objects can be exported!")
                return {'FINISHED'}

            for b_obj in self.exportable_objects:
                if b_obj.type == 'MESH':
                    if b_obj.parent and b_obj.parent.type == 'ARMATURE':
                        for b_mod in b_obj.modifiers:
                            if b_mod.type == 'ARMATURE' and b_mod.use_bone_envelopes:
                                raise NifError(
                                    f"'{b_obj.name}': Cannot export envelope skinning. If you have vertex groups, turn off envelopes.\n"
                                    f"If you don't have vertex groups, select the bones one by one press W to "
                                    f"convert their envelopes to vertex weights, and turn off envelopes."
                                )

                # check for non-uniform transforms
                scale = b_obj.scale
                if abs(scale.x - scale.y) > NifOp.props.epsilon or abs(
                        scale.y - scale.z) > NifOp.props.epsilon:
                    NifLog.warn(
                        f"Non-uniform scaling not supported.\n"
                        f"Workaround: apply size and rotation (CTRL-A) on '{b_obj.name}'."
                    )

            b_armature = math.get_armature()
            # some scenes may not have an armature, so nothing to do here
            if b_armature:
                math.set_bone_orientation(
                    b_armature.data.niftools.axis_forward,
                    b_armature.data.niftools.axis_up)

            prefix = "x" if bpy.context.scene.niftools_scene.game in (
                'MORROWIND', ) else ""
            NifLog.info("Exporting")
            if NifOp.props.animation == 'ALL_NIF':
                NifLog.info("Exporting geometry and animation")
            elif NifOp.props.animation == 'GEOM_NIF':
                # for morrowind: everything except keyframe controllers
                NifLog.info("Exporting geometry only")

            # find nif version to write

            self.version, data = scene.get_version_data()
            NifData.init(data)

            # export the actual root node (the name is fixed later to avoid confusing the exporter with duplicate names)
            root_block = self.objecthelper.export_root_node(
                self.root_objects, filebase)

            # post-processing:
            # ----------------

            NifLog.info("Checking controllers")
            if bpy.context.scene.niftools_scene.game == 'MORROWIND':
                # animations without keyframe animations crash the TESCS
                # if we are in that situation, add a trivial keyframe animation
                has_keyframecontrollers = False
                for block in block_store.block_to_obj:
                    if isinstance(block, NifFormat.NiKeyframeController):
                        has_keyframecontrollers = True
                        break
                if (not has_keyframecontrollers) and (
                        not NifOp.props.bs_animation_node):
                    NifLog.info("Defining dummy keyframe controller")
                    # add a trivial keyframe controller on the scene root
                    self.transform_anim.create_controller(
                        root_block, root_block.name)

                if NifOp.props.bs_animation_node:
                    for block in block_store.block_to_obj:
                        if isinstance(block, NifFormat.NiNode):
                            # if any of the shape children has a controller or if the ninode has a controller convert its type
                            if block.controller or any(
                                    child.controller
                                    for child in block.children if isinstance(
                                        child, NifFormat.NiGeometry)):
                                new_block = NifFormat.NiBSAnimationNode(
                                ).deepcopy(block)
                                # have to change flags to 42 to make it work
                                new_block.flags = 42
                                root_block.replace_global_node(
                                    block, new_block)
                                if root_block is block:
                                    root_block = new_block

            # oblivion skeleton export: check that all bones have a transform controller and transform interpolator
            if bpy.context.scene.niftools_scene.game in (
                    'OBLIVION', 'FALLOUT_3',
                    'SKYRIM') and filebase.lower() in ('skeleton',
                                                       'skeletonbeast'):
                self.transform_anim.add_dummy_controllers()

            # bhkConvexVerticesShape of children of bhkListShapes need an extra bhkConvexTransformShape (see issue #3308638, reported by Koniption)
            # note: block_store.block_to_obj changes during iteration, so need list copy
            for block in list(block_store.block_to_obj.keys()):
                if isinstance(block, NifFormat.bhkListShape):
                    for i, sub_shape in enumerate(block.sub_shapes):
                        if isinstance(sub_shape,
                                      NifFormat.bhkConvexVerticesShape):
                            coltf = block_store.create_block(
                                "bhkConvexTransformShape")
                            coltf.material = sub_shape.material
                            coltf.unknown_float_1 = 0.1
                            unk_8 = coltf.unknown_8_bytes
                            unk_8[0] = 96
                            unk_8[1] = 120
                            unk_8[2] = 53
                            unk_8[3] = 19
                            unk_8[4] = 24
                            unk_8[5] = 9
                            unk_8[6] = 253
                            unk_8[7] = 4
                            coltf.transform.set_identity()
                            coltf.shape = sub_shape
                            block.sub_shapes[i] = coltf

            # export constraints
            for b_obj in self.exportable_objects:
                if b_obj.constraints:
                    self.constrainthelper.export_constraints(b_obj, root_block)

            object_prop = ObjectProperty()
            object_prop.export_root_node_properties(root_block)

            # FIXME:
            """
            if self.EXPORT_FLATTENSKIN:
                # (warning: trouble if armatures parent other armatures or
                # if bones parent geometries, or if object is animated)
                # flatten skins
                skelroots = set()
                affectedbones = []
                for block in block_store.block_to_obj:
                    if isinstance(block, NifFormat.NiGeometry) and block.is_skin():
                        NifLog.info("Flattening skin on geometry {0}".format(block.name))
                        affectedbones.extend(block.flatten_skin())
                        skelroots.add(block.skin_instance.skeleton_root)
                # remove NiNodes that do not affect skin
                for skelroot in skelroots:
                    NifLog.info("Removing unused NiNodes in '{0}'".format(skelroot.name))
                    skelrootchildren = [child for child in skelroot.children
                                        if ((not isinstance(child,
                                                            NifFormat.NiNode))
                                            or (child in affectedbones))]
                    skelroot.num_children = len(skelrootchildren)
                    skelroot.children.update_size()
                    for i, child in enumerate(skelrootchildren):
                        skelroot.children[i] = child
            """

            # apply scale
            data.roots = [root_block]
            scale_correction = bpy.context.scene.niftools_scene.scale_correction
            if abs(scale_correction) > NifOp.props.epsilon:
                self.apply_scale(data, round(1 / NifOp.props.scale_correction))
                # also scale egm
                if EGMData.data:
                    EGMData.data.apply_scale(1 / scale_correction)

            # generate mopps (must be done after applying scale!)
            if bpy.context.scene.niftools_scene.game in ('OBLIVION',
                                                         'FALLOUT_3',
                                                         'SKYRIM'):
                for block in block_store.block_to_obj:
                    if isinstance(block, NifFormat.bhkMoppBvTreeShape):
                        NifLog.info("Generating mopp...")
                        block.update_mopp()
                        # print "=== DEBUG: MOPP TREE ==="
                        # block.parse_mopp(verbose = True)
                        # print "=== END OF MOPP TREE ==="
                        # warn about mopps on non-static objects
                        if any(sub_shape.layer != 1
                               for sub_shape in block.shape.sub_shapes):
                            NifLog.warn(
                                "Mopps for non-static objects may not function correctly in-game. You may wish to use simple primitives for collision."
                            )

            # export nif file:
            # ----------------
            if bpy.context.scene.niftools_scene.game == 'EMPIRE_EARTH_II':
                ext = ".nifcache"
            else:
                ext = ".nif"
            NifLog.info(f"Writing {ext} file")

            # make sure we have the right file extension
            if fileext.lower() != ext:
                NifLog.warn(
                    f"Changing extension from {fileext} to {ext} on output file"
                )
            niffile = os.path.join(directory, prefix + filebase + ext)

            data.roots = [root_block]
            # todo [export] I believe this is redundant and setting modification only is the current way?
            data.neosteam = (
                bpy.context.scene.niftools_scene.game == 'NEOSTEAM')
            if bpy.context.scene.niftools_scene.game == 'NEOSTEAM':
                data.modification = "neosteam"
            elif bpy.context.scene.niftools_scene.game == 'ATLANTICA':
                data.modification = "ndoors"
            elif bpy.context.scene.niftools_scene.game == 'HOWLING_SWORD':
                data.modification = "jmihs1"

            with open(niffile, "wb") as stream:
                data.write(stream)

            # export egm file:
            # -----------------
            if EGMData.data:
                ext = ".egm"
                NifLog.info(f"Writing {ext} file")

                egmfile = os.path.join(directory, filebase + ext)
                with open(egmfile, "wb") as stream:
                    EGMData.data.write(stream)

            # save exported file (this is used by the test suite)
            self.root_blocks = [root_block]

        except NifError:
            return {'CANCELLED'}

        NifLog.info("Finished")
        return {'FINISHED'}
    def export_constraints(self, b_obj, root_block):
        """Export the constraints of an object.

        @param b_obj: The object whose constraints to export.
        @param root_block: The root of the nif tree (required for update_a_b)."""
        if isinstance(b_obj, bpy.types.Bone):
            # bone object has its constraints stored in the posebone
            # so now we should get the posebone, but no constraints for
            # bones are exported anyway for now
            # so skip this object
            return

        if not hasattr(b_obj, "constraints"):
            # skip text buffers etc
            return

        for b_constr in b_obj.constraints:
            # rigid body joints
            if b_constr.type == 'RIGID_BODY_JOINT':
                if bpy.context.scene.niftools_scene.game not in ('OBLIVION',
                                                                 'FALLOUT_3',
                                                                 'SKYRIM'):
                    NifLog.warn(
                        f"Only Oblivion/Fallout/Skyrim rigid body constraints currently supported: Skipping {b_constr}."
                    )
                    continue
                # check that the object is a rigid body
                for otherbody, otherobj in block_store.block_to_obj.items():
                    if isinstance(
                            otherbody,
                            NifFormat.bhkRigidBody) and otherobj is b_obj:
                        hkbody = otherbody
                        break
                else:
                    # no collision body for this object
                    raise io_scene_niftools.utils.logging.NifError(
                        f"Object {b_obj.name} has a rigid body constraint, but is not exported as collision object"
                    )

                # yes there is a rigid body constraint
                # is it of a type that is supported?
                if b_constr.pivot_type == 'CONE_TWIST':
                    # ball
                    if b_obj.rigid_body.enabled:
                        n_bhkconstraint = block_store.create_block(
                            "bhkRagdollConstraint", b_constr)
                    else:
                        n_bhkconstraint = block_store.create_block(
                            "bhkMalleableConstraint", b_constr)
                        n_bhkconstraint.type = 7
                    n_bhkdescriptor = n_bhkconstraint.ragdoll
                elif b_constr.pivot_type == 'HINGE':
                    # hinge
                    if b_obj.rigid_body.enabled:
                        n_bhkconstraint = block_store.create_block(
                            "bhkLimitedHingeConstraint", b_constr)
                    else:
                        n_bhkconstraint = block_store.create_block(
                            "bhkMalleableConstraint", b_constr)
                        n_bhkconstraint.type = 2
                    n_bhkdescriptor = n_bhkconstraint.limited_hinge
                else:
                    raise io_scene_niftools.utils.logging.NifError(
                        f"Unsupported rigid body joint type ({b_constr.type}), only ball and hinge are supported."
                    )

                # defaults and getting object properties for user settings (should use constraint properties,
                # but blender does not have those...)
                if b_constr.limit_angle_max_x != 0:
                    max_angle = b_constr.limit_angle_max_x
                else:
                    max_angle = 1.5
                if b_constr.limit_angle_min_x != 0:
                    min_angle = b_constr.limit_angle_min_x
                else:
                    min_angle = 0.0

                # friction: again, just picking a reasonable value if no real value given
                if b_obj.niftools_constraint.LHMaxFriction != 0:
                    max_friction = b_obj.niftools_constraint.LHMaxFriction
                else:
                    if isinstance(n_bhkconstraint,
                                  NifFormat.bhkMalleableConstraint):
                        # malleable typically have 0 (perhaps because they have a damping parameter)
                        max_friction = 0
                    else:
                        # non-malleable typically have 10
                        if bpy.context.scene.niftools_scene.game == 'FALLOUT_3':
                            max_friction = 100
                        else:  # oblivion
                            max_friction = 10

                # parent constraint to hkbody
                hkbody.num_constraints += 1
                hkbody.constraints.update_size()
                hkbody.constraints[-1] = n_bhkconstraint

                # export n_bhkconstraint settings
                n_bhkconstraint.num_entities = 2
                n_bhkconstraint.entities.update_size()
                n_bhkconstraint.entities[0] = hkbody
                # is there a target?
                targetobj = b_constr.target
                if not targetobj:
                    NifLog.warn(
                        f"Constraint {b_constr} has no target, skipped")
                    continue
                # find target's bhkRigidBody
                for otherbody, otherobj in block_store.block_to_obj.items():
                    if isinstance(
                            otherbody,
                            NifFormat.bhkRigidBody) and otherobj == targetobj:
                        n_bhkconstraint.entities[1] = otherbody
                        break
                else:
                    # not found
                    raise io_scene_niftools.utils.logging.NifError(
                        f"Rigid body target not exported in nif tree - check that {targetobj} is selected during export."
                    )

                # priority
                n_bhkconstraint.priority = 1
                # extra malleable constraint settings
                if isinstance(n_bhkconstraint,
                              NifFormat.bhkMalleableConstraint):
                    # unknowns
                    n_bhkconstraint.unknown_int_2 = 2
                    n_bhkconstraint.unknown_int_3 = 1
                    # force required to keep bodies together
                    n_bhkconstraint.tau = b_obj.niftools_constraint.tau
                    n_bhkconstraint.damping = b_obj.niftools_constraint.damping

                # calculate pivot point and constraint matrix
                pivot = mathutils.Vector([
                    b_constr.pivot_x, b_constr.pivot_y, b_constr.pivot_z
                ]) / self.HAVOK_SCALE
                constr_matrix = mathutils.Euler(
                    (b_constr.axis_x, b_constr.axis_y, b_constr.axis_z))
                constr_matrix = constr_matrix.to_matrix()

                # transform pivot point and constraint matrix into bhkRigidBody
                # coordinates (also see import_nif.py, the
                # NifImport.import_bhk_constraints method)

                # the pivot point v' is in object coordinates
                # however nif expects it in hkbody coordinates, v
                # v * R * B = v' * O * T * B'
                # with R = rigid body transform (usually unit tf)
                # B = nif bone matrix
                # O = blender object transform
                # T = bone tail matrix (translation in Y direction)
                # B' = blender bone matrix
                # so we need to cancel out the object transformation by
                # v = v' * O * T * B' * B^{-1} * R^{-1}

                # for the rotation matrix, we transform in the same way
                # but ignore all translation parts

                # assume R is unit transform...

                # apply object transform relative to the bone head
                # (this is O * T * B' * B^{-1} at once)
                transform = mathutils.Matrix(b_obj.matrix_local)
                # pivot = pivot * transform
                constr_matrix = constr_matrix * transform.to_3x3()

                # export n_bhkdescriptor pivot point
                # n_bhkdescriptor.pivot_a.x = pivot[0]
                # n_bhkdescriptor.pivot_a.y = pivot[1]
                # n_bhkdescriptor.pivot_a.z = pivot[2]
                # export n_bhkdescriptor axes and other parameters
                # (also see import_nif.py NifImport.import_bhk_constraints)
                axis_x = mathutils.Vector([1, 0, 0]) * constr_matrix
                axis_y = mathutils.Vector([0, 1, 0]) * constr_matrix
                axis_z = mathutils.Vector([0, 0, 1]) * constr_matrix

                if isinstance(n_bhkdescriptor, NifFormat.RagdollDescriptor):
                    # z axis is the twist vector
                    n_bhkdescriptor.twist_a.x = axis_z[0]
                    n_bhkdescriptor.twist_a.y = axis_z[1]
                    n_bhkdescriptor.twist_a.z = axis_z[2]
                    # x axis is the plane vector
                    n_bhkdescriptor.plane_a.x = axis_x[0]
                    n_bhkdescriptor.plane_a.y = axis_x[1]
                    n_bhkdescriptor.plane_a.z = axis_x[2]
                    # angle limits
                    # take them twist and plane to be 45 deg (3.14 / 4 = 0.8)

                    n_bhkdescriptor.plane_min_angle = b_constr.limit_angle_min_x
                    n_bhkdescriptor.plane_max_angle = b_constr.limit_angle_max_x

                    n_bhkdescriptor.cone_max_angle = b_constr.limit_angle_max_y

                    n_bhkdescriptor.twist_min_angle = b_constr.limit_angle_min_z
                    n_bhkdescriptor.twist_max_angle = b_constr.limit_angle_max_z

                    # same for maximum cone angle
                    n_bhkdescriptor.max_friction = max_friction

                elif isinstance(n_bhkdescriptor,
                                NifFormat.LimitedHingeDescriptor):
                    # y axis is the zero angle vector on the plane of rotation
                    n_bhkdescriptor.perp_2_axle_in_a_1.x = axis_y[0]
                    n_bhkdescriptor.perp_2_axle_in_a_1.y = axis_y[1]
                    n_bhkdescriptor.perp_2_axle_in_a_1.z = axis_y[2]
                    # x axis is the axis of rotation
                    n_bhkdescriptor.axle_a.x = axis_x[0]
                    n_bhkdescriptor.axle_a.y = axis_x[1]
                    n_bhkdescriptor.axle_a.z = axis_x[2]
                    # z is the remaining axis determining the positive direction of rotation
                    n_bhkdescriptor.perp_2_axle_in_a_2.x = axis_z[0]
                    n_bhkdescriptor.perp_2_axle_in_a_2.y = axis_z[1]
                    n_bhkdescriptor.perp_2_axle_in_a_2.z = axis_z[2]
                    # angle limits typically, the constraint on one side is defined by the z axis
                    n_bhkdescriptor.min_angle = min_angle
                    # the maximum axis is typically about 90 degrees
                    # 3.14 / 2 = 1.5
                    n_bhkdescriptor.max_angle = max_angle
                    # friction
                    n_bhkdescriptor.max_friction = max_friction
                else:
                    raise ValueError(
                        f"unknown descriptor {n_bhkdescriptor.__class__.__name__}"
                    )

                # do AB
                n_bhkconstraint.update_a_b(root_block)
                n_bhkdescriptor.pivot_b.x = pivot[0]
                n_bhkdescriptor.pivot_b.y = pivot[1]
                n_bhkdescriptor.pivot_b.z = pivot[2]