Maya第三方库minq学习

minq是一个在maya场景中快速物体对象查询的库

仓库地址:https://github.com/theodox/minq

minq简单介绍

这个项目是为了减少Maya中每次对物体查询时,用到命令比如ls()listRelatives()listHistory()的频次。意在直接了当的获取所需的物体对象。

举个例子,比如我们想找到场景中角色的IK手柄,你需要:

1
2
3
4
5
6
7
iks = cmds.ls(type = 'ikHandle') or []
for ik in iks:
parents = cmds.listRelatives(ik, p=True)
for p in parents:
if p.split("|")[-1] == 1:
return p
return None

对于一个简单的需求,这显然会产生很多的代码。
而且不同的命令返回的结果格式不一样,比如ls()返回一个列表,你需要在后面加上[]
如果你想要返回完整的物体路径,使用ls()还需要添加long参数,使用listRelatives()则要使用fullPath参数。

minq是一个简化查询过程的这么一个库,例:

1
q = Scene(['top'])

它将返回一个minq.Stream对象。不过它不是最终的查询结果,你需要调用.execute()函数来将它转换成列表:

1
2
print q.execute()
# [u'|top']

你也可以直接进行迭代:

1
2
3
4
5
6
q = Scene(['top', 'front'])
for item in q:
print item

# u'|top'
# u'|front'

通常也可以直接把它们变成一个元组或列表:

1
2
3
4
5
tuple(Cameras())
# Result: (u'frontShape', u'perspShape', u'sideShape', u'topShape')

list(Scene().only('polyCreator'))
# Result: [u'polyCube1', u'polyCube2', u'polyCube3', u'polyUnite1']

当然也可以使用一些关键字,如inanyall :

1
2
3
4
5
6
7
8
u'|top' in Cameras()
# True

u'|pCube1' in Cameras()
# False

any(Meshes().like('cube'))
# True

关联式查询

在minq中,minq.Stream对象可以使用一些表达式进行关联式查询,下面是一些例子:

1
2
3
Meshes() # 获取所有polyMesh节点
Meshes().get(Parents) # 获取polyMesh节点的父级
Meshes().get(Parents).like('wall') # 获取polyMesh节点的父级中,名字包含'wall'的对象

通常我们要实现上述的需求,可能要用到这样的代码:

1
2
3
4
5
6
7
8
results = []
meshes = cmds.ls(type='mesh') or []
if meshes:
parents = cmds.listRelatives(*meshes, p=True)
for item in parents:
if re.search('wall', item):
results.append(item)
return results

和普通的查询一样,关联式查询也可以使用execute()函数,或直接进行迭代:

1
2
3
4
5
6
cubes = Meshes().get(Parents).like('cube')
print cubes.execute()
# [u'pCube1']
cmds.polyCube()
print cubes.execute()
# [u'pCube1', u'pCube2']

再次声明,minq的查询结果不是一个列表,而是一个可迭代的对象。
通常我们看到查询链中的最后一个关键字决定了查询结果:

1
2
Selected().only(Transforms)
# 这里的Transforms决定了结果

刚刚的代码可以返回所有选择的Transforms节点。我们也可以使用append()方法将其它的对象加入进去:

1
Selected().only(Transforms).append(AllChildren)

对于更复杂的查询组合,你还可以使用集合操作。
比如当我们想用一个或多个已知名称的对象开始查询,using()函数会用你提供的名称来创建Stream对象。

1
using("character_root").get(AllChidren).only(IKHandles)

这将会返回在character_root下的所有ik手柄。using()函数可以放入多个名称。

过滤

绝大多数的查询都会通过过滤的方式来缩减实际需要的结果。在minq中主要有以下三种方式:

  • .like()
  • .only()
  • .where()

.like()

这个方法是一个正则表达式过滤器,默认返回包含传入字符的对象,不区分大小写。

1
2
3
4
5
print Cameras()
# Stream([u'frontShape', u'perspShape', u'sideShape', u'topShape'])

print Cameras().like('top')
# Stream([u'topShape'])

