注意!为了优化多子材质物体,会清除没有选择集的材质,出错的请自行补上。
【标签脚本】清除选择和材质tag.py

源码:
python
import c4d
import traceback
class CleanupDialog(c4d.gui.GeDialog):
"""C4D标签&材质清理工具(兼容全版本)- 新增孤立点清理"""
ID_BTN_RUN = 1000
ID_TEXT_LOG = 1001
ID_CHECK_POINT_CLEAN = 1002 # 新增孤立点清理勾选框
def __init__(self):
c4d.gui.GeDialog.__init__(self)
self.log_content = ""
self.is_running = False # 防止重复执行
def CreateLayout(self):
"""兼容版可视化界面(修复AddCheckbox参数错误)"""
self.SetTitle("C4D标签&材质+模型优化工具")
# 核心修复:AddButton参数格式(布局标识 + 宽度 + 高度 + 按钮文本)
self.AddButton(self.ID_BTN_RUN, c4d.BFH_CENTER | c4d.BFV_TOP, 200, 30, "执行清理(先选对象)")
# 修复AddCheckbox参数:仅传递5个参数(id, flags, width, height, name)
self.AddCheckbox(self.ID_CHECK_POINT_CLEAN, c4d.BFH_LEFT | c4d.BFV_TOP, 300, 15,
"清理无依赖面的孤立点(模型优化)")
self.SetBool(self.ID_CHECK_POINT_CLEAN, True) # 默认勾选
# 日志标题
self.AddStaticText(0, c4d.BFH_LEFT | c4d.BFV_TOP, 0, 15, "运行日志")
# 日志区域
self.AddMultiLineEditText(self.ID_TEXT_LOG, c4d.BFH_SCALE | c4d.BFV_SCALE, 580, 320)
try:
# 兼容不同版本的只读标识
self.SetEditTextFlags(self.ID_TEXT_LOG, c4d.EDITTEXTFLAGS_READONLY)
except:
try:
self.SetEditTextFlags(self.ID_TEXT_LOG, 2) # 旧版本只读标识
except:
pass
return True
def AddLog(self, text):
"""添加并刷新日志(实时滚动到底部)"""
self.log_content += text + "\n"
try:
self.SetString(self.ID_TEXT_LOG, self.log_content)
# 兼容滚动逻辑(避免版本差异报错)
try:
self.SendMsgToChild(self.ID_TEXT_LOG, 100, c4d.BaseContainer(), len(self.log_content))
except:
pass
except:
print(text) # 降级输出到控制台
def ClearLog(self):
"""清空日志"""
self.log_content = ""
try:
self.SetString(self.ID_TEXT_LOG, "")
except:
pass
def is_material_used(self, doc, material):
"""
精准判断材质是否被使用(等效C4D原生逻辑)
:param doc: 文档对象
:param material: 要判断的材质
:return: True=被使用,False=未被使用
"""
def traverse_objects(obj):
if not obj:
return False
# 检查当前对象的材质标签
for tag in obj.GetTags():
if tag.CheckType(c4d.Ttexture):
if tag.GetMaterial() == material:
return True
# 递归检查子对象
for child in obj.GetChildren():
if traverse_objects(child):
return True
return False
# 检查文档所有根对象
for root_obj in doc.GetObjects():
if traverse_objects(root_obj):
return True
return False
def delete_unused_points(self, doc, obj):
"""
修复版:基于C4D官方API删除孤立点
核心:重构点数组+多边形索引,通过ResizeObject生效
"""
deleted_point_count = 0
obj_name = obj.GetName() if hasattr(obj, 'GetName') else "未知对象"
# 仅处理多边形对象
if not obj.CheckType(c4d.Opolygon):
self.AddLog(f" ⚠️ {obj_name} 不是多边形对象,跳过孤立点清理")
return deleted_point_count
try:
# 1. 获取原始数据
original_points = obj.GetAllPoints() # 原始点坐标数组
original_polys = obj.GetAllPolygons() # 原始多边形数组
point_count = len(original_points)
poly_count = len(original_polys)
if point_count == 0:
self.AddLog(f" ℹ️ {obj_name} 无顶点数据,跳过孤立点清理")
return deleted_point_count
# 2. 标记被多边形使用的点
used_points = set()
for poly in original_polys:
used_points.add(poly.a)
used_points.add(poly.b)
used_points.add(poly.c)
if poly.c != poly.d:
used_points.add(poly.d)
unused_count = point_count - len(used_points)
if unused_count == 0:
self.AddLog(f" ℹ️ {obj_name} 无孤立点需要清理(总顶点数:{point_count})")
return deleted_point_count
# 3. 构建新数据:仅保留使用的点 + 重构多边形索引
self.AddLog(f" 📊 {obj_name} - 总顶点数:{point_count} | 孤立点数:{unused_count}")
# 映射:原始点索引 → 新点索引
old_to_new_index = {}
new_points = []
new_index = 0
for old_idx in range(point_count):
if old_idx in used_points:
old_to_new_index[old_idx] = new_index
new_points.append(original_points[old_idx])
new_index += 1
# 重构多边形(更新顶点索引)
new_polys = []
for poly in original_polys:
# 替换每个顶点的旧索引为新索引
a = old_to_new_index[poly.a]
b = old_to_new_index[poly.b]
c = old_to_new_index[poly.c]
d = old_to_new_index[poly.d] if poly.c != poly.d else c
new_polys.append(c4d.CPolygon(a, b, c, d))
# 4. 应用修改(核心:ResizeObject + 重新赋值)
doc.AddUndo(c4d.UNDOTYPE_CHANGE, obj)
obj.ResizeObject(len(new_points), len(new_polys)) # 重置点/多边形数量
obj.SetAllPoints(new_points) # 设置新点坐标
for i in range(len(new_polys)):
obj.SetPolygon(i, new_polys[i]) # 设置新多边形
# 5. 刷新对象(关键:必须调用Message更新)
obj.Message(c4d.MSG_UPDATE)
obj.Message(c4d.MSG_CHANGE)
c4d.EventAdd()
deleted_point_count = unused_count
self.AddLog(f" ✂️ {obj_name} - 成功删除 {deleted_point_count} 个孤立点")
except Exception as e:
self.AddLog(f" ❌ {obj_name} 孤立点清理失败:{str(e)}")
self.AddLog(f" 📝 错误详情:{traceback.format_exc()[:200]}")
return deleted_point_count
def CleanupProcess(self):
"""核心清理逻辑(加锁防止重复执行)"""
if self.is_running:
self.AddLog("⚠️ 清理操作正在执行中,请等待!")
return
self.is_running = True
self.ClearLog()
self.AddLog("===== 开始执行清理操作 =====")
doc = c4d.documents.GetActiveDocument()
doc.StartUndo() # 开启撤销群组
# 统计变量
deleted_sel_tag_count = 0
deleted_mat_tag_count = 0
deleted_material_count = 0
deleted_point_count = 0 # 新增孤立点统计
# 获取是否启用孤立点清理
clean_points = self.GetBool(self.ID_CHECK_POINT_CLEAN) if hasattr(self, 'GetBool') else True
try:
# 兼容多版本获取选中对象
try:
objs = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
except:
objs = doc.GetActiveObjects(1) if hasattr(doc, 'GetActiveObjects') else []
if not objs:
self.AddLog("❌ 错误:请先在C4D主窗口选中需要处理的对象!")
self.is_running = False
return
# 遍历处理选中对象
for obj in objs:
obj_name = obj.GetName() if hasattr(obj, 'GetName') else "未知对象"
self.AddLog(f"\n📌 正在处理对象:{obj_name}")
# 1. 清理空多边形选择标签(精准判断空选择集)
poly_sel_tags = [tag for tag in obj.GetTags() if tag.CheckType(c4d.Tpolygonselection)]
valid_poly_sel_names = set()
for sel_tag in poly_sel_tags:
# 安全获取选择面数
sel_count = 0
try:
sel_base = sel_tag.GetBaseSelect()
sel_count = sel_base.GetCount() if sel_base else 0
except:
sel_count = 0
tag_name = sel_tag.GetName() if hasattr(sel_tag, 'GetName') else "未知标签"
self.AddLog(f" 📄 选择标签[{tag_name}] → 选择面数:{sel_count}")
if sel_count == 0:
self.AddLog(f" ✂️ 删除空选择标签:{tag_name}")
doc.AddUndo(c4d.UNDOTYPE_DELETE, sel_tag)
sel_tag.Remove()
deleted_sel_tag_count += 1
else:
valid_poly_sel_names.add(tag_name)
# 2. 清理无效材质标签(精准判断引用有效性)
material_tags = [tag for tag in obj.GetTags() if tag.CheckType(c4d.Ttexture)]
for mat_tag in material_tags:
# 安全获取选择集名称
mat_sel_name = ""
try:
mat_sel_name = mat_tag[c4d.TEXTURETAG_RESTRICTION] or ""
except:
mat_sel_name = ""
# 安全获取材质信息
mat = mat_tag.GetMaterial() if hasattr(mat_tag, 'GetMaterial') else None
mat_name = mat.GetName() if (mat and hasattr(mat, 'GetName')) else "无材质"
# 判断删除条件
need_delete = False
delete_reason = ""
if not mat_sel_name:
need_delete = True
delete_reason = "未设置任何选择集"
elif mat_sel_name not in valid_poly_sel_names:
need_delete = True
delete_reason = f"引用无效选择集:{mat_sel_name}"
if need_delete:
self.AddLog(f" ✂️ 删除材质标签[{mat_name}]:{delete_reason}")
doc.AddUndo(c4d.UNDOTYPE_DELETE, mat_tag)
mat_tag.Remove()
deleted_mat_tag_count += 1
# 3. 新增:清理孤立点(可选功能)
if clean_points:
deleted_point_count += self.delete_unused_points(doc, obj)
# 4. 清理未使用材质
self.AddLog("\n===== 开始清理未使用材质 =====")
all_materials = doc.GetMaterials() if hasattr(doc, 'GetMaterials') else []
for mat in all_materials:
if not self.is_material_used(doc, mat):
mat_name = mat.GetName() if hasattr(mat, 'GetName') else "未知材质"
self.AddLog(f" ✂️ 删除未使用材质:{mat_name}")
doc.AddUndo(c4d.UNDOTYPE_DELETE, mat)
mat.Remove()
deleted_material_count += 1
# 清理结果汇总
self.AddLog("\n===== 清理完成!汇总信息 =====")
self.AddLog(f"✅ 共删除 {deleted_sel_tag_count} 个空多边形选择标签")
self.AddLog(f"✅ 共删除 {deleted_mat_tag_count} 个无效材质标签")
self.AddLog(f"✅ 共删除 {deleted_material_count} 个未使用材质")
if clean_points:
self.AddLog(f"✅ 共删除 {deleted_point_count} 个无依赖面的孤立点")
if deleted_sel_tag_count + deleted_mat_tag_count + deleted_material_count + deleted_point_count == 0:
self.AddLog("ℹ️ 未发现需要清理的内容")
# 提交撤销并刷新界面
doc.EndUndo()
c4d.EventAdd()
except Exception as e:
self.AddLog(f"\n❌ 执行出错:{str(e)}")
self.AddLog(f"❌ 错误详情:{traceback.format_exc()}")
doc.EndUndo() # 确保撤销群组关闭
finally:
self.is_running = False # 释放执行锁
def Command(self, id, msg):
"""按钮点击事件"""
if id == self.ID_BTN_RUN:
self.CleanupProcess()
return True
# 全局窗口实例(防止重复创建)
g_dlg = None
def main():
"""启动工具(兼容全版本)"""
global g_dlg
try:
# 关闭已有窗口
if g_dlg and g_dlg.IsOpen():
g_dlg.Close()
# 创建非阻塞窗口(移除SetAlwaysOnTop兼容低版本)
g_dlg = CleanupDialog()
g_dlg.Open(
c4d.DLG_TYPE_ASYNC, # 非阻塞模式(核心)
xpos=-1, ypos=-1,
defaultw=400, defaulth=450 # 调整窗口大小适配新选项
)
except Exception as e:
# 窗口启动失败时降级执行核心逻辑
print(f"⚠️ 窗口启动失败:{str(e)}")
print("===== 直接执行清理逻辑 =====")
doc = c4d.documents.GetActiveDocument()
try:
objs = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
except:
objs = doc.GetActiveObjects(1) if hasattr(doc, 'GetActiveObjects') else []
if not objs:
print("❌ 请先选中需要处理的对象!")
else:
dlg = CleanupDialog()
dlg.CleanupProcess()
if __name__ == "__main__":
main()
【界面脚本】