1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2024-11-27 16:10:48 +01:00

Merge branch 'feature-nla-track-sequence-5x' into blender-4.0

# Conflicts:
#	io_scene_psk_psa/__init__.py
#	io_scene_psk_psa/psa/export/operators.py
This commit is contained in:
Colin Basnett 2023-09-18 13:24:12 -07:00
commit 46443162a7
8 changed files with 157 additions and 91 deletions

View File

@ -1,4 +1,3 @@
import datetime
import re
import typing
from typing import List, Iterable
@ -8,24 +7,6 @@ import bpy.types
from bpy.types import NlaStrip, Object, AnimData
class Timer:
def __enter__(self):
self.start = datetime.datetime.now()
self.interval = None
return self
def __exit__(self, *args):
self.end = datetime.datetime.now()
self.interval = self.end - self.start
@property
def duration(self):
if self.interval is not None:
return self.interval
else:
return datetime.datetime.now() - self.start
def rgb_to_srgb(c: float):
if c > 0.0031308:
return 1.055 * (pow(c, (1.0 / 2.4))) - 0.055
@ -33,7 +14,7 @@ def rgb_to_srgb(c: float):
return 12.92 * c
def get_nla_strips_in_timeframe(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]:
def get_nla_strips_in_frame_range(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]:
if animation_data is None:
return []
strips = []
@ -150,7 +131,7 @@ def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_c
# Split out the bone indices and the instigator bone names into separate lists.
# We use the bone names for the return values because the bone name is a more universal way of referencing them.
# For example, users of this function may modify bone lists, which would invalidate the indices and require a
# For example, users of this function may modify bone lists, which would invalidate the indices and require an
# index mapping scheme to resolve it. Using strings is more comfy and results in less code downstream.
instigator_bone_names = [bones[x[1]].name if x[1] is not None else None for x in bone_indices]
bone_names = [bones[x[0]].name for x in bone_indices]

View File

@ -91,7 +91,11 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
# Build list of PSA bones.
for bone in bones:
psa_bone = Psa.Bone()
psa_bone.name = bytes(bone.name, encoding='windows-1252')
try:
psa_bone.name = bytes(bone.name, encoding='windows-1252')
except UnicodeEncodeError:
raise RuntimeError(f'Bone name "{bone.name}" contains characters that cannot be encoded in the Windows-1252 codepage')
try:
parent_index = bones.index(bone.parent)
@ -165,7 +169,10 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
frame_step = -frame_step
psa_sequence = Psa.Sequence()
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
try:
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
except UnicodeEncodeError:
raise RuntimeError(f'Sequence name "{export_sequence.name}" contains characters that cannot be encoded in the Windows-1252 codepage')
psa_sequence.frame_count = frame_count
psa_sequence.frame_start_index = frame_start_index
psa_sequence.fps = frame_count / sequence_duration

View File

