1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2024-11-30 17:34:28 +01:00

Bone group filtering appears to work correctly now

This commit is contained in:
Colin Basnett 2022-01-18 13:21:38 -08:00
parent 78837863e2
commit 7fd0c6de81
7 changed files with 226 additions and 113 deletions

View File

@ -46,6 +46,7 @@ classes = [
psa_importer.PsaImportOperator,
psa_importer.PsaImportFileSelectOperator,
psa_exporter.PSA_UL_ExportActionList,
psa_exporter.PSA_UL_ExportBoneGroupList,
psa_importer.PSA_UL_ImportActionList,
psa_importer.PsaImportActionListItem,
psa_importer.PsaImportSelectAll,
@ -56,6 +57,7 @@ classes = [
psa_exporter.PsaExportSelectAll,
psa_exporter.PsaExportDeselectAll,
psa_exporter.PsaExportActionListItem,
psa_exporter.PsaExportBoneGroupListItem,
psa_exporter.PsaExportPropertyGroup,
]

View File

@ -4,6 +4,8 @@ from .data import *
class PsaBuilderOptions(object):
def __init__(self):
self.actions = []
self.bone_filter_mode = 'NONE'
self.bone_group_indices = []
# https://git.cth451.me/cth451/blender-addons/blob/master/io_export_unreal_psk_psa.py
@ -12,7 +14,7 @@ class PsaBuilder(object):
# TODO: add options in here (selected anims, eg.)
pass
def build(self, context, options) -> Psa:
def build(self, context, options: PsaBuilderOptions) -> Psa:
object = context.view_layer.objects.active
if object.type != 'ARMATURE':
@ -35,28 +37,59 @@ class PsaBuilder(object):
pose_bones.sort(key=lambda x: x[0])
pose_bones = [x[1] for x in pose_bones]
for bone in bones:
bone_indices = list(range(len(bones)))
if options.bone_filter_mode == 'BONE_GROUPS':
# Get a list of the bone indices that are explicitly part of the bone groups we are including.
bone_index_stack = []
for bone_index, pose_bone in enumerate(pose_bones):
if pose_bone.bone_group_index in options.bone_group_indices:
bone_index_stack.append(bone_index)
# For each bone that is explicitly being added, recursively walk up the hierarchy and ensure that all of
# those bone indices are also in the list.
bone_indices = set()
while len(bone_index_stack) > 0:
bone_index = bone_index_stack.pop()
bone = bones[bone_index]
if bone.parent is not None:
parent_bone_index = bone_names.index(bone.parent.name)
if parent_bone_index not in bone_indices:
bone_index_stack.append(parent_bone_index)
bone_indices.add(bone_index)
del bone_names
# Sort out list of bone indices to be exported.
bone_indices = sorted(list(bone_indices))
# The bone lists now contains only the bones that are going to be exported.
bones = [bones[bone_index] for bone_index in bone_indices]
pose_bones = [pose_bones[bone_index] for bone_index in bone_indices]
for pose_bone in bones:
psa_bone = Psa.Bone()
psa_bone.name = bytes(bone.name, encoding='utf-8')
psa_bone.children_count = len(bone.children)
psa_bone.name = bytes(pose_bone.name, encoding='utf-8')
try:
psa_bone.parent_index = bones.index(bone.parent)
parent_index = bones.index(pose_bone.parent)
psa_bone.parent_index = parent_index
psa.bones[parent_index].children_count += 1
except ValueError:
psa_bone.parent_index = -1
if bone.parent is not None:
rotation = bone.matrix.to_quaternion()
if pose_bone.parent is not None:
rotation = pose_bone.matrix.to_quaternion()
rotation.x = -rotation.x
rotation.y = -rotation.y
rotation.z = -rotation.z
quat_parent = bone.parent.matrix.to_quaternion().inverted()
parent_head = quat_parent @ bone.parent.head
parent_tail = quat_parent @ bone.parent.tail
location = (parent_tail - parent_head) + bone.head
quat_parent = pose_bone.parent.matrix.to_quaternion().inverted()
parent_head = quat_parent @ pose_bone.parent.head
parent_tail = quat_parent @ pose_bone.parent.tail
location = (parent_tail - parent_head) + pose_bone.head
else:
location = armature.matrix_local @ bone.head
rot_matrix = bone.matrix @ armature.matrix_local.to_3x3()
location = armature.matrix_local @ pose_bone.head
rot_matrix = pose_bone.matrix @ armature.matrix_local.to_3x3()
rotation = rot_matrix.to_quaternion()
psa_bone.location.x = location.x
@ -92,18 +125,18 @@ class PsaBuilder(object):
for frame in range(frame_count):
context.scene.frame_set(frame_min + frame)
for bone in pose_bones:
for pose_bone in pose_bones:
key = Psa.Key()
pose_bone_matrix = bone.matrix
pose_bone_matrix = pose_bone.matrix
if bone.parent is not None:
pose_bone_parent_matrix = bone.parent.matrix
if pose_bone.parent is not None:
pose_bone_parent_matrix = pose_bone.parent.matrix
pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix
location = pose_bone_matrix.to_translation()
rotation = pose_bone_matrix.to_quaternion().normalized()
if bone.parent is not None:
if pose_bone.parent is not None:
rotation.x = -rotation.x
rotation.y = -rotation.y
rotation.z = -rotation.z

View File

@ -1,6 +1,6 @@
import bpy
from bpy.types import Operator, PropertyGroup, Action, UIList
from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty
from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup
from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty, EnumProperty
from bpy_extras.io_utils import ExportHelper
from typing import Type
from .builder import PsaBuilder, PsaBuilderOptions
@ -43,11 +43,28 @@ class PsaExportActionListItem(PropertyGroup):
return self.action.name
class PsaExportPropertyGroup(bpy.types.PropertyGroup):
class PsaExportBoneGroupListItem(PropertyGroup):
name: StringProperty()
index: IntProperty()
is_selected: BoolProperty(default=False)
@property
def name(self):
return self.bone_group.name
class PsaExportPropertyGroup(PropertyGroup):
action_list: CollectionProperty(type=PsaExportActionListItem)
import_action_list: CollectionProperty(type=PsaExportActionListItem)
action_list_index: IntProperty(name='index for list??', default=0)
import_action_list_index: IntProperty(name='index for list??', default=0)
action_list_index: IntProperty(default=0)
bone_filter_mode: EnumProperty(
name='Bone Filter',
items={
('NONE', 'None', 'All bones will be exported.'),
('BONE_GROUPS', 'Bone Groups', 'Only bones belonging to the selected bone groups will be exported.'),
}
)
bone_group_list: CollectionProperty(type=PsaExportBoneGroupListItem)
bone_group_list_index: IntProperty(default=0)
class PsaExportOperator(Operator, ExportHelper):
@ -68,14 +85,29 @@ class PsaExportOperator(Operator, ExportHelper):
def draw(self, context):
layout = self.layout
scene = context.scene
box = layout.box()
box.label(text='Actions', icon='ACTION')
row = box.row()
row.template_list('PSA_UL_ExportActionList', 'asd', scene.psa_export, 'action_list', scene.psa_export, 'action_list_index', rows=10)
row = box.row()
row.operator('psa_export.actions_select_all', text='Select All')
row.operator('psa_export.actions_deselect_all', text='Deselect All')
box = layout.box()
box.label(text='Bone Filter', icon='FILTER')
row = box.row()
row.alignment = 'EXPAND'
row.prop(scene.psa_export, 'bone_filter_mode', expand=True, text='Bone Filter')
if scene.psa_export.bone_filter_mode == 'BONE_GROUPS':
row = box.row()
rows = max(3, min(len(scene.psa_export.bone_group_list), 10))
row.template_list('PSA_UL_ExportBoneGroupList', 'asd', scene.psa_export, 'bone_group_list', scene.psa_export, 'bone_group_list_index', rows=rows)
def is_action_for_armature(self, action):
if len(action.fcurves) == 0:
return False
@ -90,12 +122,17 @@ class PsaExportOperator(Operator, ExportHelper):
return False
def invoke(self, context, event):
if context.view_layer.objects.active is None:
self.report({'ERROR_INVALID_CONTEXT'}, 'An armature must be selected')
return {'CANCELLED'}
if context.view_layer.objects.active.type != 'ARMATURE':
self.report({'ERROR_INVALID_CONTEXT'}, 'The selected object must be an armature.')
return {'CANCELLED'}
self.armature = context.view_layer.objects.active
# Populate actions list.
context.scene.psa_export.action_list.clear()
for action in bpy.data.actions:
item = context.scene.psa_export.action_list.add()
@ -105,10 +142,20 @@ class PsaExportOperator(Operator, ExportHelper):
item.is_selected = True
if len(context.scene.psa_export.action_list) == 0:
# If there are no actions at all, we have nothing to export, so just cancel the operation.
self.report({'ERROR_INVALID_CONTEXT'}, 'There are no actions to export.')
return {'CANCELLED'}
# Populate bone groups list.
context.scene.psa_export.bone_group_list.clear()
for bone_group_index, bone_group in enumerate(self.armature.pose.bone_groups):
item = context.scene.psa_export.bone_group_list.add()
item.name = bone_group.name
item.index = bone_group_index
item.is_selected = False
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
@ -120,6 +167,8 @@ class PsaExportOperator(Operator, ExportHelper):
options = PsaBuilderOptions()
options.actions = actions
options.bone_filter_mode = context.scene.psa_export.bone_filter_mode
options.bone_group_indices = [x.index for x in context.scene.psa_export.bone_group_list if x.is_selected]
builder = PsaBuilder()
psa = builder.build(context, options)
exporter = PsaExporter(psa)
@ -127,6 +176,13 @@ class PsaExportOperator(Operator, ExportHelper):
return {'FINISHED'}
class PSA_UL_ExportBoneGroupList(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
layout.alignment = 'LEFT'
layout.prop(item, 'is_selected', icon_only=True)
layout.label(text=item.name, icon='GROUP_BONE')
class PSA_UL_ExportActionList(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
layout.alignment = 'LEFT'
@ -134,7 +190,6 @@ class PSA_UL_ExportActionList(UIList):
layout.label(text=item.action_name)
def filter_items(self, context, data, property):
# TODO: returns two lists, apparently
actions = getattr(data, property)
flt_flags = []
flt_neworder = []
@ -153,6 +208,12 @@ class PsaExportSelectAll(bpy.types.Operator):
bl_idname = 'psa_export.actions_select_all'
bl_label = 'Select All'
@classmethod
def poll(cls, context):
action_list = context.scene.psa_export.action_list
has_unselected_actions = any(map(lambda action: not action.is_selected, action_list))
return len(action_list) > 0 and has_unselected_actions
def execute(self, context):
for action in context.scene.psa_export.action_list:
action.is_selected = True
@ -163,6 +224,12 @@ class PsaExportDeselectAll(bpy.types.Operator):
bl_idname = 'psa_export.actions_deselect_all'
bl_label = 'Deselect All'
@classmethod
def poll(cls, context):
action_list = context.scene.psa_export.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list))
return len(action_list) > 0 and has_selected_actions
def execute(self, context):
for action in context.scene.psa_export.action_list:
action.is_selected = False

View File

@ -4,10 +4,11 @@ from mathutils import Vector, Quaternion, Matrix
from .data import Psa
from typing import List, AnyStr, Optional
import bpy
from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature
from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature, FileSelectParams
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty
from .reader import PsaReader
import numpy as np
class PsaImporter(object):
@ -30,14 +31,7 @@ class PsaImporter(object):
self.orig_loc: Vector = Vector()
self.orig_quat: Quaternion = Quaternion()
self.post_quat: Quaternion = Quaternion()
# TODO: this is UGLY, come up with a way to just map indices for these
self.fcurve_quat_w = None
self.fcurve_quat_x = None
self.fcurve_quat_y = None
self.fcurve_quat_z = None
self.fcurve_location_x = None
self.fcurve_location_y = None
self.fcurve_location_z = None
self.fcurves = []
# create an index mapping from bones in the PSA to bones in the target armature.
psa_to_armature_bone_indices = {}
@ -103,90 +97,105 @@ class PsaImporter(object):
import_bone = import_bones[psa_bone_index]
pose_bone = import_bone.pose_bone
# rotation
# create fcurves from rotation and location data
rotation_data_path = pose_bone.path_from_id('rotation_quaternion')
import_bone.fcurve_quat_w = action.fcurves.new(rotation_data_path, index=0)
import_bone.fcurve_quat_x = action.fcurves.new(rotation_data_path, index=1)
import_bone.fcurve_quat_y = action.fcurves.new(rotation_data_path, index=2)
import_bone.fcurve_quat_z = action.fcurves.new(rotation_data_path, index=3)
# location
location_data_path = pose_bone.path_from_id('location')
import_bone.fcurve_location_x = action.fcurves.new(location_data_path, index=0)
import_bone.fcurve_location_y = action.fcurves.new(location_data_path, index=1)
import_bone.fcurve_location_z = action.fcurves.new(location_data_path, index=2)
import_bone.fcurves.extend([
action.fcurves.new(rotation_data_path, index=0), # Qw
action.fcurves.new(rotation_data_path, index=1), # Qx
action.fcurves.new(rotation_data_path, index=2), # Qy
action.fcurves.new(rotation_data_path, index=3), # Qz
action.fcurves.new(location_data_path, index=0), # Lx
action.fcurves.new(location_data_path, index=1), # Ly
action.fcurves.new(location_data_path, index=2), # Lz
])
# add keyframes
import_bone.fcurve_quat_w.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_quat_x.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_quat_y.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_quat_z.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_location_x.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_location_y.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_location_z.keyframe_points.add(sequence.frame_count)
should_invert_root = False
key_index = 0
# Read the sequence keys from the PSA file.
sequence_name = sequence.name.decode('windows-1252')
sequence_keys = psa_reader.get_sequence_keys(sequence_name)
sequence_keys = psa_reader.read_sequence_keys(sequence_name)
for frame_index in range(sequence.frame_count):
for import_bone in import_bones:
for bone_index, import_bone in enumerate(import_bones):
if import_bone is None:
# bone does not exist in the armature, skip it
key_index += 1
continue
key_location = Vector(tuple(sequence_keys[key_index].location))
# Convert world-space transforms to local-space transforms.
key_rotation = Quaternion(tuple(sequence_keys[key_index].rotation))
q = import_bone.post_quat.copy()
q.rotate(import_bone.orig_quat)
quat = q
q = import_bone.post_quat.copy()
if import_bone.parent is None and not should_invert_root:
if import_bone.parent is None:
q.rotate(key_rotation.conjugated())
else:
q.rotate(key_rotation)
quat.rotate(q.conjugated())
key_location = Vector(tuple(sequence_keys[key_index].location))
loc = key_location - import_bone.orig_loc
loc.rotate(import_bone.post_quat.conjugated())
import_bone.fcurve_quat_w.keyframe_points[frame_index].co = frame_index, quat.w
import_bone.fcurve_quat_x.keyframe_points[frame_index].co = frame_index, quat.x
import_bone.fcurve_quat_y.keyframe_points[frame_index].co = frame_index, quat.y
import_bone.fcurve_quat_z.keyframe_points[frame_index].co = frame_index, quat.z
import_bone.fcurve_location_x.keyframe_points[frame_index].co = frame_index, loc.x
import_bone.fcurve_location_y.keyframe_points[frame_index].co = frame_index, loc.y
import_bone.fcurve_location_z.keyframe_points[frame_index].co = frame_index, loc.z
bone_fcurve_data = quat.w, quat.x, quat.y, quat.z, loc.x, loc.y, loc.z
for fcurve, datum in zip(import_bone.fcurves, bone_fcurve_data):
fcurve.keyframe_points.insert(frame_index, datum)
key_index += 1
class PsaImportActionListItem(PropertyGroup):
action_name: StringProperty()
is_selected: BoolProperty(default=True)
frame_count: IntProperty()
is_selected: BoolProperty(default=False)
@property
def name(self):
return self.action_name
def on_psa_filepath_updated(property, context):
context.scene.psa_import.action_list.clear()
try:
# Read the file and populate the action list.
psa = PsaReader(context.scene.psa_import.psa_filepath).psa
for sequence in psa.sequences.values():
item = context.scene.psa_import.action_list.add()
item.action_name = sequence.name.decode('windows-1252')
item.frame_count = sequence.frame_count
item.is_selected = True
except IOError:
# TODO: set an error somewhere so the user knows the PSA could not be read.
pass
class PsaImportPropertyGroup(bpy.types.PropertyGroup):
cool_filepath: StringProperty(default='')
armature_object: PointerProperty(type=bpy.types.Object) # TODO: figure out how to filter this to only objects of a specific type
psa_filepath: StringProperty(default='', subtype='FILE_PATH', update=on_psa_filepath_updated)
armature_object: PointerProperty(name='Armature', type=bpy.types.Object)
action_list: CollectionProperty(type=PsaImportActionListItem)
import_action_list: CollectionProperty(type=PsaImportActionListItem)
action_list_index: IntProperty(name='index for list??', default=0)
import_action_list_index: IntProperty(name='index for list??', default=0)
action_list_index: IntProperty(name='', default=0)
action_filter_name: StringProperty(default='')
class PSA_UL_ImportActionList(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
layout.alignment = 'LEFT'
layout.prop(item, 'is_selected', icon_only=True)
layout.label(text=item.action_name)
row = layout.row(align=True)
split = row.split(align=True, factor=0.75)
action_col = split.row(align=True)
action_col.alignment = 'LEFT'
action_col.prop(item, 'is_selected', icon_only=True)
action_col.label(text=item.action_name)
def draw_filter(self, context, layout):
row = layout.row()
subrow = row.row(align=True)
subrow.prop(self, 'filter_name', text="")
subrow.prop(self, 'use_filter_invert', text="", icon='ARROW_LEFTRIGHT')
subrow = row.row(align=True)
subrow.prop(self, 'use_filter_sort_reverse', text='', icon='SORT_ASC')
def filter_items(self, context, data, property):
actions = getattr(data, property)
@ -205,7 +214,13 @@ class PSA_UL_ImportActionList(UIList):
class PsaImportSelectAll(bpy.types.Operator):
bl_idname = 'psa_import.actions_select_all'
bl_label = 'Select All'
bl_label = 'All'
@classmethod
def poll(cls, context):
action_list = context.scene.psa_import.action_list
has_unselected_actions = any(map(lambda action: not action.is_selected, action_list))
return len(action_list) > 0 and has_unselected_actions
def execute(self, context):
for action in context.scene.psa_import.action_list:
@ -215,7 +230,13 @@ class PsaImportSelectAll(bpy.types.Operator):
class PsaImportDeselectAll(bpy.types.Operator):
bl_idname = 'psa_import.actions_deselect_all'
bl_label = 'Deselect All'
bl_label = 'None'
@classmethod
def poll(cls, context):
action_list = context.scene.psa_import.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list))
return len(action_list) > 0 and has_selected_actions
def execute(self, context):
for action in context.scene.psa_import.action_list:
@ -234,25 +255,34 @@ class PSA_PT_ImportPanel(Panel):
layout = self.layout
scene = context.scene
row = layout.row()
row.operator('psa_import.file_select', icon='FILE_FOLDER', text='')
row.label(text=scene.psa_import.cool_filepath)
row.prop(scene.psa_import, 'psa_filepath', text='PSA File')
row = layout.row()
row.prop_search(scene.psa_import, 'armature_object', bpy.data, 'objects')
box = layout.box()
box.label(text='Actions', icon='ACTION')
box.label(text=f'Actions ({len(scene.psa_import.action_list)})', icon='ACTION')
row = box.row()
row.template_list('PSA_UL_ImportActionList', 'asd', scene.psa_import, 'action_list', scene.psa_import, 'action_list_index', rows=10)
row = box.row()
row.operator('psa_import.actions_select_all', text='Select All')
row.operator('psa_import.actions_deselect_all', text='Deselect All')
layout.prop(scene.psa_import, 'armature_object', icon_only=True)
layout.operator('psa_import.import', text='Import')
rows = max(3, min(len(scene.psa_import.action_list), 10))
row.template_list('PSA_UL_ImportActionList', 'asd', scene.psa_import, 'action_list', scene.psa_import, 'action_list_index', rows=rows)
row = box.row(align=True)
row.label(text='Select')
row.operator('psa_import.actions_select_all', text='All')
row.operator('psa_import.actions_deselect_all', text='None')
layout.operator('psa_import.import', text=f'Import')
class PsaImportOperator(Operator):
bl_idname = 'psa_import.import'
bl_label = 'Import'
@classmethod
def poll(cls, context):
action_list = context.scene.psa_import.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list))
armature_object = context.scene.psa_import.armature_object
return has_selected_actions and armature_object is not None
def execute(self, context):
psa_reader = PsaReader(context.scene.psa_import.cool_filepath)
psa_reader = PsaReader(context.scene.psa_import.psa_filepath)
sequence_names = [x.action_name for x in context.scene.psa_import.action_list if x.is_selected]
PsaImporter().import_psa(psa_reader, sequence_names, context)
return {'FINISHED'}
@ -274,16 +304,6 @@ class PsaImportFileSelectOperator(Operator, ImportHelper):
return {'RUNNING_MODAL'}
def execute(self, context):
context.scene.psa_import.cool_filepath = self.filepath
context.scene.psa_import.psa_filepath = self.filepath
# Load the sequence names from the selected file
sequence_names = []
try:
sequence_names = PsaReader.scan_sequence_names(self.filepath)
except IOError:
pass
context.scene.psa_import.action_list.clear()
for sequence_name in sequence_names:
item = context.scene.psa_import.action_list.add()
item.action_name = sequence_name.decode('windows-1252')
item.is_selected = True
return {'FINISHED'}

