Bone group filtering is now working for both PSK and PSA exports.

This commit is contained in:
Colin Basnett
2022-01-22 18:11:41 -08:00
parent 41e14d5e19
commit abef2a7f45
8 changed files with 353 additions and 124 deletions

View File

@@ -13,6 +13,9 @@ bl_info = {
if 'bpy' in locals():
import importlib
importlib.reload(psx_data)
importlib.reload(psx_helpers)
importlib.reload(psx_types)
importlib.reload(psk_data)
importlib.reload(psk_builder)
importlib.reload(psk_exporter)
@@ -25,6 +28,9 @@ if 'bpy' in locals():
importlib.reload(psa_importer)
else:
# if i remove this line, it can be enabled just fine
from . import data as psx_data
from . import helpers as psx_helpers
from . import types as psx_types
from .psk import data as psk_data
from .psk import builder as psk_builder
from .psk import exporter as psk_exporter
@@ -40,15 +46,19 @@ import bpy
from bpy.props import PointerProperty
classes = [
psk_exporter.PskExportOperator,
# TODO: have the individual files emit a __classes__ field or something we can update it locally instead of explicitly declaring it here.
classes = []
classes.extend(psx_types.__classes__)
classes.extend(psk_exporter.__classes__)
classes.extend([
psk_importer.PskImportOperator,
psa_importer.PsaImportOperator,
psa_importer.PsaImportFileSelectOperator,
psa_exporter.PSA_UL_ExportActionList,
psa_exporter.PSA_UL_ExportBoneGroupList,
# psa_exporter.PSA_UL_ExportBoneGroupList,
psa_importer.PSA_UL_ImportActionList,
psa_importer.PsaImportActionListItem,
psa_importer.PsaImportPsaBoneItem,
psa_importer.PsaImportSelectAll,
psa_importer.PsaImportDeselectAll,
psa_importer.PSA_PT_ImportPanel,
@@ -57,9 +67,8 @@ classes = [
psa_exporter.PsaExportSelectAll,
psa_exporter.PsaExportDeselectAll,
psa_exporter.PsaExportActionListItem,
psa_exporter.PsaExportBoneGroupListItem,
psa_exporter.PsaExportPropertyGroup,
]
])
def psk_export_menu_func(self, context):
@@ -87,6 +96,7 @@ def register():
bpy.types.TOPBAR_MT_file_import.append(psa_import_menu_func)
bpy.types.Scene.psa_import = PointerProperty(type=psa_importer.PsaImportPropertyGroup)
bpy.types.Scene.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup)
bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup)
def unregister():

View File

@@ -0,0 +1,60 @@
from typing import List
def populate_bone_groups_list(armature_object, bone_group_list):
bone_group_list.clear()
item = bone_group_list.add()
item.name = '(unassigned)'
item.index = -1
item.is_selected = True
for bone_group_index, bone_group in enumerate(armature_object.pose.bone_groups):
item = bone_group_list.add()
item.name = bone_group.name
item.index = bone_group_index
item.is_selected = True
def add_bone_groups_to_layout(layout):
pass
def get_export_bone_indices_for_bone_groups(armature_object, bone_group_indices: List[int]) -> List[int]:
"""
Returns a sorted list of bone indices that should be exported for the given bone groups.
Note that the ancestors of bones within the bone groups will also be present in the returned list.
:param armature_object: Blender object with type 'ARMATURE'
:param bone_group_indices: List of bone group indices to be exported.
:return: A sorted list of bone indices that should be exported.
"""
if armature_object is None or armature_object.type != 'ARMATURE':
raise ValueError('An armature object must be supplied')
bones = armature_object.data.bones
pose_bones = armature_object.pose.bones
bone_names = [x.name for x in bones]
# Get a list of the bone indices that are explicitly part of the bone groups we are including.
bone_index_stack = []
is_exporting_none_bone_groups = -1 in bone_group_indices
for bone_index, pose_bone in enumerate(pose_bones):
if (pose_bone.bone_group is None and is_exporting_none_bone_groups) or \
(pose_bone.bone_group is not None and pose_bone.bone_group_index in 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 ancestor 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)
return list(sorted(list(bone_indices)))

View File

