Added a number of new features:

* Added "Active Action" as the sequence type, allowing the user to
  select multiple armatures with the same data block and export an
  action for each one.
* Added a "Scale" option for PSA export when exporting animations
  for an armature that had been scaled on export.
* You are now able to export meshes without materials.
This commit is contained in:
Colin Basnett
2024-12-08 03:01:23 -08:00
parent 9b0df1c942
commit 491e042cec
6 changed files with 228 additions and 69 deletions

View File

@@ -13,7 +13,9 @@ class PsaBuildSequence:
self.frame_start: int = 0 self.frame_start: int = 0
self.frame_end: int = 0 self.frame_end: int = 0
def __init__(self): def __init__(self, armature_object: Object, anim_data: AnimData):
self.armature_object = armature_object
self.anim_data = anim_data
self.name: str = '' self.name: str = ''
self.nla_state: PsaBuildSequence.NlaState = PsaBuildSequence.NlaState() self.nla_state: PsaBuildSequence.NlaState = PsaBuildSequence.NlaState()
self.compression_ratio: float = 1.0 self.compression_ratio: float = 1.0
@@ -30,6 +32,7 @@ class PsaBuildOptions:
self.sequence_name_prefix: str = '' self.sequence_name_prefix: str = ''
self.sequence_name_suffix: str = '' self.sequence_name_suffix: str = ''
self.root_motion: bool = False self.root_motion: bool = False
self.scale = 1.0
def _get_pose_bone_location_and_rotation(pose_bone: PoseBone, armature_object: Object, options: PsaBuildOptions): def _get_pose_bone_location_and_rotation(pose_bone: PoseBone, armature_object: Object, options: PsaBuildOptions):
@@ -48,6 +51,8 @@ def _get_pose_bone_location_and_rotation(pose_bone: PoseBone, armature_object: O
location = pose_bone_matrix.to_translation() location = pose_bone_matrix.to_translation()
rotation = pose_bone_matrix.to_quaternion().normalized() rotation = pose_bone_matrix.to_quaternion().normalized()
location *= options.scale
if pose_bone.parent is not None: if pose_bone.parent is not None:
rotation.conjugate() rotation.conjugate()
@@ -67,9 +72,6 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
# As a result, we need to reconstruct the list of pose bones in the same order as the # As a result, we need to reconstruct the list of pose bones in the same order as the
# armature bones. # armature bones.
bone_names = [x.name for x in bones] bone_names = [x.name for x in bones]
pose_bones = [(bone_names.index(bone.name), bone) for bone in armature_object.pose.bones]
pose_bones.sort(key=lambda x: x[0])
pose_bones = [x[1] for x in pose_bones]
# Get a list of all the bone indices and instigator bones for the bone filter settings. # Get a list of all the bone indices and instigator bones for the bone filter settings.
export_bone_names = get_export_bone_names(armature_object, options.bone_filter_mode, options.bone_collection_indices) export_bone_names = get_export_bone_names(armature_object, options.bone_filter_mode, options.bone_collection_indices)
@@ -77,12 +79,11 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
# Make the bone lists contain 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] bones = [bones[bone_index] for bone_index in bone_indices]
pose_bones = [pose_bones[bone_index] for bone_index in bone_indices]
# No bones are going to be exported. # No bones are going to be exported.
if len(bones) == 0: if len(bones) == 0:
raise RuntimeError('No bones available for export') raise RuntimeError('No bones available for export')
# Build list of PSA bones. # Build list of PSA bones.
for bone in bones: for bone in bones:
psa_bone = Psa.Bone() psa_bone = Psa.Bone()
@@ -140,8 +141,14 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
context.window_manager.progress_begin(0, len(options.sequences)) context.window_manager.progress_begin(0, len(options.sequences))
for export_sequence_index, export_sequence in enumerate(options.sequences): for export_sequence_index, export_sequence in enumerate(options.sequences):
# Look up the pose bones for the bones that are going to be exported.
pose_bones = [(bone_names.index(bone.name), bone) for bone in export_sequence.armature_object.pose.bones]
pose_bones.sort(key=lambda x: x[0])
pose_bones = [x[1] for x in pose_bones]
pose_bones = [pose_bones[bone_index] for bone_index in bone_indices]
# Link the action to the animation data and update view layer. # Link the action to the animation data and update view layer.
options.animation_data.action = export_sequence.nla_state.action export_sequence.anim_data.action = export_sequence.nla_state.action
context.view_layer.update() context.view_layer.update()
frame_start = export_sequence.nla_state.frame_start frame_start = export_sequence.nla_state.frame_start
@@ -181,7 +188,7 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
context.scene.frame_set(frame=int(frame), subframe=frame % 1.0) context.scene.frame_set(frame=int(frame), subframe=frame % 1.0)
for pose_bone in pose_bones: for pose_bone in pose_bones:
location, rotation = _get_pose_bone_location_and_rotation(pose_bone, armature_object, options) location, rotation = _get_pose_bone_location_and_rotation(pose_bone, export_sequence.armature_object, options)
key = Psa.Key() key = Psa.Key()
key.location.x = location.x key.location.x = location.x

View File

@@ -1,6 +1,6 @@
import re import re
from collections import Counter from collections import Counter
from typing import List, Iterable, Dict, Tuple from typing import List, Iterable, Dict, Tuple, Optional
import bpy import bpy
from bpy.props import StringProperty from bpy.props import StringProperty
@@ -229,40 +229,41 @@ class PSA_OT_export(Operator, ExportHelper):
flow.use_property_decorate = False flow.use_property_decorate = False
flow.prop(pg, 'sequence_source', text='Source') flow.prop(pg, 'sequence_source', text='Source')
if pg.sequence_source in {'TIMELINE_MARKERS', 'NLA_TRACK_STRIPS'}: if pg.sequence_source != 'ACTIVE_ACTION':
# ANIMDATA SOURCE if pg.sequence_source in {'TIMELINE_MARKERS', 'NLA_TRACK_STRIPS'}:
flow.prop(pg, 'should_override_animation_data') # ANIMDATA SOURCE
if pg.should_override_animation_data: flow.prop(pg, 'should_override_animation_data')
flow.prop(pg, 'animation_data_override', text=' ') if pg.should_override_animation_data:
flow.prop(pg, 'animation_data_override', text=' ')
if pg.sequence_source == 'NLA_TRACK_STRIPS': if pg.sequence_source == 'NLA_TRACK_STRIPS':
flow = sequences_panel.grid_flow() flow = sequences_panel.grid_flow()
flow.use_property_split = True flow.use_property_split = True
flow.use_property_decorate = False flow.use_property_decorate = False
flow.prop(pg, 'nla_track') flow.prop(pg, 'nla_track')
# SELECT ALL/NONE # SELECT ALL/NONE
row = sequences_panel.row(align=True) row = sequences_panel.row(align=True)
row.label(text='Select') row.label(text='Select')
row.operator(PSA_OT_export_actions_select_all.bl_idname, text='All', icon='CHECKBOX_HLT') row.operator(PSA_OT_export_actions_select_all.bl_idname, text='All', icon='CHECKBOX_HLT')
row.operator(PSA_OT_export_actions_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT') row.operator(PSA_OT_export_actions_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT')
from .ui import PSA_UL_export_sequences from .ui import PSA_UL_export_sequences
def get_sequences_propnames_from_source(sequence_source: str) -> Tuple[str, str]: def get_sequences_propnames_from_source(sequence_source: str) -> Optional[Tuple[str, str]]:
match sequence_source: match sequence_source:
case 'ACTIONS': case 'ACTIONS':
return 'action_list', 'action_list_index' return 'action_list', 'action_list_index'
case 'TIMELINE_MARKERS': case 'TIMELINE_MARKERS':
return 'marker_list', 'marker_list_index' return 'marker_list', 'marker_list_index'
case 'NLA_TRACK_STRIPS': case 'NLA_TRACK_STRIPS':
return 'nla_strip_list', 'nla_strip_list_index' return 'nla_strip_list', 'nla_strip_list_index'
case _: case _:
raise ValueError(f'Unhandled sequence source: {sequence_source}') raise ValueError(f'Unhandled sequence source: {sequence_source}')
propname, active_propname = get_sequences_propnames_from_source(pg.sequence_source) propname, active_propname = get_sequences_propnames_from_source(pg.sequence_source)
sequences_panel.template_list(PSA_UL_export_sequences.bl_idname, '', pg, propname, pg, active_propname, sequences_panel.template_list(PSA_UL_export_sequences.bl_idname, '', pg, propname, pg, active_propname,
rows=max(3, min(len(getattr(pg, propname)), 10))) rows=max(3, min(len(getattr(pg, propname)), 10)))
flow = sequences_panel.grid_flow() flow = sequences_panel.grid_flow()
flow.use_property_split = True flow.use_property_split = True
@@ -301,15 +302,16 @@ class PSA_OT_export(Operator, ExportHelper):
bones_panel.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index', bones_panel.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index',
rows=rows) rows=rows)
# ADVANCED # TRANSFORM
advanced_header, advanced_panel = layout.panel('Advanced', default_closed=False) transform_header, transform_panel = layout.panel('Advanced', default_closed=False)
advanced_header.label(text='Advanced') transform_header.label(text='Transform')
if advanced_panel: if transform_panel:
flow = advanced_panel.grid_flow() flow = transform_panel.grid_flow()
flow.use_property_split = True flow.use_property_split = True
flow.use_property_decorate = False flow.use_property_decorate = False
flow.prop(pg, 'root_motion', text='Root Motion') flow.prop(pg, 'root_motion', text='Root Motion')
flow.prop(pg, 'scale', text='Scale')
@classmethod @classmethod
def _check_context(cls, context): def _check_context(cls, context):
@@ -317,7 +319,16 @@ class PSA_OT_export(Operator, ExportHelper):
raise RuntimeError('An armature must be selected') raise RuntimeError('An armature must be selected')
if context.view_layer.objects.active.type != 'ARMATURE': if context.view_layer.objects.active.type != 'ARMATURE':
raise RuntimeError('The selected object must be an armature') raise RuntimeError('The active object must be an armature')
# If we have multiple armatures selected, make sure that they all use the same underlying armature data.
armature_objects = [obj for obj in context.selected_objects if obj.type == 'ARMATURE']
for obj in armature_objects:
if obj.data != context.view_layer.objects.active.data:
raise RuntimeError(f'All selected armatures must use the same armature data block.\n\n'
f'\The armature data block for "{obj.name}\" (\'{obj.data.name}\') does not match '
f'the active armature data block (\'{context.view_layer.objects.active.name}\')')
def invoke(self, context, _event): def invoke(self, context, _event):
try: try:
@@ -362,14 +373,16 @@ class PSA_OT_export(Operator, ExportHelper):
export_sequences: List[PsaBuildSequence] = [] export_sequences: List[PsaBuildSequence] = []
selected_armature_objects = [obj for obj in context.selected_objects if obj.type == 'ARMATURE']
match pg.sequence_source: match pg.sequence_source:
case 'ACTIONS': case 'ACTIONS':
for action_item in filter(lambda x: x.is_selected, pg.action_list): for action_item in filter(lambda x: x.is_selected, pg.action_list):
if len(action_item.action.fcurves) == 0: if len(action_item.action.fcurves) == 0:
continue continue
export_sequence = PsaBuildSequence() export_sequence = PsaBuildSequence(context.active_object, animation_data)
export_sequence.nla_state.action = action_item.action
export_sequence.name = action_item.name export_sequence.name = action_item.name
export_sequence.nla_state.action = action_item.action
export_sequence.nla_state.frame_start = action_item.frame_start export_sequence.nla_state.frame_start = action_item.frame_start
export_sequence.nla_state.frame_end = action_item.frame_end export_sequence.nla_state.frame_end = action_item.frame_end
export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action_item.action]) export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action_item.action])
@@ -378,9 +391,8 @@ class PSA_OT_export(Operator, ExportHelper):
export_sequences.append(export_sequence) export_sequences.append(export_sequence)
case 'TIMELINE_MARKERS': case 'TIMELINE_MARKERS':
for marker_item in filter(lambda x: x.is_selected, pg.marker_list): for marker_item in filter(lambda x: x.is_selected, pg.marker_list):
export_sequence = PsaBuildSequence() export_sequence = PsaBuildSequence(context.active_object, animation_data)
export_sequence.name = marker_item.name export_sequence.name = marker_item.name
export_sequence.nla_state.action = None
export_sequence.nla_state.frame_start = marker_item.frame_start export_sequence.nla_state.frame_start = marker_item.frame_start
export_sequence.nla_state.frame_end = marker_item.frame_end export_sequence.nla_state.frame_end = marker_item.frame_end
nla_strips_actions = set( nla_strips_actions = set(
@@ -389,15 +401,28 @@ class PSA_OT_export(Operator, ExportHelper):
export_sequences.append(export_sequence) export_sequences.append(export_sequence)
case 'NLA_TRACK_STRIPS': case 'NLA_TRACK_STRIPS':
for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list): for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list):
export_sequence = PsaBuildSequence() export_sequence = PsaBuildSequence(context.active_object, animation_data)
export_sequence.name = nla_strip_item.name export_sequence.name = nla_strip_item.name
export_sequence.nla_state.action = None
export_sequence.nla_state.frame_start = nla_strip_item.frame_start export_sequence.nla_state.frame_start = nla_strip_item.frame_start
export_sequence.nla_state.frame_end = nla_strip_item.frame_end export_sequence.nla_state.frame_end = nla_strip_item.frame_end
export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [nla_strip_item.action]) export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [nla_strip_item.action])
export_sequence.compression_ratio = nla_strip_item.action.psa_export.compression_ratio export_sequence.compression_ratio = nla_strip_item.action.psa_export.compression_ratio
export_sequence.key_quota = nla_strip_item.action.psa_export.key_quota export_sequence.key_quota = nla_strip_item.action.psa_export.key_quota
export_sequences.append(export_sequence) export_sequences.append(export_sequence)
case 'ACTIVE_ACTION':
for obj in selected_armature_objects:
if obj.animation_data is None or obj.animation_data.action is None:
continue
action = obj.animation_data.action
export_sequence = PsaBuildSequence(obj, obj.animation_data)
export_sequence.name = action.name
export_sequence.nla_state.action = action
export_sequence.nla_state.frame_start = int(action.frame_range[0])
export_sequence.nla_state.frame_end = int(action.frame_range[1])
export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action])
export_sequence.compression_ratio = action.psa_export.compression_ratio
export_sequence.key_quota = action.psa_export.key_quota
export_sequences.append(export_sequence)
case _: case _:
raise ValueError(f'Unhandled sequence source: {pg.sequence_source}') raise ValueError(f'Unhandled sequence source: {pg.sequence_source}')
@@ -409,6 +434,7 @@ class PSA_OT_export(Operator, ExportHelper):
options.sequence_name_prefix = pg.sequence_name_prefix options.sequence_name_prefix = pg.sequence_name_prefix
options.sequence_name_suffix = pg.sequence_name_suffix options.sequence_name_suffix = pg.sequence_name_suffix
options.root_motion = pg.root_motion options.root_motion = pg.root_motion
options.scale = pg.scale
try: try:
psa = build_psa(context, options) psa = build_psa(context, options)

