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
This commit is contained in:
220
editor/body_mesh/ops.py
Normal file
220
editor/body_mesh/ops.py
Normal file
@@ -0,0 +1,220 @@
|
||||
# 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}"
|
||||
Reference in New Issue
Block a user