@ -8,10 +8,10 @@ from bpy.types import Context, Armature, Action, Object, AnimData, TimelineMarke
from bpy_extras.io_utils import ExportHelper
from bpy_types import Operator
from .properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences
from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions
from ..export.properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences
from ..writer import write_psa
from ...helpers import populate_bone_collection_list, get_nla_strips_in_timeframe
from ...helpers import populate_bone_collection_list, get_nla_strips_in_frame_range
def is_action_for_armature(armature: Armature, action: Action):
@ -80,12 +80,14 @@ def update_actions_and_timeline_markers(context: Context, armature: Armature):
continue
if marker_name.startswith('#'):
continue
item = pg.marker_list.add()
item.name = marker_name
item.is_selected = False
frame_start, frame_end = sequence_frame_ranges[marker_name]
item.frame_start = frame_start
item.frame_end = frame_end
sequences = get_sequences_from_name_and_frame_range(marker_name, frame_start, frame_end)
for (sequence_name, frame_start, frame_end) in sequences:
item = pg.marker_list.add()
item.name = sequence_name
item.is_selected = False
item.frame_start = frame_start
item.frame_end = frame_end
def get_sequence_fps(context: Context, fps_source: str, fps_custom: float, actions: Iterable[Action]) -> float:
@ -150,7 +152,7 @@ def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context:
if next_marker_index < len(sorted_timeline_markers):
# There is a next marker. Use that next marker's frame position as the last frame of this sequence.
frame_end = sorted_timeline_markers[next_marker_index].frame
nla_strips = get_nla_strips_in_timeframe(animation_data, marker.frame, frame_end)
nla_strips = get_nla_strips_in_frame_range(animation_data, marker.frame, frame_end)
if len(nla_strips) > 0:
frame_end = min(frame_end, max(map(lambda nla_strip: nla_strip.frame_end, nla_strips)))
frame_start = max(frame_start, min(map(lambda nla_strip: nla_strip.frame_start, nla_strips)))
@ -174,11 +176,9 @@ def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context:
return sequence_frame_ranges
def get_sequences_from_action(action: Action) -> List[Tuple[str, int, int]]:
frame_start = int(action.frame_range[0])
frame_end = int(action.frame_range[1])
def get_sequences_from_name_and_frame_range(name: str, frame_start: int, frame_end: int) -> List[Tuple[str, int, int]]:
reversed_pattern = r'(.+)/(.+)'
reversed_match = re.match(reversed_pattern, action.name)
reversed_match = re.match(reversed_pattern, name)
if reversed_match:
forward_name = reversed_match.group(1)
backwards_name = reversed_match.group(2)
@ -187,7 +187,13 @@ def get_sequences_from_action(action: Action) -> List[Tuple[str, int, int]]:
(backwards_name, frame_end, frame_start)
]
else:
return [(action.name, frame_start, frame_end)]
return [(name, frame_start, frame_end)]
def get_sequences_from_action(action: Action) -> List[Tuple[str, int, int]]:
frame_start = int(action.frame_range[0])
frame_end = int(action.frame_range[1])
return get_sequences_from_name_and_frame_range(action.name, frame_start, frame_end)
def get_sequences_from_action_pose_marker(action: Action, pose_markers: List[TimelineMarker], pose_marker: TimelineMarker, pose_marker_index: int) -> List[Tuple[str, int, int]]:
@ -196,17 +202,7 @@ def get_sequences_from_action_pose_marker(action: Action, pose_markers: List[Tim
frame_end = pose_markers[pose_marker_index + 1].frame
else:
frame_end = int(action.frame_range[1])
reversed_pattern = r'(.+)/(.+)'
reversed_match = re.match(reversed_pattern, pose_marker.name)
if reversed_match:
forward_name = reversed_match.group(1)
backwards_name = reversed_match.group(2)
return [
(forward_name, frame_start, frame_end),
(backwards_name, frame_end, frame_start)
]
else:
return [(pose_marker.name, frame_start, frame_end)]
return get_sequences_from_name_and_frame_range(pose_marker.name, frame_start, frame_end)
def get_visible_sequences(pg: PSA_PG_export, sequences) -> List[PSA_PG_export_action_list_item]:
@ -254,12 +250,18 @@ class PSA_OT_export(Operator, ExportHelper):
# SOURCE
layout.prop(pg, 'sequence_source', text='Source')
if pg.sequence_source == 'TIMELINE_MARKERS':
if pg.sequence_source in {'TIMELINE_MARKERS', 'NLA_TRACK_STRIPS'}:
# ANIMDATA SOURCE
layout.prop(pg, 'should_override_animation_data')
if pg.should_override_animation_data:
layout.prop(pg, 'animation_data_override', text='')
if pg.sequence_source == 'NLA_TRACK_STRIPS':
flow = layout.grid_flow()
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(pg, 'nla_track')
# SELECT ALL/NONE
row = layout.row(align=True)
row.label(text='Select')
@ -269,25 +271,19 @@ class PSA_OT_export(Operator, ExportHelper):
# ACTIONS
if pg.sequence_source == 'ACTIONS':
rows = max(3, min(len(pg.action_list), 10))
layout.template_list('PSA_UL_export_sequences', '', pg, 'action_list', pg, 'action_list_index', rows=rows)
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'sequence_name_prefix')
col.prop(pg, 'sequence_name_suffix')
elif pg.sequence_source == 'TIMELINE_MARKERS':
rows = max(3, min(len(pg.marker_list), 10))
layout.template_list('PSA_UL_export_sequences', '', pg, 'marker_list', pg, 'marker_list_index',
rows=rows)
layout.template_list('PSA_UL_export_sequences', '', pg, 'marker_list', pg, 'marker_list_index', rows=rows)
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
rows = max(3, min(len(pg.nla_strip_list), 10))
layout.template_list('PSA_UL_export_sequences', '', pg, 'nla_strip_list', pg, 'nla_strip_list_index', rows=rows)
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'sequence_name_prefix')
col.prop(pg, 'sequence_name_suffix')
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'sequence_name_prefix')
col.prop(pg, 'sequence_name_suffix')
# Determine if there is going to be a naming conflict and display an error, if so.
selected_items = [x for x in pg.action_list if x.is_selected]
@ -359,6 +355,8 @@ class PSA_OT_export(Operator, ExportHelper):
raise RuntimeError('No actions were selected for export')
elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_list) == 0:
raise RuntimeError('No timeline markers were selected for export')
elif pg.sequence_source == 'NLA_TRACK_STRIPS' and len(pg.nla_strip_list) == 0:
raise RuntimeError('No NLA track strips were selected for export')
# Populate the export sequence list.
animation_data_object = get_animation_data_object(context)
@ -370,29 +368,38 @@ class PSA_OT_export(Operator, ExportHelper):
export_sequences: List[PsaBuildSequence] = []
if pg.sequence_source == 'ACTIONS':
for action in filter(lambda x: x.is_selected, pg.action_list):
if len(action.action.fcurves) == 0:
for action_item in filter(lambda x: x.is_selected, pg.action_list):
if len(action_item.action.fcurves) == 0:
continue
export_sequence = PsaBuildSequence()
export_sequence.nla_state.action = action.action
export_sequence.name = action.name
export_sequence.nla_state.frame_start = action.frame_start
export_sequence.nla_state.frame_end = action.frame_end
export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action.action])
export_sequence.compression_ratio = action.action.psa_export.compression_ratio
export_sequence.key_quota = action.action.psa_export.key_quota
export_sequence.nla_state.action = action_item.action
export_sequence.name = action_item.name
export_sequence.nla_state.frame_start = action_item.frame_start
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.compression_ratio = action_item.action.psa_export.compression_ratio
export_sequence.key_quota = action_item.action.psa_export.key_quota
export_sequences.append(export_sequence)
elif pg.sequence_source == 'TIMELINE_MARKERS':
for marker in pg.marker_list:
for marker_item in filter(lambda x: x.is_selected, pg.marker_list):
export_sequence = PsaBuildSequence()
export_sequence.name = marker.name
export_sequence.name = marker_item.name
export_sequence.nla_state.action = None
export_sequence.nla_state.frame_start = marker.frame_start
export_sequence.nla_state.frame_end = marker.frame_end
export_sequence.nla_state.frame_start = marker_item.frame_start
export_sequence.nla_state.frame_end = marker_item.frame_end
nla_strips_actions = set(
map(lambda x: x.action, get_nla_strips_in_timeframe(animation_data, marker.frame_start, marker.frame_end)))
map(lambda x: x.action, get_nla_strips_in_frame_range(animation_data, marker_item.frame_start, marker_item.frame_end)))
export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, nla_strips_actions)
export_sequences.append(export_sequence)
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list):
export_sequence = PsaBuildSequence()
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_end = nla_strip_item.frame_end
export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [nla_strip_item.action])
export_sequences.append(export_sequence)
else:
raise ValueError(f'Unhandled sequence source: {pg.sequence_source}')
@ -431,6 +438,8 @@ class PSA_OT_export_actions_select_all(Operator):
return pg.action_list
elif pg.sequence_source == 'TIMELINE_MARKERS':
return pg.marker_list
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
return pg.nla_strip_list
return None
@classmethod
@ -462,6 +471,8 @@ class PSA_OT_export_actions_deselect_all(Operator):
return pg.action_list
elif pg.sequence_source == 'TIMELINE_MARKERS':
return pg.marker_list
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
return pg.nla_strip_list
return None
@classmethod

