- 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
221 lines
7.2 KiB
Python
221 lines
7.2 KiB
Python
# 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}"
|