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:
@@ -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,7 +79,6 @@ 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:
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user