如果你需要更精确的匹配,可以加入关键字exact

1
2
print Cameras().like('top', exact=True)
# Stream([])

其次,like()也可以使用完整的表达式。

1
2
print Joints().like('Left(hand|foot)$')
# Result: Stream([u'Character1_LeftFoot', u'Character1_LeftHand'])

.only()

这个方法是过滤Stream对象的结果类型。
常见的用法是这样:

1
some_stream.only('camera','light')

这将会从某个Stream中过滤出摄像机和灯光的对象。
minq为我们设置好了许多类型相关的关键字,所以还有种写法是:

1
some_stream.only(Meshes, Cameras)

only()还可以使用参数来指定命名空间进行查询:

1
2
Transforms().only(namespace = 'test')
# Stream(['|test:pCube1', '|nested:test:pCube1'])

使用冒号:来精确的匹配命名空间:

1
2
3
4
ransforms().only(namespace=':test')
# Stream(['|test:pCube1']) # 'nested:test is' excluded
Transforms().only(namespace=':nested:test')
# Stream(['|nested:test:pCube1']) # 'test' is excluded

命名空间也可以与过滤类型进行组合:

1
DagNodes().only('skinClusters', namespace="export")

.where()

这个方法是可以添加一个函数对每个物体对象进行过滤。

比如下面的一个情况:

1
2
3
4
5
6
# get all the meshes whose translateX is larger than 50
result = []
for item in cmds.ls(type = mesh):
parent = cmds.listRelatives(item, p=True)[0]
if cmds.getAttr(parent + ".tx") > 50:
result.add(parent)

.where()接受一个函数作为参数,它会将左边所有Steam里的元素作为参数,过滤出结果返回为True的对象。

1
2
tx_over_fifty = lambda p: cmds.getAttr(p + ".tx") > 50
Meshes().get(Parents).where(tx_over_fifty)

它还可以简单的写作:

1
Meshes().where(item.tx > 50) # item是minq的特殊类,代表左侧Steam中每个元素

你也可以利用.where()的特性,找到所有被连接的节点:

1
2
Scene().where(cmds.listConnections)
# cmds.listConnections返回一个列表,包含传入物体所有连接的节点。如果没有则为[]

特殊类item还可以简单的对父级,子级,关联关系,历史进行查询。
比如item.has()方法:

1
2
3
4
5
# 找到所有含有子级的Transforms节点
has_kids = Transforms().where(item.has(Children))

# 找到所有没有连接的节点
no_connections = DagNodes().where_not(item.has(Connections))

与之相似的还有item.has_attr()方法,判断每个元素是否存在某个属性。

1
has_tx = Transforms().where(item.has_attr('tx'))

.having()

.having()方法同样是判断某个属性是否存在。
比如找到所有带有”exportable”属性的Transforms节点:

1
export_set =  Transforms().having('exportable')

相较于.has_attr().having()的效率会快一些。

扩展

使用minq得到一个列表后,可以使用.get()对结果进行转换。

1
2
3
4
5
6
7
8
9
10
11
# 将结果转换为其transform节点
camera_transforms = Cameras().get(Parents)
# Result: Stream([u'|front', u'|persp', u'|side', u'|top']) #

# 将结果转换为其历史节点
sphere_history = Meshes().like('sphere').get(History)
# Result: Stream([u'|pSphere1|pSphereShape1', u'polySphere1']) #

# 同上,转换为Future类型
skinned_meshes = Scene().only('skinCluster').get(Future)
# Result: Stream([u'|characterMesh|characterMeshShape']) #

还有一些.get()无法获取到物体的名称,比如:

1
2
mesh_translates = Meshes().get(Parents).get(Attribute, 't').get(Values)
# Result: Stream([(0,0,0), (10,0,0), (0,20.2123, 11)]) #

你可以使用foreach()Stream结果里每个结果执行一个自定义的函数操作:

1
2
print Cameras().short().foreach(lambda p: p.upper())
# Stream([u'FRONTSHAPE', u'PERSPSHAPE', u'SIDESHAPE', u'TOPSHAPE'])

