Compare commits
7 Commits
8.0.0
...
sampling-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b41815545 | ||
|
|
a83314c8b3 | ||
|
|
f8234b3892 | ||
|
|
35ac0bf86c | ||
|
|
515ee17203 | ||
|
|
0a5ebf4548 | ||
|
|
de1cf2316a |
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
@@ -30,7 +30,8 @@ jobs:
|
|||||||
sudo apt-get install libxfixes3 -y
|
sudo apt-get install libxfixes3 -y
|
||||||
sudo apt-get install libxi-dev -y
|
sudo apt-get install libxi-dev -y
|
||||||
sudo apt-get install libxkbcommon-x11-0 -y
|
sudo apt-get install libxkbcommon-x11-0 -y
|
||||||
sudo apt-get install libgl1-mesa-glx -y
|
sudo apt-get install libgl1 -y
|
||||||
|
sudo apt-get install libglx-mesa0 -y
|
||||||
- name: Download & Extract Blender
|
- name: Download & Extract Blender
|
||||||
run: |
|
run: |
|
||||||
wget -q $BLENDER_URL
|
wget -q $BLENDER_URL
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
id = "io_scene_psk_psa"
|
id = "io_scene_psk_psa"
|
||||||
version = "7.1.3"
|
version = "8.0.0"
|
||||||
name = "Unreal PSK/PSA (.psk/.psa)"
|
name = "Unreal PSK/PSA (.psk/.psa)"
|
||||||
tagline = "Import and export PSK and PSA files used in Unreal Engine"
|
tagline = "Import and export PSK and PSA files used in Unreal Engine"
|
||||||
maintainer = "Colin Basnett <cmbasnett@gmail.com>"
|
maintainer = "Colin Basnett <cmbasnett@gmail.com>"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from bpy.types import Bone, Action, PoseBone
|
from bpy.types import Bone, Action, PoseBone
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
from .data import *
|
from .data import *
|
||||||
from ..shared.helpers import *
|
from ..shared.helpers import *
|
||||||
@@ -33,6 +34,7 @@ class PsaBuildOptions:
|
|||||||
self.sequence_name_suffix: str = ''
|
self.sequence_name_suffix: str = ''
|
||||||
self.root_motion: bool = False
|
self.root_motion: bool = False
|
||||||
self.scale = 1.0
|
self.scale = 1.0
|
||||||
|
self.sampling_mode: str = 'INTERPOLATED' # One of ('INTERPOLATED', 'SUBFRAME')
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
@@ -184,24 +186,83 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
|
|
||||||
frame = float(frame_start)
|
frame = float(frame_start)
|
||||||
|
|
||||||
for _ in range(frame_count):
|
def add_key(location: Vector, rotation: Quaternion):
|
||||||
context.scene.frame_set(frame=int(frame), subframe=frame % 1.0)
|
key = Psa.Key()
|
||||||
|
key.location.x = location.x
|
||||||
|
key.location.y = location.y
|
||||||
|
key.location.z = location.z
|
||||||
|
key.rotation.x = rotation.x
|
||||||
|
key.rotation.y = rotation.y
|
||||||
|
key.rotation.z = rotation.z
|
||||||
|
key.rotation.w = rotation.w
|
||||||
|
key.time = 1.0 / psa_sequence.fps
|
||||||
|
psa.keys.append(key)
|
||||||
|
|
||||||
for pose_bone in pose_bones:
|
match options.sampling_mode:
|
||||||
location, rotation = _get_pose_bone_location_and_rotation(pose_bone, export_sequence.armature_object, options)
|
case 'INTERPOLATED':
|
||||||
|
# Used as a store for the last frame's pose bone locations and rotations.
|
||||||
|
last_frame: Optional[int] = None
|
||||||
|
last_frame_bone_poses: List[Tuple[Vector, Quaternion]] = []
|
||||||
|
|
||||||
key = Psa.Key()
|
next_frame: Optional[int] = None
|
||||||
key.location.x = location.x
|
next_frame_bone_poses: List[Tuple[Vector, Quaternion]] = []
|
||||||
key.location.y = location.y
|
|
||||||
key.location.z = location.z
|
|
||||||
key.rotation.x = rotation.x
|
|
||||||
key.rotation.y = rotation.y
|
|
||||||
key.rotation.z = rotation.z
|
|
||||||
key.rotation.w = rotation.w
|
|
||||||
key.time = 1.0 / psa_sequence.fps
|
|
||||||
psa.keys.append(key)
|
|
||||||
|
|
||||||
frame += frame_step
|
for _ in range(frame_count):
|
||||||
|
if last_frame is None or last_frame != int(frame):
|
||||||
|
# Populate the bone poses for frame A.
|
||||||
|
last_frame = int(frame)
|
||||||
|
|
||||||
|
# TODO: simplify this code and make it easier to follow!
|
||||||
|
if next_frame == last_frame:
|
||||||
|
# Simply transfer the data from next_frame to the last_frame so that we don't need to
|
||||||
|
# resample anything.
|
||||||
|
last_frame_bone_poses = next_frame_bone_poses.copy()
|
||||||
|
else:
|
||||||
|
last_frame_bone_poses.clear()
|
||||||
|
context.scene.frame_set(frame=last_frame)
|
||||||
|
for pose_bone in pose_bones:
|
||||||
|
location, rotation = _get_pose_bone_location_and_rotation(pose_bone,
|
||||||
|
export_sequence.armature_object,
|
||||||
|
options)
|
||||||
|
last_frame_bone_poses.append((location, rotation))
|
||||||
|
|
||||||
|
next_frame = None
|
||||||
|
next_frame_bone_poses.clear()
|
||||||
|
|
||||||
|
# If this is not a subframe, just use the last frame's bone poses.
|
||||||
|
if frame % 1.0 == 0:
|
||||||
|
for i in range(len(pose_bones)):
|
||||||
|
add_key(*last_frame_bone_poses[i])
|
||||||
|
else:
|
||||||
|
# Otherwise, this is a subframe, so we need to interpolate the pose between the next frame and the last frame.
|
||||||
|
if next_frame is None:
|
||||||
|
next_frame = last_frame + 1
|
||||||
|
context.scene.frame_set(frame=next_frame)
|
||||||
|
for pose_bone in pose_bones:
|
||||||
|
location, rotation = _get_pose_bone_location_and_rotation(pose_bone, export_sequence.armature_object, options)
|
||||||
|
next_frame_bone_poses.append((location, rotation))
|
||||||
|
|
||||||
|
factor = frame % 1.0
|
||||||
|
|
||||||
|
for i in range(len(pose_bones)):
|
||||||
|
last_location, last_rotation = last_frame_bone_poses[i]
|
||||||
|
next_location, next_rotation = next_frame_bone_poses[i]
|
||||||
|
|
||||||
|
location = last_location.lerp(next_location, factor)
|
||||||
|
rotation = last_rotation.slerp(next_rotation, factor)
|
||||||
|
|
||||||
|
add_key(location, rotation)
|
||||||
|
|
||||||
|
frame += frame_step
|
||||||
|
case 'SUBFRAME':
|
||||||
|
for _ in range(frame_count):
|
||||||
|
context.scene.frame_set(frame=int(frame), subframe=frame % 1.0)
|
||||||
|
|
||||||
|
for pose_bone in pose_bones:
|
||||||
|
location, rotation = _get_pose_bone_location_and_rotation(pose_bone, export_sequence.armature_object, options)
|
||||||
|
add_key(location, rotation)
|
||||||
|
|
||||||
|
frame += frame_step
|
||||||
|
|
||||||
frame_start_index += frame_count
|
frame_start_index += frame_count
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import re
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import List, Iterable, Dict, Tuple, cast, Optional
|
from typing import List, Iterable, Dict, Tuple, cast, Optional
|
||||||
|
|
||||||
@@ -12,7 +11,7 @@ from .properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_se
|
|||||||
get_sequences_from_name_and_frame_range
|
get_sequences_from_name_and_frame_range
|
||||||
from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions
|
from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions
|
||||||
from ..writer import write_psa
|
from ..writer import write_psa
|
||||||
from ...shared.helpers import populate_bone_collection_list, get_nla_strips_in_frame_range
|
from ...shared.helpers import populate_bone_collection_list, get_nla_strips_in_frame_range, SemanticVersion
|
||||||
from ...shared.ui import draw_bone_filter_mode
|
from ...shared.ui import draw_bone_filter_mode
|
||||||
|
|
||||||
|
|
||||||
@@ -33,14 +32,27 @@ def get_sequences_propnames_from_source(sequence_source: str) -> Optional[Tuple[
|
|||||||
def is_action_for_armature(armature: Armature, action: Action):
|
def is_action_for_armature(armature: Armature, action: Action):
|
||||||
if len(action.fcurves) == 0:
|
if len(action.fcurves) == 0:
|
||||||
return False
|
return False
|
||||||
bone_names = set([x.name for x in armature.bones])
|
|
||||||
for fcurve in action.fcurves:
|
version = SemanticVersion(bpy.app.version)
|
||||||
match = re.match(r'pose\.bones\[\"([^\"]+)\"](\[\"([^\"]+)\"])?', fcurve.data_path)
|
|
||||||
if not match:
|
if version < SemanticVersion((4, 4, 0)):
|
||||||
continue
|
import re
|
||||||
bone_name = match.group(1)
|
bone_names = set([x.name for x in armature.bones])
|
||||||
if bone_name in bone_names:
|
for fcurve in action.fcurves:
|
||||||
return True
|
match = re.match(r'pose\.bones\[\"([^\"]+)\"](\[\"([^\"]+)\"])?', fcurve.data_path)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
bone_name = match.group(1)
|
||||||
|
if bone_name in bone_names:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Look up the armature by ID and check if its data block pointer matches the armature.
|
||||||
|
for slot in filter(lambda x: x.id_root == 'OBJECT', action.slots):
|
||||||
|
# Lop off the 'OB' prefix from the identifier for the lookup.
|
||||||
|
object = bpy.data.objects.get(slot.identifier[2:], None)
|
||||||
|
if object and object.data == armature:
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -137,6 +149,17 @@ def get_sequence_fps(context: Context, fps_source: str, fps_custom: float, actio
|
|||||||
raise RuntimeError(f'Invalid FPS source "{fps_source}"')
|
raise RuntimeError(f'Invalid FPS source "{fps_source}"')
|
||||||
|
|
||||||
|
|
||||||
|
def get_sequence_compression_ratio(compression_ratio_source: str, compression_ratio_custom: float, actions: Iterable[Action]) -> float:
|
||||||
|
match compression_ratio_source:
|
||||||
|
case 'ACTION_METADATA':
|
||||||
|
# Get the minimum value of action metadata compression ratio values.
|
||||||
|
return min([action.psa_export.compression_ratio for action in actions])
|
||||||
|
case 'CUSTOM':
|
||||||
|
return compression_ratio_custom
|
||||||
|
case _:
|
||||||
|
raise RuntimeError(f'Invalid compression ratio source "{compression_ratio_source}"')
|
||||||
|
|
||||||
|
|
||||||
def get_animation_data_object(context: Context) -> Object:
|
def get_animation_data_object(context: Context) -> Object:
|
||||||
pg: PSA_PG_export = getattr(context.scene, 'psa_export')
|
pg: PSA_PG_export = getattr(context.scene, 'psa_export')
|
||||||
|
|
||||||
@@ -285,25 +308,45 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
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()
|
name_header, name_panel = layout.panel('Name', default_closed=False)
|
||||||
flow.use_property_split = True
|
name_header.label(text='Name')
|
||||||
flow.use_property_decorate = False
|
if name_panel:
|
||||||
flow.prop(pg, 'sequence_name_prefix', text='Name Prefix')
|
flow = name_panel.grid_flow()
|
||||||
flow.prop(pg, 'sequence_name_suffix')
|
flow.use_property_split = True
|
||||||
|
flow.use_property_decorate = False
|
||||||
|
flow.prop(pg, 'sequence_name_prefix', text='Name Prefix')
|
||||||
|
flow.prop(pg, 'sequence_name_suffix')
|
||||||
|
|
||||||
# Determine if there is going to be a naming conflict and display an error, if so.
|
# 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]
|
selected_items = [x for x in pg.action_list if x.is_selected]
|
||||||
action_names = [x.name for x in selected_items]
|
action_names = [x.name for x in selected_items]
|
||||||
action_name_counts = Counter(action_names)
|
action_name_counts = Counter(action_names)
|
||||||
for action_name, count in action_name_counts.items():
|
for action_name, count in action_name_counts.items():
|
||||||
if count > 1:
|
if count > 1:
|
||||||
layout.label(text=f'Duplicate action: {action_name}', icon='ERROR')
|
layout.label(text=f'Duplicate action: {action_name}', icon='ERROR')
|
||||||
break
|
break
|
||||||
|
|
||||||
# FPS
|
sampling_header, sampling_panel = layout.panel('Data Source', default_closed=False)
|
||||||
flow.prop(pg, 'fps_source')
|
sampling_header.label(text='Sampling')
|
||||||
if pg.fps_source == 'CUSTOM':
|
if sampling_panel:
|
||||||
flow.prop(pg, 'fps_custom', text='Custom')
|
flow = sampling_panel.grid_flow()
|
||||||
|
flow.use_property_split = True
|
||||||
|
flow.use_property_decorate = False
|
||||||
|
|
||||||
|
# SAMPLING MODE
|
||||||
|
flow.prop(pg, 'sampling_mode', text='Sampling Mode')
|
||||||
|
|
||||||
|
# FPS
|
||||||
|
col = flow.row(align=True)
|
||||||
|
col.prop(pg, 'fps_source', text='FPS')
|
||||||
|
if pg.fps_source == 'CUSTOM':
|
||||||
|
col.prop(pg, 'fps_custom', text='')
|
||||||
|
|
||||||
|
# COMPRESSION RATIO
|
||||||
|
col = flow.row(align=True)
|
||||||
|
col.prop(pg, 'compression_ratio_source', text='Compression Ratio')
|
||||||
|
if pg.compression_ratio_source == 'CUSTOM':
|
||||||
|
col.prop(pg, 'compression_ratio_custom', text='')
|
||||||
|
|
||||||
# BONES
|
# BONES
|
||||||
bones_header, bones_panel = layout.panel('Bones', default_closed=False)
|
bones_header, bones_panel = layout.panel('Bones', default_closed=False)
|
||||||
@@ -350,6 +393,9 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
f'\The armature data block for "{obj.name}\" (\'{obj.data.name}\') does not match '
|
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}\')')
|
f'the active armature data block (\'{context.view_layer.objects.active.name}\')')
|
||||||
|
|
||||||
|
if context.scene.is_nla_tweakmode:
|
||||||
|
raise RuntimeError('Cannot export PSA while in NLA tweak mode')
|
||||||
|
|
||||||
def invoke(self, context, _event):
|
def invoke(self, context, _event):
|
||||||
try:
|
try:
|
||||||
self._check_context(context)
|
self._check_context(context)
|
||||||
@@ -406,7 +452,7 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
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])
|
||||||
export_sequence.compression_ratio = action_item.action.psa_export.compression_ratio
|
export_sequence.compression_ratio = get_sequence_compression_ratio(pg.compression_ratio_source, pg.compression_ratio_custom, [action_item.action])
|
||||||
export_sequence.key_quota = action_item.action.psa_export.key_quota
|
export_sequence.key_quota = action_item.action.psa_export.key_quota
|
||||||
export_sequences.append(export_sequence)
|
export_sequences.append(export_sequence)
|
||||||
case 'TIMELINE_MARKERS':
|
case 'TIMELINE_MARKERS':
|
||||||
@@ -418,6 +464,7 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
nla_strips_actions = set(
|
nla_strips_actions = set(
|
||||||
map(lambda x: x.action, get_nla_strips_in_frame_range(animation_data, marker_item.frame_start, marker_item.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_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, nla_strips_actions)
|
||||||
|
export_sequence.compression_ratio = get_sequence_compression_ratio(pg.compression_ratio_source, pg.compression_ratio_custom, nla_strips_actions)
|
||||||
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):
|
||||||
@@ -426,7 +473,7 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
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 = get_sequence_compression_ratio(pg.compression_ratio_source, pg.compression_ratio_custom, [nla_strip_item.action])
|
||||||
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':
|
case 'ACTIVE_ACTION':
|
||||||
@@ -438,7 +485,7 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
export_sequence.nla_state.frame_start = int(action.frame_range[0])
|
export_sequence.nla_state.frame_start = int(action.frame_range[0])
|
||||||
export_sequence.nla_state.frame_end = int(action.frame_range[1])
|
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.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action])
|
||||||
export_sequence.compression_ratio = action.psa_export.compression_ratio
|
export_sequence.compression_ratio = get_sequence_compression_ratio(pg.compression_ratio_source, pg.compression_ratio_custom, [action])
|
||||||
export_sequence.key_quota = action.psa_export.key_quota
|
export_sequence.key_quota = action.psa_export.key_quota
|
||||||
export_sequences.append(export_sequence)
|
export_sequences.append(export_sequence)
|
||||||
case _:
|
case _:
|
||||||
@@ -453,6 +500,7 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
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
|
options.scale = pg.scale
|
||||||
|
options.sampling_mode = pg.sampling_mode
|
||||||
|
|
||||||
try:
|
try:
|
||||||
psa = build_psa(context, options)
|
psa = build_psa(context, options)
|
||||||
|
|||||||
@@ -156,6 +156,16 @@ class PSA_PG_export(PropertyGroup):
|
|||||||
)
|
)
|
||||||
fps_custom: FloatProperty(default=30.0, min=sys.float_info.epsilon, soft_min=1.0, options=empty_set, step=100,
|
fps_custom: FloatProperty(default=30.0, min=sys.float_info.epsilon, soft_min=1.0, options=empty_set, step=100,
|
||||||
soft_max=60.0)
|
soft_max=60.0)
|
||||||
|
compression_ratio_source: EnumProperty(
|
||||||
|
name='Compression Ratio Source',
|
||||||
|
options=empty_set,
|
||||||
|
description='',
|
||||||
|
items=(
|
||||||
|
('ACTION_METADATA', 'Action Metadata', 'The compression ratio will be determined by action\'s Compression Ratio property found in the PSA Export panel.\n\nIf the Sequence Source is Timeline Markers, the lowest value of all contributing actions will be used', 'ACTION', 1),
|
||||||
|
('CUSTOM', 'Custom', '', 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
compression_ratio_custom: FloatProperty(default=1.0, min=0.0, max=1.0, subtype='FACTOR', description='The key sampling ratio of the exported sequence.\n\nA compression ratio of 1.0 will export all frames, while a compression ratio of 0.5 will export half of the frames')
|
||||||
action_list: CollectionProperty(type=PSA_PG_export_action_list_item)
|
action_list: CollectionProperty(type=PSA_PG_export_action_list_item)
|
||||||
action_list_index: IntProperty(default=0)
|
action_list_index: IntProperty(default=0)
|
||||||
marker_list: CollectionProperty(type=PSA_PG_export_timeline_markers)
|
marker_list: CollectionProperty(type=PSA_PG_export_timeline_markers)
|
||||||
@@ -207,6 +217,16 @@ class PSA_PG_export(PropertyGroup):
|
|||||||
min=0.0,
|
min=0.0,
|
||||||
soft_max=100.0
|
soft_max=100.0
|
||||||
)
|
)
|
||||||
|
sampling_mode: EnumProperty(
|
||||||
|
name='Sampling Mode',
|
||||||
|
options=empty_set,
|
||||||
|
description='The method by which frames are sampled',
|
||||||
|
items=(
|
||||||
|
('INTERPOLATED', 'Interpolated', 'Sampling is performed by interpolating the evaluated bone poses from the adjacent whole frames.', 'INTERPOLATED', 0),
|
||||||
|
('SUBFRAME', 'Subframe', 'Sampling is performed by evaluating the bone poses at the subframe time.\n\nNot recommended unless you are also animating with subframes enabled.', 'SUBFRAME', 1),
|
||||||
|
),
|
||||||
|
default='INTERPOLATED'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ class PSK_OT_export_collection(Operator, ExportHelper):
|
|||||||
bones_header, bones_panel = layout.panel('Bones', default_closed=False)
|
bones_header, bones_panel = layout.panel('Bones', default_closed=False)
|
||||||
bones_header.label(text='Bones', icon='BONE_DATA')
|
bones_header.label(text='Bones', icon='BONE_DATA')
|
||||||
if bones_panel:
|
if bones_panel:
|
||||||
draw_bone_filter_mode(bones_panel, self)
|
draw_bone_filter_mode(bones_panel, self, True)
|
||||||
if self.bone_filter_mode == 'BONE_COLLECTIONS':
|
if self.bone_filter_mode == 'BONE_COLLECTIONS':
|
||||||
bones_panel.operator(PSK_OT_populate_bone_collection_list.bl_idname, icon='FILE_REFRESH')
|
bones_panel.operator(PSK_OT_populate_bone_collection_list.bl_idname, icon='FILE_REFRESH')
|
||||||
rows = max(3, min(len(self.bone_collection_list), 10))
|
rows = max(3, min(len(self.bone_collection_list), 10))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import List, Iterable, cast
|
from typing import List, Iterable, cast, Tuple
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import CollectionProperty
|
from bpy.props import CollectionProperty
|
||||||
@@ -63,7 +63,7 @@ def populate_bone_collection_list(armature_object: Object, bone_collection_list:
|
|||||||
item.count = sum(map(lambda bone: 1 if len(bone.collections) == 0 else 0, armature.bones))
|
item.count = sum(map(lambda bone: 1 if len(bone.collections) == 0 else 0, armature.bones))
|
||||||
item.is_selected = unassigned_collection_is_selected
|
item.is_selected = unassigned_collection_is_selected
|
||||||
|
|
||||||
for bone_collection_index, bone_collection in enumerate(armature.collections):
|
for bone_collection_index, bone_collection in enumerate(armature.collections_all):
|
||||||
item = bone_collection_list.add()
|
item = bone_collection_list.add()
|
||||||
item.name = bone_collection.name
|
item.name = bone_collection.name
|
||||||
item.index = bone_collection_index
|
item.index = bone_collection_index
|
||||||
@@ -92,7 +92,7 @@ def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_c
|
|||||||
# Get a list of the bone indices that we are explicitly including.
|
# Get a list of the bone indices that we are explicitly including.
|
||||||
bone_index_stack = []
|
bone_index_stack = []
|
||||||
is_exporting_unassigned_bone_collections = -1 in bone_collection_indices
|
is_exporting_unassigned_bone_collections = -1 in bone_collection_indices
|
||||||
bone_collections = list(armature_data.collections)
|
bone_collections = list(armature_data.collections_all)
|
||||||
|
|
||||||
for bone_index, bone in enumerate(bones):
|
for bone_index, bone in enumerate(bones):
|
||||||
# Check if this bone is in any of the collections in the bone collection indices list.
|
# Check if this bone is in any of the collections in the bone collection indices list.
|
||||||
@@ -154,3 +154,57 @@ def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_c
|
|||||||
|
|
||||||
def is_bdk_addon_loaded() -> bool:
|
def is_bdk_addon_loaded() -> bool:
|
||||||
return 'bdk' in dir(bpy.ops)
|
return 'bdk' in dir(bpy.ops)
|
||||||
|
|
||||||
|
|
||||||
|
class SemanticVersion(object):
|
||||||
|
def __init__(self, version: Tuple[int, int, int]):
|
||||||
|
self.major, self.minor, self.patch = version
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield self.major
|
||||||
|
yield self.minor
|
||||||
|
yield self.patch
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def compare(lhs: 'SemanticVersion', rhs: 'SemanticVersion') -> int:
|
||||||
|
"""
|
||||||
|
Compares two semantic versions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-1 if lhs < rhs
|
||||||
|
0 if lhs == rhs
|
||||||
|
1 if lhs > rhs
|
||||||
|
"""
|
||||||
|
for l, r in zip(lhs, rhs):
|
||||||
|
if l < r:
|
||||||
|
return -1
|
||||||
|
if l > r:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.major}.{self.minor}.{self.patch}'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.compare(self, other) == 0
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self == other
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return self.compare(self, other) == -1
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return self.compare(self, other) <= 0
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
return self.compare(self, other) == 1
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self.compare(self, other) >= 0
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.major, self.minor, self.patch))
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ def is_bone_filter_mode_item_available(pg, identifier):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def draw_bone_filter_mode(layout: UILayout, pg):
|
def draw_bone_filter_mode(layout: UILayout, pg, should_always_show_bone_collections=False):
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
for item_identifier, _, _ in bone_filter_mode_items:
|
for item_identifier, _, _ in bone_filter_mode_items:
|
||||||
identifier = item_identifier
|
identifier = item_identifier
|
||||||
item_layout = row.row(align=True)
|
item_layout = row.row(align=True)
|
||||||
item_layout.prop_enum(pg, 'bone_filter_mode', item_identifier)
|
item_layout.prop_enum(pg, 'bone_filter_mode', item_identifier)
|
||||||
item_layout.enabled = is_bone_filter_mode_item_available(pg, identifier)
|
item_layout.enabled = should_always_show_bone_collections or is_bone_filter_mode_item_available(pg, identifier)
|
||||||
|
|||||||
Reference in New Issue
Block a user