View File

@@ -123,7 +123,8 @@ class PSA_PG_export(PropertyGroup):
items=( items=(
('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0), ('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0),
('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences are delineated by scene timeline markers', 'MARKER_HLT', 1), ('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences are delineated by scene timeline markers', 'MARKER_HLT', 1),
('NLA_TRACK_STRIPS', 'NLA Track Strips', 'Sequences are delineated by the start & end times of strips on the selected NLA track', 'NLA', 2) ('NLA_TRACK_STRIPS', 'NLA Track Strips', 'Sequences are delineated by the start & end times of strips on the selected NLA track', 'NLA', 2),
('ACTIVE_ACTION', 'Active Action', 'The active action will be exported for each selected armature', 'ACTION', 3),
) )
) )
nla_track: StringProperty( nla_track: StringProperty(
@@ -188,6 +189,13 @@ class PSA_PG_export(PropertyGroup):
name='Show Reversed', name='Show Reversed',
description='Show reversed sequences' description='Show reversed sequences'
) )
scale: FloatProperty(
name='Scale',
default=1.0,
description='Scale factor to apply to the bone translations. Use this if you are exporting animations for a scaled PSK mesh',
min=0.0,
soft_max=100.0
)
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]: def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:

View File

@@ -3,8 +3,8 @@ from typing import Optional
import bmesh import bmesh
import numpy as np import numpy as np
from bpy.types import Material, Collection, Context, Mesh from bpy.types import Material, Collection, Context
from mathutils import Matrix from mathutils import Matrix, Vector
from .data import * from .data import *
from .properties import triangle_type_and_bit_flags_to_poly_flags from .properties import triangle_type_and_bit_flags_to_poly_flags
@@ -26,6 +26,36 @@ class PskBuildOptions(object):
self.materials: List[Material] = [] self.materials: List[Material] = []
self.scale = 1.0 self.scale = 1.0
self.export_space = 'WORLD' self.export_space = 'WORLD'
self.forward_axis = 'X'
self.up_axis = 'Z'
def get_vector_from_axis_identifier(axis_identifier: str) -> Vector:
match axis_identifier:
case 'X':
return Vector((1.0, 0.0, 0.0))
case 'Y':
return Vector((0.0, 1.0, 0.0))
case 'Z':
return Vector((0.0, 0.0, 1.0))
case '-X':
return Vector((-1.0, 0.0, 0.0))
case '-Y':
return Vector((0.0, -1.0, 0.0))
case '-Z':
return Vector((0.0, 0.0, -1.0))
def get_coordinate_system_transform(forward_axis: str = 'X', up_axis: str = 'Z') -> Matrix:
forward = get_vector_from_axis_identifier(forward_axis)
up = get_vector_from_axis_identifier(up_axis)
left = up.cross(forward)
return Matrix((
(forward.x, forward.y, forward.z, 0.0),
(left.x, left.y, left.z, 0.0),
(up.x, up.y, up.z, 0.0),
(0.0, 0.0, 0.0, 1.0)
)).inverted()
def get_mesh_objects_for_collection(collection: Collection) -> Iterable[DfsObject]: def get_mesh_objects_for_collection(collection: Collection) -> Iterable[DfsObject]:
@@ -65,11 +95,6 @@ def _get_psk_input_objects(mesh_objects: Iterable[DfsObject]) -> PskInputObjects
if len(mesh_objects) == 0: if len(mesh_objects) == 0:
raise RuntimeError('At least one mesh must be selected') raise RuntimeError('At least one mesh must be selected')
for dfs_object in mesh_objects:
mesh_data = cast(Mesh, dfs_object.obj.data)
if len(mesh_data.materials) == 0:
raise RuntimeError(f'Mesh "{dfs_object.obj.name}" must have at least one material')
input_objects = PskInputObjects() input_objects = PskInputObjects()
input_objects.mesh_objects = mesh_objects input_objects.mesh_objects = mesh_objects
input_objects.armature_object = get_armature_for_mesh_objects([x.obj for x in mesh_objects]) input_objects.armature_object = get_armature_for_mesh_objects([x.obj for x in mesh_objects])
@@ -107,12 +132,18 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
case 'WORLD': case 'WORLD':
return Matrix.Identity(4) return Matrix.Identity(4)
case 'ARMATURE': case 'ARMATURE':
return armature_object.matrix_world.inverted() if armature_object is not None:
return armature_object.matrix_world.inverted()
else:
return Matrix.Identity(4)
case _: case _:
raise ValueError(f'Invalid export space: {options.export_space}') raise ValueError(f'Invalid export space: {options.export_space}')
coordinate_system_matrix = get_coordinate_system_transform(options.forward_axis, options.up_axis)
coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion()
export_space_matrix = get_export_space_matrix() # TODO: maybe neutralize the scale here? export_space_matrix = get_export_space_matrix() # TODO: maybe neutralize the scale here?
scale_matrix = Matrix.Scale(options.scale, 4) scale_matrix = coordinate_system_matrix @ Matrix.Scale(options.scale, 4)
if armature_object is None or len(armature_object.data.bones) == 0: if armature_object is None or len(armature_object.data.bones) == 0:
# If the mesh has no armature object or no bones, simply assign it a dummy bone at the root to satisfy the # If the mesh has no armature object or no bones, simply assign it a dummy bone at the root to satisfy the
@@ -123,7 +154,7 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
psk_bone.children_count = 0 psk_bone.children_count = 0
psk_bone.parent_index = 0 psk_bone.parent_index = 0
psk_bone.location = Vector3.zero() psk_bone.location = Vector3.zero()
psk_bone.rotation = Quaternion.identity() psk_bone.rotation = coordinate_system_default_rotation
psk.bones.append(psk_bone) psk.bones.append(psk_bone)
else: else:
bone_names = get_export_bone_names(armature_object, options.bone_filter_mode, options.bone_collection_indices) bone_names = get_export_bone_names(armature_object, options.bone_filter_mode, options.bone_collection_indices)
@@ -169,6 +200,7 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
local_rotation = armature_local_matrix.to_3x3().to_quaternion().conjugated() local_rotation = armature_local_matrix.to_3x3().to_quaternion().conjugated()
rotation = bone_rotation @ local_rotation rotation = bone_rotation @ local_rotation
rotation.conjugate() rotation.conjugate()
rotation = coordinate_system_default_rotation @ rotation
location = scale_matrix @ location location = scale_matrix @ location
@@ -178,8 +210,6 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
location.y *= armature_object_scale.y location.y *= armature_object_scale.y
location.z *= armature_object_scale.z location.z *= armature_object_scale.z
print(bone.name, location)
psk_bone.location.x = location.x psk_bone.location.x = location.x
psk_bone.location.y = location.y psk_bone.location.y = location.y
psk_bone.location.z = location.z psk_bone.location.z = location.z
@@ -203,6 +233,17 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
material.psk.mesh_triangle_bit_flags) material.psk.mesh_triangle_bit_flags)
psk.materials.append(psk_material) psk.materials.append(psk_material)
# TODO: This wasn't left in a good state. We should detect if we need to add a "default" material.
# This can be done by checking if there is an empty material slot on any of the mesh objects, or if there are
# no material slots on any of the mesh objects.
# If so, it should be added to the end of the list of materials, and its index should mapped to a None value in the
# material indices list.
if len(psk.materials) == 0:
# Add a default material if no materials are present.
psk_material = Psk.Material()
psk_material.name = bytes('None', encoding='windows-1252')
psk.materials.append(psk_material)
context.window_manager.progress_begin(0, len(input_objects.mesh_objects)) context.window_manager.progress_begin(0, len(input_objects.mesh_objects))
material_names = [m.name for m in options.materials] material_names = [m.name for m in options.materials]
@@ -213,8 +254,26 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
should_flip_normals = False should_flip_normals = False
def get_material_name_indices(obj: Object, material_names: List[str]) -> Iterable[int]:
'''
Returns the index of the material in the list of material names.
If the material is not found, the index 0 is returned.
'''
for material_slot in obj.material_slots:
if material_slot.material is None:
yield 0
else:
try:
yield material_names.index(material_slot.material.name)
except ValueError:
yield 0
# MATERIALS # MATERIALS
material_indices = [material_names.index(material_slot.material.name) for material_slot in obj.material_slots] material_indices = list(get_material_name_indices(obj, material_names))
if len(material_indices) == 0:
# Add a default material if no materials are present.
material_indices = [0]
# MESH DATA # MESH DATA
match options.object_eval_state: match options.object_eval_state:
@@ -233,7 +292,13 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
depsgraph = context.evaluated_depsgraph_get() depsgraph = context.evaluated_depsgraph_get()
bm = bmesh.new() bm = bmesh.new()
bm.from_object(obj, depsgraph)
try:
bm.from_object(obj, depsgraph)
except ValueError:
raise RuntimeError(f'Object "{obj.name}" is not evaluated.\n'
'This is likely because the object is in a collection that has been excluded from the view layer.')
mesh_data = bpy.data.meshes.new('') mesh_data = bpy.data.meshes.new('')
bm.to_mesh(mesh_data) bm.to_mesh(mesh_data)
del bm del bm

