工具使用方法:
代码文件直接放入Maya的python脚本栏里全选后安小键盘回车即可使用。
首先需要载入地面物体和被放置物体

点击输入框前的按钮会自动把选择的物体输入到输入框中,也可以手动输入物体的transform节点名,中间用英文半角输入模式下的“,”隔开。可以同时输入多个地面对象和多个被放置物体,刷草时会只有鼠标在被输入的物体名所在的对象上点击时才有效,多个放置对象会随机抽取其中的一个进行放置。
注意,地面物体仅限于形状节点为mesh类型的物体,但被放置物体没有限制。
此时点击下方的“《run》”就已经能使用工具了。在地面对象上点击或拖拽都会放置被放置物体。
软选择、升降、旋转和缩放功能只有在前面的复选框被打开时才能调整。
在点击run后再操作这些功能不需要再次点run即可实时应用效果。
软选择:当工具打开时,窗口会记录当前软选择是否打开。当窗口内软选择开关打开,软选择范围内将会被随机地点放置设置好的数量个被放置物体。在窗口打开后点击窗口的软选择复选框会应用到Maya的软选择开关,在Maya里开关软选择同样会及时应用到窗口内。注意,如果鼠标正处于放置工具使用状态,按住B用鼠标左键缩放软选择范围时放置功能仍会触发。如果此时您的鼠标正处于地面模型上,它将继续放置物体。建议在需要调整软选择范围时按一下键盘Q键进入选择模式,在调整好范围后再点击工具的run按钮进行放置使用。
升降值:为满足被放置的物体数轴位置不统一而导致防治效果不能尽量满足更多人的需求,该升降值功能在打开后被放置的物体将会在放置后在对象y轴上上移或下移固定距离,单位和Maya默认单位相同。
旋转:在这旋转复选框处于打开状态时,被放置物体将在被放置到地面后沿物体y轴旋转。旋转是随机的,范围取决于mini和max值之间,输入框上限和下限分别为360和0。
缩放:当缩放复选框处于打开状态时,被放置的物体会整体进行随机缩放,缩放范围也受mini和max限制。缩放范围不受负数限制。
基于地面法线:在一些有坡度的地方放置物体时,如果想让被放置的物体垂直于被点击到的面,请打开这个复选框。默认状态时被放置物体将会将物体的y轴绝对指向面的法线方向。如果你的物体已经有旋转信息,并且您打算将这些旋转信息也应用到被放置的物体上,那么您可以点击后面的复选框,使它改为“此时为相对旋转”即可。