@@ -1,10 +1,11 @@
from .data import *
from ..helpers import *
class PsaBuilderOptions(object):
def __init__(self):
self.actions = []
self.bone_filter_mode = 'NONE'
self.bone_filter_mode = 'ALL'
self.bone_group_indices = []
@@ -34,39 +35,31 @@ class PsaBuilder(object):
# armature bones.
bone_names = [x.name for x in bones]
pose_bones = [(bone_names.index(bone.name), bone) for bone in armature.pose.bones]
del bone_names
pose_bones.sort(key=lambda x: x[0])
pose_bones = [x[1] for x in pose_bones]
bone_indices = list(range(len(bones)))
# If bone groups are specified, get only the bones that are in that specified bone groups and their ancestors.
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)
bone_indices = get_export_bone_indices_for_bone_groups(armature, options.bone_group_indices)
# 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.
# Make the bone lists contain 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]
if len(bones) == 0:
# No bones are going to be exported.
raise RuntimeError('No bones available for export')
# Ensure that the exported hierarchy has a single root bone.
root_bones = [x for x in bones if x.parent is None]
if len(root_bones) > 1:
root_bone_names = [x.name for x in bones]
raise RuntimeError('Exported bone hierarchy must have a single root bone.'
f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}')
for pose_bone in bones:
psa_bone = Psa.Bone()
psa_bone.name = bytes(pose_bone.name, encoding='utf-8')

View File

@@ -5,6 +5,8 @@ from bpy_extras.io_utils import ExportHelper
from typing import Type
from .builder import PsaBuilder, PsaBuilderOptions
from .data import *
from ..types import BoneGroupListItem
from ..helpers import *
import re
@@ -43,30 +45,29 @@ class PsaExportActionListItem(PropertyGroup):
return self.action.name
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)
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.'),
}
description='',
items=(
('ALL', 'All', 'All bones will be exported.'),
('BONE_GROUPS', 'Bone Groups', 'Only bones belonging to the selected bone groups and their ancestors will be exported.')
)
)
bone_group_list: CollectionProperty(type=PsaExportBoneGroupListItem)
bone_group_list: CollectionProperty(type=BoneGroupListItem)
bone_group_list_index: IntProperty(default=0)
def is_bone_filter_mode_item_available(context, identifier):
if identifier == "BONE_GROUPS":
obj = context.active_object
if not obj.pose or not obj.pose.bone_groups:
return False
return True
class PsaExportOperator(Operator, ExportHelper):
bl_idname = 'export.psa'
bl_label = 'Export'
@@ -84,29 +85,34 @@ class PsaExportOperator(Operator, ExportHelper):
def draw(self, context):
layout = self.layout
scene = context.scene
property_group = context.scene.psa_export
# ACTIONS
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')
row.template_list('PSA_UL_ExportActionList', 'asd', property_group, 'action_list', property_group, 'action_list_index', rows=10)
row = box.row(align=True)
row.label(text='Select')
row.operator('psa_export.actions_select_all', text='All')
row.operator('psa_export.actions_deselect_all', text='None')
# BONES
box = layout.box()
box.label(text='Bone Filter', icon='FILTER')
box.label(text='Bones', icon='BONE_DATA')
bone_filter_mode_items = property_group.bl_rna.properties['bone_filter_mode'].enum_items_static
row = box.row(align=True)
for item in bone_filter_mode_items:
identifier = item.identifier
item_layout = row.row(align=True)
item_layout.prop_enum(property_group, 'bone_filter_mode', item.identifier)
item_layout.enabled = is_bone_filter_mode_item_available(context, identifier)
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':
if property_group.bone_filter_mode == 'BONE_GROUPS':
box = layout.box()
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)
rows = max(3, min(len(property_group.bone_group_list), 10))
row.template_list('PSX_UL_BoneGroupList', '', property_group, 'bone_group_list', property_group, 'bone_group_list_index', rows=rows)
def is_action_for_armature(self, action):
if len(action.fcurves) == 0:
@@ -122,6 +128,8 @@ class PsaExportOperator(Operator, ExportHelper):
return False
def invoke(self, context, event):
property_group = context.scene.psa_export
if context.view_layer.objects.active is None:
self.report({'ERROR_INVALID_CONTEXT'}, 'An armature must be selected')
return {'CANCELLED'}
@@ -133,33 +141,29 @@ class PsaExportOperator(Operator, ExportHelper):
self.armature = context.view_layer.objects.active
# Populate actions list.
context.scene.psa_export.action_list.clear()
property_group.action_list.clear()
for action in bpy.data.actions:
item = context.scene.psa_export.action_list.add()
item = property_group.action_list.add()
item.action = action
item.action_name = action.name
if self.is_action_for_armature(action):
item.is_selected = True
if len(context.scene.psa_export.action_list) == 0:
if len(property_group.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
populate_bone_groups_list(self.armature, property_group.bone_group_list)
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
actions = [x.action for x in context.scene.psa_export.action_list if x.is_selected]
property_group = context.scene.psa_export
actions = [x.action for x in property_group.action_list if x.is_selected]
if len(actions) == 0:
self.report({'ERROR_INVALID_CONTEXT'}, 'No actions were selected for export.')
@@ -167,8 +171,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]
options.bone_filter_mode = property_group.bone_filter_mode
options.bone_group_indices = [x.index for x in property_group.bone_group_list if x.is_selected]
builder = PsaBuilder()
psa = builder.build(context, options)
exporter = PsaExporter(psa)
@@ -176,13 +180,6 @@ 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'
@@ -210,12 +207,14 @@ class PsaExportSelectAll(bpy.types.Operator):
@classmethod
def poll(cls, context):
action_list = context.scene.psa_export.action_list
property_group = context.scene.psa_export
action_list = property_group.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:
property_group = context.scene.psa_export
for action in property_group.action_list:
action.is_selected = True
return {'FINISHED'}
@@ -226,11 +225,13 @@ class PsaExportDeselectAll(bpy.types.Operator):
@classmethod
def poll(cls, context):
action_list = context.scene.psa_export.action_list
property_group = context.scene.psa_export
action_list = property_group.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:
property_group = context.scene.psa_export
for action in property_group.action_list:
action.is_selected = False
return {'FINISHED'}

