Files
blender-vrm/editor/body_mesh/ops.py
小煜 d70b36fd75 feat(body_mesh): Add body mesh separator module with edge dissolution
- Add body_mesh submodule with VRM_OT_separate_non_skin_faces operator
- Add VRM_PT_body_mesh_separator panel for UI integration
- Add utility functions for mesh operations and edge dissolution logic
- Add comprehensive edge dissolution configuration files for all body parts (head, torso, arms, legs, hands) with UV coordinates
- Add test utilities for validating mesh separation operations
- Add .gitignore to exclude Python cache files and test artifacts
- Register body_mesh module in editor package initialization
- Update registration.py to include new body mesh components
2026-01-01 22:20:31 +08:00

221 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
from collections.abc import Set as AbstractSet
from typing import TYPE_CHECKING, Optional
import bpy
from bpy.types import Context, Operator
from .utils import (
MaterialInfo,
find_body_mesh,
generate_unique_name,
get_material_face_count,
is_skin_material,
)
if TYPE_CHECKING:
from bpy.types import Object
class VRM_OT_separate_non_skin_faces(Operator):
"""将Body网格中非皮肤材质的面分离为独立对象"""
bl_idname = "vrm.separate_non_skin_faces"
bl_label = "Separate Non-Skin Faces"
bl_description = "将Body网格中非皮肤材质的面分离为独立对象"
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: Context) -> bool:
"""检查是否可以执行分离操作"""
armature = cls._get_armature(context)
if armature is None:
return False
body_mesh = find_body_mesh(armature)
if body_mesh is None:
return False
# 检查是否有非皮肤材质
mesh_data = body_mesh.data
if mesh_data is None:
return False
for material in mesh_data.materials:
if material is not None and not is_skin_material(material.name):
return True
return False
@classmethod
def _get_armature(cls, context: Context) -> Optional["Object"]:
"""获取当前选中的Armature对象"""
active_object = context.active_object
if active_object is None:
return None
if active_object.type == "ARMATURE":
return active_object
# 如果选中的是网格检查其父对象是否为Armature
if active_object.parent is not None and active_object.parent.type == "ARMATURE":
return active_object.parent
return None
def execute(self, context: Context) -> set[str]:
"""执行分离操作"""
try:
return self._execute_separation(context)
except Exception as e:
self.report({"ERROR"}, f"分离操作失败: {str(e)}")
return {"CANCELLED"}
def _execute_separation(self, context: Context) -> set[str]:
"""执行分离操作的核心逻辑"""
armature = self._get_armature(context)
if armature is None:
self.report({"ERROR"}, "未找到Armature对象")
return {"CANCELLED"}
body_mesh = find_body_mesh(armature)
if body_mesh is None:
self.report({"ERROR"}, "未找到Body网格对象")
return {"CANCELLED"}
mesh_data = body_mesh.data
if mesh_data is None:
self.report({"ERROR"}, "Body网格数据无效")
return {"CANCELLED"}
# 收集非皮肤材质信息
non_skin_materials: list[MaterialInfo] = []
for i, material in enumerate(mesh_data.materials):
if material is not None and not is_skin_material(material.name):
face_count = get_material_face_count(mesh_data, i)
if face_count > 0:
non_skin_materials.append(
MaterialInfo(
name=material.name,
index=i,
face_count=face_count,
is_skin=False,
)
)
if not non_skin_materials:
self.report({"INFO"}, "没有找到非皮肤材质的面")
return {"FINISHED"}
# 获取现有对象名称集合
existing_names = set(obj.name for obj in context.blend_data.objects)
# 为每个非皮肤材质执行分离
separated_count = 0
for mat_info in non_skin_materials:
result = self._separate_material_faces(
context, body_mesh, armature, mat_info, existing_names
)
if result:
separated_count += 1
existing_names.add(result.name)
if separated_count > 0:
self.report({"INFO"}, f"成功分离了 {separated_count} 个材质的面")
return {"FINISHED"}
else:
self.report({"WARNING"}, "没有成功分离任何面")
return {"CANCELLED"}
def _separate_material_faces(
self,
context: Context,
body_obj: "Object",
armature: "Object",
mat_info: MaterialInfo,
existing_names: set[str],
) -> Optional["Object"]:
"""分离指定材质的面为新对象"""
mesh_data = body_obj.data
if mesh_data is None:
return None
# 确保在对象模式
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
# 取消所有选择
bpy.ops.object.select_all(action="DESELECT")
# 选择Body网格并设为活动对象
body_obj.select_set(True)
context.view_layer.objects.active = body_obj
# 进入编辑模式
bpy.ops.object.mode_set(mode="EDIT")
# 取消所有选择
bpy.ops.mesh.select_all(action="DESELECT")
# 切换到面选择模式
bpy.ops.mesh.select_mode(type="FACE")
# 返回对象模式以访问网格数据
bpy.ops.object.mode_set(mode="OBJECT")
# 选择使用指定材质的面
for polygon in mesh_data.polygons:
if polygon.material_index == mat_info.index:
polygon.select = True
# 返回编辑模式执行分离
bpy.ops.object.mode_set(mode="EDIT")
# 执行分离操作
bpy.ops.mesh.separate(type="SELECTED")
# 返回对象模式
bpy.ops.object.mode_set(mode="OBJECT")
# 查找新创建的对象
new_obj = self._find_new_separated_object(context, body_obj)
if new_obj is None:
return None
# 生成唯一名称
base_name = self._generate_object_name(mat_info.name)
unique_name = generate_unique_name(base_name, existing_names)
new_obj.name = unique_name
# 设置父子关系保持与原Armature的关系
new_obj.parent = armature
new_obj.parent_type = "OBJECT"
return new_obj
def _find_new_separated_object(
self, context: Context, original_obj: "Object"
) -> Optional["Object"]:
"""查找分离操作后新创建的对象"""
# 分离后,新对象会被选中,原对象也会被选中
for obj in context.selected_objects:
if obj != original_obj and obj.type == "MESH":
return obj
return None
def _generate_object_name(self, material_name: str) -> str:
"""根据材质名称生成对象名称"""
# 简化材质名称,移除常见前缀和后缀
simplified_name = material_name
# 移除 "(Instance)" 后缀
if simplified_name.endswith(" (Instance)"):
simplified_name = simplified_name[:-11]
# 移除常见的VRM材质前缀
prefixes_to_remove = ["N00_000_00_", "N00_001_00_", "N00_002_00_"]
for prefix in prefixes_to_remove:
if simplified_name.startswith(prefix):
simplified_name = simplified_name[len(prefix) :]
break
return f"Body_{simplified_name}"