Compare commits

..

7 Commits

Author SHA1 Message Date
Yurii Ti
df6bdb96a4 Added a docstring for populate_bone_group_list 2022-05-05 17:41:39 +03:00
Yurii Ti
8495482345 [PSA Export] Select all bone groups if none were previously selected
This brings back the old behavior (before 1ac0870) where all groups
are selected by default.
2022-05-05 16:07:04 +03:00
Yurii Ti
1ac0870b31 [PSA Export] Remember selections for bone groups
* Selections for bone groups are now preserved between exporter
  invocations.

* Added typing hints to the `populate_bone_group_list` function.
2022-05-05 15:02:31 +03:00
Colin Basnett
19ff47cc83 * Added "Root Motion" option to enable root motion on export (vs. stationary root at world origin!)
* Removed performance debugging code
2022-04-30 17:33:59 -07:00
Colin Basnett
31c0ec16ab Fixed a runtime error that would occur if the user attempted to export animations using timeline markers and there were no strips occupying the space between markers 2022-04-29 19:41:15 -07:00
Colin Basnett
a31c3ab2ae Updated the README to better reflect the capabilities of the addon 2022-04-24 22:55:23 -07:00
Colin Basnett
19a8f88686 Merge branch 'feature-original-sequence-names' 2022-04-24 22:08:47 -07:00
4 changed files with 78 additions and 43 deletions

View File

