Compare commits
7 Commits
ue1-exec-c
...
5.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
122e765bca | ||
|
|
4db8764677 | ||
|
|
f185ffbe16 | ||
|
|
3d460a15e3 | ||
|
|
da39c14464 | ||
|
|
83e65687ac | ||
|
|
63fb6f7d09 |
@@ -1,7 +1,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "PSK/PSA Importer/Exporter",
|
"name": "PSK/PSA Importer/Exporter",
|
||||||
"author": "Colin Basnett, Yurii Ti",
|
"author": "Colin Basnett, Yurii Ti",
|
||||||
"version": (5, 0, 3),
|
"version": (5, 0, 6),
|
||||||
"blender": (3, 4, 0),
|
"blender": (3, 4, 0),
|
||||||
"description": "PSK/PSA Import/Export (.psk/.psa)",
|
"description": "PSK/PSA Import/Export (.psk/.psa)",
|
||||||
"warning": "",
|
"warning": "",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import datetime
|
||||||
import re
|
import re
|
||||||
import typing
|
import typing
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
@@ -8,6 +9,24 @@ import bpy.types
|
|||||||
from bpy.types import NlaStrip, Object, AnimData
|
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):
|
def rgb_to_srgb(c: float):
|
||||||
if c > 0.0031308:
|
if c > 0.0031308:
|
||||||
return 1.055 * (pow(c, (1.0 / 2.4))) - 0.055
|
return 1.055 * (pow(c, (1.0 / 2.4))) - 0.055
|
||||||
@@ -15,7 +34,7 @@ def rgb_to_srgb(c: float):
|
|||||||
return 12.92 * c
|
return 12.92 * c
|
||||||
|
|
||||||
|
|
||||||
def get_nla_strips_in_frame_range(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]:
|
def get_nla_strips_in_timeframe(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]:
|
||||||
if animation_data is None:
|
if animation_data is None:
|
||||||
return []
|
return []
|
||||||
strips = []
|
strips = []
|
||||||
@@ -124,7 +143,7 @@ def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_g
|
|||||||
|
|
||||||
# Split out the bone indices and the instigator bone names into separate lists.
|
# 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.
|
# 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 an
|
# For example, users of this function may modify bone lists, which would invalidate the indices and require a
|
||||||
# index mapping scheme to resolve it. Using strings is more comfy and results in less code downstream.
|
# 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]
|
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]
|
bone_names = [bones[x[0]].name for x in bone_indices]
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ 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.unreal_engine_1_mode: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
@@ -92,11 +91,7 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
# Build list of PSA bones.
|
# Build list of PSA bones.
|
||||||
for bone in bones:
|
for bone in bones:
|
||||||
psa_bone = Psa.Bone()
|
psa_bone = Psa.Bone()
|
||||||
|
|
||||||
try:
|
|
||||||
psa_bone.name = bytes(bone.name, encoding='windows-1252')
|
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:
|
try:
|
||||||
parent_index = bones.index(bone.parent)
|
parent_index = bones.index(bone.parent)
|
||||||
@@ -170,10 +165,7 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
frame_step = -frame_step
|
frame_step = -frame_step
|
||||||
|
|
||||||
psa_sequence = Psa.Sequence()
|
psa_sequence = Psa.Sequence()
|
||||||
try:
|
|
||||||
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
|
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_count = frame_count
|
||||||
psa_sequence.frame_start_index = frame_start_index
|
psa_sequence.frame_start_index = frame_start_index
|
||||||
psa_sequence.fps = frame_count / sequence_duration
|
psa_sequence.fps = frame_count / sequence_duration
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import os.path
|
|
||||||
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
|
||||||
@@ -9,10 +8,10 @@ from bpy.types import Context, Armature, Action, Object, AnimData, TimelineMarke
|
|||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
from bpy_types import Operator
|
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 ..builder import build_psa, PsaBuildSequence, PsaBuildOptions
|
||||||
from ..writer import write_psa, write_psa_import_commands
|
from ..export.properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences
|
||||||
from ...helpers import populate_bone_group_list, get_nla_strips_in_frame_range
|
from ..writer import write_psa
|
||||||
|
from ...helpers import populate_bone_group_list, get_nla_strips_in_timeframe
|
||||||
|
|
||||||
|
|
||||||
def is_action_for_armature(armature: Armature, action: Action):
|
def is_action_for_armature(armature: Armature, action: Action):
|
||||||
@@ -81,12 +80,10 @@ def update_actions_and_timeline_markers(context: Context, armature: Armature):
|
|||||||
continue
|
continue
|
||||||
if marker_name.startswith('#'):
|
if marker_name.startswith('#'):
|
||||||
continue
|
continue
|
||||||
frame_start, frame_end = sequence_frame_ranges[marker_name]
|
|
||||||
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 = pg.marker_list.add()
|
||||||
item.name = sequence_name
|
item.name = marker_name
|
||||||
item.is_selected = False
|
item.is_selected = False
|
||||||
|
frame_start, frame_end = sequence_frame_ranges[marker_name]
|
||||||
item.frame_start = frame_start
|
item.frame_start = frame_start
|
||||||
item.frame_end = frame_end
|
item.frame_end = frame_end
|
||||||
|
|
||||||
@@ -153,7 +150,7 @@ def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context:
|
|||||||
if next_marker_index < len(sorted_timeline_markers):
|
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.
|
# 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
|
frame_end = sorted_timeline_markers[next_marker_index].frame
|
||||||
nla_strips = get_nla_strips_in_frame_range(animation_data, marker.frame, frame_end)
|
nla_strips = get_nla_strips_in_timeframe(animation_data, marker.frame, frame_end)
|
||||||
if len(nla_strips) > 0:
|
if len(nla_strips) > 0:
|
||||||
frame_end = min(frame_end, max(map(lambda nla_strip: nla_strip.frame_end, nla_strips)))
|
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)))
|
frame_start = max(frame_start, min(map(lambda nla_strip: nla_strip.frame_start, nla_strips)))
|
||||||
@@ -177,9 +174,11 @@ def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context:
|
|||||||
return sequence_frame_ranges
|
return sequence_frame_ranges
|
||||||
|
|
||||||
|
|
||||||
def get_sequences_from_name_and_frame_range(name: str, frame_start: int, frame_end: int) -> List[Tuple[str, int, int]]:
|
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])
|
||||||
reversed_pattern = r'(.+)/(.+)'
|
reversed_pattern = r'(.+)/(.+)'
|
||||||
reversed_match = re.match(reversed_pattern, name)
|
reversed_match = re.match(reversed_pattern, action.name)
|
||||||
if reversed_match:
|
if reversed_match:
|
||||||
forward_name = reversed_match.group(1)
|
forward_name = reversed_match.group(1)
|
||||||
backwards_name = reversed_match.group(2)
|
backwards_name = reversed_match.group(2)
|
||||||
@@ -188,13 +187,7 @@ def get_sequences_from_name_and_frame_range(name: str, frame_start: int, frame_e
|
|||||||
(backwards_name, frame_end, frame_start)
|
(backwards_name, frame_end, frame_start)
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
return [(name, frame_start, frame_end)]
|
return [(action.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]]:
|
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]]:
|
||||||
@@ -203,7 +196,17 @@ def get_sequences_from_action_pose_marker(action: Action, pose_markers: List[Tim
|
|||||||
frame_end = pose_markers[pose_marker_index + 1].frame
|
frame_end = pose_markers[pose_marker_index + 1].frame
|
||||||
else:
|
else:
|
||||||
frame_end = int(action.frame_range[1])
|
frame_end = int(action.frame_range[1])
|
||||||
return get_sequences_from_name_and_frame_range(pose_marker.name, frame_start, frame_end)
|
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)]
|
||||||
|
|
||||||
|
|
||||||
def get_visible_sequences(pg: PSA_PG_export, sequences) -> List[PSA_PG_export_action_list_item]:
|
def get_visible_sequences(pg: PSA_PG_export, sequences) -> List[PSA_PG_export_action_list_item]:
|
||||||
@@ -251,18 +254,12 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
# SOURCE
|
# SOURCE
|
||||||
layout.prop(pg, 'sequence_source', text='Source')
|
layout.prop(pg, 'sequence_source', text='Source')
|
||||||
|
|
||||||
if pg.sequence_source in {'TIMELINE_MARKERS', 'NLA_TRACK_STRIPS'}:
|
if pg.sequence_source == 'TIMELINE_MARKERS':
|
||||||
# ANIMDATA SOURCE
|
# ANIMDATA SOURCE
|
||||||
layout.prop(pg, 'should_override_animation_data')
|
layout.prop(pg, 'should_override_animation_data')
|
||||||
if pg.should_override_animation_data:
|
if pg.should_override_animation_data:
|
||||||
layout.prop(pg, 'animation_data_override', text='')
|
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
|
# SELECT ALL/NONE
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.label(text='Select')
|
row.label(text='Select')
|
||||||
@@ -272,13 +269,19 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
# ACTIONS
|
# ACTIONS
|
||||||
if pg.sequence_source == 'ACTIONS':
|
if pg.sequence_source == 'ACTIONS':
|
||||||
rows = max(3, min(len(pg.action_list), 10))
|
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)
|
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':
|
elif pg.sequence_source == 'TIMELINE_MARKERS':
|
||||||
rows = max(3, min(len(pg.marker_list), 10))
|
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',
|
||||||
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
|
rows=rows)
|
||||||
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 = layout.column()
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
@@ -314,9 +317,8 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
|
|
||||||
|
# ROOT MOTION
|
||||||
layout.prop(pg, 'root_motion', text='Root Motion')
|
layout.prop(pg, 'root_motion', text='Root Motion')
|
||||||
layout.prop(pg, 'should_write_import_commands', text='Write Import Commands')
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_context(cls, context):
|
def _check_context(cls, context):
|
||||||
@@ -358,8 +360,6 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
raise RuntimeError('No actions were selected for export')
|
raise RuntimeError('No actions were selected for export')
|
||||||
elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_list) == 0:
|
elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_list) == 0:
|
||||||
raise RuntimeError('No timeline markers were selected for export')
|
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.
|
# Populate the export sequence list.
|
||||||
animation_data_object = get_animation_data_object(context)
|
animation_data_object = get_animation_data_object(context)
|
||||||
@@ -371,38 +371,29 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
export_sequences: List[PsaBuildSequence] = []
|
export_sequences: List[PsaBuildSequence] = []
|
||||||
|
|
||||||
if pg.sequence_source == 'ACTIONS':
|
if pg.sequence_source == 'ACTIONS':
|
||||||
for action_item in filter(lambda x: x.is_selected, pg.action_list):
|
for action in filter(lambda x: x.is_selected, pg.action_list):
|
||||||
if len(action_item.action.fcurves) == 0:
|
if len(action.action.fcurves) == 0:
|
||||||
continue
|
continue
|
||||||
export_sequence = PsaBuildSequence()
|
export_sequence = PsaBuildSequence()
|
||||||
export_sequence.nla_state.action = action_item.action
|
export_sequence.nla_state.action = action.action
|
||||||
export_sequence.name = action_item.name
|
export_sequence.name = action.name
|
||||||
export_sequence.nla_state.frame_start = action_item.frame_start
|
export_sequence.nla_state.frame_start = action.frame_start
|
||||||
export_sequence.nla_state.frame_end = action_item.frame_end
|
export_sequence.nla_state.frame_end = action.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.action])
|
||||||
export_sequence.compression_ratio = action_item.action.psa_export.compression_ratio
|
export_sequence.compression_ratio = action.action.psa_export.compression_ratio
|
||||||
export_sequence.key_quota = action_item.action.psa_export.key_quota
|
export_sequence.key_quota = action.action.psa_export.key_quota
|
||||||
export_sequences.append(export_sequence)
|
export_sequences.append(export_sequence)
|
||||||
elif pg.sequence_source == 'TIMELINE_MARKERS':
|
elif pg.sequence_source == 'TIMELINE_MARKERS':
|
||||||
for marker_item in filter(lambda x: x.is_selected, pg.marker_list):
|
for marker in pg.marker_list:
|
||||||
export_sequence = PsaBuildSequence()
|
export_sequence = PsaBuildSequence()
|
||||||
export_sequence.name = marker_item.name
|
export_sequence.name = marker.name
|
||||||
export_sequence.nla_state.action = None
|
export_sequence.nla_state.action = None
|
||||||
export_sequence.nla_state.frame_start = marker_item.frame_start
|
export_sequence.nla_state.frame_start = marker.frame_start
|
||||||
export_sequence.nla_state.frame_end = marker_item.frame_end
|
export_sequence.nla_state.frame_end = marker.frame_end
|
||||||
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_timeframe(animation_data, marker.frame_start, marker.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_sequences.append(export_sequence)
|
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:
|
else:
|
||||||
raise ValueError(f'Unhandled sequence source: {pg.sequence_source}')
|
raise ValueError(f'Unhandled sequence source: {pg.sequence_source}')
|
||||||
|
|
||||||
@@ -425,12 +416,6 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
|
|
||||||
write_psa(psa, self.filepath)
|
write_psa(psa, self.filepath)
|
||||||
|
|
||||||
if pg.should_write_import_commands:
|
|
||||||
# Replace the extension in the file path to be ".uc" instead of ".psa".
|
|
||||||
# This is because the Unreal Engine 1 import command expects the file to have a ".uc" extension.
|
|
||||||
filepath = os.path.splitext(self.filepath)[0] + '.uc'
|
|
||||||
write_psa_import_commands(psa, filepath)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
@@ -447,8 +432,6 @@ class PSA_OT_export_actions_select_all(Operator):
|
|||||||
return pg.action_list
|
return pg.action_list
|
||||||
elif pg.sequence_source == 'TIMELINE_MARKERS':
|
elif pg.sequence_source == 'TIMELINE_MARKERS':
|
||||||
return pg.marker_list
|
return pg.marker_list
|
||||||
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
|
|
||||||
return pg.nla_strip_list
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -480,8 +463,6 @@ class PSA_OT_export_actions_deselect_all(Operator):
|
|||||||
return pg.action_list
|
return pg.action_list
|
||||||
elif pg.sequence_source == 'TIMELINE_MARKERS':
|
elif pg.sequence_source == 'TIMELINE_MARKERS':
|
||||||
return pg.marker_list
|
return pg.marker_list
|
||||||
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
|
|
||||||
return pg.nla_strip_list
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty, \
|
from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty, \
|
||||||
StringProperty
|
StringProperty
|
||||||
from bpy.types import PropertyGroup, Object, Action, AnimData, Context
|
from bpy.types import PropertyGroup, Object, Action
|
||||||
|
|
||||||
from ...types import PSX_PG_bone_group_list_item
|
from ...types import PSX_PG_bone_group_list_item
|
||||||
|
|
||||||
@@ -20,13 +19,13 @@ empty_set = set()
|
|||||||
class PSA_PG_export_action_list_item(PropertyGroup):
|
class PSA_PG_export_action_list_item(PropertyGroup):
|
||||||
action: PointerProperty(type=Action)
|
action: PointerProperty(type=Action)
|
||||||
name: StringProperty()
|
name: StringProperty()
|
||||||
is_selected: BoolProperty(default=False)
|
is_selected: BoolProperty(default=True)
|
||||||
frame_start: IntProperty(options={'HIDDEN'})
|
frame_start: IntProperty(options={'HIDDEN'})
|
||||||
frame_end: IntProperty(options={'HIDDEN'})
|
frame_end: IntProperty(options={'HIDDEN'})
|
||||||
is_pose_marker: BoolProperty(options={'HIDDEN'})
|
is_pose_marker: BoolProperty(options={'HIDDEN'})
|
||||||
|
|
||||||
|
|
||||||
class PSA_PG_export_timeline_markers(PropertyGroup): # TODO: rename this to singular
|
class PSA_PG_export_timeline_markers(PropertyGroup):
|
||||||
marker_index: IntProperty()
|
marker_index: IntProperty()
|
||||||
name: StringProperty()
|
name: StringProperty()
|
||||||
is_selected: BoolProperty(default=True)
|
is_selected: BoolProperty(default=True)
|
||||||
@@ -34,51 +33,6 @@ class PSA_PG_export_timeline_markers(PropertyGroup): # TODO: rename this to sin
|
|||||||
frame_end: IntProperty(options={'HIDDEN'})
|
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):
|
class PSA_PG_export(PropertyGroup):
|
||||||
root_motion: BoolProperty(
|
root_motion: BoolProperty(
|
||||||
name='Root Motion',
|
name='Root Motion',
|
||||||
@@ -92,12 +46,10 @@ class PSA_PG_export(PropertyGroup):
|
|||||||
name='Override Animation Data',
|
name='Override Animation Data',
|
||||||
options=empty_set,
|
options=empty_set,
|
||||||
default=False,
|
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(
|
animation_data_override: PointerProperty(
|
||||||
type=Object,
|
type=Object,
|
||||||
update=animation_data_override_update_cb,
|
|
||||||
poll=psa_export_property_group_animation_data_override_poll
|
poll=psa_export_property_group_animation_data_override_poll
|
||||||
)
|
)
|
||||||
sequence_source: EnumProperty(
|
sequence_source: EnumProperty(
|
||||||
@@ -106,18 +58,10 @@ class PSA_PG_export(PropertyGroup):
|
|||||||
description='',
|
description='',
|
||||||
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 will be exported using timeline markers', 'MARKER_HLT',
|
||||||
('NLA_TRACK_STRIPS', 'NLA Track Strips', 'Sequences are delineated by the start & end times of strips on the selected NLA track', 'NLA', 2)
|
1),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
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(
|
fps_source: EnumProperty(
|
||||||
name='FPS Source',
|
name='FPS Source',
|
||||||
options=empty_set,
|
options=empty_set,
|
||||||
@@ -136,8 +80,6 @@ class PSA_PG_export(PropertyGroup):
|
|||||||
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)
|
||||||
marker_list_index: IntProperty(default=0)
|
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(
|
bone_filter_mode: EnumProperty(
|
||||||
name='Bone Filter',
|
name='Bone Filter',
|
||||||
options=empty_set,
|
options=empty_set,
|
||||||
@@ -184,12 +126,6 @@ class PSA_PG_export(PropertyGroup):
|
|||||||
name='Show Reversed',
|
name='Show Reversed',
|
||||||
description='Show reversed sequences'
|
description='Show reversed sequences'
|
||||||
)
|
)
|
||||||
should_write_import_commands: BoolProperty(
|
|
||||||
default=True,
|
|
||||||
options=empty_set,
|
|
||||||
name='Write PSA Import Commands',
|
|
||||||
description='Write PSA import commands to a UnrealScript file in the same directory as the exported file'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
||||||
@@ -209,7 +145,7 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
|||||||
|
|
||||||
if not pg.sequence_filter_asset:
|
if not pg.sequence_filter_asset:
|
||||||
for i, sequence in enumerate(sequences):
|
for i, sequence in enumerate(sequences):
|
||||||
if hasattr(sequence, 'action') and sequence.action is not None and sequence.action.asset_data is not None:
|
if hasattr(sequence, 'action') and sequence.action.asset_data is not None:
|
||||||
flt_flags[i] &= ~bitflag_filter_item
|
flt_flags[i] &= ~bitflag_filter_item
|
||||||
|
|
||||||
if not pg.sequence_filter_pose_marker:
|
if not pg.sequence_filter_pose_marker:
|
||||||
@@ -228,6 +164,5 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
|||||||
classes = (
|
classes = (
|
||||||
PSA_PG_export_action_list_item,
|
PSA_PG_export_action_list_item,
|
||||||
PSA_PG_export_timeline_markers,
|
PSA_PG_export_timeline_markers,
|
||||||
PSA_PG_export_nla_strip_list_item,
|
|
||||||
PSA_PG_export,
|
PSA_PG_export,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class PSA_UL_export_sequences(UIList):
|
|||||||
item = cast(PSA_PG_export_action_list_item, item)
|
item = cast(PSA_PG_export_action_list_item, item)
|
||||||
is_pose_marker = hasattr(item, 'is_pose_marker') and item.is_pose_marker
|
is_pose_marker = hasattr(item, 'is_pose_marker') and item.is_pose_marker
|
||||||
layout.prop(item, 'is_selected', icon_only=True, text=item.name)
|
layout.prop(item, 'is_selected', icon_only=True, text=item.name)
|
||||||
if hasattr(item, 'action') and item.action is not None and item.action.asset_data is not None:
|
if hasattr(item, 'action') and item.action.asset_data is not None:
|
||||||
layout.label(text='', icon='ASSET_MANAGER')
|
layout.label(text='', icon='ASSET_MANAGER')
|
||||||
|
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
|
|||||||
@@ -167,12 +167,17 @@ class PSA_OT_import(Operator, ImportHelper):
|
|||||||
options.should_convert_to_samples = pg.should_convert_to_samples
|
options.should_convert_to_samples = pg.should_convert_to_samples
|
||||||
options.bone_mapping_mode = pg.bone_mapping_mode
|
options.bone_mapping_mode = pg.bone_mapping_mode
|
||||||
|
|
||||||
|
if len(sequence_names) == 0:
|
||||||
|
self.report({'ERROR_INVALID_CONTEXT'}, 'No sequences selected')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
result = import_psa(context, psa_reader, context.view_layer.objects.active, options)
|
result = import_psa(context, psa_reader, context.view_layer.objects.active, options)
|
||||||
|
|
||||||
if len(result.warnings) > 0:
|
if len(result.warnings) > 0:
|
||||||
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
|
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
|
||||||
message += '\n'.join(result.warnings)
|
|
||||||
self.report({'WARNING'}, message)
|
self.report({'WARNING'}, message)
|
||||||
|
for warning in result.warnings:
|
||||||
|
self.report({'WARNING'}, warning)
|
||||||
else:
|
else:
|
||||||
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
|
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ empty_set = set()
|
|||||||
|
|
||||||
class PSA_PG_import_action_list_item(PropertyGroup):
|
class PSA_PG_import_action_list_item(PropertyGroup):
|
||||||
action_name: StringProperty(options=empty_set)
|
action_name: StringProperty(options=empty_set)
|
||||||
is_selected: BoolProperty(default=False, options=empty_set)
|
is_selected: BoolProperty(default=True, options=empty_set)
|
||||||
|
|
||||||
|
|
||||||
class PSA_PG_bone(PropertyGroup):
|
class PSA_PG_bone(PropertyGroup):
|
||||||
|
|||||||
@@ -5,6 +5,24 @@ import numpy as np
|
|||||||
from .data import *
|
from .data import *
|
||||||
|
|
||||||
|
|
||||||
|
def _try_fix_cue4parse_issue_103(sequences) -> bool:
|
||||||
|
# Detect if the file was exported from CUE4Parse prior to the fix for issue #103.
|
||||||
|
# https://github.com/FabianFG/CUE4Parse/issues/103
|
||||||
|
# The issue was that the frame_start_index was not being set correctly, and was always being set to the same value
|
||||||
|
# as the frame_count.
|
||||||
|
# This fix will eventually be deprecated as it is only necessary for files exported prior to the fix.
|
||||||
|
if len(sequences) > 0:
|
||||||
|
if sequences[0].frame_start_index == sequences[0].frame_count:
|
||||||
|
# Manually set the frame_start_index for each sequence. This assumes that the sequences are in order with
|
||||||
|
# no shared frames between sequences (all exporters that I know of do this, so it's a safe assumption).
|
||||||
|
frame_start_index = 0
|
||||||
|
for i, sequence in enumerate(sequences):
|
||||||
|
sequence.frame_start_index = frame_start_index
|
||||||
|
frame_start_index += sequence.frame_count
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class PsaReader(object):
|
class PsaReader(object):
|
||||||
"""
|
"""
|
||||||
This class reads the sequences and bone information immediately upon instantiation and holds onto a file handle.
|
This class reads the sequences and bone information immediately upon instantiation and holds onto a file handle.
|
||||||
@@ -86,14 +104,15 @@ class PsaReader(object):
|
|||||||
elif section.name == b'ANIMINFO':
|
elif section.name == b'ANIMINFO':
|
||||||
sequences = []
|
sequences = []
|
||||||
PsaReader._read_types(fp, Psa.Sequence, section, sequences)
|
PsaReader._read_types(fp, Psa.Sequence, section, sequences)
|
||||||
|
# Try to fix CUE4Parse bug, if necessary.
|
||||||
|
_try_fix_cue4parse_issue_103(sequences)
|
||||||
for sequence in sequences:
|
for sequence in sequences:
|
||||||
psa.sequences[sequence.name.decode()] = sequence
|
psa.sequences[sequence.name.decode()] = sequence
|
||||||
elif section.name == b'ANIMKEYS':
|
elif section.name == b'ANIMKEYS':
|
||||||
# Skip keys on this pass. We will keep this file open and read from it as needed.
|
# Skip keys on this pass. We will keep this file open and read from it as needed.
|
||||||
self.keys_data_offset = fp.tell()
|
self.keys_data_offset = fp.tell()
|
||||||
fp.seek(section.data_size * section.data_count, 1)
|
fp.seek(section.data_size * section.data_count, 1)
|
||||||
elif section.name == b'SCALEKEYS':
|
|
||||||
fp.seek(section.data_size * section.data_count, 1)
|
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f'Unrecognized section "{section.name}"')
|
fp.seek(section.data_size * section.data_count, 1)
|
||||||
|
print(f'Unrecognized section in PSA: "{section.name}"')
|
||||||
return psa
|
return psa
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import os.path
|
|
||||||
from ctypes import Structure, sizeof
|
from ctypes import Structure, sizeof
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
@@ -24,15 +23,3 @@ def write_psa(psa: Psa, path: str):
|
|||||||
write_section(fp, b'BONENAMES', Psa.Bone, psa.bones)
|
write_section(fp, b'BONENAMES', Psa.Bone, psa.bones)
|
||||||
write_section(fp, b'ANIMINFO', Psa.Sequence, list(psa.sequences.values()))
|
write_section(fp, b'ANIMINFO', Psa.Sequence, list(psa.sequences.values()))
|
||||||
write_section(fp, b'ANIMKEYS', Psa.Key, psa.keys)
|
write_section(fp, b'ANIMKEYS', Psa.Key, psa.keys)
|
||||||
|
|
||||||
|
|
||||||
def write_psa_import_commands(psa: Psa, path: str):
|
|
||||||
anim = os.path.splitext(os.path.basename(path))[0]
|
|
||||||
with open(path, 'w') as fp:
|
|
||||||
for sequence_name, sequence in psa.sequences.items():
|
|
||||||
fp.write(f'#EXEC ANIM SEQUENCE '
|
|
||||||
f'ANIM={anim} '
|
|
||||||
f'SEQ={sequence_name} '
|
|
||||||
f'STARTFRAME={sequence.frame_start_index} '
|
|
||||||
f'NUMFRAMES={sequence.frame_count} '
|
|
||||||
f'RATE={sequence.fps}\n')
|
|
||||||
|
|||||||
@@ -88,11 +88,7 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
|
|||||||
|
|
||||||
for bone in bones:
|
for bone in bones:
|
||||||
psk_bone = Psk.Bone()
|
psk_bone = Psk.Bone()
|
||||||
try:
|
|
||||||
psk_bone.name = bytes(bone.name, encoding='windows-1252')
|
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.flags = 0
|
||||||
psk_bone.children_count = 0
|
psk_bone.children_count = 0
|
||||||
|
|
||||||
@@ -133,17 +129,14 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
|
|||||||
|
|
||||||
for material_name in material_names:
|
for material_name in material_names:
|
||||||
psk_material = Psk.Material()
|
psk_material = Psk.Material()
|
||||||
try:
|
|
||||||
psk_material.name = bytes(material_name, encoding='windows-1252')
|
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_material.texture_index = len(psk.materials)
|
||||||
psk.materials.append(psk_material)
|
psk.materials.append(psk_material)
|
||||||
|
|
||||||
for input_mesh_object in input_objects.mesh_objects:
|
for input_mesh_object in input_objects.mesh_objects:
|
||||||
|
|
||||||
# MATERIALS
|
# MATERIALS
|
||||||
material_indices = [material_names.index(material_slot.material.name) for material_slot in input_mesh_object.material_slots]
|
material_indices = [material_names.index(material.name) for material in input_mesh_object.data.materials]
|
||||||
|
|
||||||
# MESH DATA
|
# MESH DATA
|
||||||
if options.use_raw_mesh_data:
|
if options.use_raw_mesh_data:
|
||||||
@@ -154,6 +147,8 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
|
|||||||
|
|
||||||
# Temporarily force the armature into the rest position.
|
# Temporarily force the armature into the rest position.
|
||||||
# We will undo this later.
|
# We will undo this later.
|
||||||
|
old_pose_position = None
|
||||||
|
if armature_object is not None:
|
||||||
old_pose_position = armature_object.data.pose_position
|
old_pose_position = armature_object.data.pose_position
|
||||||
armature_object.data.pose_position = 'REST'
|
armature_object.data.pose_position = 'REST'
|
||||||
|
|
||||||
@@ -171,6 +166,7 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
|
|||||||
mesh_object.vertex_groups.new(name=vertex_group.name)
|
mesh_object.vertex_groups.new(name=vertex_group.name)
|
||||||
|
|
||||||
# Restore the previous pose position on the armature.
|
# Restore the previous pose position on the armature.
|
||||||
|
if old_pose_position is not None:
|
||||||
armature_object.data.pose_position = old_pose_position
|
armature_object.data.pose_position = old_pose_position
|
||||||
|
|
||||||
vertex_offset = len(psk.points)
|
vertex_offset = len(psk.points)
|
||||||
|
|||||||
@@ -22,11 +22,10 @@ def populate_material_list(mesh_objects, material_list):
|
|||||||
|
|
||||||
material_names = []
|
material_names = []
|
||||||
for mesh_object in mesh_objects:
|
for mesh_object in mesh_objects:
|
||||||
for i, material_slot in enumerate(mesh_object.material_slots):
|
for i, material in enumerate(mesh_object.data.materials):
|
||||||
material = material_slot.material
|
|
||||||
# TODO: put this in the poll arg?
|
# TODO: put this in the poll arg?
|
||||||
if material is None:
|
if material is None:
|
||||||
raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')')
|
raise RuntimeError('Material cannot be empty (index ' + str(i) + ')')
|
||||||
if material.name not in material_names:
|
if material.name not in material_names:
|
||||||
material_names.append(material.name)
|
material_names.append(material.name)
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
|
|||||||
# Material already exists, just re-use it.
|
# Material already exists, just re-use it.
|
||||||
material = bpy.data.materials[material_name]
|
material = bpy.data.materials[material_name]
|
||||||
elif is_bdk_addon_loaded() and psk.has_material_references:
|
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.
|
# Attempt to load it using BDK addon's operator.
|
||||||
material_reference = psk.material_references[material_index]
|
material_reference = psk.material_references[material_index]
|
||||||
if material_reference and bpy.ops.bdk.link_material(reference=material_reference) == {'FINISHED'}:
|
if material_reference and bpy.ops.bdk.link_material(reference=material_reference) == {'FINISHED'}:
|
||||||
|
|||||||
Reference in New Issue
Block a user