前言

  这两天有朋友请教我 Maya 模型拓扑 检查的问题,通常来说检查工具是流程里面必不可少的环节。
  之前我在华强的时候也弄过流程开发的拓扑检查。
  我当时测了 Maya 内置的 Blendshape 拓扑检查,发现这个检查其实就是验证顶点数是否一致而已,因此当时没有细想,就这么简单粗暴地处理了。
  这两天我往更深的方向想了一下,点数一致并不一定就是拓扑相同的。

什么是拓扑

  拓扑其实就是布线,模型的点线面排布结构的统称,数学上也有拓扑学,我了解甚少,至少在 CG 行业里这么理解拓扑应该没有错。
  我之前也翻译过一个教程,用 Maya 讲解了拓扑的区别 Maya建模结构拓展训练教程

topo

  从截图可以看到,同样是球体但是可以用不同的布线来构成,外观看起来一样但是布线结构是完全不同的。
  通常来说只要符合 四边面构线 都是符合规范的,当然建模也尽量避免极点(多条线汇集的点),所以 Maya 默认的球体是 经纬线布线 ,这会导致上下有极点,其实并不是很好地拓扑结构。

  综上所述,拓扑就是布线结构,顶点的位置并不影响拓扑。

topo

  所以上图两个模型没有布线的修改的话,拓扑是完全一致的,即便这个形状看起来差距很大。

拓扑检查

  通常来说,拓扑的变化都会导致模型的点面线的数量变化,因此最简单粗暴地检查方法就是检查两个模型的点线面数量是否一致。
  Maya 的 Blendshape 属性也有 Check Topology 复选框,勾选也是检查顶点数是否一致。
  如何获取模型的点线面,这可以利用 cmds.polyEvaluate 来获取

  但是经过我再次思考之后,我发现有时候点线面数量完全相同,但是也会有拓扑不一致的情况。
  可以简单地演示这个问题。

topo

  细分之后,两个球体的拓扑结构是不相同的,而且符合建模的规范,但是点线面的数量是完全一样的。

方案一 遍历模型面的顶点序号 算 md5 进行比较

  这个方案是大神提供的解决方案,比较简单粗暴,直接有效。
  缺点就是效率太慢了,我用 OpenMaya 的方式进行了一定的优化来加快速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import json
import time
import hashlib
import pymel.core as pm
from maya import OpenMaya
from functools import partial, wraps

def logTime(func=None, msg="elapsed time:"):
"""logTime
log function running time

:param func: function get from decorators, defaults to None
:type func: function, optional
:param msg: default print message, defaults to "elapsed time:"
:type msg: str, optional
:return: decorator function return
:rtype: dynamic type
"""
if not func: return partial(logTime,msg=msg)
@wraps(func)
def wrapper(*args, **kwargs):
curr = time.time()
res = func(*args, **kwargs)
print(msg,time.time() - curr)
return res
return wrapper

class MeshTopology(object):
def __init__(self, mesh_node):
self.py_node = pm.PyNode(mesh_node)
if self.py_node.type() == u'transform':
self.py_node = self.py_node.getShape()
if self.py_node.type() != u'mesh':
raise TypeError('mesh_node must be a mesh type object.')

@property
def topology_structure(self):
# data = {}
# for f in self.py_node.f:
# data[f.index()] = f.getVertices()

# NOTE OpenMaya 优化
data = []
dag = self.py_node.__apimdagpath__()
itr = OpenMaya.MItMeshPolygon(dag)
while not itr.isDone():
vtx_list = OpenMaya.MIntArray()
itr.getVertices(vtx_list)
data.append(list(vtx_list))
itr.next()
return data

def to_json(self):
return json.dumps(self.topology_structure)

def to_md5(self):
return hashlib.md5(self.to_json()).hexdigest()

@logTime
def __eq__(self, other):
return self.to_md5() == other.to_md5()

