Beispiel #1
0
    def export_collision(self, b_obj, n_parent):
        """Main function for adding collision object b_obj to a node.
        Returns True if this object is exported as a collision"""

        if b_obj.display_type != "BOUNDS":
            return
        if b_obj.name.lower().startswith('bsbound'):
            # add a bounding box
            self.bs_helper.export_bounds(b_obj, n_parent, bsbound=True)

        elif b_obj.name.lower().startswith("bounding box"):
            # Morrowind bounding box
            self.bs_helper.export_bounds(b_obj, n_parent, bsbound=False)
        if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3',
                                                     'SKYRIM'):

            nodes = [n_parent]
            nodes.extend([
                block for block in n_parent.children
                if block.name[:14] == 'collisiondummy'
            ])
            for node in nodes:
                try:
                    self.bhk_helper.export_collision_helper(b_obj, node)
                    break
                except ValueError:  # adding collision failed
                    continue
            else:
                # all nodes failed so add new one
                node = types.create_ninode(b_obj)
                node.name = 'collisiondummy{:d}'.format(n_parent.num_children)
                if b_obj.niftools.flags != 0:
                    node_flag_hex = hex(b_obj.niftools.flags)
                else:
                    node_flag_hex = 0x000E  # default
                node.flags = node_flag_hex
                n_parent.add_child(node)
                self.bhk_helper.export_collision_helper(b_obj, node)

        elif bpy.context.scene.niftools_scene.game in ('ZOO_TYCOON_2', ):
            self.bound_helper.export_nicollisiondata(b_obj, n_parent)
        else:
            NifLog.warn(
                f"Collisions not supported for game '{bpy.context.scene.niftools_scene.game}', skipped collision object '{b_obj.name}'"
            )

        return True
 def get_b_blend_type_from_n_apply_mode(n_apply_mode):
     # TODO [material] Check out n_apply_modes
     if n_apply_mode == NifFormat.ApplyMode.APPLY_MODULATE:
         return "MIX"
     elif n_apply_mode == NifFormat.ApplyMode.APPLY_REPLACE:
         return "COLOR"
     elif n_apply_mode == NifFormat.ApplyMode.APPLY_DECAL:
         return "OVERLAY"
     elif n_apply_mode == NifFormat.ApplyMode.APPLY_HILIGHT:
         return "LIGHTEN"
     elif n_apply_mode == NifFormat.ApplyMode.APPLY_HILIGHT2:  # used by Oblivion for parallax
         return "MULTIPLY"
     else:
         NifLog.warn(
             f"Unknown apply mode ({n_apply_mode}) in material, using blend type 'MIX'"
         )
         return "MIX"
 def load_image(tex_path):
     """Returns an image or a generated image if none was found"""
     name = os.path.basename(tex_path)
     if name not in bpy.data.images:
         try:
             b_image = bpy.data.images.load(tex_path)
         except:
             NifLog.warn(
                 f"Texture '{name}' not found or not supported and no alternate available"
             )
             b_image = bpy.data.images.new(name=name,
                                           width=1,
                                           height=1,
                                           alpha=True)
     else:
         b_image = bpy.data.images[name]
     return b_image
Beispiel #4
0
    def import_bhk_simple_shape_phantom(self, bhkshape):
        """Imports a bhkSimpleShapePhantom block and applies the transform to the collision object"""

        # import shapes
        collision_objs = self.import_bhk_shape(bhkshape.shape)
        NifLog.warn("Support for bhkSimpleShapePhantom is limited, transform is ignored")
        # todo [pyffi/collision] current nifskope shows a transform, our nif xml doesn't, so ignore it for now
        # # find transformation matrix
        # transform = mathutils.Matrix(bhkshape.transform.as_list())
        #
        # # fix scale
        # transform.translation = transform.translation * self.HAVOK_SCALE
        #
        # # apply transform
        # for b_col_obj in collision_objs:
        #     b_col_obj.matrix_local = b_col_obj.matrix_local @ transform
        # return a list of transformed collision shapes
        return collision_objs
Beispiel #5
0
    def update_bind_position(self, n_geom, n_root, b_obj_armature):
        """Transfer the Blender bind position to the nif bind position.
        Sets the NiSkinData overall transform to the inverse of the geometry transform
        relative to the skeleton root, and sets the NiSkinData of each bone to
        the inverse of the transpose of the bone transform relative to the skeleton root, corrected
        for the overall transform."""
        if not n_geom.is_skin():
            return

        # validate skin and set up quick links
        n_geom._validate_skin()
        skininst = n_geom.skin_instance
        skindata = skininst.data
        skelroot = skininst.skeleton_root

        # calculate overall offset (including the skeleton root transform) and use its inverse
        geomtransform = (n_geom.get_transform(skelroot) *
                         skelroot.get_transform()).get_inverse(fast=False)
        skindata.set_transform(geomtransform)

        # for some nifs, somehow n_root is not set properly?!
        if not n_root:
            NifLog.warn(f"n_root was not set, bug")
            n_root = skelroot

        old_position = b_obj_armature.data.pose_position
        b_obj_armature.data.pose_position = 'POSE'

        # calculate bone offsets
        for i, bone in enumerate(skininst.bones):
            bone_name = block_store.block_to_obj[bone].name
            pose_bone = b_obj_armature.pose.bones[bone_name]
            n_bind = math.mathutils_to_nifformat_matrix(
                math.blender_bind_to_nif_bind(pose_bone.matrix))
            # todo [armature] figure out the correct transform that works universally
            # inverse skin bind in nif armature space, relative to root / geom??
            skindata.bone_list[i].set_transform(
                (n_bind * geomtransform).get_inverse(fast=False))
            # this seems to be correct for skyrim heads, but breaks stuff like ZT2 elephant
            # skindata.bone_list[i].set_transform(bone.get_transform(n_root).get_inverse())

        b_obj_armature.data.pose_position = old_position
Beispiel #6
0
def decompose_srt(b_matrix):
    """Decompose Blender transform matrix as a scale, 4x4 rotation matrix, and translation vector."""

    # get matrix components
    trans_vec, rot_quat, scale_vec = b_matrix.decompose()
    rotmat = rot_quat.to_matrix()

    # todo [armature] negative scale is not generated on armature end
    #                 no need to run costly operations here for now
    # and fix the sign of scale
    # if b_matrix.determinant() < 0:
    #     scale_vec.negate()

    # only uniform scaling allow rather large error to accommodate some nifs
    if abs(scale_vec[0] - scale_vec[1]) + abs(scale_vec[1] -
                                              scale_vec[2]) > 0.02:
        NifLog.warn(
            "Non-uniform scaling not supported. Workaround: apply size and rotation (CTRL-A)."
        )
    return scale_vec[0], rotmat.to_4x4(), trans_vec
Beispiel #7
0
    def export_source_texture(n_texture=None, filename=None):
        """Export a NiSourceTexture.

        :param n_texture: The n_texture object in blender to be exported.
        :param filename: The full or relative path to the n_texture file
            (this argument is used when exporting NiFlipControllers
            and when exporting default shader slots that have no use in
            being imported into Blender).
        :return: The exported NiSourceTexture block.
        """

        # create NiSourceTexture
        srctex = NifFormat.NiSourceTexture()
        srctex.use_external = True
        if filename is not None:
            # preset filename
            srctex.file_name = filename
        elif n_texture is not None:
            srctex.file_name = TextureWriter.export_texture_filename(n_texture)
        else:
            # this probably should not happen
            NifLog.warn(
                "Exporting source texture without texture or filename (bug?).")

        # fill in default values (TODO: can we use 6 for everything?)
        if bpy.context.scene.niftools_scene.nif_version >= 0x0A000100:
            srctex.pixel_layout = 6
        else:
            srctex.pixel_layout = 5
        srctex.use_mipmaps = 1
        srctex.alpha_format = 3
        srctex.unknown_byte = 1

        # search for duplicate
        for block in block_store.block_to_obj:
            if isinstance(block, NifFormat.NiSourceTexture) and block.get_hash(
            ) == srctex.get_hash():
                return block

        # no identical source texture found, so use and register the new one
        return block_store.register_block(srctex, n_texture)
def import_version_info(data):
    scene = bpy.context.scene.niftools_scene
    nif_version = data._version_value_._value
    user_version = data._user_version_value_._value
    user_version_2 = data._user_version_2_value_._value

    # filter possible games by nif version
    possible_games = []
    for game, versions in NifFormat.games.items():
        if game != '?':
            if nif_version in versions:
                game_enum = _game_to_enum(game)
                # go to next game if user version for this game does not match defined
                if game_enum in scene.USER_VERSION:
                    if scene.USER_VERSION[game_enum] != user_version:
                        continue
                # or user version in scene is not 0 when this game has no associated user version
                elif user_version != 0:
                    continue
                # same checks for user version 2
                if game_enum in scene.USER_VERSION_2:
                    if scene.USER_VERSION_2[game_enum] != user_version_2:
                        continue
                elif user_version_2 != 0:
                    continue
                # passed all checks, add to possible games list
                possible_games.append(game_enum)
    if len(possible_games) == 1:
        scene.game = possible_games[0]
    elif len(possible_games) > 1:
        scene.game = possible_games[0]
        # todo[version] - check if this nif's version is marked as default for any of the possible games and use that
        NifLog.warn(
            f"Game set to '{possible_games[0]}', but multiple games qualified")
    scene.nif_version = nif_version
    scene.user_version = user_version
    scene.user_version_2 = user_version_2
Beispiel #9
0
    def import_root(self, root_block):
        """Main import function."""
        # check that this is not a kf file
        if isinstance(
                root_block,
            (NifFormat.NiSequence, NifFormat.NiSequenceStreamHelper)):
            raise io_scene_niftools.utils.logging.NifError(
                "Use the KF import operator to load KF files.")

        # divinity 2: handle CStreamableAssetData
        if isinstance(root_block, NifFormat.CStreamableAssetData):
            root_block = root_block.root

        # sets the root block parent to None, so that when crawling back the script won't barf
        root_block._parent = None

        # set the block parent through the tree, to ensure I can always move backward
        self.set_parents(root_block)

        # mark armature nodes and bones
        self.armaturehelper.mark_armatures_bones(root_block)

        # import the keyframe notes
        # if NifOp.props.animation:
        #     self.animationhelper.import_text_keys(root_block)

        # read the NIF tree
        if isinstance(root_block,
                      (NifFormat.NiNode, NifFormat.NiTriBasedGeom)):
            b_obj = self.import_branch(root_block)
            ObjectProperty().import_extra_datas(root_block, b_obj)

            # now all havok objects are imported, so we are ready to import the havok constraints
            self.constrainthelper.import_bhk_constraints()

            # parent selected meshes to imported skeleton
            if NifOp.props.process == "SKELETON_ONLY":
                # update parenting & armature modifier
                for child in bpy.context.selected_objects:
                    if isinstance(child, bpy.types.Object) and not isinstance(
                            child.data, bpy.types.Armature):
                        child.parent = b_obj
                        for mod in child.modifiers:
                            if mod.type == "ARMATURE":
                                mod.object = b_obj

        elif isinstance(root_block, NifFormat.NiCamera):
            NifLog.warn('Skipped NiCamera root')

        elif isinstance(root_block, NifFormat.NiPhysXProp):
            NifLog.warn('Skipped NiPhysXProp root')

        else:
            NifLog.warn(
                f"Skipped unsupported root block type '{root_block.__class__}' (corrupted nif?)."
            )
    def get_global_uv_transform_clip(self):
        # get the values from the nodes, find the nodes by name, or search back in the node tree
        x_scale = y_scale = x_offset = y_offset = clamp_x = clamp_y = None
        # first check if there are any of the preset name - much more time efficient
        try:
            combine_node = self.b_mat.node_tree.nodes["Combine UV0"]
            if not isinstance(combine_node, bpy.types.ShaderNodeCombineXYZ):
                combine_node = None
                NifLog.warn(
                    f"Found node with name 'Combine UV0', but it was of the wrong type."
                )
        except:
            # if there is a combine node, it does not have the standard name
            combine_node = None
            NifLog.warn(f"Did not find node with 'Combine UV0' name.")

        if combine_node is None:
            # did not find a (correct) combine node, search through the first existing texture node vector input
            b_texture_node = None
            for slot_name, slot_node in self.slots.items():
                if slot_node is not None:
                    break
            if slot_node is not None:
                combine_node = self.get_input_node_of_type(
                    slot_node.inputs[0], bpy.types.ShaderNodeCombineXYZ)
                NifLog.warn(
                    f"Searching through vector input of {slot_name} texture gave {combine_node}"
                )

        if combine_node:
            x_link = combine_node.inputs[0].links
            if x_link:
                x_node = x_link[0].from_node
                x_scale = x_node.inputs[1].default_value
                x_offset = x_node.inputs[2].default_value
                clamp_x = x_node.use_clamp
            y_link = combine_node.inputs[1].links
            if y_link:
                y_node = y_link[0].from_node
                y_scale = y_node.inputs[1].default_value
                y_offset = y_node.inputs[2].default_value
                clamp_y = y_node.use_clamp
        return x_scale, y_scale, x_offset, y_offset, clamp_x, clamp_y