View File

@ -34,19 +34,13 @@ class PsaReader(object):
fp.seek(section.data_size * section.data_count, 1)
return []
def get_sequence_keys(self, sequence_name) -> List[Psa.Key]:
def read_sequence_keys(self, sequence_name) -> List[Psa.Key]:
# Set the file reader to the beginning of the keys data
sequence = self.psa.sequences[sequence_name]
data_size = sizeof(Psa.Key)
bone_count = len(self.psa.bones)
buffer_length = data_size * bone_count * sequence.frame_count
print(f'data_size: {data_size}')
print(f'buffer_length: {buffer_length}')
print(f'bone_count: {bone_count}')
print(f'sequence.frame_count: {sequence.frame_count}')
print(f'self.keys_data_offset: {self.keys_data_offset}')
sequence_keys_offset = self.keys_data_offset + (sequence.frame_start_index * bone_count * data_size)
print(f'sequence_keys_offset: {sequence_keys_offset}')
self.fp.seek(sequence_keys_offset, 0)
buffer = self.fp.read(buffer_length)
offset = 0

View File

@ -40,7 +40,7 @@ class PskBuilder(object):
modifiers = [x for x in obj.modifiers if x.type == 'ARMATURE']
if len(modifiers) == 0:
continue
elif len(modifiers) == 2:
elif len(modifiers) > 1:
raise RuntimeError(f'Mesh "{obj.name}" must have only one armature modifier')
armature_modifier_objects.add(modifiers[0].object)