View File

@@ -117,6 +117,36 @@ class PSK_OT_material_list_move_down(Operator):
empty_set = set() empty_set = set()
axis_identifiers = ('X', 'Y', 'Z', '-X', '-Y', '-Z')
forward_items = (
('X', 'X Forward', ''),
('Y', 'Y Forward', ''),
('Z', 'Z Forward', ''),
('-X', '-X Forward', ''),
('-Y', '-Y Forward', ''),
('-Z', '-Z Forward', ''),
)
up_items = (
('X', 'X Up', ''),
('Y', 'Y Up', ''),
('Z', 'Z Up', ''),
('-X', '-X Up', ''),
('-Y', '-Y Up', ''),
('-Z', '-Z Up', ''),
)
def forward_axis_update(self, context):
if self.forward_axis == self.up_axis:
# Automatically set the up axis to the next available axis
self.up_axis = next((axis for axis in axis_identifiers if axis != self.forward_axis), 'Z')
def up_axis_update(self, context):
if self.up_axis == self.forward_axis:
# Automatically set the forward axis to the next available axis
self.forward_axis = next((axis for axis in axis_identifiers if axis != self.up_axis), 'X')
class PSK_OT_export_collection(Operator, ExportHelper): class PSK_OT_export_collection(Operator, ExportHelper):
bl_idname = 'export.psk_collection' bl_idname = 'export.psk_collection'
@@ -163,7 +193,18 @@ class PSK_OT_export_collection(Operator, ExportHelper):
) )
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item) bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
bone_collection_list_index: IntProperty(default=0) bone_collection_list_index: IntProperty(default=0)
forward_axis: EnumProperty(
name='Forward',
items=forward_items,
default='X',
update=forward_axis_update
)
up_axis: EnumProperty(
name='Up',
items=up_items,
default='Z',
update=up_axis_update
)
def execute(self, context): def execute(self, context):
collection = bpy.data.collections.get(self.collection) collection = bpy.data.collections.get(self.collection)
@@ -181,6 +222,8 @@ class PSK_OT_export_collection(Operator, ExportHelper):
options.export_space = self.export_space options.export_space = self.export_space
options.bone_filter_mode = self.bone_filter_mode options.bone_filter_mode = self.bone_filter_mode
options.bone_collection_indices = [x.index for x in self.bone_collection_list if x.is_selected] options.bone_collection_indices = [x.index for x in self.bone_collection_list if x.is_selected]
options.forward_axis = self.forward_axis
options.up_axis = self.up_axis
try: try:
result = build_psk(context, input_objects, options) result = build_psk(context, input_objects, options)
@@ -204,9 +247,6 @@ class PSK_OT_export_collection(Operator, ExportHelper):
flow.use_property_split = True flow.use_property_split = True
flow.use_property_decorate = False flow.use_property_decorate = False
flow.prop(self, 'scale')
flow.prop(self, 'export_space')
# MESH # MESH
mesh_header, mesh_panel = layout.panel('Mesh', default_closed=False) mesh_header, mesh_panel = layout.panel('Mesh', default_closed=False)
mesh_header.label(text='Mesh', icon='MESH_DATA') mesh_header.label(text='Mesh', icon='MESH_DATA')
@@ -227,6 +267,18 @@ class PSK_OT_export_collection(Operator, ExportHelper):
rows = max(3, min(len(self.bone_collection_list), 10)) rows = max(3, min(len(self.bone_collection_list), 10))
bones_panel.template_list('PSX_UL_bone_collection_list', '', self, 'bone_collection_list', self, 'bone_collection_list_index', rows=rows) bones_panel.template_list('PSX_UL_bone_collection_list', '', self, 'bone_collection_list', self, 'bone_collection_list_index', rows=rows)
# TRANSFORM
transform_header, transform_panel = layout.panel('Transform', default_closed=False)
transform_header.label(text='Transform')
if transform_panel:
flow = transform_panel.grid_flow(row_major=True)
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(self, 'export_space')
flow.prop(self, 'scale')
flow.prop(self, 'forward_axis')
flow.prop(self, 'up_axis')
class PSK_OT_export(Operator, ExportHelper): class PSK_OT_export(Operator, ExportHelper):
bl_idname = 'export.psk' bl_idname = 'export.psk'

View File

@@ -1,6 +1,7 @@
from bpy.props import EnumProperty, CollectionProperty, IntProperty, PointerProperty, FloatProperty from bpy.props import EnumProperty, CollectionProperty, IntProperty, PointerProperty, FloatProperty
from bpy.types import PropertyGroup, Material from bpy.types import PropertyGroup, Material
from ...shared.data import bone_filter_mode_items
from ...shared.types import PSX_PG_bone_collection_list_item from ...shared.types import PSX_PG_bone_collection_list_item
empty_set = set() empty_set = set()