View File

@@ -17,9 +17,9 @@ class PsaImporter(object):
pass
def import_psa(self, psa_reader: PsaReader, sequence_names: List[AnyStr], context):
properties = context.scene.psa_import
property_group = context.scene.psa_import
sequences = map(lambda x: psa_reader.sequences[x], sequence_names)
armature_object = properties.armature_object
armature_object = property_group.armature_object
armature_data = armature_object.data
class ImportBone(object):
@@ -190,6 +190,14 @@ class PsaImporter(object):
print(f'total_time: {total_time}')
class PsaImportPsaBoneItem(PropertyGroup):
bone_name: StringProperty()
@property
def name(self):
return self.bone_name
class PsaImportActionListItem(PropertyGroup):
action_name: StringProperty()
frame_count: IntProperty()
@@ -201,16 +209,22 @@ class PsaImportActionListItem(PropertyGroup):
def on_psa_file_path_updated(property, context):
context.scene.psa_import.action_list.clear()
property_group = context.scene.psa_import
property_group.action_list.clear()
property_group.psa_bones.clear()
try:
# Read the file and populate the action list.
p = os.path.abspath(context.scene.psa_import.psa_file_path)
p = os.path.abspath(property_group.psa_file_path)
psa_reader = PsaReader(p)
for sequence in psa_reader.sequences.values():
item = context.scene.psa_import.action_list.add()
item = property_group.action_list.add()
item.action_name = sequence.name.decode('windows-1252')
item.frame_count = sequence.frame_count
item.is_selected = True
for psa_bone in psa_reader.bones:
item = property_group.psa_bones.add()
item.bone_name = psa_bone.name
except IOError as e:
print('ERROR READING FILE')
print(e)
@@ -218,9 +232,19 @@ def on_psa_file_path_updated(property, context):
pass
def on_armature_object_updated(property, context):
# TODO: ensure that there are matching bones between the two rigs.
property_group = context.scene.psa_import
armature_object = property_group.armature_object
if armature_object is not None:
armature_bone_names = set(map(lambda bone: bone.name, armature_object.data.bones))
psa_bone_names = set(map(lambda psa_bone: psa_bone.name, property_group.psa_bones))
class PsaImportPropertyGroup(bpy.types.PropertyGroup):
psa_file_path: StringProperty(default='', subtype='FILE_PATH', update=on_psa_file_path_updated)
armature_object: PointerProperty(name='Armature', type=bpy.types.Object)
psa_bones: CollectionProperty(type=PsaImportPsaBoneItem)
armature_object: PointerProperty(name='Object', type=bpy.types.Object, update=on_armature_object_updated)
action_list: CollectionProperty(type=PsaImportActionListItem)
action_list_index: IntProperty(name='', default=0)
action_filter_name: StringProperty(default='')
@@ -265,12 +289,14 @@ class PsaImportSelectAll(bpy.types.Operator):
@classmethod
def poll(cls, context):
action_list = context.scene.psa_import.action_list
property_group = context.scene.psa_import
action_list = property_group.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:
property_group = context.scene.psa_import
for action in property_group.action_list:
action.is_selected = True
return {'FINISHED'}
@@ -281,35 +307,41 @@ class PsaImportDeselectAll(bpy.types.Operator):
@classmethod
def poll(cls, context):
action_list = context.scene.psa_import.action_list
property_group = context.scene.psa_import
action_list = property_group.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:
property_group = context.scene.psa_import
for action in property_group.action_list:
action.is_selected = False
return {'FINISHED'}
class PSA_PT_ImportPanel(Panel):
bl_space_type = 'VIEW_3D'
bl_space_type = 'NLA_EDITOR'
bl_region_type = 'UI'
bl_label = 'PSA Import'
bl_context = 'objectmode'
bl_context = 'object'
bl_category = 'PSA Import'
@classmethod
def poll(cls, context):
return context.view_layer.objects.active is not None
def draw(self, context):
layout = self.layout
scene = context.scene
property_group = context.scene.psa_import
row = layout.row()
row.prop(scene.psa_import, 'psa_file_path', text='PSA File')
row.prop(property_group, 'psa_file_path', text='PSA File')
row = layout.row()
row.prop_search(scene.psa_import, 'armature_object', bpy.data, 'objects')
row.prop_search(property_group, 'armature_object', bpy.data, 'objects')
box = layout.box()
box.label(text=f'Actions ({len(scene.psa_import.action_list)})', icon='ACTION')
box.label(text=f'Actions ({len(property_group.action_list)})', icon='ACTION')
row = box.row()
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)
rows = max(3, min(len(property_group.action_list), 10))
row.template_list('PSA_UL_ImportActionList', '', property_group, 'action_list', property_group, 'action_list_index', rows=rows)
row = box.row(align=True)
row.label(text='Select')
row.operator('psa_import.actions_select_all', text='All')
@@ -323,14 +355,16 @@ class PsaImportOperator(Operator):
@classmethod
def poll(cls, context):
action_list = context.scene.psa_import.action_list
property_group = context.scene.psa_import
action_list = property_group.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list))
armature_object = context.scene.psa_import.armature_object
armature_object = property_group.armature_object
return has_selected_actions and armature_object is not None
def execute(self, context):
psa_reader = PsaReader(context.scene.psa_import.psa_file_path)
sequence_names = [x.action_name for x in context.scene.psa_import.action_list if x.is_selected]
property_group = context.scene.psa_import
psa_reader = PsaReader(property_group.psa_file_path)
sequence_names = [x.action_name for x in property_group.action_list if x.is_selected]
PsaImporter().import_psa(psa_reader, sequence_names, context)
return {'FINISHED'}
@@ -351,6 +385,7 @@ class PsaImportFileSelectOperator(Operator, ImportHelper):
return {'RUNNING_MODAL'}
def execute(self, context):
context.scene.psa_import.psa_file_path = self.filepath
property_group = context.scene.psa_import
property_group.psa_file_path = self.filepath
# Load the sequence names from the selected file
return {'FINISHED'}