Beispiel #11
0
    def import_root(self, root_block):
        """Main import function."""
        # check that this is not a kf file
        if isinstance(
                root_block,
            (NifFormat.NiSequence, NifFormat.NiSequenceStreamHelper)):
            raise io_scene_niftools.utils.logging.NifError(
                "Use the KF import operator to load KF files.")

        # divinity 2: handle CStreamableAssetData
        if isinstance(root_block, NifFormat.CStreamableAssetData):
            root_block = root_block.root

        # mark armature nodes and bones
        self.armaturehelper.check_for_skin(root_block)

        # read the NIF tree
        if isinstance(root_block,
                      (NifFormat.NiNode, NifFormat.NiTriBasedGeom)):
            b_obj = self.import_branch(root_block)
            ObjectProperty().import_extra_datas(root_block, b_obj)

            # now all havok objects are imported, so we are ready to import the havok constraints
            self.constrainthelper.import_bhk_constraints()

            # parent selected meshes to imported skeleton
            if NifOp.props.process == "SKELETON_ONLY":
                for b_child in self.SELECTED_OBJECTS:
                    self.objecthelper.remove_armature_modifier(b_child)
                    self.objecthelper.append_armature_modifier(b_child, b_obj)

        elif isinstance(root_block, NifFormat.NiCamera):
            NifLog.warn('Skipped NiCamera root')

        elif isinstance(root_block, NifFormat.NiPhysXProp):
            NifLog.warn('Skipped NiPhysXProp root')

        else:
            NifLog.warn(
                f"Skipped unsupported root block type '{root_block.__class__}' (corrupted nif?)."
            )
    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]
    def export_material_property(self, b_mat, flags=0x0001):
        """Return existing material property with given settings, or create
        a new one if a material property with these settings is not found."""
        # don't export material properties for these games
        if bpy.context.scene.niftools_scene.game in ('SKYRIM', ):
            return
        name = block_store.get_full_name(b_mat)
        # create n_block
        n_mat_prop = NifFormat.NiMaterialProperty()

        # list which determines whether the material name is relevant or not  only for particular names this holds,
        # such as EnvMap2 by default, the material name does not affect rendering
        specialnames = ("EnvMap2", "EnvMap", "skin", "Hair", "dynalpha",
                        "HideSecret", "Lava")

        # hack to preserve EnvMap2, skinm, ... named blocks (even if they got renamed to EnvMap2.xxx or skin.xxx on import)
        if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3',
                                                     'SKYRIM'):
            for specialname in specialnames:
                if name.lower() == specialname.lower() or name.lower(
                ).startswith(specialname.lower() + "."):
                    if name != specialname:
                        NifLog.warn(
                            f"Renaming material '{name}' to '{specialname}'")
                    name = specialname

        # clear noname materials
        if name.lower().startswith("noname"):
            NifLog.warn(f"Renaming material '{name}' to ''")
            name = ""

        n_mat_prop.name = name
        # TODO: - standard flag, check? material and texture properties in morrowind style nifs had a flag
        n_mat_prop.flags = flags
        ambient = b_mat.niftools.ambient_color
        n_mat_prop.ambient_color.r = ambient.r
        n_mat_prop.ambient_color.g = ambient.g
        n_mat_prop.ambient_color.b = ambient.b

        # todo [material] some colors in the b2.8 api allow rgb access, others don't - why??
        # diffuse mat
        n_mat_prop.diffuse_color.r, n_mat_prop.diffuse_color.g, n_mat_prop.diffuse_color.b, _ = b_mat.diffuse_color
        n_mat_prop.specular_color.r, n_mat_prop.specular_color.g, n_mat_prop.specular_color.b = b_mat.specular_color

        emissive = b_mat.niftools.emissive_color
        n_mat_prop.emissive_color.r = emissive.r
        n_mat_prop.emissive_color.g = emissive.g
        n_mat_prop.emissive_color.b = emissive.b

        # gloss mat 'Hardness' scrollbar in Blender, takes values between 1 and 511 (MW -> 0.0 - 128.0)
        n_mat_prop.glossiness = b_mat.specular_intensity
        n_mat_prop.alpha = b_mat.niftools.emissive_alpha.v
        # todo [material] this float is used by FO3's material properties
        # n_mat_prop.emit_multi = emitmulti

        # search for duplicate
        # (ignore the name string as sometimes import needs to create different materials even when NiMaterialProperty is the same)
        for n_block in block_store.block_to_obj:
            if not isinstance(n_block, NifFormat.NiMaterialProperty):
                continue

            # when optimization is enabled, ignore material name
            if EXPORT_OPTIMIZE_MATERIALS:
                ignore_strings = not (n_block.name in specialnames)
            else:
                ignore_strings = False

            # check hash
            first_index = 1 if ignore_strings else 0
            if n_block.get_hash()[first_index:] == n_mat_prop.get_hash(
            )[first_index:]:
                NifLog.warn(
                    f"Merging materials '{n_mat_prop.name}' and '{n_block.name}' (they are identical in nif)"
                )
                n_mat_prop = n_block
                break

        block_store.register_block(n_mat_prop)
        # material animation
        self.material_anim.export_material(b_mat, n_mat_prop)
        # no material property with given settings found, so use and register the new one
        return n_mat_prop
    def export_collision_helper(self, b_obj, parent_block):
        """Helper function to add collision objects to a node. This function
        exports the rigid body, and calls the appropriate function to export
        the collision geometry in the desired format.

        @param b_obj: The object to export as collision.
        @param parent_block: The NiNode parent of the collision.
        """

        rigid_body = b_obj.rigid_body
        if not rigid_body:
            NifLog.warn(
                f"'{b_obj.name}' has no rigid body, skipping rigid body export"
            )
            return

        # is it packed
        coll_ispacked = (rigid_body.collision_shape == 'MESH')

        # Set Havok Scale ratio
        b_scene = bpy.context.scene.niftools_scene
        if b_scene.user_version == 12 and b_scene.user_version_2 == 83:
            self.HAVOK_SCALE = consts.HAVOK_SCALE * 10

        # find physics properties/defaults
        # get havok material name from material name
        if b_obj.data.materials:
            n_havok_mat = b_obj.data.materials[0].name
        else:
            n_havok_mat = "HAV_MAT_STONE"

        # linear_velocity = b_obj.rigid_body.deactivate_linear_velocity
        # angular_velocity = b_obj.rigid_body.deactivate_angular_velocity
        layer = int(b_obj.nifcollision.collision_layer)

        # TODO [object][collision][flags] export bsxFlags
        # self.export_bsx_upb_flags(b_obj, parent_block)

        # if no collisions have been exported yet to this parent_block
        # then create new collision tree on parent_block
        # bhkCollisionObject -> bhkRigidBody
        if not parent_block.collision_object:
            # note: collision settings are taken from lowerclasschair01.nif
            if layer == NifFormat.OblivionLayer.OL_BIPED:
                # special collision object for creatures
                n_col_obj = self.export_bhk_blend_collision(b_obj)

                # TODO [collsion][annimation] add detection for this
                self.export_bhk_blend_controller(b_obj, parent_block)
            else:
                # usual collision object
                n_col_obj = self.export_bhk_collison_object(b_obj)

            parent_block.collision_object = n_col_obj
            n_col_obj.target = parent_block

            n_bhkrigidbody = self.export_bhk_rigid_body(b_obj, n_col_obj)

            # we will use n_col_body to attach shapes to below
            n_col_body = n_bhkrigidbody

        else:
            n_col_body = parent_block.collision_object.body
            # fix total mass
            n_col_body.mass += rigid_body.mass

        if coll_ispacked:
            self.export_collision_packed(b_obj, n_col_body, layer, n_havok_mat)
        else:
            if b_obj.nifcollision.export_bhklist:
                self.export_collision_list(b_obj, n_col_body, layer,
                                           n_havok_mat)
            else:
                self.export_collision_single(b_obj, n_col_body, layer,
                                             n_havok_mat)
    def export_collision_object(self, b_obj, layer, n_havok_mat):
        """Export object obj as box, sphere, capsule, or convex hull.
        Note: polyheder is handled by export_collision_packed."""

        # find bounding box data
        if not b_obj.data.vertices:
            NifLog.warn(f"Skipping collision object {b_obj} without vertices.")
            return None

        box_extends = self.calculate_box_extents(b_obj)
        calc_bhkshape_radius = (box_extends[0][1] - box_extends[0][0] +
                                box_extends[1][1] - box_extends[1][0] +
                                box_extends[2][1] -
                                box_extends[2][0]) / (6.0 * self.HAVOK_SCALE)

        b_r_body = b_obj.rigid_body
        if b_r_body.use_margin:
            margin = b_r_body.collision_margin
            if margin - calc_bhkshape_radius > NifOp.props.epsilon:
                radius = calc_bhkshape_radius
            else:
                radius = margin

        collision_shape = b_r_body.collision_shape
        if collision_shape in {'BOX', 'SPHERE'}:
            # note: collision settings are taken from lowerclasschair01.nif
            n_coltf = block_store.create_block("bhkConvexTransformShape",
                                               b_obj)

            # n_coltf.material = n_havok_mat[0]
            n_coltf.unknown_float_1 = 0.1

            unk_8 = n_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

            hktf = math.get_object_bind(b_obj)
            # the translation part must point to the center of the data
            # so calculate the center in local coordinates

            # TODO [collsion] Replace when method moves to bound class, causes circular dependency
            center = mathutils.Vector(
                ((box_extends[0][0] + box_extends[0][1]) / 2.0,
                 (box_extends[1][0] + box_extends[1][1]) / 2.0,
                 (box_extends[2][0] + box_extends[2][1]) / 2.0))

            # and transform it to global coordinates
            center = center @ hktf
            hktf[0][3] = center[0]
            hktf[1][3] = center[1]
            hktf[2][3] = center[2]

            # we need to store the transpose of the matrix
            hktf.transpose()
            n_coltf.transform.set_rows(*hktf)

            # fix matrix for havok coordinate system
            n_coltf.transform.m_14 /= self.HAVOK_SCALE
            n_coltf.transform.m_24 /= self.HAVOK_SCALE
            n_coltf.transform.m_34 /= self.HAVOK_SCALE

            if collision_shape == 'BOX':
                n_colbox = block_store.create_block("bhkBoxShape", b_obj)
                n_coltf.shape = n_colbox
                # n_colbox.material = n_havok_mat[0]
                n_colbox.radius = radius

                unk_8 = n_colbox.unknown_8_bytes
                unk_8[0] = 0x6b
                unk_8[1] = 0xee
                unk_8[2] = 0x43
                unk_8[3] = 0x40
                unk_8[4] = 0x3a
                unk_8[5] = 0xef
                unk_8[6] = 0x8e
                unk_8[7] = 0x3e

                # fix dimensions for havok coordinate system
                box_extends = self.calculate_box_extents(b_obj)
                dims = n_colbox.dimensions
                dims.x = (box_extends[0][1] -
                          box_extends[0][0]) / (2.0 * self.HAVOK_SCALE)
                dims.y = (box_extends[1][1] -
                          box_extends[1][0]) / (2.0 * self.HAVOK_SCALE)
                dims.z = (box_extends[2][1] -
                          box_extends[2][0]) / (2.0 * self.HAVOK_SCALE)
                n_colbox.minimum_size = min(dims.x, dims.y, dims.z)

            elif collision_shape == 'SPHERE':
                n_colsphere = block_store.create_block("bhkSphereShape", b_obj)
                n_coltf.shape = n_colsphere
                # n_colsphere.material = n_havok_mat[0]
                # TODO [object][collision] find out what this is: fix for havok coordinate system (6 * 7 = 42)
                # take average radius
                n_colsphere.radius = radius

            return n_coltf

        elif collision_shape in {'CYLINDER', 'CAPSULE'}:

            length = b_obj.dimensions.z - b_obj.dimensions.x
            radius = b_obj.dimensions.x / 2
            matrix = math.get_object_bind(b_obj)

            length_half = length / 2
            # calculate the direction unit vector
            v_dir = (mathutils.Vector(
                (0, 0, 1)) @ matrix.to_3x3().inverted()).normalized()
            first_point = matrix.translation + v_dir * length_half
            second_point = matrix.translation - v_dir * length_half

            radius /= self.HAVOK_SCALE
            first_point /= self.HAVOK_SCALE
            second_point /= self.HAVOK_SCALE

            n_col_caps = block_store.create_block("bhkCapsuleShape", b_obj)
            # n_col_caps.material = n_havok_mat[0]
            # n_col_caps.skyrim_material = n_havok_mat[1]

            cap_1 = n_col_caps.first_point
            cap_1.x = first_point.x
            cap_1.y = first_point.y
            cap_1.z = first_point.z

            cap_2 = n_col_caps.second_point
            cap_2.x = second_point.x
            cap_2.y = second_point.y
            cap_2.z = second_point.z

            n_col_caps.radius = radius
            n_col_caps.radius_1 = radius
            n_col_caps.radius_2 = radius
            return n_col_caps

        elif collision_shape == 'CONVEX_HULL':
            b_mesh = b_obj.data
            b_transform_mat = math.get_object_bind(b_obj)

            b_rot_quat = b_transform_mat.decompose()[1]
            b_scale_vec = b_transform_mat.decompose()[0]
            '''
            scale = math.avg(b_scale_vec.to_tuple())
            if scale < 0:
                scale = - (-scale) ** (1.0 / 3)
            else:
                scale = scale ** (1.0 / 3)
            rotation /= scale
            '''

            # calculate vertices, normals, and distances
            vertlist = [b_transform_mat @ vert.co for vert in b_mesh.vertices]
            fnormlist = [
                b_rot_quat @ b_face.normal for b_face in b_mesh.polygons
            ]
            fdistlist = [(b_transform_mat @ (-1 * b_mesh.vertices[
                b_mesh.polygons[b_face.index].vertices[0]].co)).dot(
                    b_rot_quat.to_matrix() @ b_face.normal)
                         for b_face in b_mesh.polygons]

            # remove duplicates through dictionary
            vertdict = {}
            for i, vert in enumerate(vertlist):
                vertdict[(int(vert[0] * consts.VERTEX_RESOLUTION),
                          int(vert[1] * consts.VERTEX_RESOLUTION),
                          int(vert[2] * consts.VERTEX_RESOLUTION))] = i

            fdict = {}
            for i, (norm, dist) in enumerate(zip(fnormlist, fdistlist)):
                fdict[(int(norm[0] * consts.NORMAL_RESOLUTION),
                       int(norm[1] * consts.NORMAL_RESOLUTION),
                       int(norm[2] * consts.NORMAL_RESOLUTION),
                       int(dist * consts.VERTEX_RESOLUTION))] = i

            # sort vertices and normals
            vertkeys = sorted(vertdict.keys())
            fkeys = sorted(fdict.keys())
            vertlist = [vertlist[vertdict[hsh]] for hsh in vertkeys]
            fnormlist = [fnormlist[fdict[hsh]] for hsh in fkeys]
            fdistlist = [fdistlist[fdict[hsh]] for hsh in fkeys]

            if len(fnormlist) > 65535 or len(vertlist) > 65535:
                raise io_scene_niftools.utils.logging.NifError(
                    "Mesh has too many polygons/vertices. Simply/split your mesh and try again."
                )

            return self.export_bhk_convex_vertices_shape(
                b_obj, fdistlist, fnormlist, radius, vertlist)

        else:
            raise io_scene_niftools.utils.logging.NifError(
                f'Cannot export collision type {collision_shape} to collision shape list'
            )
 def import_kf_root(self, kf_root, b_armature_obj, bind_data):
     """Base method to warn user that this root type is not supported"""
     NifLog.warn(f"Unknown KF root block found : {kf_root.name:s}")
     NifLog.warn(f"This type isn't currently supported: {type(kf_root)}")