foreach()也可以用作复杂过滤的转换:

1
2
3
4
5
6
def connected_shaders(object):
sgs = cmds.listConnections(object, type='shadingEngine') or []
return tuple(set( cmds.listConnections(*sgs, type = 'lambert') or []))

print Meshes().foreach(connected_shaders)
# Result: Stream([(u'lambert1',), (u'blinn1',), (u'lambert1', u'phong1')]) #

制表显示

minq支持对结果进行制表显示,需要用到.join()方法。
下面的例子制表显示所有模型和对应的材质。

1
2
3
4
5
6
7
8
9
mesh_stream = Meshes()
shader_stream = mesh_stream.foreach(connected_shaders)
table = mesh_stream.join(shaders = shader_stream)
for each_row in table:
print each_row

# dataRow(index=u'|pCube1|pCubeShape1', shaders=(u'lambert1',))
# dataRow(index=u'|pPyramid1|pPyramidShape1', shaders=(u'blinn1',))
# dataRow(index=u'|pSphere1|pSphereShape1', shaders=(u'lambert1', u'phong1'))

从上述输出的结果来看,返回的列表里的元素是具名元组。所以我们也可以将它转换成字典:

1
2
3
4
meshes, shaders = Meshes().split(2)
m = meshes.join(shaders= shaders.foreach(connected_shaders))
dict(m)
# {u'|pCube1|pCubeShape1': (u'lambert1',), u'|pPyramid1|pPyramidShape1': (u'blinn1',), u'|pSphere1|pSphereShape1': (u'lambert1', u'phong1')}

上面的例子没有进行过优化,mesh_streamshader_stream会重复的执行Meshes()查询。在对性能有要求的情况下,可以使用.split()函数将Stream结果复制分成两份。

1
2
3
4
5
meshes, shaders = Meshes().split(2)
m = meshes.join(shaders= shaders.foreach(connected_shaders))
dict(m)

# {u'|pCube1|pCubeShape1': (u'lambert1',), u'|pPyramid1|pPyramidShape1': (u'blinn1',), u'|pSphere1|pSphereShape1': (u'lambert1', u'phong1')}

打组

时常会将Stream的结果根据需要进行打组分类。
比如根据面数对模型分类:

1
2
3
4
5
6
7
8
9
10
11
12
def poly_category(obj):
count = cmds.polyEvaluate(obj, v=True)
if count > 512: return 'high'
if count > 256: return 'med'
return 'low'

for category, meshes in Meshes().group_by(poly_category):
print category, ":", meshes

# high : [u'|pHelix1|pHelixShape1']
# med : [u'|pSphere1|pSphereShape1', u'|pSphere2|pSphereShape2']
# low : [u'|pCube1|pCubeShape1']

对于Stream结果中包含dataRows元素的,你可以使用行列数进行分类,而不要使用分组函数。

集合的操作

查询可以像集合一样操作。
比如对两个结果相加:

1
2
3
4
5
6
7
8
9
cube_parents  = nodes().of_type('polyCube').parents
red_lights = lights().where(item.colorR > .5).where(item.colorG < .5)

combined = cube_parents + lights
for item combined:
print item

# |pCube1
# |ambientLight1|ambientLightShape1

同样,你需要使用.execute()将其转换成列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
q = Cameras()                                   # all cameras
o = Cameras().where(item.orthographic == True) # only ortho cameras

(q + o).execute() # union:
# Result: (u'|side|sideShape', u'|top|topShape', u'|front|frontShape', u'|persp|perspShape') #

(q - o).execute() # difference -- in q but not in o
# Result: (u'|persp|perspShape',) #

(o - q).execute() # difference is order dependent!
# Result: (,) #

(q & o).execute() #intersection -- in both sets
# Result: (u'|side|sideShape', u'|top|topShape', u'|front|frontShape') #

(q ^ o).execute() # XOR : Items not in both sets
# Result: (u'|persp|perspShape',) #
(完)