View File

@ -48,7 +48,6 @@ class PskImporter(object):
self.post_quat: Quaternion = Quaternion()
import_bones = []
should_invert_root = False
new_bone_size = 8.0
for bone_index, psk_bone in enumerate(psk.bones):
@ -57,9 +56,6 @@ class PskImporter(object):
import_bone.local_rotation = Quaternion(tuple(psk_bone.rotation))
import_bone.local_translation = Vector(tuple(psk_bone.location))
if psk_bone.parent_index == 0 and bone_index == 0:
if should_invert_root:
import_bone.world_rotation_matrix = import_bone.local_rotation.conjugated().to_matrix()
else:
import_bone.world_rotation_matrix = import_bone.local_rotation.to_matrix()
import_bone.world_matrix = Matrix.Translation(import_bone.local_translation)
import_bones.append(import_bone)
@ -82,7 +78,7 @@ class PskImporter(object):
if import_bone.parent is not None:
edit_bone.parent = armature_data.edit_bones[import_bone.psk_bone.parent_index]
elif not should_invert_root:
else:
import_bone.local_rotation.conjugate()
edit_bone.tail = Vector((0.0, new_bone_size, 0.0))
@ -126,6 +122,7 @@ class PskImporter(object):
degenerate_face_indices.add(face_index)
pass
if len(degenerate_face_indices) > 0:
print(f'WARNING: Discarded {len(degenerate_face_indices)} degenerate face(s).')
bm.to_mesh(mesh_data)