View File

@@ -2,8 +2,8 @@ import bpy
import bmesh
from collections import OrderedDict
from .data import *
from ..helpers import *
# https://github.com/bwrsandman/blender-addons/blob/master/io_export_unreal_psk_psa.py
class PskInputObjects(object):
def __init__(self):
@@ -11,6 +11,12 @@ class PskInputObjects(object):
self.armature_object = None
class PskBuilderOptions(object):
def __init__(self):
self.bone_filter_mode = 'ALL'
self.bone_group_indices = []
class PskBuilder(object):
def __init__(self):
pass
@@ -51,13 +57,16 @@ class PskBuilder(object):
return input_objects
def build(self, context) -> Psk:
def build(self, context, options: PskBuilderOptions) -> Psk:
input_objects = PskBuilder.get_input_objects(context)
armature_object = input_objects.armature_object
psk = Psk()
bones = []
materials = OrderedDict()
if input_objects.armature_object is None:
if armature_object is None:
# Static mesh (no armature)
psk_bone = Psk.Bone()
psk_bone.name = bytes('static', encoding='utf-8')
@@ -68,15 +77,30 @@ class PskBuilder(object):
psk_bone.rotation = Quaternion(0, 0, 0, 1)
psk.bones.append(psk_bone)
else:
bones = list(input_objects.armature_object.data.bones)
bones = list(armature_object.data.bones)
# If bone groups are specified, get only the bones that are in the specified bone groups and their ancestors.
if len(options.bone_group_indices) > 0:
bone_indices = get_export_bone_indices_for_bone_groups(armature_object, options.bone_group_indices)
bones = [bones[bone_index] for bone_index in bone_indices]
# Ensure that the exported hierarchy has a single root bone.
root_bones = [x for x in bones if x.parent is None]
if len(root_bones) > 1:
root_bone_names = [x.name for x in bones]
raise RuntimeError('Exported bone hierarchy must have a single root bone.'
f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}')
for bone in bones:
psk_bone = Psk.Bone()
psk_bone.name = bytes(bone.name, encoding='utf-8')
psk_bone.flags = 0
psk_bone.children_count = len(bone.children)
psk_bone.children_count = 0
try:
psk_bone.parent_index = bones.index(bone.parent)
parent_index = bones.index(bone.parent)
psk_bone.parent_index = parent_index
psk.bones[parent_index].children_count += 1
except ValueError:
psk_bone.parent_index = 0
@@ -90,8 +114,8 @@ class PskBuilder(object):
parent_tail = quat_parent @ bone.parent.tail
location = (parent_tail - parent_head) + bone.head
else:
location = input_objects.armature_object.matrix_local @ bone.head
rot_matrix = bone.matrix @ input_objects.armature_object.matrix_local.to_3x3()
location = armature_object.matrix_local @ bone.head
rot_matrix = bone.matrix @ armature_object.matrix_local.to_3x3()
rotation = rot_matrix.to_quaternion()
psk_bone.location.x = location.x
@@ -177,14 +201,34 @@ class PskBuilder(object):
psk.faces.append(face)
# WEIGHTS
# TODO: bone ~> vg might not be 1:1, provide a nice error message if this is the case
if input_objects.armature_object is not None:
armature = input_objects.armature_object.data
bone_names = [x.name for x in armature.bones]
if armature_object is not None:
# Because the vertex groups may contain entries for which there is no matching bone in the armature,
# we must filter them out and not export any weights for these vertex groups.
bone_names = [x.name for x in bones]
vertex_group_names = [x.name for x in object.vertex_groups]
bone_indices = [bone_names.index(name) for name in vertex_group_names]
vertex_group_bone_indices = dict()
for vertex_group_index, vertex_group_name in enumerate(vertex_group_names):
try:
vertex_group_bone_indices[vertex_group_index] = bone_names.index(vertex_group_name)
except ValueError:
# The vertex group does not have a matching bone in the list of bones to be exported.
# Check to see if there is an associated bone for this vertex group that exists in the armature.
# If there is, we can traverse the ancestors of that bone to find an alternate bone to use for
# weighting the vertices belonging to this vertex group.
if vertex_group_name in armature_object.data.bones:
bone = armature_object.data.bones[vertex_group_name]
while bone is not None:
try:
bone_index = bone_names.index(bone.name)
vertex_group_bone_indices[vertex_group_index] = bone_index
break
except ValueError:
bone = bone.parent
for vertex_group_index, vertex_group in enumerate(object.vertex_groups):
bone_index = bone_indices[vertex_group_index]
if vertex_group_index not in vertex_group_bone_indices:
continue
bone_index = vertex_group_bone_indices[vertex_group_index]
# TODO: exclude vertex group if it doesn't match to a bone we are exporting
for vertex_index in range(len(object.data.vertices)):
try:
weight = vertex_group.weight(vertex_index)

