From a47306533ecbd657176afa7a4ad542d19e70b8b1 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Wed, 15 Apr 2026 16:51:55 +0300 Subject: [PATCH] Working on stuff --- blender scripts/blender_join_anims.py | 65 ++++++++ blender scripts/export_anim_001.py | 228 ++++++++++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 blender scripts/blender_join_anims.py create mode 100644 blender scripts/export_anim_001.py diff --git a/blender scripts/blender_join_anims.py b/blender scripts/blender_join_anims.py new file mode 100644 index 0000000..0fd51ce --- /dev/null +++ b/blender scripts/blender_join_anims.py @@ -0,0 +1,65 @@ + +import bpy + +def append_layered_action_5_0(source_obj_name, target_obj_name): + source_obj = bpy.data.objects.get(source_obj_name) + target_obj = bpy.data.objects.get(target_obj_name) + + if not (source_obj and target_obj): + print("Ошибка: Объекты не найдены") + return + + src_action = source_obj.animation_data.action + tgt_action = target_obj.animation_data.action + + # 1. Получаем слои (обычно первый) + src_layer = src_action.layers[0] + tgt_layer = tgt_action.layers[0] + + # 2. Получаем стрипы + src_strip = src_layer.strips[0] + tgt_strip = tgt_layer.strips[0] + + # Смещение (опираемся на конец диапазона целевого экшена) + offset = tgt_action.frame_range[1] + + # 3. Итерируемся по channelbags в исходном стрипе + for src_bag in src_strip.channelbags: + # Ищем или создаем соответствующий bag в целевом стрипе + # Обычно они сопоставляются по имени или типу (например, 'Keyframe Channel Bag') + # В простейшем случае берем первый или сопоставляем по индексу + dst_bag = None + if len(tgt_strip.channelbags) > 0: + # Пытаемся найти по названию (если оно есть) или берем тот же индекс + dst_bag = tgt_strip.channelbags[0] + + if not dst_bag: + # Если в целевом стрипе нет сумок, это странно, но можно создать + # (Метод создания может зависеть от конкретного подтипа стрипа в 5.0) + continue + + #print(f"Обработка channelbag: {src_bag.name}, кривых: {len(src_bag.fcurves)}") + + # 4. Итерируемся по fcurves внутри сумки + for src_fcurve in src_bag.fcurves: + dst_fcurve = dst_bag.fcurves.find(src_fcurve.data_path, index=src_fcurve.array_index) + + if not dst_fcurve: + dst_fcurve = dst_bag.fcurves.new(data_path=src_fcurve.data_path, index=src_fcurve.array_index) + + # 5. Копируем ключи с офсетом + for keyframe in src_fcurve.keyframe_points: + new_frame = keyframe.co[0] + offset + new_value = keyframe.co[1] + + new_kp = dst_fcurve.keyframe_points.insert(new_frame, new_value, options={'FAST'}) + new_kp.interpolation = keyframe.interpolation + + # Обновляем интерполяцию + for fc in dst_bag.fcurves: + fc.update() + + print(f"Анимация успешно дозаписана. Новый конец: {tgt_action.frame_range[1]}") + +append_layered_action_5_0('Armature.001', 'Armature') + diff --git a/blender scripts/export_anim_001.py b/blender scripts/export_anim_001.py new file mode 100644 index 0000000..c782a53 --- /dev/null +++ b/blender scripts/export_anim_001.py @@ -0,0 +1,228 @@ +import bpy +import bmesh + +#! + +# Имена mesh и арматуры +mesh_name = "Joined" +armature_name = "Armature" + +# Находим объект mesh по имени +mesh_obj = bpy.data.objects.get(mesh_name) + +# Находим объект арматуры по имени +armature_obj = bpy.data.objects.get(armature_name) + +# Устанавливаем текущий кадр на 0 +bpy.context.scene.frame_set(0) + +# Принудительно обновляем сцену, чтобы применить анимацию +bpy.context.view_layer.update() + +# Открываем файл для записи +with open("C:\\Work\\Media\\witcher\\2026-04-13\\output\\gg_stand_idle001.txt", "w") as file: + # Обработка арматуры и анимации + if armature_obj and armature_obj.type == 'ARMATURE': + file.write("=== Armature Matrix ===\n") + for row in armature_obj.matrix_world: + file.write(f"{row}\n") + file.write(f"=== Armature Bones: {len(armature_obj.data.bones)}\n") + for bone in armature_obj.data.bones: + # Записываем имя кости, длину и связи + file.write(f"Bone: {bone.name}\n") + file.write(f" HEAD_LOCAL: {bone.head_local}\n") + file.write(f" TAIL_LOCAL: {bone.tail_local}\n") + file.write(f" Length: {(bone.tail_local - bone.head_local).length}\n") + for row in bone.matrix: + file.write(f" {row}\n") + file.write(f" Parent: {bone.parent.name if bone.parent else 'None'}\n") + file.write(f" Children: {[child.name for child in bone.children]}\n") + + # Обработка mesh + if mesh_obj and mesh_obj.type == 'MESH': + # Создаем копию mesh, чтобы не изменять оригинал + mesh_copy = mesh_obj.copy() + mesh_copy.data = mesh_obj.data.copy() + bpy.context.collection.objects.link(mesh_copy) + + # Убедимся, что объект активен + bpy.context.view_layer.objects.active = mesh_copy + mesh_copy.select_set(True) + + # Применяем модификатор Armature (если он есть) + for modifier in mesh_copy.modifiers: + if modifier.type == 'ARMATURE': + # Включаем модификатор, если он отключен + if not modifier.show_viewport: + modifier.show_viewport = True + if not modifier.show_render: + modifier.show_render = True + + # Проверяем, что модификатор связан с арматурой + if modifier.object is None: + print(f"Модификатор Armature на объекте {mesh_copy.name} не связан с арматурой. Пропускаем.") + continue + + # Временно применяем модификатор, чтобы получить правильные координаты вершин + try: + bpy.ops.object.modifier_apply(modifier=modifier.name) + except RuntimeError as e: + print(f"Ошибка при применении модификатора Armature: {e}") + continue + + # Переходим в режим редактирования + bpy.ops.object.mode_set(mode='EDIT') + + # Получаем BMesh представление mesh + bm = bmesh.from_edit_mesh(mesh_copy.data) + + # Записываем список вершин + file.write(f"===Vertices: {len(bm.verts)}\n") + for vertex in bm.verts: + file.write(f"Vertex {vertex.index}: {vertex.co}\n") + + # Убедимся, что у меша есть UV слой + uv_layer = bm.loops.layers.uv.active + if not uv_layer: + file.write("UV слой не найден.\n") + + if uv_layer: + file.write(f"===UV Coordinates:\n") + file.write(f"Face count: {len(bm.faces)}\n") + + for face in bm.faces: + file.write(f"Face {face.index}\n") + file.write(f"UV Count: {len(face.loops)}\n") + for loop in face.loops: + uv_coords = loop[uv_layer].uv + file.write(f" UV {uv_coords}\n") + + # Записываем нормали + file.write(f"===Normals:\n") + for vertex in bm.verts: + file.write(f"Vertex {vertex.index}: Normal {vertex.normal}\n") + + # Записываем треугольники (индексы вершин) + file.write(f"===Triangles: {len(bm.faces)}\n") + for face in bm.faces: + if len(face.verts) == 3: # Проверяем, что это треугольник + verts_indices = [vert.index for vert in face.verts] + file.write(f"Triangle: {verts_indices}\n") + + # Возвращаемся в объектный режим + bpy.ops.object.mode_set(mode='OBJECT') + + # Записываем веса вершин + file.write("=== Vertex Weights (Max 5 bones per vertex) ===\n") + + MAX_BONES = 5 + + for vertex in mesh_copy.data.vertices: + # Извлекаем все группы и веса для текущей вершины + all_weights = [] + for group_element in vertex.groups: + all_weights.append({ + 'index': group_element.group, + 'weight': group_element.weight + }) + + # Если костей больше лимита, фильтруем и перераспределяем + if len(all_weights) > MAX_BONES: + # Сортируем по весу (от большего к меньшему) + all_weights.sort(key=lambda x: x['weight'], reverse=True) + + # Берем только топ-5 + kept_weights = all_weights[:MAX_BONES] + + # Считаем сумму весов оставшихся костей для нормализации + total_weight = sum(gw['weight'] for gw in kept_weights) + + if total_weight > 0: + for gw in kept_weights: + gw['weight'] /= total_weight + else: + # На случай, если у всех веса были по 0.0 (редкий баг меша) + kept_weights[0]['weight'] = 1.0 + + final_weights = kept_weights + else: + final_weights = all_weights + + file.write(f"Vertex {vertex.index}:\n") + file.write(f"Vertex groups: {len(final_weights)}\n") + + for gw in final_weights: + group_name = mesh_copy.vertex_groups[gw['index']].name + file.write(f" Group: '{group_name}', Weight: {gw['weight']:.6f}\n") + + # Удаляем временную копию mesh + bpy.data.objects.remove(mesh_copy) + else: + file.write(f"Объект с именем '{mesh_name}' не найден или не является mesh.\n") + + # Обработка арматуры и анимации + if armature_obj and armature_obj.type == 'ARMATURE': + + # Получаем все ключевые кадры для арматуры + file.write("=== Animation Keyframes ===\n") + if armature_obj.animation_data and armature_obj.animation_data.action: + action = armature_obj.animation_data.action + + # Собираем все уникальные ключевые кадры + keyframes = set() + + # Логика для Blender 5.0 (Strip-based / ChannelBag structure) + if hasattr(action, "layers"): + for layer in action.layers: + if hasattr(layer, "strips"): + for strip in layer.strips: + # Проверяем наличие channelbags (согласно вашему dir(strip)) + if hasattr(strip, "channelbags"): + for bag in strip.channelbags: + for fcurve in bag.fcurves: + for keyframe in fcurve.keyframe_points: + keyframes.add(int(keyframe.co[0])) + + # На случай, если в этой версии используется единственное число + elif hasattr(strip, "channelbag") and strip.channelbag: + for fcurve in strip.channelbag.fcurves: + for keyframe in fcurve.keyframe_points: + keyframes.add(int(keyframe.co[0])) + + # Фоллбек для Legacy экшенов + if not keyframes and hasattr(action, "fcurves"): + for fcurve in action.fcurves: + for keyframe in fcurve.keyframe_points: + keyframes.add(int(keyframe.co[0])) + + keyframes = sorted(keyframes) + + # Сортируем ключевые кадры + keyframes = sorted(keyframes) + + # Сохраняем координаты и матрицы поворота для каждой кости на каждом ключевом кадре + file.write("=== Bone Transforms per Keyframe ===\n") + file.write(f"Keyframes: {len(keyframes)}\n") + for frame in keyframes: + # Устанавливаем текущий кадр + bpy.context.scene.frame_set(frame) + bpy.context.view_layer.update() # Обновляем сцену + + file.write(f"Frame: {frame}\n") + for bone in armature_obj.pose.bones: + # Получаем координаты и матрицу поворота кости в мировом пространстве + matrix = bone.matrix + location = matrix.translation + rotation = matrix.to_euler() + + # Записываем данные + file.write(f" Bone: {bone.name}\n") + file.write(f" Location: {location}\n") + file.write(f" Rotation: {rotation}\n") + file.write(f" Matrix:\n") + for row in matrix: + file.write(f" {row}\n") + else: + file.write(f"Объект с именем '{armature_name}' не найден или не является арматурой.\n") + +print("Данные сохранены в файл 'mesh_armature_and_animation_data.txt'") \ No newline at end of file