Beispiel #17
0
    def export_node(self, b_obj, n_parent):
        """Export a mesh/armature/empty object b_obj as child of n_parent.
        Export also all children of b_obj.

        :param n_parent:
        :param b_obj:
        """

        if not b_obj:
            return None

        b_action = self.object_anim.get_active_action(b_obj)

        # can we export this b_obj?
        if b_obj.type not in self.export_types:
            return None
        if b_obj.type == 'MESH':
            if self.export_collision(b_obj, n_parent):
                return
            else:
                # -> mesh data.
                is_multimaterial = len(set([f.material_index for f in b_obj.data.polygons])) > 1

                # determine if object tracks camera
                # nb normally, imported models will have tracking constraints on their parent empty
                # but users may create track_to constraints directly on objects, so keep it for now
                has_track = types.has_track(b_obj)

                # If this has children or animations or more than one material it gets wrapped in a purpose made NiNode.
                if not (b_action or b_obj.children or is_multimaterial or has_track):
                    mesh = self.mesh_helper.export_tri_shapes(b_obj, n_parent, self.n_root, b_obj.name)
                    if not self.n_root:
                        self.n_root = mesh
                    return mesh

                # set transform on trishapes rather than NiNodes for skinned meshes to fix an issue with clothing slots
                if b_obj.parent and b_obj.parent.type == 'ARMATURE' and b_action:
                    # mesh with armature parent should not have animation!
                    NifLog.warn(f"Mesh {b_obj.name} is skinned but also has object animation. "
                                f"The nif format does not support this, ignoring object animation.")
                    b_action = False

        # -> everything else (empty/armature) is a (more or less regular) node
        node = types.create_ninode(b_obj)
        # set parenting here so that it can be accessed
        if not self.n_root:
            self.n_root = node

        # make it child of its parent in the nif, if it has one
        if n_parent:
            n_parent.add_child(node)

        # and fill in this node's non-trivial values
        node.name = block_store.get_full_name(b_obj)
        self.set_node_flags(b_obj, node)
        math.set_object_matrix(b_obj, node)

        # export object animation
        self.transform_anim.export_transforms(node, b_obj, b_action)
        self.object_anim.export_visibility(node, b_action)
        # if it is a mesh, export the mesh as trishape children of this ninode
        if b_obj.type == 'MESH':
            return self.mesh_helper.export_tri_shapes(b_obj, node, self.n_root)
        # if it is an armature, export the bones as ninode children of this ninode
        elif b_obj.type == 'ARMATURE':
            self.armaturehelper.export_bones(b_obj, node)

        # export all children of this b_obj as children of this NiNode
        self.export_children(b_obj, node)

        return node