View File

@ -1,10 +1,11 @@
import re
import sys
from fnmatch import fnmatch
from typing import List
from typing import List, Optional
from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty, \
StringProperty
from bpy.types import PropertyGroup, Object, Action
from bpy.types import PropertyGroup, Object, Action, AnimData, Context
from ...types import PSX_PG_bone_collection_list_item
@ -25,7 +26,7 @@ class PSA_PG_export_action_list_item(PropertyGroup):
is_pose_marker: BoolProperty(options={'HIDDEN'})
class PSA_PG_export_timeline_markers(PropertyGroup):
class PSA_PG_export_timeline_markers(PropertyGroup): # TODO: rename this to singular
marker_index: IntProperty()
name: StringProperty()
is_selected: BoolProperty(default=True)
@ -33,6 +34,51 @@ class PSA_PG_export_timeline_markers(PropertyGroup):
frame_end: IntProperty(options={'HIDDEN'})
class PSA_PG_export_nla_strip_list_item(PropertyGroup):
name: StringProperty()
action: PointerProperty(type=Action)
frame_start: FloatProperty()
frame_end: FloatProperty()
is_selected: BoolProperty(default=True)
def nla_track_update_cb(self: 'PSA_PG_export', context: Context) -> None:
self.nla_strip_list.clear()
if context.object is None or context.object.animation_data is None:
return
match = re.match(r'^(\d+).+$', self.nla_track)
self.nla_track_index = int(match.group(1)) if match else -1
if self.nla_track_index >= 0:
nla_track = context.object.animation_data.nla_tracks[self.nla_track_index]
for nla_strip in nla_track.strips:
strip: PSA_PG_export_nla_strip_list_item = self.nla_strip_list.add()
strip.action = nla_strip.action
strip.name = nla_strip.name
strip.frame_start = nla_strip.frame_start
strip.frame_end = nla_strip.frame_end
def get_animation_data(pg: 'PSA_PG_export', context: Context) -> Optional[AnimData]:
animation_data_object = context.object
if pg.should_override_animation_data:
animation_data_object = pg.animation_data_override
return animation_data_object.animation_data if animation_data_object else None
def nla_track_search_cb(self, context: Context, edit_text: str):
pg = getattr(context.scene, 'psa_export')
animation_data = get_animation_data(pg, context)
if animation_data is None:
return
for index, nla_track in enumerate(animation_data.nla_tracks):
yield f'{index} - {nla_track.name}'
def animation_data_override_update_cb(self: 'PSA_PG_export', context: Context):
# Reset NLA track selection
self.nla_track = ''
class PSA_PG_export(PropertyGroup):
root_motion: BoolProperty(
name='Root Motion',
@ -46,10 +92,12 @@ class PSA_PG_export(PropertyGroup):
name='Override Animation Data',
options=empty_set,
default=False,
description='Use the animation data from a different object instead of the selected object'
description='Use the animation data from a different object instead of the selected object',
update=animation_data_override_update_cb,
)
animation_data_override: PointerProperty(
type=Object,
update=animation_data_override_update_cb,
poll=psa_export_property_group_animation_data_override_poll
)
sequence_source: EnumProperty(
@ -58,10 +106,18 @@ class PSA_PG_export(PropertyGroup):
description='',
items=(
('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0),
('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences will be exported using 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: StringProperty(
name='NLA Track',
options=empty_set,
description='',
search=nla_track_search_cb,
update=nla_track_update_cb
)
nla_track_index: IntProperty(name='NLA Track Index', default=-1)
fps_source: EnumProperty(
name='FPS Source',
options=empty_set,
@ -80,6 +136,8 @@ class PSA_PG_export(PropertyGroup):
action_list_index: IntProperty(default=0)
marker_list: CollectionProperty(type=PSA_PG_export_timeline_markers)
marker_list_index: IntProperty(default=0)
nla_strip_list: CollectionProperty(type=PSA_PG_export_nla_strip_list_item)
nla_strip_list_index: IntProperty(default=0)
bone_filter_mode: EnumProperty(
name='Bone Filter',
options=empty_set,
@ -145,7 +203,7 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
if not pg.sequence_filter_asset:
for i, sequence in enumerate(sequences):
if hasattr(sequence, 'action') and sequence.action.asset_data is not None:
if hasattr(sequence, 'action') and sequence.action is not None and sequence.action.asset_data is not None:
flt_flags[i] &= ~bitflag_filter_item
if not pg.sequence_filter_pose_marker:
@ -164,5 +222,6 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
classes = (
PSA_PG_export_action_list_item,
PSA_PG_export_timeline_markers,
PSA_PG_export_nla_strip_list_item,
PSA_PG_export,
)

View File

@ -16,7 +16,7 @@ class PSA_UL_export_sequences(UIList):
item = cast(PSA_PG_export_action_list_item, item)
is_pose_marker = hasattr(item, 'is_pose_marker') and item.is_pose_marker
layout.prop(item, 'is_selected', icon_only=True, text=item.name)
if hasattr(item, 'action') and item.action.asset_data is not None:
if hasattr(item, 'action') and item.action is not None and item.action.asset_data is not None:
layout.label(text='', icon='ASSET_MANAGER')
row = layout.row(align=True)

View File

@ -88,7 +88,11 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
for bone in bones:
psk_bone = Psk.Bone()
psk_bone.name = bytes(bone.name, encoding='windows-1252')
try:
psk_bone.name = bytes(bone.name, encoding='windows-1252')
except UnicodeEncodeError:
raise RuntimeError(
f'Bone name "{bone.name}" contains characters that cannot be encoded in the Windows-1252 codepage')
psk_bone.flags = 0
psk_bone.children_count = 0
@ -129,14 +133,17 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
for material_name in material_names:
psk_material = Psk.Material()
psk_material.name = bytes(material_name, encoding='windows-1252')
try:
psk_material.name = bytes(material_name, encoding='windows-1252')
except UnicodeEncodeError:
raise RuntimeError(f'Material name "{material_name}" contains characters that cannot be encoded in the Windows-1252 codepage')
psk_material.texture_index = len(psk.materials)
psk.materials.append(psk_material)
for input_mesh_object in input_objects.mesh_objects:
# MATERIALS
material_indices = [material_names.index(material.name) for material in input_mesh_object.data.materials]
material_indices = [material_names.index(material_slot.material.name) for material_slot in input_mesh_object.material_slots]
# MESH DATA
if options.use_raw_mesh_data:

View File

@ -22,10 +22,11 @@ def populate_material_list(mesh_objects, material_list):
material_names = []
for mesh_object in mesh_objects:
for i, material in enumerate(mesh_object.data.materials):
for i, material_slot in enumerate(mesh_object.material_slots):
material = material_slot.material
# TODO: put this in the poll arg?
if material is None:
raise RuntimeError('Material cannot be empty (index ' + str(i) + ')')
raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')')
if material.name not in material_names:
material_names.append(material.name)

View File

@ -131,7 +131,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
# Material already exists, just re-use it.
material = bpy.data.materials[material_name]
elif is_bdk_addon_loaded() and psk.has_material_references:
# Material does not yet exist and we have the BDK addon installed.
# Material does not yet exist, and we have the BDK addon installed.
# Attempt to load it using BDK addon's operator.
material_reference = psk.material_references[material_index]
if material_reference and bpy.ops.bdk.link_material(reference=material_reference) == {'FINISHED'}: