feat(body_mesh): Add edge UV data for facial and hair mesh components
- Add edge UV coordinate data for N00_000_00_FaceEyelash_00_FACE mesh instance with 112 edges - Add edge UV coordinate data for N00_000_00_HairBack_00_HAIR mesh instance with 800 edges - Update body_mesh module initialization and operations to support new mesh data - Enhance panel UI and utility functions for mesh edge UV handling - Add comprehensive UV matching test suite for validation - Update registration module to include new mesh components - Improve test utilities for edge UV data verification
This commit is contained in:
@@ -1,28 +1,58 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Set as AbstractSet
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context, Operator
|
||||
|
||||
from .utils import (
|
||||
AUTO_CONVERT_MATERIALS,
|
||||
PROCESSING_ORDER,
|
||||
MaterialInfo,
|
||||
find_body_mesh,
|
||||
find_face_mesh,
|
||||
generate_unique_name,
|
||||
get_material_face_count,
|
||||
get_target_mesh_name,
|
||||
is_retained_material,
|
||||
is_separable_material,
|
||||
is_skin_material,
|
||||
isolate_material_faces,
|
||||
load_edge_uv_data,
|
||||
select_edges_by_uv,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bpy.types import Object
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessResult:
|
||||
"""材质处理结果"""
|
||||
material_name: str
|
||||
mesh_name: str
|
||||
edges_selected: int
|
||||
edges_dissolved: int
|
||||
success: bool
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MergeOperationState:
|
||||
"""操作状态,用于错误恢复"""
|
||||
original_mode: str
|
||||
original_active_object: Optional["Object"]
|
||||
original_selected_objects: list["Object"]
|
||||
processed_materials: list[str]
|
||||
|
||||
|
||||
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_description = "将Body网格中可分离材质(CLOTH和其他)的面合并分离为单个独立对象"
|
||||
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
@@ -36,13 +66,13 @@ class VRM_OT_separate_non_skin_faces(Operator):
|
||||
if body_mesh is None:
|
||||
return False
|
||||
|
||||
# 检查是否有非皮肤材质
|
||||
# 检查是否有可分离材质(使用is_separable_material)
|
||||
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):
|
||||
if material is not None and is_separable_material(material.name):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -88,13 +118,13 @@ class VRM_OT_separate_non_skin_faces(Operator):
|
||||
self.report({"ERROR"}, "Body网格数据无效")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# 收集非皮肤材质信息
|
||||
non_skin_materials: list[MaterialInfo] = []
|
||||
# 收集可分离材质信息(使用is_separable_material)
|
||||
separable_materials: list[MaterialInfo] = []
|
||||
for i, material in enumerate(mesh_data.materials):
|
||||
if material is not None and not is_skin_material(material.name):
|
||||
if material is not None and is_separable_material(material.name):
|
||||
face_count = get_material_face_count(mesh_data, i)
|
||||
if face_count > 0:
|
||||
non_skin_materials.append(
|
||||
separable_materials.append(
|
||||
MaterialInfo(
|
||||
name=material.name,
|
||||
index=i,
|
||||
@@ -103,39 +133,40 @@ class VRM_OT_separate_non_skin_faces(Operator):
|
||||
)
|
||||
)
|
||||
|
||||
if not non_skin_materials:
|
||||
self.report({"INFO"}, "没有找到非皮肤材质的面")
|
||||
if not separable_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)
|
||||
# 将所有可分离材质的面合并分离为单个对象
|
||||
material_indices = [mat_info.index for mat_info in separable_materials]
|
||||
new_obj = self._separate_all_material_faces(
|
||||
context, body_mesh, armature, material_indices, existing_names
|
||||
)
|
||||
|
||||
if separated_count > 0:
|
||||
self.report({"INFO"}, f"成功分离了 {separated_count} 个材质的面")
|
||||
if new_obj is not None:
|
||||
total_faces = sum(mat_info.face_count for mat_info in separable_materials)
|
||||
material_names = [mat_info.name for mat_info in separable_materials]
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"成功分离了 {len(separable_materials)} 个材质的 {total_faces} 个面到 {new_obj.name}"
|
||||
)
|
||||
return {"FINISHED"}
|
||||
else:
|
||||
self.report({"WARNING"}, "没有成功分离任何面")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def _separate_material_faces(
|
||||
def _separate_all_material_faces(
|
||||
self,
|
||||
context: Context,
|
||||
body_obj: "Object",
|
||||
armature: "Object",
|
||||
mat_info: MaterialInfo,
|
||||
material_indices: list[int],
|
||||
existing_names: set[str],
|
||||
) -> Optional["Object"]:
|
||||
"""分离指定材质的面为新对象"""
|
||||
"""将所有指定材质的面合并分离为单个新对象"""
|
||||
mesh_data = body_obj.data
|
||||
if mesh_data is None:
|
||||
return None
|
||||
@@ -163,9 +194,10 @@ class VRM_OT_separate_non_skin_faces(Operator):
|
||||
# 返回对象模式以访问网格数据
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# 选择使用指定材质的面
|
||||
# 选择所有使用可分离材质的面
|
||||
material_index_set = set(material_indices)
|
||||
for polygon in mesh_data.polygons:
|
||||
if polygon.material_index == mat_info.index:
|
||||
if polygon.material_index in material_index_set:
|
||||
polygon.select = True
|
||||
|
||||
# 返回编辑模式执行分离
|
||||
@@ -182,8 +214,8 @@ class VRM_OT_separate_non_skin_faces(Operator):
|
||||
if new_obj is None:
|
||||
return None
|
||||
|
||||
# 生成唯一名称
|
||||
base_name = self._generate_object_name(mat_info.name)
|
||||
# 使用固定名称"Body_Separated",处理命名冲突
|
||||
base_name = "Body_Separated"
|
||||
unique_name = generate_unique_name(base_name, existing_names)
|
||||
new_obj.name = unique_name
|
||||
|
||||
@@ -203,18 +235,405 @@ class VRM_OT_separate_non_skin_faces(Operator):
|
||||
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}"
|
||||
class VRM_OT_merge_uv_edges(Operator):
|
||||
"""根据UV坐标融并边,将三角面转四边面"""
|
||||
|
||||
bl_idname = "vrm.merge_uv_edges"
|
||||
bl_label = "Merge UV Edges"
|
||||
bl_description = "根据UV坐标选中并融并边,将三角面转换为四边面"
|
||||
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
"""
|
||||
检查是否可以执行操作
|
||||
|
||||
Requirements 1.1, 1.2, 1.3, 1.4:
|
||||
- 检测Armature对象或其子网格对象
|
||||
- 检测Body和Face网格是否存在
|
||||
- 在任意Blender模式下工作
|
||||
"""
|
||||
armature = cls._get_armature(context)
|
||||
if armature is None:
|
||||
return False
|
||||
|
||||
# Requirements 1.1, 1.2: 检查Body和Face网格是否都存在
|
||||
body_mesh = find_body_mesh(armature)
|
||||
face_mesh = find_face_mesh(armature)
|
||||
|
||||
return body_mesh is not None and face_mesh is not None
|
||||
|
||||
@classmethod
|
||||
def _get_armature(cls, context: Context) -> Optional["Object"]:
|
||||
"""
|
||||
获取当前选中的Armature对象
|
||||
|
||||
Requirements 1.1: 支持选中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]:
|
||||
"""
|
||||
执行边融并操作
|
||||
|
||||
Requirements 5.3, 5.4, 6.1, 6.2:
|
||||
- 保存原始状态(模式、选择、活动对象)
|
||||
- 按PROCESSING_ORDER遍历材质
|
||||
- 调用_process_material处理每个材质
|
||||
- 恢复状态并报告结果
|
||||
"""
|
||||
# 保存原始状态
|
||||
state = self._save_state(context)
|
||||
|
||||
try:
|
||||
return self._execute_merge(context, state)
|
||||
except Exception as e:
|
||||
# 发生错误时恢复状态
|
||||
self._restore_state(context, state)
|
||||
self.report({"ERROR"}, f"边融并操作失败: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def _save_state(self, context: Context) -> MergeOperationState:
|
||||
"""保存当前Blender状态"""
|
||||
original_mode = context.object.mode if context.object else "OBJECT"
|
||||
original_active = context.view_layer.objects.active
|
||||
original_selected = list(context.selected_objects)
|
||||
|
||||
return MergeOperationState(
|
||||
original_mode=original_mode,
|
||||
original_active_object=original_active,
|
||||
original_selected_objects=original_selected,
|
||||
processed_materials=[],
|
||||
)
|
||||
|
||||
def _restore_state(self, context: Context, state: MergeOperationState) -> None:
|
||||
"""
|
||||
恢复原始Blender状态
|
||||
|
||||
Requirements 7.4: 错误时恢复原始状态
|
||||
"""
|
||||
try:
|
||||
# 确保在对象模式
|
||||
if context.object and context.object.mode != "OBJECT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# 取消所有选择
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
# 恢复原始选择
|
||||
for obj in state.original_selected_objects:
|
||||
if obj and obj.name in context.blend_data.objects:
|
||||
obj.select_set(True)
|
||||
|
||||
# 恢复原始活动对象
|
||||
if state.original_active_object and \
|
||||
state.original_active_object.name in context.blend_data.objects:
|
||||
context.view_layer.objects.active = state.original_active_object
|
||||
|
||||
# 恢复原始模式
|
||||
if context.object and state.original_mode != "OBJECT":
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode=state.original_mode)
|
||||
except RuntimeError:
|
||||
pass # 某些模式可能无法恢复
|
||||
except Exception:
|
||||
pass # 恢复失败时静默处理
|
||||
|
||||
def _execute_merge(
|
||||
self, context: Context, state: MergeOperationState
|
||||
) -> set[str]:
|
||||
"""
|
||||
执行边融并的核心逻辑
|
||||
|
||||
Requirements 6.1, 6.2: 按顺序处理材质,先Body后Face
|
||||
"""
|
||||
armature = self._get_armature(context)
|
||||
if armature is None:
|
||||
self.report({"ERROR"}, "未找到Armature对象")
|
||||
return {"CANCELLED"}
|
||||
|
||||
body_mesh = find_body_mesh(armature)
|
||||
face_mesh = find_face_mesh(armature)
|
||||
|
||||
if body_mesh is None or face_mesh is None:
|
||||
self.report({"ERROR"}, "未找到Body或Face网格对象")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# 确保在对象模式开始
|
||||
if context.object and context.object.mode != "OBJECT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# 处理结果统计
|
||||
results: list[ProcessResult] = []
|
||||
total_edges_dissolved = 0
|
||||
materials_processed = 0
|
||||
materials_skipped = 0
|
||||
|
||||
# 按PROCESSING_ORDER遍历材质 (Requirements 6.1, 6.2)
|
||||
for material_name in PROCESSING_ORDER:
|
||||
# 确定目标网格
|
||||
target_mesh_name = get_target_mesh_name(material_name)
|
||||
if target_mesh_name is None:
|
||||
continue
|
||||
|
||||
target_mesh = body_mesh if target_mesh_name == "Body" else face_mesh
|
||||
|
||||
# 处理材质
|
||||
result = self._process_material(context, target_mesh, material_name)
|
||||
results.append(result)
|
||||
state.processed_materials.append(material_name)
|
||||
|
||||
if result.success:
|
||||
materials_processed += 1
|
||||
total_edges_dissolved += result.edges_dissolved
|
||||
else:
|
||||
materials_skipped += 1
|
||||
if result.error_message:
|
||||
self.report({"WARNING"}, result.error_message)
|
||||
|
||||
# 确保返回对象模式 (Requirements 5.4)
|
||||
if context.object and context.object.mode != "OBJECT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# 报告结果 (Requirements 6.4)
|
||||
if materials_processed > 0:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"处理完成: {materials_processed} 个材质, "
|
||||
f"融并 {total_edges_dissolved} 条边"
|
||||
)
|
||||
return {"FINISHED"}
|
||||
else:
|
||||
self.report({"WARNING"}, f"没有成功处理任何材质 (跳过 {materials_skipped} 个)")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def _process_material(
|
||||
self,
|
||||
context: Context,
|
||||
mesh_obj: "Object",
|
||||
material_name: str,
|
||||
) -> ProcessResult:
|
||||
"""
|
||||
处理单个材质的边融并
|
||||
|
||||
Requirements 5.1, 5.2, 6.3, 7.1, 7.2:
|
||||
- 确定目标网格(Body或Face)
|
||||
- 加载JSON边数据
|
||||
- 调用isolate_material_faces隔离材质
|
||||
- 调用select_edges_by_uv选择边
|
||||
- 执行dissolve_edges融并
|
||||
- 取消隐藏(Alt+H)
|
||||
- 处理缺失JSON文件 (7.1)
|
||||
- 处理无匹配边情况 (7.2)
|
||||
"""
|
||||
import bmesh
|
||||
|
||||
mesh_name = mesh_obj.name if mesh_obj else "Unknown"
|
||||
|
||||
# 检查是否使用自动转换(tris_convert_to_quads)
|
||||
if material_name in AUTO_CONVERT_MATERIALS:
|
||||
return self._process_material_auto_convert(context, mesh_obj, material_name)
|
||||
|
||||
try:
|
||||
# 加载JSON边数据 (Requirements 5.1, 7.1)
|
||||
target_uvs = load_edge_uv_data(material_name)
|
||||
if target_uvs is None:
|
||||
# Requirements 7.1: 记录警告并跳过
|
||||
return ProcessResult(
|
||||
material_name=material_name,
|
||||
mesh_name=mesh_name,
|
||||
edges_selected=0,
|
||||
edges_dissolved=0,
|
||||
success=False,
|
||||
error_message=f"跳过: 未找到材质 {material_name} 的JSON文件",
|
||||
)
|
||||
|
||||
if len(target_uvs) == 0:
|
||||
return ProcessResult(
|
||||
material_name=material_name,
|
||||
mesh_name=mesh_name,
|
||||
edges_selected=0,
|
||||
edges_dissolved=0,
|
||||
success=False,
|
||||
error_message=f"跳过: 材质 {material_name} 的JSON文件中没有边数据",
|
||||
)
|
||||
|
||||
# 隔离材质面 (Requirements 5.2)
|
||||
if not isolate_material_faces(context, mesh_obj, material_name):
|
||||
return ProcessResult(
|
||||
material_name=material_name,
|
||||
mesh_name=mesh_name,
|
||||
edges_selected=0,
|
||||
edges_dissolved=0,
|
||||
success=False,
|
||||
error_message=f"跳过: 无法隔离材质 {material_name} 的面",
|
||||
)
|
||||
|
||||
# 此时应该在编辑模式,边选择模式
|
||||
# 取消所有边选择
|
||||
bpy.ops.mesh.select_all(action="DESELECT")
|
||||
|
||||
# 使用BMesh选择边
|
||||
mesh_data = mesh_obj.data
|
||||
bm = bmesh.from_edit_mesh(mesh_data)
|
||||
|
||||
# 获取UV层
|
||||
uv_layer = bm.loops.layers.uv.active
|
||||
if uv_layer is None:
|
||||
# 取消隐藏并返回
|
||||
bpy.ops.mesh.reveal()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
return ProcessResult(
|
||||
material_name=material_name,
|
||||
mesh_name=mesh_name,
|
||||
edges_selected=0,
|
||||
edges_dissolved=0,
|
||||
success=False,
|
||||
error_message=f"跳过: 网格 {mesh_name} 没有UV层",
|
||||
)
|
||||
|
||||
# 选择匹配UV的边 (Requirements 5.2)
|
||||
edges_selected = select_edges_by_uv(bm, uv_layer, target_uvs)
|
||||
|
||||
# 更新BMesh
|
||||
bmesh.update_edit_mesh(mesh_data)
|
||||
|
||||
# Requirements 7.2: 处理无匹配边情况
|
||||
if edges_selected == 0:
|
||||
# 取消隐藏并返回
|
||||
bpy.ops.mesh.reveal()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
return ProcessResult(
|
||||
material_name=material_name,
|
||||
mesh_name=mesh_name,
|
||||
edges_selected=0,
|
||||
edges_dissolved=0,
|
||||
success=False,
|
||||
error_message=(
|
||||
f"跳过: 材质 {material_name} 没有找到匹配的边 "
|
||||
f"(目标边数: {len(target_uvs)})"
|
||||
),
|
||||
)
|
||||
|
||||
# 执行边融并 (Requirements 5.1)
|
||||
edges_dissolved = 0
|
||||
try:
|
||||
bpy.ops.mesh.dissolve_edges()
|
||||
edges_dissolved = edges_selected
|
||||
except RuntimeError as e:
|
||||
# 融并失败,可能是边不可融并,但不算完全失败
|
||||
self.report({"WARNING"}, f"材质 {material_name} 融并边时出现问题: {str(e)}")
|
||||
|
||||
# 取消隐藏 (Alt+H equivalent) (Requirements 5.2)
|
||||
bpy.ops.mesh.reveal()
|
||||
|
||||
# 返回对象模式 (Requirements 6.3)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
return ProcessResult(
|
||||
material_name=material_name,
|
||||
mesh_name=mesh_name,
|
||||
edges_selected=edges_selected,
|
||||
edges_dissolved=edges_dissolved,
|
||||
success=True,
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Requirements 7.4: 捕获异常,尝试恢复状态
|
||||
try:
|
||||
# 尝试取消隐藏
|
||||
if context.object and context.object.mode == "EDIT":
|
||||
bpy.ops.mesh.reveal()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ProcessResult(
|
||||
material_name=material_name,
|
||||
mesh_name=mesh_name,
|
||||
edges_selected=0,
|
||||
edges_dissolved=0,
|
||||
success=False,
|
||||
error_message=f"处理材质 {material_name} 时发生错误: {str(e)}",
|
||||
)
|
||||
|
||||
def _process_material_auto_convert(
|
||||
self,
|
||||
context: Context,
|
||||
mesh_obj: "Object",
|
||||
material_name: str,
|
||||
) -> ProcessResult:
|
||||
"""
|
||||
使用Blender自带的tris_convert_to_quads处理规律网格的材质
|
||||
|
||||
对于网格结构规律的材质,直接使用Blender的自动转换功能,
|
||||
不需要基于UV坐标的边选择。
|
||||
"""
|
||||
mesh_name = mesh_obj.name if mesh_obj else "Unknown"
|
||||
|
||||
try:
|
||||
# 隔离材质面
|
||||
if not isolate_material_faces(context, mesh_obj, material_name):
|
||||
return ProcessResult(
|
||||
material_name=material_name,
|
||||
mesh_name=mesh_name,
|
||||
edges_selected=0,
|
||||
edges_dissolved=0,
|
||||
success=False,
|
||||
error_message=f"跳过: 无法隔离材质 {material_name} 的面",
|
||||
)
|
||||
|
||||
# 切换到面选择模式
|
||||
bpy.ops.mesh.select_mode(type="FACE")
|
||||
|
||||
# 选择所有可见的面
|
||||
bpy.ops.mesh.select_all(action="SELECT")
|
||||
|
||||
# 使用Blender自带的三角面转四边面
|
||||
bpy.ops.mesh.tris_convert_to_quads()
|
||||
|
||||
# 取消隐藏
|
||||
bpy.ops.mesh.reveal()
|
||||
|
||||
# 返回对象模式
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
return ProcessResult(
|
||||
material_name=material_name,
|
||||
mesh_name=mesh_name,
|
||||
edges_selected=0,
|
||||
edges_dissolved=0,
|
||||
success=True,
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# 捕获异常,尝试恢复状态
|
||||
try:
|
||||
if context.object and context.object.mode == "EDIT":
|
||||
bpy.ops.mesh.reveal()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ProcessResult(
|
||||
material_name=material_name,
|
||||
mesh_name=mesh_name,
|
||||
edges_selected=0,
|
||||
edges_dissolved=0,
|
||||
success=False,
|
||||
error_message=f"处理材质 {material_name} 时发生错误: {str(e)}",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user