Beispiel #18
0
    def import_branch(self, n_block, b_armature=None, n_armature=None):
        """Read the content of the current NIF tree branch to Blender recursively.

        :param n_block: The nif block to import.
        :param b_armature: The blender armature for the current branch.
        :param n_armature: The corresponding nif block for the armature for  the current branch.
        """
        if not n_block:
            return None

        NifLog.info(f"Importing data for block '{n_block.name.decode()}'")
        if isinstance(n_block, NifFormat.NiTriBasedGeom
                      ) and NifOp.props.process != "SKELETON_ONLY":
            return self.objecthelper.import_geometry_object(
                b_armature, n_block)

        elif isinstance(n_block, NifFormat.NiNode):
            # import object
            if self.armaturehelper.is_armature_root(n_block):
                # all bones in the tree are also imported by import_armature
                if NifOp.props.process != "GEOMETRY_ONLY":
                    b_obj = self.armaturehelper.import_armature(n_block)
                else:
                    n_name = block_store.import_name(n_block)
                    b_obj = math.get_armature()
                    NifLog.info(
                        f"Merging nif tree '{n_name}' with armature '{b_obj.name}'"
                    )
                    if n_name != b_obj.name:
                        NifLog.warn(
                            f"Using Nif block '{n_name}' as armature '{b_obj.name}' but names do not match"
                        )
                b_armature = b_obj
                n_armature = n_block

            elif self.armaturehelper.is_bone(n_block):
                # bones have already been imported during import_armature
                n_name = block_store.import_name(n_block)
                if n_name in b_armature.data.bones:
                    b_obj = b_armature.data.bones[n_name]
                else:
                    # this is a fallback for a weird bug, when a node is child of a NiLodNode in a skeletal nif
                    b_obj = self.objecthelper.create_b_obj(n_block,
                                                           None,
                                                           name=n_name)
                b_obj.niftools.flags = n_block.flags

            else:
                # import as an empty
                b_obj = NiTypes.import_empty(n_block)

            # find children
            b_children = []
            n_children = [child for child in n_block.children]
            for n_child in n_children:
                b_child = self.import_branch(n_child,
                                             b_armature=b_armature,
                                             n_armature=n_armature)
                if b_child and isinstance(b_child, bpy.types.Object):
                    b_children.append(b_child)

            # import collision objects & bounding box
            if NifOp.props.process != "SKELETON_ONLY":
                b_children.extend(self.import_collision(n_block))
                b_children.extend(
                    self.boundhelper.import_bounding_box(n_block))

            # set bind pose for children
            self.objecthelper.set_object_bind(b_obj, b_children, b_armature)

            # import extra node data, such as node type
            NiTypes.import_root_collision(n_block, b_obj)
            NiTypes.import_billboard(n_block, b_obj)
            NiTypes.import_range_lod_data(n_block, b_obj, b_children)

            # set object transform, this must be done after all children objects have been parented to b_obj
            if isinstance(b_obj, bpy.types.Object):
                # note: bones and this object's children already have their matrix set
                b_obj.matrix_local = math.import_matrix(n_block)

                # import object level animations (non-skeletal)
                if NifOp.props.animation:
                    # self.animationhelper.import_text_keys(n_block)
                    self.transform_anim.import_transforms(n_block, b_obj)
                    self.object_anim.import_visibility(n_block, b_obj)

            return b_obj

        # all else is currently discarded
        return None
    def import_constraint(self, hkbody):
        """Imports a bone havok constraint as Blender object constraint."""
        assert (isinstance(hkbody, NifFormat.bhkRigidBody))

        # check for constraints
        if not hkbody.constraints:
            return

        # find objects
        if len(collision.DICT_HAVOK_OBJECTS[hkbody]) != 1:
            NifLog.warn(
                "Rigid body with no or multiple shapes, constraints skipped")
            return

        b_hkobj = collision.DICT_HAVOK_OBJECTS[hkbody][0]

        NifLog.info(f"Importing constraints for b_hkobj.name")

        # now import all constraints
        for hkconstraint in hkbody.constraints:

            # check constraint entities
            if not hkconstraint.num_entities == 2:
                NifLog.warn("Constraint with more than 2 entities, skipped")
                continue
            if not hkconstraint.entities[0] is hkbody:
                NifLog.warn("First constraint entity not self, skipped")
                continue
            if not hkconstraint.entities[1] in collision.DICT_HAVOK_OBJECTS:
                NifLog.warn("Second constraint entity not imported, skipped")
                continue

            # get constraint descriptor
            if isinstance(hkconstraint, NifFormat.bhkRagdollConstraint):
                hkdescriptor = hkconstraint.ragdoll
                b_hkobj.rigid_body.enabled = True
            elif isinstance(hkconstraint, NifFormat.bhkLimitedHingeConstraint):
                hkdescriptor = hkconstraint.limited_hinge
                b_hkobj.rigid_body.enabled = True
            elif isinstance(hkconstraint, NifFormat.bhkHingeConstraint):
                hkdescriptor = hkconstraint.hinge
                b_hkobj.rigid_body.enabled = True
            elif isinstance(hkconstraint, NifFormat.bhkMalleableConstraint):
                if hkconstraint.type == 7:
                    hkdescriptor = hkconstraint.ragdoll
                    b_hkobj.rigid_body.enabled = False
                elif hkconstraint.type == 2:
                    hkdescriptor = hkconstraint.limited_hinge
                    b_hkobj.rigid_body.enabled = False
                else:
                    NifLog.warn(
                        f"Unknown malleable type ({hkconstraint.type:s}), skipped"
                    )
                # TODO [constraint][flag] Damping parameters not yet in Blender Python API
                # TODO [constraint][flag] tau (force between bodies) not supported by Blender
            else:
                NifLog.warn(
                    f"Unknown constraint type ({hkconstraint.__class__.__name__}), skipped"
                )
                continue

            # todo [constraints] the following is no longer possible, fixme
            return

            # add the constraint as a rigid body joint
            b_constr = b_hkobj.constraints.new('RIGID_BODY_JOINT')
            b_constr.name = b_hkobj.name
            b_constr.show_pivot = True

            # note: rigidbodyjoint parameters (from Constraint.c)
            # CONSTR_RB_AXX 0.0
            # CONSTR_RB_AXY 0.0
            # CONSTR_RB_AXZ 0.0
            # CONSTR_RB_EXTRAFZ 0.0
            # CONSTR_RB_MAXLIMIT0 0.0
            # CONSTR_RB_MAXLIMIT1 0.0
            # CONSTR_RB_MAXLIMIT2 0.0
            # CONSTR_RB_MAXLIMIT3 0.0
            # CONSTR_RB_MAXLIMIT4 0.0
            # CONSTR_RB_MAXLIMIT5 0.0
            # CONSTR_RB_MINLIMIT0 0.0
            # CONSTR_RB_MINLIMIT1 0.0
            # CONSTR_RB_MINLIMIT2 0.0
            # CONSTR_RB_MINLIMIT3 0.0
            # CONSTR_RB_MINLIMIT4 0.0
            # CONSTR_RB_MINLIMIT5 0.0
            # CONSTR_RB_PIVX 0.0
            # CONSTR_RB_PIVY 0.0
            # CONSTR_RB_PIVZ 0.0
            # CONSTR_RB_TYPE 12
            # LIMIT 63
            # PARSIZEY 63
            # TARGET [Object "capsule.002"]

            # limit 3, 4, 5 correspond to angular limits along x, y and z
            # and are measured in degrees

            # pivx/y/z is the pivot point

            # set constraint target
            b_constr.target = collision.DICT_HAVOK_OBJECTS[
                hkconstraint.entities[1]][0]
            # set rigid body type (generic)
            b_constr.pivot_type = 'GENERIC_6_DOF'
            # limiting parameters (limit everything)
            b_constr.use_angular_limit_x = True
            b_constr.use_angular_limit_y = True
            b_constr.use_angular_limit_z = True

            # get pivot point
            pivot = mathutils.Vector(
                (hkdescriptor.pivot_b.x, hkdescriptor.pivot_b.y,
                 hkdescriptor.pivot_b.z)) * self.HAVOK_SCALE

            # get z- and x-axes of the constraint
            # (also see export_nif.py NifImport.export_constraints)
            if isinstance(hkdescriptor, NifFormat.RagdollDescriptor):
                b_constr.pivot_type = 'CONE_TWIST'
                # for ragdoll, take z to be the twist axis (central axis of the
                # cone, that is)
                axis_z = mathutils.Vector(
                    (hkdescriptor.twist_a.x, hkdescriptor.twist_a.y,
                     hkdescriptor.twist_a.z))
                # for ragdoll, let x be the plane vector
                axis_x = mathutils.Vector(
                    (hkdescriptor.plane_a.x, hkdescriptor.plane_a.y,
                     hkdescriptor.plane_a.z))
                # set the angle limits
                # (see http://niftools.sourceforge.net/wiki/Oblivion/Bhk_Objects/Ragdoll_Constraint
                # for a nice picture explaining this)
                b_constr.limit_angle_min_x = hkdescriptor.plane_min_angle
                b_constr.limit_angle_max_x = hkdescriptor.plane_max_angle

                b_constr.limit_angle_min_y = -hkdescriptor.cone_max_angle
                b_constr.limit_angle_max_y = hkdescriptor.cone_max_angle

                b_constr.limit_angle_min_z = hkdescriptor.twist_min_angle
                b_constr.limit_angle_max_z = hkdescriptor.twist_max_angle

                b_hkobj.niftools_constraint.LHMaxFriction = hkdescriptor.max_friction

            elif isinstance(hkdescriptor, NifFormat.LimitedHingeDescriptor):
                # for hinge, y is the vector on the plane of rotation defining
                # the zero angle
                axis_y = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_1.x,
                                           hkdescriptor.perp_2_axle_in_a_1.y,
                                           hkdescriptor.perp_2_axle_in_a_1.z))
                # for hinge, take x to be the the axis of rotation
                # (this corresponds with Blender's convention for hinges)
                axis_x = mathutils.Vector(
                    (hkdescriptor.axle_a.x, hkdescriptor.axle_a.y,
                     hkdescriptor.axle_a.z))
                # for hinge, z is the vector on the plane of rotation defining
                # the positive direction of rotation
                axis_z = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_2.x,
                                           hkdescriptor.perp_2_axle_in_a_2.y,
                                           hkdescriptor.perp_2_axle_in_a_2.z))
                # they should form a orthogonal basis
                if (mathutils.Vector.cross(axis_x, axis_y) -
                        axis_z).length > 0.01:
                    # either not orthogonal, or negative orientation
                    if (mathutils.Vector.cross(-axis_x, axis_y) -
                            axis_z).length > 0.01:
                        NifLog.warn(
                            f"Axes are not orthogonal in {hkdescriptor.__class__.__name__}; Arbitrary orientation has been chosen"
                        )
                        axis_z = mathutils.Vector.cross(axis_x, axis_y)
                    else:
                        # fix orientation
                        NifLog.warn(
                            f"X axis flipped in {hkdescriptor.__class__.__name__} to fix orientation"
                        )
                        axis_x = -axis_x
                # getting properties with no blender constraint equivalent and setting as obj properties
                b_constr.limit_angle_max_x = hkdescriptor.max_angle
                b_constr.limit_angle_min_x = hkdescriptor.min_angle
                b_hkobj.niftools_constraint.LHMaxFriction = hkdescriptor.max_friction

                if hasattr(hkconstraint, "tau"):
                    b_hkobj.niftools_constraint.tau = hkconstraint.tau
                    b_hkobj.niftools_constraint.damping = hkconstraint.damping

            elif isinstance(hkdescriptor, NifFormat.HingeDescriptor):
                # for hinge, y is the vector on the plane of rotation defining
                # the zero angle
                axis_y = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_1.x,
                                           hkdescriptor.perp_2_axle_in_a_1.y,
                                           hkdescriptor.perp_2_axle_in_a_1.z))
                # for hinge, z is the vector on the plane of rotation defining
                # the positive direction of rotation
                axis_z = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_2.x,
                                           hkdescriptor.perp_2_axle_in_a_2.y,
                                           hkdescriptor.perp_2_axle_in_a_2.z))
                # take x to be the the axis of rotation
                # (this corresponds with Blender's convention for hinges)
                axis_x = mathutils.Vector.cross(axis_y, axis_z)
                b_hkobj.niftools_constraint.LHMaxFriction = hkdescriptor.max_friction
            else:
                raise ValueError("Unknown descriptor {0}".format(
                    hkdescriptor.__class__.__name__))

            # transform pivot point and constraint matrix into object
            # coordinates
            # (also see export_nif.py NifImport.export_constraints)

            # the pivot point v is in hkbody coordinates
            # however blender expects it in object 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 * R * B * B'^{-1} * T^{-1} * O^{-1}

            # the local rotation L at the pivot point must be such that
            # (axis_z + v) * R * B = ([0 0 1] * L + v') * O * T * B'
            # so (taking the rotation parts of all matrices!!!)
            # [0 0 1] * L = axis_z * R * B * B'^{-1} * T^{-1} * O^{-1}
            # and similarly
            # [1 0 0] * L = axis_x * R * B * B'^{-1} * T^{-1} * O^{-1}
            # hence these give us the first and last row of L
            # which is exactly enough to provide the euler angles

            # multiply with rigid body transform
            if isinstance(hkbody, NifFormat.bhkRigidBodyT):
                # set rotation
                transform = mathutils.Quaternion(
                    (hkbody.rotation.w, hkbody.rotation.x, hkbody.rotation.y,
                     hkbody.rotation.z)).to_matrix()
                transform.resize_4x4()
                # set translation
                transform[0][3] = hkbody.translation.x * self.HAVOK_SCALE
                transform[1][3] = hkbody.translation.y * self.HAVOK_SCALE
                transform[2][3] = hkbody.translation.z * self.HAVOK_SCALE
                # apply transform
                # pivot = pivot * transform
                transform = transform.to_3x3()
                axis_z = axis_z * transform
                axis_x = axis_x * transform

            # TODO [armature] update this to use the new bone system
            # next, cancel out bone matrix correction
            # note that B' = X * B with X = self.nif_import.dict_bones_extra_matrix[B]
            # so multiply with the inverse of X
            # for niBone in self.nif_import.dict_bones_extra_matrix:
            # if niBone.collision_object \
            # and niBone.collision_object.body is hkbody:
            # transform = mathutils.Matrix(
            # self.nif_import.dict_bones_extra_matrix[niBone])
            # transform.invert()
            # pivot = pivot * transform
            # transform = transform.to_3x3()
            # axis_z = axis_z * transform
            # axis_x = axis_x * transform
            # break

            # # cancel out bone tail translation
            # if b_hkobj.parent_bone:
            #     pivot[1] -= b_hkobj.parent.data.bones[
            #         b_hkobj.parent_bone].length

            # cancel out object transform
            transform = mathutils.Matrix(b_hkobj.matrix_local)
            transform.invert()
            # pivot = pivot * transform
            transform = transform.to_3x3()
            axis_z = axis_z * transform
            axis_x = axis_x * transform

            # set pivot point
            b_constr.pivot_x = pivot[0]
            b_constr.pivot_y = pivot[1]
            b_constr.pivot_z = pivot[2]

            # set euler angles
            constr_matrix = mathutils.Matrix(
                (axis_x, mathutils.Vector.cross(axis_z, axis_x), axis_z))
            constr_euler = constr_matrix.to_euler()
            b_constr.axis_x = constr_euler.x
            b_constr.axis_y = constr_euler.y
            b_constr.axis_z = constr_euler.z
            # DEBUG
            assert ((axis_x - mathutils.Vector(
                (1, 0, 0)) * constr_matrix).length < 0.0001)
            assert ((axis_z - mathutils.Vector(
                (0, 0, 1)) * constr_matrix).length < 0.0001)

            # the generic rigid body type is very buggy... so for simulation purposes let's transform it into ball and hinge
            if isinstance(hkdescriptor, NifFormat.RagdollDescriptor):
                # cone_twist
                b_constr.pivot_type = 'CONE_TWIST'
            elif isinstance(
                    hkdescriptor,
                (NifFormat.LimitedHingeDescriptor, NifFormat.HingeDescriptor)):
                # (limited) hinge
                b_constr.pivot_type = 'HINGE'
            else:
                raise ValueError("Unknown descriptor {0}".format(
                    hkdescriptor.__class__.__name__))
Beispiel #20
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'}
Beispiel #21
0
 def process_bhk(self, bhk_shape):
     """Base method to warn user that this property is not supported"""
     NifLog.warn(f"Unsupported bhk shape {bhk_shape.__class__.__name__}")
     NifLog.warn(f"This type isn't currently supported: {type(bhk_shape)}")
     return []
Beispiel #22
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 #23
0
 def process_property(self, prop):
     """Base method to warn user that this property is not supported"""
     NifLog.warn(f"Unknown property block found : {prop.name:s}")
     NifLog.warn(f"This type isn't currently supported: {type(prop)}")