下面是代码学习区
如果您只需要使用这个工具可以跳过下面这一段,到尾端。
窗口的Qt方面并没有很多新颖的技术点,但我学会了使用弹簧和分割线这两个部件的使用。他们的qt文档分别是弹簧和分割线。他们在qt的designer里只显示为Spacer和line,这让我很难才找到不去转一次代码就能直接使用它们的方法。
这两个Item虽然都继承在QtWidgets类下,但在layout中使用时,需要用.addItem才能添加它们,而不是简单的.addWidget。
弹簧:直接用代码实现弹簧时需要先指定它的方向和长宽。如果你是在designer里使用,它默认会将两边的控件尽量顶紧,但在这里,你可以手动设置它最远伸长到多远,这很有用。
分割线:它同样需要设置横向或者竖向,而且它还有一些呈现效果可以调控。它的setFrameShadow方法可以设置它为黑色实线、凸线或者凹线。
self.spacer = QtWidgets.QSpacerItem(10, 15, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) # 弹簧
self.line_v = QtWidgets.QFrame() # 分割线
self.line_v.setFrameShape(QtWidgets.QFrame.VLine)
self.line_v.setFrameShadow(QtWidgets.QFrame.Raised)
这里还要分享一个Maya的操作事件链接到qt窗口的功能,在前期的制作中,我只能在窗口中使用复选框开关来控制Maya的软选择开关,而无法在Maya里开关来控制窗口的开关。因为这要在Maya里发射事件信号到窗口里接受。现在,我在群友大佬的帮助下解决了这个事情。
import maya.api.OpenMaya as om
import maya.OpenMayaUI as omui
from PySide2.QtWidgets import *
from shiboken2 import wrapInstance
import maya.cmds as cmds
def maya_main_window():
main_window_par = omui.MQtUtil.mainWindow()
return wrapInstance(int(main_window_par), QWidget)
class UI(QDialog):
def __init__(self, parent=maya_main_window()):
super(UI, self).__init__(parent)
self.setWindowTitle(u"软选择绑定Qt按钮")
self._event_id = om.MEventMessage.addEventCallback("softSelectOptionsChanged", self.soft_select_event)
self._setup_ui()
self.soft_select_event(None)
def _setup_ui(self):
self._check_box = QCheckBox()
self._check_box.stateChanged.connect(self.soft_select)
main_layout = QHBoxLayout(self)
main_layout.addWidget(QLabel(u"软选择开关"))
main_layout.addWidget(self._check_box)
def soft_select(self, bool_value):
if bool_value == 2:
cmds.softSelect(sse=True, e=True)
if bool_value == 0:
cmds.softSelect(sse=False, e=True)
def soft_select_event(self, event):
bool_value = cmds.softSelect(sse=True, q=True)
self._check_box.setChecked(bool_value)
def closeEvent(self, event):
om.MEventMessage.removeCallback(self._event_id)
window = UI()
window.show()
从代码仲可以看出,先添加一个事件回调,参数为事件名称和回调函数。回调函数为时间信号接受的函数(槽)。这里就作为获取和设置Maya软选择开关和窗口中的软选择开关。注意,搭建起这个回调函数后需要在不用的时候用removeCallback清除它,不然它会在Maya中每次触发事件都发射信号,不管窗口还存不存在,槽函数还存不存在。还有注意调用槽函数必须在创建widgets的函数后面,不然在回调函数本身里面找不到需要的开关,因为还没被定义。
这个工具的核心逻辑主要使用的是Maya.api 1.0的MFnMesh的closestIntersection方法。有关这个方法可以点此跳转。它可以使屏幕的2d坐标通过基于镜头来产生射线击中场景内物体来反馈被击中物体的一些信息。
经过很多地方参考(复制粘贴)别人的代码,花了很长时间我才学会使用这个函数。如您所见,它的传参非常复杂。所需的2d坐标需要配合Maya的draggerContext命令,他将返回鼠标在屏幕上点击或拖拽时的二维坐标点,并调用相对应的函数方法。
sToe = 'screenToEnvironment'
if mc.draggerContext('screenToEnvironment', ex = True, q = True):
mc.deleteUI('screenToEnvironment')
mc.draggerContext('screenToEnvironment', pc = self.selSpot_2d_click, dc = self.selSpot_2d_drag, n = sToe, cur = 'crossHair')
mc.setToolTo('screenToEnvironment')
返回的2d坐标将用于closestIntersection的raySource和rayDirection使用。
closestIntersection的返回值中hitPoint、hitRayParam、hitFacePtr、hitTrianglePtr的含义如下:
fn_mesh.closestIntersection(om.MFloatPoint(pos_point),
om.MFloatVector(dire_vector),
None,
None,
False,
om.MSpace.kWorld,
99999,
True,
None,
hitPoint, #击中点的世界坐标
hitRayParam, #击中点离屏幕的射线距离
hitFacePtr, #击中点的面的id
hitTrianglePtr, #击中点的面的三角形id
None,
None)
此时只需要获得击中点世界坐标,就能直接将复制出来的被放置物体直接移动到这个位置。
至此,整个工具的最难部分已经解决。
在实现物体朝向击中面的法线方向时,由于鄙人的才疏学浅,线代基础几乎为零。四处寻找相关方法,但即便找到了也无法理解它的逻辑,多方缝合才将该功能完善。即便现在也对其中究竟不能完整理解。
我一开始想使用MFnMesh.getFaceVertexNormals()得到与该面相接的点法线获取平均值进而得到面的法线。但但只有法线不能很好的得到它的旋转,因为Maya中使用的是欧拉角旋转,万象锁和旋转层级使它无法直接用法线投影得到的角度来设置。
后来看到一篇代码可以获取一个只有一个面的平面的相接三个点来获得面的旋转朝向。但它需要获得与面相接的点的id,回顾上面的代码,我只能获取这个面的id,而没有与它相接的点的id。而且我想了很久(主要还是我菜)也没有如果通过面id获取点id的方法。但在劲爆羊厂长的群里有大佬指出了一个比较笨但很实用的方法,通过Maya的ConvertSelectionToVertices功能,可以把选择的面转化成选择与该面相接的点。但是很遗憾我在Maya的文档里找不到这个命令,在群友的帮助下我找到了这个命令的使用方法。