View File

@@ -1,9 +1,11 @@
from .data import *
from .builder import PskBuilder
from ..types import BoneGroupListItem
from ..helpers import populate_bone_group_list
from .builder import PskBuilder, PskBuilderOptions
from typing import Type
from bpy.types import Operator
from bpy.types import Operator, PropertyGroup
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty
from bpy.props import StringProperty, CollectionProperty, IntProperty, BoolProperty, EnumProperty
MAX_WEDGE_COUNT = 65536
MAX_POINT_COUNT = 4294967296
@@ -58,6 +60,16 @@ class PskExporter(object):
self.write_section(fp, b'RAWWEIGHTS', Psk.Weight, self.psk.weights)
def is_bone_filter_mode_item_available(context, identifier):
input_objects = PskBuilder.get_input_objects(context)
armature_object = input_objects.armature_object
if identifier == 'BONE_GROUPS':
if not armature_object.pose or not armature_object.pose.bone_groups:
return False
# else if... you can set up other conditions if you add more options
return True
class PskExportOperator(Operator, ExportHelper):
bl_idname = 'export.psk'
bl_label = 'Export'
@@ -73,17 +85,66 @@ class PskExportOperator(Operator, ExportHelper):
def invoke(self, context, event):
try:
PskBuilder.get_input_objects(context)
input_objects = PskBuilder.get_input_objects(context)
except RuntimeError as e:
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
return {'CANCELLED'}
property_group = context.scene.psk_export
# Populate bone groups list.
populate_bone_group_list(input_objects.armature_object, property_group.bone_group_list)
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def draw(self, context):
layout = self.layout
scene = context.scene
property_group = scene.psk_export
# BONES
box = layout.box()
box.label(text='Bones', icon='BONE_DATA')
bone_filter_mode_items = property_group.bl_rna.properties['bone_filter_mode'].enum_items_static
row = box.row(align=True)
for item in bone_filter_mode_items:
identifier = item.identifier
item_layout = row.row(align=True)
item_layout.prop_enum(property_group, 'bone_filter_mode', item.identifier)
item_layout.enabled = is_bone_filter_mode_item_available(context, identifier)
if property_group.bone_filter_mode == 'BONE_GROUPS':
row = box.row()
rows = max(3, min(len(property_group.bone_group_list), 10))
row.template_list('PSX_UL_BoneGroupList', '', property_group, 'bone_group_list', property_group, 'bone_group_list_index', rows=rows)
def execute(self, context):
property_group = context.scene.psk_export
builder = PskBuilder()
psk = builder.build(context)
options = PskBuilderOptions()
options.bone_group_indices = [x.index for x in property_group.bone_group_list if x.is_selected]
psk = builder.build(context, options)
exporter = PskExporter(psk)
exporter.export(self.filepath)
return {'FINISHED'}
class PskExportPropertyGroup(PropertyGroup):
bone_filter_mode: EnumProperty(
name='Bone Filter',
description='',
items=(
('ALL', 'All', 'All bones will be exported.'),
('BONE_GROUPS', 'Bone Groups', 'Only bones belonging to the selected bone groups and their ancestors will be exported.')
)
)
bone_group_list: CollectionProperty(type=BoneGroupListItem)
bone_group_list_index: IntProperty(default=0)
__classes__ = [
PskExportOperator,
PskExportPropertyGroup
]

View File

@@ -0,0 +1,25 @@
from bpy.types import PropertyGroup, UIList
from bpy.props import StringProperty, IntProperty, BoolProperty
class PSX_UL_BoneGroupList(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' if item.index >= 0 else 'NONE')
class BoneGroupListItem(PropertyGroup):
name: StringProperty()
index: IntProperty()
is_selected: BoolProperty(default=False)
@property
def name(self):
return self.name
__classes__ = [
BoneGroupListItem,
PSX_UL_BoneGroupList
]