Follicle will be the easiest option I think, but you need non-overlapping UVs
Other than that, you could use a closestPointOnMesh, but you'll have to compose the matrix using cross products
Here's a an example of how you could implement the second option:
def cluster_on_vtx(vtx_ls=None, cleanup=True):
if not vtx_ls:
vtx_ls =
cmds.ls(sl=True, fl=True)
for vtx in vtx_ls:
# Make sure it's a vertex
if not cmds.filterExpand(vtx, selectionMask=31):
continue
vtx_i = vtx.split('[')[-1].split(']')[0]
mesh = cmds.polyListComponentConversion(vtx)[0]
trs = cmds.listRelatives(mesh, parent=True)[0]
pos = cmds.pointPosition(vtx)
pos = [i - j for i, j in zip(pos, cmds.xform(trs, q=True, t=True, ws=True))]
# Create temp closestPointOnMatrix
tmp_cpom = cmds.createNode('closestPointOnMesh')
cmds.setAttr(f'{tmp_cpom}.inPosition', *pos)
# Create temp vectorProducts
tmp_vp_x = cmds.createNode('vectorProduct')
cmds.setAttr(f'{tmp_vp_x}.operation', 2)
cmds.setAttr(f'{tmp_vp_x}.normalizeOutput', 1)
tmp_vp_y = cmds.duplicate(tmp_vp_x)[0]
# Create temp fourByFourMatrix
tmp_fbf = cmds.createNode('fourByFourMatrix')
# Connect Mesh
cmds.connectAttr(f'{mesh}.outMesh', f'{tmp_cpom}.inMesh')
# Connect normal output to fbf's Z vector
for index, axis in enumerate('XYZ'):
cmds.connectAttr(f'{tmp_cpom}.normal{axis}', f'{tmp_fbf}.in2{index}')
# Get cross product of world Y against normal and connect to fbf's X vector
cmds.setAttr(f'{tmp_vp_x}.input1Y', 1)
for index, axis in enumerate('XYZ'):
cmds.connectAttr(f'{tmp_cpom}.normal{axis}', f'{tmp_vp_x}.input2{axis}')
cmds.connectAttr(f'{tmp_vp_x}.output{axis}', f'{tmp_fbf}.in0{index}')
# Get cross product of X vector and normal and connect to fbf's Y vector
for index, axis in enumerate('XYZ'):
cmds.connectAttr(f'{tmp_cpom}.normal{axis}', f'{tmp_vp_y}.input1{axis}')
cmds.connectAttr(f'{tmp_vp_x}.output{axis}', f'{tmp_vp_y}.input2{axis}')
cmds.connectAttr(f'{tmp_vp_y}.output{axis}', f'{tmp_fbf}.in1{index}')
# Connect potision to fbf
for index, axis in enumerate('XYZ'):
cmds.connectAttr(f'{tmp_cpom}.position{axis}', f'{tmp_fbf}.in3{index}')
# Create multMatrix to compensate for trs's position
tmp_mm = cmds.createNode('multMatrix')
cmds.connectAttr(f'{tmp_fbf}.output', f'{tmp_mm}.matrixIn[0]')
cmds.connectAttr(f'{trs}.worldMatrix[0]', f'{tmp_mm}.matrixIn[1]')
# Create transform to drive the cluster
clus_trs_zero = cmds.createNode('transform', name=f'{mesh}_{vtx_i}_clus_zero')
clus_trs = cmds.createNode('transform', name=f'{mesh}_{vtx_i}_clus')
cmds.parent(clus_trs, clus_trs_zero)
cmds.xform(clus_trs_zero, m=cmds.getAttr(f'{tmp_mm}.matrixSum'), ws=True)
clus = cmds.deformer(trs, type='cluster')[0]
cmds.connectAttr(f'{clus_trs}.worldMatrix[0]', f'{clus}.matrix')
cmds.setAttr(f'{clus}.bindPreMatrix', cmds.getAttr(f'{clus_trs}.parentInverseMatrix[0]'), type='matrix')
if cleanup:
to_del = [v for k, v in locals().items() if k.startswith('tmp_')]
cmds.delete(reversed(to_del))