选择到点后虽然里面包含点序,但还要从中获取点序范围。Maya在获取连续的点时会用[ ]来切片首尾连个点的名字。这里就需要正则表达式,但正则表达式非常复杂(还不是因为我菜),网上找了很久也没找到一篇好用的案例拿来参考(复制粘贴)。但找到了另一种更直接的方法,通过字符串索引得到“[”和“]”中间的字符串,再用“:”切开,用range进行依次遍历到一个列表里就是需要的点序了。注意如果面是个三角形或者其它,与它相接的点可能是不连续的,这样得到的点在中括号里就可能只有一个,在这里就需要用if判断一下。
mc.select(cl=True)
mc.select('{}.f[{}]'.format(trans_obj, face_num))
mc.select(mc.polyListComponentConversion(tv=True))
face_vtxLis = []
for inf in mc.ls(sl=True):
if ':' not in inf:
face_vtxLis.append(int(inf[inf.index('[') + 1]))
elif ':' in inf:
vtx_id_lis = self.extract(inf).split(':')
[face_vtxLis.append(vtx_id) for vtx_id in range(int(vtx_id_lis[0]), (int(vtx_id_lis[1]) + 1))]
if len(face_vtxLis) <= 2:
print '击中的面只有两个相接点,无法判定法线方向。'
return False
def extract(self, string, start='[', stop=']'):
return string[string.index(start) + 1: string.index(stop)]
现在终于拿到点序了,通过带入之前那篇代码,就可以通过这三个点的点法线组建一个矩阵,这个矩阵就是这个面的法线矩阵。注意,这里在生成y轴的法线时,如果直接用xz轴的法线通过外积来获取,在面数较少的如只有一个面的情况下适用,但经过反复测试,再面数高的时候它就不能很好的指向向正确的方向。
这里就用到之前所说的,用面上顶点来获取面的法线向量,通过这个法线来当做这个矩阵里y轴需要的向量。
#num是面的id,fn_mesh是物体的形状节点名。
def getY_vector(self, num, fn_mesh):
face_num = om.MScriptUtil().getInt(num)
normals = om.MFloatVectorArray()
fn_mesh.getFaceVertexNormals(face_num, normals, om.MSpace.kWorld)
face_pointLis = []
face_poinNum = normals.length()
for inf in range(face_poinNum):
face_pointLis.append([normals[inf].x, normals[inf].y, normals[inf].z])
face_normal_x = int()
face_normal_y = int()
face_normal_z = int()
for inf in range(len(face_pointLis)):
face_normal_x = face_normal_x + face_pointLis[inf][0]
face_normal_y = face_normal_y + face_pointLis[inf][1]
face_normal_z = face_normal_z + face_pointLis[inf][2]
face_normal = [face_normal_x / face_poinNum, face_normal_y / face_poinNum, face_normal_z / face_poinNum]
return Vector(face_normal)
最后,通过TransformationMatrix.getRotation方法,就能获取到该面的旋转弧度值,用弧度值乘以(180 / math.pi)获得旋转值就能直接应用到被放置物体上了。
非常感谢各位群友大佬和邱姐(神)的帮助,特别是邱姐,我的神,人生导师。牛逼!

这个工具前前后后写了一个月,花费了很多心血(主要是又菜又爱玩)。代码的可读性很差,因为我没有接受过专业代码书写培训,只想着能用就行。如果大佬们觉得有哪里可以优化,或者想添加删改某些功能,可以留言或者发邮件给我,看到有空就回。

文末是代码下载链接,视频教学在这里(留个坑位,等视频做出来了就加上,绝对不咕,咕咕咕咕~~)。工具的迭代更新会及时放入我的github里。工具的稳定性并没有保障,请在使用前保存您的场景,以防万一,如果造成损失,与我无关。
在此非常感谢您使用我的工具,您的支持是我继续学习创作的最好动力。