if __name__ == "__main__":
i, j = pm.selected()
print(MeshTopology(i) == MeshTopology(j))

  节点类的写法非常牛逼,个人还不太适应这种高端写法,但是这种写法的确很适合在 Maya 里运用。
  原理其实也非常浅显,如果将 OpenMaya 的 while 去掉,用上面注释掉的 pymel 就更加清晰了。
  遍历函数所有的面,然后获取面上的顶点序号,这个是 面顶点序号 注意和 顶点序号 是不一样的。
  然后将顶点序号存储起来,通过 json 将字符串序列化,然后通过 md5 比较字符串是否有差异。

  缺点就是需要遍历所有的面,获取速度比较慢,经过 OpenMaya 优化之后大概快了 10 倍,但是40+万面的比较还是需要 2-3s。

方案二 环边选择过滤

  这个方案是利用 Maya 双击选择循环变的方式获取边的组成结构。
  如果拓扑一样的话,获取的循环边序号就是完全一致的。
  核心思想就是利用 cmds.polySelect(edgeLoop=int) 来选择给定序号的循环边。
  选到的循环边的边序号从模型所有边序号的集合中取出,直到模型所有边序号的集合取空为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import json
import time
import hashlib
import pymel.core as pm
from maya import OpenMaya
from functools import partial, wraps

def endPorgress(func):
def wrapper(*args, **kwargs):
res = None
try:
res = func(*args, **kwargs)
except:
import traceback
traceback.print_exc()
finally:
pm.progressWindow(ep=1)
return res
return wrapper

def logTime(func=None, msg="elapsed time:"):
"""logTime
log function running time

:param func: function get from decorators, defaults to None
:type func: function, optional
:param msg: default print message, defaults to "elapsed time:"
:type msg: str, optional
:return: decorator function return
:rtype: dynamic type
"""
if not func: return partial(logTime,msg=msg)
@wraps(func)
def wrapper(*args, **kwargs):
curr = time.time()
res = func(*args, **kwargs)
print(msg,time.time() - curr)
return res
return wrapper

@logTime
@endPorgress
def checkTopology(display=False,sleep=0.05):

sel_list = [mesh for mesh in pm.ls(pm.pickWalk(d="down"),type="mesh")]

if len(sel_list) != 2:
pm.headsUpMessage("Please select 2 Mesh")
return

num_list = [(sel.numVertices(),sel.numEdges(),sel.numFaces()) for sel in sel_list]
if num_list[0] != num_list[1]:
return False

edge_num = num_list[0][1]

pm.progressWindow(
title="Check Topology",
progress=0.0,
isInterruptable=True )

res_list = []
for i,sel in enumerate(sel_list):
edge_loop_list = []
edge_list = set(range(edge_num))
pm.progressWindow( e=1, status = 'second mesh Analysis' if i else 'first mesh Analysis')
while len(edge_list) > 1:
idx = next(iter(edge_list), None)
if pm.progressWindow( query=1, isCancelled=1 ) :
return
pm.progressWindow( e=1, progress=(1-len(edge_list)/edge_num)*100 )

if idx is None: break
if display:
edge_loop = pm.polySelect(sel,edgeLoop=idx,r=1)
pm.refresh()
time.sleep(sleep) if sleep > 0 else None
else:
edge_loop = pm.polySelect(sel,edgeLoop=idx,ns=1)
edge_loop_list.append(edge_loop)
edge_list -= set(edge_loop)
res_list.append(edge_loop_list)

return res_list[0] == res_list[1]

if __name__ == "__main__":
print (checkTopology(display=False))

  这个方案我加了进度条,所以在模型不复杂的情况下 GUI 会拖慢速度 (:з」∠)
  另外我加入了 display 标记,这个标记为 True 的时候可以在 Maya 看到获取循环边的动画效果。

topo

  这个方案不需要用 OpenMaya 而且遍历速度会更快,40+万面的比较需要大概 0.6 s

总结

  上面两个方案都有个缺点,如果模型的顶点序号是不一致的,都会显示拓扑不匹配。
  除了这个问题之外,这两个方案可以满足绝大多数的模型拓扑检查情况了,而且点序号不一致也检查出来是挺好的,可以提前避免后续的 Blendshape 出问题。
  最终代码也汇总到 github 上 地址