@@ -1,4 +1,13 @@
This Blender add-on allows you to import and export meshes and animations to and from the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats). In addition, the non-standard PSKX format is also supported for import only. This Blender add-on allows you to import and export meshes and animations to and from the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats).
# Features
* Full PSK/PSA import and export capabilities
* Non-standard PSKX file format with vertex normals, extra UV channels and vertex colors is supported for import only
* Fine-grained PSA sequence importing for efficient workflow when working with large PSA files
* Automatic keyframe reduction on PSA import
* PSA sequence metadata (e.g., frame rate, sequence name) is preserved on import, allowing this data to be reused on export
* An armature's [bone groups](https://docs.blender.org/manual/en/latest/animation/armatures/properties/bone_groups.html) can be excluded from PSK/PSA export (useful for excluding non-contributing bones such as IK controllers)
* PSA sequences can be exported directly from actions or delineated using a scene's [timeline markers](https://docs.blender.org/manual/en/latest/animation/markers.html), allowing use of the [NLA](https://docs.blender.org/manual/en/latest/editors/nla/index.html) when creating sequences
# Installation # Installation
1. Download the zip file for the latest version from the [releases](https://github.com/DarklightGames/io_export_psk_psa/releases) page. 1. Download the zip file for the latest version from the [releases](https://github.com/DarklightGames/io_export_psk_psa/releases) page.

View File

@@ -1,8 +1,9 @@
import datetime import datetime
from collections import Counter from collections import Counter
from typing import List from typing import List, Iterable
from bpy.types import NlaStrip from bpy.types import NlaStrip, Object
from .types import BoneGroupListItem
class Timer: class Timer:
@@ -56,7 +57,26 @@ def get_nla_strips_in_timeframe(object, frame_min, frame_max) -> List[NlaStrip]:
return strips return strips
def populate_bone_group_list(armature_object, bone_group_list): def populate_bone_group_list(armature_object: Object, bone_group_list: Iterable[BoneGroupListItem]) -> None:
"""
Updates the bone group collection.
Bone group selections are preserved between updates unless none of the groups were previously selected;
otherwise, all groups are selected by default.
"""
has_selected_groups = any([g.is_selected for g in bone_group_list])
unassigned_group_is_selected, selected_assigned_group_names = True, []
if has_selected_groups:
# Preserve group selections before clearing the list.
# We handle selections for the unassigned group separately to cover the edge case
# where there might be an actual group with 'Unassigned' as its name.
unassigned_group_idx, unassigned_group_is_selected = next(iter([
(i, g.is_selected) for i, g in enumerate(bone_group_list) if g.index == -1]), (-1, False))
selected_assigned_group_names = [
g.name for i, g in enumerate(bone_group_list) if i != unassigned_group_idx and g.is_selected]
bone_group_list.clear() bone_group_list.clear()
if armature_object and armature_object.pose: if armature_object and armature_object.pose:
@@ -66,14 +86,14 @@ def populate_bone_group_list(armature_object, bone_group_list):
item.name = 'Unassigned' item.name = 'Unassigned'
item.index = -1 item.index = -1
item.count = 0 if None not in bone_group_counts else bone_group_counts[None] item.count = 0 if None not in bone_group_counts else bone_group_counts[None]
item.is_selected = True item.is_selected = unassigned_group_is_selected
for bone_group_index, bone_group in enumerate(armature_object.pose.bone_groups): for bone_group_index, bone_group in enumerate(armature_object.pose.bone_groups):
item = bone_group_list.add() item = bone_group_list.add()
item.name = bone_group.name item.name = bone_group.name
item.index = bone_group_index item.index = bone_group_index
item.count = 0 if bone_group not in bone_group_counts else bone_group_counts[bone_group] item.count = 0 if bone_group not in bone_group_counts else bone_group_counts[bone_group]
item.is_selected = True item.is_selected = bone_group.name in selected_assigned_group_names if has_selected_groups else True
def get_psa_sequence_name(action, should_use_original_sequence_name): def get_psa_sequence_name(action, should_use_original_sequence_name):

View File

@@ -1,6 +1,7 @@
from typing import Dict, Iterable from typing import Dict, Iterable
from bpy.types import Action from bpy.types import Action
from mathutils import Matrix
from .data import * from .data import *
from ..helpers import * from ..helpers import *
@@ -19,13 +20,7 @@ class PsaBuilderOptions(object):
self.should_trim_timeline_marker_sequences = True self.should_trim_timeline_marker_sequences = True
self.sequence_name_prefix = '' self.sequence_name_prefix = ''
self.sequence_name_suffix = '' self.sequence_name_suffix = ''
self.root_motion = False
class PsaBuilderPerformance:
def __init__(self):
self.frame_set_duration = datetime.timedelta()
self.key_build_duration = datetime.timedelta()
self.key_add_duration = datetime.timedelta()
class PsaBuilder(object): class PsaBuilder(object):
@@ -53,7 +48,6 @@ class PsaBuilder(object):
raise RuntimeError(f'Invalid FPS source "{options.fps_source}"') raise RuntimeError(f'Invalid FPS source "{options.fps_source}"')
def build(self, context, options: PsaBuilderOptions) -> Psa: def build(self, context, options: PsaBuilderOptions) -> Psa:
performance = PsaBuilderPerformance()
active_object = context.view_layer.objects.active active_object = context.view_layer.objects.active
if active_object.type != 'ARMATURE': if active_object.type != 'ARMATURE':
@@ -201,26 +195,27 @@ class PsaBuilder(object):
frame_count = frame_max - frame_min + 1 frame_count = frame_max - frame_min + 1
for frame in range(frame_count): for frame in range(frame_count):
with Timer() as t:
context.scene.frame_set(frame_min + frame) context.scene.frame_set(frame_min + frame)
performance.frame_set_duration += t.duration
for pose_bone in pose_bones: for pose_bone in pose_bones:
with Timer() as t:
key = Psa.Key() key = Psa.Key()
pose_bone_matrix = pose_bone.matrix
if pose_bone.parent is not None: if pose_bone.parent is not None:
pose_bone_matrix = pose_bone.matrix
pose_bone_parent_matrix = pose_bone.parent.matrix pose_bone_parent_matrix = pose_bone.parent.matrix
pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix
else:
if options.root_motion:
# Export root motion
pose_bone_matrix = armature.matrix_world @ pose_bone.matrix
else:
pose_bone_matrix = pose_bone.matrix
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()
if pose_bone.parent is not None: if pose_bone.parent is not None:
rotation.x = -rotation.x rotation.conjugate()
rotation.y = -rotation.y
rotation.z = -rotation.z
key.location.x = location.x key.location.x = location.x
key.location.y = location.y key.location.y = location.y
@@ -230,11 +225,8 @@ class PsaBuilder(object):
key.rotation.z = rotation.z key.rotation.z = rotation.z
key.rotation.w = rotation.w key.rotation.w = rotation.w
key.time = 1.0 / psa_sequence.fps key.time = 1.0 / psa_sequence.fps
performance.key_build_duration += t.duration
with Timer() as t:
psa.keys.append(key) psa.keys.append(key)
performance.key_add_duration += t.duration
psa_sequence.bone_count = len(pose_bones) psa_sequence.bone_count = len(pose_bones)
psa_sequence.track_time = frame_count psa_sequence.track_time = frame_count
@@ -264,8 +256,12 @@ class PsaBuilder(object):
frame_max = sorted_timeline_markers[next_marker_index].frame frame_max = sorted_timeline_markers[next_marker_index].frame
if options.should_trim_timeline_marker_sequences: if options.should_trim_timeline_marker_sequences:
nla_strips = get_nla_strips_in_timeframe(object, marker.frame, frame_max) nla_strips = get_nla_strips_in_timeframe(object, marker.frame, frame_max)
if len(nla_strips) > 0:
frame_max = min(frame_max, max(map(lambda nla_strip: nla_strip.frame_end, nla_strips))) frame_max = min(frame_max, max(map(lambda nla_strip: nla_strip.frame_end, nla_strips)))
frame_min = max(frame_min, min(map(lambda nla_strip: nla_strip.frame_start, nla_strips))) frame_min = max(frame_min, min(map(lambda nla_strip: nla_strip.frame_start, nla_strips)))
else:
# No strips in between this marker and the next, just export this as a one-frame animation.
frame_max = frame_min
else: else:
# There is no next marker. # There is no next marker.
# Find the final frame of all the NLA strips and use that as the last frame of this sequence. # Find the final frame of all the NLA strips and use that as the last frame of this sequence.

View File

@@ -65,6 +65,12 @@ def should_use_original_sequence_names_updated(_, context):
class PsaExportPropertyGroup(PropertyGroup): class PsaExportPropertyGroup(PropertyGroup):
root_motion: BoolProperty(
name='Root Motion',
options=set(),
default=False,
description='When set, the root bone will be transformed as it appears in the scene',
)
sequence_source: EnumProperty( sequence_source: EnumProperty(
name='Source', name='Source',
options=set(), options=set(),
@@ -165,6 +171,9 @@ class PsaExportOperator(Operator, ExportHelper):
# SOURCE # SOURCE
layout.prop(pg, 'sequence_source', text='Source') layout.prop(pg, 'sequence_source', text='Source')
# ROOT MOTION
layout.prop(pg, 'root_motion', text='Root Motion')
# SELECT ALL/NONE # SELECT ALL/NONE
row = layout.row(align=True) row = layout.row(align=True)
row.label(text='Select') row.label(text='Select')
@@ -297,6 +306,7 @@ class PsaExportOperator(Operator, ExportHelper):
options.should_trim_timeline_marker_sequences = pg.should_trim_timeline_marker_sequences options.should_trim_timeline_marker_sequences = pg.should_trim_timeline_marker_sequences
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
builder = PsaBuilder() builder = PsaBuilder()