2022-05-21 02:13:12 +02:00
|
|
|
import re
|
2023-01-03 04:36:31 +01:00
|
|
|
import typing
|
2022-05-05 14:02:31 +02:00
|
|
|
from typing import List, Iterable
|
2022-04-25 07:08:36 +02:00
|
|
|
|
2023-03-09 10:51:04 +01:00
|
|
|
import addon_utils
|
2022-08-07 08:52:18 +02:00
|
|
|
import bpy.types
|
2023-01-03 04:36:31 +01:00
|
|
|
from bpy.types import NlaStrip, Object, AnimData
|
2022-04-04 09:32:12 +02:00
|
|
|
|
|
|
|
|
2023-01-03 04:36:31 +01:00
|
|
|
def rgb_to_srgb(c: float):
|
2022-01-28 00:11:32 +01:00
|
|
|
if c > 0.0031308:
|
|
|
|
return 1.055 * (pow(c, (1.0 / 2.4))) - 0.055
|
|
|
|
else:
|
|
|
|
return 12.92 * c
|
|
|
|
|
|
|
|
|
2023-08-11 11:09:16 +02:00
|
|
|
def get_nla_strips_in_frame_range(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]:
|
2022-06-28 03:10:37 +02:00
|
|
|
if animation_data is None:
|
2022-02-12 00:21:31 +01:00
|
|
|
return []
|
|
|
|
strips = []
|
2022-06-28 03:10:37 +02:00
|
|
|
for nla_track in animation_data.nla_tracks:
|
2022-04-14 09:07:43 +02:00
|
|
|
if nla_track.mute:
|
|
|
|
continue
|
2022-02-12 00:21:31 +01:00
|
|
|
for strip in nla_track.strips:
|
2022-02-13 04:58:34 +01:00
|
|
|
if (strip.frame_start < frame_min and strip.frame_end > frame_max) or \
|
|
|
|
(frame_min <= strip.frame_start < frame_max) or \
|
|
|
|
(frame_min < strip.frame_end <= frame_max):
|
2022-02-12 00:21:31 +01:00
|
|
|
strips.append(strip)
|
|
|
|
return strips
|
|
|
|
|
|
|
|
|
2023-09-18 06:18:41 +02:00
|
|
|
def populate_bone_collection_list(armature_object: Object, bone_collection_list: bpy.props.CollectionProperty) -> None:
|
2024-01-23 00:23:01 +01:00
|
|
|
'''
|
2023-09-18 06:18:41 +02:00
|
|
|
Updates the bone collections collection.
|
2022-05-05 16:38:22 +02:00
|
|
|
|
2023-09-18 06:18:41 +02:00
|
|
|
Bone collection selections are preserved between updates unless none of the groups were previously selected;
|
|
|
|
otherwise, all collections are selected by default.
|
2024-01-23 00:23:01 +01:00
|
|
|
'''
|
2023-09-18 06:18:41 +02:00
|
|
|
has_selected_collections = any([g.is_selected for g in bone_collection_list])
|
|
|
|
unassigned_collection_is_selected, selected_assigned_collection_names = True, []
|
2022-05-05 14:02:31 +02:00
|
|
|
|
2023-10-19 22:24:06 +02:00
|
|
|
if armature_object is None:
|
|
|
|
return
|
|
|
|
|
2023-09-18 06:18:41 +02:00
|
|
|
if has_selected_collections:
|
2022-05-05 14:52:38 +02:00
|
|
|
# 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.
|
2023-09-18 06:18:41 +02:00
|
|
|
unassigned_collection_idx, unassigned_collection_is_selected = next(iter([
|
|
|
|
(i, g.is_selected) for i, g in enumerate(bone_collection_list) if g.index == -1]), (-1, False))
|
2022-05-05 14:52:38 +02:00
|
|
|
|
2023-09-18 06:18:41 +02:00
|
|
|
selected_assigned_collection_names = [
|
|
|
|
g.name for i, g in enumerate(bone_collection_list) if i != unassigned_collection_idx and g.is_selected]
|
2022-05-05 14:02:31 +02:00
|
|
|
|
2023-09-18 06:18:41 +02:00
|
|
|
bone_collection_list.clear()
|
2022-01-23 03:11:41 +01:00
|
|
|
|
2023-09-18 06:18:41 +02:00
|
|
|
armature = armature_object.data
|
2022-02-12 00:21:31 +01:00
|
|
|
|
2023-09-18 06:18:41 +02:00
|
|
|
if armature is None:
|
|
|
|
return
|
2022-02-12 00:21:31 +01:00
|
|
|
|
2023-09-18 06:18:41 +02:00
|
|
|
item = bone_collection_list.add()
|
|
|
|
item.name = 'Unassigned'
|
|
|
|
item.index = -1
|
|
|
|
# Count the number of bones without an assigned bone collection
|
|
|
|
item.count = sum(map(lambda bone: 1 if len(bone.collections) == 0 else 0, armature.bones))
|
|
|
|
item.is_selected = unassigned_collection_is_selected
|
|
|
|
|
|
|
|
for bone_collection_index, bone_collection in enumerate(armature.collections):
|
|
|
|
item = bone_collection_list.add()
|
|
|
|
item.name = bone_collection.name
|
|
|
|
item.index = bone_collection_index
|
|
|
|
item.count = len(bone_collection.bones)
|
|
|
|
item.is_selected = bone_collection.name in selected_assigned_collection_names if has_selected_collections else True
|
2022-01-23 03:11:41 +01:00
|
|
|
|
|
|
|
|
2022-05-21 02:13:12 +02:00
|
|
|
def check_bone_names(bone_names: Iterable[str]):
|
2022-11-13 01:26:40 +01:00
|
|
|
pattern = re.compile(r'^[a-zA-Z\d_\- ]+$')
|
2022-05-21 02:13:12 +02:00
|
|
|
invalid_bone_names = [x for x in bone_names if pattern.match(x) is None]
|
|
|
|
if len(invalid_bone_names) > 0:
|
|
|
|
raise RuntimeError(f'The following bone names are invalid: {invalid_bone_names}.\n'
|
2023-07-30 05:51:23 +02:00
|
|
|
f'Bone names must only contain letters, numbers, spaces, hyphens and underscores.\n'
|
|
|
|
f'You can bypass this by disabling "Enforce Bone Name Restrictions" in the export settings.')
|
2022-05-21 02:13:12 +02:00
|
|
|
|
|
|
|
|
2023-09-18 06:18:41 +02:00
|
|
|
def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_collection_indices: List[int]) -> List[str]:
|
2024-01-23 00:23:01 +01:00
|
|
|
'''
|
2023-09-18 06:18:41 +02:00
|
|
|
Returns a sorted list of bone indices that should be exported for the given bone filter mode and bone collections.
|
2022-01-23 03:11:41 +01:00
|
|
|
|
2023-09-18 06:18:41 +02:00
|
|
|
Note that the ancestors of bones within the bone collections will also be present in the returned list.
|
2022-01-23 03:11:41 +01:00
|
|
|
|
|
|
|
:param armature_object: Blender object with type 'ARMATURE'
|
2023-09-18 06:18:41 +02:00
|
|
|
:param bone_filter_mode: One of ['ALL', 'BONE_COLLECTIONS']
|
|
|
|
:param bone_collection_indices: List of bone collection indices to be exported.
|
2022-01-23 03:11:41 +01:00
|
|
|
:return: A sorted list of bone indices that should be exported.
|
2024-01-23 00:23:01 +01:00
|
|
|
'''
|
2022-01-23 03:11:41 +01:00
|
|
|
if armature_object is None or armature_object.type != 'ARMATURE':
|
|
|
|
raise ValueError('An armature object must be supplied')
|
|
|
|
|
2023-01-03 04:36:31 +01:00
|
|
|
armature_data = typing.cast(bpy.types.Armature, armature_object.data)
|
|
|
|
bones = armature_data.bones
|
2022-01-23 03:11:41 +01:00
|
|
|
bone_names = [x.name for x in bones]
|
|
|
|
|
2022-02-12 10:45:19 +01:00
|
|
|
# Get a list of the bone indices that we are explicitly including.
|
2022-01-23 03:11:41 +01:00
|
|
|
bone_index_stack = []
|
2023-09-18 06:18:41 +02:00
|
|
|
is_exporting_unassigned_bone_collections = -1 in bone_collection_indices
|
|
|
|
bone_collections = list(armature_data.collections)
|
|
|
|
|
|
|
|
for bone_index, bone in enumerate(bones):
|
|
|
|
# Check if this bone is in any of the collections in the bone collection indices list.
|
|
|
|
this_bone_collection_indices = set(bone_collections.index(x) for x in bone.collections)
|
|
|
|
is_in_exported_bone_collections = len(set(bone_collection_indices).intersection(this_bone_collection_indices)) > 0
|
|
|
|
|
2022-02-12 10:45:19 +01:00
|
|
|
if bone_filter_mode == 'ALL' or \
|
2023-09-18 06:18:41 +02:00
|
|
|
(len(bone.collections) == 0 and is_exporting_unassigned_bone_collections) or \
|
|
|
|
is_in_exported_bone_collections:
|
2022-02-12 10:45:19 +01:00
|
|
|
bone_index_stack.append((bone_index, None))
|
2022-01-23 03:11:41 +01:00
|
|
|
|
|
|
|
# For each bone that is explicitly being added, recursively walk up the hierarchy and ensure that all of
|
|
|
|
# those ancestor bone indices are also in the list.
|
2022-02-12 10:45:19 +01:00
|
|
|
bone_indices = dict()
|
2022-01-23 03:11:41 +01:00
|
|
|
while len(bone_index_stack) > 0:
|
2022-02-12 10:45:19 +01:00
|
|
|
bone_index, instigator_bone_index = bone_index_stack.pop()
|
2022-01-23 03:11:41 +01:00
|
|
|
bone = bones[bone_index]
|
|
|
|
if bone.parent is not None:
|
|
|
|
parent_bone_index = bone_names.index(bone.parent.name)
|
|
|
|
if parent_bone_index not in bone_indices:
|
2022-02-12 10:45:19 +01:00
|
|
|
bone_index_stack.append((parent_bone_index, bone_index))
|
|
|
|
bone_indices[bone_index] = instigator_bone_index
|
|
|
|
|
|
|
|
# Sort the bone index list in-place.
|
|
|
|
bone_indices = [(x[0], x[1]) for x in bone_indices.items()]
|
|
|
|
bone_indices.sort(key=lambda x: x[0])
|
|
|
|
|
|
|
|
# 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.
|
2023-08-26 01:56:59 +02:00
|
|
|
# For example, users of this function may modify bone lists, which would invalidate the indices and require an
|
2022-02-12 10:45:19 +01:00
|
|
|
# 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]
|
|
|
|
|
|
|
|
# Ensure that the hierarchy we are sending back has a single root bone.
|
|
|
|
bone_indices = [x[0] for x in bone_indices]
|
|
|
|
root_bones = [bones[bone_index] for bone_index in bone_indices if bones[bone_index].parent is None]
|
|
|
|
if len(root_bones) > 1:
|
|
|
|
# There is more than one root bone.
|
|
|
|
# Print out why each root bone was included by linking it to one of the explicitly included bones.
|
|
|
|
root_bone_names = [bone.name for bone in root_bones]
|
|
|
|
for root_bone_name in root_bone_names:
|
|
|
|
bone_name = root_bone_name
|
|
|
|
while True:
|
|
|
|
# Traverse the instigator chain until the end to find the true instigator bone.
|
|
|
|
# TODO: in future, it would be preferential to have a readout of *all* instigator bones.
|
|
|
|
instigator_bone_name = instigator_bone_names[bone_names.index(bone_name)]
|
|
|
|
if instigator_bone_name is None:
|
|
|
|
print(f'Root bone "{root_bone_name}" was included because {bone_name} was marked for export')
|
|
|
|
break
|
|
|
|
bone_name = instigator_bone_name
|
|
|
|
|
|
|
|
raise RuntimeError('Exported bone hierarchy must have a single root bone.\n'
|
|
|
|
f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}.\n'
|
|
|
|
f'Additional debugging information has been written to the console.')
|
|
|
|
|
|
|
|
return bone_names
|
2023-03-09 10:51:04 +01:00
|
|
|
|
|
|
|
|
|
|
|
def is_bdk_addon_loaded():
|
|
|
|
return addon_utils.check('bdk_addon')[1]
|