# -*- coding: utf-8 -*-
import maya.api.OpenMaya as om
import maya.cmds as cmds
# 只能感知模式 且 加上材质自动转换 清除会恢复原来的材质
# =========================
# ✅ 核心检测逻辑 (保持 API 2.0 高速)
# =========================
def get_high_speed_overlap(source_mesh, target_mesh, threshold=0.15):
sel = om.MSelectionList()
try:
sel.add(source_mesh)
sel.add(target_mesh)
except: return []
source_dag, target_dag = sel.getDagPath(0), sel.getDagPath(1)
source_fn, target_fn = om.MFnMesh(source_dag), om.MFnMesh(target_dag)
points = source_fn.getPoints(om.MSpace.kWorld)
normals = source_fn.getVertexNormals(False, om.MSpace.kWorld)
bad_vtx_indices = []
cmds.progressWindow(title='模型间检测中...', progress=0, max=len(points), isInterruptable=True)
for i in range(len(points)):
if i % 1000 == 0:
if cmds.progressWindow(query=True, isCancelled=True): break
cmds.progressWindow(edit=True, progress=i)
p, n = points[i], normals[i]
cp, _ = target_fn.getClosestPoint(p, om.MSpace.kWorld)
if p.distanceTo(cp) < threshold:
bad_vtx_indices.append(i)
continue
ray_dir = om.MFloatVector(-n.x, -n.y, -n.z)
hit = target_fn.closestIntersection(
om.MFloatPoint(p.x + n.x*0.001, p.y + n.y*0.001, p.z + n.z*0.001),
ray_dir, om.MSpace.kWorld, 100.0, False)
if hit and hit[1] < 2.0:
bad_vtx_indices.append(i)
cmds.progressWindow(endProgress=True)
return bad_vtx_indices
def get_self_intersection_fast(mesh, threshold=0.2):
sel = om.MSelectionList()
try: sel.add(mesh)
except: return []
fn = om.MFnMesh(sel.getDagPath(0))
points, (counts, indices) = fn.getPoints(om.MSpace.kWorld), fn.getVertices()
bad_vertices, offset = set(), 0
cmds.progressWindow(title='自穿插检测中...', progress=0, max=len(counts), isInterruptable=True)
for fid, vcnt in enumerate(counts):
if fid % 500 == 0:
if cmds.progressWindow(query=True, isCancelled=True): break
cmds.progressWindow(edit=True, progress=fid)
f_idx = indices[offset:offset+vcnt]
offset += vcnt
center = om.MPoint(
sum([points[v].x for v in f_idx])/vcnt,
sum([points[v].y for v in f_idx])/vcnt,
sum([points[v].z for v in f_idx])/vcnt
)
norm = fn.getPolygonNormal(fid, om.MSpace.kWorld)
ray_s = om.MFloatPoint(center.x + norm.x*0.001, center.y + norm.y*0.001, center.z + norm.z*0.001)
hit = fn.closestIntersection(ray_s, om.MFloatVector(norm), om.MSpace.kWorld, threshold, False)
if hit and hit[2] != fid:
for v in f_idx: bad_vertices.add(v)
cmds.progressWindow(endProgress=True)
return list(bad_vertices)
# =========================
# ✅ UI 类定义
# =========================
class SmartOverlapUI:
def __init__(self):
self.win_name = "SmartOverlapUI_V3"
self.last_mesh = None
self.last_bad_vtx = []
self.original_materials = {} # 用于存储原始材质:{ mesh_name: shading_group }
self.temp_lambert_sg = "OverlapCheck_Lambert_SG"
self.create_ui()
def create_ui(self):
if cmds.window(self.win_name, exists=True):
cmds.deleteUI(self.win_name)
self.win = cmds.window(self.win_name, title="晴空穿插检查工具", widthHeight=(350, 280), sizeable=True)
self.root_form = cmds.formLayout(numberOfDivisions=100)
self.top_col = cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
cmds.text(l="智能感知模式开启 (含材质自动转换)", fn="boldLabelFont", height=30, bgc=[0.25, 0.25, 0.25])
cmds.text(l=" • 检测时自动转为灰色 Lambert\n • 点击 [清除颜色] 恢复原始材质", al="left", height=40)
# cmds.text(l="智能感知模式开启", fn="boldLabelFont", height=30, bgc=[0.25, 0.25, 0.25])
cmds.text(l=" • 选 1 个:检测自穿插\n • 选 2 个:前选被检,后选参考", al="left", height=40)
cmds.setParent(self.root_form)
self.mid_col = cmds.columnLayout(adjustableColumn=True, rowSpacing=15)
cmds.separator(style="in", h=10)
self.threshold_fld = cmds.floatSliderGrp(label='敏感度: ', field=True, minValue=0, maxValue=1.0,
value=0.15, step=0.01, columnWidth3=[60, 50, 180])
cmds.setParent(self.root_form)
self.run_btn = cmds.button(label="🚀 执行检测", height=50, bgc=[0.32, 0.52, 0.72], command=self.smart_run)
self.clear_btn = cmds.button(label="清除颜色并恢复材质", height=35, command=self.clear_all_and_restore)
self.select_btn = cmds.button(label="选问题点", height=35, command=self.select_last_bad)
cmds.formLayout(self.root_form, edit=True,
attachForm=[
(self.top_col, 'top', 5), (self.top_col, 'left', 10), (self.top_col, 'right', 10),
(self.mid_col, 'left', 10), (self.mid_col, 'right', 10),
(self.run_btn, 'left', 10), (self.run_btn, 'right', 10),
(self.clear_btn, 'left', 10), (self.clear_btn, 'bottom', 15),
(self.select_btn, 'right', 10), (self.select_btn, 'bottom', 15)
],
attachControl=[
(self.mid_col, 'top', 5, self.top_col),
(self.run_btn, 'top', 10, self.mid_col),
(self.clear_btn, 'top', 10, self.run_btn),
(self.select_btn, 'top', 10, self.run_btn)
],
attachPosition=[
(self.clear_btn, 'right', 5, 50),
(self.select_btn, 'left', 5, 50)
]
)
cmds.showWindow(self.win)
def _setup_temp_material(self):
""" 创建临时的灰色 Lambert 材质组 """
if not cmds.objExists(self.temp_lambert_sg):
shader = cmds.shadingNode('lambert', asShader=True, name="OverlapCheck_Lambert")
cmds.setAttr(shader + ".color", 0.5, 0.5, 0.5, type="double3")
self.temp_lambert_sg = cmds.sets(renderable=True, noSurfaceShader=True, empty=True, name=self.temp_lambert_sg)
cmds.connectAttr(shader + ".outColor", self.temp_lambert_sg + ".surfaceShader")
def _apply_temp_material(self, mesh):
""" 记录并应用临时材质 """
self._setup_temp_material()
# 获取物体当前的 ShadingEngine (SG)
shapes = cmds.listRelatives(mesh, shapes=True, fullPath=True)
if shapes:
sgs = cmds.listConnections(shapes[0], destination=True, type='shadingEngine')
if sgs:
self.original_materials[mesh] = sgs[0] # 记录第一个材质球
cmds.sets(mesh, edit=True, forceElement=self.temp_lambert_sg)
def clear_all_and_restore(self, *args):
""" 清除顶点色并恢复原始材质 """
sel = cmds.ls(sl=True, long=True) or ([self.last_mesh] if self.last_mesh else [])
for obj in [o for o in sel if cmds.objExists(o)]:
# 1. 清除顶点颜色
cmds.polyColorPerVertex(obj, remove=True)
cmds.setAttr(obj + ".displayColors", 0)
# 2. 恢复材质
if obj in self.original_materials:
sg = self.original_materials[obj]
if cmds.objExists(sg):
cmds.sets(obj, edit=True, forceElement=sg)
del self.original_materials[obj]
# 清理字典中已删除的物体
self.original_materials = {k: v for k, v in self.original_materials.items() if cmds.objExists(k)}
def select_last_bad(self, *args):
if self.last_bad_vtx: cmds.select(self.last_bad_vtx)
else: cmds.warning("没有发现可疑顶点。")
def smart_run(self, *args):
sel = cmds.ls(sl=True, long=True)
val = cmds.floatSliderGrp(self.threshold_fld, query=True, value=True)
if not sel:
cmds.warning("请在场景中选择模型!")
return
src = sel[0]
self.last_mesh = src
# 检测前:应用 Lambert 材质
self._apply_temp_material(src)
# 智能判定模式
if len(sel) == 1:
bad_indices = get_self_intersection_fast(src, val)
else:
bad_indices = get_high_speed_overlap(src, sel[1], val)
if bad_indices:
cmds.setAttr(src + ".displayColors", 1)
# 背景刷成深灰,让红点更明显
cmds.polyColorPerVertex(src, colorRGB=[0.2, 0.2, 0.2], alpha=1)
self.last_bad_vtx = ["{}.vtx[{}]".format(src, i) for i in bad_indices]
cmds.polyColorPerVertex(self.last_bad_vtx, colorRGB=[1, 0, 0], alpha=1)
cmds.confirmDialog(title='结果', message='检测到 %d 个穿插点\n已临时切换材质,点击[清除]按钮可恢复。' % len(bad_indices))
else:
self.last_bad_vtx = []
# 如果没问题,直接恢复材质
self.clear_all_and_restore()
cmds.confirmDialog(title='结果', message='干净!没有发现穿插。')
# =========================
# ✅ 启动函数
# =========================
def main():
return SmartOverlapUI()
if __name__ == "__main__":
import __main__
__main__.main = main
main()